From c7c1b7717e99395889a17817923b3def34799bf7 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira Date: Sun, 26 Jul 2026 12:44:34 +0100 Subject: [PATCH 001/125] docs(discovery): publish provisional feature baseline --- docs/discovery/BASELINE-STATUS.md | 62 + docs/discovery/README.md | 21 + docs/discovery/audit-method.md | 80 + docs/discovery/domains/agent.md | 153 + docs/discovery/domains/calendar.md | 45 + docs/discovery/domains/chat.md | 197 + docs/discovery/domains/contact.md | 24 + docs/discovery/domains/cookbook.md | 85 + docs/discovery/domains/document.md | 108 + docs/discovery/domains/email.md | 87 + docs/discovery/domains/frontend.md | 149 + docs/discovery/domains/media.md | 170 + docs/discovery/domains/memory.md | 24 + docs/discovery/domains/model.md | 177 + docs/discovery/domains/note.md | 24 + docs/discovery/domains/platform.md | 213 + docs/discovery/domains/research.md | 86 + docs/discovery/domains/security.md | 154 + docs/discovery/domains/skill.md | 24 + docs/discovery/feature-catalog.json | 4797 +++++++++++++++++ docs/discovery/feature-catalog.md | 85 + .../discovery/references/source-provenance.md | 28 + .../tools/test_validate_discovery_evidence.py | 224 + .../tools/validate_discovery_consistency.py | 592 ++ .../tools/validate_discovery_docs.py | 172 + .../tools/validate_discovery_evidence.py | 1413 +++++ 26 files changed, 9194 insertions(+) create mode 100644 docs/discovery/BASELINE-STATUS.md create mode 100644 docs/discovery/README.md create mode 100644 docs/discovery/audit-method.md create mode 100644 docs/discovery/domains/agent.md create mode 100644 docs/discovery/domains/calendar.md create mode 100644 docs/discovery/domains/chat.md create mode 100644 docs/discovery/domains/contact.md create mode 100644 docs/discovery/domains/cookbook.md create mode 100644 docs/discovery/domains/document.md create mode 100644 docs/discovery/domains/email.md create mode 100644 docs/discovery/domains/frontend.md create mode 100644 docs/discovery/domains/media.md create mode 100644 docs/discovery/domains/memory.md create mode 100644 docs/discovery/domains/model.md create mode 100644 docs/discovery/domains/note.md create mode 100644 docs/discovery/domains/platform.md create mode 100644 docs/discovery/domains/research.md create mode 100644 docs/discovery/domains/security.md create mode 100644 docs/discovery/domains/skill.md create mode 100644 docs/discovery/feature-catalog.json create mode 100644 docs/discovery/feature-catalog.md create mode 100644 docs/discovery/references/source-provenance.md create mode 100644 docs/discovery/tools/test_validate_discovery_evidence.py create mode 100755 docs/discovery/tools/validate_discovery_consistency.py create mode 100755 docs/discovery/tools/validate_discovery_docs.py create mode 100644 docs/discovery/tools/validate_discovery_evidence.py diff --git a/docs/discovery/BASELINE-STATUS.md b/docs/discovery/BASELINE-STATUS.md new file mode 100644 index 000000000..7cb6ec391 --- /dev/null +++ b/docs/discovery/BASELINE-STATUS.md @@ -0,0 +1,62 @@ +# Discovery Baseline Status + +## Purpose and scope + +This package is a commit-pinned discovery baseline and feature index for the `discovery` branch at frozen commit `d8a2059df8e53bc7275c45339849d14c8651e73c`. It inventories **79 feature records** across **16 domains** to help maintainers locate likely implementation areas and identify validation gaps. It is not an authoritative architecture reference or a runtime-certification record. + +The canonical inventory is [`feature-catalog.json`](feature-catalog.json); [`feature-catalog.md`](feature-catalog.md) and the files in [`domains/`](domains/) are derived reading views. See [`audit-method.md`](audit-method.md) for the status and maturity definitions. + +## What maintainers may use now + +- Use the catalog and domain views as a frozen discovery index, including their feature IDs, stated scope, likely source locations, and declared runtime prerequisites. +- Treat a catalog status such as `verified` as meaning implementation was identified during discovery. It does **not** mean every evidence locator, line range, test claim, or runtime behaviour has passed semantic validation. +- Use the structural checks to confirm package shape and cross-view consistency; use the evidence validator to assess whether individual evidence assertions are semantically supported. + +## Validation snapshot + +Structural validation passes: the catalog has 79 records, the 16 domain views match it, and the package structural validators pass. All 11 focused evidence-validator tests pass. + +The evidence validator found **170 evidence entries**: **82 valid**, **66 invalid**, **6 ambiguous**, and **16 unsupported**. Its non-zero exit is expected while those semantic evidence defects remain. + +Structural validity checks the documentation schema, record counts, derived-view consistency, file existence, line-range bounds, links, and public-safety rules. Semantic evidence validity additionally checks whether the cited locator exists, falls within its cited range, uses a supported parser, and actually supports the feature claim. Passing the former does not establish the latter. + +## E2 review decisions + +E2 means directly relevant automated test evidence supports the feature claim; a test file’s existence alone is insufficient. The generated evidence report was used to reassess all ten E2 records. + +| Feature | Decision | Reason | +|---|---|---| +| `CHAT-001` | Retain E2 | Two cited tests directly exercise documented streaming-related behaviour. | +| `MODEL-006` | Retain E2 | The cited device-flow test exercises the Copilot start/poll contract and verification URI. | +| `MODEL-007` | Retain E2 | The cited device-flow test exercises the ChatGPT subscription contract and verification URI. | +| `RESEARCH-003` | Demote to E1 | The cited test covers ranking, not provider connectivity or dispatch; the route-to-provider implementation path was identified. | +| `DOCUMENT-002` | Demote to E1 | The cited marker test does not support form processing or rendering; the document route does call the PDF form handlers. | +| `EMAIL-001` | Demote to E1 | The cited health test is narrower than setup, SMTP, and polling; application setup invokes the email router and its poller. | +| `SECURITY-002` | Demote to E1 | Relevant vault-password tests exist, but the cited test locator is fabricated; the application mounts the vault route implementation. | +| `SECURITY-004` | Demote to E1 | Relevant injection tests exist, but the cited locator names are fabricated; callers use the documented context guard. | +| `SECURITY-005` | Demote to E1 | Relevant guard tests exist, but the cited locator names are fabricated; route code calls the documented URL guard. | +| `PLATFORM-009` | Demote to E0 | The manifests and diagnostic script establish discovered operational artifacts, not a traced application path or directly relevant automated test. | + +The current maturity distribution is **E0: 68**, **E1: 8**, **E2: 3**, **E3: 0**, **E4: 0**. Runtime validation is still pending where the catalog says it requires external services, interactive authentication, specialised hardware, or host Docker GPU support. + +## Known discovery caveats + +- `AGENT-004` includes a legacy no-op activity-log shim rather than active assistant-log behaviour. +- `FRONTEND-007` points to a missing `static/backgrounds.html` target; its existing variant pages do not make that route functional. +- `RESEARCH-003` retains a compatibility module that aliases the canonical search implementation, and `DOCUMENT-002` separates optional PDF viewing from form handling. +- Secret-storage and vault-command handling are distinct implementation areas; this index does not make an end-to-end security guarantee. + +## Recommended next documentation work + +Repair semantic evidence selectively while architecture and operations documentation is written, beginning with the seven E2 demotions and maintainer-owned feature descriptions. Do not wait for the complete evidence queue before documenting the system. Record controlled runtime observations when external services, credentials, hardware, or Docker GPU access are available, and label unsupported claims explicitly. + +## Validation commands + +```bash +PYTHONDONTWRITEBYTECODE=1 python3 docs/discovery/tools/validate_discovery_docs.py --repo-root . +PYTHONDONTWRITEBYTECODE=1 python3 docs/discovery/tools/validate_discovery_consistency.py +PYTHONDONTWRITEBYTECODE=1 python3 -m unittest docs/discovery/tools/test_validate_discovery_evidence.py -v +PYTHONDONTWRITEBYTECODE=1 python3 docs/discovery/tools/validate_discovery_evidence.py --repo-root . --catalog docs/discovery/feature-catalog.json --output-dir +``` + +Supply a local report directory outside `docs/discovery/` for the final command so generated reports are not added to the package. diff --git a/docs/discovery/README.md b/docs/discovery/README.md new file mode 100644 index 000000000..1a397da74 --- /dev/null +++ b/docs/discovery/README.md @@ -0,0 +1,21 @@ +# Odysseus Discovery Package + +## Provisional discovery baseline + +This is a commit-pinned discovery baseline and feature index for the `discovery` branch at `d8a2059df8e53bc7275c45339849d14c8651e73c`. It contains 79 feature records across 16 domains. It is **not** an authoritative architecture reference, a runtime certification, or a claim that every evidence citation is semantically valid. + +Read [`BASELINE-STATUS.md`](BASELINE-STATUS.md) first for the publication status, evidence-validation totals, E2 decisions, known caveats, and the recommended next documentation work. + +## Package contents + +- [`feature-catalog.json`](feature-catalog.json) is the canonical machine-readable catalog. +- [`feature-catalog.md`](feature-catalog.md) and [`domains/`](domains/) are derived reading views. +- [`audit-method.md`](audit-method.md) defines feature status and evidence maturity. +- [`references/source-provenance.md`](references/source-provenance.md) records the frozen snapshot. +- [`tools/`](tools/) contains the structural, consistency, and evidence validators. + +A feature status such as `verified` means implementation was identified during discovery. It does not mean every evidence locator, line range, test claim, or runtime behaviour has passed semantic validation. + +## Validation + +Run the commands in [`BASELINE-STATUS.md`](BASELINE-STATUS.md#validation-commands). Structural checks and semantic evidence checks have different purposes; see that status document for the current results and interpretation. diff --git a/docs/discovery/audit-method.md b/docs/discovery/audit-method.md new file mode 100644 index 000000000..b2c3bd1f1 --- /dev/null +++ b/docs/discovery/audit-method.md @@ -0,0 +1,80 @@ +# Odysseus Functional Audit Methodology + +## Purpose + +This document specifies the methodology and evidence standards for the read-only discovery audit of **Odysseus**. + +## Snapshot Baseline + +- **Repository**: `odysseus-dev/odysseus` +- **Audit Target Branch**: `discovery` +- **Frozen Commit SHA**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Snapshot Date**: `2026-07-23T14:49:02Z` +- **Audit Mode**: Read-Only inventory & documentation review + +## Rules of Engagement + +1. **No Code Mutations**: Application code and tests outside `docs/discovery/` remain untouched. +2. **No External Operations**: No GitHub issues, PRs, comments, labels, or branch mutations. +3. **Zero Inferred Success**: Documentation claims require empirical evidence of implementation and reachability. Filenames, README descriptions, and docstrings alone do not constitute proof. +4. **Strict Status Categorization**: All capabilities are assigned exactly one authorized status: + - `verified`: Implemented, reachable, and supported by code evidence. + - `partial`: Partially implemented or missing full frontend/backend connection. + - `disabled`: Gated off by default feature flags or configuration. + - `experimental`: Active but requiring non-standard hardware or runtimes. + - `legacy`: Obsolete feature retained for backwards compatibility. + - `dead-code-candidate`: Code exists but is unreachable from UI or API routes. + - `unverified`: Implementation present but untestable without external secrets or hardware. + +## Evidence Maturity Scale + +Evidence maturity is evaluated independently from catalog feature status. A feature status such as `verified` records that implementation was identified during discovery; it is not a statement that every evidence locator, test claim, line range, or runtime behaviour has passed semantic validation. + +- **E0 - Discovered**: Candidate identified in documentation, route declaration, or source file. +- **E1 - Code-path traced**: Frontend/API entry point connected through services and data handlers. +- **E2 - Test-backed**: At least one directly relevant automated test assertion supports the feature claim. A test file's existence, an unrelated assertion, or an invalid test locator does not establish E2. +- **E3 - Runtime-validated**: Maintainer reproduced behavior in a recorded local environment. +- **E4 - Maintainer-accepted**: Maintainers accepted the feature description and support status. + +## Audit Workflow + +```mermaid +flowchart TD + P0["Phase 0: Snapshot Isolation
(Commit d8a2059)"] --> P1["Phase 1: Codebase Discovery
(Routes, Services, Static JS, Specs)"] + P1 --> P2["Phase 2: Feature Reachability & Verification
(Route matching, FE entrypoints, tests)"] + P2 --> P3["Phase 3: Catalog & Evidence Compilation
(File paths, symbols, exact line ranges)"] + P3 --> P4["Phase 4: Quality & Integrity Audit
(100% path existence check, schema validation)"] +``` + +### Phase 0: Snapshot Isolation +The audit is pinned to git commit `d8a2059df8e53bc7275c45339849d14c8651e73c`. All file paths, symbol declarations, and line ranges map strictly to this commit. + +### Phase 1: Codebase Discovery +All top-level and nested directories were traversed, including: +- Backend Entry Points (`app.py`, `routes/`, `routes/*/*.py`, `companion/`) +- Core Framework (`core/database.py`, `core/session_manager.py`, `core/auth.py`) +- Business Logic Services (`src/`, `services/`, `mcp_servers/`) +- Frontend Assets (`static/app.js`, `static/js/`, `static/index.html`) +- Test Suites (`tests/`, `tests/cli/`, `tests/streaming/`) +- Operations & Docker (`Dockerfile`, `docker-compose*.yml`, `scripts/`) + +### Phase 2: Verification Protocol +For each feature candidate, the following table was evaluated: +- **User Reachability**: Frontend UI element, modal, route, or CLI script. +- **API Entrypoint**: FastAPI `@router` declaration or WebSocket/SSE handler. +- **Backend Execution**: Concrete Python module method, service, or tool call. +- **Data Persistence**: Disk file, SQLite table, or vector collection. +- **Test Coverage**: Automated test file executing assertions against the component. + +### Phase 3: Evidence Linking Standard +Every feature entry in `feature-catalog.json` contains a structured `evidence` list with: +- `path`: Relative path from repository root. +- `symbol`: Route, class, function, or element symbol name. +- `line_range`: Inclusive line range (e.g. `L120-L250`). +- `explanation`: Short factual statement proving reachability or implementation. + +### Phase 4: Quality Check & Schema Constraints +Before finalization: +1. Every evidence file path is validated against the checkout. +2. Every Markdown entry matches `feature-catalog.json`. +3. Recommendation language is separated from empirical factual observations. diff --git a/docs/discovery/domains/agent.md b/docs/discovery/domains/agent.md new file mode 100644 index 000000000..82574ddc3 --- /dev/null +++ b/docs/discovery/domains/agent.md @@ -0,0 +1,153 @@ +# Agent + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## AGENT-001 — Autonomous Agent Loop & Tool Execution Engine + +- **Domain**: `agent` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Executes multi-step agent reasoning loops, tool invocation parsing, and automated response generation. + +### Evidence summary + +- `src/agent_loop.py` — `run_agent_loop` — Core loop evaluating model tool requests and executing handlers. +- `src/tool_execution.py` — `execute_tool_call` — Dispatches tool invocation requests to underlying tool handlers. + +### Unknowns + +- Infinite tool loop if termination condition fails. + +## AGENT-002 — Scheduled Tasks & Event Bus Dispatcher + +- **Domain**: `agent` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Schedules background recurring or delayed tasks, emits event bus triggers, and executes automated flows. + +### Evidence summary + +- `routes/task_routes.py` — `@router.get('')` — Fetches active scheduled tasks. +- `src/task_scheduler.py` — `TaskScheduler` — Async task scheduler dispatching cron and delay triggers. + +### Unknowns + +- Task execution failure handling on system restart. + +## AGENT-003 — Webhook Event Subscriptions & Trigger Processing + +- **Domain**: `agent` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Manages incoming/outgoing webhook subscriptions, endpoint authentication tokens, and event triggers. + +### Evidence summary + +- `routes/webhook_routes.py` — `@router.get('/webhooks')` — Returns list of registered webhooks. +- `src/webhook_manager.py` — `WebhookManager` — Handles payload delivery and signature verification. + +### Unknowns + +- SSRF risks when contacting external webhook URLs if unvalidated. + +## AGENT-004 — Assistant Settings, Task Check-Ins & Background Job Monitor + +- **Domain**: `agent` +- **Status**: `partial` +- **Evidence Maturity**: `E1` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Manages per-user assistant sessions and scheduled check-in settings, drains background job completions, and retains a legacy no-op activity logging shim. + +### Evidence summary + +- `routes/assistant_routes.py` — `setup_assistant_routes` — Active assistant session, settings, manual check-in, run-status and timezone-list endpoints, including the owner-scoping guards. +- `src/bg_monitor.py` — `_drain_agent` — Runs the agent loop headless against a session to produce the background-job follow-up turn. +- `src/bg_monitor.py` — `_run_followup` — Drains completed background jobs and auto-continues the owning session, deferring while a live turn is in progress. +- `src/assistant_log.py` — `log_to_assistant` — Legacy no-op activity logging shim retained for existing callers; documented as inactive rather than as current behaviour. + +### Unknowns + +- Route `/api/assistant/logs` cited in legacy docs is absent from assistant router. +- Existing unit test `tests/cli/test_logs_cli_resolve_nonstring.py` tests CLI target-name resolution logic, not active assistant routes or bg_monitor execution loop. + +## AGENT-005 — Model Context Protocol (MCP) Server Integration + +- **Domain**: `agent` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Integrates external MCP servers over stdio/SSE to expand agent capabilities dynamically. + +### Evidence summary + +- `routes/mcp_routes.py` — `setup_mcp_routes` — Exposes management endpoints for external MCP servers. +- `src/mcp_manager.py` — `McpManager` — Manages MCP server subprocess lifecycles. + +### Unknowns + +- Subprocess leaks if external MCP server process fails to terminate clean. + +## AGENT-006 — AI Interaction Tools & Pipeline Orchestration + +- **Domain**: `agent` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Provides specialized AI interaction tools for agent self-debugging, debate, and multi-model collaboration. + +### Evidence summary + +- `src/ai_interaction.py` — `init_ai_interaction_tools` — Registers specialized multi-agent interaction primitives. +- `src/builtin_actions.py` — `execute_builtin_action` — Executes pre-built action intent sequences. + +### Unknowns + +- High API token consumption during extended agent debates. + +## AGENT-007 — Subprocess & Background Job Execution Tools + +- **Domain**: `agent` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Provides sandboxed bash/shell tool execution capabilities with output streaming and background tracking. + +### Evidence summary + +- `src/agent_tools/subprocess_tools.py` — `run_command` — Executes shell commands in background/foreground. +- `src/bg_jobs.py` — `JobManager` — Tracks async background subprocess tasks. + +### Unknowns + +- Arbitrary shell command execution permissions if sandbox confinement fails. diff --git a/docs/discovery/domains/calendar.md b/docs/discovery/domains/calendar.md new file mode 100644 index 000000000..4a2dad8d3 --- /dev/null +++ b/docs/discovery/domains/calendar.md @@ -0,0 +1,45 @@ +# Calendar + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## CALENDAR-001 — CalDAV Calendar Synchronization & Account Setup + +- **Domain**: `calendar` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: pending — Requires a controlled external CalDAV server. + +### Purpose + +Connects to remote CalDAV servers (Apple iCloud, Nextcloud, Google) to sync calendar event feeds. + +### Evidence summary + +- `routes/calendar_routes.py` — `setup_calendar_routes` — Exposes CalDAV setup and manual sync trigger routes. +- `src/caldav_sync.py` — `CalDavSync` — Fetches and parses remote iCalendar VEVENT objects. + +### Unknowns + +- Invalid SSL certificates on self-hosted CalDAV servers. + +## CALENDAR-002 — Calendar Event Operations & iCalendar Parsing + +- **Domain**: `calendar` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Creates, updates, deletes, and displays calendar events with timezone conversion and reminder notifications. + +### Evidence summary + +- `routes/calendar_routes.py` — `@router.get('/events')` — Fetches calendar events for requested date window. +- `src/tools/calendar.py` — `CalendarTool` — Agent tool for creating and modifying calendar entries. + +### Unknowns + +- Recurring RRULE event expansion calculation bugs across leap years. diff --git a/docs/discovery/domains/chat.md b/docs/discovery/domains/chat.md new file mode 100644 index 000000000..8ff904c6b --- /dev/null +++ b/docs/discovery/domains/chat.md @@ -0,0 +1,197 @@ +# Chat + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## CHAT-001 — Core Chat Streaming & SSE Message Generation + +- **Domain**: `chat` +- **Status**: `verified` +- **Evidence Maturity**: `E2` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: pending — Requires access to a live LLM provider endpoint (OpenAI API key or local Ollama server). + +### Purpose + +Handles real-time Server-Sent Events (SSE) chat streaming, token rendering, and model response generation. + +### Evidence summary + +- `routes/chat_routes.py` — `chat_stream` — POST /api/chat_stream SSE endpoint; builds the shared chat context, then dispatches to the chat-mode or agent-mode streaming path. +- `routes/chat_helpers.py` — `build_chat_context` — Shared context builder invoked by chat_stream; runs message preprocessing and assembles the memory/RAG/web context preface. +- `src/chat_handler.py` — `ChatHandler.preprocess_message` — Message preprocessing (attachments, URLs, tool preprocessing) reached from build_chat_context via routes/chat_helpers.py:preprocess. +- `src/chat_processor.py` — `ChatProcessor.build_context_preface` — Builds the retrieval and web-source context preface injected into the streamed request. +- `src/llm_core.py` — `stream_llm_with_fallback` — Chat-mode streaming dispatcher called from chat_stream; wraps stream_llm with an ordered provider fallback chain. +- `src/llm_core.py` — `stream_llm` — Per-request streaming entry wrapped by stream_llm_with_fallback; acquires the local model slot and delegates to _stream_llm_inner. +- `src/agent_loop.py` — `stream_agent_loop` — Agent-mode streaming path called from chat_stream when the request selects agent mode. +- `tests/test_chat_metrics.py` — `test_stream_llm_passes_through_llamacpp_timings` — Inspected unit test asserting stream_llm forwards backend generation timings into the emitted metrics chunk. +- `tests/test_resend_message_nondestructive.py` — `test_resend_message_does_not_truncate_by_default` — Inspected unit test asserting the frontend resend path does not truncate prior conversation turns. + +### Unknowns + +- Stream interruption on connection drops requires retry logic. + +## CHAT-002 — Session Management & Conversation State + +- **Domain**: `chat` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Manages session creation, listing, switching, renaming, and persistence of conversation metadata. + +### Evidence summary + +- `routes/session_routes.py` — `@router.get('/api/sessions')` — Lists active sessions filtered by user owner scope. +- `core/session_manager.py` — `SessionManager` — Provides thread-safe session storage operations. + +### Unknowns + +- Concurrent file writes to sessions.json under high load. + +## CHAT-003 — Chat History & Message Editing/Truncation + +- **Domain**: `chat` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Provides history retrieval, message content updating, message deletion, and history branch truncation. + +### Evidence summary + +- `routes/history/history_routes.py` — `@router.get('/api/history/{session_id}')` — Fetches message history timeline for a session. +- `routes/history_routes.py` — `_sys.modules[__name__] = _canonical` — Backward-compatibility shim module. + +### Unknowns + +- Truncating messages re-indexes context window and clears cached tool calls. + +## CHAT-004 — File & Multimodal Attachment Handling + +- **Domain**: `chat` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Handles uploading, mime validation, image preview, vision encoding, and file attachments in chat messages. + +### Evidence summary + +- `routes/upload_routes.py` — `@router.post('')` — Accepts multi-part file uploads and generates vision metadata. +- `src/upload_handler.py` — `UploadHandler.save_file` — Validates upload size and atomicity on disk. + +### Unknowns + +- Large file uploads may consume server disk space if cleanup task fails. + +## CHAT-005 — Chat Message Search + +- **Domain**: `chat` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Enables full-text keyword search across stored chat messages and sessions. + +### Evidence summary + +- `routes/search_routes.py` — `setup_search_routes` — Registers chat message search endpoint. +- `src/session_search.py` — `search_sessions` — Executes query matching against session transcripts. + +### Unknowns + +- Full table scans on un-indexed text columns for very large databases. + +## CHAT-006 — System Prompts & Preset Management + +- **Domain**: `chat` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Provides creation, selection, and customization of system prompt presets for chat sessions. + +### Evidence summary + +- `routes/preset_routes.py` — `setup_preset_routes` — API routes for listing and modifying system prompt presets. +- `src/preset_manager.py` — `PresetManager` — Disk-backed manager for prompt presets. + +### Unknowns + +- Invalid JSON syntax in user presets file can corrupt preset loading. + +## CHAT-007 — Emoji Rendering & Twemoji SVG Proxy + +- **Domain**: `chat` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Proxies Twemoji SVG icons locally to render flat SVG emojis in message text without external CDN dependencies. + +### Evidence summary + +- `routes/emoji_routes.py` — `setup_emoji_routes` — Serves locally cached Twemoji SVGs. + +### Unknowns + +- First request fetches SVG from remote CDN before caching locally. + +## CHAT-008 — Input History Recall (Arrow Up) + +- **Domain**: `chat` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Allows users to cycle through previously sent prompt messages in the chat composer input using Arrow-Up/Down keys. + +### Evidence summary + +- `static/js/composerArrowUpRecall.js` — `initComposerRecall` — Listens for ArrowUp keypress on composer textarea. + +### Unknowns + +- Client-side browser storage limits. + +## CHAT-009 — Context Window Compaction & Truncation + +- **Domain**: `chat` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Compacts session transcript history when prompt size exceeds context limits using summarization. + +### Evidence summary + +- `routes/history/history_routes.py` — `@router.post('/api/session/{session_id}/compact')` — Triggers context summarization and compaction. +- `src/context_compactor.py` — `compact_context` — Executes context token pruning and summary generation. + +### Unknowns + +- Aggressive compaction may discard subtle user instructions. diff --git a/docs/discovery/domains/contact.md b/docs/discovery/domains/contact.md new file mode 100644 index 000000000..47122b20a --- /dev/null +++ b/docs/discovery/domains/contact.md @@ -0,0 +1,24 @@ +# Contact + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## CONTACT-001 — CardDAV Contact Management & Address Book Integration + +- **Domain**: `contact` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Connects to CardDAV servers, imports VCard contacts, and provides contact lookup for email/calendar autocomplete. + +### Evidence summary + +- `routes/contacts/contacts_routes.py` — `@router.get('/list')` — Returns contact list filtered by search query. +- `src/tools/contacts.py` — `ContactsTool` — Agent tool for querying user address book contacts. + +### Unknowns + +- VCard 3.0 vs 4.0 property parsing mismatches. diff --git a/docs/discovery/domains/cookbook.md b/docs/discovery/domains/cookbook.md new file mode 100644 index 000000000..6d22bccef --- /dev/null +++ b/docs/discovery/domains/cookbook.md @@ -0,0 +1,85 @@ +# Cookbook + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## COOKBOOK-001 — Local Model Download & Recipe Lifecycle Management + +- **Domain**: `cookbook` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Downloads HuggingFace models, configures execution parameters, and manages local GGUF/MLX model servers. + +### Evidence summary + +- `routes/cookbook_routes.py` — `setup_cookbook_routes` — Exposes model downloading and process serving endpoints. +- `static/js/cookbook.js` — `initCookbook` — UI manager for local model library. + +### Unknowns + +- Disk space exhaustion during multi-gigabyte GGUF weights downloads. + +## COOKBOOK-002 — Hardware Model Fitting ('What Fits?') Analysis Engine + +- **Domain**: `cookbook` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Calculates RAM/VRAM requirements, quantized size, and context overhead to determine model compatibility. + +### Evidence summary + +- `routes/hwfit_routes.py` — `setup_hwfit_routes` — Calculates hardware model compatibility. +- `services/hwfit/fit.py` — `calculate_fit` — Performs parameter and memory fit calculations. + +### Unknowns + +- Inaccurate VRAM estimation for non-standard KV-cache quantization. + +## COOKBOOK-003 — HuggingFace & MLX Model Discovery Services + +- **Domain**: `cookbook` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Searches HuggingFace Hub and local MLX model repositories for compatible GGUF and MLX weights. + +### Evidence summary + +- `services/hwfit/hf_discovery.py` — `search_hf_models` — Queries HuggingFace API for model tags and files. + +### Unknowns + +- HuggingFace API rate limits when searching without an API token. + +## COOKBOOK-004 — Host Docker Access for Inference Container Runtimes + +- **Domain**: `cookbook` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: pending — Requires Docker access and supported physical GPU hardware. + +### Purpose + +Detects and connects to host Docker engine to launch containerized Ollama, vLLM, or SGLang runtimes. + +### Evidence summary + +- `src/host_docker_access.py` — `HostDockerAccess` — Interacts with host docker daemon. + +### Unknowns + +- Permission denied accessing docker socket on non-root setups. diff --git a/docs/discovery/domains/document.md b/docs/discovery/domains/document.md new file mode 100644 index 000000000..ca8ece487 --- /dev/null +++ b/docs/discovery/domains/document.md @@ -0,0 +1,108 @@ +# Document + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## DOCUMENT-001 — Document & Canvas Artifact Management + +- **Domain**: `document` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Renders dynamic canvas documents, handles live editing, markdown preview, and side-by-side artifact display. + +### Evidence summary + +- `routes/document_routes.py` — `setup_document_routes` — Registers document artifact CRUD routes. +- `static/js/document.js` — `initDocumentView` — Renders interactive canvas document panel. + +### Unknowns + +- Concurrent edits on the same document artifact. + +## DOCUMENT-002 — PDF Form Processing & High-Fidelity Rendering + +- **Domain**: `document` +- **Status**: `verified` +- **Evidence Maturity**: `E1` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: pending — Requires optional PyMuPDF (`fitz`) or pypdf runtime dependency. + +### Purpose + +Extracts form fields from PDF files, fills dynamic values, and generates PDF previews. + +### Evidence summary + +- `src/pdf_runtime.py` — `load_pymupdf_for_pdf_viewer` — Loads optional PyMuPDF runtime for PDF viewing. +- `src/pdf_forms.py` — `extract_form_fields` — Handles PDF form field extraction and filling. +- `tests/test_document_pdf_marker.py` — `test_marker_removed_without_eating_following_text` — Tests PDF text extraction wrapper stripping without content corruption. + +### Unknowns + +- Complex XFA PDF forms may not extract cleanly with standard pdf parsers. + +## DOCUMENT-003 — Personal Document Indexing & RAG Retrieval + +- **Domain**: `document` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Indexes local user documents (PDF, DOCX, TXT) into ChromaDB for semantic vector retrieval. + +### Evidence summary + +- `routes/personal_routes.py` — `setup_personal_routes` — Personal document RAG indexing and search API endpoints. +- `src/personal_docs.py` — `PersonalDocsManager` — Handles file text chunking and vector storage. + +### Unknowns + +- Slow vector embedding indexing step for massive multi-thousand page documents. + +## DOCUMENT-004 — Document Conversion & Text Extraction Engine + +- **Domain**: `document` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Converts office formats (.docx, .xlsx, .pptx) and HTML into clean Markdown text representations. + +### Evidence summary + +- `src/markitdown_runtime.py` — `convert_to_markdown` — Converts binary office documents into structured Markdown text. + +### Unknowns + +- Formatting loss when parsing legacy binary doc/xls files. + +## DOCUMENT-005 — Document Library UI Navigation + +- **Domain**: `document` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Provides dedicated UI view for browsing, filtering, and organizing saved user documents. + +### Evidence summary + +- `static/js/documentLibrary.js` — `initDocumentLibrary` — Renders document library navigation grid. +- `app.py` — `serve_library` — Serves SPA shell for /library route. + +### Unknowns + +- Large folder trees may cause initial DOM render slowdown. diff --git a/docs/discovery/domains/email.md b/docs/discovery/domains/email.md new file mode 100644 index 000000000..12b28ca43 --- /dev/null +++ b/docs/discovery/domains/email.md @@ -0,0 +1,87 @@ +# Email + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## EMAIL-001 — Email Account Setup, IMAP/SMTP Connection & Polling + +- **Domain**: `email` +- **Status**: `verified` +- **Evidence Maturity**: `E1` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: pending — Requires a controlled live IMAP account and network access. + +### Purpose + +Configures IMAP/SMTP email accounts, validates TLS certificates, and polls background inbox updates. + +### Evidence summary + +- `routes/email_routes.py` — `setup_email_routes` — Sets up email account management and synchronization routes. +- `routes/email_pollers.py` — `_start_poller` — Background poller for email inbox synchronization. +- `tests/test_service_health_email.py` — `test_email_ok_all_connect` — Tests IMAP connection health probing and status reporting. + +### Unknowns + +- Account lockouts if bad credentials are repeatedly polled. + +## EMAIL-002 — Email Searching, Threading & Message Operations + +- **Domain**: `email` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Parses email headers, folds signatures, groups messages into threads, and executes full-text email search. + +### Evidence summary + +- `routes/email_routes.py` — `@router.get('/search')` — Executes search across cached email headers and text. +- `src/email_thread_parser.py` — `parse_email_thread` — Builds conversation tree from Message-ID and In-Reply-To headers. + +### Unknowns + +- Malformed MIME email structures failing HTML sanitization. + +## EMAIL-003 — Email Composition, Draft Management & Sending + +- **Domain**: `email` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: pending — Requires a controlled live SMTP account and network access. + +### Purpose + +Creates, saves, and dispatches HTML/plaintext email messages via SMTP. + +### Evidence summary + +- `routes/email_routes.py` — `@router.post('/send')` — Sends email message via user SMTP credentials. + +### Unknowns + +- SMTP connection drop mid-send causing unsent mail state. + +## EMAIL-004 — Email MCP Server & Codex Integration Bridge + +- **Domain**: `email` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Exposes constrained email reading and draft capabilities to external Codex / MCP agents with scope checks. + +### Evidence summary + +- `mcp_servers/email_server.py` — `EmailMcpServer` — MCP server exposing email tools over stdio/SSE. +- `routes/codex_routes.py` — `setup_codex_routes` — Bridge endpoints for external Codex plugin integration. + +### Unknowns + +- Unauthorized mail sending if token scopes are improperly scoped. diff --git a/docs/discovery/domains/frontend.md b/docs/discovery/domains/frontend.md new file mode 100644 index 000000000..6f9891783 --- /dev/null +++ b/docs/discovery/domains/frontend.md @@ -0,0 +1,149 @@ +# Frontend + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## FRONTEND-001 — Single Page Application Shell & Client Router + +- **Domain**: `frontend` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Main HTML5 SPA shell, DOM lifecycle initializers, tab navigation, and deep-link route handlers. + +### Evidence summary + +- `static/index.html` — `index.html` — Main SPA entry point containing modal roots and CSS bundles. +- `app.py` — `serve_index` — Serves index.html with dynamically generated CSP nonces. + +### Unknowns + +- Stale browser static cache if asset hashing is omitted during deployment. + +## FRONTEND-002 — Dynamic Theme, Color System & Custom Fonts + +- **Domain**: `frontend` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Supports dark/light themes, custom CSS variables, color picker controls, and user font uploads. + +### Evidence summary + +- `static/js/theme.js` — `applyTheme` — Applies custom HSL theme variables to DOM document root. +- `routes/font_routes.py` — `setup_font_routes` — Allows uploading and serving custom WOFF2 font files. + +### Unknowns + +- Flash of unstyled content (FOUC) on slow connections. + +## FRONTEND-003 — Window Manager, Tile Layout & Modal Control System + +- **Domain**: `frontend` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Manages draggable tool windows, snapped multi-tile viewports, modal dialog Z-ordering, and ESC key stacks. + +### Evidence summary + +- `static/js/modalManager.js` — `ModalManager` — Controls modal open/close transitions and focus trapping. +- `static/js/tileManager.js` — `TileManager` — Handles viewport split-pane grid arrangements. + +### Unknowns + +- Overlap artifacts when opening many simultaneous tool floating windows. + +## FRONTEND-004 — Global Keyboard Shortcuts & Accessibility Controls + +- **Domain**: `frontend` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Provides configurable hotkeys (Ctrl+K search, Esc close, Alt+1-9 tabs) and high-contrast accessibility options. + +### Evidence summary + +- `static/js/keyboard-shortcuts.js` — `initShortcuts` — Binds global keydown handlers for system shortcuts. +- `static/js/a11y.js` — `initA11y` — Applies ARIA roles and dyslexic font toggles. + +### Unknowns + +- Browser keybinding collisions with browser default hotkeys. + +## FRONTEND-005 — Markdown, LaTeX & Code Block Streaming Renderer + +- **Domain**: `frontend` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Parses incoming SSE markdown streams, renders KaTeX math formulas, syntax-highlighted code, and interactive runners. + +### Evidence summary + +- `static/js/markdown.js` — `renderMarkdown` — Converts markdown prose to HTML nodes with syntax highlighting. +- `static/js/streamingSegmenter.js` — `Segmenter` — Parses un-closed markdown fences during live stream. + +### Unknowns + +- DOM thrashing if streaming segmenter updates UI too frequently. + +## FRONTEND-006 — Interactive Tour & Guided Onboarding System + +- **Domain**: `frontend` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Presents interactive step-by-step feature tours and UI tooltip hints for new users. + +### Evidence summary + +- `static/js/tourHints.js` — `startTour` — Renders guided feature tour overlays over target UI elements. + +### Unknowns + +- Tour step misalignment if window is resized mid-tour. + +## FRONTEND-007 — Background Effects Prototyping Sandbox + +- **Domain**: `frontend` +- **Status**: `dead-code-candidate` +- **Evidence Maturity**: `E1` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Standalone sandbox page for prototyping visual background animations, waves, and whirlpool effects. + +### Evidence summary + +- `app.py` — `serve_backgrounds` — Serves visual background sandbox HTML page route. +- `static/wave-variants.html` — `wave-variants.html` — Interactive background effect prototyping sandbox variant. + +### Unknowns + +- Route `/backgrounds` in app.py L918 attempts to serve `static/backgrounds.html` which is missing from disk; variant templates `wave-variants.html` and `whirlpool-variants.html` exist. diff --git a/docs/discovery/domains/media.md b/docs/discovery/domains/media.md new file mode 100644 index 000000000..cc0622ceb --- /dev/null +++ b/docs/discovery/domains/media.md @@ -0,0 +1,170 @@ +# Media + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## MEDIA-001 — Gallery Image Library & Album Operations + +- **Domain**: `media` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Organizes images into custom albums, provides grid browsing, tagging, and album metadata management. + +### Evidence summary + +- `routes/gallery/gallery_routes.py` — `@router.get('/api/gallery/library')` — Fetches image library list with tag filters. +- `static/js/gallery.js` — `initGallery` — Main gallery grid renderer and uploader. + +### Unknowns + +- Thumbnail generation overhead for high-resolution RAW camera images. + +## MEDIA-002 — Image Processing, AI Upscaling & Style Transfer + +- **Domain**: `media` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Executes local image enhancement, background removal, face sharpening, and AI upscaling operations. + +### Evidence summary + +- `routes/gallery/gallery_routes.py` — `@router.post('/api/gallery/ai-upscale')` — Runs RealESRGAN image upscaling. +- `routes/gallery/gallery_routes.py` — `@router.post('/api/image/remove-bg')` — Executes background removal pass. + +### Unknowns + +- High GPU memory allocation when upscaling 4K images. + +## MEDIA-003 — Interactive Image Canvas Editor & Persisted Drafts + +- **Domain**: `media` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Provides full multi-layer raster canvas editor, brush tools, transforms, masks, and draft project persistence. + +### Evidence summary + +- `routes/editor_draft_routes.py` — `setup_editor_draft_routes` — API routes for saving and loading canvas project drafts. +- `static/js/editor/history-panel.js` — `HistoryManager` — Canvas undo/redo stack manager. + +### Unknowns + +- Browser memory leak if multi-gigabyte layer undo buffers are kept indefinitely. + +## MEDIA-004 — Text-to-Speech (TTS) Synthesis Service + +- **Domain**: `media` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Synthesizes spoken audio from text using local Kokoro, EdgeTTS, or OpenAI TTS engines. + +### Evidence summary + +- `routes/tts_routes.py` — `@router.post('/synthesize')` — Synthesizes TTS audio clip. +- `services/tts/tts_service.py` — `TTSService` — Provider abstraction layer for audio speech generation. + +### Unknowns + +- Audio synthesis latency on CPU-only hardware setups. + +## MEDIA-005 — Speech-to-Text (STT) Audio Transcription Service + +- **Domain**: `media` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Transcribes user audio recordings into text using faster-whisper or local speech models. + +### Evidence summary + +- `routes/stt_routes.py` — `@router.post('/transcribe')` — Accepts multipart audio file and returns transcription text. +- `services/stt/stt_service.py` — `STTService` — Whisper audio transcription engine wrapper. + +### Unknowns + +- Missing ffmpeg system dependency prevents audio format decoding. + +## MEDIA-006 — Digital Signature Stamp Storage & Placement + +- **Domain**: `media` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Stores transparent PNG user signatures and stamps for placement onto PDF forms and documents. + +### Evidence summary + +- `routes/signature_routes.py` — `setup_signature_routes` — CRUD endpoints for managing user signature PNG stamps. + +### Unknowns + +- Cross-site scripting if signature image titles contain unescaped user input. + +## MEDIA-007 — Generated Image Artifact Route & MCP Integration + +- **Domain**: `media` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Serves generated AI artwork artifacts and integrates with image generation MCP server. + +### Evidence summary + +- `app.py` — `serve_generated_image` — Serves generated image artifacts with cache headers. +- `src/generated_images.py` — `resolve_generated_image_path` — Confines requested image path within artifacts directory. + +### Unknowns + +- Path traversal vulnerability if filename parameter is un-sanitized. + +## MEDIA-008 — Native MLX Image Bridge (macOS Apple Silicon) + +- **Domain**: `media` +- **Status**: `experimental` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: pending — Requires Apple Silicon, macOS tooling, and the compiled MLX bridge. + +### Purpose + +Native Apple Swift bridge for hardware-accelerated diffusion and MLX image colorization on macOS. + +### Evidence summary + +- `swift/odysseus-mlx-image-bridge/Package.swift` — `Package` — Swift package manifest for native MLX image bridge. +- `scripts/mlx_image_server.py` — `main` — Python daemon wrapping native Swift MLX binary. + +### Unknowns + +- Binary build requires Xcode command line tools build step (`build-macos-app.sh`). diff --git a/docs/discovery/domains/memory.md b/docs/discovery/domains/memory.md new file mode 100644 index 000000000..22b9f8b78 --- /dev/null +++ b/docs/discovery/domains/memory.md @@ -0,0 +1,24 @@ +# Memory + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## MEMORY-001 — Persistent Long-Term Memory & Vector Indexing + +- **Domain**: `memory` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Extracts facts, user preferences, and temporal memories from chat sessions into vector/relational storage. + +### Evidence summary + +- `routes/memory/memory_routes.py` — `@router.get('')` — Fetches long-term user memory timeline. +- `services/memory/memory_extractor.py` — `MemoryExtractor` — LLM-driven fact extraction from conversation transcripts. + +### Unknowns + +- Conflicting memory facts extracted from contradictory user prompts. diff --git a/docs/discovery/domains/model.md b/docs/discovery/domains/model.md new file mode 100644 index 000000000..1855c0d3f --- /dev/null +++ b/docs/discovery/domains/model.md @@ -0,0 +1,177 @@ +# Model + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## MODEL-001 — Multi-Provider LLM Model Discovery & Metadata Management + +- **Domain**: `model` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Discovers models from OpenAI, Anthropic, Ollama, vLLM, LMStudio, OpenRouter, and Google AI Studio endpoints. + +### Evidence summary + +- `routes/model_routes.py` — `@router.get('/api/models')` — Returns unified list of available models across providers. +- `src/model_discovery.py` — `ModelDiscovery.discover_all` — Queries connected provider endpoints for available model IDs. + +### Unknowns + +- Remote endpoint timeouts may slow down full discovery refresh. + +## MODEL-002 — Model Capability & Context Limits Detection + +- **Domain**: `model` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Detects vision, tool calling, reasoning, and context window limits for connected model endpoints. + +### Evidence summary + +- `src/model_capabilities.py` — `get_model_capabilities` — Maps model names to vision and tool support flags. +- `src/endpoint_resolver.py` — `resolve_endpoint_headers` — Resolves auth headers and target URLs for model endpoints. + +### Unknowns + +- Incorrect context limit metadata for unlisted custom fine-tunes. + +## MODEL-003 — LLM Core Provider Communication & Fallback Routing + +- **Domain**: `model` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: pending — Provider dispatch, header injection and fallback advancement are only observable against a reachable LLM provider endpoint; not exercised in this documentation pass. + +### Purpose + +Manages HTTP request dispatching, authorization header injection, and fallback provider routing for LLM calls. + +### Evidence summary + +- `src/llm_core.py` — `llm_call_async` — Non-streaming provider request dispatcher: resolves the endpoint, injects authorization headers and executes the HTTP call. +- `src/llm_core.py` — `llm_call_async_with_fallback` — Ordered fallback wrapper that retries llm_call_async across the configured candidate endpoints. +- `src/llm_core.py` — `stream_llm_with_fallback` — Ordered fallback wrapper for the streaming path; advances to the next candidate when a provider yields an empty completion. + +### Unknowns + +- Unexpected API changes in upstream third-party model providers. + +## MODEL-004 — Model Selection & Display Ordering Preferences + +- **Domain**: `model` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Allows pinning, sorting, and hiding specific models in the UI selection dropdown. + +### Evidence summary + +- `routes/model_routes.py` — `@router.post('/order')` — Saves custom model display order preference. + +### Unknowns + +- Stale model IDs in custom order lists after model endpoints are removed. + +## MODEL-005 — Side-by-Side Model Comparison (A/B Testing) + +- **Domain**: `model` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Enables dual-model side-by-side response evaluation, arena scoring, and latency comparison. + +### Evidence summary + +- `routes/compare/compare_routes.py` — `@router.post('/start')` — Starts a parallel dual-model comparison stream. +- `static/js/compare/index.js` — `initCompareView` — Renders side-by-side model chat panes. + +### Unknowns + +- High memory and network usage when streaming two model responses simultaneously. + +## MODEL-006 — GitHub Copilot Device Flow Authentication + +- **Domain**: `model` +- **Status**: `verified` +- **Evidence Maturity**: `E2` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: pending — Requires an interactive GitHub Copilot OAuth device-flow account. + +### Purpose + +Authenticates with GitHub Copilot via OAuth device flow to use Copilot models directly. + +### Evidence summary + +- `routes/copilot_routes.py` — `setup_copilot_routes` — Builds the Copilot device-flow router at prefix /api/copilot, wiring _start_device_flow and _poll_device_flow. +- `routes/device_flow.py` — `create_device_flow_router` — Shared factory registering POST /device/start and POST /device/poll under the caller-supplied prefix. +- `src/copilot.py` — `request_device_code` — Issues the GitHub device-code request that begins the Copilot OAuth device flow. +- `src/copilot.py` — `poll_access_token` — Polls GitHub for the access token once the user has authorized the device code. +- `tests/test_provider_device_flow_js.py` — `test_copilot_success_uses_complete_verification_uri` — Inspected unit test asserting the Copilot device-flow runner surfaces the complete verification URI returned by the backend. + +### Unknowns + +- Token expiration requires manual device re-authentication. + +## MODEL-007 — ChatGPT Subscription Device Flow Authentication + +- **Domain**: `model` +- **Status**: `verified` +- **Evidence Maturity**: `E2` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: pending — Requires an interactive ChatGPT subscription OAuth flow. + +### Purpose + +Authenticates with ChatGPT Pro/Plus subscription tokens via device login flow. + +### Evidence summary + +- `routes/chatgpt_subscription_routes.py` — `setup_chatgpt_subscription_routes` — Builds the ChatGPT subscription device-flow router at prefix /api/chatgpt-subscription. +- `routes/device_flow.py` — `create_device_flow_router` — Shared factory registering POST /device/start and POST /device/poll under the caller-supplied prefix. +- `src/chatgpt_subscription.py` — `request_device_code` — Issues the ChatGPT device-authorization request that begins the subscription OAuth device flow. +- `src/chatgpt_subscription.py` — `poll_device_auth` — Polls the ChatGPT device-authorization endpoint for completion using the stored device_auth_id and user_code. +- `tests/test_provider_device_flow_js.py` — `test_chatgpt_success_uses_plain_verification_uri` — Inspected unit test asserting the ChatGPT device-flow runner uses the plain verification URI rather than the Copilot complete-URI form. + +### Unknowns + +- Changes in OpenAI auth endpoint security challenges. + +## MODEL-008 — Embedding Model Lane & Vector Provider Setup + +- **Domain**: `model` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Configures local sentence-transformers, FastEmbed, or remote OpenAI embedding model lanes. + +### Evidence summary + +- `routes/embedding_routes.py` — `setup_embedding_routes` — Provides embedding provider configuration endpoints. +- `src/embeddings.py` — `EmbeddingManager` — Generates dense vector embeddings for RAG and memory. + +### Unknowns + +- First-time download of heavy PyTorch model weights on CPU-only machines. diff --git a/docs/discovery/domains/note.md b/docs/discovery/domains/note.md new file mode 100644 index 000000000..e0c197428 --- /dev/null +++ b/docs/discovery/domains/note.md @@ -0,0 +1,24 @@ +# Note + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## NOTE-001 — Interactive Notes & Checklist Management + +- **Domain**: `note` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Provides Google Keep-style notes, rich markdown text, checklist items, pinning, color tags, and reminders. + +### Evidence summary + +- `routes/note/note_routes.py` — `@router.get('')` — Lists all user notes with pin and archive states. +- `static/js/notes.js` — `initNotesView` — Main interactive notes grid and modal manager. + +### Unknowns + +- Concurrent edits on note item checkboxes. diff --git a/docs/discovery/domains/platform.md b/docs/discovery/domains/platform.md new file mode 100644 index 000000000..037edddde --- /dev/null +++ b/docs/discovery/domains/platform.md @@ -0,0 +1,213 @@ +# Platform + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## PLATFORM-001 — Application Initialization & Lifespan Management + +- **Domain**: `platform` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Orchestrates server startup, database table migration, background daemon initialization, and clean shutdown. + +### Evidence summary + +- `app.py` — `_lifespan` — FastAPI lifespan context manager executing startup tasks. +- `src/app_initializer.py` — `initialize_app` — Initializes app directories, DB schemas, and logging. + +### Unknowns + +- Un-handled exceptions during startup halt application launch. + +## PLATFORM-002 — System Health, Readiness & Version Monitoring APIs + +- **Domain**: `platform` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Exposes Liveness (/api/health), Readiness (/api/ready), App Version (/api/version), and Client Perf APIs. + +### Evidence summary + +- `app.py` — `readiness_check` — Performs system component integrity check. +- `src/readiness.py` — `check_readiness` — Checks database, storage, and key paths for read/write access. + +### Unknowns + +- Readiness check delays if verifying connectivity to offline remote endpoints. + +## PLATFORM-003 — Database Schema, Migrations & SQLite Persistence + +- **Domain**: `platform` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Defines core relational tables (users, tokens, tasks, sessions) and executes automated SQLite schema upgrades. + +### Evidence summary + +- `core/database.py` — `init_db` — Creates ORM tables and establishes connection pool. +- `scripts/update_database.py` — `run_migrations` — Applies missing schema columns and indices. + +### Unknowns + +- SQLite file lock contention under high concurrent write loads. + +## PLATFORM-004 — User Data Export & Import Backup Infrastructure + +- **Domain**: `platform` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Exports complete user workspace state (sessions, memory, skills, notes, presets) into a zip archive. + +### Evidence summary + +- `routes/backup_routes.py` — `setup_backup_routes` — Handles workspace data export and import upload unpack. +- `docs/backup-restore.md` — `Documentation` — Backup and restore operational documentation. + +### Unknowns + +- Corrupt archive files causing partial data restore. + +## PLATFORM-005 — File Cleanup & Storage Maintenance Engine + +- **Domain**: `platform` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Scans data directories for orphaned files, old uploads, temporary vision images, and frees disk space. + +### Evidence summary + +- `routes/cleanup/cleanup_routes.py` — `@router.get('/preview')` — Previews reclaimable disk space across storage directories. +- `src/cleanup_service.py` — `CleanupService` — Executes filesystem purge of orphaned asset files. + +### Unknowns + +- Deletes files uploaded in active sessions if retention window is set too short. + +## PLATFORM-006 — System Health & RAG Diagnostic Suite + +- **Domain**: `platform` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Executes real-time integrity diagnostics across ChromaDB, SearXNG, local models, and network interfaces. + +### Evidence summary + +- `routes/diagnostics_routes.py` — `setup_diagnostics_routes` — Runs subsystem health check suite. +- `src/service_health.py` — `collect_health_status` — Inspects vector database, email, search, and local provider status. + +### Unknowns + +- Diagnostic timeout if external search provider is unreachable. + +## PLATFORM-007 — Desktop CLI Utilities & Shell Integration Tools + +- **Domain**: `platform` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Provides command-line interface tools (`odysseus`, `odysseus-mcp`, `odysseus-mail`) for terminal usage. + +### Evidence summary + +- `scripts/_lib/cli.py` — `main` — Shared CLI framework for terminal helper commands. +- `scripts/odysseus` — `odysseus` — Main terminal launcher script. + +### Unknowns + +- Outdated CLI scripts if backend API schemas change. + +## PLATFORM-008 — Desktop Companion App Integration + +- **Domain**: `platform` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Provides API routes and pairing mechanisms for the native macOS/desktop menu bar companion app. + +### Evidence summary + +- `companion/routes.py` — `setup_companion_routes` — Endpoints for pairing and status sync with desktop companion. +- `companion/pairing.py` — `PairingManager` — Generates and validates companion pairing codes. + +### Unknowns + +- Pairing code expiration timing window. + +## PLATFORM-009 — Docker Containerization & GPU Hardware Manifests + +- **Domain**: `platform` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: pending — Requires Docker GPU pass-through and compatible host drivers. + +### Purpose + +Provides multi-stage Dockerfile and Docker Compose manifests for CPU, NVIDIA CUDA, and AMD ROCm GPUs. + +### Evidence summary + +- `Dockerfile` — `multi-stage-build` — Multi-stage container build environment. +- `docker-compose.gpu-nvidia.yml` — `nvidia-gpu-manifest` — NVIDIA GPU pass-through container specification. +- `scripts/check-docker-gpu.sh` — `check-docker-gpu` — Automated diagnostic test script for host NVIDIA GPU passthrough. + +### Unknowns + +- Driver version incompatibility with host NVIDIA/AMD kernel drivers. + +## PLATFORM-010 — Legacy FAISS Vector Index Migration Script + +- **Domain**: `platform` +- **Status**: `legacy` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Legacy utility script to migrate older FAISS vector indices into ChromaDB. + +### Evidence summary + +- `scripts/migrate_faiss_to_chroma.py` — `migrate_faiss` — Reads FAISS vector index files and writes to ChromaDB collection. + +### Unknowns + +- Superseded by native ChromaDB vector index pipeline. diff --git a/docs/discovery/domains/research.md b/docs/discovery/domains/research.md new file mode 100644 index 000000000..2df5a4b75 --- /dev/null +++ b/docs/discovery/domains/research.md @@ -0,0 +1,86 @@ +# Research + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## RESEARCH-001 — Deep Research Execution Engine & SSE Progress Streaming + +- **Domain**: `research` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Executes multi-step recursive deep research tasks, web page scraping, synthesis, and streams live progress. + +### Evidence summary + +- `routes/research/research_routes.py` — `@router.post('/api/research/start')` — Initiates deep research job. +- `src/deep_research.py` — `DeepResearchEngine` — Recursive search and summary crawler. + +### Unknowns + +- High memory consumption when parsing multi-megabyte HTML target pages. + +## RESEARCH-002 — Research Library, Detail View & Image Controls + +- **Domain**: `research` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Stores completed research reports, generated diagrams, reference links, and manages image visibility. + +### Evidence summary + +- `routes/research/research_routes.py` — `@router.get('/api/research/library')` — Returns all saved research reports. + +### Unknowns + +- Orphaned report files if storage directory is modified out-of-band. + +## RESEARCH-003 — Web Search Engine Integration (SearXNG & Multi-Provider) + +- **Domain**: `research` +- **Status**: `verified` +- **Evidence Maturity**: `E1` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: pending — Requires an active SearXNG instance or external search API provider. + +### Purpose + +Queries SearXNG, DuckDuckGo, or Google Search instances to retrieve web search snippets. + +### Evidence summary + +- `routes/search_routes.py` — `setup_search_routes` — Defines /api/search, /api/search/config, and /api/search/query endpoints. +- `src/search/core.py` — `SearchEngine` — Compatibility module aliasing services.search.core. +- `tests/test_search_ranking.py` — `test_news_queries_prefer_news_sources_over_sports_and_social_results` — Tests search result domain ranking and scoring. + +### Unknowns + +- Search provider IP throttling or rate-limiting. + +## RESEARCH-004 — Research Result Peeking & Topic Spinoff Generation + +- **Domain**: `research` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Extracts preliminary research snippets and spawns child research sessions focused on specific sub-topics. + +### Evidence summary + +- `routes/research/research_routes.py` — `@router.post('/api/research/spinoff/{session_id}')` — Spawns child research session for specific query. + +### Unknowns + +- Deep recursion tree depth when spawning multiple nested spinoffs. diff --git a/docs/discovery/domains/security.md b/docs/discovery/domains/security.md new file mode 100644 index 000000000..2d07dae22 --- /dev/null +++ b/docs/discovery/domains/security.md @@ -0,0 +1,154 @@ +# Security + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## SECURITY-001 — Authentication, Session Cookies & User Management + +- **Domain**: `security` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Handles bcrypt password hashing, session cookie issuance, authentication enforcement, and user administration. + +### Evidence summary + +- `routes/auth_routes.py` — `@router.post('/login')` — Authenticates credentials and sets session cookie. +- `core/auth.py` — `AuthManager` — Handles user creation, password verification, and session tokens. + +### Unknowns + +- Cookie session hijack if deployed over unencrypted HTTP without HTTPS cookie flags. + +## SECURITY-002 — System Vault Encrypted Secret Storage + +- **Domain**: `security` +- **Status**: `verified` +- **Evidence Maturity**: `E1` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: pending — Requires installed Bitwarden CLI (`bw`) executable. + +### Purpose + +Encrypts API keys, passphrases, and third-party secrets on disk using AES-GCM / PBKDF2 key derivation. + +### Evidence summary + +- `routes/vault_routes.py` — `setup_vault_routes` — Admin routes for vault configuration, login, unlock, lock, and logout. +- `src/secret_storage.py` — `SecretStorage` — Fernet symmetric key DB secret encryption. +- `tests/test_vault_password_not_in_argv.py` — `test_bw_password_not_in_argv` — Verifies master password is fed via stdin and never appears in process argv. + +### Unknowns + +- Loss of vault master passphrase renders all encrypted secrets permanently unrecoverable. + +## SECURITY-003 — API Token Management & Scope Access Control + +- **Domain**: `security` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Generates scoped API bearer tokens (read/write/admin) for external tool and script authentication. + +### Evidence summary + +- `routes/api_token_routes.py` — `setup_api_token_routes` — Exposes API token creation, scope assignment, and revocation. +- `core/database.py` — `ApiToken` — SQLAlchemy ORM schema for API tokens and permissions. + +### Unknowns + +- Leaked API bearer tokens with excessive permission scopes. + +## SECURITY-004 — Prompt Security & Injection Defense Engine + +- **Domain**: `security` +- **Status**: `verified` +- **Evidence Maturity**: `E1` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Scans system prompts and external inputs for prompt injection attempts, jailbreaks, and sensitive data leaks. + +### Evidence summary + +- `src/prompt_security.py` — `untrusted_context_message` — Wraps untrusted context with guard delimiters and sets metadata.trusted = False. +- `src/tool_security.py` — `NON_ADMIN_BLOCKED_TOOLS` — Enforces tool execution safety for non-admin user roles. +- `tests/test_skill_index_prompt_injection.py` — `test_skill_index` — Verifies skill index descriptions cannot leak into trusted system prompts. +- `tests/test_tool_output_prompt_injection.py` — `test_tool_output` — Tool output injection guards. + +### Unknowns + +- False positives blocking legitimate complex coding or security prompts. + +## SECURITY-005 — URL & Path Confinement Security Guards + +- **Domain**: `security` +- **Status**: `verified` +- **Evidence Maturity**: `E1` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Prevents SSRF attacks and path traversal by validating target IP addresses and resolving symlinks. + +### Evidence summary + +- `src/url_safety.py` — `check_outbound_url` — Rejects non-HTTP(S) schemes, link-local, cloud metadata SSRF addresses. +- `src/url_security.py` — `validate_public_http_url` — Validates public-facing endpoints. +- `tests/test_url_safety.py` — `test_url_safety` — Scheme validation, cloud metadata SSRF rejection, IP classification. +- `tests/test_tool_path_confinement.py` — `test_path_confinement` — Path traversal checks. +- `tests/test_workspace_confine.py` — `test_workspace_confine` — Workspace confinement checks. + +### Unknowns + +- DNS rebinding attacks if IP address is re-resolved post-validation. + +## SECURITY-006 — HTTP Security Headers Middleware + +- **Domain**: `security` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Injects standard OWASP HTTP security headers (CSP, HSTS, X-Content-Type-Options, X-Frame-Options). + +### Evidence summary + +- `core/middleware.py` — `SecurityHeadersMiddleware` — Sets strict security headers and CSP nonces on HTTP responses. + +### Unknowns + +- Strict Content Security Policy (CSP) blocking third-party embedded web resources. + +## SECURITY-007 — Admin System Data Wipe ('Danger Zone') + +- **Domain**: `security` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Provides administrative reset operations to wipe sessions, cache, uploaded files, or factory reset state. + +### Evidence summary + +- `routes/admin_wipe/admin_wipe_routes.py` — `@router.delete('/wipe/{kind}')` — Executes systemic data wipe based on requested scope. + +### Unknowns + +- Accidental catastrophic data loss if triggered without user confirmation. diff --git a/docs/discovery/domains/skill.md b/docs/discovery/domains/skill.md new file mode 100644 index 000000000..43fb72180 --- /dev/null +++ b/docs/discovery/domains/skill.md @@ -0,0 +1,24 @@ +# Skill + +Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. + +## SKILL-001 — Dynamic Skill Management & Code Execution Engine + +- **Domain**: `skill` +- **Status**: `verified` +- **Evidence Maturity**: `E0` +- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. + +### Purpose + +Allows users to create, import, edit, test, and execute custom Python/Markdown skills dynamically. + +### Evidence summary + +- `routes/skills_routes.py` — `setup_skills_routes` — Exposes CRUD and remote import routes for user skills. +- `services/memory/skills.py` — `SkillsManager` — Handles skill storage, parsing, and execution. + +### Unknowns + +- Arbitrary code execution risks if skill import URL is untrusted. diff --git a/docs/discovery/feature-catalog.json b/docs/discovery/feature-catalog.json new file mode 100644 index 000000000..93cdc6fd0 --- /dev/null +++ b/docs/discovery/feature-catalog.json @@ -0,0 +1,4797 @@ +[ + { + "id": "CHAT-001", + "domain": "chat", + "name": "Core Chat Streaming & SSE Message Generation", + "purpose": "Handles real-time Server-Sent Events (SSE) chat streaming, token rendering, and model response generation.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/chatStream.js", + "static/js/chat.js", + "static/js/streamingRenderer.js" + ], + "backend_entrypoints": [ + "routes/chat_routes.py:chat_stream", + "routes/chat_helpers.py:build_chat_context", + "src/chat_handler.py:ChatHandler.preprocess_message", + "src/chat_processor.py:ChatProcessor.build_context_preface", + "src/llm_core.py:stream_llm_with_fallback", + "src/agent_loop.py:stream_agent_loop" + ], + "routes": [ + "/api/chat_stream", + "/api/chat" + ], + "configuration": [ + "OPENAI_API_KEY", + "OLLAMA_BASE_URL", + "REQUEST_TIMEOUT" + ], + "persistence": [ + "SESSIONS_FILE", + "DATA_DIR/sessions/" + ], + "dependencies": [ + "fastapi", + "starlette.responses.StreamingResponse", + "httpx" + ], + "tests": [ + "tests/test_chat_metrics.py", + "tests/test_resend_message_nondestructive.py" + ], + "documentation": [ + "docs/chat.webm", + "README.md" + ], + "risks": [ + "Stream interruption on connection drops requires retry logic." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/chat_routes.py", + "symbol": "chat_stream", + "line_range": "L702-L1870", + "explanation": "POST /api/chat_stream SSE endpoint; builds the shared chat context, then dispatches to the chat-mode or agent-mode streaming path." + }, + { + "path": "routes/chat_helpers.py", + "symbol": "build_chat_context", + "line_range": "L669-L863", + "explanation": "Shared context builder invoked by chat_stream; runs message preprocessing and assembles the memory/RAG/web context preface." + }, + { + "path": "src/chat_handler.py", + "symbol": "ChatHandler.preprocess_message", + "line_range": "L123-L315", + "explanation": "Message preprocessing (attachments, URLs, tool preprocessing) reached from build_chat_context via routes/chat_helpers.py:preprocess." + }, + { + "path": "src/chat_processor.py", + "symbol": "ChatProcessor.build_context_preface", + "line_range": "L263-L493", + "explanation": "Builds the retrieval and web-source context preface injected into the streamed request." + }, + { + "path": "src/llm_core.py", + "symbol": "stream_llm_with_fallback", + "line_range": "L2794-L2895", + "explanation": "Chat-mode streaming dispatcher called from chat_stream; wraps stream_llm with an ordered provider fallback chain." + }, + { + "path": "src/llm_core.py", + "symbol": "stream_llm", + "line_range": "L2131-L2151", + "explanation": "Per-request streaming entry wrapped by stream_llm_with_fallback; acquires the local model slot and delegates to _stream_llm_inner." + }, + { + "path": "src/agent_loop.py", + "symbol": "stream_agent_loop", + "line_range": "L3079-L5248", + "explanation": "Agent-mode streaming path called from chat_stream when the request selects agent mode." + }, + { + "path": "tests/test_chat_metrics.py", + "symbol": "test_stream_llm_passes_through_llamacpp_timings", + "line_range": "L129-L136", + "explanation": "Inspected unit test asserting stream_llm forwards backend generation timings into the emitted metrics chunk." + }, + { + "path": "tests/test_resend_message_nondestructive.py", + "symbol": "test_resend_message_does_not_truncate_by_default", + "line_range": "L23-L36", + "explanation": "Inspected unit test asserting the frontend resend path does not truncate prior conversation turns." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E2", + "runtime_validation": { + "required": true, + "status": "pending", + "reason": "Requires access to a live LLM provider endpoint (OpenAI API key or local Ollama server)." + } + }, + { + "id": "CHAT-002", + "domain": "chat", + "name": "Session Management & Conversation State", + "purpose": "Manages session creation, listing, switching, renaming, and persistence of conversation metadata.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/sessions.js", + "static/js/sidebar-layout.js" + ], + "backend_entrypoints": [ + "routes/session_routes.py:setup_session_routes", + "core/session_manager.py:SessionManager" + ], + "routes": [ + "/api/sessions", + "/api/sessions/{session_id}" + ], + "configuration": [ + "SESSIONS_FILE", + "DATA_DIR/odysseus.db" + ], + "persistence": [ + "DATA_DIR/sessions.json", + "SQLite session table" + ], + "dependencies": [ + "sqlite3", + "pydantic" + ], + "tests": [ + "tests/test_session_manager.py", + "tests/test_session_routes_utcnow.py" + ], + "documentation": [ + "docs/setup.md" + ], + "risks": [ + "Concurrent file writes to sessions.json under high load." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/session_routes.py", + "symbol": "@router.get('/api/sessions')", + "line_range": "L150-L210", + "explanation": "Lists active sessions filtered by user owner scope." + }, + { + "path": "core/session_manager.py", + "symbol": "SessionManager", + "line_range": "L30-L150", + "explanation": "Provides thread-safe session storage operations." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "CHAT-003", + "domain": "chat", + "name": "Chat History & Message Editing/Truncation", + "purpose": "Provides history retrieval, message content updating, message deletion, and history branch truncation.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/chat.js", + "static/js/sessions.js" + ], + "backend_entrypoints": [ + "routes/history/history_routes.py:setup_history_routes" + ], + "routes": [ + "/api/history/{session_id}", + "/api/session/{session_id}/truncate", + "/api/session/{session_id}/edit-message" + ], + "configuration": [ + "MAX_HISTORY_MESSAGES" + ], + "persistence": [ + "DATA_DIR/sessions/" + ], + "dependencies": [ + "sqlite3", + "fastapi" + ], + "tests": [ + "tests/test_history_db_fallback_hidden.py", + "tests/test_truncate_message_count_regression.py" + ], + "documentation": [ + "docs/attachments.md" + ], + "risks": [ + "Truncating messages re-indexes context window and clears cached tool calls." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/history/history_routes.py", + "symbol": "@router.get('/api/history/{session_id}')", + "line_range": "L178-L230", + "explanation": "Fetches message history timeline for a session." + }, + { + "path": "routes/history_routes.py", + "symbol": "_sys.modules[__name__] = _canonical", + "line_range": "L1-L17", + "explanation": "Backward-compatibility shim module." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "CHAT-004", + "domain": "chat", + "name": "File & Multimodal Attachment Handling", + "purpose": "Handles uploading, mime validation, image preview, vision encoding, and file attachments in chat messages.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/fileHandler.js", + "static/js/chat.js" + ], + "backend_entrypoints": [ + "routes/upload_routes.py:setup_upload_routes", + "src/upload_handler.py:UploadHandler" + ], + "routes": [ + "/api/upload", + "/api/upload/{file_id}/vision", + "/api/upload/cleanup" + ], + "configuration": [ + "MAX_UPLOAD_SIZE_MB", + "ALLOWED_UPLOAD_EXTENSIONS" + ], + "persistence": [ + "DATA_DIR/uploads/" + ], + "dependencies": [ + "pillow", + "python-magic" + ], + "tests": [ + "tests/test_upload_handler_atomicity.py", + "tests/test_upload_routes_owner_scope.py" + ], + "documentation": [ + "docs/attachments.md" + ], + "risks": [ + "Large file uploads may consume server disk space if cleanup task fails." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/upload_routes.py", + "symbol": "@router.post('')", + "line_range": "L257-L310", + "explanation": "Accepts multi-part file uploads and generates vision metadata." + }, + { + "path": "src/upload_handler.py", + "symbol": "UploadHandler.save_file", + "line_range": "L50-L140", + "explanation": "Validates upload size and atomicity on disk." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "CHAT-005", + "domain": "chat", + "name": "Chat Message Search", + "purpose": "Enables full-text keyword search across stored chat messages and sessions.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/search-chat.js", + "static/js/search.js" + ], + "backend_entrypoints": [ + "routes/search_routes.py:setup_search_routes", + "src/session_search.py:search_sessions" + ], + "routes": [ + "/api/search/chat" + ], + "configuration": [ + "SEARCH_INDEX_CACHE_TTL" + ], + "persistence": [ + "SQLite FTS / session indices" + ], + "dependencies": [ + "sqlite3" + ], + "tests": [ + "tests/test_session_search.py", + "tests/test_session_search_batch_fetch.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Full table scans on un-indexed text columns for very large databases." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/search_routes.py", + "symbol": "setup_search_routes", + "line_range": "L30-L80", + "explanation": "Registers chat message search endpoint." + }, + { + "path": "src/session_search.py", + "symbol": "search_sessions", + "line_range": "L20-L90", + "explanation": "Executes query matching against session transcripts." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "CHAT-006", + "domain": "chat", + "name": "System Prompts & Preset Management", + "purpose": "Provides creation, selection, and customization of system prompt presets for chat sessions.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/presets.js", + "static/js/settings.js" + ], + "backend_entrypoints": [ + "routes/preset_routes.py:setup_preset_routes", + "src/preset_manager.py:PresetManager" + ], + "routes": [ + "/api/presets", + "/api/presets/{preset_id}" + ], + "configuration": [ + "PRESETS_FILE" + ], + "persistence": [ + "DATA_DIR/presets.json" + ], + "dependencies": [ + "pydantic", + "json" + ], + "tests": [ + "tests/cli/test_preset_cli_store.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Invalid JSON syntax in user presets file can corrupt preset loading." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/preset_routes.py", + "symbol": "setup_preset_routes", + "line_range": "L30-L110", + "explanation": "API routes for listing and modifying system prompt presets." + }, + { + "path": "src/preset_manager.py", + "symbol": "PresetManager", + "line_range": "L15-L100", + "explanation": "Disk-backed manager for prompt presets." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "CHAT-007", + "domain": "chat", + "name": "Emoji Rendering & Twemoji SVG Proxy", + "purpose": "Proxies Twemoji SVG icons locally to render flat SVG emojis in message text without external CDN dependencies.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/emojiPicker.js", + "static/js/emojiShortcodes.js" + ], + "backend_entrypoints": [ + "routes/emoji_routes.py:setup_emoji_routes" + ], + "routes": [ + "/api/emoji/{code}.svg" + ], + "configuration": [ + "EMOJI_CACHE_DIR" + ], + "persistence": [ + "DATA_DIR/emoji_cache/" + ], + "dependencies": [ + "httpx", + "fastapi.responses.Response" + ], + "tests": [ + "tests/test_censor_pref_js.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "First request fetches SVG from remote CDN before caching locally." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/emoji_routes.py", + "symbol": "setup_emoji_routes", + "line_range": "L20-L100", + "explanation": "Serves locally cached Twemoji SVGs." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "CHAT-008", + "domain": "chat", + "name": "Input History Recall (Arrow Up)", + "purpose": "Allows users to cycle through previously sent prompt messages in the chat composer input using Arrow-Up/Down keys.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/composerArrowUpRecall.js", + "static/js/chat.js" + ], + "backend_entrypoints": [ + "None (Pure client-side state feature)" + ], + "routes": [], + "configuration": [ + "localStorage key: chat_composer_history" + ], + "persistence": [ + "Browser localStorage" + ], + "dependencies": [ + "Vanilla JavaScript DOM keyboard listeners" + ], + "tests": [], + "documentation": [ + "README.md" + ], + "risks": [ + "Client-side browser storage limits." + ], + "unknowns": [], + "evidence": [ + { + "path": "static/js/composerArrowUpRecall.js", + "symbol": "initComposerRecall", + "line_range": "L1-L80", + "explanation": "Listens for ArrowUp keypress on composer textarea." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "CHAT-009", + "domain": "chat", + "name": "Context Window Compaction & Truncation", + "purpose": "Compacts session transcript history when prompt size exceeds context limits using summarization.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/chat.js" + ], + "backend_entrypoints": [ + "routes/history/history_routes.py:compact_session", + "src/context_compactor.py:compact_context" + ], + "routes": [ + "/api/session/{session_id}/compact", + "/api/session/{session_id}/context" + ], + "configuration": [ + "MAX_CONTEXT_TOKENS", + "COMPACTION_THRESHOLD" + ], + "persistence": [ + "DATA_DIR/sessions/" + ], + "dependencies": [ + "tiktoken", + "fastapi" + ], + "tests": [ + "tests/test_context_compactor.py" + ], + "documentation": [ + "specs/architecture-runtime-inventory.md" + ], + "risks": [ + "Aggressive compaction may discard subtle user instructions." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/history/history_routes.py", + "symbol": "@router.post('/api/session/{session_id}/compact')", + "line_range": "L751-L790", + "explanation": "Triggers context summarization and compaction." + }, + { + "path": "src/context_compactor.py", + "symbol": "compact_context", + "line_range": "L25-L110", + "explanation": "Executes context token pruning and summary generation." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "AGENT-001", + "domain": "agent", + "name": "Autonomous Agent Loop & Tool Execution Engine", + "purpose": "Executes multi-step agent reasoning loops, tool invocation parsing, and automated response generation.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/chatStream.js" + ], + "backend_entrypoints": [ + "src/agent_loop.py:run_agent_loop", + "src/tool_execution.py:execute_tool_call" + ], + "routes": [ + "/api/chat/stream" + ], + "configuration": [ + "MAX_AGENT_STEPS", + "ENABLE_TOOL_EXECUTION" + ], + "persistence": [ + "Session transcript tool calls" + ], + "dependencies": [ + "asyncio", + "pydantic" + ], + "tests": [ + "tests/test_tool_policy.py", + "tests/test_unknown_tool_calls.py" + ], + "documentation": [ + "specs/architecture-runtime-inventory.md" + ], + "risks": [ + "Infinite tool loop if termination condition fails." + ], + "unknowns": [], + "evidence": [ + { + "path": "src/agent_loop.py", + "symbol": "run_agent_loop", + "line_range": "L40-L210", + "explanation": "Core loop evaluating model tool requests and executing handlers." + }, + { + "path": "src/tool_execution.py", + "symbol": "execute_tool_call", + "line_range": "L30-L150", + "explanation": "Dispatches tool invocation requests to underlying tool handlers." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "AGENT-002", + "domain": "agent", + "name": "Scheduled Tasks & Event Bus Dispatcher", + "purpose": "Schedules background recurring or delayed tasks, emits event bus triggers, and executes automated flows.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/tasks.js" + ], + "backend_entrypoints": [ + "routes/task_routes.py:setup_task_routes", + "src/task_scheduler.py:TaskScheduler", + "src/event_bus.py" + ], + "routes": [ + "/api/tasks", + "/api/tasks/{task_id}/run", + "/api/tasks/{task_id}/pause", + "/api/tasks/{task_id}/resume" + ], + "configuration": [ + "TASK_SCHEDULER_INTERVAL" + ], + "persistence": [ + "DATA_DIR/tasks.db" + ], + "dependencies": [ + "apscheduler", + "sqlite3" + ], + "tests": [ + "tests/test_task_scheduler_cancel.py", + "tests/test_task_chain_owner_scope.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Task execution failure handling on system restart." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/task_routes.py", + "symbol": "@router.get('')", + "line_range": "L341-L370", + "explanation": "Fetches active scheduled tasks." + }, + { + "path": "src/task_scheduler.py", + "symbol": "TaskScheduler", + "line_range": "L40-L280", + "explanation": "Async task scheduler dispatching cron and delay triggers." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "AGENT-003", + "domain": "agent", + "name": "Webhook Event Subscriptions & Trigger Processing", + "purpose": "Manages incoming/outgoing webhook subscriptions, endpoint authentication tokens, and event triggers.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/admin.js" + ], + "backend_entrypoints": [ + "routes/webhook_routes.py:setup_webhook_routes", + "src/webhook_manager.py:WebhookManager" + ], + "routes": [ + "/api/webhooks", + "/v1/chat", + "/api/webhooks/{webhook_id}/test" + ], + "configuration": [ + "WEBHOOK_SECRET_KEY" + ], + "persistence": [ + "DATA_DIR/webhooks.db" + ], + "dependencies": [ + "httpx", + "sqlite3" + ], + "tests": [ + "tests/test_webhook_ssrf_resilience.py", + "tests/test_webhook_trigger_auth_exempt.py" + ], + "documentation": [ + "docs/pr-blocker-audit.md" + ], + "risks": [ + "SSRF risks when contacting external webhook URLs if unvalidated." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/webhook_routes.py", + "symbol": "@router.get('/webhooks')", + "line_range": "L71-L94", + "explanation": "Returns list of registered webhooks." + }, + { + "path": "src/webhook_manager.py", + "symbol": "WebhookManager", + "line_range": "L30-L160", + "explanation": "Handles payload delivery and signature verification." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "AGENT-004", + "domain": "agent", + "name": "Assistant Settings, Task Check-Ins & Background Job Monitor", + "purpose": "Manages per-user assistant sessions and scheduled check-in settings, drains background job completions, and retains a legacy no-op activity logging shim.", + "status": "partial", + "frontend_entrypoints": [ + "static/js/assistant.js" + ], + "backend_entrypoints": [ + "routes/assistant_routes.py:setup_assistant_routes", + "src/assistant_log.py", + "src/bg_monitor.py" + ], + "routes": [ + "/api/assistant/session", + "/api/assistant/settings", + "/api/assistant/run/{task_id}", + "/api/assistant/run-status/{task_id}", + "/api/assistant/available-timezones" + ], + "configuration": [ + "CrewMember.timezone (IANA tz name driving check-in scheduling)", + "CrewMember.model", + "CrewMember.enabled_tools", + "ScheduledTask.scheduled_time / ScheduledTask.status", + "src/bg_monitor.py:POLL_INTERVAL_S", + "src/bg_monitor.py:_FOLLOWUP_MAX_ROUNDS" + ], + "persistence": [ + "CrewMember (core/database.py, is_default_assistant singleton per owner)", + "Session pinned via CrewMember.session_id", + "ScheduledTask (per-owner check-in rows)", + "TaskRun (most recent run status for run-status polling)" + ], + "dependencies": [ + "logging", + "asyncio" + ], + "tests": [ + "tests/cli/test_logs_cli_resolve_nonstring.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Assistant seeding is owner-scoped: routes/assistant_routes.py documents that reaching an /api/assistant route under a synthetic owner previously seeded a duplicate CrewMember plus Morning/Midday/Evening tasks that double-fired; correctness now depends on RESERVED_USERNAMES staying complete.", + "Check-in next_run values are recomputed from CrewMember.timezone through compute_next_run, so a missing or stale IANA timezone shifts when check-ins fire.", + "Background follow-up continuation is retry-until-success: _run_followup returns False to defer and mark_followed_up() runs only after a successful agent run, so a persistently failing continuation is retried every POLL_INTERVAL_S tick without a bounded attempt count." + ], + "unknowns": [ + "Route `/api/assistant/logs` cited in legacy docs is absent from assistant router.", + "Existing unit test `tests/cli/test_logs_cli_resolve_nonstring.py` tests CLI target-name resolution logic, not active assistant routes or bg_monitor execution loop." + ], + "evidence": [ + { + "path": "routes/assistant_routes.py", + "symbol": "setup_assistant_routes", + "line_range": "L80-L326", + "explanation": "Active assistant session, settings, manual check-in, run-status and timezone-list endpoints, including the owner-scoping guards." + }, + { + "path": "src/bg_monitor.py", + "symbol": "_drain_agent", + "line_range": "L28-L72", + "explanation": "Runs the agent loop headless against a session to produce the background-job follow-up turn." + }, + { + "path": "src/bg_monitor.py", + "symbol": "_run_followup", + "line_range": "L75-L132", + "explanation": "Drains completed background jobs and auto-continues the owning session, deferring while a live turn is in progress." + }, + { + "path": "src/assistant_log.py", + "symbol": "log_to_assistant", + "line_range": "L34-L48", + "explanation": "Legacy no-op activity logging shim retained for existing callers; documented as inactive rather than as current behaviour." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E1", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "AGENT-005", + "domain": "agent", + "name": "Model Context Protocol (MCP) Server Integration", + "purpose": "Integrates external MCP servers over stdio/SSE to expand agent capabilities dynamically.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/settings.js", + "static/js/admin.js" + ], + "backend_entrypoints": [ + "routes/mcp_routes.py:setup_mcp_routes", + "src/mcp_manager.py:McpManager", + "mcp_servers/" + ], + "routes": [ + "/api/mcp/servers", + "/api/mcp/tools", + "/api/mcp/connect" + ], + "configuration": [ + "MCP_CONFIG_PATH" + ], + "persistence": [ + "DATA_DIR/mcp_config.json" + ], + "dependencies": [ + "mcp", + "asyncio" + ], + "tests": [ + "tests/test_mcp_param_hint_hardening.py", + "tests/cli/test_mcp_cli_json.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Subprocess leaks if external MCP server process fails to terminate clean." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/mcp_routes.py", + "symbol": "setup_mcp_routes", + "line_range": "L60-L240", + "explanation": "Exposes management endpoints for external MCP servers." + }, + { + "path": "src/mcp_manager.py", + "symbol": "McpManager", + "line_range": "L40-L300", + "explanation": "Manages MCP server subprocess lifecycles." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "AGENT-006", + "domain": "agent", + "name": "AI Interaction Tools & Pipeline Orchestration", + "purpose": "Provides specialized AI interaction tools for agent self-debugging, debate, and multi-model collaboration.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/chatStream.js" + ], + "backend_entrypoints": [ + "src/ai_interaction.py", + "src/action_intents.py", + "src/builtin_actions.py" + ], + "routes": [ + "Implicit agent tool calls" + ], + "configuration": [ + "MAX_PIPELINE_TURNS" + ], + "persistence": [ + "Session transcript state" + ], + "dependencies": [ + "asyncio" + ], + "tests": [ + "tests/test_builtin_actions_nonstring.py", + "tests/test_model_interaction_registry.py" + ], + "documentation": [ + "specs/architecture-runtime-inventory.md" + ], + "risks": [ + "High API token consumption during extended agent debates." + ], + "unknowns": [], + "evidence": [ + { + "path": "src/ai_interaction.py", + "symbol": "init_ai_interaction_tools", + "line_range": "L30-L140", + "explanation": "Registers specialized multi-agent interaction primitives." + }, + { + "path": "src/builtin_actions.py", + "symbol": "execute_builtin_action", + "line_range": "L20-L90", + "explanation": "Executes pre-built action intent sequences." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "AGENT-007", + "domain": "agent", + "name": "Subprocess & Background Job Execution Tools", + "purpose": "Provides sandboxed bash/shell tool execution capabilities with output streaming and background tracking.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/chatStream.js", + "static/js/tasks.js" + ], + "backend_entrypoints": [ + "src/agent_tools/subprocess_tools.py", + "src/agent_tools/bg_job_tools.py", + "src/bg_jobs.py" + ], + "routes": [ + "Implicit agent tool calls" + ], + "configuration": [ + "ALLOW_SHELL_EXECUTION", + "SANDBOX_DIR" + ], + "persistence": [ + "DATA_DIR/bg_jobs/" + ], + "dependencies": [ + "subprocess", + "asyncio" + ], + "tests": [ + "tests/test_bg_job_tools.py", + "tests/test_task_shell_tools.py" + ], + "documentation": [ + "THREAT_MODEL.md" + ], + "risks": [ + "Arbitrary shell command execution permissions if sandbox confinement fails." + ], + "unknowns": [], + "evidence": [ + { + "path": "src/agent_tools/subprocess_tools.py", + "symbol": "run_command", + "line_range": "L25-L120", + "explanation": "Executes shell commands in background/foreground." + }, + { + "path": "src/bg_jobs.py", + "symbol": "JobManager", + "line_range": "L30-L150", + "explanation": "Tracks async background subprocess tasks." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "MODEL-001", + "domain": "model", + "name": "Multi-Provider LLM Model Discovery & Metadata Management", + "purpose": "Discovers models from OpenAI, Anthropic, Ollama, vLLM, LMStudio, OpenRouter, and Google AI Studio endpoints.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/models.js", + "static/js/modelPicker.js" + ], + "backend_entrypoints": [ + "routes/model_routes.py:setup_model_routes", + "src/model_discovery.py:ModelDiscovery" + ], + "routes": [ + "/api/models", + "/api/models/active", + "/api/model-endpoints" + ], + "configuration": [ + "OPENAI_API_KEY", + "OLLAMA_BASE_URL", + "ANTHROPIC_API_KEY" + ], + "persistence": [ + "DATA_DIR/model_endpoints.json" + ], + "dependencies": [ + "httpx", + "pydantic" + ], + "tests": [ + "tests/test_model_routes.py", + "tests/test_provider_classification.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Remote endpoint timeouts may slow down full discovery refresh." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/model_routes.py", + "symbol": "@router.get('/api/models')", + "line_range": "L100-L180", + "explanation": "Returns unified list of available models across providers." + }, + { + "path": "src/model_discovery.py", + "symbol": "ModelDiscovery.discover_all", + "line_range": "L45-L210", + "explanation": "Queries connected provider endpoints for available model IDs." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "MODEL-002", + "domain": "model", + "name": "Model Capability & Context Limits Detection", + "purpose": "Detects vision, tool calling, reasoning, and context window limits for connected model endpoints.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/models.js" + ], + "backend_entrypoints": [ + "src/model_capabilities.py", + "src/model_context.py", + "src/endpoint_resolver.py" + ], + "routes": [ + "/api/model-endpoints/{ep_id}/probe" + ], + "configuration": [ + "MODEL_CAPABILITY_OVERRODES" + ], + "persistence": [ + "In-memory capabilities cache" + ], + "dependencies": [ + "pydantic" + ], + "tests": [ + "tests/test_endpoint_resolver_headers.py", + "tests/test_vision_model_detection.py" + ], + "documentation": [ + "specs/architecture-runtime-inventory.md" + ], + "risks": [ + "Incorrect context limit metadata for unlisted custom fine-tunes." + ], + "unknowns": [], + "evidence": [ + { + "path": "src/model_capabilities.py", + "symbol": "get_model_capabilities", + "line_range": "L30-L120", + "explanation": "Maps model names to vision and tool support flags." + }, + { + "path": "src/endpoint_resolver.py", + "symbol": "resolve_endpoint_headers", + "line_range": "L20-L80", + "explanation": "Resolves auth headers and target URLs for model endpoints." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "MODEL-003", + "domain": "model", + "name": "LLM Core Provider Communication & Fallback Routing", + "purpose": "Manages HTTP request dispatching, authorization header injection, and fallback provider routing for LLM calls.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/chatStream.js" + ], + "backend_entrypoints": [ + "src/llm_core.py:llm_call_async", + "src/llm_core.py:llm_call_async_with_fallback", + "src/llm_core.py:stream_llm_with_fallback" + ], + "routes": [ + "Implicit backend calls" + ], + "configuration": [ + "LLM_RETRY_ATTEMPTS", + "LLM_TIMEOUT" + ], + "persistence": [ + "Session state" + ], + "dependencies": [ + "httpx", + "asyncio" + ], + "tests": [ + "tests/test_llm_core_concurrency.py", + "tests/test_llm_core_fallback.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Unexpected API changes in upstream third-party model providers." + ], + "unknowns": [], + "evidence": [ + { + "path": "src/llm_core.py", + "symbol": "llm_call_async", + "line_range": "L1949-L2118", + "explanation": "Non-streaming provider request dispatcher: resolves the endpoint, injects authorization headers and executes the HTTP call." + }, + { + "path": "src/llm_core.py", + "symbol": "llm_call_async_with_fallback", + "line_range": "L1932-L1946", + "explanation": "Ordered fallback wrapper that retries llm_call_async across the configured candidate endpoints." + }, + { + "path": "src/llm_core.py", + "symbol": "stream_llm_with_fallback", + "line_range": "L2794-L2895", + "explanation": "Ordered fallback wrapper for the streaming path; advances to the next candidate when a provider yields an empty completion." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": true, + "status": "pending", + "reason": "Provider dispatch, header injection and fallback advancement are only observable against a reachable LLM provider endpoint; not exercised in this documentation pass." + } + }, + { + "id": "MODEL-004", + "domain": "model", + "name": "Model Selection & Display Ordering Preferences", + "purpose": "Allows pinning, sorting, and hiding specific models in the UI selection dropdown.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/modelSort.js", + "static/js/modelPicker.js" + ], + "backend_entrypoints": [ + "routes/model_routes.py:save_model_order" + ], + "routes": [ + "/api/models/order", + "/api/models/order/reset" + ], + "configuration": [], + "persistence": [ + "DATA_DIR/model_order.json" + ], + "dependencies": [ + "json" + ], + "tests": [ + "tests/test_model_defaults.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Stale model IDs in custom order lists after model endpoints are removed." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/model_routes.py", + "symbol": "@router.post('/order')", + "line_range": "L500-L550", + "explanation": "Saves custom model display order preference." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "MODEL-005", + "domain": "model", + "name": "Side-by-Side Model Comparison (A/B Testing)", + "purpose": "Enables dual-model side-by-side response evaluation, arena scoring, and latency comparison.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/compare/index.js", + "static/js/compare/panes.js" + ], + "backend_entrypoints": [ + "routes/compare/compare_routes.py:setup_compare_routes" + ], + "routes": [ + "/api/compare/start", + "/api/compare/{comp_id}/vote", + "/api/compare/history" + ], + "configuration": [ + "COMPARE_ENABLED" + ], + "persistence": [ + "DATA_DIR/compare_history.db" + ], + "dependencies": [ + "sqlite3" + ], + "tests": [ + "tests/test_endpoint_owner_scope_followup.py" + ], + "documentation": [ + "docs/compare.webm" + ], + "risks": [ + "High memory and network usage when streaming two model responses simultaneously." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/compare/compare_routes.py", + "symbol": "@router.post('/start')", + "line_range": "L70-L150", + "explanation": "Starts a parallel dual-model comparison stream." + }, + { + "path": "static/js/compare/index.js", + "symbol": "initCompareView", + "line_range": "L1-L100", + "explanation": "Renders side-by-side model chat panes." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "MODEL-006", + "domain": "model", + "name": "GitHub Copilot Device Flow Authentication", + "purpose": "Authenticates with GitHub Copilot via OAuth device flow to use Copilot models directly.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/providerDeviceFlow.js" + ], + "backend_entrypoints": [ + "routes/copilot_routes.py:setup_copilot_routes", + "routes/device_flow.py:create_device_flow_router", + "src/copilot.py:request_device_code", + "src/copilot.py:poll_access_token" + ], + "routes": [ + "/api/copilot/device/start", + "/api/copilot/device/poll" + ], + "configuration": [ + "COPILOT_CLIENT_ID" + ], + "persistence": [ + "DATA_DIR/copilot_auth.json" + ], + "dependencies": [ + "httpx" + ], + "tests": [ + "tests/test_provider_device_flow_js.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Token expiration requires manual device re-authentication." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/copilot_routes.py", + "symbol": "setup_copilot_routes", + "line_range": "L166-L173", + "explanation": "Builds the Copilot device-flow router at prefix /api/copilot, wiring _start_device_flow and _poll_device_flow." + }, + { + "path": "routes/device_flow.py", + "symbol": "create_device_flow_router", + "line_range": "L135-L193", + "explanation": "Shared factory registering POST /device/start and POST /device/poll under the caller-supplied prefix." + }, + { + "path": "src/copilot.py", + "symbol": "request_device_code", + "line_range": "L150-L161", + "explanation": "Issues the GitHub device-code request that begins the Copilot OAuth device flow." + }, + { + "path": "src/copilot.py", + "symbol": "poll_access_token", + "line_range": "L164-L180", + "explanation": "Polls GitHub for the access token once the user has authorized the device code." + }, + { + "path": "tests/test_provider_device_flow_js.py", + "symbol": "test_copilot_success_uses_complete_verification_uri", + "line_range": "L28-L65", + "explanation": "Inspected unit test asserting the Copilot device-flow runner surfaces the complete verification URI returned by the backend." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E2", + "runtime_validation": { + "required": true, + "status": "pending", + "reason": "Requires an interactive GitHub Copilot OAuth device-flow account." + } + }, + { + "id": "MODEL-007", + "domain": "model", + "name": "ChatGPT Subscription Device Flow Authentication", + "purpose": "Authenticates with ChatGPT Pro/Plus subscription tokens via device login flow.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/providerDeviceFlow.js" + ], + "backend_entrypoints": [ + "routes/chatgpt_subscription_routes.py:setup_chatgpt_subscription_routes", + "routes/device_flow.py:create_device_flow_router", + "src/chatgpt_subscription.py:request_device_code", + "src/chatgpt_subscription.py:poll_device_auth" + ], + "routes": [ + "/api/chatgpt-subscription/device/start", + "/api/chatgpt-subscription/device/poll" + ], + "configuration": [], + "persistence": [ + "DATA_DIR/chatgpt_auth.json" + ], + "dependencies": [ + "httpx" + ], + "tests": [ + "tests/test_provider_device_flow_js.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Changes in OpenAI auth endpoint security challenges." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/chatgpt_subscription_routes.py", + "symbol": "setup_chatgpt_subscription_routes", + "line_range": "L163-L170", + "explanation": "Builds the ChatGPT subscription device-flow router at prefix /api/chatgpt-subscription." + }, + { + "path": "routes/device_flow.py", + "symbol": "create_device_flow_router", + "line_range": "L135-L193", + "explanation": "Shared factory registering POST /device/start and POST /device/poll under the caller-supplied prefix." + }, + { + "path": "src/chatgpt_subscription.py", + "symbol": "request_device_code", + "line_range": "L168-L181", + "explanation": "Issues the ChatGPT device-authorization request that begins the subscription OAuth device flow." + }, + { + "path": "src/chatgpt_subscription.py", + "symbol": "poll_device_auth", + "line_range": "L184-L193", + "explanation": "Polls the ChatGPT device-authorization endpoint for completion using the stored device_auth_id and user_code." + }, + { + "path": "tests/test_provider_device_flow_js.py", + "symbol": "test_chatgpt_success_uses_plain_verification_uri", + "line_range": "L68-L95", + "explanation": "Inspected unit test asserting the ChatGPT device-flow runner uses the plain verification URI rather than the Copilot complete-URI form." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E2", + "runtime_validation": { + "required": true, + "status": "pending", + "reason": "Requires an interactive ChatGPT subscription OAuth flow." + } + }, + { + "id": "MODEL-008", + "domain": "model", + "name": "Embedding Model Lane & Vector Provider Setup", + "purpose": "Configures local sentence-transformers, FastEmbed, or remote OpenAI embedding model lanes.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/settings.js" + ], + "backend_entrypoints": [ + "routes/embedding_routes.py:setup_embedding_routes", + "src/embeddings.py", + "src/embedding_lanes.py" + ], + "routes": [ + "/api/embeddings/active", + "/api/embeddings/test" + ], + "configuration": [ + "EMBEDDING_PROVIDER", + "EMBEDDING_MODEL" + ], + "persistence": [ + "DATA_DIR/embeddings_config.json" + ], + "dependencies": [ + "fastembed", + "sentence-transformers" + ], + "tests": [ + "tests/test_embedding_lane_ndarray_restore.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "First-time download of heavy PyTorch model weights on CPU-only machines." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/embedding_routes.py", + "symbol": "setup_embedding_routes", + "line_range": "L30-L120", + "explanation": "Provides embedding provider configuration endpoints." + }, + { + "path": "src/embeddings.py", + "symbol": "EmbeddingManager", + "line_range": "L40-L200", + "explanation": "Generates dense vector embeddings for RAG and memory." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "COOKBOOK-001", + "domain": "cookbook", + "name": "Local Model Download & Recipe Lifecycle Management", + "purpose": "Downloads HuggingFace models, configures execution parameters, and manages local GGUF/MLX model servers.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/cookbook.js", + "static/js/cookbookServe.js", + "static/js/cookbookDownload.js" + ], + "backend_entrypoints": [ + "routes/cookbook_routes.py:setup_cookbook_routes", + "src/tools/cookbook.py", + "src/cookbook_serve_lifecycle.py" + ], + "routes": [ + "/api/cookbook/download", + "/api/cookbook/serve", + "/api/cookbook/status" + ], + "configuration": [ + "COOKBOOK_MODELS_DIR" + ], + "persistence": [ + "DATA_DIR/models/" + ], + "dependencies": [ + "huggingface_hub", + "subprocess" + ], + "tests": [ + "tests/test_cookbook_endpoint_registration.py", + "tests/test_cookbook_port_parsing_js.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Disk space exhaustion during multi-gigabyte GGUF weights downloads." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/cookbook_routes.py", + "symbol": "setup_cookbook_routes", + "line_range": "L100-L300", + "explanation": "Exposes model downloading and process serving endpoints." + }, + { + "path": "static/js/cookbook.js", + "symbol": "initCookbook", + "line_range": "L1-L150", + "explanation": "UI manager for local model library." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "COOKBOOK-002", + "domain": "cookbook", + "name": "Hardware Model Fitting ('What Fits?') Analysis Engine", + "purpose": "Calculates RAM/VRAM requirements, quantized size, and context overhead to determine model compatibility.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/cookbook-hwfit.js" + ], + "backend_entrypoints": [ + "routes/hwfit_routes.py:setup_hwfit_routes", + "services/hwfit/fit.py:calculate_fit" + ], + "routes": [ + "/api/hwfit/fit", + "/api/hwfit/profile" + ], + "configuration": [ + "FORCE_GPU_VENDOR" + ], + "persistence": [ + "System hardware specs" + ], + "dependencies": [ + "psutil", + "torch" + ], + "tests": [ + "tests/test_hwfit_gemma4_12b.py", + "tests/test_hwfit_bandwidth_nonstring.py", + "tests/test_hwfit_gpu_count_nonnumeric.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Inaccurate VRAM estimation for non-standard KV-cache quantization." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/hwfit_routes.py", + "symbol": "setup_hwfit_routes", + "line_range": "L40-L120", + "explanation": "Calculates hardware model compatibility." + }, + { + "path": "services/hwfit/fit.py", + "symbol": "calculate_fit", + "line_range": "L30-L140", + "explanation": "Performs parameter and memory fit calculations." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "COOKBOOK-003", + "domain": "cookbook", + "name": "HuggingFace & MLX Model Discovery Services", + "purpose": "Searches HuggingFace Hub and local MLX model repositories for compatible GGUF and MLX weights.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/cookbook.js" + ], + "backend_entrypoints": [ + "routes/hwfit_routes.py:hf_search", + "services/hwfit/hf_discovery.py" + ], + "routes": [ + "/api/hwfit/hf-search", + "/api/hwfit/mlx-models" + ], + "configuration": [ + "HF_TOKEN" + ], + "persistence": [ + "Cache directory" + ], + "dependencies": [ + "huggingface_hub" + ], + "tests": [ + "tests/test_cookbook_hf_token.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "HuggingFace API rate limits when searching without an API token." + ], + "unknowns": [], + "evidence": [ + { + "path": "services/hwfit/hf_discovery.py", + "symbol": "search_hf_models", + "line_range": "L20-L90", + "explanation": "Queries HuggingFace API for model tags and files." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "COOKBOOK-004", + "domain": "cookbook", + "name": "Host Docker Access for Inference Container Runtimes", + "purpose": "Detects and connects to host Docker engine to launch containerized Ollama, vLLM, or SGLang runtimes.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/cookbookServe.js" + ], + "backend_entrypoints": [ + "src/host_docker_access.py:HostDockerAccess" + ], + "routes": [ + "Implicit local docker socket API calls" + ], + "configuration": [ + "DOCKER_HOST" + ], + "persistence": [ + "/var/run/docker.sock" + ], + "dependencies": [ + "docker" + ], + "tests": [ + "tests/test_cookbook_docker_access.py" + ], + "documentation": [ + "docker/host-docker.yml" + ], + "risks": [ + "Permission denied accessing docker socket on non-root setups." + ], + "unknowns": [], + "evidence": [ + { + "path": "src/host_docker_access.py", + "symbol": "HostDockerAccess", + "line_range": "L50-L62", + "explanation": "Interacts with host docker daemon." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": true, + "status": "pending", + "reason": "Requires Docker access and supported physical GPU hardware." + } + }, + { + "id": "RESEARCH-001", + "domain": "research", + "name": "Deep Research Execution Engine & SSE Progress Streaming", + "purpose": "Executes multi-step recursive deep research tasks, web page scraping, synthesis, and streams live progress.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/research/panel.js", + "static/js/research/jobs.js", + "static/js/researchSynapse.js" + ], + "backend_entrypoints": [ + "routes/research/research_routes.py:start_research", + "src/deep_research.py", + "services/research/service.py" + ], + "routes": [ + "/api/research/start", + "/api/research/stream/{session_id}", + "/api/research/active" + ], + "configuration": [ + "MAX_RESEARCH_DEPTH", + "SEARXNG_URL" + ], + "persistence": [ + "DATA_DIR/research/" + ], + "dependencies": [ + "beautifulsoup4", + "httpx", + "asyncio" + ], + "tests": [ + "tests/test_research_owner_scope_routes.py", + "tests/test_services_research_low_quality_sources.py" + ], + "documentation": [ + "docs/research.webm" + ], + "risks": [ + "High memory consumption when parsing multi-megabyte HTML target pages." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/research/research_routes.py", + "symbol": "@router.post('/api/research/start')", + "line_range": "L492-L550", + "explanation": "Initiates deep research job." + }, + { + "path": "src/deep_research.py", + "symbol": "DeepResearchEngine", + "line_range": "L40-L300", + "explanation": "Recursive search and summary crawler." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "RESEARCH-002", + "domain": "research", + "name": "Research Library, Detail View & Image Controls", + "purpose": "Stores completed research reports, generated diagrams, reference links, and manages image visibility.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/research/panel.js" + ], + "backend_entrypoints": [ + "routes/research/research_routes.py:get_library" + ], + "routes": [ + "/api/research/library", + "/api/research/detail/{session_id}", + "/api/research/{session_id}/hide-image" + ], + "configuration": [], + "persistence": [ + "DATA_DIR/research/library.json" + ], + "dependencies": [ + "pydantic" + ], + "tests": [ + "tests/cli/test_research_cli_preview.py", + "tests/test_research_routes_path_confinement.py" + ], + "documentation": [ + "docs/research.webm" + ], + "risks": [ + "Orphaned report files if storage directory is modified out-of-band." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/research/research_routes.py", + "symbol": "@router.get('/api/research/library')", + "line_range": "L366-L415", + "explanation": "Returns all saved research reports." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "RESEARCH-003", + "domain": "research", + "name": "Web Search Engine Integration (SearXNG & Multi-Provider)", + "purpose": "Queries SearXNG, DuckDuckGo, or Google Search instances to retrieve web search snippets.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/chat.js", + "static/js/search.js" + ], + "backend_entrypoints": [ + "routes/search_routes.py:setup_search_routes", + "src/search/core.py", + "services/search/service.py" + ], + "routes": [ + "/api/search" + ], + "configuration": [ + "SEARXNG_URL", + "SEARCH_PROVIDER" + ], + "persistence": [ + "In-memory search cache" + ], + "dependencies": [ + "httpx" + ], + "tests": [ + "tests/test_search_ranking.py", + "tests/test_searxng_image_pinned.py" + ], + "documentation": [ + "config/searxng/settings.yml" + ], + "risks": [ + "Search provider IP throttling or rate-limiting." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/search_routes.py", + "symbol": "setup_search_routes", + "line_range": "L39-L100", + "explanation": "Defines /api/search, /api/search/config, and /api/search/query endpoints." + }, + { + "path": "src/search/core.py", + "symbol": "SearchEngine", + "line_range": "L1-L12", + "explanation": "Compatibility module aliasing services.search.core." + }, + { + "path": "tests/test_search_ranking.py", + "symbol": "test_news_queries_prefer_news_sources_over_sports_and_social_results", + "line_range": "L1-L39", + "explanation": "Tests search result domain ranking and scoring." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E1", + "runtime_validation": { + "required": true, + "status": "pending", + "reason": "Requires an active SearXNG instance or external search API provider." + } + }, + { + "id": "RESEARCH-004", + "domain": "research", + "name": "Research Result Peeking & Topic Spinoff Generation", + "purpose": "Extracts preliminary research snippets and spawns child research sessions focused on specific sub-topics.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/research/panel.js" + ], + "backend_entrypoints": [ + "routes/research/research_routes.py:peek_result", + "routes/research/research_routes.py:spinoff_research" + ], + "routes": [ + "/api/research/result-peek/{session_id}", + "/api/research/spinoff/{session_id}" + ], + "configuration": [], + "persistence": [ + "DATA_DIR/research/" + ], + "dependencies": [ + "fastapi" + ], + "tests": [ + "tests/test_research_routes_path_confinement.py" + ], + "documentation": [ + "docs/research.webm" + ], + "risks": [ + "Deep recursion tree depth when spawning multiple nested spinoffs." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/research/research_routes.py", + "symbol": "@router.post('/api/research/spinoff/{session_id}')", + "line_range": "L635-L680", + "explanation": "Spawns child research session for specific query." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "DOCUMENT-001", + "domain": "document", + "name": "Document & Canvas Artifact Management", + "purpose": "Renders dynamic canvas documents, handles live editing, markdown preview, and side-by-side artifact display.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/document.js", + "static/js/documentLibrary.js" + ], + "backend_entrypoints": [ + "routes/document_routes.py:setup_document_routes", + "src/document_actions.py", + "src/document_processor.py" + ], + "routes": [ + "/api/document", + "/api/document/{id}" + ], + "configuration": [ + "MAX_DOCUMENT_SIZE_MB" + ], + "persistence": [ + "DATA_DIR/documents/" + ], + "dependencies": [ + "pydantic", + "fastapi" + ], + "tests": [ + "tests/test_document_actions_nonstring.py", + "tests/test_document_diff_discard_on_update_js.py" + ], + "documentation": [ + "docs/document.webm" + ], + "risks": [ + "Concurrent edits on the same document artifact." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/document_routes.py", + "symbol": "setup_document_routes", + "line_range": "L100-L300", + "explanation": "Registers document artifact CRUD routes." + }, + { + "path": "static/js/document.js", + "symbol": "initDocumentView", + "line_range": "L1-L150", + "explanation": "Renders interactive canvas document panel." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "DOCUMENT-002", + "domain": "document", + "name": "PDF Form Processing & High-Fidelity Rendering", + "purpose": "Extracts form fields from PDF files, fills dynamic values, and generates PDF previews.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/document.js" + ], + "backend_entrypoints": [ + "src/pdf_runtime.py", + "src/pdf_forms.py", + "src/pdf_form_doc.py" + ], + "routes": [ + "Implicit PDF document processing" + ], + "configuration": [], + "persistence": [ + "DATA_DIR/documents/pdf/" + ], + "dependencies": [ + "pypdf", + "fitz" + ], + "tests": [ + "tests/test_document_pdf_marker.py", + "tests/test_security_headers_pdf_preview.py" + ], + "documentation": [ + "docs/attachments.md" + ], + "risks": [ + "Complex XFA PDF forms may not extract cleanly with standard pdf parsers." + ], + "unknowns": [], + "evidence": [ + { + "path": "src/pdf_runtime.py", + "symbol": "load_pymupdf_for_pdf_viewer", + "line_range": "L9-L15", + "explanation": "Loads optional PyMuPDF runtime for PDF viewing." + }, + { + "path": "src/pdf_forms.py", + "symbol": "extract_form_fields", + "line_range": "L1-L100", + "explanation": "Handles PDF form field extraction and filling." + }, + { + "path": "tests/test_document_pdf_marker.py", + "symbol": "test_marker_removed_without_eating_following_text", + "line_range": "L1-L30", + "explanation": "Tests PDF text extraction wrapper stripping without content corruption." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E1", + "runtime_validation": { + "required": true, + "status": "pending", + "reason": "Requires optional PyMuPDF (`fitz`) or pypdf runtime dependency." + } + }, + { + "id": "DOCUMENT-003", + "domain": "document", + "name": "Personal Document Indexing & RAG Retrieval", + "purpose": "Indexes local user documents (PDF, DOCX, TXT) into ChromaDB for semantic vector retrieval.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/rag.js" + ], + "backend_entrypoints": [ + "routes/personal_routes.py:setup_personal_routes", + "src/personal_docs.py", + "src/rag_manager.py" + ], + "routes": [ + "/api/personal/documents", + "/api/personal/search", + "/api/personal/index" + ], + "configuration": [ + "CHROMADB_DIR" + ], + "persistence": [ + "DATA_DIR/chroma/" + ], + "dependencies": [ + "chromadb", + "pypdf", + "docx" + ], + "tests": [ + "tests/test_personal_docs_pdf_index.py", + "tests/test_rag_index_hidden_dirs.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Slow vector embedding indexing step for massive multi-thousand page documents." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/personal_routes.py", + "symbol": "setup_personal_routes", + "line_range": "L50-L160", + "explanation": "Personal document RAG indexing and search API endpoints." + }, + { + "path": "src/personal_docs.py", + "symbol": "PersonalDocsManager", + "line_range": "L30-L180", + "explanation": "Handles file text chunking and vector storage." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "DOCUMENT-004", + "domain": "document", + "name": "Document Conversion & Text Extraction Engine", + "purpose": "Converts office formats (.docx, .xlsx, .pptx) and HTML into clean Markdown text representations.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/fileHandler.js" + ], + "backend_entrypoints": [ + "src/markitdown_runtime.py", + "src/office_doc.py" + ], + "routes": [ + "Implicit file conversion calls" + ], + "configuration": [], + "persistence": [ + "Temporary conversion cache" + ], + "dependencies": [ + "markitdown", + "python-docx" + ], + "tests": [ + "tests/cli/test_docs_cli_content_length.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Formatting loss when parsing legacy binary doc/xls files." + ], + "unknowns": [], + "evidence": [ + { + "path": "src/markitdown_runtime.py", + "symbol": "convert_to_markdown", + "line_range": "L15-L80", + "explanation": "Converts binary office documents into structured Markdown text." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "DOCUMENT-005", + "domain": "document", + "name": "Document Library UI Navigation", + "purpose": "Provides dedicated UI view for browsing, filtering, and organizing saved user documents.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/documentLibrary.js" + ], + "backend_entrypoints": [ + "app.py:serve_library" + ], + "routes": [ + "/library" + ], + "configuration": [], + "persistence": [ + "DATA_DIR/documents/" + ], + "dependencies": [ + "Vanilla JS" + ], + "tests": [ + "tests/test_document_close_clears_active_route.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Large folder trees may cause initial DOM render slowdown." + ], + "unknowns": [], + "evidence": [ + { + "path": "static/js/documentLibrary.js", + "symbol": "initDocumentLibrary", + "line_range": "L1-L100", + "explanation": "Renders document library navigation grid." + }, + { + "path": "app.py", + "symbol": "serve_library", + "line_range": "L911-L914", + "explanation": "Serves SPA shell for /library route." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "EMAIL-001", + "domain": "email", + "name": "Email Account Setup, IMAP/SMTP Connection & Polling", + "purpose": "Configures IMAP/SMTP email accounts, validates TLS certificates, and polls background inbox updates.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/emailInbox.js" + ], + "backend_entrypoints": [ + "routes/email_routes.py:setup_email_routes", + "routes/email_pollers.py" + ], + "routes": [ + "/api/email/accounts", + "/api/email/poll" + ], + "configuration": [ + "EMAIL_POLL_INTERVAL" + ], + "persistence": [ + "DATA_DIR/email_accounts.json" + ], + "dependencies": [ + "imaplib", + "smtplib" + ], + "tests": [ + "tests/test_service_health_email.py", + "tests/test_active_email_reply_guard.py" + ], + "documentation": [ + "docs/email-outlook.md" + ], + "risks": [ + "Account lockouts if bad credentials are repeatedly polled." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/email_routes.py", + "symbol": "setup_email_routes", + "line_range": "L1453-L1500", + "explanation": "Sets up email account management and synchronization routes." + }, + { + "path": "routes/email_pollers.py", + "symbol": "_start_poller", + "line_range": "L1-L100", + "explanation": "Background poller for email inbox synchronization." + }, + { + "path": "tests/test_service_health_email.py", + "symbol": "test_email_ok_all_connect", + "line_range": "L1-L80", + "explanation": "Tests IMAP connection health probing and status reporting." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E1", + "runtime_validation": { + "required": true, + "status": "pending", + "reason": "Requires a controlled live IMAP account and network access." + } + }, + { + "id": "EMAIL-002", + "domain": "email", + "name": "Email Searching, Threading & Message Operations", + "purpose": "Parses email headers, folds signatures, groups messages into threads, and executes full-text email search.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/emailLibrary.js", + "static/js/emailLibrary/state.js" + ], + "backend_entrypoints": [ + "routes/email_routes.py:search_email", + "src/email_thread_parser.py" + ], + "routes": [ + "/api/email/search", + "/api/email/threads", + "/api/email/messages" + ], + "configuration": [], + "persistence": [ + "DATA_DIR/email_cache.db" + ], + "dependencies": [ + "sqlite3", + "email" + ], + "tests": [ + "tests/test_reply_recipients_js.py", + "tests/test_signature_fold_js.py" + ], + "documentation": [ + "docs/email-outlook.md" + ], + "risks": [ + "Malformed MIME email structures failing HTML sanitization." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/email_routes.py", + "symbol": "@router.get('/search')", + "line_range": "L250-L320", + "explanation": "Executes search across cached email headers and text." + }, + { + "path": "src/email_thread_parser.py", + "symbol": "parse_email_thread", + "line_range": "L20-L100", + "explanation": "Builds conversation tree from Message-ID and In-Reply-To headers." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "EMAIL-003", + "domain": "email", + "name": "Email Composition, Draft Management & Sending", + "purpose": "Creates, saves, and dispatches HTML/plaintext email messages via SMTP.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/emailShared.js" + ], + "backend_entrypoints": [ + "routes/email_routes.py:send_email" + ], + "routes": [ + "/api/email/draft", + "/api/email/send" + ], + "configuration": [], + "persistence": [ + "DATA_DIR/email_drafts.json" + ], + "dependencies": [ + "smtplib" + ], + "tests": [ + "tests/test_schedule_email_offset_normalization.py" + ], + "documentation": [ + "docs/email-outlook.md" + ], + "risks": [ + "SMTP connection drop mid-send causing unsent mail state." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/email_routes.py", + "symbol": "@router.post('/send')", + "line_range": "L450-L520", + "explanation": "Sends email message via user SMTP credentials." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": true, + "status": "pending", + "reason": "Requires a controlled live SMTP account and network access." + } + }, + { + "id": "EMAIL-004", + "domain": "email", + "name": "Email MCP Server & Codex Integration Bridge", + "purpose": "Exposes constrained email reading and draft capabilities to external Codex / MCP agents with scope checks.", + "status": "verified", + "frontend_entrypoints": [ + "integrations/codex/scripts/odysseus_api.py" + ], + "backend_entrypoints": [ + "mcp_servers/email_server.py", + "routes/codex_routes.py:setup_codex_routes" + ], + "routes": [ + "/api/codex/email/search", + "/api/codex/email/send" + ], + "configuration": [ + "CODEX_API_KEY" + ], + "persistence": [ + "API token scopes" + ], + "dependencies": [ + "mcp", + "fastapi" + ], + "tests": [ + "tests/cli/test_mail_cli_recipients.py" + ], + "documentation": [ + "integrations/codex/README.md" + ], + "risks": [ + "Unauthorized mail sending if token scopes are improperly scoped." + ], + "unknowns": [], + "evidence": [ + { + "path": "mcp_servers/email_server.py", + "symbol": "EmailMcpServer", + "line_range": "L20-L120", + "explanation": "MCP server exposing email tools over stdio/SSE." + }, + { + "path": "routes/codex_routes.py", + "symbol": "setup_codex_routes", + "line_range": "L30-L110", + "explanation": "Bridge endpoints for external Codex plugin integration." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "CALENDAR-001", + "domain": "calendar", + "name": "CalDAV Calendar Synchronization & Account Setup", + "purpose": "Connects to remote CalDAV servers (Apple iCloud, Nextcloud, Google) to sync calendar event feeds.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/calendar.js" + ], + "backend_entrypoints": [ + "routes/calendar_routes.py:setup_calendar_routes", + "src/caldav_sync.py:CalDavSync" + ], + "routes": [ + "/api/calendar/config", + "/api/calendar/sync" + ], + "configuration": [ + "CALDAV_URL", + "CALDAV_USERNAME" + ], + "persistence": [ + "DATA_DIR/calendar_config.json" + ], + "dependencies": [ + "caldav", + "vobject" + ], + "tests": [ + "tests/test_caldav_url_hardening.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Invalid SSL certificates on self-hosted CalDAV servers." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/calendar_routes.py", + "symbol": "setup_calendar_routes", + "line_range": "L50-L180", + "explanation": "Exposes CalDAV setup and manual sync trigger routes." + }, + { + "path": "src/caldav_sync.py", + "symbol": "CalDavSync", + "line_range": "L30-L160", + "explanation": "Fetches and parses remote iCalendar VEVENT objects." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": true, + "status": "pending", + "reason": "Requires a controlled external CalDAV server." + } + }, + { + "id": "CALENDAR-002", + "domain": "calendar", + "name": "Calendar Event Operations & iCalendar Parsing", + "purpose": "Creates, updates, deletes, and displays calendar events with timezone conversion and reminder notifications.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/calendar/reminders.js", + "static/js/calendar/utils.js" + ], + "backend_entrypoints": [ + "routes/calendar_routes.py:create_event", + "src/caldav_writeback.py", + "src/tools/calendar.py" + ], + "routes": [ + "/api/calendar/events", + "/api/calendar/events/{event_id}" + ], + "configuration": [ + "USER_TIMEZONE" + ], + "persistence": [ + "DATA_DIR/calendar_events.db" + ], + "dependencies": [ + "sqlite3", + "icalendar" + ], + "tests": [ + "tests/test_calendar_parse_dt_time_first.py", + "tests/test_calendar_update_event_tz.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Recurring RRULE event expansion calculation bugs across leap years." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/calendar_routes.py", + "symbol": "@router.get('/events')", + "line_range": "L220-L310", + "explanation": "Fetches calendar events for requested date window." + }, + { + "path": "src/tools/calendar.py", + "symbol": "CalendarTool", + "line_range": "L25-L120", + "explanation": "Agent tool for creating and modifying calendar entries." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "MEDIA-001", + "domain": "media", + "name": "Gallery Image Library & Album Operations", + "purpose": "Organizes images into custom albums, provides grid browsing, tagging, and album metadata management.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/gallery.js" + ], + "backend_entrypoints": [ + "routes/gallery/gallery_routes.py:setup_gallery_routes" + ], + "routes": [ + "/api/gallery/library", + "/api/gallery/albums", + "/api/gallery/upload" + ], + "configuration": [ + "GALLERY_STORAGE_DIR" + ], + "persistence": [ + "DATA_DIR/gallery/" + ], + "dependencies": [ + "pillow", + "sqlite3" + ], + "tests": [ + "tests/test_gallery_image_endpoint_owner_scope.py", + "tests/test_gallery_owner_filter_single_user.py" + ], + "documentation": [ + "docs/gallery.webm" + ], + "risks": [ + "Thumbnail generation overhead for high-resolution RAW camera images." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/gallery/gallery_routes.py", + "symbol": "@router.get('/api/gallery/library')", + "line_range": "L657-L720", + "explanation": "Fetches image library list with tag filters." + }, + { + "path": "static/js/gallery.js", + "symbol": "initGallery", + "line_range": "L1-L180", + "explanation": "Main gallery grid renderer and uploader." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "MEDIA-002", + "domain": "media", + "name": "Image Processing, AI Upscaling & Style Transfer", + "purpose": "Executes local image enhancement, background removal, face sharpening, and AI upscaling operations.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/galleryEditor.js", + "static/js/editor/ai-tool-runner.js" + ], + "backend_entrypoints": [ + "routes/gallery/gallery_routes.py:ai_upscale", + "routes/gallery/gallery_routes.py:remove_bg" + ], + "routes": [ + "/api/gallery/ai-upscale", + "/api/gallery/style-transfer", + "/api/image/inpaint", + "/api/image/remove-bg" + ], + "configuration": [ + "REALESRGAN_MODEL_PATH" + ], + "persistence": [ + "DATA_DIR/gallery/processed/" + ], + "dependencies": [ + "onnxruntime", + "opencv-python" + ], + "tests": [ + "tests/test_sanitize_multimodal_merge.py" + ], + "documentation": [ + "docs/gallery.webm" + ], + "risks": [ + "High GPU memory allocation when upscaling 4K images." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/gallery/gallery_routes.py", + "symbol": "@router.post('/api/gallery/ai-upscale')", + "line_range": "L544-L580", + "explanation": "Runs RealESRGAN image upscaling." + }, + { + "path": "routes/gallery/gallery_routes.py", + "symbol": "@router.post('/api/image/remove-bg')", + "line_range": "L1950-L2010", + "explanation": "Executes background removal pass." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "MEDIA-003", + "domain": "media", + "name": "Interactive Image Canvas Editor & Persisted Drafts", + "purpose": "Provides full multi-layer raster canvas editor, brush tools, transforms, masks, and draft project persistence.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/galleryEditor.js", + "static/js/editor/history-panel.js" + ], + "backend_entrypoints": [ + "routes/editor_draft_routes.py:setup_editor_draft_routes" + ], + "routes": [ + "/api/editor/drafts", + "/api/editor/drafts/{draft_id}" + ], + "configuration": [ + "MAX_DRAFT_PROJECTS" + ], + "persistence": [ + "DATA_DIR/editor_drafts/" + ], + "dependencies": [ + "HTML5 Canvas API", + "pydantic" + ], + "tests": [ + "tests/test_canvas_coords_empty_touches_js.py", + "tests/test_snap_other_layers_nonarray_js.py" + ], + "documentation": [ + "docs/gallery.webm" + ], + "risks": [ + "Browser memory leak if multi-gigabyte layer undo buffers are kept indefinitely." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/editor_draft_routes.py", + "symbol": "setup_editor_draft_routes", + "line_range": "L30-L110", + "explanation": "API routes for saving and loading canvas project drafts." + }, + { + "path": "static/js/editor/history-panel.js", + "symbol": "HistoryManager", + "line_range": "L1-L90", + "explanation": "Canvas undo/redo stack manager." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "MEDIA-004", + "domain": "media", + "name": "Text-to-Speech (TTS) Synthesis Service", + "purpose": "Synthesizes spoken audio from text using local Kokoro, EdgeTTS, or OpenAI TTS engines.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/tts-ai.js" + ], + "backend_entrypoints": [ + "routes/tts_routes.py:setup_tts_routes", + "services/tts/tts_service.py:TTSService" + ], + "routes": [ + "/api/tts/synthesize", + "/api/tts/stats" + ], + "configuration": [ + "TTS_PROVIDER", + "TTS_VOICE" + ], + "persistence": [ + "DATA_DIR/tts_cache/" + ], + "dependencies": [ + "soundfile", + "edge-tts" + ], + "tests": [ + "tests/test_tts_available_nonstring_provider.py", + "tests/test_tts_cache_stats.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Audio synthesis latency on CPU-only hardware setups." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/tts_routes.py", + "symbol": "@router.post('/synthesize')", + "line_range": "L30-L75", + "explanation": "Synthesizes TTS audio clip." + }, + { + "path": "services/tts/tts_service.py", + "symbol": "TTSService", + "line_range": "L25-L140", + "explanation": "Provider abstraction layer for audio speech generation." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "MEDIA-005", + "domain": "media", + "name": "Speech-to-Text (STT) Audio Transcription Service", + "purpose": "Transcribes user audio recordings into text using faster-whisper or local speech models.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/voiceRecorder.js" + ], + "backend_entrypoints": [ + "routes/stt_routes.py:setup_stt_routes", + "services/stt/stt_service.py:STTService" + ], + "routes": [ + "/api/stt/transcribe", + "/api/stt/stats" + ], + "configuration": [ + "STT_PROVIDER", + "WHISPER_MODEL_SIZE" + ], + "persistence": [ + "Temporary audio buffer" + ], + "dependencies": [ + "faster-whisper", + "ffmpeg-python" + ], + "tests": [ + "tests/test_stt_leak.py", + "tests/test_speech_service_toggles.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Missing ffmpeg system dependency prevents audio format decoding." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/stt_routes.py", + "symbol": "@router.post('/transcribe')", + "line_range": "L25-L55", + "explanation": "Accepts multipart audio file and returns transcription text." + }, + { + "path": "services/stt/stt_service.py", + "symbol": "STTService", + "line_range": "L20-L110", + "explanation": "Whisper audio transcription engine wrapper." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "MEDIA-006", + "domain": "media", + "name": "Digital Signature Stamp Storage & Placement", + "purpose": "Stores transparent PNG user signatures and stamps for placement onto PDF forms and documents.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/signature.js" + ], + "backend_entrypoints": [ + "routes/signature_routes.py:setup_signature_routes" + ], + "routes": [ + "/api/signatures", + "/api/signatures/{sig_id}" + ], + "configuration": [], + "persistence": [ + "DATA_DIR/signatures/" + ], + "dependencies": [ + "pillow" + ], + "tests": [ + "tests/test_signature_route_hardening.py", + "tests/test_signature_settings_dom_xss.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Cross-site scripting if signature image titles contain unescaped user input." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/signature_routes.py", + "symbol": "setup_signature_routes", + "line_range": "L30-L120", + "explanation": "CRUD endpoints for managing user signature PNG stamps." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "MEDIA-007", + "domain": "media", + "name": "Generated Image Artifact Route & MCP Integration", + "purpose": "Serves generated AI artwork artifacts and integrates with image generation MCP server.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/chat.js" + ], + "backend_entrypoints": [ + "app.py:serve_generated_image", + "src/generated_images.py", + "mcp_servers/image_gen_server.py" + ], + "routes": [ + "/api/generated-image/{filename}" + ], + "configuration": [], + "persistence": [ + "DATA_DIR/generated_images/" + ], + "dependencies": [ + "pillow", + "mcp" + ], + "tests": [ + "tests/test_image_models_nondict_system.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Path traversal vulnerability if filename parameter is un-sanitized." + ], + "unknowns": [], + "evidence": [ + { + "path": "app.py", + "symbol": "serve_generated_image", + "line_range": "L499-L520", + "explanation": "Serves generated image artifacts with cache headers." + }, + { + "path": "src/generated_images.py", + "symbol": "resolve_generated_image_path", + "line_range": "L20-L32", + "explanation": "Confines requested image path within artifacts directory." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "MEDIA-008", + "domain": "media", + "name": "Native MLX Image Bridge (macOS Apple Silicon)", + "purpose": "Native Apple Swift bridge for hardware-accelerated diffusion and MLX image colorization on macOS.", + "status": "experimental", + "frontend_entrypoints": [ + "static/js/editor/ai-tools-misc.js" + ], + "backend_entrypoints": [ + "swift/odysseus-mlx-image-bridge/Package.swift", + "scripts/mlx_image_server.py", + "scripts/diffusion_server.py" + ], + "routes": [ + "/api/image/mlx-colorize" + ], + "configuration": [ + "ENABLE_MLX_ACCELERATION" + ], + "persistence": [ + "Build artifact binary" + ], + "dependencies": [ + "Swift", + "MLX Framework" + ], + "tests": [ + "tests/helpers/import_state.py" + ], + "documentation": [ + "swift/odysseus-mlx-image-bridge/Package.swift" + ], + "risks": [ + "Requires macOS host with Apple Silicon M-series chip and compiled Swift binary." + ], + "unknowns": [ + "Binary build requires Xcode command line tools build step (`build-macos-app.sh`)." + ], + "evidence": [ + { + "path": "swift/odysseus-mlx-image-bridge/Package.swift", + "symbol": "Package", + "line_range": "L1-L30", + "explanation": "Swift package manifest for native MLX image bridge." + }, + { + "path": "scripts/mlx_image_server.py", + "symbol": "main", + "line_range": "L20-L90", + "explanation": "Python daemon wrapping native Swift MLX binary." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": true, + "status": "pending", + "reason": "Requires Apple Silicon, macOS tooling, and the compiled MLX bridge." + } + }, + { + "id": "SECURITY-001", + "domain": "security", + "name": "Authentication, Session Cookies & User Management", + "purpose": "Handles bcrypt password hashing, session cookie issuance, authentication enforcement, and user administration.", + "status": "verified", + "frontend_entrypoints": [ + "static/login.html", + "static/js/admin.js" + ], + "backend_entrypoints": [ + "routes/auth_routes.py:setup_auth_routes", + "core/auth.py:AuthManager" + ], + "routes": [ + "/api/auth/login", + "/api/auth/me", + "/api/auth/users", + "/api/auth/logout" + ], + "configuration": [ + "AUTH_ENABLED", + "SESSION_COOKIE_NAME" + ], + "persistence": [ + "DATA_DIR/auth.db" + ], + "dependencies": [ + "bcrypt", + "itsdangerous" + ], + "tests": [ + "tests/test_rename_user_owner_sync.py", + "tests/test_reserved_username_admin_escalation.py" + ], + "documentation": [ + "THREAT_MODEL.md", + "SECURITY.md" + ], + "risks": [ + "Cookie session hijack if deployed over unencrypted HTTP without HTTPS cookie flags." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/auth_routes.py", + "symbol": "@router.post('/login')", + "line_range": "L100-L180", + "explanation": "Authenticates credentials and sets session cookie." + }, + { + "path": "core/auth.py", + "symbol": "AuthManager", + "line_range": "L40-L260", + "explanation": "Handles user creation, password verification, and session tokens." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "SECURITY-002", + "domain": "security", + "name": "System Vault Encrypted Secret Storage", + "purpose": "Encrypts API keys, passphrases, and third-party secrets on disk using AES-GCM / PBKDF2 key derivation.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/settings.js" + ], + "backend_entrypoints": [ + "routes/vault_routes.py:setup_vault_routes", + "src/secret_storage.py:SecretVault", + "src/tools/vault.py" + ], + "routes": [ + "/api/vault/config", + "/api/vault/unlock", + "/api/vault/lock" + ], + "configuration": [ + "VAULT_SALT" + ], + "persistence": [ + "DATA_DIR/vault.enc" + ], + "dependencies": [ + "cryptography" + ], + "tests": [ + "tests/test_vault_password_not_in_argv.py" + ], + "documentation": [ + "THREAT_MODEL.md" + ], + "risks": [ + "Loss of vault master passphrase renders all encrypted secrets permanently unrecoverable." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/vault_routes.py", + "symbol": "setup_vault_routes", + "line_range": "L126-L229", + "explanation": "Admin routes for vault configuration, login, unlock, lock, and logout." + }, + { + "path": "src/secret_storage.py", + "symbol": "SecretStorage", + "line_range": "L57-L87", + "explanation": "Fernet symmetric key DB secret encryption." + }, + { + "path": "tests/test_vault_password_not_in_argv.py", + "symbol": "test_bw_password_not_in_argv", + "line_range": "L1-L117", + "explanation": "Verifies master password is fed via stdin and never appears in process argv." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E1", + "runtime_validation": { + "required": true, + "status": "pending", + "reason": "Requires installed Bitwarden CLI (`bw`) executable." + } + }, + { + "id": "SECURITY-003", + "domain": "security", + "name": "API Token Management & Scope Access Control", + "purpose": "Generates scoped API bearer tokens (read/write/admin) for external tool and script authentication.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/settings.js" + ], + "backend_entrypoints": [ + "routes/api_token_routes.py:setup_api_token_routes", + "core/database.py:ApiToken" + ], + "routes": [ + "/api/tokens", + "/api/tokens/{token_id}" + ], + "configuration": [], + "persistence": [ + "DATA_DIR/odysseus.db (api_tokens table)" + ], + "dependencies": [ + "sqlalchemy", + "secrets" + ], + "tests": [ + "tests/test_api_key_file_permissions.py" + ], + "documentation": [ + "SECURITY.md" + ], + "risks": [ + "Leaked API bearer tokens with excessive permission scopes." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/api_token_routes.py", + "symbol": "setup_api_token_routes", + "line_range": "L30-L150", + "explanation": "Exposes API token creation, scope assignment, and revocation." + }, + { + "path": "core/database.py", + "symbol": "ApiToken", + "line_range": "L50-L90", + "explanation": "SQLAlchemy ORM schema for API tokens and permissions." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "SECURITY-004", + "domain": "security", + "name": "Prompt Security & Injection Defense Engine", + "purpose": "Scans system prompts and external inputs for prompt injection attempts, jailbreaks, and sensitive data leaks.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/chat.js" + ], + "backend_entrypoints": [ + "src/prompt_security.py:sanitize_prompt", + "src/tool_security.py:validate_tool_input" + ], + "routes": [ + "Implicit security inspection" + ], + "configuration": [ + "ENABLE_PROMPT_SANITY_CHECK" + ], + "persistence": [], + "dependencies": [ + "re" + ], + "tests": [ + "tests/test_skill_index_prompt_injection.py", + "tests/test_tool_output_prompt_injection.py" + ], + "documentation": [ + "THREAT_MODEL.md" + ], + "risks": [ + "False positives blocking legitimate complex coding or security prompts." + ], + "unknowns": [], + "evidence": [ + { + "path": "src/prompt_security.py", + "symbol": "untrusted_context_message", + "line_range": "L64-L86", + "explanation": "Wraps untrusted context with guard delimiters and sets metadata.trusted = False." + }, + { + "path": "src/tool_security.py", + "symbol": "NON_ADMIN_BLOCKED_TOOLS", + "line_range": "L42-L78", + "explanation": "Enforces tool execution safety for non-admin user roles." + }, + { + "path": "tests/test_skill_index_prompt_injection.py", + "symbol": "test_skill_index", + "line_range": "L1-L208", + "explanation": "Verifies skill index descriptions cannot leak into trusted system prompts." + }, + { + "path": "tests/test_tool_output_prompt_injection.py", + "symbol": "test_tool_output", + "line_range": "L1-L50", + "explanation": "Tool output injection guards." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E1", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "SECURITY-005", + "domain": "security", + "name": "URL & Path Confinement Security Guards", + "purpose": "Prevents SSRF attacks and path traversal by validating target IP addresses and resolving symlinks.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/fileHandler.js" + ], + "backend_entrypoints": [ + "src/url_security.py:validate_url", + "src/url_safety.py", + "core/log_safety.py" + ], + "routes": [ + "Implicit guard functions" + ], + "configuration": [ + "ALLOWED_DOMAINS_WHITELIST" + ], + "persistence": [], + "dependencies": [ + "ipaddress", + "urllib.parse" + ], + "tests": [ + "tests/test_url_safety.py", + "tests/test_tool_path_confinement.py", + "tests/test_workspace_confine.py" + ], + "documentation": [ + "THREAT_MODEL.md" + ], + "risks": [ + "DNS rebinding attacks if IP address is re-resolved post-validation." + ], + "unknowns": [], + "evidence": [ + { + "path": "src/url_safety.py", + "symbol": "check_outbound_url", + "line_range": "L60-L108", + "explanation": "Rejects non-HTTP(S) schemes, link-local, cloud metadata SSRF addresses." + }, + { + "path": "src/url_security.py", + "symbol": "validate_public_http_url", + "line_range": "L81-L94", + "explanation": "Validates public-facing endpoints." + }, + { + "path": "tests/test_url_safety.py", + "symbol": "test_url_safety", + "line_range": "L1-L117", + "explanation": "Scheme validation, cloud metadata SSRF rejection, IP classification." + }, + { + "path": "tests/test_tool_path_confinement.py", + "symbol": "test_path_confinement", + "line_range": "L1-L50", + "explanation": "Path traversal checks." + }, + { + "path": "tests/test_workspace_confine.py", + "symbol": "test_workspace_confine", + "line_range": "L1-L50", + "explanation": "Workspace confinement checks." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E1", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "SECURITY-006", + "domain": "security", + "name": "HTTP Security Headers Middleware", + "purpose": "Injects standard OWASP HTTP security headers (CSP, HSTS, X-Content-Type-Options, X-Frame-Options).", + "status": "verified", + "frontend_entrypoints": [ + "static/app.js" + ], + "backend_entrypoints": [ + "core/middleware.py:SecurityHeadersMiddleware" + ], + "routes": [ + "Applies globally to all routes" + ], + "configuration": [ + "CSP_NONCE_ENABLED" + ], + "persistence": [], + "dependencies": [ + "starlette.middleware.base" + ], + "tests": [ + "tests/test_security_headers_middleware.py", + "tests/test_security_headers_pdf_preview.py" + ], + "documentation": [ + "SECURITY.md" + ], + "risks": [ + "Strict Content Security Policy (CSP) blocking third-party embedded web resources." + ], + "unknowns": [], + "evidence": [ + { + "path": "core/middleware.py", + "symbol": "SecurityHeadersMiddleware", + "line_range": "L40-L110", + "explanation": "Sets strict security headers and CSP nonces on HTTP responses." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "SECURITY-007", + "domain": "security", + "name": "Admin System Data Wipe ('Danger Zone')", + "purpose": "Provides administrative reset operations to wipe sessions, cache, uploaded files, or factory reset state.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/admin.js" + ], + "backend_entrypoints": [ + "routes/admin_wipe/admin_wipe_routes.py:setup_admin_wipe_routes" + ], + "routes": [ + "/api/admin/wipe/{kind}" + ], + "configuration": [], + "persistence": [ + "ALL storage locations" + ], + "dependencies": [ + "os", + "shutil" + ], + "tests": [ + "tests/test_session_ghost_delete.py" + ], + "documentation": [ + "THREAT_MODEL.md" + ], + "risks": [ + "Accidental catastrophic data loss if triggered without user confirmation." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/admin_wipe/admin_wipe_routes.py", + "symbol": "@router.delete('/wipe/{kind}')", + "line_range": "L71-L130", + "explanation": "Executes systemic data wipe based on requested scope." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "FRONTEND-001", + "domain": "frontend", + "name": "Single Page Application Shell & Client Router", + "purpose": "Main HTML5 SPA shell, DOM lifecycle initializers, tab navigation, and deep-link route handlers.", + "status": "verified", + "frontend_entrypoints": [ + "static/index.html", + "static/app.js", + "static/js/init.js" + ], + "backend_entrypoints": [ + "app.py:serve_index" + ], + "routes": [ + "/", + "/notes", + "/calendar", + "/cookbook", + "/email", + "/memory", + "/gallery", + "/tasks", + "/library" + ], + "configuration": [], + "persistence": [ + "static/index.html" + ], + "dependencies": [ + "Vanilla HTML5/JS" + ], + "tests": [ + "tests/test_app_static_mime.py", + "tests/test_serve_html_with_nonce.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Stale browser static cache if asset hashing is omitted during deployment." + ], + "unknowns": [], + "evidence": [ + { + "path": "static/index.html", + "symbol": "index.html", + "line_range": "L1-L200", + "explanation": "Main SPA entry point containing modal roots and CSS bundles." + }, + { + "path": "app.py", + "symbol": "serve_index", + "line_range": "L867-L878", + "explanation": "Serves index.html with dynamically generated CSP nonces." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "FRONTEND-002", + "domain": "frontend", + "name": "Dynamic Theme, Color System & Custom Fonts", + "purpose": "Supports dark/light themes, custom CSS variables, color picker controls, and user font uploads.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/theme.js", + "static/style.css", + "static/js/colorPicker.js" + ], + "backend_entrypoints": [ + "routes/font_routes.py:setup_font_routes", + "routes/prefs_routes.py:setup_prefs_routes" + ], + "routes": [ + "/api/font/custom", + "/api/prefs" + ], + "configuration": [], + "persistence": [ + "DATA_DIR/user_prefs.json", + "static/fonts/custom/" + ], + "dependencies": [ + "Vanilla CSS", + "pydantic" + ], + "tests": [ + "tests/test_prefs_atomic_write.py", + "tests/test_select_dropdown_theme_css.py" + ], + "documentation": [ + "docs/theme.webm" + ], + "risks": [ + "Flash of unstyled content (FOUC) on slow connections." + ], + "unknowns": [], + "evidence": [ + { + "path": "static/js/theme.js", + "symbol": "applyTheme", + "line_range": "L1-L90", + "explanation": "Applies custom HSL theme variables to DOM document root." + }, + { + "path": "routes/font_routes.py", + "symbol": "setup_font_routes", + "line_range": "L20-L55", + "explanation": "Allows uploading and serving custom WOFF2 font files." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "FRONTEND-003", + "domain": "frontend", + "name": "Window Manager, Tile Layout & Modal Control System", + "purpose": "Manages draggable tool windows, snapped multi-tile viewports, modal dialog Z-ordering, and ESC key stacks.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/modalManager.js", + "static/js/tileManager.js", + "static/js/windowDrag.js", + "static/js/windowResize.js", + "static/js/escMenuStack.js" + ], + "backend_entrypoints": [ + "None (Client UI subsystem)" + ], + "routes": [], + "configuration": [ + "localStorage: tile_layout_state" + ], + "persistence": [ + "Browser localStorage" + ], + "dependencies": [ + "Vanilla JS DOM event listeners" + ], + "tests": [ + "tests/test_portal_dropdown_z_js.py", + "tests/test_tile_manager_snap_zones_js.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Overlap artifacts when opening many simultaneous tool floating windows." + ], + "unknowns": [], + "evidence": [ + { + "path": "static/js/modalManager.js", + "symbol": "ModalManager", + "line_range": "L1-L140", + "explanation": "Controls modal open/close transitions and focus trapping." + }, + { + "path": "static/js/tileManager.js", + "symbol": "TileManager", + "line_range": "L1-L180", + "explanation": "Handles viewport split-pane grid arrangements." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "FRONTEND-004", + "domain": "frontend", + "name": "Global Keyboard Shortcuts & Accessibility Controls", + "purpose": "Provides configurable hotkeys (Ctrl+K search, Esc close, Alt+1-9 tabs) and high-contrast accessibility options.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/keyboard-shortcuts.js", + "static/js/a11y.js" + ], + "backend_entrypoints": [ + "None (Client UI subsystem)" + ], + "routes": [], + "configuration": [ + "localStorage: user_hotkeys" + ], + "persistence": [ + "Browser localStorage" + ], + "dependencies": [ + "Vanilla JS" + ], + "tests": [ + "tests/test_keyboard_shortcuts.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Browser keybinding collisions with browser default hotkeys." + ], + "unknowns": [], + "evidence": [ + { + "path": "static/js/keyboard-shortcuts.js", + "symbol": "initShortcuts", + "line_range": "L1-L110", + "explanation": "Binds global keydown handlers for system shortcuts." + }, + { + "path": "static/js/a11y.js", + "symbol": "initA11y", + "line_range": "L1-L80", + "explanation": "Applies ARIA roles and dyslexic font toggles." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "FRONTEND-005", + "domain": "frontend", + "name": "Markdown, LaTeX & Code Block Streaming Renderer", + "purpose": "Parses incoming SSE markdown streams, renders KaTeX math formulas, syntax-highlighted code, and interactive runners.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/markdown.js", + "static/js/streamingRenderer.js", + "static/js/streamingSegmenter.js", + "static/js/codeRunner.js" + ], + "backend_entrypoints": [ + "None (Client rendering subsystem)" + ], + "routes": [], + "configuration": [], + "persistence": [ + "Browser DOM" + ], + "dependencies": [ + "highlight.js", + "katex" + ], + "tests": [ + "tests/streaming/segmenter.test.mjs", + "tests/test_streaming_segmenter_js.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "DOM thrashing if streaming segmenter updates UI too frequently." + ], + "unknowns": [], + "evidence": [ + { + "path": "static/js/markdown.js", + "symbol": "renderMarkdown", + "line_range": "L1-L200", + "explanation": "Converts markdown prose to HTML nodes with syntax highlighting." + }, + { + "path": "static/js/streamingSegmenter.js", + "symbol": "Segmenter", + "line_range": "L1-L150", + "explanation": "Parses un-closed markdown fences during live stream." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "FRONTEND-006", + "domain": "frontend", + "name": "Interactive Tour & Guided Onboarding System", + "purpose": "Presents interactive step-by-step feature tours and UI tooltip hints for new users.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/tourHints.js", + "static/js/tourAutoplay.js" + ], + "backend_entrypoints": [ + "None (Client UI subsystem)" + ], + "routes": [], + "configuration": [ + "localStorage: tour_completed" + ], + "persistence": [ + "Browser localStorage" + ], + "dependencies": [ + "Vanilla JS" + ], + "tests": [ + "tests/test_task_routes.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Tour step misalignment if window is resized mid-tour." + ], + "unknowns": [], + "evidence": [ + { + "path": "static/js/tourHints.js", + "symbol": "startTour", + "line_range": "L1-L120", + "explanation": "Renders guided feature tour overlays over target UI elements." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "FRONTEND-007", + "domain": "frontend", + "name": "Background Effects Prototyping Sandbox", + "purpose": "Standalone sandbox page for prototyping visual background animations, waves, and whirlpool effects.", + "status": "dead-code-candidate", + "frontend_entrypoints": [ + "static/wave-variants.html", + "static/whirlpool-variants.html" + ], + "backend_entrypoints": [ + "app.py:serve_backgrounds" + ], + "routes": [ + "/backgrounds" + ], + "configuration": [], + "persistence": [ + "None" + ], + "dependencies": [ + "HTML5 Canvas", + "WebGL" + ], + "tests": [], + "documentation": [ + "README.md" + ], + "risks": [ + "High GPU utilization when rendering complex shader wave animations." + ], + "unknowns": [ + "Route `/backgrounds` in app.py L918 attempts to serve `static/backgrounds.html` which is missing from disk; variant templates `wave-variants.html` and `whirlpool-variants.html` exist." + ], + "evidence": [ + { + "path": "app.py", + "symbol": "serve_backgrounds", + "line_range": "L915-L918", + "explanation": "Serves visual background sandbox HTML page route." + }, + { + "path": "static/wave-variants.html", + "symbol": "wave-variants.html", + "line_range": "L1-L150", + "explanation": "Interactive background effect prototyping sandbox variant." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E1", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "PLATFORM-001", + "domain": "platform", + "name": "Application Initialization & Lifespan Management", + "purpose": "Orchestrates server startup, database table migration, background daemon initialization, and clean shutdown.", + "status": "verified", + "frontend_entrypoints": [ + "app.py" + ], + "backend_entrypoints": [ + "app.py:_lifespan", + "src/app_initializer.py:initialize_app" + ], + "routes": [ + "App startup lifespan" + ], + "configuration": [], + "persistence": [ + "DATA_DIR/" + ], + "dependencies": [ + "asyncio", + "logging" + ], + "tests": [ + "tests/test_app_helpers.py" + ], + "documentation": [ + "docs/setup.md" + ], + "risks": [ + "Un-handled exceptions during startup halt application launch." + ], + "unknowns": [], + "evidence": [ + { + "path": "app.py", + "symbol": "_lifespan", + "line_range": "L996-L1030", + "explanation": "FastAPI lifespan context manager executing startup tasks." + }, + { + "path": "src/app_initializer.py", + "symbol": "initialize_app", + "line_range": "L29-L125", + "explanation": "Initializes app directories, DB schemas, and logging." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "PLATFORM-002", + "domain": "platform", + "name": "System Health, Readiness & Version Monitoring APIs", + "purpose": "Exposes Liveness (/api/health), Readiness (/api/ready), App Version (/api/version), and Client Perf APIs.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/admin.js" + ], + "backend_entrypoints": [ + "app.py:health_check", + "app.py:readiness_check", + "app.py:get_version", + "src/readiness.py" + ], + "routes": [ + "/api/health", + "/api/ready", + "/api/version", + "/api/runtime", + "/api/client-perf" + ], + "configuration": [], + "persistence": [], + "dependencies": [ + "fastapi" + ], + "tests": [ + "tests/test_readiness.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Readiness check delays if verifying connectivity to offline remote endpoints." + ], + "unknowns": [], + "evidence": [ + { + "path": "app.py", + "symbol": "readiness_check", + "line_range": "L963-L973", + "explanation": "Performs system component integrity check." + }, + { + "path": "src/readiness.py", + "symbol": "check_readiness", + "line_range": "L15-L61", + "explanation": "Checks database, storage, and key paths for read/write access." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "PLATFORM-003", + "domain": "platform", + "name": "Database Schema, Migrations & SQLite Persistence", + "purpose": "Defines core relational tables (users, tokens, tasks, sessions) and executes automated SQLite schema upgrades.", + "status": "verified", + "frontend_entrypoints": [ + "scripts/update_database.py" + ], + "backend_entrypoints": [ + "core/database.py:init_db", + "scripts/update_database.py:run_migrations", + "src/database.py" + ], + "routes": [ + "Implicit DB queries" + ], + "configuration": [ + "DATABASE_URL" + ], + "persistence": [ + "DATA_DIR/odysseus.db" + ], + "dependencies": [ + "sqlalchemy", + "alembic", + "sqlite3" + ], + "tests": [ + "tests/test_sqlite_foreign_keys.py", + "tests/test_update_database_script.py", + "tests/test_app_db_permissions.py" + ], + "documentation": [ + "docs/setup.md" + ], + "risks": [ + "SQLite file lock contention under high concurrent write loads." + ], + "unknowns": [], + "evidence": [ + { + "path": "core/database.py", + "symbol": "init_db", + "line_range": "L40-L120", + "explanation": "Creates ORM tables and establishes connection pool." + }, + { + "path": "scripts/update_database.py", + "symbol": "run_migrations", + "line_range": "L20-L110", + "explanation": "Applies missing schema columns and indices." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "PLATFORM-004", + "domain": "platform", + "name": "User Data Export & Import Backup Infrastructure", + "purpose": "Exports complete user workspace state (sessions, memory, skills, notes, presets) into a zip archive.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/settings.js" + ], + "backend_entrypoints": [ + "routes/backup_routes.py:setup_backup_routes", + "scripts/odysseus-backup" + ], + "routes": [ + "/api/backup/export", + "/api/backup/import" + ], + "configuration": [], + "persistence": [ + "ZIP archive files" + ], + "dependencies": [ + "zipfile", + "json" + ], + "tests": [ + "tests/cli/test_preset_cli_store.py" + ], + "documentation": [ + "docs/backup-restore.md" + ], + "risks": [ + "Corrupt archive files causing partial data restore." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/backup_routes.py", + "symbol": "setup_backup_routes", + "line_range": "L30-L140", + "explanation": "Handles workspace data export and import upload unpack." + }, + { + "path": "docs/backup-restore.md", + "symbol": "Documentation", + "line_range": "L1-L50", + "explanation": "Backup and restore operational documentation." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "PLATFORM-005", + "domain": "platform", + "name": "File Cleanup & Storage Maintenance Engine", + "purpose": "Scans data directories for orphaned files, old uploads, temporary vision images, and frees disk space.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/storage.js" + ], + "backend_entrypoints": [ + "routes/cleanup/cleanup_routes.py:setup_cleanup_routes", + "src/cleanup_service.py", + "src/session_image_cleanup.py" + ], + "routes": [ + "/api/cleanup/preview", + "/api/cleanup" + ], + "configuration": [ + "CLEANUP_RETENTION_DAYS" + ], + "persistence": [ + "DATA_DIR/uploads/" + ], + "dependencies": [ + "os", + "shutil" + ], + "tests": [ + "tests/test_session_actions_cleanup.py", + "tests/test_session_image_cleanup.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Deletes files uploaded in active sessions if retention window is set too short." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/cleanup/cleanup_routes.py", + "symbol": "@router.get('/preview')", + "line_range": "L22-L37", + "explanation": "Previews reclaimable disk space across storage directories." + }, + { + "path": "src/cleanup_service.py", + "symbol": "CleanupService", + "line_range": "L25-L120", + "explanation": "Executes filesystem purge of orphaned asset files." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "PLATFORM-006", + "domain": "platform", + "name": "System Health & RAG Diagnostic Suite", + "purpose": "Executes real-time integrity diagnostics across ChromaDB, SearXNG, local models, and network interfaces.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/settings.js" + ], + "backend_entrypoints": [ + "routes/diagnostics_routes.py:setup_diagnostics_routes", + "src/service_health.py" + ], + "routes": [ + "/api/diagnostics" + ], + "configuration": [], + "persistence": [], + "dependencies": [ + "httpx", + "chromadb" + ], + "tests": [ + "tests/test_service_health_collect.py", + "tests/test_service_health_chromadb.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Diagnostic timeout if external search provider is unreachable." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/diagnostics_routes.py", + "symbol": "setup_diagnostics_routes", + "line_range": "L30-L100", + "explanation": "Runs subsystem health check suite." + }, + { + "path": "src/service_health.py", + "symbol": "collect_health_status", + "line_range": "L20-L150", + "explanation": "Inspects vector database, email, search, and local provider status." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "PLATFORM-007", + "domain": "platform", + "name": "Desktop CLI Utilities & Shell Integration Tools", + "purpose": "Provides command-line interface tools (`odysseus`, `odysseus-mcp`, `odysseus-mail`) for terminal usage.", + "status": "verified", + "frontend_entrypoints": [ + "scripts/odysseus", + "scripts/odysseus-mcp", + "scripts/odysseus-mail", + "scripts/odysseus-calendar" + ], + "backend_entrypoints": [ + "scripts/_lib/cli.py:main" + ], + "routes": [ + "CLI commands" + ], + "configuration": [], + "persistence": [ + "CLI configuration" + ], + "dependencies": [ + "urllib", + "json" + ], + "tests": [ + "tests/cli/test_sessions_cli.py", + "tests/cli/test_mail_cli_recipients.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Outdated CLI scripts if backend API schemas change." + ], + "unknowns": [], + "evidence": [ + { + "path": "scripts/_lib/cli.py", + "symbol": "main", + "line_range": "L15-L110", + "explanation": "Shared CLI framework for terminal helper commands." + }, + { + "path": "scripts/odysseus", + "symbol": "odysseus", + "line_range": "L1-L50", + "explanation": "Main terminal launcher script." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "PLATFORM-008", + "domain": "platform", + "name": "Desktop Companion App Integration", + "purpose": "Provides API routes and pairing mechanisms for the native macOS/desktop menu bar companion app.", + "status": "verified", + "frontend_entrypoints": [ + "companion/README.md" + ], + "backend_entrypoints": [ + "companion/pairing.py", + "companion/routes.py:setup_companion_routes" + ], + "routes": [ + "/companion/pair", + "/companion/status" + ], + "configuration": [ + "COMPANION_SECRET_KEY" + ], + "persistence": [ + "companion_pairing.json" + ], + "dependencies": [ + "fastapi" + ], + "tests": [ + "tests/helpers/import_state.py" + ], + "documentation": [ + "companion/README.md" + ], + "risks": [ + "Pairing code expiration timing window." + ], + "unknowns": [], + "evidence": [ + { + "path": "companion/routes.py", + "symbol": "setup_companion_routes", + "line_range": "L20-L120", + "explanation": "Endpoints for pairing and status sync with desktop companion." + }, + { + "path": "companion/pairing.py", + "symbol": "PairingManager", + "line_range": "L15-L80", + "explanation": "Generates and validates companion pairing codes." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "PLATFORM-009", + "domain": "platform", + "name": "Docker Containerization & GPU Hardware Manifests", + "purpose": "Provides multi-stage Dockerfile and Docker Compose manifests for CPU, NVIDIA CUDA, and AMD ROCm GPUs.", + "status": "verified", + "frontend_entrypoints": [ + "Dockerfile", + "docker-compose.yml", + "docker-compose.gpu-nvidia.yml", + "docker-compose.gpu-amd.yml" + ], + "backend_entrypoints": [ + "docker/entrypoint.sh" + ], + "routes": [ + "Containerized application" + ], + "configuration": [ + "DOCKER_GPU_VENDOR" + ], + "persistence": [ + "Container volumes" + ], + "dependencies": [ + "Docker", + "NVIDIA Container Toolkit" + ], + "tests": [ + "scripts/check-docker-gpu.sh" + ], + "documentation": [ + "docs/setup.md" + ], + "risks": [ + "Driver version incompatibility with host NVIDIA/AMD kernel drivers." + ], + "unknowns": [], + "evidence": [ + { + "path": "Dockerfile", + "symbol": "multi-stage-build", + "line_range": "L1-L113", + "explanation": "Multi-stage container build environment." + }, + { + "path": "docker-compose.gpu-nvidia.yml", + "symbol": "nvidia-gpu-manifest", + "line_range": "L1-L179", + "explanation": "NVIDIA GPU pass-through container specification." + }, + { + "path": "scripts/check-docker-gpu.sh", + "symbol": "check-docker-gpu", + "line_range": "L1-L615", + "explanation": "Automated diagnostic test script for host NVIDIA GPU passthrough." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": true, + "status": "pending", + "reason": "Requires Docker GPU pass-through and compatible host drivers." + } + }, + { + "id": "PLATFORM-010", + "domain": "platform", + "name": "Legacy FAISS Vector Index Migration Script", + "purpose": "Legacy utility script to migrate older FAISS vector indices into ChromaDB.", + "status": "legacy", + "frontend_entrypoints": [ + "None" + ], + "backend_entrypoints": [ + "scripts/migrate_faiss_to_chroma.py" + ], + "routes": [ + "CLI Script" + ], + "configuration": [], + "persistence": [ + "Legacy FAISS index files" + ], + "dependencies": [ + "faiss", + "chromadb" + ], + "tests": [ + "tests/helpers/import_state.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Fails if legacy FAISS index files do not exist." + ], + "unknowns": [ + "Superseded by native ChromaDB vector index pipeline." + ], + "evidence": [ + { + "path": "scripts/migrate_faiss_to_chroma.py", + "symbol": "migrate_faiss", + "line_range": "L15-L80", + "explanation": "Reads FAISS vector index files and writes to ChromaDB collection." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "NOTE-001", + "domain": "note", + "name": "Interactive Notes & Checklist Management", + "purpose": "Provides Google Keep-style notes, rich markdown text, checklist items, pinning, color tags, and reminders.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/notes.js" + ], + "backend_entrypoints": [ + "routes/note/note_routes.py:setup_note_routes", + "src/tools/notes.py" + ], + "routes": [ + "/api/notes", + "/api/notes/{note_id}", + "/api/notes/{note_id}/pin", + "/api/notes/reorder" + ], + "configuration": [], + "persistence": [ + "DATA_DIR/notes.db" + ], + "dependencies": [ + "sqlite3", + "pydantic" + ], + "tests": [ + "tests/test_notes_fail_closed_auth.py", + "tests/test_manage_notes_owner_gate.py" + ], + "documentation": [ + "docs/notes.webm" + ], + "risks": [ + "Concurrent edits on note item checkboxes." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/note/note_routes.py", + "symbol": "@router.get('')", + "line_range": "L623-L650", + "explanation": "Lists all user notes with pin and archive states." + }, + { + "path": "static/js/notes.js", + "symbol": "initNotesView", + "line_range": "L1-L160", + "explanation": "Main interactive notes grid and modal manager." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "SKILL-001", + "domain": "skill", + "name": "Dynamic Skill Management & Code Execution Engine", + "purpose": "Allows users to create, import, edit, test, and execute custom Python/Markdown skills dynamically.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/skills.js" + ], + "backend_entrypoints": [ + "routes/skills_routes.py:setup_skills_routes", + "services/memory/skills.py:SkillsManager" + ], + "routes": [ + "/api/skills", + "/api/skills/{skill_id}/invoke", + "/api/skills/import-from-url" + ], + "configuration": [ + "SKILLS_DIR" + ], + "persistence": [ + "DATA_DIR/skills/" + ], + "dependencies": [ + "pydantic", + "httpx" + ], + "tests": [ + "tests/test_skills_routes_owner_update.py", + "tests/test_skill_importer.py", + "tests/test_skill_save_no_rename.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "Arbitrary code execution risks if skill import URL is untrusted." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/skills_routes.py", + "symbol": "setup_skills_routes", + "line_range": "L100-L300", + "explanation": "Exposes CRUD and remote import routes for user skills." + }, + { + "path": "services/memory/skills.py", + "symbol": "SkillsManager", + "line_range": "L40-L220", + "explanation": "Handles skill storage, parsing, and execution." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "CONTACT-001", + "domain": "contact", + "name": "CardDAV Contact Management & Address Book Integration", + "purpose": "Connects to CardDAV servers, imports VCard contacts, and provides contact lookup for email/calendar autocomplete.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/emailLibrary.js" + ], + "backend_entrypoints": [ + "routes/contacts/contacts_routes.py:setup_contacts_routes", + "src/tools/contacts.py" + ], + "routes": [ + "/api/contacts/list", + "/api/contacts/search", + "/api/contacts/add", + "/api/contacts/config" + ], + "configuration": [ + "CARDDAV_URL" + ], + "persistence": [ + "DATA_DIR/contacts.db" + ], + "dependencies": [ + "vobject", + "sqlite3" + ], + "tests": [ + "tests/test_contacts_carddav_security.py", + "tests/cli/test_contacts_cli_rows.py" + ], + "documentation": [ + "README.md" + ], + "risks": [ + "VCard 3.0 vs 4.0 property parsing mismatches." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/contacts/contacts_routes.py", + "symbol": "@router.get('/list')", + "line_range": "L741-L764", + "explanation": "Returns contact list filtered by search query." + }, + { + "path": "src/tools/contacts.py", + "symbol": "ContactsTool", + "line_range": "L20-L110", + "explanation": "Agent tool for querying user address book contacts." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + }, + { + "id": "MEMORY-001", + "domain": "memory", + "name": "Persistent Long-Term Memory & Vector Indexing", + "purpose": "Extracts facts, user preferences, and temporal memories from chat sessions into vector/relational storage.", + "status": "verified", + "frontend_entrypoints": [ + "static/js/memory.js" + ], + "backend_entrypoints": [ + "routes/memory/memory_routes.py:setup_memory_routes", + "services/memory/service.py", + "mcp_servers/memory_server.py" + ], + "routes": [ + "/api/memory", + "/api/memory/search", + "/api/memory/extract", + "/api/memory/audit" + ], + "configuration": [ + "MEMORY_AUTO_EXTRACT" + ], + "persistence": [ + "DATA_DIR/memory.db", + "DATA_DIR/memory_chroma/" + ], + "dependencies": [ + "sqlite3", + "chromadb" + ], + "tests": [ + "tests/test_memory_routes_session_owner.py", + "tests/test_consolidate_memory_explicit_drops.py" + ], + "documentation": [ + "docs/theme.webm" + ], + "risks": [ + "Conflicting memory facts extracted from contradictory user prompts." + ], + "unknowns": [], + "evidence": [ + { + "path": "routes/memory/memory_routes.py", + "symbol": "@router.get('')", + "line_range": "L132-L150", + "explanation": "Fetches long-term user memory timeline." + }, + { + "path": "services/memory/memory_extractor.py", + "symbol": "MemoryExtractor", + "line_range": "L30-L160", + "explanation": "LLM-driven fact extraction from conversation transcripts." + } + ], + "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", + "evidence_maturity": "E0", + "runtime_validation": { + "required": false, + "status": "not-required", + "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." + } + } +] diff --git a/docs/discovery/feature-catalog.md b/docs/discovery/feature-catalog.md new file mode 100644 index 000000000..9a93c0317 --- /dev/null +++ b/docs/discovery/feature-catalog.md @@ -0,0 +1,85 @@ +# Feature Catalog + +This is a human-readable index derived from [`feature-catalog.json`](feature-catalog.json). The JSON file is canonical. + +| ID | Feature | Domain | Status | Evidence | Runtime | +|---|---|---|---|---|---| +| `AGENT-001` | Autonomous Agent Loop & Tool Execution Engine | `agent` | `verified` | `E0` | not required | +| `AGENT-002` | Scheduled Tasks & Event Bus Dispatcher | `agent` | `verified` | `E0` | not required | +| `AGENT-003` | Webhook Event Subscriptions & Trigger Processing | `agent` | `verified` | `E0` | not required | +| `AGENT-004` | Assistant Settings, Task Check-Ins & Background Job Monitor | `agent` | `partial` | `E1` | not required | +| `AGENT-005` | Model Context Protocol (MCP) Server Integration | `agent` | `verified` | `E0` | not required | +| `AGENT-006` | AI Interaction Tools & Pipeline Orchestration | `agent` | `verified` | `E0` | not required | +| `AGENT-007` | Subprocess & Background Job Execution Tools | `agent` | `verified` | `E0` | not required | +| `CALENDAR-001` | CalDAV Calendar Synchronization & Account Setup | `calendar` | `verified` | `E0` | pending | +| `CALENDAR-002` | Calendar Event Operations & iCalendar Parsing | `calendar` | `verified` | `E0` | not required | +| `CHAT-001` | Core Chat Streaming & SSE Message Generation | `chat` | `verified` | `E2` | pending | +| `CHAT-002` | Session Management & Conversation State | `chat` | `verified` | `E0` | not required | +| `CHAT-003` | Chat History & Message Editing/Truncation | `chat` | `verified` | `E0` | not required | +| `CHAT-004` | File & Multimodal Attachment Handling | `chat` | `verified` | `E0` | not required | +| `CHAT-005` | Chat Message Search | `chat` | `verified` | `E0` | not required | +| `CHAT-006` | System Prompts & Preset Management | `chat` | `verified` | `E0` | not required | +| `CHAT-007` | Emoji Rendering & Twemoji SVG Proxy | `chat` | `verified` | `E0` | not required | +| `CHAT-008` | Input History Recall (Arrow Up) | `chat` | `verified` | `E0` | not required | +| `CHAT-009` | Context Window Compaction & Truncation | `chat` | `verified` | `E0` | not required | +| `CONTACT-001` | CardDAV Contact Management & Address Book Integration | `contact` | `verified` | `E0` | not required | +| `COOKBOOK-001` | Local Model Download & Recipe Lifecycle Management | `cookbook` | `verified` | `E0` | not required | +| `COOKBOOK-002` | Hardware Model Fitting ('What Fits?') Analysis Engine | `cookbook` | `verified` | `E0` | not required | +| `COOKBOOK-003` | HuggingFace & MLX Model Discovery Services | `cookbook` | `verified` | `E0` | not required | +| `COOKBOOK-004` | Host Docker Access for Inference Container Runtimes | `cookbook` | `verified` | `E0` | pending | +| `DOCUMENT-001` | Document & Canvas Artifact Management | `document` | `verified` | `E0` | not required | +| `DOCUMENT-002` | PDF Form Processing & High-Fidelity Rendering | `document` | `verified` | `E1` | pending | +| `DOCUMENT-003` | Personal Document Indexing & RAG Retrieval | `document` | `verified` | `E0` | not required | +| `DOCUMENT-004` | Document Conversion & Text Extraction Engine | `document` | `verified` | `E0` | not required | +| `DOCUMENT-005` | Document Library UI Navigation | `document` | `verified` | `E0` | not required | +| `EMAIL-001` | Email Account Setup, IMAP/SMTP Connection & Polling | `email` | `verified` | `E1` | pending | +| `EMAIL-002` | Email Searching, Threading & Message Operations | `email` | `verified` | `E0` | not required | +| `EMAIL-003` | Email Composition, Draft Management & Sending | `email` | `verified` | `E0` | pending | +| `EMAIL-004` | Email MCP Server & Codex Integration Bridge | `email` | `verified` | `E0` | not required | +| `FRONTEND-001` | Single Page Application Shell & Client Router | `frontend` | `verified` | `E0` | not required | +| `FRONTEND-002` | Dynamic Theme, Color System & Custom Fonts | `frontend` | `verified` | `E0` | not required | +| `FRONTEND-003` | Window Manager, Tile Layout & Modal Control System | `frontend` | `verified` | `E0` | not required | +| `FRONTEND-004` | Global Keyboard Shortcuts & Accessibility Controls | `frontend` | `verified` | `E0` | not required | +| `FRONTEND-005` | Markdown, LaTeX & Code Block Streaming Renderer | `frontend` | `verified` | `E0` | not required | +| `FRONTEND-006` | Interactive Tour & Guided Onboarding System | `frontend` | `verified` | `E0` | not required | +| `FRONTEND-007` | Background Effects Prototyping Sandbox | `frontend` | `dead-code-candidate` | `E1` | not required | +| `MEDIA-001` | Gallery Image Library & Album Operations | `media` | `verified` | `E0` | not required | +| `MEDIA-002` | Image Processing, AI Upscaling & Style Transfer | `media` | `verified` | `E0` | not required | +| `MEDIA-003` | Interactive Image Canvas Editor & Persisted Drafts | `media` | `verified` | `E0` | not required | +| `MEDIA-004` | Text-to-Speech (TTS) Synthesis Service | `media` | `verified` | `E0` | not required | +| `MEDIA-005` | Speech-to-Text (STT) Audio Transcription Service | `media` | `verified` | `E0` | not required | +| `MEDIA-006` | Digital Signature Stamp Storage & Placement | `media` | `verified` | `E0` | not required | +| `MEDIA-007` | Generated Image Artifact Route & MCP Integration | `media` | `verified` | `E0` | not required | +| `MEDIA-008` | Native MLX Image Bridge (macOS Apple Silicon) | `media` | `experimental` | `E0` | pending | +| `MEMORY-001` | Persistent Long-Term Memory & Vector Indexing | `memory` | `verified` | `E0` | not required | +| `MODEL-001` | Multi-Provider LLM Model Discovery & Metadata Management | `model` | `verified` | `E0` | not required | +| `MODEL-002` | Model Capability & Context Limits Detection | `model` | `verified` | `E0` | not required | +| `MODEL-003` | LLM Core Provider Communication & Fallback Routing | `model` | `verified` | `E0` | pending | +| `MODEL-004` | Model Selection & Display Ordering Preferences | `model` | `verified` | `E0` | not required | +| `MODEL-005` | Side-by-Side Model Comparison (A/B Testing) | `model` | `verified` | `E0` | not required | +| `MODEL-006` | GitHub Copilot Device Flow Authentication | `model` | `verified` | `E2` | pending | +| `MODEL-007` | ChatGPT Subscription Device Flow Authentication | `model` | `verified` | `E2` | pending | +| `MODEL-008` | Embedding Model Lane & Vector Provider Setup | `model` | `verified` | `E0` | not required | +| `NOTE-001` | Interactive Notes & Checklist Management | `note` | `verified` | `E0` | not required | +| `PLATFORM-001` | Application Initialization & Lifespan Management | `platform` | `verified` | `E0` | not required | +| `PLATFORM-002` | System Health, Readiness & Version Monitoring APIs | `platform` | `verified` | `E0` | not required | +| `PLATFORM-003` | Database Schema, Migrations & SQLite Persistence | `platform` | `verified` | `E0` | not required | +| `PLATFORM-004` | User Data Export & Import Backup Infrastructure | `platform` | `verified` | `E0` | not required | +| `PLATFORM-005` | File Cleanup & Storage Maintenance Engine | `platform` | `verified` | `E0` | not required | +| `PLATFORM-006` | System Health & RAG Diagnostic Suite | `platform` | `verified` | `E0` | not required | +| `PLATFORM-007` | Desktop CLI Utilities & Shell Integration Tools | `platform` | `verified` | `E0` | not required | +| `PLATFORM-008` | Desktop Companion App Integration | `platform` | `verified` | `E0` | not required | +| `PLATFORM-009` | Docker Containerization & GPU Hardware Manifests | `platform` | `verified` | `E0` | pending | +| `PLATFORM-010` | Legacy FAISS Vector Index Migration Script | `platform` | `legacy` | `E0` | not required | +| `RESEARCH-001` | Deep Research Execution Engine & SSE Progress Streaming | `research` | `verified` | `E0` | not required | +| `RESEARCH-002` | Research Library, Detail View & Image Controls | `research` | `verified` | `E0` | not required | +| `RESEARCH-003` | Web Search Engine Integration (SearXNG & Multi-Provider) | `research` | `verified` | `E1` | pending | +| `RESEARCH-004` | Research Result Peeking & Topic Spinoff Generation | `research` | `verified` | `E0` | not required | +| `SECURITY-001` | Authentication, Session Cookies & User Management | `security` | `verified` | `E0` | not required | +| `SECURITY-002` | System Vault Encrypted Secret Storage | `security` | `verified` | `E1` | pending | +| `SECURITY-003` | API Token Management & Scope Access Control | `security` | `verified` | `E0` | not required | +| `SECURITY-004` | Prompt Security & Injection Defense Engine | `security` | `verified` | `E1` | not required | +| `SECURITY-005` | URL & Path Confinement Security Guards | `security` | `verified` | `E1` | not required | +| `SECURITY-006` | HTTP Security Headers Middleware | `security` | `verified` | `E0` | not required | +| `SECURITY-007` | Admin System Data Wipe ('Danger Zone') | `security` | `verified` | `E0` | not required | +| `SKILL-001` | Dynamic Skill Management & Code Execution Engine | `skill` | `verified` | `E0` | not required | diff --git a/docs/discovery/references/source-provenance.md b/docs/discovery/references/source-provenance.md new file mode 100644 index 000000000..c7a9a581d --- /dev/null +++ b/docs/discovery/references/source-provenance.md @@ -0,0 +1,28 @@ +# Source Provenance & Audit Baseline + +## Target Repository & Snapshot + +- **Repository**: `odysseus-dev/odysseus` +- **Branch**: `discovery` +- **Pinned Commit SHA**: `d8a2059df8e53bc7275c45339849d14c8651e73c` +- **Snapshot Date**: `2026-07-23T14:49:02Z` + +## Discovery Package Organization + +The public discovery documentation package under `docs/discovery/` is structured as follows: + +- `feature-catalog.json`: Canonical machine-readable JSON catalog containing 79 feature records. +- `feature-catalog.md`: Human-readable summary derived from `feature-catalog.json`. +- `BASELINE-STATUS.md`: Publication status, evidence-validation snapshot, and durable maintainer guidance. +- `audit-method.md`: Audit rules, scope, and evidence maturity definitions (E0 to E4). +- `domains/`: 16 functional domain markdown files detailing feature implementations. +- `references/`: Audit provenance and repository snapshot metadata. +- `tools/`: Structural, consistency, and evidence validators with focused evidence-validator tests. + +## Exclusion Principles + +This public documentation package explicitly excludes: +- Internal planning artifacts or private meeting notes. +- Machine-specific filesystem paths or user environments. +- API keys, credentials, or private service endpoints. +- Application code or automated test mutations. diff --git a/docs/discovery/tools/test_validate_discovery_evidence.py b/docs/discovery/tools/test_validate_discovery_evidence.py new file mode 100644 index 000000000..5e3144fe0 --- /dev/null +++ b/docs/discovery/tools/test_validate_discovery_evidence.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +"""Focused negative tests for validate_discovery_evidence.py.""" + +from __future__ import annotations + +import importlib.util +import shutil +import sys +import tempfile +import unittest +from pathlib import Path + + +MODULE_PATH = Path(__file__).with_name("validate_discovery_evidence.py") +SPEC = importlib.util.spec_from_file_location("validate_discovery_evidence", MODULE_PATH) +assert SPEC and SPEC.loader +validator = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = validator +SPEC.loader.exec_module(validator) + + +class FakeJavascriptParser: + supported = True + reason = "test parser" + + def __init__(self, symbols: list[validator.Located] | None = None) -> None: + self.symbols = symbols or [] + + def parse(self, path: Path) -> list[validator.Located]: + return self.symbols + + +class UnsupportedJavascriptParser: + supported = False + reason = "no repository-local parser" + + +class EvidenceNegativeTests(unittest.TestCase): + def setUp(self) -> None: + self.temp = tempfile.TemporaryDirectory() + self.root = Path(self.temp.name) + (self.root / "sample.py").write_text( + "from fastapi import APIRouter\n" + "router = APIRouter(prefix='/api')\n" + "\n" + "class ChatHandler:\n" + " def preprocess_message(self):\n" + " return True\n" + "\n" + "@router.post('/chat')\n" + "def chat_stream():\n" + " return True\n", + encoding="utf-8", + ) + (self.root / "sample.js").write_text( + "export const present = () => true;\n", encoding="utf-8" + ) + (self.root / "sample.sh").write_text( + "#!/usr/bin/env bash\nreal_function() {\n return 0\n}\n", + encoding="utf-8", + ) + self.backup = self.root / "copied-backups" + self.backup.mkdir() + for path in self.root.glob("sample.*"): + shutil.copy2(path, self.backup / path.name) + + def tearDown(self) -> None: + for backup in self.backup.iterdir(): + target = self.root / backup.name + shutil.copy2(backup, target) + self.assertEqual(target.read_bytes(), backup.read_bytes()) + self.temp.cleanup() + + def validate( + self, + evidence: dict[str, str], + javascript_parser: object | None = None, + ) -> validator.Validation: + return validator.validate_evidence( + self.root, + "TEST-001", + 0, + evidence, + javascript_parser or FakeJavascriptParser(), + ) + + def test_missing_python_symbol(self) -> None: + result = self.validate( + { + "path": "sample.py", + "kind": "python-function", + "locator": "fabricated", + "line_range": "L1-L1", + "explanation": "negative fixture", + } + ) + self.assertEqual(result.result, "invalid-locator") + + def test_incorrect_qualified_method(self) -> None: + result = self.validate( + { + "path": "sample.py", + "kind": "python-method", + "locator": "WrongHandler.preprocess_message", + "line_range": "L5-L6", + "explanation": "negative fixture", + } + ) + self.assertEqual(result.result, "invalid-locator") + + def test_symbol_outside_cited_range(self) -> None: + result = self.validate( + { + "path": "sample.py", + "kind": "python-method", + "locator": "ChatHandler.preprocess_message", + "line_range": "L1-L2", + "explanation": "negative fixture", + } + ) + self.assertEqual(result.result, "locator-outside-range") + + def test_fabricated_test_function(self) -> None: + result = self.validate( + { + "path": "sample.py", + "kind": "test-function", + "locator": "test_fabricated", + "line_range": "L1-L2", + "explanation": "negative fixture", + } + ) + self.assertEqual(result.result, "invalid-locator") + + def test_nonexistent_javascript_symbol_with_parser(self) -> None: + result = self.validate( + { + "path": "sample.js", + "kind": "javascript-function", + "locator": "missing", + "line_range": "L1-L1", + "explanation": "negative fixture", + }, + FakeJavascriptParser( + [validator.Located("present", "javascript-export", 1, 1)] + ), + ) + self.assertEqual(result.result, "invalid-locator") + + def test_unsupported_javascript_parser(self) -> None: + result = self.validate( + { + "path": "sample.js", + "kind": "javascript-function", + "locator": "present", + "line_range": "L1-L1", + "explanation": "negative fixture", + }, + UnsupportedJavascriptParser(), + ) + self.assertEqual(result.result, "unsupported") + + def test_route_path_mismatch(self) -> None: + result = self.validate( + { + "path": "sample.py", + "kind": "python-route", + "locator": "POST /api/wrong -> chat_stream", + "line_range": "L9-L10", + "explanation": "negative fixture", + } + ) + self.assertEqual(result.result, "invalid-locator") + self.assertIn("path", result.problem or "") + + def test_http_method_mismatch(self) -> None: + result = self.validate( + { + "path": "sample.py", + "kind": "python-route", + "locator": "GET /api/chat -> chat_stream", + "line_range": "L9-L10", + "explanation": "negative fixture", + } + ) + self.assertEqual(result.result, "invalid-locator") + self.assertIn("method", result.problem or "") + + def test_shell_function_mismatch(self) -> None: + result = self.validate( + { + "path": "sample.sh", + "kind": "shell-function", + "locator": "fabricated", + "line_range": "L1-L4", + "explanation": "negative fixture", + } + ) + self.assertEqual(result.result, "invalid-locator") + + def test_invalid_file_level_evidence(self) -> None: + result = self.validate( + { + "path": "missing.file", + "kind": "file", + "explanation": "negative fixture", + } + ) + self.assertEqual(result.result, "invalid-path") + + def test_file_level_evidence_rejects_fake_symbol(self) -> None: + result = self.validate( + { + "path": "sample.sh", + "kind": "file", + "locator": "whole-script", + "explanation": "negative fixture", + } + ) + self.assertEqual(result.result, "invalid-locator") + + +if __name__ == "__main__": + unittest.main() diff --git a/docs/discovery/tools/validate_discovery_consistency.py b/docs/discovery/tools/validate_discovery_consistency.py new file mode 100755 index 000000000..953b093e1 --- /dev/null +++ b/docs/discovery/tools/validate_discovery_consistency.py @@ -0,0 +1,592 @@ +#!/usr/bin/env python3 + +from __future__ import annotations + +import json +import re +import sys +from collections import Counter, defaultdict +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] +CATALOG_PATH = ROOT / "feature-catalog.json" +CATALOG_MD_PATH = ROOT / "feature-catalog.md" +DOMAINS_DIR = ROOT / "domains" +REVIEWS_DIR = ROOT / "reviews" + +EXPECTED_COMMIT = "d8a2059df8e53bc7275c45339849d14c8651e73c" +EXPECTED_FEATURES = 79 +EXPECTED_DOMAINS = 16 + +VALID_STATUSES = { + "verified", + "partial", + "disabled", + "experimental", + "legacy", + "dead-code-candidate", + "unverified", +} + +VALID_MATURITY = {"E0", "E1", "E2", "E3", "E4"} + +VALID_RUNTIME = { + "not-required", + "pending", + "blocked", + "completed", +} + +REQUIRED_FIELDS = { + "id", + "domain", + "name", + "purpose", + "status", + "evidence_maturity", + "verified_at_commit", + "evidence", + "runtime_validation", +} + +FEATURE_ID_RE = re.compile(r"^[A-Z][A-Z0-9]*-\d{3}$") + +DOMAIN_HEADING_RE = re.compile( + r"^##\s+`?([A-Z][A-Z0-9]*-\d{3})`?" + r"\s+(?:—|-)\s+(.+?)\s*$" +) + +DOMAIN_FIELD_RE = re.compile( + r"^-\s+\*\*" + r"(Domain|Status|Evidence Maturity|Commit Verified)" + r"\*\*:\s*(.*?)\s*$" +) + +DOMAIN_RUNTIME_RE = re.compile( + r"^-\s+\*\*Runtime Validation\*\*:\s*(.*?)\s*$" +) + +REVIEW_HEADING_RE = re.compile( + r"^###\s+(?:\d+\.\s+)?" + r"([A-Z][A-Z0-9]*-\d{3})" + r"\s+(?:—|-)\s+.+$" +) + +REVIEW_RESULT_RE = re.compile( + r"^-\s+\*\*Resulting Status(?:\s+and|/)\s+Maturity\*\*:" + r"\s*`([^`]+)`\s*/\s*`([^`]+)`\s*$" +) + + +def clean(value: str) -> str: + value = value.strip() + + if ( + len(value) >= 2 + and value.startswith("`") + and value.endswith("`") + ): + return value[1:-1].strip() + + return value + + +def load_catalog(errors: list[str]) -> list[dict[str, Any]]: + try: + data = json.loads( + CATALOG_PATH.read_text(encoding="utf-8") + ) + except Exception as exc: + errors.append(f"Unable to read catalog: {exc}") + return [] + + if isinstance(data, list): + features = data + elif isinstance(data, dict) and isinstance(data.get("features"), list): + features = data["features"] + else: + errors.append( + "Catalog must be an array or contain a features array" + ) + return [] + + if not all(isinstance(feature, dict) for feature in features): + errors.append("Every catalog feature must be an object") + return [] + + return features + + +def validate_catalog( + features: list[dict[str, Any]], + errors: list[str], +) -> None: + if len(features) != EXPECTED_FEATURES: + errors.append( + f"Expected {EXPECTED_FEATURES} features, " + f"found {len(features)}" + ) + + ids = [feature.get("id") for feature in features] + + duplicates = sorted( + feature_id + for feature_id, count in Counter(ids).items() + if feature_id and count > 1 + ) + + if duplicates: + errors.append( + "Duplicate feature IDs: " + ", ".join(duplicates) + ) + + for index, feature in enumerate(features): + feature_id = feature.get("id") + label = ( + feature_id + if isinstance(feature_id, str) + else f"" + ) + + missing = sorted( + field + for field in REQUIRED_FIELDS + if feature.get(field) in (None, "", []) + ) + + if missing: + errors.append( + f"{label}: missing fields: {', '.join(missing)}" + ) + + if ( + not isinstance(feature_id, str) + or not FEATURE_ID_RE.fullmatch(feature_id) + ): + errors.append(f"{label}: invalid feature ID") + + if feature.get("status") not in VALID_STATUSES: + errors.append( + f"{label}: invalid status " + f"{feature.get('status')!r}" + ) + + if feature.get("evidence_maturity") not in VALID_MATURITY: + errors.append( + f"{label}: invalid maturity " + f"{feature.get('evidence_maturity')!r}" + ) + + if feature.get("verified_at_commit") != EXPECTED_COMMIT: + errors.append( + f"{label}: incorrect verified_at_commit" + ) + + runtime = feature.get("runtime_validation") + + if not isinstance(runtime, dict): + errors.append( + f"{label}: runtime_validation must be an object" + ) + continue + + required = runtime.get("required") + runtime_status = runtime.get("status") + reason = runtime.get("reason") + + if not isinstance(required, bool): + errors.append( + f"{label}: runtime required must be boolean" + ) + + if runtime_status not in VALID_RUNTIME: + errors.append( + f"{label}: invalid runtime status " + f"{runtime_status!r}" + ) + + if not isinstance(reason, str) or not reason.strip(): + errors.append( + f"{label}: runtime reason is blank" + ) + + if required is False and runtime_status != "not-required": + errors.append( + f"{label}: required=false requires not-required" + ) + + if required is True and runtime_status == "not-required": + errors.append( + f"{label}: required=true cannot be not-required" + ) + + +def parse_domain( + path: Path, + errors: list[str], +) -> dict[str, dict[str, str]]: + records: dict[str, dict[str, str]] = {} + current_id: str | None = None + + for line_number, line in enumerate( + path.read_text(encoding="utf-8").splitlines(), + start=1, + ): + heading = DOMAIN_HEADING_RE.match(line) + + if heading: + current_id = heading.group(1) + + if current_id in records: + errors.append( + f"{path.relative_to(ROOT)}:{line_number}: " + f"duplicate heading {current_id}" + ) + + records[current_id] = { + "Name": heading.group(2).strip(), + } + continue + + field = DOMAIN_FIELD_RE.match(line) + + if field and current_id: + value = clean(field.group(2)) + + if not value: + errors.append( + f"{path.relative_to(ROOT)}:{line_number}: " + f"blank {field.group(1)}" + ) + + records[current_id][field.group(1)] = value + continue + + runtime = DOMAIN_RUNTIME_RE.match(line) + + if runtime and current_id: + value = clean(runtime.group(1)) + runtime_status = re.split( + r"\s+(?:—|-)\s+", + value, + maxsplit=1, + )[0] + records[current_id]["Runtime Validation"] = ( + runtime_status.strip("` ") + ) + + return records + + +def validate_domains( + features: list[dict[str, Any]], + errors: list[str], +) -> None: + catalog = { + feature["id"]: feature + for feature in features + if feature.get("id") + } + + expected_by_domain: defaultdict[str, set[str]] = defaultdict(set) + + for feature in features: + expected_by_domain[feature["domain"]].add(feature["id"]) + + paths = sorted(DOMAINS_DIR.glob("*.md")) + + if len(paths) != EXPECTED_DOMAINS: + errors.append( + f"Expected {EXPECTED_DOMAINS} domain files, " + f"found {len(paths)}" + ) + + all_found: set[str] = set() + + for path in paths: + domain = path.stem + records = parse_domain(path, errors) + found = set(records) + expected = expected_by_domain.get(domain, set()) + all_found.update(found) + + if found != expected: + missing = sorted(expected - found) + extra = sorted(found - expected) + + errors.append( + f"{domain}: missing={missing}, unexpected={extra}" + ) + + for feature_id, record in records.items(): + feature = catalog.get(feature_id) + + if feature is None: + continue + + expected_values = { + "Name": feature["name"], + "Domain": feature["domain"], + "Status": feature["status"], + "Evidence Maturity": feature["evidence_maturity"], + "Commit Verified": feature["verified_at_commit"], + "Runtime Validation": ( + feature["runtime_validation"]["status"] + ), + } + + for field, expected_value in expected_values.items(): + actual = record.get(field) + + if actual != expected_value: + errors.append( + f"{path.relative_to(ROOT)}: " + f"{feature_id} {field}: " + f"{actual!r} != {expected_value!r}" + ) + + if all_found != set(catalog): + errors.append( + "Domain Markdown IDs do not match catalog JSON" + ) + + +def validate_catalog_markdown( + features: list[dict[str, Any]], + errors: list[str], +) -> None: + expected = { + feature["id"]: feature + for feature in features + } + found: dict[str, list[str]] = {} + + for line in CATALOG_MD_PATH.read_text( + encoding="utf-8" + ).splitlines(): + if not line.startswith("|"): + continue + + cells = [ + cell.strip() + for cell in line.strip().strip("|").split("|") + ] + + if len(cells) < 6: + continue + + feature_id = clean(cells[0]) + + if FEATURE_ID_RE.fullmatch(feature_id): + found[feature_id] = cells + + if set(found) != set(expected): + errors.append( + "feature-catalog.md IDs do not match JSON" + ) + + for feature_id, cells in found.items(): + feature = expected[feature_id] + runtime = feature["runtime_validation"] + runtime_display = ( + runtime["status"] + if runtime["required"] + else "not required" + ) + + actual = { + "name": cells[1].replace("\\|", "|"), + "domain": clean(cells[2]), + "status": clean(cells[3]), + "maturity": clean(cells[4]), + "runtime": clean(cells[5]), + } + + wanted = { + "name": feature["name"], + "domain": feature["domain"], + "status": feature["status"], + "maturity": feature["evidence_maturity"], + "runtime": runtime_display, + } + + for field, expected_value in wanted.items(): + if actual[field] != expected_value: + errors.append( + f"feature-catalog.md: {feature_id} " + f"{field}: {actual[field]!r} " + f"!= {expected_value!r}" + ) + + +def validate_reviews( + features: list[dict[str, Any]], + errors: list[str], +) -> int: + catalog = { + feature["id"]: feature + for feature in features + } + checked = 0 + + for path in sorted( + REVIEWS_DIR.glob("evidence-sample-*.md") + ): + current_id: str | None = None + results: set[str] = set() + + for line_number, line in enumerate( + path.read_text(encoding="utf-8").splitlines(), + start=1, + ): + heading = REVIEW_HEADING_RE.match(line) + + if heading: + current_id = heading.group(1) + continue + + result = REVIEW_RESULT_RE.match(line) + + if not result or current_id is None: + continue + + status, maturity = result.groups() + results.add(current_id) + checked += 1 + + feature = catalog.get(current_id) + + if feature is None: + errors.append( + f"{path.relative_to(ROOT)}:{line_number}: " + f"unknown feature {current_id}" + ) + continue + + if feature["status"] != status: + errors.append( + f"{current_id}: review status {status!r} " + f"!= catalog {feature['status']!r}" + ) + + if feature["evidence_maturity"] != maturity: + errors.append( + f"{current_id}: review maturity {maturity!r} " + f"!= catalog " + f"{feature['evidence_maturity']!r}" + ) + + if path.name == "evidence-sample-01.md" and len(results) != 12: + errors.append( + f"{path.relative_to(ROOT)}: expected 12 " + f"review results, found {len(results)}" + ) + + return checked + + +def validate_whitespace(errors: list[str]) -> None: + for path in sorted(ROOT.rglob("*")): + if not path.is_file(): + continue + + if path.suffix not in {".md", ".json", ".py", ".txt"}: + continue + + for line_number, line in enumerate( + path.read_text( + encoding="utf-8", + errors="replace", + ).splitlines(), + start=1, + ): + if line != line.rstrip(" \t"): + errors.append( + f"{path.relative_to(ROOT)}:{line_number}: " + "trailing whitespace" + ) + + if any(ROOT.rglob("*.pyc")): + errors.append("Generated .pyc files exist") + + if any( + path.is_dir() + for path in ROOT.rglob("__pycache__") + ): + errors.append("__pycache__ exists") + + +def main() -> int: + errors: list[str] = [] + features = load_catalog(errors) + reviewed = 0 + + if features: + validate_catalog(features, errors) + validate_domains(features, errors) + validate_catalog_markdown(features, errors) + reviewed = validate_reviews(features, errors) + + validate_whitespace(errors) + + print(f"Catalog Features: {len(features)}") + print( + "Unique Feature IDs:", + len({feature.get("id") for feature in features}), + ) + print( + "Domain Files:", + len(list(DOMAINS_DIR.glob("*.md"))), + ) + + if features: + print( + "Statuses:", + dict( + Counter( + feature.get("status") + for feature in features + ) + ), + ) + print( + "Evidence Maturity:", + dict( + Counter( + feature.get("evidence_maturity") + for feature in features + ) + ), + ) + print( + "Runtime Validation:", + dict( + Counter( + ( + feature.get( + "runtime_validation", + {}, + ).get("required"), + feature.get( + "runtime_validation", + {}, + ).get("status"), + ) + for feature in features + ) + ), + ) + + print(f"Review Results Checked: {reviewed}") + print(f"Consistency Errors: {len(errors)}") + + for error in errors: + print(f"ERROR: {error}") + + return 1 if errors else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/discovery/tools/validate_discovery_docs.py b/docs/discovery/tools/validate_discovery_docs.py new file mode 100755 index 000000000..656b800ec --- /dev/null +++ b/docs/discovery/tools/validate_discovery_docs.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Validate the Odysseus public discovery documentation package.""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +REPO_ROOT = ROOT.parents[1] +CATALOG_PATH = ROOT / "feature-catalog.json" +DOMAINS_DIR = ROOT / "domains" + +EXPECTED_COMMIT = "d8a2059df8e53bc7275c45339849d14c8651e73c" +ALLOWED_STATUSES = { + "verified", "partial", "disabled", "experimental", "legacy", + "dead-code-candidate", "unverified" +} + +LINE_RANGE_RE = re.compile(r"^L([1-9]\d*)-L([1-9]\d*)$") +FEATURE_HEADING_RE = re.compile( + r"^##\s+`?([A-Z][A-Z0-9]*-\d{3})`?\s+(?:—|-)\s+.+$", + re.MULTILINE, +) +LINK_RE = re.compile(r"(? argparse.Namespace: + parser = argparse.ArgumentParser(description="Validate Odysseus discovery docs.") + parser.add_argument( + "--repo-root", + type=Path, + default=REPO_ROOT, + help="Path to Odysseus repository root.", + ) + return parser.parse_args() + +def validate() -> int: + args = parse_args() + repo_root = args.repo_root.resolve() + errors: list[str] = [] + + # 1. Validate Catalog JSON existence and content + if not CATALOG_PATH.is_file(): + errors.append(f"Missing catalog file: {CATALOG_PATH}") + print(f"Errors: {len(errors)}") + for e in errors: + print(f"ERROR: {e}") + return 1 + + try: + catalog = json.loads(CATALOG_PATH.read_text(encoding="utf-8")) + except Exception as e: + errors.append(f"Failed to parse catalog JSON: {e}") + print(f"Errors: {len(errors)}") + for err in errors: + print(f"ERROR: {err}") + return 1 + + if not isinstance(catalog, list): + errors.append("feature-catalog.json must be a JSON array") + return 1 + + feature_ids = [item.get("id") for item in catalog if isinstance(item, dict)] + if len(feature_ids) != 79: + errors.append(f"Expected 79 unique feature IDs, found {len(feature_ids)}") + if len(feature_ids) != len(set(feature_ids)): + errors.append("Duplicate feature IDs found in catalog JSON") + + # Domain catalog counts + catalog_domain_counts: dict[str, int] = {} + for item in catalog: + if not isinstance(item, dict): + errors.append("Catalog item is not an object") + continue + fid = item.get("id", "") + status = item.get("status") + domain = item.get("domain", "").lower() + catalog_domain_counts[domain] = catalog_domain_counts.get(domain, 0) + 1 + + if status not in ALLOWED_STATUSES: + errors.append(f"{fid}: Invalid status '{status}'") + + evidence_list = item.get("evidence") + if not isinstance(evidence_list, list) or not evidence_list: + errors.append(f"{fid}: Missing or empty evidence list") + continue + + for ev in evidence_list: + if not isinstance(ev, dict): + errors.append(f"{fid}: Evidence item is not an object") + continue + path_str = ev.get("path") + lr_str = str(ev.get("line_range", "")) + if not path_str or Path(path_str).is_absolute() or ".." in Path(path_str).parts: + errors.append(f"{fid}: Unsafe or invalid path '{path_str}'") + continue + + # Check path exists in repo + target_path = repo_root / path_str + if not target_path.is_file(): + errors.append(f"{fid}: Referenced path '{path_str}' does not exist on disk") + continue + + # Check line range format & bounds + m = LINE_RANGE_RE.fullmatch(lr_str) + if not m: + errors.append(f"{fid}: Invalid line range format '{lr_str}' for path '{path_str}'") + continue + + start, end = int(m.group(1)), int(m.group(2)) + lines_cnt = len(target_path.read_text(encoding="utf-8", errors="ignore").splitlines()) + if start > end or end > lines_cnt or start < 1: + errors.append( + f"{fid}: Line range '{lr_str}' exceeds file length ({lines_cnt} lines) in '{path_str}'" + ) + + # 2. Check Domain Markdown files + md_feature_ids: list[str] = [] + domain_files = sorted(DOMAINS_DIR.glob("*.md")) + for df in domain_files: + domain_name = df.stem.lower() + content = df.read_text(encoding="utf-8") + found_ids = FEATURE_HEADING_RE.findall(content) + md_feature_ids.extend(found_ids) + if len(found_ids) != catalog_domain_counts.get(domain_name, 0): + errors.append( + f"Domain '{domain_name}' count mismatch: catalog has {catalog_domain_counts.get(domain_name, 0)}, Markdown has {len(found_ids)}" + ) + + if sorted(md_feature_ids) != sorted(feature_ids): + errors.append("Markdown domain feature IDs do not match catalog JSON feature IDs") + + # 3. Check for forbidden/private terms, sensitive credentials, and broken links across all docs + for md_file in ROOT.rglob("*.md"): + rel_md = md_file.relative_to(ROOT) + content = md_file.read_text(encoding="utf-8") + + # Forbidden terms scan + forbidden_matches = FORBIDDEN_TERMS_RE.findall(content) + if forbidden_matches: + errors.append( + f"{rel_md}: Found forbidden/internal terms: {set(forbidden_matches)}" + ) + + # Broken local link check + for target in LINK_RE.findall(content): + target = target.strip().strip("<>") + if not target or target.startswith(("#", "http://", "https://", "mailto:")): + continue + target_path = target.split("#", 1)[0] + resolved = (md_file.parent / target_path).resolve() + if not resolved.exists(): + errors.append(f"{rel_md}: Broken local link '{target}'") + + print(f"Catalog Features: {len(catalog)}") + print(f"Domain Files: {len(domain_files)}") + print(f"Validation Errors: {len(errors)}") + for err in errors: + print(f"ERROR: {err}") + + return 1 if errors else 0 + +if __name__ == "__main__": + sys.exit(validate()) diff --git a/docs/discovery/tools/validate_discovery_evidence.py b/docs/discovery/tools/validate_discovery_evidence.py new file mode 100644 index 000000000..4315478f7 --- /dev/null +++ b/docs/discovery/tools/validate_discovery_evidence.py @@ -0,0 +1,1413 @@ +#!/usr/bin/env python3 +"""Validate feature-catalog evidence locators against repository source. + +Evidence identity is resolved from a language parser where one is available. +The cited line range is treated as generated display metadata and checked only +after the locator has been resolved. +""" + +from __future__ import annotations + +import argparse +import ast +import difflib +import json +import re +import subprocess +import sys +from collections import Counter, defaultdict +from dataclasses import asdict, dataclass +from pathlib import Path +from typing import Any, Iterable + + +DISCOVERY_ROOT = Path(__file__).resolve().parents[1] +DEFAULT_REPO_ROOT = DISCOVERY_ROOT.parents[1] +DEFAULT_CATALOG = DISCOVERY_ROOT / "feature-catalog.json" + +RESULTS = { + "valid", + "invalid-path", + "invalid-locator", + "locator-outside-range", + "range-mismatch", + "ambiguous", + "unsupported", + "file-level-valid", +} + +PYTHON_KINDS = { + "python-function", + "python-method", + "python-class", + "python-variable", + "python-route", + "python-module", + "test-function", +} +JAVASCRIPT_KINDS = { + "javascript-function", + "javascript-class", + "javascript-export", + "javascript-event", +} +FILE_EXTENSIONS = { + ".html", + ".md", + ".swift", + ".yaml", + ".yml", +} +RANGE_RE = re.compile(r"^L([1-9]\d*)-L([1-9]\d*)$") +LEGACY_ROUTE_RE = re.compile( + r"^@(?P[A-Za-z_]\w*)\." + r"(?Pget|post|put|patch|delete|options|head)" + r"\(\s*['\"](?P[^'\"]*)['\"]\s*\)$", + re.IGNORECASE, +) +CANONICAL_ROUTE_RE = re.compile( + r"^(?PGET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+" + r"(?P\S+)(?:\s+->\s+(?P[\w.]+))?$" +) +SHELL_FUNCTION_RE = re.compile( + r"^\s*(?:function\s+)?(?P[A-Za-z_]\w*)\s*" + r"(?:\(\s*\))?\s*\{" +) +MARKDOWN_HEADING_RE = re.compile(r"^(?P#{1,6})\s+(?P.+?)\s*$") + + +@dataclass(frozen=True) +class Located: + name: str + kind: str + start: int + end: int + details: dict[str, Any] | None = None + + @property + def line_range(self) -> str: + return f"L{self.start}-L{self.end}" + + +@dataclass +class Validation: + feature_id: str + evidence_index: int + current_evidence: dict[str, Any] + inferred_kind: str + locator: str | None + result: str + problem: str | None = None + resolved: Located | None = None + suggested_kind: str | None = None + suggested_locator: str | None = None + generated_line_range: str | None = None + confidence: str = "high" + manual_review_reason: str | None = None + + def to_dict(self) -> dict[str, Any]: + data = asdict(self) + data["resolved"] = asdict(self.resolved) if self.resolved else None + return data + + +def parse_args(argv: list[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Validate discovery evidence identities and line ranges." + ) + parser.add_argument("--repo-root", type=Path, default=DEFAULT_REPO_ROOT) + parser.add_argument("--catalog", type=Path, default=DEFAULT_CATALOG) + parser.add_argument( + "--output-dir", + type=Path, + default=DISCOVERY_ROOT, + help="Directory for the report and repair queue.", + ) + return parser.parse_args(argv) + + +def load_catalog(path: Path) -> list[dict[str, Any]]: + data = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(data, list) or not all(isinstance(item, dict) for item in data): + raise ValueError("catalog must be an array of feature objects") + return data + + +def safe_target(repo_root: Path, raw_path: Any) -> Path | None: + if not isinstance(raw_path, str) or not raw_path: + return None + candidate = Path(raw_path) + if candidate.is_absolute() or ".." in candidate.parts: + return None + resolved = (repo_root / candidate).resolve() + try: + resolved.relative_to(repo_root.resolve()) + except ValueError: + return None + return resolved + + +def parse_range(raw: Any) -> tuple[int, int] | None: + if raw in (None, ""): + return None + match = RANGE_RE.fullmatch(str(raw)) + if not match: + return None + return int(match.group(1)), int(match.group(2)) + + +def extension_for(path: str) -> str: + suffix = Path(path).suffix.lower() + return suffix or "[none]" + + +def legacy_locator(evidence: dict[str, Any]) -> str | None: + value = evidence.get("locator", evidence.get("symbol")) + return value if isinstance(value, str) and value.strip() else None + + +def infer_kind(evidence: dict[str, Any], target: Path | None) -> str: + explicit = evidence.get("kind") + if isinstance(explicit, str) and explicit: + return explicit + + path = str(evidence.get("path", "")) + suffix = Path(path).suffix.lower() + locator = legacy_locator(evidence) or "" + + if suffix == ".py": + if LEGACY_ROUTE_RE.fullmatch(locator) or CANONICAL_ROUTE_RE.fullmatch(locator): + return "python-route" + if path.startswith("tests/") and locator.startswith("test"): + return "test-function" + if "." in locator and all(part.isidentifier() for part in locator.split(".")): + return "python-method" + if locator.isupper(): + return "python-variable" + if target and target.is_file(): + try: + tree = ast.parse(target.read_text(encoding="utf-8")) + except (OSError, SyntaxError, UnicodeError): + return "manual-review" + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef) and node.name == locator: + return "python-class" + return "python-function" + if suffix == ".js": + if locator.startswith("on:") or locator.startswith("event:"): + return "javascript-event" + if locator and locator[:1].isupper(): + return "javascript-class" + return "javascript-function" + if suffix in {".sh", ".bash"}: + return "shell-function" + if suffix == ".md": + return "documentation-section" + if suffix in {".json", ".yaml", ".yml", ".toml", ".ini"}: + return "configuration-key" + if suffix in FILE_EXTENSIONS or Path(path).name in {"Dockerfile"}: + return "manual-review" + if not suffix: + return "manual-review" + return "manual-review" + + +class PythonIndex(ast.NodeVisitor): + """Collect Python declarations, assignments, and decorated routes.""" + + def __init__(self) -> None: + self.symbols: list[Located] = [] + self.routes: list[Located] = [] + self.scope: list[str] = [] + self.constants: dict[str, str] = {} + self.router_prefixes: dict[str, str | None] = {} + + @classmethod + def from_path(cls, path: Path) -> "PythonIndex": + index = cls() + tree = ast.parse(path.read_text(encoding="utf-8")) + index._collect_static_values(tree) + index.visit(tree) + return index + + def _collect_static_values(self, tree: ast.AST) -> None: + for node in getattr(tree, "body", []): + if isinstance(node, (ast.Assign, ast.AnnAssign)): + value = self._string_value(node.value) + targets = node.targets if isinstance(node, ast.Assign) else [node.target] + for target in targets: + if isinstance(target, ast.Name) and value is not None: + self.constants[target.id] = value + if isinstance(target, ast.Name) and isinstance( + node.value, ast.Call + ): + call_name = self._call_name(node.value.func) + if call_name.endswith("APIRouter"): + prefix = self._router_prefix(node.value) + self.router_prefixes[target.id] = prefix + + def _qualified(self, name: str) -> str: + return ".".join([*self.scope, name]) + + @staticmethod + def _call_name(node: ast.AST) -> str: + if isinstance(node, ast.Name): + return node.id + if isinstance(node, ast.Attribute): + return f"{PythonIndex._call_name(node.value)}.{node.attr}".strip(".") + return "" + + def _string_value(self, node: ast.AST | None) -> str | None: + if isinstance(node, ast.Constant) and isinstance(node.value, str): + return node.value + if isinstance(node, ast.Name): + return self.constants.get(node.id) + if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): + left = self._string_value(node.left) + right = self._string_value(node.right) + return left + right if left is not None and right is not None else None + return None + + def _keyword_string(self, call: ast.Call, name: str) -> str | None: + for keyword in call.keywords: + if keyword.arg == name: + return self._string_value(keyword.value) + return None + + def _router_prefix(self, call: ast.Call) -> str | None: + if not any(keyword.arg == "prefix" for keyword in call.keywords): + return "" + return self._keyword_string(call, "prefix") + + def visit_ClassDef(self, node: ast.ClassDef) -> None: + self.symbols.append( + Located( + self._qualified(node.name), + "python-class", + node.lineno, + node.end_lineno or node.lineno, + ) + ) + self.scope.append(node.name) + self.generic_visit(node) + self.scope.pop() + + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + self._visit_function(node) + + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: + self._visit_function(node) + + def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: + qualified = self._qualified(node.name) + parent_is_class = bool( + self.scope + and any( + item.kind == "python-class" and item.name == ".".join(self.scope) + for item in self.symbols + ) + ) + kind = ( + "test-function" + if node.name.startswith("test") + else "python-method" + if parent_is_class + else "python-function" + ) + located = Located( + qualified, kind, node.lineno, node.end_lineno or node.lineno + ) + self.symbols.append(located) + self._collect_routes(node, qualified) + self.scope.append(node.name) + self.generic_visit(node) + self.scope.pop() + + def _collect_routes( + self, node: ast.FunctionDef | ast.AsyncFunctionDef, qualified: str + ) -> None: + methods = {"get", "post", "put", "patch", "delete", "options", "head"} + for decorator in node.decorator_list: + if not isinstance(decorator, ast.Call) or not isinstance( + decorator.func, ast.Attribute + ): + continue + method = decorator.func.attr.lower() + if method not in methods: + continue + router = self._call_name(decorator.func.value) + local_path = ( + self._string_value(decorator.args[0]) if decorator.args else None + ) + if local_path is None: + continue + prefix = self.router_prefixes.get(router) + full_path = None if router in self.router_prefixes and prefix is None else ( + f"{prefix or ''}{local_path}" + ) + details = { + "method": method.upper(), + "path": local_path, + "full_path": full_path, + "function": qualified, + "router": router, + "router_prefix": prefix, + "prefix_resolved": full_path is not None, + } + route_name = f"{method.upper()} {full_path or local_path} -> {qualified}" + self.routes.append( + Located( + route_name, + "python-route", + node.lineno, + node.end_lineno or node.lineno, + details, + ) + ) + + def visit_Assign(self, node: ast.Assign) -> None: + if isinstance(node.value, ast.Call): + call_name = self._call_name(node.value.func) + if call_name.endswith("APIRouter"): + for target in node.targets: + if isinstance(target, ast.Name): + self.router_prefixes[target.id] = self._router_prefix(node.value) + for target in node.targets: + self._collect_assignment(target, node) + self.generic_visit(node) + + def visit_AnnAssign(self, node: ast.AnnAssign) -> None: + self._collect_assignment(node.target, node) + self.generic_visit(node) + + def _collect_assignment(self, target: ast.AST, node: ast.AST) -> None: + if isinstance(target, (ast.Tuple, ast.List)): + for item in target.elts: + self._collect_assignment(item, node) + return + if not isinstance(target, ast.Name): + return + self.symbols.append( + Located( + self._qualified(target.id), + "python-variable", + node.lineno, + node.end_lineno or node.lineno, + ) + ) + + +NODE_PARSER = r""" +const fs = require("fs"); +const parserName = process.argv[1]; +const parserEntry = process.argv[2]; +const filename = process.argv[3]; +const source = fs.readFileSync(filename, "utf8"); +const parser = require(parserEntry); +let tree; +if (parserName === "@babel/parser") { + tree = parser.parse(source, { + sourceType: "unambiguous", + plugins: ["jsx", "classProperties", "optionalChaining", "topLevelAwait"] + }); +} else if (parserName === "typescript") { + throw new Error("typescript parser normalization is not implemented"); +} else { + const parse = parser.parse || parser.Parser?.parse; + tree = parse.call(parser, source, { + ecmaVersion: "latest", sourceType: "module", locations: true + }); +} +const out = []; +function loc(node) { + const l = node.loc; + return {start: l.start.line, end: l.end.line}; +} +function add(name, kind, node, extra={}) { + if (!name || !node.loc) return; + out.push({name, kind, ...loc(node), details: extra}); +} +function walk(node, scope=[], exported=false) { + if (!node || typeof node !== "object") return; + if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") { + if (node.declaration) walk(node.declaration, scope, true); + return; + } + if (node.type === "FunctionDeclaration") { + const name = node.id?.name; + add([...scope, name].filter(Boolean).join("."), exported ? "javascript-export" : "javascript-function", node); + scope = [...scope, name].filter(Boolean); + } else if (node.type === "ClassDeclaration") { + const name = node.id?.name; + add([...scope, name].filter(Boolean).join("."), exported ? "javascript-export" : "javascript-class", node); + scope = [...scope, name].filter(Boolean); + } else if (node.type === "MethodDefinition" || node.type === "ClassMethod") { + const name = node.key?.name || node.key?.value; + add([...scope, name].filter(Boolean).join("."), "javascript-function", node); + scope = [...scope, name].filter(Boolean); + } else if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") { + const initType = node.init?.type || ""; + const isFunction = initType === "ArrowFunctionExpression" || initType === "FunctionExpression"; + if (isFunction || exported) { + add([...scope, node.id.name].join("."), exported ? "javascript-export" : "javascript-function", node); + } + } else if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" + && node.callee.property?.name === "addEventListener" + && ["Literal", "StringLiteral"].includes(node.arguments?.[0]?.type)) { + add("event:" + node.arguments[0].value, "javascript-event", node, {event: node.arguments[0].value}); + } + for (const [key, value] of Object.entries(node)) { + if (key === "loc" || key === "start" || key === "end") continue; + if (Array.isArray(value)) value.forEach(child => walk(child, scope, exported)); + else if (value && typeof value === "object") walk(value, scope, exported); + } +} +walk(tree.program || tree); +process.stdout.write(JSON.stringify(out)); +""" + + +class JavascriptParser: + CANDIDATES = ("acorn", "espree", "@babel/parser", "typescript") + + def __init__(self, repo_root: Path) -> None: + self.repo_root = repo_root.resolve() + self.node = self._node_binary() + self.parser_name, self.parser_entry, self.reason = self._find_parser() + + @staticmethod + def _node_binary() -> str | None: + from shutil import which + + return which("node") + + def _package_roots(self) -> list[tuple[Path, set[str]]]: + roots: list[tuple[Path, set[str]]] = [] + for package in self.repo_root.rglob("package.json"): + if "node_modules" in package.parts: + continue + try: + data = json.loads(package.read_text(encoding="utf-8")) + except (OSError, ValueError): + continue + declared: set[str] = set() + for key in ("dependencies", "devDependencies", "peerDependencies"): + values = data.get(key) + if isinstance(values, dict): + declared.update(values) + roots.append((package.parent, declared)) + return roots + + def _find_parser(self) -> tuple[str | None, str | None, str]: + if not self.node: + return None, None, "Node.js is unavailable" + package_roots = self._package_roots() + for name in self.CANDIDATES: + for package_root, declared in package_roots: + local = package_root / "node_modules" / name + if name not in declared and not local.exists(): + continue + probe = subprocess.run( + [ + self.node, + "-e", + ( + "const p=require.resolve(process.argv[1]," + "{paths:[process.argv[2]]});process.stdout.write(p)" + ), + name, + str(package_root), + ], + cwd=package_root, + text=True, + capture_output=True, + check=False, + ) + if probe.returncode: + continue + resolved = Path(probe.stdout).resolve() + try: + resolved.relative_to(self.repo_root) + except ValueError: + continue + if name == "typescript": + return None, None, ( + "TypeScript is repository-local but its AST " + "normalization is not implemented" + ) + return name, str(resolved), f"repository-local parser: {name}" + return None, None, ( + "no repository-local Acorn, Espree, Babel parser, or supported " + "TypeScript parser is available" + ) + + @property + def supported(self) -> bool: + return bool(self.node and self.parser_name) + + def parse(self, path: Path) -> list[Located]: + if not self.supported: + raise RuntimeError(self.reason) + process = subprocess.run( + [ + self.node or "node", + "-e", + NODE_PARSER, + self.parser_name or "", + self.parser_entry or "", + str(path), + ], + cwd=self.repo_root, + text=True, + capture_output=True, + check=False, + ) + if process.returncode: + raise RuntimeError(process.stderr.strip() or "JavaScript parser failed") + return [Located(**item) for item in json.loads(process.stdout)] + + +def choose_symbol( + symbols: Iterable[Located], locator: str +) -> tuple[Located | None, list[Located]]: + candidates = list(symbols) + exact = [item for item in candidates if item.name == locator] + if not exact and "." not in locator: + exact = [item for item in candidates if item.name.split(".")[-1] == locator] + return (exact[0] if len(exact) == 1 else None), exact + + +def apply_range_result( + validation: Validation, located: Located, cited: tuple[int, int] | None +) -> Validation: + validation.resolved = located + validation.generated_line_range = located.line_range + validation.suggested_locator = located.name + validation.suggested_kind = located.kind + if cited is None: + if validation.current_evidence.get("line_range") not in (None, ""): + validation.result = "range-mismatch" + validation.problem = "line_range is malformed" + else: + validation.result = "valid" + return validation + start, end = cited + if start <= located.start <= end: + # Evidence ranges are excerpts. They must contain the declaration that + # gives the locator its identity, but need not contain the whole body. + validation.result = "valid" + else: + validation.result = "locator-outside-range" + validation.problem = ( + f"locator declaration at L{located.start} is outside cited " + f"L{start}-L{end}; parser-derived span is {located.line_range}" + ) + return validation + + +def suggest_nearby( + symbols: list[Located], locator: str, cited: tuple[int, int] | None +) -> tuple[Located | None, str]: + if cited: + overlapping = [ + item + for item in symbols + if cited[0] <= item.start <= cited[1] + or item.start <= cited[0] <= item.end + ] + if len(overlapping) == 1: + return overlapping[0], "high" + leaf_matches = [ + item + for item in overlapping + if item.name.split(".")[-1] == locator.split(".")[-1] + ] + if len(leaf_matches) == 1: + return leaf_matches[0], "high" + names = [item.name for item in symbols] + close = difflib.get_close_matches(locator, names, n=1, cutoff=0.72) + if close: + return next(item for item in symbols if item.name == close[0]), "medium" + return None, "low" + + +def validate_python( + validation: Validation, target: Path, cited: tuple[int, int] | None +) -> Validation: + try: + index = PythonIndex.from_path(target) + except (OSError, UnicodeError, SyntaxError) as exc: + validation.result = "unsupported" + validation.problem = f"Python AST parsing failed: {exc}" + validation.manual_review_reason = validation.problem + return validation + + locator = validation.locator or "" + if validation.inferred_kind == "python-module": + if locator: + validation.result = "invalid-locator" + validation.problem = "python-module evidence must not name a symbol" + else: + validation.result = "file-level-valid" + validation.suggested_kind = "python-module" + return validation + + if validation.inferred_kind == "python-route": + return validate_route(validation, index.routes, cited) + + located, exact = choose_symbol(index.symbols, locator) + if len(exact) > 1: + validation.result = "ambiguous" + validation.problem = f"locator matches {len(exact)} declarations" + validation.manual_review_reason = validation.problem + validation.confidence = "low" + return validation + if not located: + validation.result = "invalid-locator" + validation.problem = f"Python locator {locator!r} does not exist" + suggestion, confidence = suggest_nearby(index.symbols, locator, cited) + validation.confidence = confidence + if suggestion: + validation.suggested_kind = suggestion.kind + validation.suggested_locator = suggestion.name + validation.generated_line_range = suggestion.line_range + return validation + return apply_range_result(validation, located, cited) + + +def validate_route( + validation: Validation, routes: list[Located], cited: tuple[int, int] | None +) -> Validation: + locator = validation.locator or "" + legacy = LEGACY_ROUTE_RE.fullmatch(locator) + canonical = CANONICAL_ROUTE_RE.fullmatch(locator) + if not legacy and not canonical: + validation.result = "invalid-locator" + validation.problem = ( + "route locator must be '@router.method(\"/path\")' or " + "'METHOD /path -> qualified.function'" + ) + return validation + expected_method = (legacy or canonical).group("method").upper() + expected_path = (legacy or canonical).group("path") + expected_function = canonical.group("function") if canonical else None + method_matches = [ + route + for route in routes + if route.details and route.details["method"] == expected_method + ] + path_matches = [ + route + for route in method_matches + if route.details + and expected_path in {route.details["path"], route.details["full_path"]} + ] + if not method_matches: + validation.result = "invalid-locator" + validation.problem = f"HTTP method {expected_method} is not declared in this file" + return validation + if not path_matches: + validation.result = "invalid-locator" + validation.problem = ( + f"route path {expected_path!r} is not declared for {expected_method}" + ) + return validation + if expected_function: + function_matches = [ + route + for route in path_matches + if route.details and route.details["function"] == expected_function + ] + if not function_matches: + validation.result = "invalid-locator" + validation.problem = ( + f"route exists but containing function is not {expected_function!r}" + ) + return validation + path_matches = function_matches + if len(path_matches) > 1: + validation.result = "ambiguous" + validation.problem = f"route locator matches {len(path_matches)} functions" + validation.manual_review_reason = validation.problem + validation.confidence = "low" + return validation + route = path_matches[0] + details = route.details or {} + if details.get("full_path") is None and expected_path != details.get("path"): + validation.result = "ambiguous" + validation.problem = "final router prefix cannot be resolved statically" + validation.manual_review_reason = validation.problem + validation.resolved = route + validation.generated_line_range = route.line_range + validation.suggested_locator = route.name + validation.suggested_kind = "python-route" + validation.confidence = "medium" + return validation + return apply_range_result(validation, route, cited) + + +def validate_javascript( + validation: Validation, + target: Path, + cited: tuple[int, int] | None, + parser: JavascriptParser, +) -> Validation: + if not parser.supported: + validation.result = "unsupported" + validation.problem = parser.reason + validation.manual_review_reason = parser.reason + validation.confidence = "low" + return validation + try: + symbols = parser.parse(target) + except (OSError, RuntimeError, ValueError, json.JSONDecodeError) as exc: + validation.result = "unsupported" + validation.problem = f"JavaScript parser failed: {exc}" + validation.manual_review_reason = validation.problem + validation.confidence = "low" + return validation + locator = validation.locator or "" + located, exact = choose_symbol(symbols, locator) + if len(exact) > 1: + validation.result = "ambiguous" + validation.problem = f"locator matches {len(exact)} JavaScript declarations" + validation.manual_review_reason = validation.problem + validation.confidence = "low" + return validation + if not located: + validation.result = "invalid-locator" + validation.problem = f"JavaScript locator {locator!r} does not exist" + suggestion, confidence = suggest_nearby(symbols, locator, cited) + validation.confidence = confidence + if suggestion: + validation.suggested_kind = suggestion.kind + validation.suggested_locator = suggestion.name + validation.generated_line_range = suggestion.line_range + return validation + return apply_range_result(validation, located, cited) + + +def shell_functions(path: Path) -> list[Located]: + lines = path.read_text(encoding="utf-8").splitlines() + functions: list[Located] = [] + for index, line in enumerate(lines): + match = SHELL_FUNCTION_RE.match(line) + if not match: + continue + depth = 0 + end = index + 1 + for offset in range(index, len(lines)): + code = lines[offset].split("#", 1)[0] + depth += code.count("{") - code.count("}") + end = offset + 1 + if depth <= 0: + break + functions.append( + Located(match.group("name"), "shell-function", index + 1, end) + ) + return functions + + +def validate_shell( + validation: Validation, target: Path, cited: tuple[int, int] | None +) -> Validation: + try: + check = subprocess.run( + ["bash", "-n", str(target)], text=True, capture_output=True, check=False + ) + except OSError as exc: + validation.result = "unsupported" + validation.problem = f"bash unavailable: {exc}" + return validation + if check.returncode: + validation.result = "unsupported" + validation.problem = f"shell syntax check failed: {check.stderr.strip()}" + return validation + functions = shell_functions(target) + located, exact = choose_symbol(functions, validation.locator or "") + if len(exact) > 1: + validation.result = "ambiguous" + validation.problem = "shell function locator is duplicated" + validation.manual_review_reason = validation.problem + return validation + if not located: + validation.result = "invalid-locator" + validation.problem = f"shell function {validation.locator!r} does not exist" + return validation + return apply_range_result(validation, located, cited) + + +def validate_documentation( + validation: Validation, target: Path, cited: tuple[int, int] | None +) -> Validation: + locator = validation.locator or "" + if not locator: + validation.result = "invalid-locator" + validation.problem = "documentation-section requires a heading locator" + return validation + lines = target.read_text(encoding="utf-8").splitlines() + headings: list[Located] = [] + for number, line in enumerate(lines, 1): + match = MARKDOWN_HEADING_RE.match(line) + if not match: + continue + level = len(match.group("marks")) + end = len(lines) + for later, later_line in enumerate(lines[number:], number + 1): + later_match = MARKDOWN_HEADING_RE.match(later_line) + if later_match and len(later_match.group("marks")) <= level: + end = later - 1 + break + headings.append( + Located( + match.group("title").strip(), + "documentation-section", + number, + end, + ) + ) + located, exact = choose_symbol(headings, locator) + if len(exact) > 1: + validation.result = "ambiguous" + validation.problem = "documentation heading is duplicated" + validation.manual_review_reason = validation.problem + return validation + if not located: + validation.result = "invalid-locator" + validation.problem = f"documentation heading {locator!r} does not exist" + return validation + return apply_range_result(validation, located, cited) + + +def validate_evidence( + repo_root: Path, + feature_id: str, + index: int, + evidence: dict[str, Any], + javascript_parser: JavascriptParser, +) -> Validation: + target = safe_target(repo_root, evidence.get("path")) + kind = infer_kind(evidence, target) + locator = legacy_locator(evidence) + validation = Validation( + feature_id=feature_id, + evidence_index=index, + current_evidence=evidence, + inferred_kind=kind, + locator=locator, + result="ambiguous", + ) + if target is None or not target.is_file(): + validation.result = "invalid-path" + validation.problem = f"evidence path is unsafe or missing: {evidence.get('path')!r}" + validation.confidence = "high" + return validation + + cited = parse_range(evidence.get("line_range")) + explicit_range = evidence.get("line_range") + if explicit_range not in (None, "") and cited is None: + validation.result = "range-mismatch" + validation.problem = f"malformed line_range: {explicit_range!r}" + return validation + if cited is not None: + line_count = len( + target.read_text(encoding="utf-8", errors="replace").splitlines() + ) + if cited[0] > cited[1] or cited[1] > max(line_count, 1): + validation.result = "range-mismatch" + validation.problem = ( + f"line_range L{cited[0]}-L{cited[1]} is outside the " + f"{line_count}-line file" + ) + return validation + + if kind == "file": + if locator: + validation.result = "invalid-locator" + validation.problem = "file evidence must not include a locator/symbol" + validation.suggested_kind = "file" + validation.confidence = "high" + return validation + validation.result = "file-level-valid" + validation.suggested_kind = "file" + line_count = len(target.read_text(encoding="utf-8", errors="replace").splitlines()) + validation.generated_line_range = f"L1-L{max(line_count, 1)}" + return validation + + if not locator and kind not in {"python-module", "file"}: + validation.result = "invalid-locator" + validation.problem = f"{kind} evidence requires a locator" + return validation + + if kind in PYTHON_KINDS: + return validate_python(validation, target, cited) + if kind in JAVASCRIPT_KINDS: + return validate_javascript(validation, target, cited, javascript_parser) + if kind == "shell-function": + return validate_shell(validation, target, cited) + if kind == "documentation-section": + return validate_documentation(validation, target, cited) + if kind == "configuration-key": + validation.result = "ambiguous" + validation.problem = "configuration-key requires a format-aware parser" + validation.manual_review_reason = ( + "This validator does not guess configuration keys from text." + ) + validation.suggested_kind = ( + "file" if evidence.get("kind") is None else "manual-review" + ) + validation.confidence = "low" + return validation + + validation.result = "ambiguous" + validation.problem = f"no automatic resolver for inferred kind {kind!r}" + validation.manual_review_reason = ( + "Use explicit file evidence if the whole file is authoritative, " + "or manual-review with a precise reason." + ) + if evidence.get("kind") is None: + validation.suggested_kind = "file" + validation.suggested_locator = None + line_count = len(target.read_text(encoding="utf-8", errors="replace").splitlines()) + validation.generated_line_range = f"L1-L{max(line_count, 1)}" + validation.confidence = "medium" + return validation + + +def proposed_schema() -> dict[str, Any]: + return { + "required": ["path", "kind", "explanation"], + "optional": ["locator", "line_range"], + "properties": { + "path": "repository-relative file path", + "kind": sorted( + PYTHON_KINDS + | JAVASCRIPT_KINDS + | { + "shell-function", + "configuration-key", + "file", + "documentation-section", + "manual-review", + } + ), + "locator": ( + "parser-resolvable identity; omit for file evidence. " + "Routes use 'METHOD /path -> qualified.function'." + ), + "line_range": ( + "optional generated display metadata in Lx-Ly form; " + "never the primary identity" + ), + "explanation": "why this evidence supports the feature claim", + }, + "examples": [ + { + "path": "src/chat_handler.py", + "kind": "python-method", + "locator": "ChatHandler.preprocess_message", + "explanation": "Preprocesses attachments and URLs for chat requests.", + }, + { + "path": "Dockerfile", + "kind": "file", + "explanation": "Defines the container build.", + }, + ], + } + + +def assess_e2( + features: list[dict[str, Any]], validations: list[Validation] +) -> list[dict[str, Any]]: + """Record the audit's claim-relevance review for the ten frozen E2 records.""" + by_key = { + (item.feature_id, item.current_evidence.get("path"), item.locator): item + for item in validations + } + decisions = { + "CHAT-001": ( + "yes", + "Both cited tests exercise documented chat-stream behavior: emitted " + "stream metrics and non-destructive resend behavior.", + "retain", + ), + "MODEL-006": ( + "yes", + "The Node-backed test exercises the Copilot device-flow start/poll " + "contract and complete verification URI.", + "retain", + ), + "MODEL-007": ( + "yes", + "The Node-backed test exercises the ChatGPT subscription device-flow " + "contract and verification URI.", + "retain", + ), + "RESEARCH-003": ( + "partial", + "The test covers result ranking only, not SearXNG connectivity or " + "multi-provider dispatch in the feature claim.", + "demote", + ), + "DOCUMENT-002": ( + "no", + "The test covers removal of a PDF content marker, not PDF form " + "processing or high-fidelity rendering.", + "demote", + ), + "EMAIL-001": ( + "partial", + "The test covers health probing of account connections, not account " + "setup, SMTP behavior, or inbox polling.", + "demote", + ), + "SECURITY-002": ( + "yes", + "Relevant vault password-handling tests exist, but the cited test " + "function name is fabricated.", + "demote", + ), + "SECURITY-004": ( + "yes", + "Relevant prompt-injection tests exist, but both cited locator names " + "are fabricated umbrella names.", + "demote", + ), + "SECURITY-005": ( + "yes", + "Relevant URL and path confinement tests exist, but all three cited " + "locator names are fabricated umbrella names.", + "demote", + ), + "PLATFORM-009": ( + "manual-review", + "The shell diagnostic is executable evidence, but no automated test " + "function or test-suite evidence is cited and the script uses a " + "file-like fabricated symbol.", + "demote", + ), + } + assessments: list[dict[str, Any]] = [] + for feature in features: + if feature.get("evidence_maturity") != "E2": + continue + test_evidence = [ + evidence + for evidence in feature.get("evidence", []) + if str(evidence.get("path", "")).startswith("tests/") + ] + cited: list[dict[str, Any]] = [] + for evidence in test_evidence: + item = by_key.get( + (feature["id"], evidence.get("path"), legacy_locator(evidence)) + ) + cited.append( + { + "path": evidence.get("path"), + "locator": legacy_locator(evidence), + "exists": bool( + item + and item.result + not in {"invalid-path", "invalid-locator", "unsupported"} + ), + "validation_result": item.result if item else "not-validated", + } + ) + relevance, reason, decision = decisions[feature["id"]] + assessments.append( + { + "feature_id": feature["id"], + "feature_name": feature["name"], + "cited_test_evidence": cited, + "test_function_exists": ( + all(item["exists"] for item in cited) if cited else False + ), + "direct_support": relevance, + "support_reason": reason, + "suggested_decision": decision, + } + ) + return assessments + + +def build_report( + repo_root: Path, + catalog_path: Path, + features: list[dict[str, Any]], + validations: list[Validation], + javascript_parser: JavascriptParser, +) -> dict[str, Any]: + extensions = Counter() + kinds = Counter() + for item in validations: + extensions[extension_for(str(item.current_evidence.get("path", "")))] += 1 + kinds[item.inferred_kind] += 1 + results = Counter(item.result for item in validations) + affected = sorted( + { + item.feature_id + for item in validations + if item.result not in {"valid", "file-level-valid"} + } + ) + invalid_results = { + "invalid-path", + "invalid-locator", + "locator-outside-range", + "range-mismatch", + } + invalid_features = sorted( + { + item.feature_id + for item in validations + if item.result in invalid_results + } + ) + ambiguous = [ + item.to_dict() + for item in validations + if item.result in {"ambiguous", "unsupported"} + ] + return { + "audit": { + "repo_root": str(repo_root), + "catalog": str(catalog_path), + "feature_count": len(features), + "evidence_count": len(validations), + "javascript_parser": { + "supported": javascript_parser.supported, + "parser": javascript_parser.parser_name, + "reason": javascript_parser.reason, + }, + }, + "inventory": { + "total_evidence_items": len(validations), + "by_extension": dict(sorted(extensions.items())), + "by_inferred_evidence_type": dict(sorted(kinds.items())), + "features_with_invalid_or_unresolved_entries": affected, + "features_with_invalid_entries": invalid_features, + "invalid_feature_count": len(invalid_features), + "affected_feature_count": len(affected), + "ambiguous_or_unsupported_entries": ambiguous, + }, + "result_counts": {name: results.get(name, 0) for name in sorted(RESULTS)}, + "summary_counts": { + "valid": results["valid"] + results["file-level-valid"], + "invalid": sum( + results[name] + for name in ( + "invalid-path", + "invalid-locator", + "locator-outside-range", + "range-mismatch", + ) + ), + "ambiguous": results["ambiguous"], + "unsupported": results["unsupported"], + }, + "evidence_model": proposed_schema(), + "e2_assessment": assess_e2(features, validations), + "entries": [item.to_dict() for item in validations], + } + + +def build_repair_queue(validations: list[Validation]) -> dict[str, Any]: + grouped: dict[str, list[dict[str, Any]]] = defaultdict(list) + for item in validations: + if item.result in {"valid", "file-level-valid"}: + continue + grouped[item.feature_id].append( + { + "evidence_index": item.evidence_index, + "current_evidence": item.current_evidence, + "detected_problem": item.problem or item.result, + "result": item.result, + "suggested_kind": item.suggested_kind, + "suggested_locator": item.suggested_locator, + "generated_line_range": item.generated_line_range, + "confidence": item.confidence, + "manual_review_reason": item.manual_review_reason, + } + ) + return { + "feature_count": len(grouped), + "defect_count": sum(len(items) for items in grouped.values()), + "features": [ + {"feature_id": feature_id, "defects": grouped[feature_id]} + for feature_id in sorted(grouped) + ], + } + + +def render_markdown(report: dict[str, Any]) -> str: + inventory = report["inventory"] + summary = report["summary_counts"] + lines = [ + "# Discovery evidence validation report", + "", + "This report is machine-generated. The catalog was not edited.", + "", + "## Summary", + "", + f"- Features: {report['audit']['feature_count']}", + f"- Evidence items: {report['audit']['evidence_count']}", + f"- Valid (including file-level): {summary['valid']}", + f"- Invalid: {summary['invalid']}", + f"- Ambiguous: {summary['ambiguous']}", + f"- Unsupported: {summary['unsupported']}", + f"- Affected features: {inventory['affected_feature_count']}", + "", + "## Evidence by extension", + "", + "| Extension | Items |", + "|---|---:|", + ] + lines.extend( + f"| `{extension}` | {count} |" + for extension, count in inventory["by_extension"].items() + ) + lines.extend( + [ + "", + "## Evidence by inferred kind", + "", + "| Kind | Items |", + "|---|---:|", + ] + ) + lines.extend( + f"| `{kind}` | {count} |" + for kind, count in inventory["by_inferred_evidence_type"].items() + ) + lines.extend( + [ + "", + "## Result categories", + "", + "| Result | Items |", + "|---|---:|", + ] + ) + lines.extend( + f"| `{result}` | {count} |" + for result, count in report["result_counts"].items() + ) + lines.extend( + [ + "", + "## JavaScript parser", + "", + report["audit"]["javascript_parser"]["reason"] + ".", + "", + "## Affected features", + "", + ", ".join( + f"`{feature_id}`" + for feature_id in inventory[ + "features_with_invalid_or_unresolved_entries" + ] + ) + or "None.", + "", + "## E2 relevance review", + "", + "| Feature | Cited test evidence | Exists | Direct support | Decision |", + "|---|---|---:|---|---|", + ] + ) + for item in report["e2_assessment"]: + citations = "<br>".join( + f"`{test['path']}:{test['locator']}`" + for test in item["cited_test_evidence"] + ) or "None" + lines.append( + f"| `{item['feature_id']}` | {citations} | " + f"{'yes' if item['test_function_exists'] else 'no'} | " + f"{item['direct_support']}: {item['support_reason']} | " + f"**{item['suggested_decision']}** |" + ) + lines.extend( + [ + "", + "### Numerical inconsistency resolved", + "", + "The six problematic cited E2 test records are the six nonexistent " + "locator names in SECURITY-002 (one), SECURITY-004 (two), and " + "SECURITY-005 (three). Seven feature demotions were suggested because " + "PLATFORM-009 is an additional feature-level maturity problem: it " + "cites no automated test function. The other three demotions are " + "claim-relevance findings for existing tests, so the figures measure " + "different things and should not be compared as the same denominator.", + "", + "## Proposed evidence schema", + "", + "Required: `path`, `kind`, and `explanation`. `locator` is required " + "for symbol/route/section evidence and omitted for `file`. " + "`line_range` is optional generated metadata.", + "", + "```json", + json.dumps(report["evidence_model"]["examples"], indent=2), + "```", + "", + "## Defects", + "", + "| Feature | Path | Locator | Result | Generated range | Problem |", + "|---|---|---|---|---|---|", + ] + ) + for item in report["entries"]: + if item["result"] in {"valid", "file-level-valid"}: + continue + evidence = item["current_evidence"] + problem = (item["problem"] or "").replace("|", "\\|") + lines.append( + f"| `{item['feature_id']}` | `{evidence.get('path', '')}` | " + f"`{item.get('locator') or ''}` | `{item['result']}` | " + f"`{item.get('generated_line_range') or ''}` | {problem} |" + ) + return "\n".join(lines) + "\n" + + +def write_outputs(output_dir: Path, report: dict[str, Any], queue: dict[str, Any]) -> None: + output_dir.mkdir(parents=True, exist_ok=True) + (output_dir / "evidence-validation-report.json").write_text( + json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" + ) + (output_dir / "evidence-validation-report.md").write_text( + render_markdown(report), encoding="utf-8" + ) + (output_dir / "evidence-repair-queue.json").write_text( + json.dumps(queue, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" + ) + + +def run(argv: list[str] | None = None) -> int: + args = parse_args(argv) + repo_root = args.repo_root.resolve() + catalog_path = args.catalog.resolve() + features = load_catalog(catalog_path) + javascript_parser = JavascriptParser(repo_root) + validations = [ + validate_evidence( + repo_root, + str(feature.get("id", f"<feature:{feature_index}>")), + evidence_index, + evidence, + javascript_parser, + ) + for feature_index, feature in enumerate(features) + for evidence_index, evidence in enumerate(feature.get("evidence", [])) + if isinstance(evidence, dict) + ] + report = build_report( + repo_root, catalog_path, features, validations, javascript_parser + ) + queue = build_repair_queue(validations) + write_outputs(args.output_dir.resolve(), report, queue) + print(json.dumps(report["summary_counts"], sort_keys=True)) + return 1 if queue["defect_count"] else 0 + + +if __name__ == "__main__": + sys.exit(run()) From 91764ad3e79f8a5acf6bda8ef9585814d97fe214 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <alexandremagteixeira@gmail.com> Date: Sun, 26 Jul 2026 14:13:15 +0100 Subject: [PATCH 002/125] docs(discovery): replace audit catalog with system maps --- docs/discovery/BASELINE-STATUS.md | 62 - docs/discovery/README.md | 21 - docs/discovery/audit-method.md | 80 - docs/discovery/domains/agent.md | 153 - docs/discovery/domains/calendar.md | 45 - docs/discovery/domains/chat.md | 197 - docs/discovery/domains/contact.md | 24 - docs/discovery/domains/cookbook.md | 85 - docs/discovery/domains/document.md | 108 - docs/discovery/domains/email.md | 87 - docs/discovery/domains/frontend.md | 149 - docs/discovery/domains/media.md | 170 - docs/discovery/domains/memory.md | 24 - docs/discovery/domains/model.md | 177 - docs/discovery/domains/note.md | 24 - docs/discovery/domains/platform.md | 213 - docs/discovery/domains/research.md | 86 - docs/discovery/domains/security.md | 154 - docs/discovery/domains/skill.md | 24 - docs/discovery/feature-catalog.json | 4797 ----------------- docs/discovery/feature-catalog.md | 85 - .../discovery/references/source-provenance.md | 28 - .../tools/test_validate_discovery_evidence.py | 224 - .../tools/validate_discovery_consistency.py | 592 -- .../tools/validate_discovery_docs.py | 172 - .../tools/validate_discovery_evidence.py | 1413 ----- specs/discovery/README.md | 30 + specs/discovery/safety-boundaries.md | 129 + specs/discovery/system-map.md | 123 + 29 files changed, 282 insertions(+), 9194 deletions(-) delete mode 100644 docs/discovery/BASELINE-STATUS.md delete mode 100644 docs/discovery/README.md delete mode 100644 docs/discovery/audit-method.md delete mode 100644 docs/discovery/domains/agent.md delete mode 100644 docs/discovery/domains/calendar.md delete mode 100644 docs/discovery/domains/chat.md delete mode 100644 docs/discovery/domains/contact.md delete mode 100644 docs/discovery/domains/cookbook.md delete mode 100644 docs/discovery/domains/document.md delete mode 100644 docs/discovery/domains/email.md delete mode 100644 docs/discovery/domains/frontend.md delete mode 100644 docs/discovery/domains/media.md delete mode 100644 docs/discovery/domains/memory.md delete mode 100644 docs/discovery/domains/model.md delete mode 100644 docs/discovery/domains/note.md delete mode 100644 docs/discovery/domains/platform.md delete mode 100644 docs/discovery/domains/research.md delete mode 100644 docs/discovery/domains/security.md delete mode 100644 docs/discovery/domains/skill.md delete mode 100644 docs/discovery/feature-catalog.json delete mode 100644 docs/discovery/feature-catalog.md delete mode 100644 docs/discovery/references/source-provenance.md delete mode 100644 docs/discovery/tools/test_validate_discovery_evidence.py delete mode 100755 docs/discovery/tools/validate_discovery_consistency.py delete mode 100755 docs/discovery/tools/validate_discovery_docs.py delete mode 100644 docs/discovery/tools/validate_discovery_evidence.py create mode 100644 specs/discovery/README.md create mode 100644 specs/discovery/safety-boundaries.md create mode 100644 specs/discovery/system-map.md diff --git a/docs/discovery/BASELINE-STATUS.md b/docs/discovery/BASELINE-STATUS.md deleted file mode 100644 index 7cb6ec391..000000000 --- a/docs/discovery/BASELINE-STATUS.md +++ /dev/null @@ -1,62 +0,0 @@ -# Discovery Baseline Status - -## Purpose and scope - -This package is a commit-pinned discovery baseline and feature index for the `discovery` branch at frozen commit `d8a2059df8e53bc7275c45339849d14c8651e73c`. It inventories **79 feature records** across **16 domains** to help maintainers locate likely implementation areas and identify validation gaps. It is not an authoritative architecture reference or a runtime-certification record. - -The canonical inventory is [`feature-catalog.json`](feature-catalog.json); [`feature-catalog.md`](feature-catalog.md) and the files in [`domains/`](domains/) are derived reading views. See [`audit-method.md`](audit-method.md) for the status and maturity definitions. - -## What maintainers may use now - -- Use the catalog and domain views as a frozen discovery index, including their feature IDs, stated scope, likely source locations, and declared runtime prerequisites. -- Treat a catalog status such as `verified` as meaning implementation was identified during discovery. It does **not** mean every evidence locator, line range, test claim, or runtime behaviour has passed semantic validation. -- Use the structural checks to confirm package shape and cross-view consistency; use the evidence validator to assess whether individual evidence assertions are semantically supported. - -## Validation snapshot - -Structural validation passes: the catalog has 79 records, the 16 domain views match it, and the package structural validators pass. All 11 focused evidence-validator tests pass. - -The evidence validator found **170 evidence entries**: **82 valid**, **66 invalid**, **6 ambiguous**, and **16 unsupported**. Its non-zero exit is expected while those semantic evidence defects remain. - -Structural validity checks the documentation schema, record counts, derived-view consistency, file existence, line-range bounds, links, and public-safety rules. Semantic evidence validity additionally checks whether the cited locator exists, falls within its cited range, uses a supported parser, and actually supports the feature claim. Passing the former does not establish the latter. - -## E2 review decisions - -E2 means directly relevant automated test evidence supports the feature claim; a test file’s existence alone is insufficient. The generated evidence report was used to reassess all ten E2 records. - -| Feature | Decision | Reason | -|---|---|---| -| `CHAT-001` | Retain E2 | Two cited tests directly exercise documented streaming-related behaviour. | -| `MODEL-006` | Retain E2 | The cited device-flow test exercises the Copilot start/poll contract and verification URI. | -| `MODEL-007` | Retain E2 | The cited device-flow test exercises the ChatGPT subscription contract and verification URI. | -| `RESEARCH-003` | Demote to E1 | The cited test covers ranking, not provider connectivity or dispatch; the route-to-provider implementation path was identified. | -| `DOCUMENT-002` | Demote to E1 | The cited marker test does not support form processing or rendering; the document route does call the PDF form handlers. | -| `EMAIL-001` | Demote to E1 | The cited health test is narrower than setup, SMTP, and polling; application setup invokes the email router and its poller. | -| `SECURITY-002` | Demote to E1 | Relevant vault-password tests exist, but the cited test locator is fabricated; the application mounts the vault route implementation. | -| `SECURITY-004` | Demote to E1 | Relevant injection tests exist, but the cited locator names are fabricated; callers use the documented context guard. | -| `SECURITY-005` | Demote to E1 | Relevant guard tests exist, but the cited locator names are fabricated; route code calls the documented URL guard. | -| `PLATFORM-009` | Demote to E0 | The manifests and diagnostic script establish discovered operational artifacts, not a traced application path or directly relevant automated test. | - -The current maturity distribution is **E0: 68**, **E1: 8**, **E2: 3**, **E3: 0**, **E4: 0**. Runtime validation is still pending where the catalog says it requires external services, interactive authentication, specialised hardware, or host Docker GPU support. - -## Known discovery caveats - -- `AGENT-004` includes a legacy no-op activity-log shim rather than active assistant-log behaviour. -- `FRONTEND-007` points to a missing `static/backgrounds.html` target; its existing variant pages do not make that route functional. -- `RESEARCH-003` retains a compatibility module that aliases the canonical search implementation, and `DOCUMENT-002` separates optional PDF viewing from form handling. -- Secret-storage and vault-command handling are distinct implementation areas; this index does not make an end-to-end security guarantee. - -## Recommended next documentation work - -Repair semantic evidence selectively while architecture and operations documentation is written, beginning with the seven E2 demotions and maintainer-owned feature descriptions. Do not wait for the complete evidence queue before documenting the system. Record controlled runtime observations when external services, credentials, hardware, or Docker GPU access are available, and label unsupported claims explicitly. - -## Validation commands - -```bash -PYTHONDONTWRITEBYTECODE=1 python3 docs/discovery/tools/validate_discovery_docs.py --repo-root . -PYTHONDONTWRITEBYTECODE=1 python3 docs/discovery/tools/validate_discovery_consistency.py -PYTHONDONTWRITEBYTECODE=1 python3 -m unittest docs/discovery/tools/test_validate_discovery_evidence.py -v -PYTHONDONTWRITEBYTECODE=1 python3 docs/discovery/tools/validate_discovery_evidence.py --repo-root . --catalog docs/discovery/feature-catalog.json --output-dir <local-report-directory> -``` - -Supply a local report directory outside `docs/discovery/` for the final command so generated reports are not added to the package. diff --git a/docs/discovery/README.md b/docs/discovery/README.md deleted file mode 100644 index 1a397da74..000000000 --- a/docs/discovery/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Odysseus Discovery Package - -## Provisional discovery baseline - -This is a commit-pinned discovery baseline and feature index for the `discovery` branch at `d8a2059df8e53bc7275c45339849d14c8651e73c`. It contains 79 feature records across 16 domains. It is **not** an authoritative architecture reference, a runtime certification, or a claim that every evidence citation is semantically valid. - -Read [`BASELINE-STATUS.md`](BASELINE-STATUS.md) first for the publication status, evidence-validation totals, E2 decisions, known caveats, and the recommended next documentation work. - -## Package contents - -- [`feature-catalog.json`](feature-catalog.json) is the canonical machine-readable catalog. -- [`feature-catalog.md`](feature-catalog.md) and [`domains/`](domains/) are derived reading views. -- [`audit-method.md`](audit-method.md) defines feature status and evidence maturity. -- [`references/source-provenance.md`](references/source-provenance.md) records the frozen snapshot. -- [`tools/`](tools/) contains the structural, consistency, and evidence validators. - -A feature status such as `verified` means implementation was identified during discovery. It does not mean every evidence locator, line range, test claim, or runtime behaviour has passed semantic validation. - -## Validation - -Run the commands in [`BASELINE-STATUS.md`](BASELINE-STATUS.md#validation-commands). Structural checks and semantic evidence checks have different purposes; see that status document for the current results and interpretation. diff --git a/docs/discovery/audit-method.md b/docs/discovery/audit-method.md deleted file mode 100644 index b2c3bd1f1..000000000 --- a/docs/discovery/audit-method.md +++ /dev/null @@ -1,80 +0,0 @@ -# Odysseus Functional Audit Methodology - -## Purpose - -This document specifies the methodology and evidence standards for the read-only discovery audit of **Odysseus**. - -## Snapshot Baseline - -- **Repository**: `odysseus-dev/odysseus` -- **Audit Target Branch**: `discovery` -- **Frozen Commit SHA**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Snapshot Date**: `2026-07-23T14:49:02Z` -- **Audit Mode**: Read-Only inventory & documentation review - -## Rules of Engagement - -1. **No Code Mutations**: Application code and tests outside `docs/discovery/` remain untouched. -2. **No External Operations**: No GitHub issues, PRs, comments, labels, or branch mutations. -3. **Zero Inferred Success**: Documentation claims require empirical evidence of implementation and reachability. Filenames, README descriptions, and docstrings alone do not constitute proof. -4. **Strict Status Categorization**: All capabilities are assigned exactly one authorized status: - - `verified`: Implemented, reachable, and supported by code evidence. - - `partial`: Partially implemented or missing full frontend/backend connection. - - `disabled`: Gated off by default feature flags or configuration. - - `experimental`: Active but requiring non-standard hardware or runtimes. - - `legacy`: Obsolete feature retained for backwards compatibility. - - `dead-code-candidate`: Code exists but is unreachable from UI or API routes. - - `unverified`: Implementation present but untestable without external secrets or hardware. - -## Evidence Maturity Scale - -Evidence maturity is evaluated independently from catalog feature status. A feature status such as `verified` records that implementation was identified during discovery; it is not a statement that every evidence locator, test claim, line range, or runtime behaviour has passed semantic validation. - -- **E0 - Discovered**: Candidate identified in documentation, route declaration, or source file. -- **E1 - Code-path traced**: Frontend/API entry point connected through services and data handlers. -- **E2 - Test-backed**: At least one directly relevant automated test assertion supports the feature claim. A test file's existence, an unrelated assertion, or an invalid test locator does not establish E2. -- **E3 - Runtime-validated**: Maintainer reproduced behavior in a recorded local environment. -- **E4 - Maintainer-accepted**: Maintainers accepted the feature description and support status. - -## Audit Workflow - -```mermaid -flowchart TD - P0["Phase 0: Snapshot Isolation<br/>(Commit d8a2059)"] --> P1["Phase 1: Codebase Discovery<br/>(Routes, Services, Static JS, Specs)"] - P1 --> P2["Phase 2: Feature Reachability & Verification<br/>(Route matching, FE entrypoints, tests)"] - P2 --> P3["Phase 3: Catalog & Evidence Compilation<br/>(File paths, symbols, exact line ranges)"] - P3 --> P4["Phase 4: Quality & Integrity Audit<br/>(100% path existence check, schema validation)"] -``` - -### Phase 0: Snapshot Isolation -The audit is pinned to git commit `d8a2059df8e53bc7275c45339849d14c8651e73c`. All file paths, symbol declarations, and line ranges map strictly to this commit. - -### Phase 1: Codebase Discovery -All top-level and nested directories were traversed, including: -- Backend Entry Points (`app.py`, `routes/`, `routes/*/*.py`, `companion/`) -- Core Framework (`core/database.py`, `core/session_manager.py`, `core/auth.py`) -- Business Logic Services (`src/`, `services/`, `mcp_servers/`) -- Frontend Assets (`static/app.js`, `static/js/`, `static/index.html`) -- Test Suites (`tests/`, `tests/cli/`, `tests/streaming/`) -- Operations & Docker (`Dockerfile`, `docker-compose*.yml`, `scripts/`) - -### Phase 2: Verification Protocol -For each feature candidate, the following table was evaluated: -- **User Reachability**: Frontend UI element, modal, route, or CLI script. -- **API Entrypoint**: FastAPI `@router` declaration or WebSocket/SSE handler. -- **Backend Execution**: Concrete Python module method, service, or tool call. -- **Data Persistence**: Disk file, SQLite table, or vector collection. -- **Test Coverage**: Automated test file executing assertions against the component. - -### Phase 3: Evidence Linking Standard -Every feature entry in `feature-catalog.json` contains a structured `evidence` list with: -- `path`: Relative path from repository root. -- `symbol`: Route, class, function, or element symbol name. -- `line_range`: Inclusive line range (e.g. `L120-L250`). -- `explanation`: Short factual statement proving reachability or implementation. - -### Phase 4: Quality Check & Schema Constraints -Before finalization: -1. Every evidence file path is validated against the checkout. -2. Every Markdown entry matches `feature-catalog.json`. -3. Recommendation language is separated from empirical factual observations. diff --git a/docs/discovery/domains/agent.md b/docs/discovery/domains/agent.md deleted file mode 100644 index 82574ddc3..000000000 --- a/docs/discovery/domains/agent.md +++ /dev/null @@ -1,153 +0,0 @@ -# Agent - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## AGENT-001 — Autonomous Agent Loop & Tool Execution Engine - -- **Domain**: `agent` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Executes multi-step agent reasoning loops, tool invocation parsing, and automated response generation. - -### Evidence summary - -- `src/agent_loop.py` — `run_agent_loop` — Core loop evaluating model tool requests and executing handlers. -- `src/tool_execution.py` — `execute_tool_call` — Dispatches tool invocation requests to underlying tool handlers. - -### Unknowns - -- Infinite tool loop if termination condition fails. - -## AGENT-002 — Scheduled Tasks & Event Bus Dispatcher - -- **Domain**: `agent` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Schedules background recurring or delayed tasks, emits event bus triggers, and executes automated flows. - -### Evidence summary - -- `routes/task_routes.py` — `@router.get('')` — Fetches active scheduled tasks. -- `src/task_scheduler.py` — `TaskScheduler` — Async task scheduler dispatching cron and delay triggers. - -### Unknowns - -- Task execution failure handling on system restart. - -## AGENT-003 — Webhook Event Subscriptions & Trigger Processing - -- **Domain**: `agent` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Manages incoming/outgoing webhook subscriptions, endpoint authentication tokens, and event triggers. - -### Evidence summary - -- `routes/webhook_routes.py` — `@router.get('/webhooks')` — Returns list of registered webhooks. -- `src/webhook_manager.py` — `WebhookManager` — Handles payload delivery and signature verification. - -### Unknowns - -- SSRF risks when contacting external webhook URLs if unvalidated. - -## AGENT-004 — Assistant Settings, Task Check-Ins & Background Job Monitor - -- **Domain**: `agent` -- **Status**: `partial` -- **Evidence Maturity**: `E1` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Manages per-user assistant sessions and scheduled check-in settings, drains background job completions, and retains a legacy no-op activity logging shim. - -### Evidence summary - -- `routes/assistant_routes.py` — `setup_assistant_routes` — Active assistant session, settings, manual check-in, run-status and timezone-list endpoints, including the owner-scoping guards. -- `src/bg_monitor.py` — `_drain_agent` — Runs the agent loop headless against a session to produce the background-job follow-up turn. -- `src/bg_monitor.py` — `_run_followup` — Drains completed background jobs and auto-continues the owning session, deferring while a live turn is in progress. -- `src/assistant_log.py` — `log_to_assistant` — Legacy no-op activity logging shim retained for existing callers; documented as inactive rather than as current behaviour. - -### Unknowns - -- Route `/api/assistant/logs` cited in legacy docs is absent from assistant router. -- Existing unit test `tests/cli/test_logs_cli_resolve_nonstring.py` tests CLI target-name resolution logic, not active assistant routes or bg_monitor execution loop. - -## AGENT-005 — Model Context Protocol (MCP) Server Integration - -- **Domain**: `agent` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Integrates external MCP servers over stdio/SSE to expand agent capabilities dynamically. - -### Evidence summary - -- `routes/mcp_routes.py` — `setup_mcp_routes` — Exposes management endpoints for external MCP servers. -- `src/mcp_manager.py` — `McpManager` — Manages MCP server subprocess lifecycles. - -### Unknowns - -- Subprocess leaks if external MCP server process fails to terminate clean. - -## AGENT-006 — AI Interaction Tools & Pipeline Orchestration - -- **Domain**: `agent` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Provides specialized AI interaction tools for agent self-debugging, debate, and multi-model collaboration. - -### Evidence summary - -- `src/ai_interaction.py` — `init_ai_interaction_tools` — Registers specialized multi-agent interaction primitives. -- `src/builtin_actions.py` — `execute_builtin_action` — Executes pre-built action intent sequences. - -### Unknowns - -- High API token consumption during extended agent debates. - -## AGENT-007 — Subprocess & Background Job Execution Tools - -- **Domain**: `agent` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Provides sandboxed bash/shell tool execution capabilities with output streaming and background tracking. - -### Evidence summary - -- `src/agent_tools/subprocess_tools.py` — `run_command` — Executes shell commands in background/foreground. -- `src/bg_jobs.py` — `JobManager` — Tracks async background subprocess tasks. - -### Unknowns - -- Arbitrary shell command execution permissions if sandbox confinement fails. diff --git a/docs/discovery/domains/calendar.md b/docs/discovery/domains/calendar.md deleted file mode 100644 index 4a2dad8d3..000000000 --- a/docs/discovery/domains/calendar.md +++ /dev/null @@ -1,45 +0,0 @@ -# Calendar - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## CALENDAR-001 — CalDAV Calendar Synchronization & Account Setup - -- **Domain**: `calendar` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: pending — Requires a controlled external CalDAV server. - -### Purpose - -Connects to remote CalDAV servers (Apple iCloud, Nextcloud, Google) to sync calendar event feeds. - -### Evidence summary - -- `routes/calendar_routes.py` — `setup_calendar_routes` — Exposes CalDAV setup and manual sync trigger routes. -- `src/caldav_sync.py` — `CalDavSync` — Fetches and parses remote iCalendar VEVENT objects. - -### Unknowns - -- Invalid SSL certificates on self-hosted CalDAV servers. - -## CALENDAR-002 — Calendar Event Operations & iCalendar Parsing - -- **Domain**: `calendar` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Creates, updates, deletes, and displays calendar events with timezone conversion and reminder notifications. - -### Evidence summary - -- `routes/calendar_routes.py` — `@router.get('/events')` — Fetches calendar events for requested date window. -- `src/tools/calendar.py` — `CalendarTool` — Agent tool for creating and modifying calendar entries. - -### Unknowns - -- Recurring RRULE event expansion calculation bugs across leap years. diff --git a/docs/discovery/domains/chat.md b/docs/discovery/domains/chat.md deleted file mode 100644 index 8ff904c6b..000000000 --- a/docs/discovery/domains/chat.md +++ /dev/null @@ -1,197 +0,0 @@ -# Chat - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## CHAT-001 — Core Chat Streaming & SSE Message Generation - -- **Domain**: `chat` -- **Status**: `verified` -- **Evidence Maturity**: `E2` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: pending — Requires access to a live LLM provider endpoint (OpenAI API key or local Ollama server). - -### Purpose - -Handles real-time Server-Sent Events (SSE) chat streaming, token rendering, and model response generation. - -### Evidence summary - -- `routes/chat_routes.py` — `chat_stream` — POST /api/chat_stream SSE endpoint; builds the shared chat context, then dispatches to the chat-mode or agent-mode streaming path. -- `routes/chat_helpers.py` — `build_chat_context` — Shared context builder invoked by chat_stream; runs message preprocessing and assembles the memory/RAG/web context preface. -- `src/chat_handler.py` — `ChatHandler.preprocess_message` — Message preprocessing (attachments, URLs, tool preprocessing) reached from build_chat_context via routes/chat_helpers.py:preprocess. -- `src/chat_processor.py` — `ChatProcessor.build_context_preface` — Builds the retrieval and web-source context preface injected into the streamed request. -- `src/llm_core.py` — `stream_llm_with_fallback` — Chat-mode streaming dispatcher called from chat_stream; wraps stream_llm with an ordered provider fallback chain. -- `src/llm_core.py` — `stream_llm` — Per-request streaming entry wrapped by stream_llm_with_fallback; acquires the local model slot and delegates to _stream_llm_inner. -- `src/agent_loop.py` — `stream_agent_loop` — Agent-mode streaming path called from chat_stream when the request selects agent mode. -- `tests/test_chat_metrics.py` — `test_stream_llm_passes_through_llamacpp_timings` — Inspected unit test asserting stream_llm forwards backend generation timings into the emitted metrics chunk. -- `tests/test_resend_message_nondestructive.py` — `test_resend_message_does_not_truncate_by_default` — Inspected unit test asserting the frontend resend path does not truncate prior conversation turns. - -### Unknowns - -- Stream interruption on connection drops requires retry logic. - -## CHAT-002 — Session Management & Conversation State - -- **Domain**: `chat` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Manages session creation, listing, switching, renaming, and persistence of conversation metadata. - -### Evidence summary - -- `routes/session_routes.py` — `@router.get('/api/sessions')` — Lists active sessions filtered by user owner scope. -- `core/session_manager.py` — `SessionManager` — Provides thread-safe session storage operations. - -### Unknowns - -- Concurrent file writes to sessions.json under high load. - -## CHAT-003 — Chat History & Message Editing/Truncation - -- **Domain**: `chat` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Provides history retrieval, message content updating, message deletion, and history branch truncation. - -### Evidence summary - -- `routes/history/history_routes.py` — `@router.get('/api/history/{session_id}')` — Fetches message history timeline for a session. -- `routes/history_routes.py` — `_sys.modules[__name__] = _canonical` — Backward-compatibility shim module. - -### Unknowns - -- Truncating messages re-indexes context window and clears cached tool calls. - -## CHAT-004 — File & Multimodal Attachment Handling - -- **Domain**: `chat` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Handles uploading, mime validation, image preview, vision encoding, and file attachments in chat messages. - -### Evidence summary - -- `routes/upload_routes.py` — `@router.post('')` — Accepts multi-part file uploads and generates vision metadata. -- `src/upload_handler.py` — `UploadHandler.save_file` — Validates upload size and atomicity on disk. - -### Unknowns - -- Large file uploads may consume server disk space if cleanup task fails. - -## CHAT-005 — Chat Message Search - -- **Domain**: `chat` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Enables full-text keyword search across stored chat messages and sessions. - -### Evidence summary - -- `routes/search_routes.py` — `setup_search_routes` — Registers chat message search endpoint. -- `src/session_search.py` — `search_sessions` — Executes query matching against session transcripts. - -### Unknowns - -- Full table scans on un-indexed text columns for very large databases. - -## CHAT-006 — System Prompts & Preset Management - -- **Domain**: `chat` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Provides creation, selection, and customization of system prompt presets for chat sessions. - -### Evidence summary - -- `routes/preset_routes.py` — `setup_preset_routes` — API routes for listing and modifying system prompt presets. -- `src/preset_manager.py` — `PresetManager` — Disk-backed manager for prompt presets. - -### Unknowns - -- Invalid JSON syntax in user presets file can corrupt preset loading. - -## CHAT-007 — Emoji Rendering & Twemoji SVG Proxy - -- **Domain**: `chat` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Proxies Twemoji SVG icons locally to render flat SVG emojis in message text without external CDN dependencies. - -### Evidence summary - -- `routes/emoji_routes.py` — `setup_emoji_routes` — Serves locally cached Twemoji SVGs. - -### Unknowns - -- First request fetches SVG from remote CDN before caching locally. - -## CHAT-008 — Input History Recall (Arrow Up) - -- **Domain**: `chat` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Allows users to cycle through previously sent prompt messages in the chat composer input using Arrow-Up/Down keys. - -### Evidence summary - -- `static/js/composerArrowUpRecall.js` — `initComposerRecall` — Listens for ArrowUp keypress on composer textarea. - -### Unknowns - -- Client-side browser storage limits. - -## CHAT-009 — Context Window Compaction & Truncation - -- **Domain**: `chat` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Compacts session transcript history when prompt size exceeds context limits using summarization. - -### Evidence summary - -- `routes/history/history_routes.py` — `@router.post('/api/session/{session_id}/compact')` — Triggers context summarization and compaction. -- `src/context_compactor.py` — `compact_context` — Executes context token pruning and summary generation. - -### Unknowns - -- Aggressive compaction may discard subtle user instructions. diff --git a/docs/discovery/domains/contact.md b/docs/discovery/domains/contact.md deleted file mode 100644 index 47122b20a..000000000 --- a/docs/discovery/domains/contact.md +++ /dev/null @@ -1,24 +0,0 @@ -# Contact - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## CONTACT-001 — CardDAV Contact Management & Address Book Integration - -- **Domain**: `contact` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Connects to CardDAV servers, imports VCard contacts, and provides contact lookup for email/calendar autocomplete. - -### Evidence summary - -- `routes/contacts/contacts_routes.py` — `@router.get('/list')` — Returns contact list filtered by search query. -- `src/tools/contacts.py` — `ContactsTool` — Agent tool for querying user address book contacts. - -### Unknowns - -- VCard 3.0 vs 4.0 property parsing mismatches. diff --git a/docs/discovery/domains/cookbook.md b/docs/discovery/domains/cookbook.md deleted file mode 100644 index 6d22bccef..000000000 --- a/docs/discovery/domains/cookbook.md +++ /dev/null @@ -1,85 +0,0 @@ -# Cookbook - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## COOKBOOK-001 — Local Model Download & Recipe Lifecycle Management - -- **Domain**: `cookbook` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Downloads HuggingFace models, configures execution parameters, and manages local GGUF/MLX model servers. - -### Evidence summary - -- `routes/cookbook_routes.py` — `setup_cookbook_routes` — Exposes model downloading and process serving endpoints. -- `static/js/cookbook.js` — `initCookbook` — UI manager for local model library. - -### Unknowns - -- Disk space exhaustion during multi-gigabyte GGUF weights downloads. - -## COOKBOOK-002 — Hardware Model Fitting ('What Fits?') Analysis Engine - -- **Domain**: `cookbook` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Calculates RAM/VRAM requirements, quantized size, and context overhead to determine model compatibility. - -### Evidence summary - -- `routes/hwfit_routes.py` — `setup_hwfit_routes` — Calculates hardware model compatibility. -- `services/hwfit/fit.py` — `calculate_fit` — Performs parameter and memory fit calculations. - -### Unknowns - -- Inaccurate VRAM estimation for non-standard KV-cache quantization. - -## COOKBOOK-003 — HuggingFace & MLX Model Discovery Services - -- **Domain**: `cookbook` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Searches HuggingFace Hub and local MLX model repositories for compatible GGUF and MLX weights. - -### Evidence summary - -- `services/hwfit/hf_discovery.py` — `search_hf_models` — Queries HuggingFace API for model tags and files. - -### Unknowns - -- HuggingFace API rate limits when searching without an API token. - -## COOKBOOK-004 — Host Docker Access for Inference Container Runtimes - -- **Domain**: `cookbook` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: pending — Requires Docker access and supported physical GPU hardware. - -### Purpose - -Detects and connects to host Docker engine to launch containerized Ollama, vLLM, or SGLang runtimes. - -### Evidence summary - -- `src/host_docker_access.py` — `HostDockerAccess` — Interacts with host docker daemon. - -### Unknowns - -- Permission denied accessing docker socket on non-root setups. diff --git a/docs/discovery/domains/document.md b/docs/discovery/domains/document.md deleted file mode 100644 index ca8ece487..000000000 --- a/docs/discovery/domains/document.md +++ /dev/null @@ -1,108 +0,0 @@ -# Document - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## DOCUMENT-001 — Document & Canvas Artifact Management - -- **Domain**: `document` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Renders dynamic canvas documents, handles live editing, markdown preview, and side-by-side artifact display. - -### Evidence summary - -- `routes/document_routes.py` — `setup_document_routes` — Registers document artifact CRUD routes. -- `static/js/document.js` — `initDocumentView` — Renders interactive canvas document panel. - -### Unknowns - -- Concurrent edits on the same document artifact. - -## DOCUMENT-002 — PDF Form Processing & High-Fidelity Rendering - -- **Domain**: `document` -- **Status**: `verified` -- **Evidence Maturity**: `E1` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: pending — Requires optional PyMuPDF (`fitz`) or pypdf runtime dependency. - -### Purpose - -Extracts form fields from PDF files, fills dynamic values, and generates PDF previews. - -### Evidence summary - -- `src/pdf_runtime.py` — `load_pymupdf_for_pdf_viewer` — Loads optional PyMuPDF runtime for PDF viewing. -- `src/pdf_forms.py` — `extract_form_fields` — Handles PDF form field extraction and filling. -- `tests/test_document_pdf_marker.py` — `test_marker_removed_without_eating_following_text` — Tests PDF text extraction wrapper stripping without content corruption. - -### Unknowns - -- Complex XFA PDF forms may not extract cleanly with standard pdf parsers. - -## DOCUMENT-003 — Personal Document Indexing & RAG Retrieval - -- **Domain**: `document` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Indexes local user documents (PDF, DOCX, TXT) into ChromaDB for semantic vector retrieval. - -### Evidence summary - -- `routes/personal_routes.py` — `setup_personal_routes` — Personal document RAG indexing and search API endpoints. -- `src/personal_docs.py` — `PersonalDocsManager` — Handles file text chunking and vector storage. - -### Unknowns - -- Slow vector embedding indexing step for massive multi-thousand page documents. - -## DOCUMENT-004 — Document Conversion & Text Extraction Engine - -- **Domain**: `document` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Converts office formats (.docx, .xlsx, .pptx) and HTML into clean Markdown text representations. - -### Evidence summary - -- `src/markitdown_runtime.py` — `convert_to_markdown` — Converts binary office documents into structured Markdown text. - -### Unknowns - -- Formatting loss when parsing legacy binary doc/xls files. - -## DOCUMENT-005 — Document Library UI Navigation - -- **Domain**: `document` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Provides dedicated UI view for browsing, filtering, and organizing saved user documents. - -### Evidence summary - -- `static/js/documentLibrary.js` — `initDocumentLibrary` — Renders document library navigation grid. -- `app.py` — `serve_library` — Serves SPA shell for /library route. - -### Unknowns - -- Large folder trees may cause initial DOM render slowdown. diff --git a/docs/discovery/domains/email.md b/docs/discovery/domains/email.md deleted file mode 100644 index 12b28ca43..000000000 --- a/docs/discovery/domains/email.md +++ /dev/null @@ -1,87 +0,0 @@ -# Email - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## EMAIL-001 — Email Account Setup, IMAP/SMTP Connection & Polling - -- **Domain**: `email` -- **Status**: `verified` -- **Evidence Maturity**: `E1` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: pending — Requires a controlled live IMAP account and network access. - -### Purpose - -Configures IMAP/SMTP email accounts, validates TLS certificates, and polls background inbox updates. - -### Evidence summary - -- `routes/email_routes.py` — `setup_email_routes` — Sets up email account management and synchronization routes. -- `routes/email_pollers.py` — `_start_poller` — Background poller for email inbox synchronization. -- `tests/test_service_health_email.py` — `test_email_ok_all_connect` — Tests IMAP connection health probing and status reporting. - -### Unknowns - -- Account lockouts if bad credentials are repeatedly polled. - -## EMAIL-002 — Email Searching, Threading & Message Operations - -- **Domain**: `email` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Parses email headers, folds signatures, groups messages into threads, and executes full-text email search. - -### Evidence summary - -- `routes/email_routes.py` — `@router.get('/search')` — Executes search across cached email headers and text. -- `src/email_thread_parser.py` — `parse_email_thread` — Builds conversation tree from Message-ID and In-Reply-To headers. - -### Unknowns - -- Malformed MIME email structures failing HTML sanitization. - -## EMAIL-003 — Email Composition, Draft Management & Sending - -- **Domain**: `email` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: pending — Requires a controlled live SMTP account and network access. - -### Purpose - -Creates, saves, and dispatches HTML/plaintext email messages via SMTP. - -### Evidence summary - -- `routes/email_routes.py` — `@router.post('/send')` — Sends email message via user SMTP credentials. - -### Unknowns - -- SMTP connection drop mid-send causing unsent mail state. - -## EMAIL-004 — Email MCP Server & Codex Integration Bridge - -- **Domain**: `email` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Exposes constrained email reading and draft capabilities to external Codex / MCP agents with scope checks. - -### Evidence summary - -- `mcp_servers/email_server.py` — `EmailMcpServer` — MCP server exposing email tools over stdio/SSE. -- `routes/codex_routes.py` — `setup_codex_routes` — Bridge endpoints for external Codex plugin integration. - -### Unknowns - -- Unauthorized mail sending if token scopes are improperly scoped. diff --git a/docs/discovery/domains/frontend.md b/docs/discovery/domains/frontend.md deleted file mode 100644 index 6f9891783..000000000 --- a/docs/discovery/domains/frontend.md +++ /dev/null @@ -1,149 +0,0 @@ -# Frontend - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## FRONTEND-001 — Single Page Application Shell & Client Router - -- **Domain**: `frontend` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Main HTML5 SPA shell, DOM lifecycle initializers, tab navigation, and deep-link route handlers. - -### Evidence summary - -- `static/index.html` — `index.html` — Main SPA entry point containing modal roots and CSS bundles. -- `app.py` — `serve_index` — Serves index.html with dynamically generated CSP nonces. - -### Unknowns - -- Stale browser static cache if asset hashing is omitted during deployment. - -## FRONTEND-002 — Dynamic Theme, Color System & Custom Fonts - -- **Domain**: `frontend` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Supports dark/light themes, custom CSS variables, color picker controls, and user font uploads. - -### Evidence summary - -- `static/js/theme.js` — `applyTheme` — Applies custom HSL theme variables to DOM document root. -- `routes/font_routes.py` — `setup_font_routes` — Allows uploading and serving custom WOFF2 font files. - -### Unknowns - -- Flash of unstyled content (FOUC) on slow connections. - -## FRONTEND-003 — Window Manager, Tile Layout & Modal Control System - -- **Domain**: `frontend` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Manages draggable tool windows, snapped multi-tile viewports, modal dialog Z-ordering, and ESC key stacks. - -### Evidence summary - -- `static/js/modalManager.js` — `ModalManager` — Controls modal open/close transitions and focus trapping. -- `static/js/tileManager.js` — `TileManager` — Handles viewport split-pane grid arrangements. - -### Unknowns - -- Overlap artifacts when opening many simultaneous tool floating windows. - -## FRONTEND-004 — Global Keyboard Shortcuts & Accessibility Controls - -- **Domain**: `frontend` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Provides configurable hotkeys (Ctrl+K search, Esc close, Alt+1-9 tabs) and high-contrast accessibility options. - -### Evidence summary - -- `static/js/keyboard-shortcuts.js` — `initShortcuts` — Binds global keydown handlers for system shortcuts. -- `static/js/a11y.js` — `initA11y` — Applies ARIA roles and dyslexic font toggles. - -### Unknowns - -- Browser keybinding collisions with browser default hotkeys. - -## FRONTEND-005 — Markdown, LaTeX & Code Block Streaming Renderer - -- **Domain**: `frontend` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Parses incoming SSE markdown streams, renders KaTeX math formulas, syntax-highlighted code, and interactive runners. - -### Evidence summary - -- `static/js/markdown.js` — `renderMarkdown` — Converts markdown prose to HTML nodes with syntax highlighting. -- `static/js/streamingSegmenter.js` — `Segmenter` — Parses un-closed markdown fences during live stream. - -### Unknowns - -- DOM thrashing if streaming segmenter updates UI too frequently. - -## FRONTEND-006 — Interactive Tour & Guided Onboarding System - -- **Domain**: `frontend` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Presents interactive step-by-step feature tours and UI tooltip hints for new users. - -### Evidence summary - -- `static/js/tourHints.js` — `startTour` — Renders guided feature tour overlays over target UI elements. - -### Unknowns - -- Tour step misalignment if window is resized mid-tour. - -## FRONTEND-007 — Background Effects Prototyping Sandbox - -- **Domain**: `frontend` -- **Status**: `dead-code-candidate` -- **Evidence Maturity**: `E1` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Standalone sandbox page for prototyping visual background animations, waves, and whirlpool effects. - -### Evidence summary - -- `app.py` — `serve_backgrounds` — Serves visual background sandbox HTML page route. -- `static/wave-variants.html` — `wave-variants.html` — Interactive background effect prototyping sandbox variant. - -### Unknowns - -- Route `/backgrounds` in app.py L918 attempts to serve `static/backgrounds.html` which is missing from disk; variant templates `wave-variants.html` and `whirlpool-variants.html` exist. diff --git a/docs/discovery/domains/media.md b/docs/discovery/domains/media.md deleted file mode 100644 index cc0622ceb..000000000 --- a/docs/discovery/domains/media.md +++ /dev/null @@ -1,170 +0,0 @@ -# Media - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## MEDIA-001 — Gallery Image Library & Album Operations - -- **Domain**: `media` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Organizes images into custom albums, provides grid browsing, tagging, and album metadata management. - -### Evidence summary - -- `routes/gallery/gallery_routes.py` — `@router.get('/api/gallery/library')` — Fetches image library list with tag filters. -- `static/js/gallery.js` — `initGallery` — Main gallery grid renderer and uploader. - -### Unknowns - -- Thumbnail generation overhead for high-resolution RAW camera images. - -## MEDIA-002 — Image Processing, AI Upscaling & Style Transfer - -- **Domain**: `media` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Executes local image enhancement, background removal, face sharpening, and AI upscaling operations. - -### Evidence summary - -- `routes/gallery/gallery_routes.py` — `@router.post('/api/gallery/ai-upscale')` — Runs RealESRGAN image upscaling. -- `routes/gallery/gallery_routes.py` — `@router.post('/api/image/remove-bg')` — Executes background removal pass. - -### Unknowns - -- High GPU memory allocation when upscaling 4K images. - -## MEDIA-003 — Interactive Image Canvas Editor & Persisted Drafts - -- **Domain**: `media` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Provides full multi-layer raster canvas editor, brush tools, transforms, masks, and draft project persistence. - -### Evidence summary - -- `routes/editor_draft_routes.py` — `setup_editor_draft_routes` — API routes for saving and loading canvas project drafts. -- `static/js/editor/history-panel.js` — `HistoryManager` — Canvas undo/redo stack manager. - -### Unknowns - -- Browser memory leak if multi-gigabyte layer undo buffers are kept indefinitely. - -## MEDIA-004 — Text-to-Speech (TTS) Synthesis Service - -- **Domain**: `media` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Synthesizes spoken audio from text using local Kokoro, EdgeTTS, or OpenAI TTS engines. - -### Evidence summary - -- `routes/tts_routes.py` — `@router.post('/synthesize')` — Synthesizes TTS audio clip. -- `services/tts/tts_service.py` — `TTSService` — Provider abstraction layer for audio speech generation. - -### Unknowns - -- Audio synthesis latency on CPU-only hardware setups. - -## MEDIA-005 — Speech-to-Text (STT) Audio Transcription Service - -- **Domain**: `media` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Transcribes user audio recordings into text using faster-whisper or local speech models. - -### Evidence summary - -- `routes/stt_routes.py` — `@router.post('/transcribe')` — Accepts multipart audio file and returns transcription text. -- `services/stt/stt_service.py` — `STTService` — Whisper audio transcription engine wrapper. - -### Unknowns - -- Missing ffmpeg system dependency prevents audio format decoding. - -## MEDIA-006 — Digital Signature Stamp Storage & Placement - -- **Domain**: `media` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Stores transparent PNG user signatures and stamps for placement onto PDF forms and documents. - -### Evidence summary - -- `routes/signature_routes.py` — `setup_signature_routes` — CRUD endpoints for managing user signature PNG stamps. - -### Unknowns - -- Cross-site scripting if signature image titles contain unescaped user input. - -## MEDIA-007 — Generated Image Artifact Route & MCP Integration - -- **Domain**: `media` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Serves generated AI artwork artifacts and integrates with image generation MCP server. - -### Evidence summary - -- `app.py` — `serve_generated_image` — Serves generated image artifacts with cache headers. -- `src/generated_images.py` — `resolve_generated_image_path` — Confines requested image path within artifacts directory. - -### Unknowns - -- Path traversal vulnerability if filename parameter is un-sanitized. - -## MEDIA-008 — Native MLX Image Bridge (macOS Apple Silicon) - -- **Domain**: `media` -- **Status**: `experimental` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: pending — Requires Apple Silicon, macOS tooling, and the compiled MLX bridge. - -### Purpose - -Native Apple Swift bridge for hardware-accelerated diffusion and MLX image colorization on macOS. - -### Evidence summary - -- `swift/odysseus-mlx-image-bridge/Package.swift` — `Package` — Swift package manifest for native MLX image bridge. -- `scripts/mlx_image_server.py` — `main` — Python daemon wrapping native Swift MLX binary. - -### Unknowns - -- Binary build requires Xcode command line tools build step (`build-macos-app.sh`). diff --git a/docs/discovery/domains/memory.md b/docs/discovery/domains/memory.md deleted file mode 100644 index 22b9f8b78..000000000 --- a/docs/discovery/domains/memory.md +++ /dev/null @@ -1,24 +0,0 @@ -# Memory - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## MEMORY-001 — Persistent Long-Term Memory & Vector Indexing - -- **Domain**: `memory` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Extracts facts, user preferences, and temporal memories from chat sessions into vector/relational storage. - -### Evidence summary - -- `routes/memory/memory_routes.py` — `@router.get('')` — Fetches long-term user memory timeline. -- `services/memory/memory_extractor.py` — `MemoryExtractor` — LLM-driven fact extraction from conversation transcripts. - -### Unknowns - -- Conflicting memory facts extracted from contradictory user prompts. diff --git a/docs/discovery/domains/model.md b/docs/discovery/domains/model.md deleted file mode 100644 index 1855c0d3f..000000000 --- a/docs/discovery/domains/model.md +++ /dev/null @@ -1,177 +0,0 @@ -# Model - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## MODEL-001 — Multi-Provider LLM Model Discovery & Metadata Management - -- **Domain**: `model` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Discovers models from OpenAI, Anthropic, Ollama, vLLM, LMStudio, OpenRouter, and Google AI Studio endpoints. - -### Evidence summary - -- `routes/model_routes.py` — `@router.get('/api/models')` — Returns unified list of available models across providers. -- `src/model_discovery.py` — `ModelDiscovery.discover_all` — Queries connected provider endpoints for available model IDs. - -### Unknowns - -- Remote endpoint timeouts may slow down full discovery refresh. - -## MODEL-002 — Model Capability & Context Limits Detection - -- **Domain**: `model` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Detects vision, tool calling, reasoning, and context window limits for connected model endpoints. - -### Evidence summary - -- `src/model_capabilities.py` — `get_model_capabilities` — Maps model names to vision and tool support flags. -- `src/endpoint_resolver.py` — `resolve_endpoint_headers` — Resolves auth headers and target URLs for model endpoints. - -### Unknowns - -- Incorrect context limit metadata for unlisted custom fine-tunes. - -## MODEL-003 — LLM Core Provider Communication & Fallback Routing - -- **Domain**: `model` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: pending — Provider dispatch, header injection and fallback advancement are only observable against a reachable LLM provider endpoint; not exercised in this documentation pass. - -### Purpose - -Manages HTTP request dispatching, authorization header injection, and fallback provider routing for LLM calls. - -### Evidence summary - -- `src/llm_core.py` — `llm_call_async` — Non-streaming provider request dispatcher: resolves the endpoint, injects authorization headers and executes the HTTP call. -- `src/llm_core.py` — `llm_call_async_with_fallback` — Ordered fallback wrapper that retries llm_call_async across the configured candidate endpoints. -- `src/llm_core.py` — `stream_llm_with_fallback` — Ordered fallback wrapper for the streaming path; advances to the next candidate when a provider yields an empty completion. - -### Unknowns - -- Unexpected API changes in upstream third-party model providers. - -## MODEL-004 — Model Selection & Display Ordering Preferences - -- **Domain**: `model` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Allows pinning, sorting, and hiding specific models in the UI selection dropdown. - -### Evidence summary - -- `routes/model_routes.py` — `@router.post('/order')` — Saves custom model display order preference. - -### Unknowns - -- Stale model IDs in custom order lists after model endpoints are removed. - -## MODEL-005 — Side-by-Side Model Comparison (A/B Testing) - -- **Domain**: `model` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Enables dual-model side-by-side response evaluation, arena scoring, and latency comparison. - -### Evidence summary - -- `routes/compare/compare_routes.py` — `@router.post('/start')` — Starts a parallel dual-model comparison stream. -- `static/js/compare/index.js` — `initCompareView` — Renders side-by-side model chat panes. - -### Unknowns - -- High memory and network usage when streaming two model responses simultaneously. - -## MODEL-006 — GitHub Copilot Device Flow Authentication - -- **Domain**: `model` -- **Status**: `verified` -- **Evidence Maturity**: `E2` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: pending — Requires an interactive GitHub Copilot OAuth device-flow account. - -### Purpose - -Authenticates with GitHub Copilot via OAuth device flow to use Copilot models directly. - -### Evidence summary - -- `routes/copilot_routes.py` — `setup_copilot_routes` — Builds the Copilot device-flow router at prefix /api/copilot, wiring _start_device_flow and _poll_device_flow. -- `routes/device_flow.py` — `create_device_flow_router` — Shared factory registering POST /device/start and POST /device/poll under the caller-supplied prefix. -- `src/copilot.py` — `request_device_code` — Issues the GitHub device-code request that begins the Copilot OAuth device flow. -- `src/copilot.py` — `poll_access_token` — Polls GitHub for the access token once the user has authorized the device code. -- `tests/test_provider_device_flow_js.py` — `test_copilot_success_uses_complete_verification_uri` — Inspected unit test asserting the Copilot device-flow runner surfaces the complete verification URI returned by the backend. - -### Unknowns - -- Token expiration requires manual device re-authentication. - -## MODEL-007 — ChatGPT Subscription Device Flow Authentication - -- **Domain**: `model` -- **Status**: `verified` -- **Evidence Maturity**: `E2` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: pending — Requires an interactive ChatGPT subscription OAuth flow. - -### Purpose - -Authenticates with ChatGPT Pro/Plus subscription tokens via device login flow. - -### Evidence summary - -- `routes/chatgpt_subscription_routes.py` — `setup_chatgpt_subscription_routes` — Builds the ChatGPT subscription device-flow router at prefix /api/chatgpt-subscription. -- `routes/device_flow.py` — `create_device_flow_router` — Shared factory registering POST /device/start and POST /device/poll under the caller-supplied prefix. -- `src/chatgpt_subscription.py` — `request_device_code` — Issues the ChatGPT device-authorization request that begins the subscription OAuth device flow. -- `src/chatgpt_subscription.py` — `poll_device_auth` — Polls the ChatGPT device-authorization endpoint for completion using the stored device_auth_id and user_code. -- `tests/test_provider_device_flow_js.py` — `test_chatgpt_success_uses_plain_verification_uri` — Inspected unit test asserting the ChatGPT device-flow runner uses the plain verification URI rather than the Copilot complete-URI form. - -### Unknowns - -- Changes in OpenAI auth endpoint security challenges. - -## MODEL-008 — Embedding Model Lane & Vector Provider Setup - -- **Domain**: `model` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Configures local sentence-transformers, FastEmbed, or remote OpenAI embedding model lanes. - -### Evidence summary - -- `routes/embedding_routes.py` — `setup_embedding_routes` — Provides embedding provider configuration endpoints. -- `src/embeddings.py` — `EmbeddingManager` — Generates dense vector embeddings for RAG and memory. - -### Unknowns - -- First-time download of heavy PyTorch model weights on CPU-only machines. diff --git a/docs/discovery/domains/note.md b/docs/discovery/domains/note.md deleted file mode 100644 index e0c197428..000000000 --- a/docs/discovery/domains/note.md +++ /dev/null @@ -1,24 +0,0 @@ -# Note - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## NOTE-001 — Interactive Notes & Checklist Management - -- **Domain**: `note` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Provides Google Keep-style notes, rich markdown text, checklist items, pinning, color tags, and reminders. - -### Evidence summary - -- `routes/note/note_routes.py` — `@router.get('')` — Lists all user notes with pin and archive states. -- `static/js/notes.js` — `initNotesView` — Main interactive notes grid and modal manager. - -### Unknowns - -- Concurrent edits on note item checkboxes. diff --git a/docs/discovery/domains/platform.md b/docs/discovery/domains/platform.md deleted file mode 100644 index 037edddde..000000000 --- a/docs/discovery/domains/platform.md +++ /dev/null @@ -1,213 +0,0 @@ -# Platform - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## PLATFORM-001 — Application Initialization & Lifespan Management - -- **Domain**: `platform` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Orchestrates server startup, database table migration, background daemon initialization, and clean shutdown. - -### Evidence summary - -- `app.py` — `_lifespan` — FastAPI lifespan context manager executing startup tasks. -- `src/app_initializer.py` — `initialize_app` — Initializes app directories, DB schemas, and logging. - -### Unknowns - -- Un-handled exceptions during startup halt application launch. - -## PLATFORM-002 — System Health, Readiness & Version Monitoring APIs - -- **Domain**: `platform` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Exposes Liveness (/api/health), Readiness (/api/ready), App Version (/api/version), and Client Perf APIs. - -### Evidence summary - -- `app.py` — `readiness_check` — Performs system component integrity check. -- `src/readiness.py` — `check_readiness` — Checks database, storage, and key paths for read/write access. - -### Unknowns - -- Readiness check delays if verifying connectivity to offline remote endpoints. - -## PLATFORM-003 — Database Schema, Migrations & SQLite Persistence - -- **Domain**: `platform` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Defines core relational tables (users, tokens, tasks, sessions) and executes automated SQLite schema upgrades. - -### Evidence summary - -- `core/database.py` — `init_db` — Creates ORM tables and establishes connection pool. -- `scripts/update_database.py` — `run_migrations` — Applies missing schema columns and indices. - -### Unknowns - -- SQLite file lock contention under high concurrent write loads. - -## PLATFORM-004 — User Data Export & Import Backup Infrastructure - -- **Domain**: `platform` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Exports complete user workspace state (sessions, memory, skills, notes, presets) into a zip archive. - -### Evidence summary - -- `routes/backup_routes.py` — `setup_backup_routes` — Handles workspace data export and import upload unpack. -- `docs/backup-restore.md` — `Documentation` — Backup and restore operational documentation. - -### Unknowns - -- Corrupt archive files causing partial data restore. - -## PLATFORM-005 — File Cleanup & Storage Maintenance Engine - -- **Domain**: `platform` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Scans data directories for orphaned files, old uploads, temporary vision images, and frees disk space. - -### Evidence summary - -- `routes/cleanup/cleanup_routes.py` — `@router.get('/preview')` — Previews reclaimable disk space across storage directories. -- `src/cleanup_service.py` — `CleanupService` — Executes filesystem purge of orphaned asset files. - -### Unknowns - -- Deletes files uploaded in active sessions if retention window is set too short. - -## PLATFORM-006 — System Health & RAG Diagnostic Suite - -- **Domain**: `platform` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Executes real-time integrity diagnostics across ChromaDB, SearXNG, local models, and network interfaces. - -### Evidence summary - -- `routes/diagnostics_routes.py` — `setup_diagnostics_routes` — Runs subsystem health check suite. -- `src/service_health.py` — `collect_health_status` — Inspects vector database, email, search, and local provider status. - -### Unknowns - -- Diagnostic timeout if external search provider is unreachable. - -## PLATFORM-007 — Desktop CLI Utilities & Shell Integration Tools - -- **Domain**: `platform` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Provides command-line interface tools (`odysseus`, `odysseus-mcp`, `odysseus-mail`) for terminal usage. - -### Evidence summary - -- `scripts/_lib/cli.py` — `main` — Shared CLI framework for terminal helper commands. -- `scripts/odysseus` — `odysseus` — Main terminal launcher script. - -### Unknowns - -- Outdated CLI scripts if backend API schemas change. - -## PLATFORM-008 — Desktop Companion App Integration - -- **Domain**: `platform` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Provides API routes and pairing mechanisms for the native macOS/desktop menu bar companion app. - -### Evidence summary - -- `companion/routes.py` — `setup_companion_routes` — Endpoints for pairing and status sync with desktop companion. -- `companion/pairing.py` — `PairingManager` — Generates and validates companion pairing codes. - -### Unknowns - -- Pairing code expiration timing window. - -## PLATFORM-009 — Docker Containerization & GPU Hardware Manifests - -- **Domain**: `platform` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: pending — Requires Docker GPU pass-through and compatible host drivers. - -### Purpose - -Provides multi-stage Dockerfile and Docker Compose manifests for CPU, NVIDIA CUDA, and AMD ROCm GPUs. - -### Evidence summary - -- `Dockerfile` — `multi-stage-build` — Multi-stage container build environment. -- `docker-compose.gpu-nvidia.yml` — `nvidia-gpu-manifest` — NVIDIA GPU pass-through container specification. -- `scripts/check-docker-gpu.sh` — `check-docker-gpu` — Automated diagnostic test script for host NVIDIA GPU passthrough. - -### Unknowns - -- Driver version incompatibility with host NVIDIA/AMD kernel drivers. - -## PLATFORM-010 — Legacy FAISS Vector Index Migration Script - -- **Domain**: `platform` -- **Status**: `legacy` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Legacy utility script to migrate older FAISS vector indices into ChromaDB. - -### Evidence summary - -- `scripts/migrate_faiss_to_chroma.py` — `migrate_faiss` — Reads FAISS vector index files and writes to ChromaDB collection. - -### Unknowns - -- Superseded by native ChromaDB vector index pipeline. diff --git a/docs/discovery/domains/research.md b/docs/discovery/domains/research.md deleted file mode 100644 index 2df5a4b75..000000000 --- a/docs/discovery/domains/research.md +++ /dev/null @@ -1,86 +0,0 @@ -# Research - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## RESEARCH-001 — Deep Research Execution Engine & SSE Progress Streaming - -- **Domain**: `research` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Executes multi-step recursive deep research tasks, web page scraping, synthesis, and streams live progress. - -### Evidence summary - -- `routes/research/research_routes.py` — `@router.post('/api/research/start')` — Initiates deep research job. -- `src/deep_research.py` — `DeepResearchEngine` — Recursive search and summary crawler. - -### Unknowns - -- High memory consumption when parsing multi-megabyte HTML target pages. - -## RESEARCH-002 — Research Library, Detail View & Image Controls - -- **Domain**: `research` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Stores completed research reports, generated diagrams, reference links, and manages image visibility. - -### Evidence summary - -- `routes/research/research_routes.py` — `@router.get('/api/research/library')` — Returns all saved research reports. - -### Unknowns - -- Orphaned report files if storage directory is modified out-of-band. - -## RESEARCH-003 — Web Search Engine Integration (SearXNG & Multi-Provider) - -- **Domain**: `research` -- **Status**: `verified` -- **Evidence Maturity**: `E1` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: pending — Requires an active SearXNG instance or external search API provider. - -### Purpose - -Queries SearXNG, DuckDuckGo, or Google Search instances to retrieve web search snippets. - -### Evidence summary - -- `routes/search_routes.py` — `setup_search_routes` — Defines /api/search, /api/search/config, and /api/search/query endpoints. -- `src/search/core.py` — `SearchEngine` — Compatibility module aliasing services.search.core. -- `tests/test_search_ranking.py` — `test_news_queries_prefer_news_sources_over_sports_and_social_results` — Tests search result domain ranking and scoring. - -### Unknowns - -- Search provider IP throttling or rate-limiting. - -## RESEARCH-004 — Research Result Peeking & Topic Spinoff Generation - -- **Domain**: `research` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Extracts preliminary research snippets and spawns child research sessions focused on specific sub-topics. - -### Evidence summary - -- `routes/research/research_routes.py` — `@router.post('/api/research/spinoff/{session_id}')` — Spawns child research session for specific query. - -### Unknowns - -- Deep recursion tree depth when spawning multiple nested spinoffs. diff --git a/docs/discovery/domains/security.md b/docs/discovery/domains/security.md deleted file mode 100644 index 2d07dae22..000000000 --- a/docs/discovery/domains/security.md +++ /dev/null @@ -1,154 +0,0 @@ -# Security - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## SECURITY-001 — Authentication, Session Cookies & User Management - -- **Domain**: `security` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Handles bcrypt password hashing, session cookie issuance, authentication enforcement, and user administration. - -### Evidence summary - -- `routes/auth_routes.py` — `@router.post('/login')` — Authenticates credentials and sets session cookie. -- `core/auth.py` — `AuthManager` — Handles user creation, password verification, and session tokens. - -### Unknowns - -- Cookie session hijack if deployed over unencrypted HTTP without HTTPS cookie flags. - -## SECURITY-002 — System Vault Encrypted Secret Storage - -- **Domain**: `security` -- **Status**: `verified` -- **Evidence Maturity**: `E1` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: pending — Requires installed Bitwarden CLI (`bw`) executable. - -### Purpose - -Encrypts API keys, passphrases, and third-party secrets on disk using AES-GCM / PBKDF2 key derivation. - -### Evidence summary - -- `routes/vault_routes.py` — `setup_vault_routes` — Admin routes for vault configuration, login, unlock, lock, and logout. -- `src/secret_storage.py` — `SecretStorage` — Fernet symmetric key DB secret encryption. -- `tests/test_vault_password_not_in_argv.py` — `test_bw_password_not_in_argv` — Verifies master password is fed via stdin and never appears in process argv. - -### Unknowns - -- Loss of vault master passphrase renders all encrypted secrets permanently unrecoverable. - -## SECURITY-003 — API Token Management & Scope Access Control - -- **Domain**: `security` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Generates scoped API bearer tokens (read/write/admin) for external tool and script authentication. - -### Evidence summary - -- `routes/api_token_routes.py` — `setup_api_token_routes` — Exposes API token creation, scope assignment, and revocation. -- `core/database.py` — `ApiToken` — SQLAlchemy ORM schema for API tokens and permissions. - -### Unknowns - -- Leaked API bearer tokens with excessive permission scopes. - -## SECURITY-004 — Prompt Security & Injection Defense Engine - -- **Domain**: `security` -- **Status**: `verified` -- **Evidence Maturity**: `E1` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Scans system prompts and external inputs for prompt injection attempts, jailbreaks, and sensitive data leaks. - -### Evidence summary - -- `src/prompt_security.py` — `untrusted_context_message` — Wraps untrusted context with guard delimiters and sets metadata.trusted = False. -- `src/tool_security.py` — `NON_ADMIN_BLOCKED_TOOLS` — Enforces tool execution safety for non-admin user roles. -- `tests/test_skill_index_prompt_injection.py` — `test_skill_index` — Verifies skill index descriptions cannot leak into trusted system prompts. -- `tests/test_tool_output_prompt_injection.py` — `test_tool_output` — Tool output injection guards. - -### Unknowns - -- False positives blocking legitimate complex coding or security prompts. - -## SECURITY-005 — URL & Path Confinement Security Guards - -- **Domain**: `security` -- **Status**: `verified` -- **Evidence Maturity**: `E1` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Prevents SSRF attacks and path traversal by validating target IP addresses and resolving symlinks. - -### Evidence summary - -- `src/url_safety.py` — `check_outbound_url` — Rejects non-HTTP(S) schemes, link-local, cloud metadata SSRF addresses. -- `src/url_security.py` — `validate_public_http_url` — Validates public-facing endpoints. -- `tests/test_url_safety.py` — `test_url_safety` — Scheme validation, cloud metadata SSRF rejection, IP classification. -- `tests/test_tool_path_confinement.py` — `test_path_confinement` — Path traversal checks. -- `tests/test_workspace_confine.py` — `test_workspace_confine` — Workspace confinement checks. - -### Unknowns - -- DNS rebinding attacks if IP address is re-resolved post-validation. - -## SECURITY-006 — HTTP Security Headers Middleware - -- **Domain**: `security` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Injects standard OWASP HTTP security headers (CSP, HSTS, X-Content-Type-Options, X-Frame-Options). - -### Evidence summary - -- `core/middleware.py` — `SecurityHeadersMiddleware` — Sets strict security headers and CSP nonces on HTTP responses. - -### Unknowns - -- Strict Content Security Policy (CSP) blocking third-party embedded web resources. - -## SECURITY-007 — Admin System Data Wipe ('Danger Zone') - -- **Domain**: `security` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Provides administrative reset operations to wipe sessions, cache, uploaded files, or factory reset state. - -### Evidence summary - -- `routes/admin_wipe/admin_wipe_routes.py` — `@router.delete('/wipe/{kind}')` — Executes systemic data wipe based on requested scope. - -### Unknowns - -- Accidental catastrophic data loss if triggered without user confirmation. diff --git a/docs/discovery/domains/skill.md b/docs/discovery/domains/skill.md deleted file mode 100644 index 43fb72180..000000000 --- a/docs/discovery/domains/skill.md +++ /dev/null @@ -1,24 +0,0 @@ -# Skill - -Features in this document are generated from [`../feature-catalog.json`](../feature-catalog.json), the canonical inventory. - -## SKILL-001 — Dynamic Skill Management & Code Execution Engine - -- **Domain**: `skill` -- **Status**: `verified` -- **Evidence Maturity**: `E0` -- **Commit Verified**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Runtime Validation**: not-required — No separate environment-dependent runtime validation was identified during this documentation pass. - -### Purpose - -Allows users to create, import, edit, test, and execute custom Python/Markdown skills dynamically. - -### Evidence summary - -- `routes/skills_routes.py` — `setup_skills_routes` — Exposes CRUD and remote import routes for user skills. -- `services/memory/skills.py` — `SkillsManager` — Handles skill storage, parsing, and execution. - -### Unknowns - -- Arbitrary code execution risks if skill import URL is untrusted. diff --git a/docs/discovery/feature-catalog.json b/docs/discovery/feature-catalog.json deleted file mode 100644 index 93cdc6fd0..000000000 --- a/docs/discovery/feature-catalog.json +++ /dev/null @@ -1,4797 +0,0 @@ -[ - { - "id": "CHAT-001", - "domain": "chat", - "name": "Core Chat Streaming & SSE Message Generation", - "purpose": "Handles real-time Server-Sent Events (SSE) chat streaming, token rendering, and model response generation.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/chatStream.js", - "static/js/chat.js", - "static/js/streamingRenderer.js" - ], - "backend_entrypoints": [ - "routes/chat_routes.py:chat_stream", - "routes/chat_helpers.py:build_chat_context", - "src/chat_handler.py:ChatHandler.preprocess_message", - "src/chat_processor.py:ChatProcessor.build_context_preface", - "src/llm_core.py:stream_llm_with_fallback", - "src/agent_loop.py:stream_agent_loop" - ], - "routes": [ - "/api/chat_stream", - "/api/chat" - ], - "configuration": [ - "OPENAI_API_KEY", - "OLLAMA_BASE_URL", - "REQUEST_TIMEOUT" - ], - "persistence": [ - "SESSIONS_FILE", - "DATA_DIR/sessions/" - ], - "dependencies": [ - "fastapi", - "starlette.responses.StreamingResponse", - "httpx" - ], - "tests": [ - "tests/test_chat_metrics.py", - "tests/test_resend_message_nondestructive.py" - ], - "documentation": [ - "docs/chat.webm", - "README.md" - ], - "risks": [ - "Stream interruption on connection drops requires retry logic." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/chat_routes.py", - "symbol": "chat_stream", - "line_range": "L702-L1870", - "explanation": "POST /api/chat_stream SSE endpoint; builds the shared chat context, then dispatches to the chat-mode or agent-mode streaming path." - }, - { - "path": "routes/chat_helpers.py", - "symbol": "build_chat_context", - "line_range": "L669-L863", - "explanation": "Shared context builder invoked by chat_stream; runs message preprocessing and assembles the memory/RAG/web context preface." - }, - { - "path": "src/chat_handler.py", - "symbol": "ChatHandler.preprocess_message", - "line_range": "L123-L315", - "explanation": "Message preprocessing (attachments, URLs, tool preprocessing) reached from build_chat_context via routes/chat_helpers.py:preprocess." - }, - { - "path": "src/chat_processor.py", - "symbol": "ChatProcessor.build_context_preface", - "line_range": "L263-L493", - "explanation": "Builds the retrieval and web-source context preface injected into the streamed request." - }, - { - "path": "src/llm_core.py", - "symbol": "stream_llm_with_fallback", - "line_range": "L2794-L2895", - "explanation": "Chat-mode streaming dispatcher called from chat_stream; wraps stream_llm with an ordered provider fallback chain." - }, - { - "path": "src/llm_core.py", - "symbol": "stream_llm", - "line_range": "L2131-L2151", - "explanation": "Per-request streaming entry wrapped by stream_llm_with_fallback; acquires the local model slot and delegates to _stream_llm_inner." - }, - { - "path": "src/agent_loop.py", - "symbol": "stream_agent_loop", - "line_range": "L3079-L5248", - "explanation": "Agent-mode streaming path called from chat_stream when the request selects agent mode." - }, - { - "path": "tests/test_chat_metrics.py", - "symbol": "test_stream_llm_passes_through_llamacpp_timings", - "line_range": "L129-L136", - "explanation": "Inspected unit test asserting stream_llm forwards backend generation timings into the emitted metrics chunk." - }, - { - "path": "tests/test_resend_message_nondestructive.py", - "symbol": "test_resend_message_does_not_truncate_by_default", - "line_range": "L23-L36", - "explanation": "Inspected unit test asserting the frontend resend path does not truncate prior conversation turns." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E2", - "runtime_validation": { - "required": true, - "status": "pending", - "reason": "Requires access to a live LLM provider endpoint (OpenAI API key or local Ollama server)." - } - }, - { - "id": "CHAT-002", - "domain": "chat", - "name": "Session Management & Conversation State", - "purpose": "Manages session creation, listing, switching, renaming, and persistence of conversation metadata.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/sessions.js", - "static/js/sidebar-layout.js" - ], - "backend_entrypoints": [ - "routes/session_routes.py:setup_session_routes", - "core/session_manager.py:SessionManager" - ], - "routes": [ - "/api/sessions", - "/api/sessions/{session_id}" - ], - "configuration": [ - "SESSIONS_FILE", - "DATA_DIR/odysseus.db" - ], - "persistence": [ - "DATA_DIR/sessions.json", - "SQLite session table" - ], - "dependencies": [ - "sqlite3", - "pydantic" - ], - "tests": [ - "tests/test_session_manager.py", - "tests/test_session_routes_utcnow.py" - ], - "documentation": [ - "docs/setup.md" - ], - "risks": [ - "Concurrent file writes to sessions.json under high load." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/session_routes.py", - "symbol": "@router.get('/api/sessions')", - "line_range": "L150-L210", - "explanation": "Lists active sessions filtered by user owner scope." - }, - { - "path": "core/session_manager.py", - "symbol": "SessionManager", - "line_range": "L30-L150", - "explanation": "Provides thread-safe session storage operations." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "CHAT-003", - "domain": "chat", - "name": "Chat History & Message Editing/Truncation", - "purpose": "Provides history retrieval, message content updating, message deletion, and history branch truncation.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/chat.js", - "static/js/sessions.js" - ], - "backend_entrypoints": [ - "routes/history/history_routes.py:setup_history_routes" - ], - "routes": [ - "/api/history/{session_id}", - "/api/session/{session_id}/truncate", - "/api/session/{session_id}/edit-message" - ], - "configuration": [ - "MAX_HISTORY_MESSAGES" - ], - "persistence": [ - "DATA_DIR/sessions/" - ], - "dependencies": [ - "sqlite3", - "fastapi" - ], - "tests": [ - "tests/test_history_db_fallback_hidden.py", - "tests/test_truncate_message_count_regression.py" - ], - "documentation": [ - "docs/attachments.md" - ], - "risks": [ - "Truncating messages re-indexes context window and clears cached tool calls." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/history/history_routes.py", - "symbol": "@router.get('/api/history/{session_id}')", - "line_range": "L178-L230", - "explanation": "Fetches message history timeline for a session." - }, - { - "path": "routes/history_routes.py", - "symbol": "_sys.modules[__name__] = _canonical", - "line_range": "L1-L17", - "explanation": "Backward-compatibility shim module." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "CHAT-004", - "domain": "chat", - "name": "File & Multimodal Attachment Handling", - "purpose": "Handles uploading, mime validation, image preview, vision encoding, and file attachments in chat messages.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/fileHandler.js", - "static/js/chat.js" - ], - "backend_entrypoints": [ - "routes/upload_routes.py:setup_upload_routes", - "src/upload_handler.py:UploadHandler" - ], - "routes": [ - "/api/upload", - "/api/upload/{file_id}/vision", - "/api/upload/cleanup" - ], - "configuration": [ - "MAX_UPLOAD_SIZE_MB", - "ALLOWED_UPLOAD_EXTENSIONS" - ], - "persistence": [ - "DATA_DIR/uploads/" - ], - "dependencies": [ - "pillow", - "python-magic" - ], - "tests": [ - "tests/test_upload_handler_atomicity.py", - "tests/test_upload_routes_owner_scope.py" - ], - "documentation": [ - "docs/attachments.md" - ], - "risks": [ - "Large file uploads may consume server disk space if cleanup task fails." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/upload_routes.py", - "symbol": "@router.post('')", - "line_range": "L257-L310", - "explanation": "Accepts multi-part file uploads and generates vision metadata." - }, - { - "path": "src/upload_handler.py", - "symbol": "UploadHandler.save_file", - "line_range": "L50-L140", - "explanation": "Validates upload size and atomicity on disk." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "CHAT-005", - "domain": "chat", - "name": "Chat Message Search", - "purpose": "Enables full-text keyword search across stored chat messages and sessions.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/search-chat.js", - "static/js/search.js" - ], - "backend_entrypoints": [ - "routes/search_routes.py:setup_search_routes", - "src/session_search.py:search_sessions" - ], - "routes": [ - "/api/search/chat" - ], - "configuration": [ - "SEARCH_INDEX_CACHE_TTL" - ], - "persistence": [ - "SQLite FTS / session indices" - ], - "dependencies": [ - "sqlite3" - ], - "tests": [ - "tests/test_session_search.py", - "tests/test_session_search_batch_fetch.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Full table scans on un-indexed text columns for very large databases." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/search_routes.py", - "symbol": "setup_search_routes", - "line_range": "L30-L80", - "explanation": "Registers chat message search endpoint." - }, - { - "path": "src/session_search.py", - "symbol": "search_sessions", - "line_range": "L20-L90", - "explanation": "Executes query matching against session transcripts." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "CHAT-006", - "domain": "chat", - "name": "System Prompts & Preset Management", - "purpose": "Provides creation, selection, and customization of system prompt presets for chat sessions.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/presets.js", - "static/js/settings.js" - ], - "backend_entrypoints": [ - "routes/preset_routes.py:setup_preset_routes", - "src/preset_manager.py:PresetManager" - ], - "routes": [ - "/api/presets", - "/api/presets/{preset_id}" - ], - "configuration": [ - "PRESETS_FILE" - ], - "persistence": [ - "DATA_DIR/presets.json" - ], - "dependencies": [ - "pydantic", - "json" - ], - "tests": [ - "tests/cli/test_preset_cli_store.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Invalid JSON syntax in user presets file can corrupt preset loading." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/preset_routes.py", - "symbol": "setup_preset_routes", - "line_range": "L30-L110", - "explanation": "API routes for listing and modifying system prompt presets." - }, - { - "path": "src/preset_manager.py", - "symbol": "PresetManager", - "line_range": "L15-L100", - "explanation": "Disk-backed manager for prompt presets." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "CHAT-007", - "domain": "chat", - "name": "Emoji Rendering & Twemoji SVG Proxy", - "purpose": "Proxies Twemoji SVG icons locally to render flat SVG emojis in message text without external CDN dependencies.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/emojiPicker.js", - "static/js/emojiShortcodes.js" - ], - "backend_entrypoints": [ - "routes/emoji_routes.py:setup_emoji_routes" - ], - "routes": [ - "/api/emoji/{code}.svg" - ], - "configuration": [ - "EMOJI_CACHE_DIR" - ], - "persistence": [ - "DATA_DIR/emoji_cache/" - ], - "dependencies": [ - "httpx", - "fastapi.responses.Response" - ], - "tests": [ - "tests/test_censor_pref_js.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "First request fetches SVG from remote CDN before caching locally." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/emoji_routes.py", - "symbol": "setup_emoji_routes", - "line_range": "L20-L100", - "explanation": "Serves locally cached Twemoji SVGs." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "CHAT-008", - "domain": "chat", - "name": "Input History Recall (Arrow Up)", - "purpose": "Allows users to cycle through previously sent prompt messages in the chat composer input using Arrow-Up/Down keys.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/composerArrowUpRecall.js", - "static/js/chat.js" - ], - "backend_entrypoints": [ - "None (Pure client-side state feature)" - ], - "routes": [], - "configuration": [ - "localStorage key: chat_composer_history" - ], - "persistence": [ - "Browser localStorage" - ], - "dependencies": [ - "Vanilla JavaScript DOM keyboard listeners" - ], - "tests": [], - "documentation": [ - "README.md" - ], - "risks": [ - "Client-side browser storage limits." - ], - "unknowns": [], - "evidence": [ - { - "path": "static/js/composerArrowUpRecall.js", - "symbol": "initComposerRecall", - "line_range": "L1-L80", - "explanation": "Listens for ArrowUp keypress on composer textarea." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "CHAT-009", - "domain": "chat", - "name": "Context Window Compaction & Truncation", - "purpose": "Compacts session transcript history when prompt size exceeds context limits using summarization.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/chat.js" - ], - "backend_entrypoints": [ - "routes/history/history_routes.py:compact_session", - "src/context_compactor.py:compact_context" - ], - "routes": [ - "/api/session/{session_id}/compact", - "/api/session/{session_id}/context" - ], - "configuration": [ - "MAX_CONTEXT_TOKENS", - "COMPACTION_THRESHOLD" - ], - "persistence": [ - "DATA_DIR/sessions/" - ], - "dependencies": [ - "tiktoken", - "fastapi" - ], - "tests": [ - "tests/test_context_compactor.py" - ], - "documentation": [ - "specs/architecture-runtime-inventory.md" - ], - "risks": [ - "Aggressive compaction may discard subtle user instructions." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/history/history_routes.py", - "symbol": "@router.post('/api/session/{session_id}/compact')", - "line_range": "L751-L790", - "explanation": "Triggers context summarization and compaction." - }, - { - "path": "src/context_compactor.py", - "symbol": "compact_context", - "line_range": "L25-L110", - "explanation": "Executes context token pruning and summary generation." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "AGENT-001", - "domain": "agent", - "name": "Autonomous Agent Loop & Tool Execution Engine", - "purpose": "Executes multi-step agent reasoning loops, tool invocation parsing, and automated response generation.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/chatStream.js" - ], - "backend_entrypoints": [ - "src/agent_loop.py:run_agent_loop", - "src/tool_execution.py:execute_tool_call" - ], - "routes": [ - "/api/chat/stream" - ], - "configuration": [ - "MAX_AGENT_STEPS", - "ENABLE_TOOL_EXECUTION" - ], - "persistence": [ - "Session transcript tool calls" - ], - "dependencies": [ - "asyncio", - "pydantic" - ], - "tests": [ - "tests/test_tool_policy.py", - "tests/test_unknown_tool_calls.py" - ], - "documentation": [ - "specs/architecture-runtime-inventory.md" - ], - "risks": [ - "Infinite tool loop if termination condition fails." - ], - "unknowns": [], - "evidence": [ - { - "path": "src/agent_loop.py", - "symbol": "run_agent_loop", - "line_range": "L40-L210", - "explanation": "Core loop evaluating model tool requests and executing handlers." - }, - { - "path": "src/tool_execution.py", - "symbol": "execute_tool_call", - "line_range": "L30-L150", - "explanation": "Dispatches tool invocation requests to underlying tool handlers." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "AGENT-002", - "domain": "agent", - "name": "Scheduled Tasks & Event Bus Dispatcher", - "purpose": "Schedules background recurring or delayed tasks, emits event bus triggers, and executes automated flows.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/tasks.js" - ], - "backend_entrypoints": [ - "routes/task_routes.py:setup_task_routes", - "src/task_scheduler.py:TaskScheduler", - "src/event_bus.py" - ], - "routes": [ - "/api/tasks", - "/api/tasks/{task_id}/run", - "/api/tasks/{task_id}/pause", - "/api/tasks/{task_id}/resume" - ], - "configuration": [ - "TASK_SCHEDULER_INTERVAL" - ], - "persistence": [ - "DATA_DIR/tasks.db" - ], - "dependencies": [ - "apscheduler", - "sqlite3" - ], - "tests": [ - "tests/test_task_scheduler_cancel.py", - "tests/test_task_chain_owner_scope.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Task execution failure handling on system restart." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/task_routes.py", - "symbol": "@router.get('')", - "line_range": "L341-L370", - "explanation": "Fetches active scheduled tasks." - }, - { - "path": "src/task_scheduler.py", - "symbol": "TaskScheduler", - "line_range": "L40-L280", - "explanation": "Async task scheduler dispatching cron and delay triggers." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "AGENT-003", - "domain": "agent", - "name": "Webhook Event Subscriptions & Trigger Processing", - "purpose": "Manages incoming/outgoing webhook subscriptions, endpoint authentication tokens, and event triggers.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/admin.js" - ], - "backend_entrypoints": [ - "routes/webhook_routes.py:setup_webhook_routes", - "src/webhook_manager.py:WebhookManager" - ], - "routes": [ - "/api/webhooks", - "/v1/chat", - "/api/webhooks/{webhook_id}/test" - ], - "configuration": [ - "WEBHOOK_SECRET_KEY" - ], - "persistence": [ - "DATA_DIR/webhooks.db" - ], - "dependencies": [ - "httpx", - "sqlite3" - ], - "tests": [ - "tests/test_webhook_ssrf_resilience.py", - "tests/test_webhook_trigger_auth_exempt.py" - ], - "documentation": [ - "docs/pr-blocker-audit.md" - ], - "risks": [ - "SSRF risks when contacting external webhook URLs if unvalidated." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/webhook_routes.py", - "symbol": "@router.get('/webhooks')", - "line_range": "L71-L94", - "explanation": "Returns list of registered webhooks." - }, - { - "path": "src/webhook_manager.py", - "symbol": "WebhookManager", - "line_range": "L30-L160", - "explanation": "Handles payload delivery and signature verification." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "AGENT-004", - "domain": "agent", - "name": "Assistant Settings, Task Check-Ins & Background Job Monitor", - "purpose": "Manages per-user assistant sessions and scheduled check-in settings, drains background job completions, and retains a legacy no-op activity logging shim.", - "status": "partial", - "frontend_entrypoints": [ - "static/js/assistant.js" - ], - "backend_entrypoints": [ - "routes/assistant_routes.py:setup_assistant_routes", - "src/assistant_log.py", - "src/bg_monitor.py" - ], - "routes": [ - "/api/assistant/session", - "/api/assistant/settings", - "/api/assistant/run/{task_id}", - "/api/assistant/run-status/{task_id}", - "/api/assistant/available-timezones" - ], - "configuration": [ - "CrewMember.timezone (IANA tz name driving check-in scheduling)", - "CrewMember.model", - "CrewMember.enabled_tools", - "ScheduledTask.scheduled_time / ScheduledTask.status", - "src/bg_monitor.py:POLL_INTERVAL_S", - "src/bg_monitor.py:_FOLLOWUP_MAX_ROUNDS" - ], - "persistence": [ - "CrewMember (core/database.py, is_default_assistant singleton per owner)", - "Session pinned via CrewMember.session_id", - "ScheduledTask (per-owner check-in rows)", - "TaskRun (most recent run status for run-status polling)" - ], - "dependencies": [ - "logging", - "asyncio" - ], - "tests": [ - "tests/cli/test_logs_cli_resolve_nonstring.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Assistant seeding is owner-scoped: routes/assistant_routes.py documents that reaching an /api/assistant route under a synthetic owner previously seeded a duplicate CrewMember plus Morning/Midday/Evening tasks that double-fired; correctness now depends on RESERVED_USERNAMES staying complete.", - "Check-in next_run values are recomputed from CrewMember.timezone through compute_next_run, so a missing or stale IANA timezone shifts when check-ins fire.", - "Background follow-up continuation is retry-until-success: _run_followup returns False to defer and mark_followed_up() runs only after a successful agent run, so a persistently failing continuation is retried every POLL_INTERVAL_S tick without a bounded attempt count." - ], - "unknowns": [ - "Route `/api/assistant/logs` cited in legacy docs is absent from assistant router.", - "Existing unit test `tests/cli/test_logs_cli_resolve_nonstring.py` tests CLI target-name resolution logic, not active assistant routes or bg_monitor execution loop." - ], - "evidence": [ - { - "path": "routes/assistant_routes.py", - "symbol": "setup_assistant_routes", - "line_range": "L80-L326", - "explanation": "Active assistant session, settings, manual check-in, run-status and timezone-list endpoints, including the owner-scoping guards." - }, - { - "path": "src/bg_monitor.py", - "symbol": "_drain_agent", - "line_range": "L28-L72", - "explanation": "Runs the agent loop headless against a session to produce the background-job follow-up turn." - }, - { - "path": "src/bg_monitor.py", - "symbol": "_run_followup", - "line_range": "L75-L132", - "explanation": "Drains completed background jobs and auto-continues the owning session, deferring while a live turn is in progress." - }, - { - "path": "src/assistant_log.py", - "symbol": "log_to_assistant", - "line_range": "L34-L48", - "explanation": "Legacy no-op activity logging shim retained for existing callers; documented as inactive rather than as current behaviour." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E1", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "AGENT-005", - "domain": "agent", - "name": "Model Context Protocol (MCP) Server Integration", - "purpose": "Integrates external MCP servers over stdio/SSE to expand agent capabilities dynamically.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/settings.js", - "static/js/admin.js" - ], - "backend_entrypoints": [ - "routes/mcp_routes.py:setup_mcp_routes", - "src/mcp_manager.py:McpManager", - "mcp_servers/" - ], - "routes": [ - "/api/mcp/servers", - "/api/mcp/tools", - "/api/mcp/connect" - ], - "configuration": [ - "MCP_CONFIG_PATH" - ], - "persistence": [ - "DATA_DIR/mcp_config.json" - ], - "dependencies": [ - "mcp", - "asyncio" - ], - "tests": [ - "tests/test_mcp_param_hint_hardening.py", - "tests/cli/test_mcp_cli_json.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Subprocess leaks if external MCP server process fails to terminate clean." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/mcp_routes.py", - "symbol": "setup_mcp_routes", - "line_range": "L60-L240", - "explanation": "Exposes management endpoints for external MCP servers." - }, - { - "path": "src/mcp_manager.py", - "symbol": "McpManager", - "line_range": "L40-L300", - "explanation": "Manages MCP server subprocess lifecycles." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "AGENT-006", - "domain": "agent", - "name": "AI Interaction Tools & Pipeline Orchestration", - "purpose": "Provides specialized AI interaction tools for agent self-debugging, debate, and multi-model collaboration.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/chatStream.js" - ], - "backend_entrypoints": [ - "src/ai_interaction.py", - "src/action_intents.py", - "src/builtin_actions.py" - ], - "routes": [ - "Implicit agent tool calls" - ], - "configuration": [ - "MAX_PIPELINE_TURNS" - ], - "persistence": [ - "Session transcript state" - ], - "dependencies": [ - "asyncio" - ], - "tests": [ - "tests/test_builtin_actions_nonstring.py", - "tests/test_model_interaction_registry.py" - ], - "documentation": [ - "specs/architecture-runtime-inventory.md" - ], - "risks": [ - "High API token consumption during extended agent debates." - ], - "unknowns": [], - "evidence": [ - { - "path": "src/ai_interaction.py", - "symbol": "init_ai_interaction_tools", - "line_range": "L30-L140", - "explanation": "Registers specialized multi-agent interaction primitives." - }, - { - "path": "src/builtin_actions.py", - "symbol": "execute_builtin_action", - "line_range": "L20-L90", - "explanation": "Executes pre-built action intent sequences." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "AGENT-007", - "domain": "agent", - "name": "Subprocess & Background Job Execution Tools", - "purpose": "Provides sandboxed bash/shell tool execution capabilities with output streaming and background tracking.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/chatStream.js", - "static/js/tasks.js" - ], - "backend_entrypoints": [ - "src/agent_tools/subprocess_tools.py", - "src/agent_tools/bg_job_tools.py", - "src/bg_jobs.py" - ], - "routes": [ - "Implicit agent tool calls" - ], - "configuration": [ - "ALLOW_SHELL_EXECUTION", - "SANDBOX_DIR" - ], - "persistence": [ - "DATA_DIR/bg_jobs/" - ], - "dependencies": [ - "subprocess", - "asyncio" - ], - "tests": [ - "tests/test_bg_job_tools.py", - "tests/test_task_shell_tools.py" - ], - "documentation": [ - "THREAT_MODEL.md" - ], - "risks": [ - "Arbitrary shell command execution permissions if sandbox confinement fails." - ], - "unknowns": [], - "evidence": [ - { - "path": "src/agent_tools/subprocess_tools.py", - "symbol": "run_command", - "line_range": "L25-L120", - "explanation": "Executes shell commands in background/foreground." - }, - { - "path": "src/bg_jobs.py", - "symbol": "JobManager", - "line_range": "L30-L150", - "explanation": "Tracks async background subprocess tasks." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "MODEL-001", - "domain": "model", - "name": "Multi-Provider LLM Model Discovery & Metadata Management", - "purpose": "Discovers models from OpenAI, Anthropic, Ollama, vLLM, LMStudio, OpenRouter, and Google AI Studio endpoints.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/models.js", - "static/js/modelPicker.js" - ], - "backend_entrypoints": [ - "routes/model_routes.py:setup_model_routes", - "src/model_discovery.py:ModelDiscovery" - ], - "routes": [ - "/api/models", - "/api/models/active", - "/api/model-endpoints" - ], - "configuration": [ - "OPENAI_API_KEY", - "OLLAMA_BASE_URL", - "ANTHROPIC_API_KEY" - ], - "persistence": [ - "DATA_DIR/model_endpoints.json" - ], - "dependencies": [ - "httpx", - "pydantic" - ], - "tests": [ - "tests/test_model_routes.py", - "tests/test_provider_classification.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Remote endpoint timeouts may slow down full discovery refresh." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/model_routes.py", - "symbol": "@router.get('/api/models')", - "line_range": "L100-L180", - "explanation": "Returns unified list of available models across providers." - }, - { - "path": "src/model_discovery.py", - "symbol": "ModelDiscovery.discover_all", - "line_range": "L45-L210", - "explanation": "Queries connected provider endpoints for available model IDs." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "MODEL-002", - "domain": "model", - "name": "Model Capability & Context Limits Detection", - "purpose": "Detects vision, tool calling, reasoning, and context window limits for connected model endpoints.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/models.js" - ], - "backend_entrypoints": [ - "src/model_capabilities.py", - "src/model_context.py", - "src/endpoint_resolver.py" - ], - "routes": [ - "/api/model-endpoints/{ep_id}/probe" - ], - "configuration": [ - "MODEL_CAPABILITY_OVERRODES" - ], - "persistence": [ - "In-memory capabilities cache" - ], - "dependencies": [ - "pydantic" - ], - "tests": [ - "tests/test_endpoint_resolver_headers.py", - "tests/test_vision_model_detection.py" - ], - "documentation": [ - "specs/architecture-runtime-inventory.md" - ], - "risks": [ - "Incorrect context limit metadata for unlisted custom fine-tunes." - ], - "unknowns": [], - "evidence": [ - { - "path": "src/model_capabilities.py", - "symbol": "get_model_capabilities", - "line_range": "L30-L120", - "explanation": "Maps model names to vision and tool support flags." - }, - { - "path": "src/endpoint_resolver.py", - "symbol": "resolve_endpoint_headers", - "line_range": "L20-L80", - "explanation": "Resolves auth headers and target URLs for model endpoints." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "MODEL-003", - "domain": "model", - "name": "LLM Core Provider Communication & Fallback Routing", - "purpose": "Manages HTTP request dispatching, authorization header injection, and fallback provider routing for LLM calls.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/chatStream.js" - ], - "backend_entrypoints": [ - "src/llm_core.py:llm_call_async", - "src/llm_core.py:llm_call_async_with_fallback", - "src/llm_core.py:stream_llm_with_fallback" - ], - "routes": [ - "Implicit backend calls" - ], - "configuration": [ - "LLM_RETRY_ATTEMPTS", - "LLM_TIMEOUT" - ], - "persistence": [ - "Session state" - ], - "dependencies": [ - "httpx", - "asyncio" - ], - "tests": [ - "tests/test_llm_core_concurrency.py", - "tests/test_llm_core_fallback.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Unexpected API changes in upstream third-party model providers." - ], - "unknowns": [], - "evidence": [ - { - "path": "src/llm_core.py", - "symbol": "llm_call_async", - "line_range": "L1949-L2118", - "explanation": "Non-streaming provider request dispatcher: resolves the endpoint, injects authorization headers and executes the HTTP call." - }, - { - "path": "src/llm_core.py", - "symbol": "llm_call_async_with_fallback", - "line_range": "L1932-L1946", - "explanation": "Ordered fallback wrapper that retries llm_call_async across the configured candidate endpoints." - }, - { - "path": "src/llm_core.py", - "symbol": "stream_llm_with_fallback", - "line_range": "L2794-L2895", - "explanation": "Ordered fallback wrapper for the streaming path; advances to the next candidate when a provider yields an empty completion." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": true, - "status": "pending", - "reason": "Provider dispatch, header injection and fallback advancement are only observable against a reachable LLM provider endpoint; not exercised in this documentation pass." - } - }, - { - "id": "MODEL-004", - "domain": "model", - "name": "Model Selection & Display Ordering Preferences", - "purpose": "Allows pinning, sorting, and hiding specific models in the UI selection dropdown.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/modelSort.js", - "static/js/modelPicker.js" - ], - "backend_entrypoints": [ - "routes/model_routes.py:save_model_order" - ], - "routes": [ - "/api/models/order", - "/api/models/order/reset" - ], - "configuration": [], - "persistence": [ - "DATA_DIR/model_order.json" - ], - "dependencies": [ - "json" - ], - "tests": [ - "tests/test_model_defaults.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Stale model IDs in custom order lists after model endpoints are removed." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/model_routes.py", - "symbol": "@router.post('/order')", - "line_range": "L500-L550", - "explanation": "Saves custom model display order preference." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "MODEL-005", - "domain": "model", - "name": "Side-by-Side Model Comparison (A/B Testing)", - "purpose": "Enables dual-model side-by-side response evaluation, arena scoring, and latency comparison.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/compare/index.js", - "static/js/compare/panes.js" - ], - "backend_entrypoints": [ - "routes/compare/compare_routes.py:setup_compare_routes" - ], - "routes": [ - "/api/compare/start", - "/api/compare/{comp_id}/vote", - "/api/compare/history" - ], - "configuration": [ - "COMPARE_ENABLED" - ], - "persistence": [ - "DATA_DIR/compare_history.db" - ], - "dependencies": [ - "sqlite3" - ], - "tests": [ - "tests/test_endpoint_owner_scope_followup.py" - ], - "documentation": [ - "docs/compare.webm" - ], - "risks": [ - "High memory and network usage when streaming two model responses simultaneously." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/compare/compare_routes.py", - "symbol": "@router.post('/start')", - "line_range": "L70-L150", - "explanation": "Starts a parallel dual-model comparison stream." - }, - { - "path": "static/js/compare/index.js", - "symbol": "initCompareView", - "line_range": "L1-L100", - "explanation": "Renders side-by-side model chat panes." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "MODEL-006", - "domain": "model", - "name": "GitHub Copilot Device Flow Authentication", - "purpose": "Authenticates with GitHub Copilot via OAuth device flow to use Copilot models directly.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/providerDeviceFlow.js" - ], - "backend_entrypoints": [ - "routes/copilot_routes.py:setup_copilot_routes", - "routes/device_flow.py:create_device_flow_router", - "src/copilot.py:request_device_code", - "src/copilot.py:poll_access_token" - ], - "routes": [ - "/api/copilot/device/start", - "/api/copilot/device/poll" - ], - "configuration": [ - "COPILOT_CLIENT_ID" - ], - "persistence": [ - "DATA_DIR/copilot_auth.json" - ], - "dependencies": [ - "httpx" - ], - "tests": [ - "tests/test_provider_device_flow_js.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Token expiration requires manual device re-authentication." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/copilot_routes.py", - "symbol": "setup_copilot_routes", - "line_range": "L166-L173", - "explanation": "Builds the Copilot device-flow router at prefix /api/copilot, wiring _start_device_flow and _poll_device_flow." - }, - { - "path": "routes/device_flow.py", - "symbol": "create_device_flow_router", - "line_range": "L135-L193", - "explanation": "Shared factory registering POST /device/start and POST /device/poll under the caller-supplied prefix." - }, - { - "path": "src/copilot.py", - "symbol": "request_device_code", - "line_range": "L150-L161", - "explanation": "Issues the GitHub device-code request that begins the Copilot OAuth device flow." - }, - { - "path": "src/copilot.py", - "symbol": "poll_access_token", - "line_range": "L164-L180", - "explanation": "Polls GitHub for the access token once the user has authorized the device code." - }, - { - "path": "tests/test_provider_device_flow_js.py", - "symbol": "test_copilot_success_uses_complete_verification_uri", - "line_range": "L28-L65", - "explanation": "Inspected unit test asserting the Copilot device-flow runner surfaces the complete verification URI returned by the backend." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E2", - "runtime_validation": { - "required": true, - "status": "pending", - "reason": "Requires an interactive GitHub Copilot OAuth device-flow account." - } - }, - { - "id": "MODEL-007", - "domain": "model", - "name": "ChatGPT Subscription Device Flow Authentication", - "purpose": "Authenticates with ChatGPT Pro/Plus subscription tokens via device login flow.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/providerDeviceFlow.js" - ], - "backend_entrypoints": [ - "routes/chatgpt_subscription_routes.py:setup_chatgpt_subscription_routes", - "routes/device_flow.py:create_device_flow_router", - "src/chatgpt_subscription.py:request_device_code", - "src/chatgpt_subscription.py:poll_device_auth" - ], - "routes": [ - "/api/chatgpt-subscription/device/start", - "/api/chatgpt-subscription/device/poll" - ], - "configuration": [], - "persistence": [ - "DATA_DIR/chatgpt_auth.json" - ], - "dependencies": [ - "httpx" - ], - "tests": [ - "tests/test_provider_device_flow_js.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Changes in OpenAI auth endpoint security challenges." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/chatgpt_subscription_routes.py", - "symbol": "setup_chatgpt_subscription_routes", - "line_range": "L163-L170", - "explanation": "Builds the ChatGPT subscription device-flow router at prefix /api/chatgpt-subscription." - }, - { - "path": "routes/device_flow.py", - "symbol": "create_device_flow_router", - "line_range": "L135-L193", - "explanation": "Shared factory registering POST /device/start and POST /device/poll under the caller-supplied prefix." - }, - { - "path": "src/chatgpt_subscription.py", - "symbol": "request_device_code", - "line_range": "L168-L181", - "explanation": "Issues the ChatGPT device-authorization request that begins the subscription OAuth device flow." - }, - { - "path": "src/chatgpt_subscription.py", - "symbol": "poll_device_auth", - "line_range": "L184-L193", - "explanation": "Polls the ChatGPT device-authorization endpoint for completion using the stored device_auth_id and user_code." - }, - { - "path": "tests/test_provider_device_flow_js.py", - "symbol": "test_chatgpt_success_uses_plain_verification_uri", - "line_range": "L68-L95", - "explanation": "Inspected unit test asserting the ChatGPT device-flow runner uses the plain verification URI rather than the Copilot complete-URI form." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E2", - "runtime_validation": { - "required": true, - "status": "pending", - "reason": "Requires an interactive ChatGPT subscription OAuth flow." - } - }, - { - "id": "MODEL-008", - "domain": "model", - "name": "Embedding Model Lane & Vector Provider Setup", - "purpose": "Configures local sentence-transformers, FastEmbed, or remote OpenAI embedding model lanes.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/settings.js" - ], - "backend_entrypoints": [ - "routes/embedding_routes.py:setup_embedding_routes", - "src/embeddings.py", - "src/embedding_lanes.py" - ], - "routes": [ - "/api/embeddings/active", - "/api/embeddings/test" - ], - "configuration": [ - "EMBEDDING_PROVIDER", - "EMBEDDING_MODEL" - ], - "persistence": [ - "DATA_DIR/embeddings_config.json" - ], - "dependencies": [ - "fastembed", - "sentence-transformers" - ], - "tests": [ - "tests/test_embedding_lane_ndarray_restore.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "First-time download of heavy PyTorch model weights on CPU-only machines." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/embedding_routes.py", - "symbol": "setup_embedding_routes", - "line_range": "L30-L120", - "explanation": "Provides embedding provider configuration endpoints." - }, - { - "path": "src/embeddings.py", - "symbol": "EmbeddingManager", - "line_range": "L40-L200", - "explanation": "Generates dense vector embeddings for RAG and memory." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "COOKBOOK-001", - "domain": "cookbook", - "name": "Local Model Download & Recipe Lifecycle Management", - "purpose": "Downloads HuggingFace models, configures execution parameters, and manages local GGUF/MLX model servers.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/cookbook.js", - "static/js/cookbookServe.js", - "static/js/cookbookDownload.js" - ], - "backend_entrypoints": [ - "routes/cookbook_routes.py:setup_cookbook_routes", - "src/tools/cookbook.py", - "src/cookbook_serve_lifecycle.py" - ], - "routes": [ - "/api/cookbook/download", - "/api/cookbook/serve", - "/api/cookbook/status" - ], - "configuration": [ - "COOKBOOK_MODELS_DIR" - ], - "persistence": [ - "DATA_DIR/models/" - ], - "dependencies": [ - "huggingface_hub", - "subprocess" - ], - "tests": [ - "tests/test_cookbook_endpoint_registration.py", - "tests/test_cookbook_port_parsing_js.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Disk space exhaustion during multi-gigabyte GGUF weights downloads." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/cookbook_routes.py", - "symbol": "setup_cookbook_routes", - "line_range": "L100-L300", - "explanation": "Exposes model downloading and process serving endpoints." - }, - { - "path": "static/js/cookbook.js", - "symbol": "initCookbook", - "line_range": "L1-L150", - "explanation": "UI manager for local model library." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "COOKBOOK-002", - "domain": "cookbook", - "name": "Hardware Model Fitting ('What Fits?') Analysis Engine", - "purpose": "Calculates RAM/VRAM requirements, quantized size, and context overhead to determine model compatibility.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/cookbook-hwfit.js" - ], - "backend_entrypoints": [ - "routes/hwfit_routes.py:setup_hwfit_routes", - "services/hwfit/fit.py:calculate_fit" - ], - "routes": [ - "/api/hwfit/fit", - "/api/hwfit/profile" - ], - "configuration": [ - "FORCE_GPU_VENDOR" - ], - "persistence": [ - "System hardware specs" - ], - "dependencies": [ - "psutil", - "torch" - ], - "tests": [ - "tests/test_hwfit_gemma4_12b.py", - "tests/test_hwfit_bandwidth_nonstring.py", - "tests/test_hwfit_gpu_count_nonnumeric.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Inaccurate VRAM estimation for non-standard KV-cache quantization." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/hwfit_routes.py", - "symbol": "setup_hwfit_routes", - "line_range": "L40-L120", - "explanation": "Calculates hardware model compatibility." - }, - { - "path": "services/hwfit/fit.py", - "symbol": "calculate_fit", - "line_range": "L30-L140", - "explanation": "Performs parameter and memory fit calculations." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "COOKBOOK-003", - "domain": "cookbook", - "name": "HuggingFace & MLX Model Discovery Services", - "purpose": "Searches HuggingFace Hub and local MLX model repositories for compatible GGUF and MLX weights.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/cookbook.js" - ], - "backend_entrypoints": [ - "routes/hwfit_routes.py:hf_search", - "services/hwfit/hf_discovery.py" - ], - "routes": [ - "/api/hwfit/hf-search", - "/api/hwfit/mlx-models" - ], - "configuration": [ - "HF_TOKEN" - ], - "persistence": [ - "Cache directory" - ], - "dependencies": [ - "huggingface_hub" - ], - "tests": [ - "tests/test_cookbook_hf_token.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "HuggingFace API rate limits when searching without an API token." - ], - "unknowns": [], - "evidence": [ - { - "path": "services/hwfit/hf_discovery.py", - "symbol": "search_hf_models", - "line_range": "L20-L90", - "explanation": "Queries HuggingFace API for model tags and files." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "COOKBOOK-004", - "domain": "cookbook", - "name": "Host Docker Access for Inference Container Runtimes", - "purpose": "Detects and connects to host Docker engine to launch containerized Ollama, vLLM, or SGLang runtimes.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/cookbookServe.js" - ], - "backend_entrypoints": [ - "src/host_docker_access.py:HostDockerAccess" - ], - "routes": [ - "Implicit local docker socket API calls" - ], - "configuration": [ - "DOCKER_HOST" - ], - "persistence": [ - "/var/run/docker.sock" - ], - "dependencies": [ - "docker" - ], - "tests": [ - "tests/test_cookbook_docker_access.py" - ], - "documentation": [ - "docker/host-docker.yml" - ], - "risks": [ - "Permission denied accessing docker socket on non-root setups." - ], - "unknowns": [], - "evidence": [ - { - "path": "src/host_docker_access.py", - "symbol": "HostDockerAccess", - "line_range": "L50-L62", - "explanation": "Interacts with host docker daemon." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": true, - "status": "pending", - "reason": "Requires Docker access and supported physical GPU hardware." - } - }, - { - "id": "RESEARCH-001", - "domain": "research", - "name": "Deep Research Execution Engine & SSE Progress Streaming", - "purpose": "Executes multi-step recursive deep research tasks, web page scraping, synthesis, and streams live progress.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/research/panel.js", - "static/js/research/jobs.js", - "static/js/researchSynapse.js" - ], - "backend_entrypoints": [ - "routes/research/research_routes.py:start_research", - "src/deep_research.py", - "services/research/service.py" - ], - "routes": [ - "/api/research/start", - "/api/research/stream/{session_id}", - "/api/research/active" - ], - "configuration": [ - "MAX_RESEARCH_DEPTH", - "SEARXNG_URL" - ], - "persistence": [ - "DATA_DIR/research/" - ], - "dependencies": [ - "beautifulsoup4", - "httpx", - "asyncio" - ], - "tests": [ - "tests/test_research_owner_scope_routes.py", - "tests/test_services_research_low_quality_sources.py" - ], - "documentation": [ - "docs/research.webm" - ], - "risks": [ - "High memory consumption when parsing multi-megabyte HTML target pages." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/research/research_routes.py", - "symbol": "@router.post('/api/research/start')", - "line_range": "L492-L550", - "explanation": "Initiates deep research job." - }, - { - "path": "src/deep_research.py", - "symbol": "DeepResearchEngine", - "line_range": "L40-L300", - "explanation": "Recursive search and summary crawler." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "RESEARCH-002", - "domain": "research", - "name": "Research Library, Detail View & Image Controls", - "purpose": "Stores completed research reports, generated diagrams, reference links, and manages image visibility.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/research/panel.js" - ], - "backend_entrypoints": [ - "routes/research/research_routes.py:get_library" - ], - "routes": [ - "/api/research/library", - "/api/research/detail/{session_id}", - "/api/research/{session_id}/hide-image" - ], - "configuration": [], - "persistence": [ - "DATA_DIR/research/library.json" - ], - "dependencies": [ - "pydantic" - ], - "tests": [ - "tests/cli/test_research_cli_preview.py", - "tests/test_research_routes_path_confinement.py" - ], - "documentation": [ - "docs/research.webm" - ], - "risks": [ - "Orphaned report files if storage directory is modified out-of-band." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/research/research_routes.py", - "symbol": "@router.get('/api/research/library')", - "line_range": "L366-L415", - "explanation": "Returns all saved research reports." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "RESEARCH-003", - "domain": "research", - "name": "Web Search Engine Integration (SearXNG & Multi-Provider)", - "purpose": "Queries SearXNG, DuckDuckGo, or Google Search instances to retrieve web search snippets.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/chat.js", - "static/js/search.js" - ], - "backend_entrypoints": [ - "routes/search_routes.py:setup_search_routes", - "src/search/core.py", - "services/search/service.py" - ], - "routes": [ - "/api/search" - ], - "configuration": [ - "SEARXNG_URL", - "SEARCH_PROVIDER" - ], - "persistence": [ - "In-memory search cache" - ], - "dependencies": [ - "httpx" - ], - "tests": [ - "tests/test_search_ranking.py", - "tests/test_searxng_image_pinned.py" - ], - "documentation": [ - "config/searxng/settings.yml" - ], - "risks": [ - "Search provider IP throttling or rate-limiting." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/search_routes.py", - "symbol": "setup_search_routes", - "line_range": "L39-L100", - "explanation": "Defines /api/search, /api/search/config, and /api/search/query endpoints." - }, - { - "path": "src/search/core.py", - "symbol": "SearchEngine", - "line_range": "L1-L12", - "explanation": "Compatibility module aliasing services.search.core." - }, - { - "path": "tests/test_search_ranking.py", - "symbol": "test_news_queries_prefer_news_sources_over_sports_and_social_results", - "line_range": "L1-L39", - "explanation": "Tests search result domain ranking and scoring." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E1", - "runtime_validation": { - "required": true, - "status": "pending", - "reason": "Requires an active SearXNG instance or external search API provider." - } - }, - { - "id": "RESEARCH-004", - "domain": "research", - "name": "Research Result Peeking & Topic Spinoff Generation", - "purpose": "Extracts preliminary research snippets and spawns child research sessions focused on specific sub-topics.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/research/panel.js" - ], - "backend_entrypoints": [ - "routes/research/research_routes.py:peek_result", - "routes/research/research_routes.py:spinoff_research" - ], - "routes": [ - "/api/research/result-peek/{session_id}", - "/api/research/spinoff/{session_id}" - ], - "configuration": [], - "persistence": [ - "DATA_DIR/research/" - ], - "dependencies": [ - "fastapi" - ], - "tests": [ - "tests/test_research_routes_path_confinement.py" - ], - "documentation": [ - "docs/research.webm" - ], - "risks": [ - "Deep recursion tree depth when spawning multiple nested spinoffs." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/research/research_routes.py", - "symbol": "@router.post('/api/research/spinoff/{session_id}')", - "line_range": "L635-L680", - "explanation": "Spawns child research session for specific query." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "DOCUMENT-001", - "domain": "document", - "name": "Document & Canvas Artifact Management", - "purpose": "Renders dynamic canvas documents, handles live editing, markdown preview, and side-by-side artifact display.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/document.js", - "static/js/documentLibrary.js" - ], - "backend_entrypoints": [ - "routes/document_routes.py:setup_document_routes", - "src/document_actions.py", - "src/document_processor.py" - ], - "routes": [ - "/api/document", - "/api/document/{id}" - ], - "configuration": [ - "MAX_DOCUMENT_SIZE_MB" - ], - "persistence": [ - "DATA_DIR/documents/" - ], - "dependencies": [ - "pydantic", - "fastapi" - ], - "tests": [ - "tests/test_document_actions_nonstring.py", - "tests/test_document_diff_discard_on_update_js.py" - ], - "documentation": [ - "docs/document.webm" - ], - "risks": [ - "Concurrent edits on the same document artifact." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/document_routes.py", - "symbol": "setup_document_routes", - "line_range": "L100-L300", - "explanation": "Registers document artifact CRUD routes." - }, - { - "path": "static/js/document.js", - "symbol": "initDocumentView", - "line_range": "L1-L150", - "explanation": "Renders interactive canvas document panel." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "DOCUMENT-002", - "domain": "document", - "name": "PDF Form Processing & High-Fidelity Rendering", - "purpose": "Extracts form fields from PDF files, fills dynamic values, and generates PDF previews.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/document.js" - ], - "backend_entrypoints": [ - "src/pdf_runtime.py", - "src/pdf_forms.py", - "src/pdf_form_doc.py" - ], - "routes": [ - "Implicit PDF document processing" - ], - "configuration": [], - "persistence": [ - "DATA_DIR/documents/pdf/" - ], - "dependencies": [ - "pypdf", - "fitz" - ], - "tests": [ - "tests/test_document_pdf_marker.py", - "tests/test_security_headers_pdf_preview.py" - ], - "documentation": [ - "docs/attachments.md" - ], - "risks": [ - "Complex XFA PDF forms may not extract cleanly with standard pdf parsers." - ], - "unknowns": [], - "evidence": [ - { - "path": "src/pdf_runtime.py", - "symbol": "load_pymupdf_for_pdf_viewer", - "line_range": "L9-L15", - "explanation": "Loads optional PyMuPDF runtime for PDF viewing." - }, - { - "path": "src/pdf_forms.py", - "symbol": "extract_form_fields", - "line_range": "L1-L100", - "explanation": "Handles PDF form field extraction and filling." - }, - { - "path": "tests/test_document_pdf_marker.py", - "symbol": "test_marker_removed_without_eating_following_text", - "line_range": "L1-L30", - "explanation": "Tests PDF text extraction wrapper stripping without content corruption." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E1", - "runtime_validation": { - "required": true, - "status": "pending", - "reason": "Requires optional PyMuPDF (`fitz`) or pypdf runtime dependency." - } - }, - { - "id": "DOCUMENT-003", - "domain": "document", - "name": "Personal Document Indexing & RAG Retrieval", - "purpose": "Indexes local user documents (PDF, DOCX, TXT) into ChromaDB for semantic vector retrieval.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/rag.js" - ], - "backend_entrypoints": [ - "routes/personal_routes.py:setup_personal_routes", - "src/personal_docs.py", - "src/rag_manager.py" - ], - "routes": [ - "/api/personal/documents", - "/api/personal/search", - "/api/personal/index" - ], - "configuration": [ - "CHROMADB_DIR" - ], - "persistence": [ - "DATA_DIR/chroma/" - ], - "dependencies": [ - "chromadb", - "pypdf", - "docx" - ], - "tests": [ - "tests/test_personal_docs_pdf_index.py", - "tests/test_rag_index_hidden_dirs.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Slow vector embedding indexing step for massive multi-thousand page documents." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/personal_routes.py", - "symbol": "setup_personal_routes", - "line_range": "L50-L160", - "explanation": "Personal document RAG indexing and search API endpoints." - }, - { - "path": "src/personal_docs.py", - "symbol": "PersonalDocsManager", - "line_range": "L30-L180", - "explanation": "Handles file text chunking and vector storage." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "DOCUMENT-004", - "domain": "document", - "name": "Document Conversion & Text Extraction Engine", - "purpose": "Converts office formats (.docx, .xlsx, .pptx) and HTML into clean Markdown text representations.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/fileHandler.js" - ], - "backend_entrypoints": [ - "src/markitdown_runtime.py", - "src/office_doc.py" - ], - "routes": [ - "Implicit file conversion calls" - ], - "configuration": [], - "persistence": [ - "Temporary conversion cache" - ], - "dependencies": [ - "markitdown", - "python-docx" - ], - "tests": [ - "tests/cli/test_docs_cli_content_length.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Formatting loss when parsing legacy binary doc/xls files." - ], - "unknowns": [], - "evidence": [ - { - "path": "src/markitdown_runtime.py", - "symbol": "convert_to_markdown", - "line_range": "L15-L80", - "explanation": "Converts binary office documents into structured Markdown text." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "DOCUMENT-005", - "domain": "document", - "name": "Document Library UI Navigation", - "purpose": "Provides dedicated UI view for browsing, filtering, and organizing saved user documents.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/documentLibrary.js" - ], - "backend_entrypoints": [ - "app.py:serve_library" - ], - "routes": [ - "/library" - ], - "configuration": [], - "persistence": [ - "DATA_DIR/documents/" - ], - "dependencies": [ - "Vanilla JS" - ], - "tests": [ - "tests/test_document_close_clears_active_route.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Large folder trees may cause initial DOM render slowdown." - ], - "unknowns": [], - "evidence": [ - { - "path": "static/js/documentLibrary.js", - "symbol": "initDocumentLibrary", - "line_range": "L1-L100", - "explanation": "Renders document library navigation grid." - }, - { - "path": "app.py", - "symbol": "serve_library", - "line_range": "L911-L914", - "explanation": "Serves SPA shell for /library route." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "EMAIL-001", - "domain": "email", - "name": "Email Account Setup, IMAP/SMTP Connection & Polling", - "purpose": "Configures IMAP/SMTP email accounts, validates TLS certificates, and polls background inbox updates.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/emailInbox.js" - ], - "backend_entrypoints": [ - "routes/email_routes.py:setup_email_routes", - "routes/email_pollers.py" - ], - "routes": [ - "/api/email/accounts", - "/api/email/poll" - ], - "configuration": [ - "EMAIL_POLL_INTERVAL" - ], - "persistence": [ - "DATA_DIR/email_accounts.json" - ], - "dependencies": [ - "imaplib", - "smtplib" - ], - "tests": [ - "tests/test_service_health_email.py", - "tests/test_active_email_reply_guard.py" - ], - "documentation": [ - "docs/email-outlook.md" - ], - "risks": [ - "Account lockouts if bad credentials are repeatedly polled." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/email_routes.py", - "symbol": "setup_email_routes", - "line_range": "L1453-L1500", - "explanation": "Sets up email account management and synchronization routes." - }, - { - "path": "routes/email_pollers.py", - "symbol": "_start_poller", - "line_range": "L1-L100", - "explanation": "Background poller for email inbox synchronization." - }, - { - "path": "tests/test_service_health_email.py", - "symbol": "test_email_ok_all_connect", - "line_range": "L1-L80", - "explanation": "Tests IMAP connection health probing and status reporting." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E1", - "runtime_validation": { - "required": true, - "status": "pending", - "reason": "Requires a controlled live IMAP account and network access." - } - }, - { - "id": "EMAIL-002", - "domain": "email", - "name": "Email Searching, Threading & Message Operations", - "purpose": "Parses email headers, folds signatures, groups messages into threads, and executes full-text email search.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/emailLibrary.js", - "static/js/emailLibrary/state.js" - ], - "backend_entrypoints": [ - "routes/email_routes.py:search_email", - "src/email_thread_parser.py" - ], - "routes": [ - "/api/email/search", - "/api/email/threads", - "/api/email/messages" - ], - "configuration": [], - "persistence": [ - "DATA_DIR/email_cache.db" - ], - "dependencies": [ - "sqlite3", - "email" - ], - "tests": [ - "tests/test_reply_recipients_js.py", - "tests/test_signature_fold_js.py" - ], - "documentation": [ - "docs/email-outlook.md" - ], - "risks": [ - "Malformed MIME email structures failing HTML sanitization." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/email_routes.py", - "symbol": "@router.get('/search')", - "line_range": "L250-L320", - "explanation": "Executes search across cached email headers and text." - }, - { - "path": "src/email_thread_parser.py", - "symbol": "parse_email_thread", - "line_range": "L20-L100", - "explanation": "Builds conversation tree from Message-ID and In-Reply-To headers." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "EMAIL-003", - "domain": "email", - "name": "Email Composition, Draft Management & Sending", - "purpose": "Creates, saves, and dispatches HTML/plaintext email messages via SMTP.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/emailShared.js" - ], - "backend_entrypoints": [ - "routes/email_routes.py:send_email" - ], - "routes": [ - "/api/email/draft", - "/api/email/send" - ], - "configuration": [], - "persistence": [ - "DATA_DIR/email_drafts.json" - ], - "dependencies": [ - "smtplib" - ], - "tests": [ - "tests/test_schedule_email_offset_normalization.py" - ], - "documentation": [ - "docs/email-outlook.md" - ], - "risks": [ - "SMTP connection drop mid-send causing unsent mail state." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/email_routes.py", - "symbol": "@router.post('/send')", - "line_range": "L450-L520", - "explanation": "Sends email message via user SMTP credentials." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": true, - "status": "pending", - "reason": "Requires a controlled live SMTP account and network access." - } - }, - { - "id": "EMAIL-004", - "domain": "email", - "name": "Email MCP Server & Codex Integration Bridge", - "purpose": "Exposes constrained email reading and draft capabilities to external Codex / MCP agents with scope checks.", - "status": "verified", - "frontend_entrypoints": [ - "integrations/codex/scripts/odysseus_api.py" - ], - "backend_entrypoints": [ - "mcp_servers/email_server.py", - "routes/codex_routes.py:setup_codex_routes" - ], - "routes": [ - "/api/codex/email/search", - "/api/codex/email/send" - ], - "configuration": [ - "CODEX_API_KEY" - ], - "persistence": [ - "API token scopes" - ], - "dependencies": [ - "mcp", - "fastapi" - ], - "tests": [ - "tests/cli/test_mail_cli_recipients.py" - ], - "documentation": [ - "integrations/codex/README.md" - ], - "risks": [ - "Unauthorized mail sending if token scopes are improperly scoped." - ], - "unknowns": [], - "evidence": [ - { - "path": "mcp_servers/email_server.py", - "symbol": "EmailMcpServer", - "line_range": "L20-L120", - "explanation": "MCP server exposing email tools over stdio/SSE." - }, - { - "path": "routes/codex_routes.py", - "symbol": "setup_codex_routes", - "line_range": "L30-L110", - "explanation": "Bridge endpoints for external Codex plugin integration." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "CALENDAR-001", - "domain": "calendar", - "name": "CalDAV Calendar Synchronization & Account Setup", - "purpose": "Connects to remote CalDAV servers (Apple iCloud, Nextcloud, Google) to sync calendar event feeds.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/calendar.js" - ], - "backend_entrypoints": [ - "routes/calendar_routes.py:setup_calendar_routes", - "src/caldav_sync.py:CalDavSync" - ], - "routes": [ - "/api/calendar/config", - "/api/calendar/sync" - ], - "configuration": [ - "CALDAV_URL", - "CALDAV_USERNAME" - ], - "persistence": [ - "DATA_DIR/calendar_config.json" - ], - "dependencies": [ - "caldav", - "vobject" - ], - "tests": [ - "tests/test_caldav_url_hardening.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Invalid SSL certificates on self-hosted CalDAV servers." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/calendar_routes.py", - "symbol": "setup_calendar_routes", - "line_range": "L50-L180", - "explanation": "Exposes CalDAV setup and manual sync trigger routes." - }, - { - "path": "src/caldav_sync.py", - "symbol": "CalDavSync", - "line_range": "L30-L160", - "explanation": "Fetches and parses remote iCalendar VEVENT objects." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": true, - "status": "pending", - "reason": "Requires a controlled external CalDAV server." - } - }, - { - "id": "CALENDAR-002", - "domain": "calendar", - "name": "Calendar Event Operations & iCalendar Parsing", - "purpose": "Creates, updates, deletes, and displays calendar events with timezone conversion and reminder notifications.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/calendar/reminders.js", - "static/js/calendar/utils.js" - ], - "backend_entrypoints": [ - "routes/calendar_routes.py:create_event", - "src/caldav_writeback.py", - "src/tools/calendar.py" - ], - "routes": [ - "/api/calendar/events", - "/api/calendar/events/{event_id}" - ], - "configuration": [ - "USER_TIMEZONE" - ], - "persistence": [ - "DATA_DIR/calendar_events.db" - ], - "dependencies": [ - "sqlite3", - "icalendar" - ], - "tests": [ - "tests/test_calendar_parse_dt_time_first.py", - "tests/test_calendar_update_event_tz.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Recurring RRULE event expansion calculation bugs across leap years." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/calendar_routes.py", - "symbol": "@router.get('/events')", - "line_range": "L220-L310", - "explanation": "Fetches calendar events for requested date window." - }, - { - "path": "src/tools/calendar.py", - "symbol": "CalendarTool", - "line_range": "L25-L120", - "explanation": "Agent tool for creating and modifying calendar entries." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "MEDIA-001", - "domain": "media", - "name": "Gallery Image Library & Album Operations", - "purpose": "Organizes images into custom albums, provides grid browsing, tagging, and album metadata management.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/gallery.js" - ], - "backend_entrypoints": [ - "routes/gallery/gallery_routes.py:setup_gallery_routes" - ], - "routes": [ - "/api/gallery/library", - "/api/gallery/albums", - "/api/gallery/upload" - ], - "configuration": [ - "GALLERY_STORAGE_DIR" - ], - "persistence": [ - "DATA_DIR/gallery/" - ], - "dependencies": [ - "pillow", - "sqlite3" - ], - "tests": [ - "tests/test_gallery_image_endpoint_owner_scope.py", - "tests/test_gallery_owner_filter_single_user.py" - ], - "documentation": [ - "docs/gallery.webm" - ], - "risks": [ - "Thumbnail generation overhead for high-resolution RAW camera images." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/gallery/gallery_routes.py", - "symbol": "@router.get('/api/gallery/library')", - "line_range": "L657-L720", - "explanation": "Fetches image library list with tag filters." - }, - { - "path": "static/js/gallery.js", - "symbol": "initGallery", - "line_range": "L1-L180", - "explanation": "Main gallery grid renderer and uploader." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "MEDIA-002", - "domain": "media", - "name": "Image Processing, AI Upscaling & Style Transfer", - "purpose": "Executes local image enhancement, background removal, face sharpening, and AI upscaling operations.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/galleryEditor.js", - "static/js/editor/ai-tool-runner.js" - ], - "backend_entrypoints": [ - "routes/gallery/gallery_routes.py:ai_upscale", - "routes/gallery/gallery_routes.py:remove_bg" - ], - "routes": [ - "/api/gallery/ai-upscale", - "/api/gallery/style-transfer", - "/api/image/inpaint", - "/api/image/remove-bg" - ], - "configuration": [ - "REALESRGAN_MODEL_PATH" - ], - "persistence": [ - "DATA_DIR/gallery/processed/" - ], - "dependencies": [ - "onnxruntime", - "opencv-python" - ], - "tests": [ - "tests/test_sanitize_multimodal_merge.py" - ], - "documentation": [ - "docs/gallery.webm" - ], - "risks": [ - "High GPU memory allocation when upscaling 4K images." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/gallery/gallery_routes.py", - "symbol": "@router.post('/api/gallery/ai-upscale')", - "line_range": "L544-L580", - "explanation": "Runs RealESRGAN image upscaling." - }, - { - "path": "routes/gallery/gallery_routes.py", - "symbol": "@router.post('/api/image/remove-bg')", - "line_range": "L1950-L2010", - "explanation": "Executes background removal pass." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "MEDIA-003", - "domain": "media", - "name": "Interactive Image Canvas Editor & Persisted Drafts", - "purpose": "Provides full multi-layer raster canvas editor, brush tools, transforms, masks, and draft project persistence.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/galleryEditor.js", - "static/js/editor/history-panel.js" - ], - "backend_entrypoints": [ - "routes/editor_draft_routes.py:setup_editor_draft_routes" - ], - "routes": [ - "/api/editor/drafts", - "/api/editor/drafts/{draft_id}" - ], - "configuration": [ - "MAX_DRAFT_PROJECTS" - ], - "persistence": [ - "DATA_DIR/editor_drafts/" - ], - "dependencies": [ - "HTML5 Canvas API", - "pydantic" - ], - "tests": [ - "tests/test_canvas_coords_empty_touches_js.py", - "tests/test_snap_other_layers_nonarray_js.py" - ], - "documentation": [ - "docs/gallery.webm" - ], - "risks": [ - "Browser memory leak if multi-gigabyte layer undo buffers are kept indefinitely." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/editor_draft_routes.py", - "symbol": "setup_editor_draft_routes", - "line_range": "L30-L110", - "explanation": "API routes for saving and loading canvas project drafts." - }, - { - "path": "static/js/editor/history-panel.js", - "symbol": "HistoryManager", - "line_range": "L1-L90", - "explanation": "Canvas undo/redo stack manager." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "MEDIA-004", - "domain": "media", - "name": "Text-to-Speech (TTS) Synthesis Service", - "purpose": "Synthesizes spoken audio from text using local Kokoro, EdgeTTS, or OpenAI TTS engines.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/tts-ai.js" - ], - "backend_entrypoints": [ - "routes/tts_routes.py:setup_tts_routes", - "services/tts/tts_service.py:TTSService" - ], - "routes": [ - "/api/tts/synthesize", - "/api/tts/stats" - ], - "configuration": [ - "TTS_PROVIDER", - "TTS_VOICE" - ], - "persistence": [ - "DATA_DIR/tts_cache/" - ], - "dependencies": [ - "soundfile", - "edge-tts" - ], - "tests": [ - "tests/test_tts_available_nonstring_provider.py", - "tests/test_tts_cache_stats.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Audio synthesis latency on CPU-only hardware setups." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/tts_routes.py", - "symbol": "@router.post('/synthesize')", - "line_range": "L30-L75", - "explanation": "Synthesizes TTS audio clip." - }, - { - "path": "services/tts/tts_service.py", - "symbol": "TTSService", - "line_range": "L25-L140", - "explanation": "Provider abstraction layer for audio speech generation." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "MEDIA-005", - "domain": "media", - "name": "Speech-to-Text (STT) Audio Transcription Service", - "purpose": "Transcribes user audio recordings into text using faster-whisper or local speech models.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/voiceRecorder.js" - ], - "backend_entrypoints": [ - "routes/stt_routes.py:setup_stt_routes", - "services/stt/stt_service.py:STTService" - ], - "routes": [ - "/api/stt/transcribe", - "/api/stt/stats" - ], - "configuration": [ - "STT_PROVIDER", - "WHISPER_MODEL_SIZE" - ], - "persistence": [ - "Temporary audio buffer" - ], - "dependencies": [ - "faster-whisper", - "ffmpeg-python" - ], - "tests": [ - "tests/test_stt_leak.py", - "tests/test_speech_service_toggles.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Missing ffmpeg system dependency prevents audio format decoding." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/stt_routes.py", - "symbol": "@router.post('/transcribe')", - "line_range": "L25-L55", - "explanation": "Accepts multipart audio file and returns transcription text." - }, - { - "path": "services/stt/stt_service.py", - "symbol": "STTService", - "line_range": "L20-L110", - "explanation": "Whisper audio transcription engine wrapper." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "MEDIA-006", - "domain": "media", - "name": "Digital Signature Stamp Storage & Placement", - "purpose": "Stores transparent PNG user signatures and stamps for placement onto PDF forms and documents.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/signature.js" - ], - "backend_entrypoints": [ - "routes/signature_routes.py:setup_signature_routes" - ], - "routes": [ - "/api/signatures", - "/api/signatures/{sig_id}" - ], - "configuration": [], - "persistence": [ - "DATA_DIR/signatures/" - ], - "dependencies": [ - "pillow" - ], - "tests": [ - "tests/test_signature_route_hardening.py", - "tests/test_signature_settings_dom_xss.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Cross-site scripting if signature image titles contain unescaped user input." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/signature_routes.py", - "symbol": "setup_signature_routes", - "line_range": "L30-L120", - "explanation": "CRUD endpoints for managing user signature PNG stamps." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "MEDIA-007", - "domain": "media", - "name": "Generated Image Artifact Route & MCP Integration", - "purpose": "Serves generated AI artwork artifacts and integrates with image generation MCP server.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/chat.js" - ], - "backend_entrypoints": [ - "app.py:serve_generated_image", - "src/generated_images.py", - "mcp_servers/image_gen_server.py" - ], - "routes": [ - "/api/generated-image/{filename}" - ], - "configuration": [], - "persistence": [ - "DATA_DIR/generated_images/" - ], - "dependencies": [ - "pillow", - "mcp" - ], - "tests": [ - "tests/test_image_models_nondict_system.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Path traversal vulnerability if filename parameter is un-sanitized." - ], - "unknowns": [], - "evidence": [ - { - "path": "app.py", - "symbol": "serve_generated_image", - "line_range": "L499-L520", - "explanation": "Serves generated image artifacts with cache headers." - }, - { - "path": "src/generated_images.py", - "symbol": "resolve_generated_image_path", - "line_range": "L20-L32", - "explanation": "Confines requested image path within artifacts directory." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "MEDIA-008", - "domain": "media", - "name": "Native MLX Image Bridge (macOS Apple Silicon)", - "purpose": "Native Apple Swift bridge for hardware-accelerated diffusion and MLX image colorization on macOS.", - "status": "experimental", - "frontend_entrypoints": [ - "static/js/editor/ai-tools-misc.js" - ], - "backend_entrypoints": [ - "swift/odysseus-mlx-image-bridge/Package.swift", - "scripts/mlx_image_server.py", - "scripts/diffusion_server.py" - ], - "routes": [ - "/api/image/mlx-colorize" - ], - "configuration": [ - "ENABLE_MLX_ACCELERATION" - ], - "persistence": [ - "Build artifact binary" - ], - "dependencies": [ - "Swift", - "MLX Framework" - ], - "tests": [ - "tests/helpers/import_state.py" - ], - "documentation": [ - "swift/odysseus-mlx-image-bridge/Package.swift" - ], - "risks": [ - "Requires macOS host with Apple Silicon M-series chip and compiled Swift binary." - ], - "unknowns": [ - "Binary build requires Xcode command line tools build step (`build-macos-app.sh`)." - ], - "evidence": [ - { - "path": "swift/odysseus-mlx-image-bridge/Package.swift", - "symbol": "Package", - "line_range": "L1-L30", - "explanation": "Swift package manifest for native MLX image bridge." - }, - { - "path": "scripts/mlx_image_server.py", - "symbol": "main", - "line_range": "L20-L90", - "explanation": "Python daemon wrapping native Swift MLX binary." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": true, - "status": "pending", - "reason": "Requires Apple Silicon, macOS tooling, and the compiled MLX bridge." - } - }, - { - "id": "SECURITY-001", - "domain": "security", - "name": "Authentication, Session Cookies & User Management", - "purpose": "Handles bcrypt password hashing, session cookie issuance, authentication enforcement, and user administration.", - "status": "verified", - "frontend_entrypoints": [ - "static/login.html", - "static/js/admin.js" - ], - "backend_entrypoints": [ - "routes/auth_routes.py:setup_auth_routes", - "core/auth.py:AuthManager" - ], - "routes": [ - "/api/auth/login", - "/api/auth/me", - "/api/auth/users", - "/api/auth/logout" - ], - "configuration": [ - "AUTH_ENABLED", - "SESSION_COOKIE_NAME" - ], - "persistence": [ - "DATA_DIR/auth.db" - ], - "dependencies": [ - "bcrypt", - "itsdangerous" - ], - "tests": [ - "tests/test_rename_user_owner_sync.py", - "tests/test_reserved_username_admin_escalation.py" - ], - "documentation": [ - "THREAT_MODEL.md", - "SECURITY.md" - ], - "risks": [ - "Cookie session hijack if deployed over unencrypted HTTP without HTTPS cookie flags." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/auth_routes.py", - "symbol": "@router.post('/login')", - "line_range": "L100-L180", - "explanation": "Authenticates credentials and sets session cookie." - }, - { - "path": "core/auth.py", - "symbol": "AuthManager", - "line_range": "L40-L260", - "explanation": "Handles user creation, password verification, and session tokens." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "SECURITY-002", - "domain": "security", - "name": "System Vault Encrypted Secret Storage", - "purpose": "Encrypts API keys, passphrases, and third-party secrets on disk using AES-GCM / PBKDF2 key derivation.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/settings.js" - ], - "backend_entrypoints": [ - "routes/vault_routes.py:setup_vault_routes", - "src/secret_storage.py:SecretVault", - "src/tools/vault.py" - ], - "routes": [ - "/api/vault/config", - "/api/vault/unlock", - "/api/vault/lock" - ], - "configuration": [ - "VAULT_SALT" - ], - "persistence": [ - "DATA_DIR/vault.enc" - ], - "dependencies": [ - "cryptography" - ], - "tests": [ - "tests/test_vault_password_not_in_argv.py" - ], - "documentation": [ - "THREAT_MODEL.md" - ], - "risks": [ - "Loss of vault master passphrase renders all encrypted secrets permanently unrecoverable." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/vault_routes.py", - "symbol": "setup_vault_routes", - "line_range": "L126-L229", - "explanation": "Admin routes for vault configuration, login, unlock, lock, and logout." - }, - { - "path": "src/secret_storage.py", - "symbol": "SecretStorage", - "line_range": "L57-L87", - "explanation": "Fernet symmetric key DB secret encryption." - }, - { - "path": "tests/test_vault_password_not_in_argv.py", - "symbol": "test_bw_password_not_in_argv", - "line_range": "L1-L117", - "explanation": "Verifies master password is fed via stdin and never appears in process argv." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E1", - "runtime_validation": { - "required": true, - "status": "pending", - "reason": "Requires installed Bitwarden CLI (`bw`) executable." - } - }, - { - "id": "SECURITY-003", - "domain": "security", - "name": "API Token Management & Scope Access Control", - "purpose": "Generates scoped API bearer tokens (read/write/admin) for external tool and script authentication.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/settings.js" - ], - "backend_entrypoints": [ - "routes/api_token_routes.py:setup_api_token_routes", - "core/database.py:ApiToken" - ], - "routes": [ - "/api/tokens", - "/api/tokens/{token_id}" - ], - "configuration": [], - "persistence": [ - "DATA_DIR/odysseus.db (api_tokens table)" - ], - "dependencies": [ - "sqlalchemy", - "secrets" - ], - "tests": [ - "tests/test_api_key_file_permissions.py" - ], - "documentation": [ - "SECURITY.md" - ], - "risks": [ - "Leaked API bearer tokens with excessive permission scopes." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/api_token_routes.py", - "symbol": "setup_api_token_routes", - "line_range": "L30-L150", - "explanation": "Exposes API token creation, scope assignment, and revocation." - }, - { - "path": "core/database.py", - "symbol": "ApiToken", - "line_range": "L50-L90", - "explanation": "SQLAlchemy ORM schema for API tokens and permissions." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "SECURITY-004", - "domain": "security", - "name": "Prompt Security & Injection Defense Engine", - "purpose": "Scans system prompts and external inputs for prompt injection attempts, jailbreaks, and sensitive data leaks.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/chat.js" - ], - "backend_entrypoints": [ - "src/prompt_security.py:sanitize_prompt", - "src/tool_security.py:validate_tool_input" - ], - "routes": [ - "Implicit security inspection" - ], - "configuration": [ - "ENABLE_PROMPT_SANITY_CHECK" - ], - "persistence": [], - "dependencies": [ - "re" - ], - "tests": [ - "tests/test_skill_index_prompt_injection.py", - "tests/test_tool_output_prompt_injection.py" - ], - "documentation": [ - "THREAT_MODEL.md" - ], - "risks": [ - "False positives blocking legitimate complex coding or security prompts." - ], - "unknowns": [], - "evidence": [ - { - "path": "src/prompt_security.py", - "symbol": "untrusted_context_message", - "line_range": "L64-L86", - "explanation": "Wraps untrusted context with guard delimiters and sets metadata.trusted = False." - }, - { - "path": "src/tool_security.py", - "symbol": "NON_ADMIN_BLOCKED_TOOLS", - "line_range": "L42-L78", - "explanation": "Enforces tool execution safety for non-admin user roles." - }, - { - "path": "tests/test_skill_index_prompt_injection.py", - "symbol": "test_skill_index", - "line_range": "L1-L208", - "explanation": "Verifies skill index descriptions cannot leak into trusted system prompts." - }, - { - "path": "tests/test_tool_output_prompt_injection.py", - "symbol": "test_tool_output", - "line_range": "L1-L50", - "explanation": "Tool output injection guards." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E1", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "SECURITY-005", - "domain": "security", - "name": "URL & Path Confinement Security Guards", - "purpose": "Prevents SSRF attacks and path traversal by validating target IP addresses and resolving symlinks.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/fileHandler.js" - ], - "backend_entrypoints": [ - "src/url_security.py:validate_url", - "src/url_safety.py", - "core/log_safety.py" - ], - "routes": [ - "Implicit guard functions" - ], - "configuration": [ - "ALLOWED_DOMAINS_WHITELIST" - ], - "persistence": [], - "dependencies": [ - "ipaddress", - "urllib.parse" - ], - "tests": [ - "tests/test_url_safety.py", - "tests/test_tool_path_confinement.py", - "tests/test_workspace_confine.py" - ], - "documentation": [ - "THREAT_MODEL.md" - ], - "risks": [ - "DNS rebinding attacks if IP address is re-resolved post-validation." - ], - "unknowns": [], - "evidence": [ - { - "path": "src/url_safety.py", - "symbol": "check_outbound_url", - "line_range": "L60-L108", - "explanation": "Rejects non-HTTP(S) schemes, link-local, cloud metadata SSRF addresses." - }, - { - "path": "src/url_security.py", - "symbol": "validate_public_http_url", - "line_range": "L81-L94", - "explanation": "Validates public-facing endpoints." - }, - { - "path": "tests/test_url_safety.py", - "symbol": "test_url_safety", - "line_range": "L1-L117", - "explanation": "Scheme validation, cloud metadata SSRF rejection, IP classification." - }, - { - "path": "tests/test_tool_path_confinement.py", - "symbol": "test_path_confinement", - "line_range": "L1-L50", - "explanation": "Path traversal checks." - }, - { - "path": "tests/test_workspace_confine.py", - "symbol": "test_workspace_confine", - "line_range": "L1-L50", - "explanation": "Workspace confinement checks." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E1", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "SECURITY-006", - "domain": "security", - "name": "HTTP Security Headers Middleware", - "purpose": "Injects standard OWASP HTTP security headers (CSP, HSTS, X-Content-Type-Options, X-Frame-Options).", - "status": "verified", - "frontend_entrypoints": [ - "static/app.js" - ], - "backend_entrypoints": [ - "core/middleware.py:SecurityHeadersMiddleware" - ], - "routes": [ - "Applies globally to all routes" - ], - "configuration": [ - "CSP_NONCE_ENABLED" - ], - "persistence": [], - "dependencies": [ - "starlette.middleware.base" - ], - "tests": [ - "tests/test_security_headers_middleware.py", - "tests/test_security_headers_pdf_preview.py" - ], - "documentation": [ - "SECURITY.md" - ], - "risks": [ - "Strict Content Security Policy (CSP) blocking third-party embedded web resources." - ], - "unknowns": [], - "evidence": [ - { - "path": "core/middleware.py", - "symbol": "SecurityHeadersMiddleware", - "line_range": "L40-L110", - "explanation": "Sets strict security headers and CSP nonces on HTTP responses." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "SECURITY-007", - "domain": "security", - "name": "Admin System Data Wipe ('Danger Zone')", - "purpose": "Provides administrative reset operations to wipe sessions, cache, uploaded files, or factory reset state.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/admin.js" - ], - "backend_entrypoints": [ - "routes/admin_wipe/admin_wipe_routes.py:setup_admin_wipe_routes" - ], - "routes": [ - "/api/admin/wipe/{kind}" - ], - "configuration": [], - "persistence": [ - "ALL storage locations" - ], - "dependencies": [ - "os", - "shutil" - ], - "tests": [ - "tests/test_session_ghost_delete.py" - ], - "documentation": [ - "THREAT_MODEL.md" - ], - "risks": [ - "Accidental catastrophic data loss if triggered without user confirmation." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/admin_wipe/admin_wipe_routes.py", - "symbol": "@router.delete('/wipe/{kind}')", - "line_range": "L71-L130", - "explanation": "Executes systemic data wipe based on requested scope." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "FRONTEND-001", - "domain": "frontend", - "name": "Single Page Application Shell & Client Router", - "purpose": "Main HTML5 SPA shell, DOM lifecycle initializers, tab navigation, and deep-link route handlers.", - "status": "verified", - "frontend_entrypoints": [ - "static/index.html", - "static/app.js", - "static/js/init.js" - ], - "backend_entrypoints": [ - "app.py:serve_index" - ], - "routes": [ - "/", - "/notes", - "/calendar", - "/cookbook", - "/email", - "/memory", - "/gallery", - "/tasks", - "/library" - ], - "configuration": [], - "persistence": [ - "static/index.html" - ], - "dependencies": [ - "Vanilla HTML5/JS" - ], - "tests": [ - "tests/test_app_static_mime.py", - "tests/test_serve_html_with_nonce.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Stale browser static cache if asset hashing is omitted during deployment." - ], - "unknowns": [], - "evidence": [ - { - "path": "static/index.html", - "symbol": "index.html", - "line_range": "L1-L200", - "explanation": "Main SPA entry point containing modal roots and CSS bundles." - }, - { - "path": "app.py", - "symbol": "serve_index", - "line_range": "L867-L878", - "explanation": "Serves index.html with dynamically generated CSP nonces." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "FRONTEND-002", - "domain": "frontend", - "name": "Dynamic Theme, Color System & Custom Fonts", - "purpose": "Supports dark/light themes, custom CSS variables, color picker controls, and user font uploads.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/theme.js", - "static/style.css", - "static/js/colorPicker.js" - ], - "backend_entrypoints": [ - "routes/font_routes.py:setup_font_routes", - "routes/prefs_routes.py:setup_prefs_routes" - ], - "routes": [ - "/api/font/custom", - "/api/prefs" - ], - "configuration": [], - "persistence": [ - "DATA_DIR/user_prefs.json", - "static/fonts/custom/" - ], - "dependencies": [ - "Vanilla CSS", - "pydantic" - ], - "tests": [ - "tests/test_prefs_atomic_write.py", - "tests/test_select_dropdown_theme_css.py" - ], - "documentation": [ - "docs/theme.webm" - ], - "risks": [ - "Flash of unstyled content (FOUC) on slow connections." - ], - "unknowns": [], - "evidence": [ - { - "path": "static/js/theme.js", - "symbol": "applyTheme", - "line_range": "L1-L90", - "explanation": "Applies custom HSL theme variables to DOM document root." - }, - { - "path": "routes/font_routes.py", - "symbol": "setup_font_routes", - "line_range": "L20-L55", - "explanation": "Allows uploading and serving custom WOFF2 font files." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "FRONTEND-003", - "domain": "frontend", - "name": "Window Manager, Tile Layout & Modal Control System", - "purpose": "Manages draggable tool windows, snapped multi-tile viewports, modal dialog Z-ordering, and ESC key stacks.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/modalManager.js", - "static/js/tileManager.js", - "static/js/windowDrag.js", - "static/js/windowResize.js", - "static/js/escMenuStack.js" - ], - "backend_entrypoints": [ - "None (Client UI subsystem)" - ], - "routes": [], - "configuration": [ - "localStorage: tile_layout_state" - ], - "persistence": [ - "Browser localStorage" - ], - "dependencies": [ - "Vanilla JS DOM event listeners" - ], - "tests": [ - "tests/test_portal_dropdown_z_js.py", - "tests/test_tile_manager_snap_zones_js.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Overlap artifacts when opening many simultaneous tool floating windows." - ], - "unknowns": [], - "evidence": [ - { - "path": "static/js/modalManager.js", - "symbol": "ModalManager", - "line_range": "L1-L140", - "explanation": "Controls modal open/close transitions and focus trapping." - }, - { - "path": "static/js/tileManager.js", - "symbol": "TileManager", - "line_range": "L1-L180", - "explanation": "Handles viewport split-pane grid arrangements." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "FRONTEND-004", - "domain": "frontend", - "name": "Global Keyboard Shortcuts & Accessibility Controls", - "purpose": "Provides configurable hotkeys (Ctrl+K search, Esc close, Alt+1-9 tabs) and high-contrast accessibility options.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/keyboard-shortcuts.js", - "static/js/a11y.js" - ], - "backend_entrypoints": [ - "None (Client UI subsystem)" - ], - "routes": [], - "configuration": [ - "localStorage: user_hotkeys" - ], - "persistence": [ - "Browser localStorage" - ], - "dependencies": [ - "Vanilla JS" - ], - "tests": [ - "tests/test_keyboard_shortcuts.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Browser keybinding collisions with browser default hotkeys." - ], - "unknowns": [], - "evidence": [ - { - "path": "static/js/keyboard-shortcuts.js", - "symbol": "initShortcuts", - "line_range": "L1-L110", - "explanation": "Binds global keydown handlers for system shortcuts." - }, - { - "path": "static/js/a11y.js", - "symbol": "initA11y", - "line_range": "L1-L80", - "explanation": "Applies ARIA roles and dyslexic font toggles." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "FRONTEND-005", - "domain": "frontend", - "name": "Markdown, LaTeX & Code Block Streaming Renderer", - "purpose": "Parses incoming SSE markdown streams, renders KaTeX math formulas, syntax-highlighted code, and interactive runners.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/markdown.js", - "static/js/streamingRenderer.js", - "static/js/streamingSegmenter.js", - "static/js/codeRunner.js" - ], - "backend_entrypoints": [ - "None (Client rendering subsystem)" - ], - "routes": [], - "configuration": [], - "persistence": [ - "Browser DOM" - ], - "dependencies": [ - "highlight.js", - "katex" - ], - "tests": [ - "tests/streaming/segmenter.test.mjs", - "tests/test_streaming_segmenter_js.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "DOM thrashing if streaming segmenter updates UI too frequently." - ], - "unknowns": [], - "evidence": [ - { - "path": "static/js/markdown.js", - "symbol": "renderMarkdown", - "line_range": "L1-L200", - "explanation": "Converts markdown prose to HTML nodes with syntax highlighting." - }, - { - "path": "static/js/streamingSegmenter.js", - "symbol": "Segmenter", - "line_range": "L1-L150", - "explanation": "Parses un-closed markdown fences during live stream." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "FRONTEND-006", - "domain": "frontend", - "name": "Interactive Tour & Guided Onboarding System", - "purpose": "Presents interactive step-by-step feature tours and UI tooltip hints for new users.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/tourHints.js", - "static/js/tourAutoplay.js" - ], - "backend_entrypoints": [ - "None (Client UI subsystem)" - ], - "routes": [], - "configuration": [ - "localStorage: tour_completed" - ], - "persistence": [ - "Browser localStorage" - ], - "dependencies": [ - "Vanilla JS" - ], - "tests": [ - "tests/test_task_routes.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Tour step misalignment if window is resized mid-tour." - ], - "unknowns": [], - "evidence": [ - { - "path": "static/js/tourHints.js", - "symbol": "startTour", - "line_range": "L1-L120", - "explanation": "Renders guided feature tour overlays over target UI elements." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "FRONTEND-007", - "domain": "frontend", - "name": "Background Effects Prototyping Sandbox", - "purpose": "Standalone sandbox page for prototyping visual background animations, waves, and whirlpool effects.", - "status": "dead-code-candidate", - "frontend_entrypoints": [ - "static/wave-variants.html", - "static/whirlpool-variants.html" - ], - "backend_entrypoints": [ - "app.py:serve_backgrounds" - ], - "routes": [ - "/backgrounds" - ], - "configuration": [], - "persistence": [ - "None" - ], - "dependencies": [ - "HTML5 Canvas", - "WebGL" - ], - "tests": [], - "documentation": [ - "README.md" - ], - "risks": [ - "High GPU utilization when rendering complex shader wave animations." - ], - "unknowns": [ - "Route `/backgrounds` in app.py L918 attempts to serve `static/backgrounds.html` which is missing from disk; variant templates `wave-variants.html` and `whirlpool-variants.html` exist." - ], - "evidence": [ - { - "path": "app.py", - "symbol": "serve_backgrounds", - "line_range": "L915-L918", - "explanation": "Serves visual background sandbox HTML page route." - }, - { - "path": "static/wave-variants.html", - "symbol": "wave-variants.html", - "line_range": "L1-L150", - "explanation": "Interactive background effect prototyping sandbox variant." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E1", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "PLATFORM-001", - "domain": "platform", - "name": "Application Initialization & Lifespan Management", - "purpose": "Orchestrates server startup, database table migration, background daemon initialization, and clean shutdown.", - "status": "verified", - "frontend_entrypoints": [ - "app.py" - ], - "backend_entrypoints": [ - "app.py:_lifespan", - "src/app_initializer.py:initialize_app" - ], - "routes": [ - "App startup lifespan" - ], - "configuration": [], - "persistence": [ - "DATA_DIR/" - ], - "dependencies": [ - "asyncio", - "logging" - ], - "tests": [ - "tests/test_app_helpers.py" - ], - "documentation": [ - "docs/setup.md" - ], - "risks": [ - "Un-handled exceptions during startup halt application launch." - ], - "unknowns": [], - "evidence": [ - { - "path": "app.py", - "symbol": "_lifespan", - "line_range": "L996-L1030", - "explanation": "FastAPI lifespan context manager executing startup tasks." - }, - { - "path": "src/app_initializer.py", - "symbol": "initialize_app", - "line_range": "L29-L125", - "explanation": "Initializes app directories, DB schemas, and logging." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "PLATFORM-002", - "domain": "platform", - "name": "System Health, Readiness & Version Monitoring APIs", - "purpose": "Exposes Liveness (/api/health), Readiness (/api/ready), App Version (/api/version), and Client Perf APIs.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/admin.js" - ], - "backend_entrypoints": [ - "app.py:health_check", - "app.py:readiness_check", - "app.py:get_version", - "src/readiness.py" - ], - "routes": [ - "/api/health", - "/api/ready", - "/api/version", - "/api/runtime", - "/api/client-perf" - ], - "configuration": [], - "persistence": [], - "dependencies": [ - "fastapi" - ], - "tests": [ - "tests/test_readiness.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Readiness check delays if verifying connectivity to offline remote endpoints." - ], - "unknowns": [], - "evidence": [ - { - "path": "app.py", - "symbol": "readiness_check", - "line_range": "L963-L973", - "explanation": "Performs system component integrity check." - }, - { - "path": "src/readiness.py", - "symbol": "check_readiness", - "line_range": "L15-L61", - "explanation": "Checks database, storage, and key paths for read/write access." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "PLATFORM-003", - "domain": "platform", - "name": "Database Schema, Migrations & SQLite Persistence", - "purpose": "Defines core relational tables (users, tokens, tasks, sessions) and executes automated SQLite schema upgrades.", - "status": "verified", - "frontend_entrypoints": [ - "scripts/update_database.py" - ], - "backend_entrypoints": [ - "core/database.py:init_db", - "scripts/update_database.py:run_migrations", - "src/database.py" - ], - "routes": [ - "Implicit DB queries" - ], - "configuration": [ - "DATABASE_URL" - ], - "persistence": [ - "DATA_DIR/odysseus.db" - ], - "dependencies": [ - "sqlalchemy", - "alembic", - "sqlite3" - ], - "tests": [ - "tests/test_sqlite_foreign_keys.py", - "tests/test_update_database_script.py", - "tests/test_app_db_permissions.py" - ], - "documentation": [ - "docs/setup.md" - ], - "risks": [ - "SQLite file lock contention under high concurrent write loads." - ], - "unknowns": [], - "evidence": [ - { - "path": "core/database.py", - "symbol": "init_db", - "line_range": "L40-L120", - "explanation": "Creates ORM tables and establishes connection pool." - }, - { - "path": "scripts/update_database.py", - "symbol": "run_migrations", - "line_range": "L20-L110", - "explanation": "Applies missing schema columns and indices." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "PLATFORM-004", - "domain": "platform", - "name": "User Data Export & Import Backup Infrastructure", - "purpose": "Exports complete user workspace state (sessions, memory, skills, notes, presets) into a zip archive.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/settings.js" - ], - "backend_entrypoints": [ - "routes/backup_routes.py:setup_backup_routes", - "scripts/odysseus-backup" - ], - "routes": [ - "/api/backup/export", - "/api/backup/import" - ], - "configuration": [], - "persistence": [ - "ZIP archive files" - ], - "dependencies": [ - "zipfile", - "json" - ], - "tests": [ - "tests/cli/test_preset_cli_store.py" - ], - "documentation": [ - "docs/backup-restore.md" - ], - "risks": [ - "Corrupt archive files causing partial data restore." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/backup_routes.py", - "symbol": "setup_backup_routes", - "line_range": "L30-L140", - "explanation": "Handles workspace data export and import upload unpack." - }, - { - "path": "docs/backup-restore.md", - "symbol": "Documentation", - "line_range": "L1-L50", - "explanation": "Backup and restore operational documentation." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "PLATFORM-005", - "domain": "platform", - "name": "File Cleanup & Storage Maintenance Engine", - "purpose": "Scans data directories for orphaned files, old uploads, temporary vision images, and frees disk space.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/storage.js" - ], - "backend_entrypoints": [ - "routes/cleanup/cleanup_routes.py:setup_cleanup_routes", - "src/cleanup_service.py", - "src/session_image_cleanup.py" - ], - "routes": [ - "/api/cleanup/preview", - "/api/cleanup" - ], - "configuration": [ - "CLEANUP_RETENTION_DAYS" - ], - "persistence": [ - "DATA_DIR/uploads/" - ], - "dependencies": [ - "os", - "shutil" - ], - "tests": [ - "tests/test_session_actions_cleanup.py", - "tests/test_session_image_cleanup.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Deletes files uploaded in active sessions if retention window is set too short." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/cleanup/cleanup_routes.py", - "symbol": "@router.get('/preview')", - "line_range": "L22-L37", - "explanation": "Previews reclaimable disk space across storage directories." - }, - { - "path": "src/cleanup_service.py", - "symbol": "CleanupService", - "line_range": "L25-L120", - "explanation": "Executes filesystem purge of orphaned asset files." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "PLATFORM-006", - "domain": "platform", - "name": "System Health & RAG Diagnostic Suite", - "purpose": "Executes real-time integrity diagnostics across ChromaDB, SearXNG, local models, and network interfaces.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/settings.js" - ], - "backend_entrypoints": [ - "routes/diagnostics_routes.py:setup_diagnostics_routes", - "src/service_health.py" - ], - "routes": [ - "/api/diagnostics" - ], - "configuration": [], - "persistence": [], - "dependencies": [ - "httpx", - "chromadb" - ], - "tests": [ - "tests/test_service_health_collect.py", - "tests/test_service_health_chromadb.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Diagnostic timeout if external search provider is unreachable." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/diagnostics_routes.py", - "symbol": "setup_diagnostics_routes", - "line_range": "L30-L100", - "explanation": "Runs subsystem health check suite." - }, - { - "path": "src/service_health.py", - "symbol": "collect_health_status", - "line_range": "L20-L150", - "explanation": "Inspects vector database, email, search, and local provider status." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "PLATFORM-007", - "domain": "platform", - "name": "Desktop CLI Utilities & Shell Integration Tools", - "purpose": "Provides command-line interface tools (`odysseus`, `odysseus-mcp`, `odysseus-mail`) for terminal usage.", - "status": "verified", - "frontend_entrypoints": [ - "scripts/odysseus", - "scripts/odysseus-mcp", - "scripts/odysseus-mail", - "scripts/odysseus-calendar" - ], - "backend_entrypoints": [ - "scripts/_lib/cli.py:main" - ], - "routes": [ - "CLI commands" - ], - "configuration": [], - "persistence": [ - "CLI configuration" - ], - "dependencies": [ - "urllib", - "json" - ], - "tests": [ - "tests/cli/test_sessions_cli.py", - "tests/cli/test_mail_cli_recipients.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Outdated CLI scripts if backend API schemas change." - ], - "unknowns": [], - "evidence": [ - { - "path": "scripts/_lib/cli.py", - "symbol": "main", - "line_range": "L15-L110", - "explanation": "Shared CLI framework for terminal helper commands." - }, - { - "path": "scripts/odysseus", - "symbol": "odysseus", - "line_range": "L1-L50", - "explanation": "Main terminal launcher script." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "PLATFORM-008", - "domain": "platform", - "name": "Desktop Companion App Integration", - "purpose": "Provides API routes and pairing mechanisms for the native macOS/desktop menu bar companion app.", - "status": "verified", - "frontend_entrypoints": [ - "companion/README.md" - ], - "backend_entrypoints": [ - "companion/pairing.py", - "companion/routes.py:setup_companion_routes" - ], - "routes": [ - "/companion/pair", - "/companion/status" - ], - "configuration": [ - "COMPANION_SECRET_KEY" - ], - "persistence": [ - "companion_pairing.json" - ], - "dependencies": [ - "fastapi" - ], - "tests": [ - "tests/helpers/import_state.py" - ], - "documentation": [ - "companion/README.md" - ], - "risks": [ - "Pairing code expiration timing window." - ], - "unknowns": [], - "evidence": [ - { - "path": "companion/routes.py", - "symbol": "setup_companion_routes", - "line_range": "L20-L120", - "explanation": "Endpoints for pairing and status sync with desktop companion." - }, - { - "path": "companion/pairing.py", - "symbol": "PairingManager", - "line_range": "L15-L80", - "explanation": "Generates and validates companion pairing codes." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "PLATFORM-009", - "domain": "platform", - "name": "Docker Containerization & GPU Hardware Manifests", - "purpose": "Provides multi-stage Dockerfile and Docker Compose manifests for CPU, NVIDIA CUDA, and AMD ROCm GPUs.", - "status": "verified", - "frontend_entrypoints": [ - "Dockerfile", - "docker-compose.yml", - "docker-compose.gpu-nvidia.yml", - "docker-compose.gpu-amd.yml" - ], - "backend_entrypoints": [ - "docker/entrypoint.sh" - ], - "routes": [ - "Containerized application" - ], - "configuration": [ - "DOCKER_GPU_VENDOR" - ], - "persistence": [ - "Container volumes" - ], - "dependencies": [ - "Docker", - "NVIDIA Container Toolkit" - ], - "tests": [ - "scripts/check-docker-gpu.sh" - ], - "documentation": [ - "docs/setup.md" - ], - "risks": [ - "Driver version incompatibility with host NVIDIA/AMD kernel drivers." - ], - "unknowns": [], - "evidence": [ - { - "path": "Dockerfile", - "symbol": "multi-stage-build", - "line_range": "L1-L113", - "explanation": "Multi-stage container build environment." - }, - { - "path": "docker-compose.gpu-nvidia.yml", - "symbol": "nvidia-gpu-manifest", - "line_range": "L1-L179", - "explanation": "NVIDIA GPU pass-through container specification." - }, - { - "path": "scripts/check-docker-gpu.sh", - "symbol": "check-docker-gpu", - "line_range": "L1-L615", - "explanation": "Automated diagnostic test script for host NVIDIA GPU passthrough." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": true, - "status": "pending", - "reason": "Requires Docker GPU pass-through and compatible host drivers." - } - }, - { - "id": "PLATFORM-010", - "domain": "platform", - "name": "Legacy FAISS Vector Index Migration Script", - "purpose": "Legacy utility script to migrate older FAISS vector indices into ChromaDB.", - "status": "legacy", - "frontend_entrypoints": [ - "None" - ], - "backend_entrypoints": [ - "scripts/migrate_faiss_to_chroma.py" - ], - "routes": [ - "CLI Script" - ], - "configuration": [], - "persistence": [ - "Legacy FAISS index files" - ], - "dependencies": [ - "faiss", - "chromadb" - ], - "tests": [ - "tests/helpers/import_state.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Fails if legacy FAISS index files do not exist." - ], - "unknowns": [ - "Superseded by native ChromaDB vector index pipeline." - ], - "evidence": [ - { - "path": "scripts/migrate_faiss_to_chroma.py", - "symbol": "migrate_faiss", - "line_range": "L15-L80", - "explanation": "Reads FAISS vector index files and writes to ChromaDB collection." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "NOTE-001", - "domain": "note", - "name": "Interactive Notes & Checklist Management", - "purpose": "Provides Google Keep-style notes, rich markdown text, checklist items, pinning, color tags, and reminders.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/notes.js" - ], - "backend_entrypoints": [ - "routes/note/note_routes.py:setup_note_routes", - "src/tools/notes.py" - ], - "routes": [ - "/api/notes", - "/api/notes/{note_id}", - "/api/notes/{note_id}/pin", - "/api/notes/reorder" - ], - "configuration": [], - "persistence": [ - "DATA_DIR/notes.db" - ], - "dependencies": [ - "sqlite3", - "pydantic" - ], - "tests": [ - "tests/test_notes_fail_closed_auth.py", - "tests/test_manage_notes_owner_gate.py" - ], - "documentation": [ - "docs/notes.webm" - ], - "risks": [ - "Concurrent edits on note item checkboxes." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/note/note_routes.py", - "symbol": "@router.get('')", - "line_range": "L623-L650", - "explanation": "Lists all user notes with pin and archive states." - }, - { - "path": "static/js/notes.js", - "symbol": "initNotesView", - "line_range": "L1-L160", - "explanation": "Main interactive notes grid and modal manager." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "SKILL-001", - "domain": "skill", - "name": "Dynamic Skill Management & Code Execution Engine", - "purpose": "Allows users to create, import, edit, test, and execute custom Python/Markdown skills dynamically.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/skills.js" - ], - "backend_entrypoints": [ - "routes/skills_routes.py:setup_skills_routes", - "services/memory/skills.py:SkillsManager" - ], - "routes": [ - "/api/skills", - "/api/skills/{skill_id}/invoke", - "/api/skills/import-from-url" - ], - "configuration": [ - "SKILLS_DIR" - ], - "persistence": [ - "DATA_DIR/skills/" - ], - "dependencies": [ - "pydantic", - "httpx" - ], - "tests": [ - "tests/test_skills_routes_owner_update.py", - "tests/test_skill_importer.py", - "tests/test_skill_save_no_rename.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "Arbitrary code execution risks if skill import URL is untrusted." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/skills_routes.py", - "symbol": "setup_skills_routes", - "line_range": "L100-L300", - "explanation": "Exposes CRUD and remote import routes for user skills." - }, - { - "path": "services/memory/skills.py", - "symbol": "SkillsManager", - "line_range": "L40-L220", - "explanation": "Handles skill storage, parsing, and execution." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "CONTACT-001", - "domain": "contact", - "name": "CardDAV Contact Management & Address Book Integration", - "purpose": "Connects to CardDAV servers, imports VCard contacts, and provides contact lookup for email/calendar autocomplete.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/emailLibrary.js" - ], - "backend_entrypoints": [ - "routes/contacts/contacts_routes.py:setup_contacts_routes", - "src/tools/contacts.py" - ], - "routes": [ - "/api/contacts/list", - "/api/contacts/search", - "/api/contacts/add", - "/api/contacts/config" - ], - "configuration": [ - "CARDDAV_URL" - ], - "persistence": [ - "DATA_DIR/contacts.db" - ], - "dependencies": [ - "vobject", - "sqlite3" - ], - "tests": [ - "tests/test_contacts_carddav_security.py", - "tests/cli/test_contacts_cli_rows.py" - ], - "documentation": [ - "README.md" - ], - "risks": [ - "VCard 3.0 vs 4.0 property parsing mismatches." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/contacts/contacts_routes.py", - "symbol": "@router.get('/list')", - "line_range": "L741-L764", - "explanation": "Returns contact list filtered by search query." - }, - { - "path": "src/tools/contacts.py", - "symbol": "ContactsTool", - "line_range": "L20-L110", - "explanation": "Agent tool for querying user address book contacts." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - }, - { - "id": "MEMORY-001", - "domain": "memory", - "name": "Persistent Long-Term Memory & Vector Indexing", - "purpose": "Extracts facts, user preferences, and temporal memories from chat sessions into vector/relational storage.", - "status": "verified", - "frontend_entrypoints": [ - "static/js/memory.js" - ], - "backend_entrypoints": [ - "routes/memory/memory_routes.py:setup_memory_routes", - "services/memory/service.py", - "mcp_servers/memory_server.py" - ], - "routes": [ - "/api/memory", - "/api/memory/search", - "/api/memory/extract", - "/api/memory/audit" - ], - "configuration": [ - "MEMORY_AUTO_EXTRACT" - ], - "persistence": [ - "DATA_DIR/memory.db", - "DATA_DIR/memory_chroma/" - ], - "dependencies": [ - "sqlite3", - "chromadb" - ], - "tests": [ - "tests/test_memory_routes_session_owner.py", - "tests/test_consolidate_memory_explicit_drops.py" - ], - "documentation": [ - "docs/theme.webm" - ], - "risks": [ - "Conflicting memory facts extracted from contradictory user prompts." - ], - "unknowns": [], - "evidence": [ - { - "path": "routes/memory/memory_routes.py", - "symbol": "@router.get('')", - "line_range": "L132-L150", - "explanation": "Fetches long-term user memory timeline." - }, - { - "path": "services/memory/memory_extractor.py", - "symbol": "MemoryExtractor", - "line_range": "L30-L160", - "explanation": "LLM-driven fact extraction from conversation transcripts." - } - ], - "verified_at_commit": "d8a2059df8e53bc7275c45339849d14c8651e73c", - "evidence_maturity": "E0", - "runtime_validation": { - "required": false, - "status": "not-required", - "reason": "No separate environment-dependent runtime validation was identified during this documentation pass." - } - } -] diff --git a/docs/discovery/feature-catalog.md b/docs/discovery/feature-catalog.md deleted file mode 100644 index 9a93c0317..000000000 --- a/docs/discovery/feature-catalog.md +++ /dev/null @@ -1,85 +0,0 @@ -# Feature Catalog - -This is a human-readable index derived from [`feature-catalog.json`](feature-catalog.json). The JSON file is canonical. - -| ID | Feature | Domain | Status | Evidence | Runtime | -|---|---|---|---|---|---| -| `AGENT-001` | Autonomous Agent Loop & Tool Execution Engine | `agent` | `verified` | `E0` | not required | -| `AGENT-002` | Scheduled Tasks & Event Bus Dispatcher | `agent` | `verified` | `E0` | not required | -| `AGENT-003` | Webhook Event Subscriptions & Trigger Processing | `agent` | `verified` | `E0` | not required | -| `AGENT-004` | Assistant Settings, Task Check-Ins & Background Job Monitor | `agent` | `partial` | `E1` | not required | -| `AGENT-005` | Model Context Protocol (MCP) Server Integration | `agent` | `verified` | `E0` | not required | -| `AGENT-006` | AI Interaction Tools & Pipeline Orchestration | `agent` | `verified` | `E0` | not required | -| `AGENT-007` | Subprocess & Background Job Execution Tools | `agent` | `verified` | `E0` | not required | -| `CALENDAR-001` | CalDAV Calendar Synchronization & Account Setup | `calendar` | `verified` | `E0` | pending | -| `CALENDAR-002` | Calendar Event Operations & iCalendar Parsing | `calendar` | `verified` | `E0` | not required | -| `CHAT-001` | Core Chat Streaming & SSE Message Generation | `chat` | `verified` | `E2` | pending | -| `CHAT-002` | Session Management & Conversation State | `chat` | `verified` | `E0` | not required | -| `CHAT-003` | Chat History & Message Editing/Truncation | `chat` | `verified` | `E0` | not required | -| `CHAT-004` | File & Multimodal Attachment Handling | `chat` | `verified` | `E0` | not required | -| `CHAT-005` | Chat Message Search | `chat` | `verified` | `E0` | not required | -| `CHAT-006` | System Prompts & Preset Management | `chat` | `verified` | `E0` | not required | -| `CHAT-007` | Emoji Rendering & Twemoji SVG Proxy | `chat` | `verified` | `E0` | not required | -| `CHAT-008` | Input History Recall (Arrow Up) | `chat` | `verified` | `E0` | not required | -| `CHAT-009` | Context Window Compaction & Truncation | `chat` | `verified` | `E0` | not required | -| `CONTACT-001` | CardDAV Contact Management & Address Book Integration | `contact` | `verified` | `E0` | not required | -| `COOKBOOK-001` | Local Model Download & Recipe Lifecycle Management | `cookbook` | `verified` | `E0` | not required | -| `COOKBOOK-002` | Hardware Model Fitting ('What Fits?') Analysis Engine | `cookbook` | `verified` | `E0` | not required | -| `COOKBOOK-003` | HuggingFace & MLX Model Discovery Services | `cookbook` | `verified` | `E0` | not required | -| `COOKBOOK-004` | Host Docker Access for Inference Container Runtimes | `cookbook` | `verified` | `E0` | pending | -| `DOCUMENT-001` | Document & Canvas Artifact Management | `document` | `verified` | `E0` | not required | -| `DOCUMENT-002` | PDF Form Processing & High-Fidelity Rendering | `document` | `verified` | `E1` | pending | -| `DOCUMENT-003` | Personal Document Indexing & RAG Retrieval | `document` | `verified` | `E0` | not required | -| `DOCUMENT-004` | Document Conversion & Text Extraction Engine | `document` | `verified` | `E0` | not required | -| `DOCUMENT-005` | Document Library UI Navigation | `document` | `verified` | `E0` | not required | -| `EMAIL-001` | Email Account Setup, IMAP/SMTP Connection & Polling | `email` | `verified` | `E1` | pending | -| `EMAIL-002` | Email Searching, Threading & Message Operations | `email` | `verified` | `E0` | not required | -| `EMAIL-003` | Email Composition, Draft Management & Sending | `email` | `verified` | `E0` | pending | -| `EMAIL-004` | Email MCP Server & Codex Integration Bridge | `email` | `verified` | `E0` | not required | -| `FRONTEND-001` | Single Page Application Shell & Client Router | `frontend` | `verified` | `E0` | not required | -| `FRONTEND-002` | Dynamic Theme, Color System & Custom Fonts | `frontend` | `verified` | `E0` | not required | -| `FRONTEND-003` | Window Manager, Tile Layout & Modal Control System | `frontend` | `verified` | `E0` | not required | -| `FRONTEND-004` | Global Keyboard Shortcuts & Accessibility Controls | `frontend` | `verified` | `E0` | not required | -| `FRONTEND-005` | Markdown, LaTeX & Code Block Streaming Renderer | `frontend` | `verified` | `E0` | not required | -| `FRONTEND-006` | Interactive Tour & Guided Onboarding System | `frontend` | `verified` | `E0` | not required | -| `FRONTEND-007` | Background Effects Prototyping Sandbox | `frontend` | `dead-code-candidate` | `E1` | not required | -| `MEDIA-001` | Gallery Image Library & Album Operations | `media` | `verified` | `E0` | not required | -| `MEDIA-002` | Image Processing, AI Upscaling & Style Transfer | `media` | `verified` | `E0` | not required | -| `MEDIA-003` | Interactive Image Canvas Editor & Persisted Drafts | `media` | `verified` | `E0` | not required | -| `MEDIA-004` | Text-to-Speech (TTS) Synthesis Service | `media` | `verified` | `E0` | not required | -| `MEDIA-005` | Speech-to-Text (STT) Audio Transcription Service | `media` | `verified` | `E0` | not required | -| `MEDIA-006` | Digital Signature Stamp Storage & Placement | `media` | `verified` | `E0` | not required | -| `MEDIA-007` | Generated Image Artifact Route & MCP Integration | `media` | `verified` | `E0` | not required | -| `MEDIA-008` | Native MLX Image Bridge (macOS Apple Silicon) | `media` | `experimental` | `E0` | pending | -| `MEMORY-001` | Persistent Long-Term Memory & Vector Indexing | `memory` | `verified` | `E0` | not required | -| `MODEL-001` | Multi-Provider LLM Model Discovery & Metadata Management | `model` | `verified` | `E0` | not required | -| `MODEL-002` | Model Capability & Context Limits Detection | `model` | `verified` | `E0` | not required | -| `MODEL-003` | LLM Core Provider Communication & Fallback Routing | `model` | `verified` | `E0` | pending | -| `MODEL-004` | Model Selection & Display Ordering Preferences | `model` | `verified` | `E0` | not required | -| `MODEL-005` | Side-by-Side Model Comparison (A/B Testing) | `model` | `verified` | `E0` | not required | -| `MODEL-006` | GitHub Copilot Device Flow Authentication | `model` | `verified` | `E2` | pending | -| `MODEL-007` | ChatGPT Subscription Device Flow Authentication | `model` | `verified` | `E2` | pending | -| `MODEL-008` | Embedding Model Lane & Vector Provider Setup | `model` | `verified` | `E0` | not required | -| `NOTE-001` | Interactive Notes & Checklist Management | `note` | `verified` | `E0` | not required | -| `PLATFORM-001` | Application Initialization & Lifespan Management | `platform` | `verified` | `E0` | not required | -| `PLATFORM-002` | System Health, Readiness & Version Monitoring APIs | `platform` | `verified` | `E0` | not required | -| `PLATFORM-003` | Database Schema, Migrations & SQLite Persistence | `platform` | `verified` | `E0` | not required | -| `PLATFORM-004` | User Data Export & Import Backup Infrastructure | `platform` | `verified` | `E0` | not required | -| `PLATFORM-005` | File Cleanup & Storage Maintenance Engine | `platform` | `verified` | `E0` | not required | -| `PLATFORM-006` | System Health & RAG Diagnostic Suite | `platform` | `verified` | `E0` | not required | -| `PLATFORM-007` | Desktop CLI Utilities & Shell Integration Tools | `platform` | `verified` | `E0` | not required | -| `PLATFORM-008` | Desktop Companion App Integration | `platform` | `verified` | `E0` | not required | -| `PLATFORM-009` | Docker Containerization & GPU Hardware Manifests | `platform` | `verified` | `E0` | pending | -| `PLATFORM-010` | Legacy FAISS Vector Index Migration Script | `platform` | `legacy` | `E0` | not required | -| `RESEARCH-001` | Deep Research Execution Engine & SSE Progress Streaming | `research` | `verified` | `E0` | not required | -| `RESEARCH-002` | Research Library, Detail View & Image Controls | `research` | `verified` | `E0` | not required | -| `RESEARCH-003` | Web Search Engine Integration (SearXNG & Multi-Provider) | `research` | `verified` | `E1` | pending | -| `RESEARCH-004` | Research Result Peeking & Topic Spinoff Generation | `research` | `verified` | `E0` | not required | -| `SECURITY-001` | Authentication, Session Cookies & User Management | `security` | `verified` | `E0` | not required | -| `SECURITY-002` | System Vault Encrypted Secret Storage | `security` | `verified` | `E1` | pending | -| `SECURITY-003` | API Token Management & Scope Access Control | `security` | `verified` | `E0` | not required | -| `SECURITY-004` | Prompt Security & Injection Defense Engine | `security` | `verified` | `E1` | not required | -| `SECURITY-005` | URL & Path Confinement Security Guards | `security` | `verified` | `E1` | not required | -| `SECURITY-006` | HTTP Security Headers Middleware | `security` | `verified` | `E0` | not required | -| `SECURITY-007` | Admin System Data Wipe ('Danger Zone') | `security` | `verified` | `E0` | not required | -| `SKILL-001` | Dynamic Skill Management & Code Execution Engine | `skill` | `verified` | `E0` | not required | diff --git a/docs/discovery/references/source-provenance.md b/docs/discovery/references/source-provenance.md deleted file mode 100644 index c7a9a581d..000000000 --- a/docs/discovery/references/source-provenance.md +++ /dev/null @@ -1,28 +0,0 @@ -# Source Provenance & Audit Baseline - -## Target Repository & Snapshot - -- **Repository**: `odysseus-dev/odysseus` -- **Branch**: `discovery` -- **Pinned Commit SHA**: `d8a2059df8e53bc7275c45339849d14c8651e73c` -- **Snapshot Date**: `2026-07-23T14:49:02Z` - -## Discovery Package Organization - -The public discovery documentation package under `docs/discovery/` is structured as follows: - -- `feature-catalog.json`: Canonical machine-readable JSON catalog containing 79 feature records. -- `feature-catalog.md`: Human-readable summary derived from `feature-catalog.json`. -- `BASELINE-STATUS.md`: Publication status, evidence-validation snapshot, and durable maintainer guidance. -- `audit-method.md`: Audit rules, scope, and evidence maturity definitions (E0 to E4). -- `domains/`: 16 functional domain markdown files detailing feature implementations. -- `references/`: Audit provenance and repository snapshot metadata. -- `tools/`: Structural, consistency, and evidence validators with focused evidence-validator tests. - -## Exclusion Principles - -This public documentation package explicitly excludes: -- Internal planning artifacts or private meeting notes. -- Machine-specific filesystem paths or user environments. -- API keys, credentials, or private service endpoints. -- Application code or automated test mutations. diff --git a/docs/discovery/tools/test_validate_discovery_evidence.py b/docs/discovery/tools/test_validate_discovery_evidence.py deleted file mode 100644 index 5e3144fe0..000000000 --- a/docs/discovery/tools/test_validate_discovery_evidence.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env python3 -"""Focused negative tests for validate_discovery_evidence.py.""" - -from __future__ import annotations - -import importlib.util -import shutil -import sys -import tempfile -import unittest -from pathlib import Path - - -MODULE_PATH = Path(__file__).with_name("validate_discovery_evidence.py") -SPEC = importlib.util.spec_from_file_location("validate_discovery_evidence", MODULE_PATH) -assert SPEC and SPEC.loader -validator = importlib.util.module_from_spec(SPEC) -sys.modules[SPEC.name] = validator -SPEC.loader.exec_module(validator) - - -class FakeJavascriptParser: - supported = True - reason = "test parser" - - def __init__(self, symbols: list[validator.Located] | None = None) -> None: - self.symbols = symbols or [] - - def parse(self, path: Path) -> list[validator.Located]: - return self.symbols - - -class UnsupportedJavascriptParser: - supported = False - reason = "no repository-local parser" - - -class EvidenceNegativeTests(unittest.TestCase): - def setUp(self) -> None: - self.temp = tempfile.TemporaryDirectory() - self.root = Path(self.temp.name) - (self.root / "sample.py").write_text( - "from fastapi import APIRouter\n" - "router = APIRouter(prefix='/api')\n" - "\n" - "class ChatHandler:\n" - " def preprocess_message(self):\n" - " return True\n" - "\n" - "@router.post('/chat')\n" - "def chat_stream():\n" - " return True\n", - encoding="utf-8", - ) - (self.root / "sample.js").write_text( - "export const present = () => true;\n", encoding="utf-8" - ) - (self.root / "sample.sh").write_text( - "#!/usr/bin/env bash\nreal_function() {\n return 0\n}\n", - encoding="utf-8", - ) - self.backup = self.root / "copied-backups" - self.backup.mkdir() - for path in self.root.glob("sample.*"): - shutil.copy2(path, self.backup / path.name) - - def tearDown(self) -> None: - for backup in self.backup.iterdir(): - target = self.root / backup.name - shutil.copy2(backup, target) - self.assertEqual(target.read_bytes(), backup.read_bytes()) - self.temp.cleanup() - - def validate( - self, - evidence: dict[str, str], - javascript_parser: object | None = None, - ) -> validator.Validation: - return validator.validate_evidence( - self.root, - "TEST-001", - 0, - evidence, - javascript_parser or FakeJavascriptParser(), - ) - - def test_missing_python_symbol(self) -> None: - result = self.validate( - { - "path": "sample.py", - "kind": "python-function", - "locator": "fabricated", - "line_range": "L1-L1", - "explanation": "negative fixture", - } - ) - self.assertEqual(result.result, "invalid-locator") - - def test_incorrect_qualified_method(self) -> None: - result = self.validate( - { - "path": "sample.py", - "kind": "python-method", - "locator": "WrongHandler.preprocess_message", - "line_range": "L5-L6", - "explanation": "negative fixture", - } - ) - self.assertEqual(result.result, "invalid-locator") - - def test_symbol_outside_cited_range(self) -> None: - result = self.validate( - { - "path": "sample.py", - "kind": "python-method", - "locator": "ChatHandler.preprocess_message", - "line_range": "L1-L2", - "explanation": "negative fixture", - } - ) - self.assertEqual(result.result, "locator-outside-range") - - def test_fabricated_test_function(self) -> None: - result = self.validate( - { - "path": "sample.py", - "kind": "test-function", - "locator": "test_fabricated", - "line_range": "L1-L2", - "explanation": "negative fixture", - } - ) - self.assertEqual(result.result, "invalid-locator") - - def test_nonexistent_javascript_symbol_with_parser(self) -> None: - result = self.validate( - { - "path": "sample.js", - "kind": "javascript-function", - "locator": "missing", - "line_range": "L1-L1", - "explanation": "negative fixture", - }, - FakeJavascriptParser( - [validator.Located("present", "javascript-export", 1, 1)] - ), - ) - self.assertEqual(result.result, "invalid-locator") - - def test_unsupported_javascript_parser(self) -> None: - result = self.validate( - { - "path": "sample.js", - "kind": "javascript-function", - "locator": "present", - "line_range": "L1-L1", - "explanation": "negative fixture", - }, - UnsupportedJavascriptParser(), - ) - self.assertEqual(result.result, "unsupported") - - def test_route_path_mismatch(self) -> None: - result = self.validate( - { - "path": "sample.py", - "kind": "python-route", - "locator": "POST /api/wrong -> chat_stream", - "line_range": "L9-L10", - "explanation": "negative fixture", - } - ) - self.assertEqual(result.result, "invalid-locator") - self.assertIn("path", result.problem or "") - - def test_http_method_mismatch(self) -> None: - result = self.validate( - { - "path": "sample.py", - "kind": "python-route", - "locator": "GET /api/chat -> chat_stream", - "line_range": "L9-L10", - "explanation": "negative fixture", - } - ) - self.assertEqual(result.result, "invalid-locator") - self.assertIn("method", result.problem or "") - - def test_shell_function_mismatch(self) -> None: - result = self.validate( - { - "path": "sample.sh", - "kind": "shell-function", - "locator": "fabricated", - "line_range": "L1-L4", - "explanation": "negative fixture", - } - ) - self.assertEqual(result.result, "invalid-locator") - - def test_invalid_file_level_evidence(self) -> None: - result = self.validate( - { - "path": "missing.file", - "kind": "file", - "explanation": "negative fixture", - } - ) - self.assertEqual(result.result, "invalid-path") - - def test_file_level_evidence_rejects_fake_symbol(self) -> None: - result = self.validate( - { - "path": "sample.sh", - "kind": "file", - "locator": "whole-script", - "explanation": "negative fixture", - } - ) - self.assertEqual(result.result, "invalid-locator") - - -if __name__ == "__main__": - unittest.main() diff --git a/docs/discovery/tools/validate_discovery_consistency.py b/docs/discovery/tools/validate_discovery_consistency.py deleted file mode 100755 index 953b093e1..000000000 --- a/docs/discovery/tools/validate_discovery_consistency.py +++ /dev/null @@ -1,592 +0,0 @@ -#!/usr/bin/env python3 - -from __future__ import annotations - -import json -import re -import sys -from collections import Counter, defaultdict -from pathlib import Path -from typing import Any - - -ROOT = Path(__file__).resolve().parents[1] -CATALOG_PATH = ROOT / "feature-catalog.json" -CATALOG_MD_PATH = ROOT / "feature-catalog.md" -DOMAINS_DIR = ROOT / "domains" -REVIEWS_DIR = ROOT / "reviews" - -EXPECTED_COMMIT = "d8a2059df8e53bc7275c45339849d14c8651e73c" -EXPECTED_FEATURES = 79 -EXPECTED_DOMAINS = 16 - -VALID_STATUSES = { - "verified", - "partial", - "disabled", - "experimental", - "legacy", - "dead-code-candidate", - "unverified", -} - -VALID_MATURITY = {"E0", "E1", "E2", "E3", "E4"} - -VALID_RUNTIME = { - "not-required", - "pending", - "blocked", - "completed", -} - -REQUIRED_FIELDS = { - "id", - "domain", - "name", - "purpose", - "status", - "evidence_maturity", - "verified_at_commit", - "evidence", - "runtime_validation", -} - -FEATURE_ID_RE = re.compile(r"^[A-Z][A-Z0-9]*-\d{3}$") - -DOMAIN_HEADING_RE = re.compile( - r"^##\s+`?([A-Z][A-Z0-9]*-\d{3})`?" - r"\s+(?:—|-)\s+(.+?)\s*$" -) - -DOMAIN_FIELD_RE = re.compile( - r"^-\s+\*\*" - r"(Domain|Status|Evidence Maturity|Commit Verified)" - r"\*\*:\s*(.*?)\s*$" -) - -DOMAIN_RUNTIME_RE = re.compile( - r"^-\s+\*\*Runtime Validation\*\*:\s*(.*?)\s*$" -) - -REVIEW_HEADING_RE = re.compile( - r"^###\s+(?:\d+\.\s+)?" - r"([A-Z][A-Z0-9]*-\d{3})" - r"\s+(?:—|-)\s+.+$" -) - -REVIEW_RESULT_RE = re.compile( - r"^-\s+\*\*Resulting Status(?:\s+and|/)\s+Maturity\*\*:" - r"\s*`([^`]+)`\s*/\s*`([^`]+)`\s*$" -) - - -def clean(value: str) -> str: - value = value.strip() - - if ( - len(value) >= 2 - and value.startswith("`") - and value.endswith("`") - ): - return value[1:-1].strip() - - return value - - -def load_catalog(errors: list[str]) -> list[dict[str, Any]]: - try: - data = json.loads( - CATALOG_PATH.read_text(encoding="utf-8") - ) - except Exception as exc: - errors.append(f"Unable to read catalog: {exc}") - return [] - - if isinstance(data, list): - features = data - elif isinstance(data, dict) and isinstance(data.get("features"), list): - features = data["features"] - else: - errors.append( - "Catalog must be an array or contain a features array" - ) - return [] - - if not all(isinstance(feature, dict) for feature in features): - errors.append("Every catalog feature must be an object") - return [] - - return features - - -def validate_catalog( - features: list[dict[str, Any]], - errors: list[str], -) -> None: - if len(features) != EXPECTED_FEATURES: - errors.append( - f"Expected {EXPECTED_FEATURES} features, " - f"found {len(features)}" - ) - - ids = [feature.get("id") for feature in features] - - duplicates = sorted( - feature_id - for feature_id, count in Counter(ids).items() - if feature_id and count > 1 - ) - - if duplicates: - errors.append( - "Duplicate feature IDs: " + ", ".join(duplicates) - ) - - for index, feature in enumerate(features): - feature_id = feature.get("id") - label = ( - feature_id - if isinstance(feature_id, str) - else f"<index:{index}>" - ) - - missing = sorted( - field - for field in REQUIRED_FIELDS - if feature.get(field) in (None, "", []) - ) - - if missing: - errors.append( - f"{label}: missing fields: {', '.join(missing)}" - ) - - if ( - not isinstance(feature_id, str) - or not FEATURE_ID_RE.fullmatch(feature_id) - ): - errors.append(f"{label}: invalid feature ID") - - if feature.get("status") not in VALID_STATUSES: - errors.append( - f"{label}: invalid status " - f"{feature.get('status')!r}" - ) - - if feature.get("evidence_maturity") not in VALID_MATURITY: - errors.append( - f"{label}: invalid maturity " - f"{feature.get('evidence_maturity')!r}" - ) - - if feature.get("verified_at_commit") != EXPECTED_COMMIT: - errors.append( - f"{label}: incorrect verified_at_commit" - ) - - runtime = feature.get("runtime_validation") - - if not isinstance(runtime, dict): - errors.append( - f"{label}: runtime_validation must be an object" - ) - continue - - required = runtime.get("required") - runtime_status = runtime.get("status") - reason = runtime.get("reason") - - if not isinstance(required, bool): - errors.append( - f"{label}: runtime required must be boolean" - ) - - if runtime_status not in VALID_RUNTIME: - errors.append( - f"{label}: invalid runtime status " - f"{runtime_status!r}" - ) - - if not isinstance(reason, str) or not reason.strip(): - errors.append( - f"{label}: runtime reason is blank" - ) - - if required is False and runtime_status != "not-required": - errors.append( - f"{label}: required=false requires not-required" - ) - - if required is True and runtime_status == "not-required": - errors.append( - f"{label}: required=true cannot be not-required" - ) - - -def parse_domain( - path: Path, - errors: list[str], -) -> dict[str, dict[str, str]]: - records: dict[str, dict[str, str]] = {} - current_id: str | None = None - - for line_number, line in enumerate( - path.read_text(encoding="utf-8").splitlines(), - start=1, - ): - heading = DOMAIN_HEADING_RE.match(line) - - if heading: - current_id = heading.group(1) - - if current_id in records: - errors.append( - f"{path.relative_to(ROOT)}:{line_number}: " - f"duplicate heading {current_id}" - ) - - records[current_id] = { - "Name": heading.group(2).strip(), - } - continue - - field = DOMAIN_FIELD_RE.match(line) - - if field and current_id: - value = clean(field.group(2)) - - if not value: - errors.append( - f"{path.relative_to(ROOT)}:{line_number}: " - f"blank {field.group(1)}" - ) - - records[current_id][field.group(1)] = value - continue - - runtime = DOMAIN_RUNTIME_RE.match(line) - - if runtime and current_id: - value = clean(runtime.group(1)) - runtime_status = re.split( - r"\s+(?:—|-)\s+", - value, - maxsplit=1, - )[0] - records[current_id]["Runtime Validation"] = ( - runtime_status.strip("` ") - ) - - return records - - -def validate_domains( - features: list[dict[str, Any]], - errors: list[str], -) -> None: - catalog = { - feature["id"]: feature - for feature in features - if feature.get("id") - } - - expected_by_domain: defaultdict[str, set[str]] = defaultdict(set) - - for feature in features: - expected_by_domain[feature["domain"]].add(feature["id"]) - - paths = sorted(DOMAINS_DIR.glob("*.md")) - - if len(paths) != EXPECTED_DOMAINS: - errors.append( - f"Expected {EXPECTED_DOMAINS} domain files, " - f"found {len(paths)}" - ) - - all_found: set[str] = set() - - for path in paths: - domain = path.stem - records = parse_domain(path, errors) - found = set(records) - expected = expected_by_domain.get(domain, set()) - all_found.update(found) - - if found != expected: - missing = sorted(expected - found) - extra = sorted(found - expected) - - errors.append( - f"{domain}: missing={missing}, unexpected={extra}" - ) - - for feature_id, record in records.items(): - feature = catalog.get(feature_id) - - if feature is None: - continue - - expected_values = { - "Name": feature["name"], - "Domain": feature["domain"], - "Status": feature["status"], - "Evidence Maturity": feature["evidence_maturity"], - "Commit Verified": feature["verified_at_commit"], - "Runtime Validation": ( - feature["runtime_validation"]["status"] - ), - } - - for field, expected_value in expected_values.items(): - actual = record.get(field) - - if actual != expected_value: - errors.append( - f"{path.relative_to(ROOT)}: " - f"{feature_id} {field}: " - f"{actual!r} != {expected_value!r}" - ) - - if all_found != set(catalog): - errors.append( - "Domain Markdown IDs do not match catalog JSON" - ) - - -def validate_catalog_markdown( - features: list[dict[str, Any]], - errors: list[str], -) -> None: - expected = { - feature["id"]: feature - for feature in features - } - found: dict[str, list[str]] = {} - - for line in CATALOG_MD_PATH.read_text( - encoding="utf-8" - ).splitlines(): - if not line.startswith("|"): - continue - - cells = [ - cell.strip() - for cell in line.strip().strip("|").split("|") - ] - - if len(cells) < 6: - continue - - feature_id = clean(cells[0]) - - if FEATURE_ID_RE.fullmatch(feature_id): - found[feature_id] = cells - - if set(found) != set(expected): - errors.append( - "feature-catalog.md IDs do not match JSON" - ) - - for feature_id, cells in found.items(): - feature = expected[feature_id] - runtime = feature["runtime_validation"] - runtime_display = ( - runtime["status"] - if runtime["required"] - else "not required" - ) - - actual = { - "name": cells[1].replace("\\|", "|"), - "domain": clean(cells[2]), - "status": clean(cells[3]), - "maturity": clean(cells[4]), - "runtime": clean(cells[5]), - } - - wanted = { - "name": feature["name"], - "domain": feature["domain"], - "status": feature["status"], - "maturity": feature["evidence_maturity"], - "runtime": runtime_display, - } - - for field, expected_value in wanted.items(): - if actual[field] != expected_value: - errors.append( - f"feature-catalog.md: {feature_id} " - f"{field}: {actual[field]!r} " - f"!= {expected_value!r}" - ) - - -def validate_reviews( - features: list[dict[str, Any]], - errors: list[str], -) -> int: - catalog = { - feature["id"]: feature - for feature in features - } - checked = 0 - - for path in sorted( - REVIEWS_DIR.glob("evidence-sample-*.md") - ): - current_id: str | None = None - results: set[str] = set() - - for line_number, line in enumerate( - path.read_text(encoding="utf-8").splitlines(), - start=1, - ): - heading = REVIEW_HEADING_RE.match(line) - - if heading: - current_id = heading.group(1) - continue - - result = REVIEW_RESULT_RE.match(line) - - if not result or current_id is None: - continue - - status, maturity = result.groups() - results.add(current_id) - checked += 1 - - feature = catalog.get(current_id) - - if feature is None: - errors.append( - f"{path.relative_to(ROOT)}:{line_number}: " - f"unknown feature {current_id}" - ) - continue - - if feature["status"] != status: - errors.append( - f"{current_id}: review status {status!r} " - f"!= catalog {feature['status']!r}" - ) - - if feature["evidence_maturity"] != maturity: - errors.append( - f"{current_id}: review maturity {maturity!r} " - f"!= catalog " - f"{feature['evidence_maturity']!r}" - ) - - if path.name == "evidence-sample-01.md" and len(results) != 12: - errors.append( - f"{path.relative_to(ROOT)}: expected 12 " - f"review results, found {len(results)}" - ) - - return checked - - -def validate_whitespace(errors: list[str]) -> None: - for path in sorted(ROOT.rglob("*")): - if not path.is_file(): - continue - - if path.suffix not in {".md", ".json", ".py", ".txt"}: - continue - - for line_number, line in enumerate( - path.read_text( - encoding="utf-8", - errors="replace", - ).splitlines(), - start=1, - ): - if line != line.rstrip(" \t"): - errors.append( - f"{path.relative_to(ROOT)}:{line_number}: " - "trailing whitespace" - ) - - if any(ROOT.rglob("*.pyc")): - errors.append("Generated .pyc files exist") - - if any( - path.is_dir() - for path in ROOT.rglob("__pycache__") - ): - errors.append("__pycache__ exists") - - -def main() -> int: - errors: list[str] = [] - features = load_catalog(errors) - reviewed = 0 - - if features: - validate_catalog(features, errors) - validate_domains(features, errors) - validate_catalog_markdown(features, errors) - reviewed = validate_reviews(features, errors) - - validate_whitespace(errors) - - print(f"Catalog Features: {len(features)}") - print( - "Unique Feature IDs:", - len({feature.get("id") for feature in features}), - ) - print( - "Domain Files:", - len(list(DOMAINS_DIR.glob("*.md"))), - ) - - if features: - print( - "Statuses:", - dict( - Counter( - feature.get("status") - for feature in features - ) - ), - ) - print( - "Evidence Maturity:", - dict( - Counter( - feature.get("evidence_maturity") - for feature in features - ) - ), - ) - print( - "Runtime Validation:", - dict( - Counter( - ( - feature.get( - "runtime_validation", - {}, - ).get("required"), - feature.get( - "runtime_validation", - {}, - ).get("status"), - ) - for feature in features - ) - ), - ) - - print(f"Review Results Checked: {reviewed}") - print(f"Consistency Errors: {len(errors)}") - - for error in errors: - print(f"ERROR: {error}") - - return 1 if errors else 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/docs/discovery/tools/validate_discovery_docs.py b/docs/discovery/tools/validate_discovery_docs.py deleted file mode 100755 index 656b800ec..000000000 --- a/docs/discovery/tools/validate_discovery_docs.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python3 -"""Validate the Odysseus public discovery documentation package.""" - -from __future__ import annotations - -import argparse -import json -import re -import sys -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[1] -REPO_ROOT = ROOT.parents[1] -CATALOG_PATH = ROOT / "feature-catalog.json" -DOMAINS_DIR = ROOT / "domains" - -EXPECTED_COMMIT = "d8a2059df8e53bc7275c45339849d14c8651e73c" -ALLOWED_STATUSES = { - "verified", "partial", "disabled", "experimental", "legacy", - "dead-code-candidate", "unverified" -} - -LINE_RANGE_RE = re.compile(r"^L([1-9]\d*)-L([1-9]\d*)$") -FEATURE_HEADING_RE = re.compile( - r"^##\s+`?([A-Z][A-Z0-9]*-\d{3})`?\s+(?:—|-)\s+.+$", - re.MULTILINE, -) -LINK_RE = re.compile(r"(?<!!)\[[^\]]*\]\(([^)]+)\)") -FORBIDDEN_TERMS_RE = re.compile( - r"(roadforge|kanban|matrix|owner link|editor link|github support|" - r"private maintainer|OD-AUD-|[A-Z]{2,10}-AUD-\d+|TASK-\d+)", - re.IGNORECASE, -) - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description="Validate Odysseus discovery docs.") - parser.add_argument( - "--repo-root", - type=Path, - default=REPO_ROOT, - help="Path to Odysseus repository root.", - ) - return parser.parse_args() - -def validate() -> int: - args = parse_args() - repo_root = args.repo_root.resolve() - errors: list[str] = [] - - # 1. Validate Catalog JSON existence and content - if not CATALOG_PATH.is_file(): - errors.append(f"Missing catalog file: {CATALOG_PATH}") - print(f"Errors: {len(errors)}") - for e in errors: - print(f"ERROR: {e}") - return 1 - - try: - catalog = json.loads(CATALOG_PATH.read_text(encoding="utf-8")) - except Exception as e: - errors.append(f"Failed to parse catalog JSON: {e}") - print(f"Errors: {len(errors)}") - for err in errors: - print(f"ERROR: {err}") - return 1 - - if not isinstance(catalog, list): - errors.append("feature-catalog.json must be a JSON array") - return 1 - - feature_ids = [item.get("id") for item in catalog if isinstance(item, dict)] - if len(feature_ids) != 79: - errors.append(f"Expected 79 unique feature IDs, found {len(feature_ids)}") - if len(feature_ids) != len(set(feature_ids)): - errors.append("Duplicate feature IDs found in catalog JSON") - - # Domain catalog counts - catalog_domain_counts: dict[str, int] = {} - for item in catalog: - if not isinstance(item, dict): - errors.append("Catalog item is not an object") - continue - fid = item.get("id", "<missing>") - status = item.get("status") - domain = item.get("domain", "").lower() - catalog_domain_counts[domain] = catalog_domain_counts.get(domain, 0) + 1 - - if status not in ALLOWED_STATUSES: - errors.append(f"{fid}: Invalid status '{status}'") - - evidence_list = item.get("evidence") - if not isinstance(evidence_list, list) or not evidence_list: - errors.append(f"{fid}: Missing or empty evidence list") - continue - - for ev in evidence_list: - if not isinstance(ev, dict): - errors.append(f"{fid}: Evidence item is not an object") - continue - path_str = ev.get("path") - lr_str = str(ev.get("line_range", "")) - if not path_str or Path(path_str).is_absolute() or ".." in Path(path_str).parts: - errors.append(f"{fid}: Unsafe or invalid path '{path_str}'") - continue - - # Check path exists in repo - target_path = repo_root / path_str - if not target_path.is_file(): - errors.append(f"{fid}: Referenced path '{path_str}' does not exist on disk") - continue - - # Check line range format & bounds - m = LINE_RANGE_RE.fullmatch(lr_str) - if not m: - errors.append(f"{fid}: Invalid line range format '{lr_str}' for path '{path_str}'") - continue - - start, end = int(m.group(1)), int(m.group(2)) - lines_cnt = len(target_path.read_text(encoding="utf-8", errors="ignore").splitlines()) - if start > end or end > lines_cnt or start < 1: - errors.append( - f"{fid}: Line range '{lr_str}' exceeds file length ({lines_cnt} lines) in '{path_str}'" - ) - - # 2. Check Domain Markdown files - md_feature_ids: list[str] = [] - domain_files = sorted(DOMAINS_DIR.glob("*.md")) - for df in domain_files: - domain_name = df.stem.lower() - content = df.read_text(encoding="utf-8") - found_ids = FEATURE_HEADING_RE.findall(content) - md_feature_ids.extend(found_ids) - if len(found_ids) != catalog_domain_counts.get(domain_name, 0): - errors.append( - f"Domain '{domain_name}' count mismatch: catalog has {catalog_domain_counts.get(domain_name, 0)}, Markdown has {len(found_ids)}" - ) - - if sorted(md_feature_ids) != sorted(feature_ids): - errors.append("Markdown domain feature IDs do not match catalog JSON feature IDs") - - # 3. Check for forbidden/private terms, sensitive credentials, and broken links across all docs - for md_file in ROOT.rglob("*.md"): - rel_md = md_file.relative_to(ROOT) - content = md_file.read_text(encoding="utf-8") - - # Forbidden terms scan - forbidden_matches = FORBIDDEN_TERMS_RE.findall(content) - if forbidden_matches: - errors.append( - f"{rel_md}: Found forbidden/internal terms: {set(forbidden_matches)}" - ) - - # Broken local link check - for target in LINK_RE.findall(content): - target = target.strip().strip("<>") - if not target or target.startswith(("#", "http://", "https://", "mailto:")): - continue - target_path = target.split("#", 1)[0] - resolved = (md_file.parent / target_path).resolve() - if not resolved.exists(): - errors.append(f"{rel_md}: Broken local link '{target}'") - - print(f"Catalog Features: {len(catalog)}") - print(f"Domain Files: {len(domain_files)}") - print(f"Validation Errors: {len(errors)}") - for err in errors: - print(f"ERROR: {err}") - - return 1 if errors else 0 - -if __name__ == "__main__": - sys.exit(validate()) diff --git a/docs/discovery/tools/validate_discovery_evidence.py b/docs/discovery/tools/validate_discovery_evidence.py deleted file mode 100644 index 4315478f7..000000000 --- a/docs/discovery/tools/validate_discovery_evidence.py +++ /dev/null @@ -1,1413 +0,0 @@ -#!/usr/bin/env python3 -"""Validate feature-catalog evidence locators against repository source. - -Evidence identity is resolved from a language parser where one is available. -The cited line range is treated as generated display metadata and checked only -after the locator has been resolved. -""" - -from __future__ import annotations - -import argparse -import ast -import difflib -import json -import re -import subprocess -import sys -from collections import Counter, defaultdict -from dataclasses import asdict, dataclass -from pathlib import Path -from typing import Any, Iterable - - -DISCOVERY_ROOT = Path(__file__).resolve().parents[1] -DEFAULT_REPO_ROOT = DISCOVERY_ROOT.parents[1] -DEFAULT_CATALOG = DISCOVERY_ROOT / "feature-catalog.json" - -RESULTS = { - "valid", - "invalid-path", - "invalid-locator", - "locator-outside-range", - "range-mismatch", - "ambiguous", - "unsupported", - "file-level-valid", -} - -PYTHON_KINDS = { - "python-function", - "python-method", - "python-class", - "python-variable", - "python-route", - "python-module", - "test-function", -} -JAVASCRIPT_KINDS = { - "javascript-function", - "javascript-class", - "javascript-export", - "javascript-event", -} -FILE_EXTENSIONS = { - ".html", - ".md", - ".swift", - ".yaml", - ".yml", -} -RANGE_RE = re.compile(r"^L([1-9]\d*)-L([1-9]\d*)$") -LEGACY_ROUTE_RE = re.compile( - r"^@(?P<router>[A-Za-z_]\w*)\." - r"(?P<method>get|post|put|patch|delete|options|head)" - r"\(\s*['\"](?P<path>[^'\"]*)['\"]\s*\)$", - re.IGNORECASE, -) -CANONICAL_ROUTE_RE = re.compile( - r"^(?P<method>GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s+" - r"(?P<path>\S+)(?:\s+->\s+(?P<function>[\w.]+))?$" -) -SHELL_FUNCTION_RE = re.compile( - r"^\s*(?:function\s+)?(?P<name>[A-Za-z_]\w*)\s*" - r"(?:\(\s*\))?\s*\{" -) -MARKDOWN_HEADING_RE = re.compile(r"^(?P<marks>#{1,6})\s+(?P<title>.+?)\s*$") - - -@dataclass(frozen=True) -class Located: - name: str - kind: str - start: int - end: int - details: dict[str, Any] | None = None - - @property - def line_range(self) -> str: - return f"L{self.start}-L{self.end}" - - -@dataclass -class Validation: - feature_id: str - evidence_index: int - current_evidence: dict[str, Any] - inferred_kind: str - locator: str | None - result: str - problem: str | None = None - resolved: Located | None = None - suggested_kind: str | None = None - suggested_locator: str | None = None - generated_line_range: str | None = None - confidence: str = "high" - manual_review_reason: str | None = None - - def to_dict(self) -> dict[str, Any]: - data = asdict(self) - data["resolved"] = asdict(self.resolved) if self.resolved else None - return data - - -def parse_args(argv: list[str] | None = None) -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Validate discovery evidence identities and line ranges." - ) - parser.add_argument("--repo-root", type=Path, default=DEFAULT_REPO_ROOT) - parser.add_argument("--catalog", type=Path, default=DEFAULT_CATALOG) - parser.add_argument( - "--output-dir", - type=Path, - default=DISCOVERY_ROOT, - help="Directory for the report and repair queue.", - ) - return parser.parse_args(argv) - - -def load_catalog(path: Path) -> list[dict[str, Any]]: - data = json.loads(path.read_text(encoding="utf-8")) - if not isinstance(data, list) or not all(isinstance(item, dict) for item in data): - raise ValueError("catalog must be an array of feature objects") - return data - - -def safe_target(repo_root: Path, raw_path: Any) -> Path | None: - if not isinstance(raw_path, str) or not raw_path: - return None - candidate = Path(raw_path) - if candidate.is_absolute() or ".." in candidate.parts: - return None - resolved = (repo_root / candidate).resolve() - try: - resolved.relative_to(repo_root.resolve()) - except ValueError: - return None - return resolved - - -def parse_range(raw: Any) -> tuple[int, int] | None: - if raw in (None, ""): - return None - match = RANGE_RE.fullmatch(str(raw)) - if not match: - return None - return int(match.group(1)), int(match.group(2)) - - -def extension_for(path: str) -> str: - suffix = Path(path).suffix.lower() - return suffix or "[none]" - - -def legacy_locator(evidence: dict[str, Any]) -> str | None: - value = evidence.get("locator", evidence.get("symbol")) - return value if isinstance(value, str) and value.strip() else None - - -def infer_kind(evidence: dict[str, Any], target: Path | None) -> str: - explicit = evidence.get("kind") - if isinstance(explicit, str) and explicit: - return explicit - - path = str(evidence.get("path", "")) - suffix = Path(path).suffix.lower() - locator = legacy_locator(evidence) or "" - - if suffix == ".py": - if LEGACY_ROUTE_RE.fullmatch(locator) or CANONICAL_ROUTE_RE.fullmatch(locator): - return "python-route" - if path.startswith("tests/") and locator.startswith("test"): - return "test-function" - if "." in locator and all(part.isidentifier() for part in locator.split(".")): - return "python-method" - if locator.isupper(): - return "python-variable" - if target and target.is_file(): - try: - tree = ast.parse(target.read_text(encoding="utf-8")) - except (OSError, SyntaxError, UnicodeError): - return "manual-review" - for node in ast.walk(tree): - if isinstance(node, ast.ClassDef) and node.name == locator: - return "python-class" - return "python-function" - if suffix == ".js": - if locator.startswith("on:") or locator.startswith("event:"): - return "javascript-event" - if locator and locator[:1].isupper(): - return "javascript-class" - return "javascript-function" - if suffix in {".sh", ".bash"}: - return "shell-function" - if suffix == ".md": - return "documentation-section" - if suffix in {".json", ".yaml", ".yml", ".toml", ".ini"}: - return "configuration-key" - if suffix in FILE_EXTENSIONS or Path(path).name in {"Dockerfile"}: - return "manual-review" - if not suffix: - return "manual-review" - return "manual-review" - - -class PythonIndex(ast.NodeVisitor): - """Collect Python declarations, assignments, and decorated routes.""" - - def __init__(self) -> None: - self.symbols: list[Located] = [] - self.routes: list[Located] = [] - self.scope: list[str] = [] - self.constants: dict[str, str] = {} - self.router_prefixes: dict[str, str | None] = {} - - @classmethod - def from_path(cls, path: Path) -> "PythonIndex": - index = cls() - tree = ast.parse(path.read_text(encoding="utf-8")) - index._collect_static_values(tree) - index.visit(tree) - return index - - def _collect_static_values(self, tree: ast.AST) -> None: - for node in getattr(tree, "body", []): - if isinstance(node, (ast.Assign, ast.AnnAssign)): - value = self._string_value(node.value) - targets = node.targets if isinstance(node, ast.Assign) else [node.target] - for target in targets: - if isinstance(target, ast.Name) and value is not None: - self.constants[target.id] = value - if isinstance(target, ast.Name) and isinstance( - node.value, ast.Call - ): - call_name = self._call_name(node.value.func) - if call_name.endswith("APIRouter"): - prefix = self._router_prefix(node.value) - self.router_prefixes[target.id] = prefix - - def _qualified(self, name: str) -> str: - return ".".join([*self.scope, name]) - - @staticmethod - def _call_name(node: ast.AST) -> str: - if isinstance(node, ast.Name): - return node.id - if isinstance(node, ast.Attribute): - return f"{PythonIndex._call_name(node.value)}.{node.attr}".strip(".") - return "" - - def _string_value(self, node: ast.AST | None) -> str | None: - if isinstance(node, ast.Constant) and isinstance(node.value, str): - return node.value - if isinstance(node, ast.Name): - return self.constants.get(node.id) - if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add): - left = self._string_value(node.left) - right = self._string_value(node.right) - return left + right if left is not None and right is not None else None - return None - - def _keyword_string(self, call: ast.Call, name: str) -> str | None: - for keyword in call.keywords: - if keyword.arg == name: - return self._string_value(keyword.value) - return None - - def _router_prefix(self, call: ast.Call) -> str | None: - if not any(keyword.arg == "prefix" for keyword in call.keywords): - return "" - return self._keyword_string(call, "prefix") - - def visit_ClassDef(self, node: ast.ClassDef) -> None: - self.symbols.append( - Located( - self._qualified(node.name), - "python-class", - node.lineno, - node.end_lineno or node.lineno, - ) - ) - self.scope.append(node.name) - self.generic_visit(node) - self.scope.pop() - - def visit_FunctionDef(self, node: ast.FunctionDef) -> None: - self._visit_function(node) - - def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: - self._visit_function(node) - - def _visit_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None: - qualified = self._qualified(node.name) - parent_is_class = bool( - self.scope - and any( - item.kind == "python-class" and item.name == ".".join(self.scope) - for item in self.symbols - ) - ) - kind = ( - "test-function" - if node.name.startswith("test") - else "python-method" - if parent_is_class - else "python-function" - ) - located = Located( - qualified, kind, node.lineno, node.end_lineno or node.lineno - ) - self.symbols.append(located) - self._collect_routes(node, qualified) - self.scope.append(node.name) - self.generic_visit(node) - self.scope.pop() - - def _collect_routes( - self, node: ast.FunctionDef | ast.AsyncFunctionDef, qualified: str - ) -> None: - methods = {"get", "post", "put", "patch", "delete", "options", "head"} - for decorator in node.decorator_list: - if not isinstance(decorator, ast.Call) or not isinstance( - decorator.func, ast.Attribute - ): - continue - method = decorator.func.attr.lower() - if method not in methods: - continue - router = self._call_name(decorator.func.value) - local_path = ( - self._string_value(decorator.args[0]) if decorator.args else None - ) - if local_path is None: - continue - prefix = self.router_prefixes.get(router) - full_path = None if router in self.router_prefixes and prefix is None else ( - f"{prefix or ''}{local_path}" - ) - details = { - "method": method.upper(), - "path": local_path, - "full_path": full_path, - "function": qualified, - "router": router, - "router_prefix": prefix, - "prefix_resolved": full_path is not None, - } - route_name = f"{method.upper()} {full_path or local_path} -> {qualified}" - self.routes.append( - Located( - route_name, - "python-route", - node.lineno, - node.end_lineno or node.lineno, - details, - ) - ) - - def visit_Assign(self, node: ast.Assign) -> None: - if isinstance(node.value, ast.Call): - call_name = self._call_name(node.value.func) - if call_name.endswith("APIRouter"): - for target in node.targets: - if isinstance(target, ast.Name): - self.router_prefixes[target.id] = self._router_prefix(node.value) - for target in node.targets: - self._collect_assignment(target, node) - self.generic_visit(node) - - def visit_AnnAssign(self, node: ast.AnnAssign) -> None: - self._collect_assignment(node.target, node) - self.generic_visit(node) - - def _collect_assignment(self, target: ast.AST, node: ast.AST) -> None: - if isinstance(target, (ast.Tuple, ast.List)): - for item in target.elts: - self._collect_assignment(item, node) - return - if not isinstance(target, ast.Name): - return - self.symbols.append( - Located( - self._qualified(target.id), - "python-variable", - node.lineno, - node.end_lineno or node.lineno, - ) - ) - - -NODE_PARSER = r""" -const fs = require("fs"); -const parserName = process.argv[1]; -const parserEntry = process.argv[2]; -const filename = process.argv[3]; -const source = fs.readFileSync(filename, "utf8"); -const parser = require(parserEntry); -let tree; -if (parserName === "@babel/parser") { - tree = parser.parse(source, { - sourceType: "unambiguous", - plugins: ["jsx", "classProperties", "optionalChaining", "topLevelAwait"] - }); -} else if (parserName === "typescript") { - throw new Error("typescript parser normalization is not implemented"); -} else { - const parse = parser.parse || parser.Parser?.parse; - tree = parse.call(parser, source, { - ecmaVersion: "latest", sourceType: "module", locations: true - }); -} -const out = []; -function loc(node) { - const l = node.loc; - return {start: l.start.line, end: l.end.line}; -} -function add(name, kind, node, extra={}) { - if (!name || !node.loc) return; - out.push({name, kind, ...loc(node), details: extra}); -} -function walk(node, scope=[], exported=false) { - if (!node || typeof node !== "object") return; - if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") { - if (node.declaration) walk(node.declaration, scope, true); - return; - } - if (node.type === "FunctionDeclaration") { - const name = node.id?.name; - add([...scope, name].filter(Boolean).join("."), exported ? "javascript-export" : "javascript-function", node); - scope = [...scope, name].filter(Boolean); - } else if (node.type === "ClassDeclaration") { - const name = node.id?.name; - add([...scope, name].filter(Boolean).join("."), exported ? "javascript-export" : "javascript-class", node); - scope = [...scope, name].filter(Boolean); - } else if (node.type === "MethodDefinition" || node.type === "ClassMethod") { - const name = node.key?.name || node.key?.value; - add([...scope, name].filter(Boolean).join("."), "javascript-function", node); - scope = [...scope, name].filter(Boolean); - } else if (node.type === "VariableDeclarator" && node.id?.type === "Identifier") { - const initType = node.init?.type || ""; - const isFunction = initType === "ArrowFunctionExpression" || initType === "FunctionExpression"; - if (isFunction || exported) { - add([...scope, node.id.name].join("."), exported ? "javascript-export" : "javascript-function", node); - } - } else if (node.type === "CallExpression" && node.callee?.type === "MemberExpression" - && node.callee.property?.name === "addEventListener" - && ["Literal", "StringLiteral"].includes(node.arguments?.[0]?.type)) { - add("event:" + node.arguments[0].value, "javascript-event", node, {event: node.arguments[0].value}); - } - for (const [key, value] of Object.entries(node)) { - if (key === "loc" || key === "start" || key === "end") continue; - if (Array.isArray(value)) value.forEach(child => walk(child, scope, exported)); - else if (value && typeof value === "object") walk(value, scope, exported); - } -} -walk(tree.program || tree); -process.stdout.write(JSON.stringify(out)); -""" - - -class JavascriptParser: - CANDIDATES = ("acorn", "espree", "@babel/parser", "typescript") - - def __init__(self, repo_root: Path) -> None: - self.repo_root = repo_root.resolve() - self.node = self._node_binary() - self.parser_name, self.parser_entry, self.reason = self._find_parser() - - @staticmethod - def _node_binary() -> str | None: - from shutil import which - - return which("node") - - def _package_roots(self) -> list[tuple[Path, set[str]]]: - roots: list[tuple[Path, set[str]]] = [] - for package in self.repo_root.rglob("package.json"): - if "node_modules" in package.parts: - continue - try: - data = json.loads(package.read_text(encoding="utf-8")) - except (OSError, ValueError): - continue - declared: set[str] = set() - for key in ("dependencies", "devDependencies", "peerDependencies"): - values = data.get(key) - if isinstance(values, dict): - declared.update(values) - roots.append((package.parent, declared)) - return roots - - def _find_parser(self) -> tuple[str | None, str | None, str]: - if not self.node: - return None, None, "Node.js is unavailable" - package_roots = self._package_roots() - for name in self.CANDIDATES: - for package_root, declared in package_roots: - local = package_root / "node_modules" / name - if name not in declared and not local.exists(): - continue - probe = subprocess.run( - [ - self.node, - "-e", - ( - "const p=require.resolve(process.argv[1]," - "{paths:[process.argv[2]]});process.stdout.write(p)" - ), - name, - str(package_root), - ], - cwd=package_root, - text=True, - capture_output=True, - check=False, - ) - if probe.returncode: - continue - resolved = Path(probe.stdout).resolve() - try: - resolved.relative_to(self.repo_root) - except ValueError: - continue - if name == "typescript": - return None, None, ( - "TypeScript is repository-local but its AST " - "normalization is not implemented" - ) - return name, str(resolved), f"repository-local parser: {name}" - return None, None, ( - "no repository-local Acorn, Espree, Babel parser, or supported " - "TypeScript parser is available" - ) - - @property - def supported(self) -> bool: - return bool(self.node and self.parser_name) - - def parse(self, path: Path) -> list[Located]: - if not self.supported: - raise RuntimeError(self.reason) - process = subprocess.run( - [ - self.node or "node", - "-e", - NODE_PARSER, - self.parser_name or "", - self.parser_entry or "", - str(path), - ], - cwd=self.repo_root, - text=True, - capture_output=True, - check=False, - ) - if process.returncode: - raise RuntimeError(process.stderr.strip() or "JavaScript parser failed") - return [Located(**item) for item in json.loads(process.stdout)] - - -def choose_symbol( - symbols: Iterable[Located], locator: str -) -> tuple[Located | None, list[Located]]: - candidates = list(symbols) - exact = [item for item in candidates if item.name == locator] - if not exact and "." not in locator: - exact = [item for item in candidates if item.name.split(".")[-1] == locator] - return (exact[0] if len(exact) == 1 else None), exact - - -def apply_range_result( - validation: Validation, located: Located, cited: tuple[int, int] | None -) -> Validation: - validation.resolved = located - validation.generated_line_range = located.line_range - validation.suggested_locator = located.name - validation.suggested_kind = located.kind - if cited is None: - if validation.current_evidence.get("line_range") not in (None, ""): - validation.result = "range-mismatch" - validation.problem = "line_range is malformed" - else: - validation.result = "valid" - return validation - start, end = cited - if start <= located.start <= end: - # Evidence ranges are excerpts. They must contain the declaration that - # gives the locator its identity, but need not contain the whole body. - validation.result = "valid" - else: - validation.result = "locator-outside-range" - validation.problem = ( - f"locator declaration at L{located.start} is outside cited " - f"L{start}-L{end}; parser-derived span is {located.line_range}" - ) - return validation - - -def suggest_nearby( - symbols: list[Located], locator: str, cited: tuple[int, int] | None -) -> tuple[Located | None, str]: - if cited: - overlapping = [ - item - for item in symbols - if cited[0] <= item.start <= cited[1] - or item.start <= cited[0] <= item.end - ] - if len(overlapping) == 1: - return overlapping[0], "high" - leaf_matches = [ - item - for item in overlapping - if item.name.split(".")[-1] == locator.split(".")[-1] - ] - if len(leaf_matches) == 1: - return leaf_matches[0], "high" - names = [item.name for item in symbols] - close = difflib.get_close_matches(locator, names, n=1, cutoff=0.72) - if close: - return next(item for item in symbols if item.name == close[0]), "medium" - return None, "low" - - -def validate_python( - validation: Validation, target: Path, cited: tuple[int, int] | None -) -> Validation: - try: - index = PythonIndex.from_path(target) - except (OSError, UnicodeError, SyntaxError) as exc: - validation.result = "unsupported" - validation.problem = f"Python AST parsing failed: {exc}" - validation.manual_review_reason = validation.problem - return validation - - locator = validation.locator or "" - if validation.inferred_kind == "python-module": - if locator: - validation.result = "invalid-locator" - validation.problem = "python-module evidence must not name a symbol" - else: - validation.result = "file-level-valid" - validation.suggested_kind = "python-module" - return validation - - if validation.inferred_kind == "python-route": - return validate_route(validation, index.routes, cited) - - located, exact = choose_symbol(index.symbols, locator) - if len(exact) > 1: - validation.result = "ambiguous" - validation.problem = f"locator matches {len(exact)} declarations" - validation.manual_review_reason = validation.problem - validation.confidence = "low" - return validation - if not located: - validation.result = "invalid-locator" - validation.problem = f"Python locator {locator!r} does not exist" - suggestion, confidence = suggest_nearby(index.symbols, locator, cited) - validation.confidence = confidence - if suggestion: - validation.suggested_kind = suggestion.kind - validation.suggested_locator = suggestion.name - validation.generated_line_range = suggestion.line_range - return validation - return apply_range_result(validation, located, cited) - - -def validate_route( - validation: Validation, routes: list[Located], cited: tuple[int, int] | None -) -> Validation: - locator = validation.locator or "" - legacy = LEGACY_ROUTE_RE.fullmatch(locator) - canonical = CANONICAL_ROUTE_RE.fullmatch(locator) - if not legacy and not canonical: - validation.result = "invalid-locator" - validation.problem = ( - "route locator must be '@router.method(\"/path\")' or " - "'METHOD /path -> qualified.function'" - ) - return validation - expected_method = (legacy or canonical).group("method").upper() - expected_path = (legacy or canonical).group("path") - expected_function = canonical.group("function") if canonical else None - method_matches = [ - route - for route in routes - if route.details and route.details["method"] == expected_method - ] - path_matches = [ - route - for route in method_matches - if route.details - and expected_path in {route.details["path"], route.details["full_path"]} - ] - if not method_matches: - validation.result = "invalid-locator" - validation.problem = f"HTTP method {expected_method} is not declared in this file" - return validation - if not path_matches: - validation.result = "invalid-locator" - validation.problem = ( - f"route path {expected_path!r} is not declared for {expected_method}" - ) - return validation - if expected_function: - function_matches = [ - route - for route in path_matches - if route.details and route.details["function"] == expected_function - ] - if not function_matches: - validation.result = "invalid-locator" - validation.problem = ( - f"route exists but containing function is not {expected_function!r}" - ) - return validation - path_matches = function_matches - if len(path_matches) > 1: - validation.result = "ambiguous" - validation.problem = f"route locator matches {len(path_matches)} functions" - validation.manual_review_reason = validation.problem - validation.confidence = "low" - return validation - route = path_matches[0] - details = route.details or {} - if details.get("full_path") is None and expected_path != details.get("path"): - validation.result = "ambiguous" - validation.problem = "final router prefix cannot be resolved statically" - validation.manual_review_reason = validation.problem - validation.resolved = route - validation.generated_line_range = route.line_range - validation.suggested_locator = route.name - validation.suggested_kind = "python-route" - validation.confidence = "medium" - return validation - return apply_range_result(validation, route, cited) - - -def validate_javascript( - validation: Validation, - target: Path, - cited: tuple[int, int] | None, - parser: JavascriptParser, -) -> Validation: - if not parser.supported: - validation.result = "unsupported" - validation.problem = parser.reason - validation.manual_review_reason = parser.reason - validation.confidence = "low" - return validation - try: - symbols = parser.parse(target) - except (OSError, RuntimeError, ValueError, json.JSONDecodeError) as exc: - validation.result = "unsupported" - validation.problem = f"JavaScript parser failed: {exc}" - validation.manual_review_reason = validation.problem - validation.confidence = "low" - return validation - locator = validation.locator or "" - located, exact = choose_symbol(symbols, locator) - if len(exact) > 1: - validation.result = "ambiguous" - validation.problem = f"locator matches {len(exact)} JavaScript declarations" - validation.manual_review_reason = validation.problem - validation.confidence = "low" - return validation - if not located: - validation.result = "invalid-locator" - validation.problem = f"JavaScript locator {locator!r} does not exist" - suggestion, confidence = suggest_nearby(symbols, locator, cited) - validation.confidence = confidence - if suggestion: - validation.suggested_kind = suggestion.kind - validation.suggested_locator = suggestion.name - validation.generated_line_range = suggestion.line_range - return validation - return apply_range_result(validation, located, cited) - - -def shell_functions(path: Path) -> list[Located]: - lines = path.read_text(encoding="utf-8").splitlines() - functions: list[Located] = [] - for index, line in enumerate(lines): - match = SHELL_FUNCTION_RE.match(line) - if not match: - continue - depth = 0 - end = index + 1 - for offset in range(index, len(lines)): - code = lines[offset].split("#", 1)[0] - depth += code.count("{") - code.count("}") - end = offset + 1 - if depth <= 0: - break - functions.append( - Located(match.group("name"), "shell-function", index + 1, end) - ) - return functions - - -def validate_shell( - validation: Validation, target: Path, cited: tuple[int, int] | None -) -> Validation: - try: - check = subprocess.run( - ["bash", "-n", str(target)], text=True, capture_output=True, check=False - ) - except OSError as exc: - validation.result = "unsupported" - validation.problem = f"bash unavailable: {exc}" - return validation - if check.returncode: - validation.result = "unsupported" - validation.problem = f"shell syntax check failed: {check.stderr.strip()}" - return validation - functions = shell_functions(target) - located, exact = choose_symbol(functions, validation.locator or "") - if len(exact) > 1: - validation.result = "ambiguous" - validation.problem = "shell function locator is duplicated" - validation.manual_review_reason = validation.problem - return validation - if not located: - validation.result = "invalid-locator" - validation.problem = f"shell function {validation.locator!r} does not exist" - return validation - return apply_range_result(validation, located, cited) - - -def validate_documentation( - validation: Validation, target: Path, cited: tuple[int, int] | None -) -> Validation: - locator = validation.locator or "" - if not locator: - validation.result = "invalid-locator" - validation.problem = "documentation-section requires a heading locator" - return validation - lines = target.read_text(encoding="utf-8").splitlines() - headings: list[Located] = [] - for number, line in enumerate(lines, 1): - match = MARKDOWN_HEADING_RE.match(line) - if not match: - continue - level = len(match.group("marks")) - end = len(lines) - for later, later_line in enumerate(lines[number:], number + 1): - later_match = MARKDOWN_HEADING_RE.match(later_line) - if later_match and len(later_match.group("marks")) <= level: - end = later - 1 - break - headings.append( - Located( - match.group("title").strip(), - "documentation-section", - number, - end, - ) - ) - located, exact = choose_symbol(headings, locator) - if len(exact) > 1: - validation.result = "ambiguous" - validation.problem = "documentation heading is duplicated" - validation.manual_review_reason = validation.problem - return validation - if not located: - validation.result = "invalid-locator" - validation.problem = f"documentation heading {locator!r} does not exist" - return validation - return apply_range_result(validation, located, cited) - - -def validate_evidence( - repo_root: Path, - feature_id: str, - index: int, - evidence: dict[str, Any], - javascript_parser: JavascriptParser, -) -> Validation: - target = safe_target(repo_root, evidence.get("path")) - kind = infer_kind(evidence, target) - locator = legacy_locator(evidence) - validation = Validation( - feature_id=feature_id, - evidence_index=index, - current_evidence=evidence, - inferred_kind=kind, - locator=locator, - result="ambiguous", - ) - if target is None or not target.is_file(): - validation.result = "invalid-path" - validation.problem = f"evidence path is unsafe or missing: {evidence.get('path')!r}" - validation.confidence = "high" - return validation - - cited = parse_range(evidence.get("line_range")) - explicit_range = evidence.get("line_range") - if explicit_range not in (None, "") and cited is None: - validation.result = "range-mismatch" - validation.problem = f"malformed line_range: {explicit_range!r}" - return validation - if cited is not None: - line_count = len( - target.read_text(encoding="utf-8", errors="replace").splitlines() - ) - if cited[0] > cited[1] or cited[1] > max(line_count, 1): - validation.result = "range-mismatch" - validation.problem = ( - f"line_range L{cited[0]}-L{cited[1]} is outside the " - f"{line_count}-line file" - ) - return validation - - if kind == "file": - if locator: - validation.result = "invalid-locator" - validation.problem = "file evidence must not include a locator/symbol" - validation.suggested_kind = "file" - validation.confidence = "high" - return validation - validation.result = "file-level-valid" - validation.suggested_kind = "file" - line_count = len(target.read_text(encoding="utf-8", errors="replace").splitlines()) - validation.generated_line_range = f"L1-L{max(line_count, 1)}" - return validation - - if not locator and kind not in {"python-module", "file"}: - validation.result = "invalid-locator" - validation.problem = f"{kind} evidence requires a locator" - return validation - - if kind in PYTHON_KINDS: - return validate_python(validation, target, cited) - if kind in JAVASCRIPT_KINDS: - return validate_javascript(validation, target, cited, javascript_parser) - if kind == "shell-function": - return validate_shell(validation, target, cited) - if kind == "documentation-section": - return validate_documentation(validation, target, cited) - if kind == "configuration-key": - validation.result = "ambiguous" - validation.problem = "configuration-key requires a format-aware parser" - validation.manual_review_reason = ( - "This validator does not guess configuration keys from text." - ) - validation.suggested_kind = ( - "file" if evidence.get("kind") is None else "manual-review" - ) - validation.confidence = "low" - return validation - - validation.result = "ambiguous" - validation.problem = f"no automatic resolver for inferred kind {kind!r}" - validation.manual_review_reason = ( - "Use explicit file evidence if the whole file is authoritative, " - "or manual-review with a precise reason." - ) - if evidence.get("kind") is None: - validation.suggested_kind = "file" - validation.suggested_locator = None - line_count = len(target.read_text(encoding="utf-8", errors="replace").splitlines()) - validation.generated_line_range = f"L1-L{max(line_count, 1)}" - validation.confidence = "medium" - return validation - - -def proposed_schema() -> dict[str, Any]: - return { - "required": ["path", "kind", "explanation"], - "optional": ["locator", "line_range"], - "properties": { - "path": "repository-relative file path", - "kind": sorted( - PYTHON_KINDS - | JAVASCRIPT_KINDS - | { - "shell-function", - "configuration-key", - "file", - "documentation-section", - "manual-review", - } - ), - "locator": ( - "parser-resolvable identity; omit for file evidence. " - "Routes use 'METHOD /path -> qualified.function'." - ), - "line_range": ( - "optional generated display metadata in Lx-Ly form; " - "never the primary identity" - ), - "explanation": "why this evidence supports the feature claim", - }, - "examples": [ - { - "path": "src/chat_handler.py", - "kind": "python-method", - "locator": "ChatHandler.preprocess_message", - "explanation": "Preprocesses attachments and URLs for chat requests.", - }, - { - "path": "Dockerfile", - "kind": "file", - "explanation": "Defines the container build.", - }, - ], - } - - -def assess_e2( - features: list[dict[str, Any]], validations: list[Validation] -) -> list[dict[str, Any]]: - """Record the audit's claim-relevance review for the ten frozen E2 records.""" - by_key = { - (item.feature_id, item.current_evidence.get("path"), item.locator): item - for item in validations - } - decisions = { - "CHAT-001": ( - "yes", - "Both cited tests exercise documented chat-stream behavior: emitted " - "stream metrics and non-destructive resend behavior.", - "retain", - ), - "MODEL-006": ( - "yes", - "The Node-backed test exercises the Copilot device-flow start/poll " - "contract and complete verification URI.", - "retain", - ), - "MODEL-007": ( - "yes", - "The Node-backed test exercises the ChatGPT subscription device-flow " - "contract and verification URI.", - "retain", - ), - "RESEARCH-003": ( - "partial", - "The test covers result ranking only, not SearXNG connectivity or " - "multi-provider dispatch in the feature claim.", - "demote", - ), - "DOCUMENT-002": ( - "no", - "The test covers removal of a PDF content marker, not PDF form " - "processing or high-fidelity rendering.", - "demote", - ), - "EMAIL-001": ( - "partial", - "The test covers health probing of account connections, not account " - "setup, SMTP behavior, or inbox polling.", - "demote", - ), - "SECURITY-002": ( - "yes", - "Relevant vault password-handling tests exist, but the cited test " - "function name is fabricated.", - "demote", - ), - "SECURITY-004": ( - "yes", - "Relevant prompt-injection tests exist, but both cited locator names " - "are fabricated umbrella names.", - "demote", - ), - "SECURITY-005": ( - "yes", - "Relevant URL and path confinement tests exist, but all three cited " - "locator names are fabricated umbrella names.", - "demote", - ), - "PLATFORM-009": ( - "manual-review", - "The shell diagnostic is executable evidence, but no automated test " - "function or test-suite evidence is cited and the script uses a " - "file-like fabricated symbol.", - "demote", - ), - } - assessments: list[dict[str, Any]] = [] - for feature in features: - if feature.get("evidence_maturity") != "E2": - continue - test_evidence = [ - evidence - for evidence in feature.get("evidence", []) - if str(evidence.get("path", "")).startswith("tests/") - ] - cited: list[dict[str, Any]] = [] - for evidence in test_evidence: - item = by_key.get( - (feature["id"], evidence.get("path"), legacy_locator(evidence)) - ) - cited.append( - { - "path": evidence.get("path"), - "locator": legacy_locator(evidence), - "exists": bool( - item - and item.result - not in {"invalid-path", "invalid-locator", "unsupported"} - ), - "validation_result": item.result if item else "not-validated", - } - ) - relevance, reason, decision = decisions[feature["id"]] - assessments.append( - { - "feature_id": feature["id"], - "feature_name": feature["name"], - "cited_test_evidence": cited, - "test_function_exists": ( - all(item["exists"] for item in cited) if cited else False - ), - "direct_support": relevance, - "support_reason": reason, - "suggested_decision": decision, - } - ) - return assessments - - -def build_report( - repo_root: Path, - catalog_path: Path, - features: list[dict[str, Any]], - validations: list[Validation], - javascript_parser: JavascriptParser, -) -> dict[str, Any]: - extensions = Counter() - kinds = Counter() - for item in validations: - extensions[extension_for(str(item.current_evidence.get("path", "")))] += 1 - kinds[item.inferred_kind] += 1 - results = Counter(item.result for item in validations) - affected = sorted( - { - item.feature_id - for item in validations - if item.result not in {"valid", "file-level-valid"} - } - ) - invalid_results = { - "invalid-path", - "invalid-locator", - "locator-outside-range", - "range-mismatch", - } - invalid_features = sorted( - { - item.feature_id - for item in validations - if item.result in invalid_results - } - ) - ambiguous = [ - item.to_dict() - for item in validations - if item.result in {"ambiguous", "unsupported"} - ] - return { - "audit": { - "repo_root": str(repo_root), - "catalog": str(catalog_path), - "feature_count": len(features), - "evidence_count": len(validations), - "javascript_parser": { - "supported": javascript_parser.supported, - "parser": javascript_parser.parser_name, - "reason": javascript_parser.reason, - }, - }, - "inventory": { - "total_evidence_items": len(validations), - "by_extension": dict(sorted(extensions.items())), - "by_inferred_evidence_type": dict(sorted(kinds.items())), - "features_with_invalid_or_unresolved_entries": affected, - "features_with_invalid_entries": invalid_features, - "invalid_feature_count": len(invalid_features), - "affected_feature_count": len(affected), - "ambiguous_or_unsupported_entries": ambiguous, - }, - "result_counts": {name: results.get(name, 0) for name in sorted(RESULTS)}, - "summary_counts": { - "valid": results["valid"] + results["file-level-valid"], - "invalid": sum( - results[name] - for name in ( - "invalid-path", - "invalid-locator", - "locator-outside-range", - "range-mismatch", - ) - ), - "ambiguous": results["ambiguous"], - "unsupported": results["unsupported"], - }, - "evidence_model": proposed_schema(), - "e2_assessment": assess_e2(features, validations), - "entries": [item.to_dict() for item in validations], - } - - -def build_repair_queue(validations: list[Validation]) -> dict[str, Any]: - grouped: dict[str, list[dict[str, Any]]] = defaultdict(list) - for item in validations: - if item.result in {"valid", "file-level-valid"}: - continue - grouped[item.feature_id].append( - { - "evidence_index": item.evidence_index, - "current_evidence": item.current_evidence, - "detected_problem": item.problem or item.result, - "result": item.result, - "suggested_kind": item.suggested_kind, - "suggested_locator": item.suggested_locator, - "generated_line_range": item.generated_line_range, - "confidence": item.confidence, - "manual_review_reason": item.manual_review_reason, - } - ) - return { - "feature_count": len(grouped), - "defect_count": sum(len(items) for items in grouped.values()), - "features": [ - {"feature_id": feature_id, "defects": grouped[feature_id]} - for feature_id in sorted(grouped) - ], - } - - -def render_markdown(report: dict[str, Any]) -> str: - inventory = report["inventory"] - summary = report["summary_counts"] - lines = [ - "# Discovery evidence validation report", - "", - "This report is machine-generated. The catalog was not edited.", - "", - "## Summary", - "", - f"- Features: {report['audit']['feature_count']}", - f"- Evidence items: {report['audit']['evidence_count']}", - f"- Valid (including file-level): {summary['valid']}", - f"- Invalid: {summary['invalid']}", - f"- Ambiguous: {summary['ambiguous']}", - f"- Unsupported: {summary['unsupported']}", - f"- Affected features: {inventory['affected_feature_count']}", - "", - "## Evidence by extension", - "", - "| Extension | Items |", - "|---|---:|", - ] - lines.extend( - f"| `{extension}` | {count} |" - for extension, count in inventory["by_extension"].items() - ) - lines.extend( - [ - "", - "## Evidence by inferred kind", - "", - "| Kind | Items |", - "|---|---:|", - ] - ) - lines.extend( - f"| `{kind}` | {count} |" - for kind, count in inventory["by_inferred_evidence_type"].items() - ) - lines.extend( - [ - "", - "## Result categories", - "", - "| Result | Items |", - "|---|---:|", - ] - ) - lines.extend( - f"| `{result}` | {count} |" - for result, count in report["result_counts"].items() - ) - lines.extend( - [ - "", - "## JavaScript parser", - "", - report["audit"]["javascript_parser"]["reason"] + ".", - "", - "## Affected features", - "", - ", ".join( - f"`{feature_id}`" - for feature_id in inventory[ - "features_with_invalid_or_unresolved_entries" - ] - ) - or "None.", - "", - "## E2 relevance review", - "", - "| Feature | Cited test evidence | Exists | Direct support | Decision |", - "|---|---|---:|---|---|", - ] - ) - for item in report["e2_assessment"]: - citations = "<br>".join( - f"`{test['path']}:{test['locator']}`" - for test in item["cited_test_evidence"] - ) or "None" - lines.append( - f"| `{item['feature_id']}` | {citations} | " - f"{'yes' if item['test_function_exists'] else 'no'} | " - f"{item['direct_support']}: {item['support_reason']} | " - f"**{item['suggested_decision']}** |" - ) - lines.extend( - [ - "", - "### Numerical inconsistency resolved", - "", - "The six problematic cited E2 test records are the six nonexistent " - "locator names in SECURITY-002 (one), SECURITY-004 (two), and " - "SECURITY-005 (three). Seven feature demotions were suggested because " - "PLATFORM-009 is an additional feature-level maturity problem: it " - "cites no automated test function. The other three demotions are " - "claim-relevance findings for existing tests, so the figures measure " - "different things and should not be compared as the same denominator.", - "", - "## Proposed evidence schema", - "", - "Required: `path`, `kind`, and `explanation`. `locator` is required " - "for symbol/route/section evidence and omitted for `file`. " - "`line_range` is optional generated metadata.", - "", - "```json", - json.dumps(report["evidence_model"]["examples"], indent=2), - "```", - "", - "## Defects", - "", - "| Feature | Path | Locator | Result | Generated range | Problem |", - "|---|---|---|---|---|---|", - ] - ) - for item in report["entries"]: - if item["result"] in {"valid", "file-level-valid"}: - continue - evidence = item["current_evidence"] - problem = (item["problem"] or "").replace("|", "\\|") - lines.append( - f"| `{item['feature_id']}` | `{evidence.get('path', '')}` | " - f"`{item.get('locator') or ''}` | `{item['result']}` | " - f"`{item.get('generated_line_range') or ''}` | {problem} |" - ) - return "\n".join(lines) + "\n" - - -def write_outputs(output_dir: Path, report: dict[str, Any], queue: dict[str, Any]) -> None: - output_dir.mkdir(parents=True, exist_ok=True) - (output_dir / "evidence-validation-report.json").write_text( - json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" - ) - (output_dir / "evidence-validation-report.md").write_text( - render_markdown(report), encoding="utf-8" - ) - (output_dir / "evidence-repair-queue.json").write_text( - json.dumps(queue, indent=2, ensure_ascii=False) + "\n", encoding="utf-8" - ) - - -def run(argv: list[str] | None = None) -> int: - args = parse_args(argv) - repo_root = args.repo_root.resolve() - catalog_path = args.catalog.resolve() - features = load_catalog(catalog_path) - javascript_parser = JavascriptParser(repo_root) - validations = [ - validate_evidence( - repo_root, - str(feature.get("id", f"<feature:{feature_index}>")), - evidence_index, - evidence, - javascript_parser, - ) - for feature_index, feature in enumerate(features) - for evidence_index, evidence in enumerate(feature.get("evidence", [])) - if isinstance(evidence, dict) - ] - report = build_report( - repo_root, catalog_path, features, validations, javascript_parser - ) - queue = build_repair_queue(validations) - write_outputs(args.output_dir.resolve(), report, queue) - print(json.dumps(report["summary_counts"], sort_keys=True)) - return 1 if queue["defect_count"] else 0 - - -if __name__ == "__main__": - sys.exit(run()) diff --git a/specs/discovery/README.md b/specs/discovery/README.md new file mode 100644 index 000000000..faec5ed36 --- /dev/null +++ b/specs/discovery/README.md @@ -0,0 +1,30 @@ +# Discovery guide + +Discovery keeps a shared maintainer understanding of how the checked-in Odysseus code works without becoming a permanent audit system. It is a compact companion to the code for cross-cutting systems, not a feature certification or a substitute for normal testing. + +## Working model + +- Code is the ground truth for current behaviour. Trace current paths in source before recording a claim. +- These specs record current code-grounded behaviour and system boundaries only. They do not record accepted intent, design direction, refactor plans, or decision history. +- Keep this package in sync with code changes that alter a documented cross-cutting system, its authority boundary, or its canonical implementation location. +- Do not exhaustively revalidate existing functionality simply because it appears in a map. Investigate when there is a report, normal use visibly fails, a change affects the area, or a high-authority boundary needs review. +- Record a confirmed, actionable problem in the relevant project within the canonical Plane workspace. Keep high-level design, prioritization, ownership, and refactor discussion in Plane. Do not create parallel issue lists here. +- After implementation changes the code, update the relevant map to describe the resulting current state. +- Review safety-sensitive authority first: execution, data access, external tools, credentials, destructive operations, and unattended work. +- Cite stable source locations such as modules, routes, classes, and functions. Avoid fragile line ranges and generated evidence tables. + +## Package + +- [Current system map](system-map.md) describes the code-grounded subsystem boundaries. +- [Safety boundaries](safety-boundaries.md) records where review effort is most valuable. + +The existing [architecture runtime inventory](../architecture-runtime-inventory.md) remains useful structural context. It is explicitly a draft snapshot; re-check its measurements against the code before using them for an implementation decision. + +## How to use this package + +1. Start at the subsystem in the system map and confirm the relevant source. +2. For a defect, create or update the relevant project within the canonical Plane workspace with a reproducible report and ownership. +3. For a design, prioritization, ownership, or refactor question, use a Plane thread. +4. Make and validate the implementation through the normal engineering workflow, then update this package if the code changed a documented system boundary. + +This package intentionally has no generator, validator, maturity scale, feature database, or duplicate work tracker. diff --git a/specs/discovery/safety-boundaries.md b/specs/discovery/safety-boundaries.md new file mode 100644 index 000000000..69b966c1b --- /dev/null +++ b/specs/discovery/safety-boundaries.md @@ -0,0 +1,129 @@ +# Safety boundaries + +This document prioritizes review of broad authority. It records code-grounded safeguards and questions; it does not declare a vulnerability merely because a feature is powerful. No destructive test, external connection, or real credential was used for this map. + +## Shell and subprocess execution + +**Boundary.** Shell routes, agent bash and python tools, local model serving, and detached background jobs. + +**Available authority.** Commands run as the application process user and can create child processes. + +**User-controlled inputs.** Direct shell requests, model-produced tool arguments, scheduled-task prompts, and model-serving configuration. + +**Current safeguards.** Agent dispatch applies owner/admin checks and tool policy; process helpers use timeouts or bounded background-job lifecycle where implemented. + +**Confirmed risks or gaps.** Intentional authority with a confirmed gap: the agent shell starts in its workspace but is not sandboxed to it, and has no egress sandbox. This is documented in source and the threat model; it is not a newly demonstrated bypass. + +**Required verification.** Role-gate and disabled-tool outcomes, direct shell-route behaviour, and timeout, cancellation, and output handling for foreground and detached processes remain unverified. + +## Filesystem access and workspace confinement + +**Boundary.** Agent read, write, patch, listing, glob, and grep tools. + +**Available authority.** Read and modify files within active workspace confinement or fallback allowlisted roots. + +**User-controlled inputs.** Tool paths, patches, file contents, search patterns, and workspace selection passed into the tool dispatcher. + +**Current safeguards.** src/tool_execution.py resolves paths, blocks sensitive subpaths, applies allowlist containment, and tightens paths to the active workspace when one is bound. File tools use those resolvers. + +**Confirmed risks or gaps.** Intentional authority with safeguards. The file-tool policy does not sandbox the shell; treating a workspace as a whole-process containment boundary would be incorrect. + +**Required verification.** Traversal, symlink, sensitive-name, absolute-path, and workspace-switch behaviour remains unverified. + +## Agent-controlled tool dispatch + +**Boundary.** Model output becomes native or parsed tool calls and is dispatched by the agent loop. + +**Available authority.** The authority of every enabled tool, including privileged built-ins and external tools. + +**User-controlled inputs.** Chat content, attached/retrieved content that may influence the model, tool arguments, per-request tool selection, and policy toggles. + +**Current safeguards.** src/tool_security.py blocks protected tools for non-admin users and fails closed for malformed tool names; src/tool_policy.py supports disabled and guide-only policy; prompt-security helpers label untrusted context. + +**Confirmed risks or gaps.** Credible risk requiring verification: aliases, legacy text tools, native function calls, and MCP-qualified names must all reach the same policy outcome. The code has specific alias handling for email/MCP names, which makes this a sensitive compatibility seam. + +**Required verification.** The current policy outcomes for owner role, request mode, disabled state, native versus parsed invocation, qualified aliases, and external-content entry points remain unverified. + +## MCP and external tool servers + +**Boundary.** Configured MCP servers and their tools are exposed to the agent through the MCP manager and routes. + +**Available authority.** Depends on the server: external network access, local process access, messaging, or data mutation may be delegated outside the application. + +**User-controlled inputs.** Server configuration, remote OAuth completion, tool arguments, and model-selected MCP calls. + +**Current safeguards.** MCP routes are registered through routes/mcp_routes.py; MCP-qualified tools are denied to non-admin users by src/tool_security.py. OAuth state and token persistence are handled in src/mcp_oauth.py. + +**Confirmed risks or gaps.** Credible risk requiring verification: an MCP server authority is broader than the application can infer from its tool name. This map does not establish a trust or approval model for server installation and individual tool invocation. + +**Required verification.** Server onboarding, credential storage, server-origin trust, OAuth callback deployment, tool disablement, and invocation audit behaviour remain unverified. + +## Outbound network requests and URL validation + +**Boundary.** Search/content fetch, research, webhooks, skill import, provider endpoints, and other HTTP clients. + +**Available authority.** The application can make outbound requests from its network position. + +**User-controlled inputs.** Search/fetch URLs, imported skill URLs, webhook configuration, and some endpoint settings. + +**Current safeguards.** src/url_security.py validates untrusted public HTTP URLs and fails closed on unsuitable schemes or private addresses. services/search/content.py resolves and rejects non-public hosts, pins resolved addresses for fetches, caps bodies, and limits redirects. + +**Confirmed risks or gaps.** Intentional split: administrator-created model endpoints may target private providers, while untrusted URLs use public-address checks. That distinction is required for self-hosted deployments but needs explicit call-site review. + +**Required verification.** The URL-source classification for outbound clients and the current handling of redirects and DNS changes remain unverified. + +## Secrets, credentials, and vault sessions + +**Boundary.** Application-managed encrypted secrets, API keys, provider credentials, and Bitwarden/Vaultwarden CLI sessions. + +**Available authority.** Credentials unlock remote providers and connected personal services. + +**User-controlled inputs.** Administrative configuration, login/unlock requests, imported settings, and agent vault tool arguments. + +**Current safeguards.** src/secret_storage.py uses a locally stored Fernet key with restrictive permissions for supported database secrets. Vault routes require an administrator, avoid passing master passwords in command arguments, and set restrictive permissions on the vault-session file. + +**Confirmed risks or gaps.** Confirmed current boundary: vault session data is persisted through the vault path, not through src/secret_storage.py. This is an unresolved question about current security semantics, not a confirmed exposure. + +**Required verification.** Current encryption-at-rest, owner scope, rotation, lock/logout, backup/restore, and log/tool-result exposure behaviour remains unverified. + +## Authentication and privileged administration + +**Boundary.** Session authentication, API tokens, privileged routes, and internal tool loopback. + +**Available authority.** Administrative identity can access execution, settings, integrations, data deletion, and secrets. + +**User-controlled inputs.** Login/signup data, session cookies, API tokens, authentication configuration, and requests to privileged routes. + +**Current safeguards.** core/auth.py, core/middleware.py, and route-level checks establish identity and administrator gates. app.py warns when localhost bypass is configured; SECURITY.md documents deployment requirements. + +**Confirmed risks or gaps.** Intentional authority with safeguards. Security depends on deployments keeping authentication enabled and internal services private; this map does not audit reverse-proxy or environment configuration. + +**Required verification.** Setup, anonymous, non-admin, admin, token, and internal-loopback behaviour, including privileged-route gate consistency, remains unverified. + +## Deletion, wipe, backup, and restore + +**Boundary.** Administrative wipe, cleanup, backup import/export, and the backup restore command. + +**Available authority.** Delete or replace user data and credentials. + +**User-controlled inputs.** Administrative HTTP requests, cleanup choices, backup payloads, archive paths, and restore command options. + +**Current safeguards.** Administrative wipe routes use the administrative boundary. Cleanup exposes a preview route before mutation. The documented backup tool requires explicit restore confirmation, stages the old data directory, and validates archive members before extraction. + +**Confirmed risks or gaps.** Intentional destructive authority. Backup archives contain secrets by design, as documented in docs/backup-restore.md; this is an operator confidentiality responsibility, not a code defect established here. + +**Required verification.** Role-gate, confirmation, archive-rejection, staged-recovery, and owner-isolation behaviour remains unverified. No destructive runtime test was performed. + +## Background jobs and unattended task execution + +**Boundary.** Scheduled tasks, background-job monitor, startup tasks, and notification/delivery work that continue without an active browser request. + +**Available authority.** Scheduled agent work can obtain model access and, for eligible owners, shell and file tools; task output can interact with connected services. + +**User-controlled inputs.** Stored task prompt, schedule, model/crew selection, enabled-tool configuration, output target, and prior persisted state. + +**Current safeguards.** src/task_scheduler.py serializes execution, records task runs, associates work with an owner, and applies the agent owner-based tool gate. src/bg_jobs.py keeps bounded state and can terminate overlong subprocess jobs. + +**Confirmed risks or gaps.** Credible risk requiring verification: authority is inherited and exercised later, so changes to roles, task configuration, and disabled tools must be checked at execution time rather than assumed from task creation. + +**Required verification.** Creation, editing, role-change, scheduling, cancellation, restart-recovery, and execution behaviour remains unverified, including whether current policy is re-evaluated before privileged action. diff --git a/specs/discovery/system-map.md b/specs/discovery/system-map.md new file mode 100644 index 000000000..fcaf07e6a --- /dev/null +++ b/specs/discovery/system-map.md @@ -0,0 +1,123 @@ +# Current system map + +This document describes the checked-in code at the same commit as this document. It is a subsystem map, not an exhaustive feature catalog or runtime certification. Each section records only current implementation, canonical locations, confirmed current problems, and unresolved factual questions. + +## Startup and application composition + +**Current implementation.** app.py creates the application, mounts static assets, constructs shared services, registers route factories, and owns lifespan startup and shutdown. src/app_initializer.py prepares application state; core/ provides persistence, authentication, middleware, sessions, and platform helpers. + +**Canonical code locations.** app.py; src/app_initializer.py; core/database.py; core/auth.py; core/middleware.py; routes/. + +**Confirmed problems.** None recorded by this mapping. + +**Open questions.** Which component currently owns startup and shutdown for each long-lived service? + +## Frontend shell and browser interaction + +**Current implementation.** static/index.html is served by the root and SPA deep-link routes in app.py; static/app.js, static/style.css, and static/js/ implement the client surface. + +**Canonical code locations.** static/index.html; static/app.js; static/js/; static/style.css; app.py deep-link handlers. + +**Confirmed problems.** The /backgrounds route calls serve_html_with_nonce for static/backgrounds.html, but that file is absent from static/. This is a confirmed broken prototype route, not evidence about the rest of the frontend. + +**Open questions.** Is /backgrounds currently an intentionally supported route or an obsolete prototype? + +## Chat, sessions, and streaming + +**Current implementation.** routes/chat_routes.py and routes/chat_helpers.py coordinate requests, session state, and SSE delivery. src/chat_handler.py, src/chat_processor.py, src/llm_core.py, and src/session_actions.py provide message preparation, provider interaction, and session operations. + +**Canonical code locations.** routes/chat_routes.py; routes/chat_helpers.py; routes/session_routes.py; src/chat_handler.py; src/chat_processor.py; src/llm_core.py; core/session_manager.py. + +**Confirmed problems.** src/agent_loop.py annotates _resolved_tool_event_name with Any but imports no Any and does not enable postponed annotation evaluation. Python evaluates that annotation while importing the module, so this is an import-time defect at the checked baseline. + +**Open questions.** No end-to-end provider or browser streaming run was performed for this map. + +## Agents, tools, and execution + +**Current implementation.** src/agent_loop.py drives multi-round tool use. src/tool_execution.py dispatches calls and binds workspace context. src/agent_tools/ contains individual implementations; src/tool_security.py and src/tool_policy.py apply role and request policies. Long-running command work is represented by src/bg_jobs.py. + +**Canonical code locations.** src/agent_loop.py; src/tool_execution.py; src/agent_tools/; src/tool_security.py; src/tool_policy.py; src/tool_schemas.py; src/bg_jobs.py. + +**Confirmed problems.** The import-time annotation defect above blocks the main agent/tool path. The shell is intentionally not a filesystem or network sandbox; that is an authority boundary, not by itself a vulnerability claim. + +**Open questions.** Which native, legacy, and MCP-qualified invocation paths reach each policy gate? + +## Models, providers, and local serving + +**Current implementation.** Model routes delegate to discovery, capabilities, endpoint resolution, and LLM core modules. Cookbook routes and hardware-fit services handle model lifecycle and local-serving support. + +**Canonical code locations.** routes/model_routes.py; src/model_discovery.py; src/model_capabilities.py; src/endpoint_resolver.py; src/llm_core.py; routes/cookbook_routes.py; src/cookbook_serve_lifecycle.py; services/hwfit/. + +**Confirmed problems.** None recorded by this mapping. + +**Open questions.** Which endpoint inputs are administrator-created and permitted to use private provider addresses? + +## Search and research + +**Current implementation.** HTTP search routes use services/search/; research is exposed through routes/research/ and implemented in services/research/, src/deep_research.py, and related helpers. src/search/ remains an import-compatibility layer for callers not yet moved to services.search. + +**Canonical code locations.** routes/search_routes.py; services/search/; routes/research/research_routes.py; services/research/; src/deep_research.py; src/search/. + +**Confirmed problems.** The two search package paths are a confirmed compatibility arrangement, not two independent canonical implementations: several src/search modules forward to services.search. The remaining wrappers and compatibility imports can drift. + +**Open questions.** No live provider request was made; provider configuration and network access remain unverified. + +## Documents, retrieval, and personal knowledge + +**Current implementation.** Document routes coordinate upload handling, document processing, and editor actions. Personal-document and RAG modules use Chroma and embedding clients. PDF viewing uses the optional-dependency loader in src/pdf_runtime.py; form extraction and filling live separately in src/pdf_forms.py and src/pdf_form_doc.py. + +**Canonical code locations.** routes/document_routes.py; src/upload_handler.py; src/document_processor.py; src/document_actions.py; src/personal_docs.py; src/rag_manager.py; src/embeddings.py; src/pdf_runtime.py; src/pdf_forms.py; src/pdf_form_doc.py. + +**Confirmed problems.** PDF viewing/runtime loading and PDF form processing are separate implementations. That separation is confirmed and intentional in the source; it is not a defect without a reported behavioural failure. + +**Open questions.** Optional PDF dependencies and representative uploaded documents were not exercised. + +## Memory and skills + +**Current implementation.** Memory routes use services/memory/ and vector helpers. Skills are exposed through routes/skills_routes.py, stored and managed in services/memory/skills.py, and may be imported through services/memory/skill_importer.py. + +**Canonical code locations.** routes/memory/memory_routes.py; services/memory/; src/memory.py; src/memory_vector.py; routes/skills_routes.py; services/memory/skills.py; services/memory/skill_importer.py. + +**Confirmed problems.** None recorded by this mapping. + +**Open questions.** Which imported skill content can reach execution-capable paths, and which validation occurs before that point? + +## Personal services: email, calendar, contacts, notes, and tasks + +**Current implementation.** Dedicated route modules own email, CalDAV calendar, CardDAV contacts, notes, and tasks. Supporting modules include email helpers and pollers, CalDAV sync and writeback, and the task scheduler. + +**Canonical code locations.** routes/email_routes.py; routes/calendar_routes.py; routes/contacts/contacts_routes.py; routes/note/note_routes.py; routes/task_routes.py; routes/assistant_routes.py; src/caldav_sync.py; src/caldav_writeback.py; src/task_scheduler.py. + +**Confirmed problems.** None recorded by this mapping. + +**Open questions.** External account behaviour, writeback, and delivery require controlled credentials and are not runtime-validated here. + +## Media, speech, and image work + +**Current implementation.** Gallery and image routes coordinate media features. Service modules own speech and media integrations; src/generated_images.py and src/visual_report.py support artifact handling and presentation. + +**Canonical code locations.** routes/gallery/gallery_routes.py; routes/stt_routes.py; routes/tts_routes.py; src/generated_images.py; services/stt/; services/tts/; services/faces/; src/visual_report.py. + +**Confirmed problems.** None recorded by this mapping. + +**Open questions.** Hardware- and provider-dependent media workflows were not exercised. + +## Authentication, secrets, and privileged administration + +**Current implementation.** core/auth.py and core/middleware.py provide identity and request gates. src/secret_storage.py encrypts application-managed database secrets with a local Fernet key. Vault handling is separate: routes/vault_routes.py and src/tools/vault.py invoke the Bitwarden CLI and persist its session data in the application data area. + +**Canonical code locations.** core/auth.py; core/middleware.py; routes/auth_routes.py; routes/api_token_routes.py; src/secret_storage.py; routes/vault_routes.py; src/tools/vault.py; routes/admin_wipe/admin_wipe_routes.py. + +**Confirmed problems.** Vault-command handling and local application secret storage are distinct paths with different storage mechanisms. This is a source-confirmed boundary, not evidence that either path is compromised. + +**Open questions.** What are the current confidentiality, ownership, rotation, and backup semantics for vault session data? + +## Persistence, background work, and operations + +**Current implementation.** SQLite models and persistence are centred in core/database.py; managers use application data paths. The scheduler and background-job monitor can continue work outside a live browser request. Operational routes cover cleanup, backup, and administrative wipe; the repository also provides a backup script and user documentation. + +**Canonical code locations.** core/database.py; src/runtime_paths.py; src/task_scheduler.py; src/bg_jobs.py; src/bg_monitor.py; routes/backup_routes.py; routes/cleanup/cleanup_routes.py; routes/admin_wipe/admin_wipe_routes.py; scripts/odysseus-backup; docs/backup-restore.md. + +**Confirmed problems.** None recorded by this mapping. + +**Open questions.** What current behaviour applies to background execution, cancellation, retries, and authority inheritance? From 2ac72d596429f92e70ca8a78bf463ba893bcb8b6 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <alexandremagteixeira@gmail.com> Date: Sun, 26 Jul 2026 14:32:20 +0100 Subject: [PATCH 003/125] docs(discovery): improve map readability --- specs/discovery/README.md | 43 +++++----- specs/discovery/safety-boundaries.md | 123 +++++++++++++++------------ specs/discovery/system-map.md | 116 ++++++++++++++----------- 3 files changed, 157 insertions(+), 125 deletions(-) diff --git a/specs/discovery/README.md b/specs/discovery/README.md index faec5ed36..166ed3656 100644 --- a/specs/discovery/README.md +++ b/specs/discovery/README.md @@ -1,30 +1,33 @@ -# Discovery guide +# Odysseus discovery maps -Discovery keeps a shared maintainer understanding of how the checked-in Odysseus code works without becoming a permanent audit system. It is a compact companion to the code for cross-cutting systems, not a feature certification or a substitute for normal testing. +Compact, code-grounded maps of the cross-cutting systems in the checked-in Odysseus codebase. They are not a feature certification or substitute for normal testing, and do not become a permanent audit system. -## Working model +> [!IMPORTANT] +> Code is the source of truth for current behaviour. These specifications describe only the checked-in system and its current boundaries; keep them synchronized with relevant code changes. -- Code is the ground truth for current behaviour. Trace current paths in source before recording a claim. -- These specs record current code-grounded behaviour and system boundaries only. They do not record accepted intent, design direction, refactor plans, or decision history. -- Keep this package in sync with code changes that alter a documented cross-cutting system, its authority boundary, or its canonical implementation location. -- Do not exhaustively revalidate existing functionality simply because it appears in a map. Investigate when there is a report, normal use visibly fails, a change affects the area, or a high-authority boundary needs review. -- Record a confirmed, actionable problem in the relevant project within the canonical Plane workspace. Keep high-level design, prioritization, ownership, and refactor discussion in Plane. Do not create parallel issue lists here. -- After implementation changes the code, update the relevant map to describe the resulting current state. -- Review safety-sensitive authority first: execution, data access, external tools, credentials, destructive operations, and unattended work. -- Cite stable source locations such as modules, routes, classes, and functions. Avoid fragile line ranges and generated evidence tables. +## Explore the maps -## Package +| Document | Purpose | +|---|---| +| [Current system map](system-map.md) | Explains the current subsystem boundaries, implementation locations, confirmed problems, and factual open questions. | +| [Safety boundaries](safety-boundaries.md) | Maps broad authority, safeguards, confirmed risks or gaps, and unverified behaviour. | -- [Current system map](system-map.md) describes the code-grounded subsystem boundaries. -- [Safety boundaries](safety-boundaries.md) records where review effort is most valuable. +## Working rules -The existing [architecture runtime inventory](../architecture-runtime-inventory.md) remains useful structural context. It is explicitly a draft snapshot; re-check its measurements against the code before using them for an implementation decision. +- **Trace the code first.** Confirm the current path in source before recording a claim. +- **Keep specs current-state only.** Do not record intentions, design direction, refactor plans, or decision history here. +- **Synchronize with code.** Update this package when a code change alters a documented cross-cutting system, authority boundary, or canonical implementation location. +- **Investigate with cause.** Do not exhaustively revalidate existing functionality without a report, visible failure, relevant change, or high-authority review need. +- **Keep planning in Plane.** Record confirmed problems in the relevant project within the canonical Plane workspace; keep high-level design, prioritization, ownership, and refactor discussion there. +- **Review authority carefully.** Give execution, data access, external tools, credentials, destructive operations, and unattended work focused review. +- **Use stable locations.** Cite modules, routes, classes, and functions instead of fragile line ranges or generated evidence tables. -## How to use this package +## Working flow -1. Start at the subsystem in the system map and confirm the relevant source. +1. Start with the relevant map and trace the cited code. 2. For a defect, create or update the relevant project within the canonical Plane workspace with a reproducible report and ownership. -3. For a design, prioritization, ownership, or refactor question, use a Plane thread. -4. Make and validate the implementation through the normal engineering workflow, then update this package if the code changed a documented system boundary. +3. Use a Plane thread for design, prioritization, ownership, or refactor discussion. +4. After implementation changes the code, update the affected map to describe the resulting current state. -This package intentionally has no generator, validator, maturity scale, feature database, or duplicate work tracker. +> [!NOTE] +> This package intentionally contains no generator, validator, maturity scale, feature database, or parallel work tracker. The [architecture runtime inventory](../architecture-runtime-inventory.md) remains useful structural context, but is an explicitly draft snapshot. diff --git a/specs/discovery/safety-boundaries.md b/specs/discovery/safety-boundaries.md index 69b966c1b..ee8b67d98 100644 --- a/specs/discovery/safety-boundaries.md +++ b/specs/discovery/safety-boundaries.md @@ -1,129 +1,142 @@ # Safety boundaries -This document prioritizes review of broad authority. It records code-grounded safeguards and questions; it does not declare a vulnerability merely because a feature is powerful. No destructive test, external connection, or real credential was used for this map. +> [!IMPORTANT] +> Broad authority does not by itself establish a vulnerability. This map records code-grounded safeguards, confirmed risks or gaps, and unverified behaviour. No destructive test, external connection, or real credential was used for this map. + +## Navigate the boundaries + +- [Shell and subprocess execution](#shell-and-subprocess-execution) +- [Filesystem access and workspace confinement](#filesystem-access-and-workspace-confinement) +- [Agent-controlled tool dispatch](#agent-controlled-tool-dispatch) +- [MCP and external tool servers](#mcp-and-external-tool-servers) +- [Outbound network requests and URL validation](#outbound-network-requests-and-url-validation) +- [Secrets, credentials, and vault sessions](#secrets-credentials-and-vault-sessions) +- [Authentication and privileged administration](#authentication-and-privileged-administration) +- [Deletion, wipe, backup, and restore](#deletion-wipe-backup-and-restore) +- [Background jobs and unattended task execution](#background-jobs-and-unattended-task-execution) ## Shell and subprocess execution -**Boundary.** Shell routes, agent bash and python tools, local model serving, and detached background jobs. +- **Boundary:** Shell routes, agent `bash` and `python` tools, local model serving, and detached background jobs. -**Available authority.** Commands run as the application process user and can create child processes. +- **Available authority:** Commands run as the application process user and can create child processes. -**User-controlled inputs.** Direct shell requests, model-produced tool arguments, scheduled-task prompts, and model-serving configuration. +- **User-controlled inputs:** Direct shell requests, model-produced tool arguments, scheduled-task prompts, and model-serving configuration. -**Current safeguards.** Agent dispatch applies owner/admin checks and tool policy; process helpers use timeouts or bounded background-job lifecycle where implemented. +- **Current safeguards:** Agent dispatch applies owner/admin checks and tool policy; process helpers use timeouts or bounded background-job lifecycle where implemented. -**Confirmed risks or gaps.** Intentional authority with a confirmed gap: the agent shell starts in its workspace but is not sandboxed to it, and has no egress sandbox. This is documented in source and the threat model; it is not a newly demonstrated bypass. +- **Confirmed risks or gaps:** Intentional authority with a confirmed gap: the agent shell starts in its workspace but is not sandboxed to it, and has no egress sandbox. This is documented in source and the threat model; it is not a newly demonstrated bypass. -**Required verification.** Role-gate and disabled-tool outcomes, direct shell-route behaviour, and timeout, cancellation, and output handling for foreground and detached processes remain unverified. +- **Unverified behaviour:** Role-gate and disabled-tool outcomes, direct shell-route behaviour, and timeout, cancellation, and output handling for foreground and detached processes remain unverified. ## Filesystem access and workspace confinement -**Boundary.** Agent read, write, patch, listing, glob, and grep tools. +- **Boundary:** Agent read, write, patch, listing, glob, and grep tools. -**Available authority.** Read and modify files within active workspace confinement or fallback allowlisted roots. +- **Available authority:** Read and modify files within active workspace confinement or fallback allowlisted roots. -**User-controlled inputs.** Tool paths, patches, file contents, search patterns, and workspace selection passed into the tool dispatcher. +- **User-controlled inputs:** Tool paths, patches, file contents, search patterns, and workspace selection passed into the tool dispatcher. -**Current safeguards.** src/tool_execution.py resolves paths, blocks sensitive subpaths, applies allowlist containment, and tightens paths to the active workspace when one is bound. File tools use those resolvers. +- **Current safeguards:** [`src/tool_execution.py`](../../src/tool_execution.py) resolves paths, blocks sensitive subpaths, applies allowlist containment, and tightens paths to the active workspace when one is bound. File tools use those resolvers. -**Confirmed risks or gaps.** Intentional authority with safeguards. The file-tool policy does not sandbox the shell; treating a workspace as a whole-process containment boundary would be incorrect. +- **Confirmed risks or gaps:** Intentional authority with safeguards. The file-tool policy does not sandbox the shell; treating a workspace as a whole-process containment boundary would be incorrect. -**Required verification.** Traversal, symlink, sensitive-name, absolute-path, and workspace-switch behaviour remains unverified. +- **Unverified behaviour:** Traversal, symlink, sensitive-name, absolute-path, and workspace-switch behaviour remains unverified. ## Agent-controlled tool dispatch -**Boundary.** Model output becomes native or parsed tool calls and is dispatched by the agent loop. +- **Boundary:** Model output becomes native or parsed tool calls and is dispatched by the agent loop. -**Available authority.** The authority of every enabled tool, including privileged built-ins and external tools. +- **Available authority:** The authority of every enabled tool, including privileged built-ins and external tools. -**User-controlled inputs.** Chat content, attached/retrieved content that may influence the model, tool arguments, per-request tool selection, and policy toggles. +- **User-controlled inputs:** Chat content, attached/retrieved content that may influence the model, tool arguments, per-request tool selection, and policy toggles. -**Current safeguards.** src/tool_security.py blocks protected tools for non-admin users and fails closed for malformed tool names; src/tool_policy.py supports disabled and guide-only policy; prompt-security helpers label untrusted context. +- **Current safeguards:** [`src/tool_security.py`](../../src/tool_security.py) blocks protected tools for non-admin users and fails closed for malformed tool names; [`src/tool_policy.py`](../../src/tool_policy.py) supports disabled and guide-only policy; prompt-security helpers label untrusted context. -**Confirmed risks or gaps.** Credible risk requiring verification: aliases, legacy text tools, native function calls, and MCP-qualified names must all reach the same policy outcome. The code has specific alias handling for email/MCP names, which makes this a sensitive compatibility seam. +- **Confirmed risks or gaps:** Credible risk requiring verification: aliases, legacy text tools, native function calls, and MCP-qualified names must all reach the same policy outcome. The code has specific alias handling for email/MCP names, which makes this a sensitive compatibility seam. -**Required verification.** The current policy outcomes for owner role, request mode, disabled state, native versus parsed invocation, qualified aliases, and external-content entry points remain unverified. +- **Unverified behaviour:** The current policy outcomes for owner role, request mode, disabled state, native versus parsed invocation, qualified aliases, and external-content entry points remain unverified. ## MCP and external tool servers -**Boundary.** Configured MCP servers and their tools are exposed to the agent through the MCP manager and routes. +- **Boundary:** Configured MCP servers and their tools are exposed to the agent through the MCP manager and routes. -**Available authority.** Depends on the server: external network access, local process access, messaging, or data mutation may be delegated outside the application. +- **Available authority:** Depends on the server: external network access, local process access, messaging, or data mutation may be delegated outside the application. -**User-controlled inputs.** Server configuration, remote OAuth completion, tool arguments, and model-selected MCP calls. +- **User-controlled inputs:** Server configuration, remote OAuth completion, tool arguments, and model-selected MCP calls. -**Current safeguards.** MCP routes are registered through routes/mcp_routes.py; MCP-qualified tools are denied to non-admin users by src/tool_security.py. OAuth state and token persistence are handled in src/mcp_oauth.py. +- **Current safeguards:** MCP routes are registered through [`routes/mcp_routes.py`](../../routes/mcp_routes.py); MCP-qualified tools are denied to non-admin users by [`src/tool_security.py`](../../src/tool_security.py). OAuth state and token persistence are handled in [`src/mcp_oauth.py`](../../src/mcp_oauth.py). -**Confirmed risks or gaps.** Credible risk requiring verification: an MCP server authority is broader than the application can infer from its tool name. This map does not establish a trust or approval model for server installation and individual tool invocation. +- **Confirmed risks or gaps:** Credible risk requiring verification: an MCP server authority is broader than the application can infer from its tool name. This map does not establish a trust or approval model for server installation and individual tool invocation. -**Required verification.** Server onboarding, credential storage, server-origin trust, OAuth callback deployment, tool disablement, and invocation audit behaviour remain unverified. +- **Unverified behaviour:** Server onboarding, credential storage, server-origin trust, OAuth callback deployment, tool disablement, and invocation audit behaviour remain unverified. ## Outbound network requests and URL validation -**Boundary.** Search/content fetch, research, webhooks, skill import, provider endpoints, and other HTTP clients. +- **Boundary:** Search/content fetch, research, webhooks, skill import, provider endpoints, and other HTTP clients. -**Available authority.** The application can make outbound requests from its network position. +- **Available authority:** The application can make outbound requests from its network position. -**User-controlled inputs.** Search/fetch URLs, imported skill URLs, webhook configuration, and some endpoint settings. +- **User-controlled inputs:** Search/fetch URLs, imported skill URLs, webhook configuration, and some endpoint settings. -**Current safeguards.** src/url_security.py validates untrusted public HTTP URLs and fails closed on unsuitable schemes or private addresses. services/search/content.py resolves and rejects non-public hosts, pins resolved addresses for fetches, caps bodies, and limits redirects. +- **Current safeguards:** [`src/url_security.py`](../../src/url_security.py) validates untrusted public HTTP URLs and fails closed on unsuitable schemes or private addresses. [`services/search/content.py`](../../services/search/content.py) resolves and rejects non-public hosts, pins resolved addresses for fetches, caps bodies, and limits redirects. -**Confirmed risks or gaps.** Intentional split: administrator-created model endpoints may target private providers, while untrusted URLs use public-address checks. That distinction is required for self-hosted deployments but needs explicit call-site review. +- **Confirmed risks or gaps:** Intentional split: administrator-created model endpoints may target private providers, while untrusted URLs use public-address checks. That distinction is required for self-hosted deployments but needs explicit call-site review. -**Required verification.** The URL-source classification for outbound clients and the current handling of redirects and DNS changes remain unverified. +- **Unverified behaviour:** The URL-source classification for outbound clients and the current handling of redirects and DNS changes remain unverified. ## Secrets, credentials, and vault sessions -**Boundary.** Application-managed encrypted secrets, API keys, provider credentials, and Bitwarden/Vaultwarden CLI sessions. +- **Boundary:** Application-managed encrypted secrets, API keys, provider credentials, and Bitwarden/Vaultwarden CLI sessions. -**Available authority.** Credentials unlock remote providers and connected personal services. +- **Available authority:** Credentials unlock remote providers and connected personal services. -**User-controlled inputs.** Administrative configuration, login/unlock requests, imported settings, and agent vault tool arguments. +- **User-controlled inputs:** Administrative configuration, login/unlock requests, imported settings, and agent vault tool arguments. -**Current safeguards.** src/secret_storage.py uses a locally stored Fernet key with restrictive permissions for supported database secrets. Vault routes require an administrator, avoid passing master passwords in command arguments, and set restrictive permissions on the vault-session file. +- **Current safeguards:** [`src/secret_storage.py`](../../src/secret_storage.py) uses a locally stored Fernet key with restrictive permissions for supported database secrets. Vault routes require an administrator, avoid passing master passwords in command arguments, and set restrictive permissions on the vault-session file. -**Confirmed risks or gaps.** Confirmed current boundary: vault session data is persisted through the vault path, not through src/secret_storage.py. This is an unresolved question about current security semantics, not a confirmed exposure. +- **Confirmed risks or gaps:** Confirmed current boundary: vault session data is persisted through the vault path, not through [`src/secret_storage.py`](../../src/secret_storage.py). This is an unresolved question about current security semantics, not a confirmed exposure. -**Required verification.** Current encryption-at-rest, owner scope, rotation, lock/logout, backup/restore, and log/tool-result exposure behaviour remains unverified. +- **Unverified behaviour:** Current encryption-at-rest, owner scope, rotation, lock/logout, backup/restore, and log/tool-result exposure behaviour remains unverified. ## Authentication and privileged administration -**Boundary.** Session authentication, API tokens, privileged routes, and internal tool loopback. +- **Boundary:** Session authentication, API tokens, privileged routes, and internal tool loopback. -**Available authority.** Administrative identity can access execution, settings, integrations, data deletion, and secrets. +- **Available authority:** Administrative identity can access execution, settings, integrations, data deletion, and secrets. -**User-controlled inputs.** Login/signup data, session cookies, API tokens, authentication configuration, and requests to privileged routes. +- **User-controlled inputs:** Login/signup data, session cookies, API tokens, authentication configuration, and requests to privileged routes. -**Current safeguards.** core/auth.py, core/middleware.py, and route-level checks establish identity and administrator gates. app.py warns when localhost bypass is configured; SECURITY.md documents deployment requirements. +- **Current safeguards:** [`core/auth.py`](../../core/auth.py), [`core/middleware.py`](../../core/middleware.py), and route-level checks establish identity and administrator gates. [`app.py`](../../app.py) warns when localhost bypass is configured; [`SECURITY.md`](../../SECURITY.md) documents deployment requirements. -**Confirmed risks or gaps.** Intentional authority with safeguards. Security depends on deployments keeping authentication enabled and internal services private; this map does not audit reverse-proxy or environment configuration. +- **Confirmed risks or gaps:** Intentional authority with safeguards. Security depends on deployments keeping authentication enabled and internal services private; this map does not audit reverse-proxy or environment configuration. -**Required verification.** Setup, anonymous, non-admin, admin, token, and internal-loopback behaviour, including privileged-route gate consistency, remains unverified. +- **Unverified behaviour:** Setup, anonymous, non-admin, admin, token, and internal-loopback behaviour, including privileged-route gate consistency, remains unverified. ## Deletion, wipe, backup, and restore -**Boundary.** Administrative wipe, cleanup, backup import/export, and the backup restore command. +- **Boundary:** Administrative wipe, cleanup, backup import/export, and the backup restore command. -**Available authority.** Delete or replace user data and credentials. +- **Available authority:** Delete or replace user data and credentials. -**User-controlled inputs.** Administrative HTTP requests, cleanup choices, backup payloads, archive paths, and restore command options. +- **User-controlled inputs:** Administrative HTTP requests, cleanup choices, backup payloads, archive paths, and restore command options. -**Current safeguards.** Administrative wipe routes use the administrative boundary. Cleanup exposes a preview route before mutation. The documented backup tool requires explicit restore confirmation, stages the old data directory, and validates archive members before extraction. +- **Current safeguards:** Administrative wipe routes use the administrative boundary. Cleanup exposes a preview route before mutation. The documented backup tool requires explicit restore confirmation, stages the old data directory, and validates archive members before extraction. -**Confirmed risks or gaps.** Intentional destructive authority. Backup archives contain secrets by design, as documented in docs/backup-restore.md; this is an operator confidentiality responsibility, not a code defect established here. +- **Confirmed risks or gaps:** Intentional destructive authority. Backup archives contain secrets by design, as documented in [`docs/backup-restore.md`](../../docs/backup-restore.md); this is an operator confidentiality responsibility, not a code defect established here. -**Required verification.** Role-gate, confirmation, archive-rejection, staged-recovery, and owner-isolation behaviour remains unverified. No destructive runtime test was performed. +- **Unverified behaviour:** Role-gate, confirmation, archive-rejection, staged-recovery, and owner-isolation behaviour remains unverified. No destructive runtime test was performed. ## Background jobs and unattended task execution -**Boundary.** Scheduled tasks, background-job monitor, startup tasks, and notification/delivery work that continue without an active browser request. +- **Boundary:** Scheduled tasks, background-job monitor, startup tasks, and notification/delivery work that continue without an active browser request. -**Available authority.** Scheduled agent work can obtain model access and, for eligible owners, shell and file tools; task output can interact with connected services. +- **Available authority:** Scheduled agent work can obtain model access and, for eligible owners, shell and file tools; task output can interact with connected services. -**User-controlled inputs.** Stored task prompt, schedule, model/crew selection, enabled-tool configuration, output target, and prior persisted state. +- **User-controlled inputs:** Stored task prompt, schedule, model/crew selection, enabled-tool configuration, output target, and prior persisted state. -**Current safeguards.** src/task_scheduler.py serializes execution, records task runs, associates work with an owner, and applies the agent owner-based tool gate. src/bg_jobs.py keeps bounded state and can terminate overlong subprocess jobs. +- **Current safeguards:** [`src/task_scheduler.py`](../../src/task_scheduler.py) serializes execution, records task runs, associates work with an owner, and applies the agent owner-based tool gate. [`src/bg_jobs.py`](../../src/bg_jobs.py) keeps bounded state and can terminate overlong subprocess jobs. -**Confirmed risks or gaps.** Credible risk requiring verification: authority is inherited and exercised later, so changes to roles, task configuration, and disabled tools must be checked at execution time rather than assumed from task creation. +- **Confirmed risks or gaps:** Credible risk requiring verification: authority is inherited and exercised later, so changes to roles, task configuration, and disabled tools must be checked at execution time rather than assumed from task creation. -**Required verification.** Creation, editing, role-change, scheduling, cancellation, restart-recovery, and execution behaviour remains unverified, including whether current policy is re-evaluated before privileged action. +- **Unverified behaviour:** Creation, editing, role-change, scheduling, cancellation, restart-recovery, and execution behaviour remains unverified, including whether current policy is re-evaluated before privileged action. diff --git a/specs/discovery/system-map.md b/specs/discovery/system-map.md index fcaf07e6a..2f9f50eaa 100644 --- a/specs/discovery/system-map.md +++ b/specs/discovery/system-map.md @@ -1,123 +1,139 @@ # Current system map -This document describes the checked-in code at the same commit as this document. It is a subsystem map, not an exhaustive feature catalog or runtime certification. Each section records only current implementation, canonical locations, confirmed current problems, and unresolved factual questions. +> [!NOTE] +> This map describes the checked-in code at the same commit as this document. It is a subsystem map, not an exhaustive feature catalog or runtime certification. Each section records only current implementation, canonical locations, confirmed current problems, and unresolved factual questions. + +## Navigate the system + +- [Startup and application composition](#startup-and-application-composition) +- [Frontend shell and browser interaction](#frontend-shell-and-browser-interaction) +- [Chat, sessions, and streaming](#chat-sessions-and-streaming) +- [Agents, tools, and execution](#agents-tools-and-execution) +- [Models, providers, and local serving](#models-providers-and-local-serving) +- [Search and research](#search-and-research) +- [Documents, retrieval, and personal knowledge](#documents-retrieval-and-personal-knowledge) +- [Memory and skills](#memory-and-skills) +- [Email, calendar, contacts, notes, and tasks](#email-calendar-contacts-notes-and-tasks) +- [Media, speech, and image work](#media-speech-and-image-work) +- [Authentication, secrets, and privileged administration](#authentication-secrets-and-privileged-administration) +- [Persistence, background work, and operations](#persistence-background-work-and-operations) ## Startup and application composition -**Current implementation.** app.py creates the application, mounts static assets, constructs shared services, registers route factories, and owns lifespan startup and shutdown. src/app_initializer.py prepares application state; core/ provides persistence, authentication, middleware, sessions, and platform helpers. +- **How it works:** [`app.py`](../../app.py) creates the application, mounts static assets, constructs shared services, registers route factories, and owns lifespan startup and shutdown. [`src/app_initializer.py`](../../src/app_initializer.py) prepares application state; [`core/`](../../core/) provides persistence, authentication, middleware, sessions, and platform helpers. -**Canonical code locations.** app.py; src/app_initializer.py; core/database.py; core/auth.py; core/middleware.py; routes/. +- **Canonical code:** [`app.py`](../../app.py); [`src/app_initializer.py`](../../src/app_initializer.py); [`core/database.py`](../../core/database.py); [`core/auth.py`](../../core/auth.py); [`core/middleware.py`](../../core/middleware.py); [`routes/`](../../routes/). -**Confirmed problems.** None recorded by this mapping. +- **Known problems:** None recorded by this mapping. -**Open questions.** Which component currently owns startup and shutdown for each long-lived service? +- **Open question:** Which component currently owns startup and shutdown for each long-lived service? ## Frontend shell and browser interaction -**Current implementation.** static/index.html is served by the root and SPA deep-link routes in app.py; static/app.js, static/style.css, and static/js/ implement the client surface. +- **How it works:** [`static/index.html`](../../static/index.html) is served by the root and SPA deep-link routes in [`app.py`](../../app.py); [`static/app.js`](../../static/app.js), [`static/style.css`](../../static/style.css), and [`static/js/`](../../static/js/) implement the client surface. -**Canonical code locations.** static/index.html; static/app.js; static/js/; static/style.css; app.py deep-link handlers. +- **Canonical code:** [`static/index.html`](../../static/index.html); [`static/app.js`](../../static/app.js); [`static/js/`](../../static/js/); [`static/style.css`](../../static/style.css); [`app.py`](../../app.py) deep-link handlers. -**Confirmed problems.** The /backgrounds route calls serve_html_with_nonce for static/backgrounds.html, but that file is absent from static/. This is a confirmed broken prototype route, not evidence about the rest of the frontend. +- **Known problems:** The `/backgrounds` route in [`app.py`](../../app.py) calls `serve_html_with_nonce` for `static/backgrounds.html`, but that file is absent from [`static/`](../../static/). This is a confirmed broken prototype route, not evidence about the rest of the frontend. -**Open questions.** Is /backgrounds currently an intentionally supported route or an obsolete prototype? +- **Open question:** Is `/backgrounds` currently an intentionally supported route or an obsolete prototype? ## Chat, sessions, and streaming -**Current implementation.** routes/chat_routes.py and routes/chat_helpers.py coordinate requests, session state, and SSE delivery. src/chat_handler.py, src/chat_processor.py, src/llm_core.py, and src/session_actions.py provide message preparation, provider interaction, and session operations. +- **How it works:** [`routes/chat_routes.py`](../../routes/chat_routes.py) and [`routes/chat_helpers.py`](../../routes/chat_helpers.py) coordinate requests, session state, and SSE delivery. [`src/chat_handler.py`](../../src/chat_handler.py), [`src/chat_processor.py`](../../src/chat_processor.py), [`src/llm_core.py`](../../src/llm_core.py), and [`src/session_actions.py`](../../src/session_actions.py) provide message preparation, provider interaction, and session operations. -**Canonical code locations.** routes/chat_routes.py; routes/chat_helpers.py; routes/session_routes.py; src/chat_handler.py; src/chat_processor.py; src/llm_core.py; core/session_manager.py. +- **Canonical code:** [`routes/chat_routes.py`](../../routes/chat_routes.py); [`routes/chat_helpers.py`](../../routes/chat_helpers.py); [`routes/session_routes.py`](../../routes/session_routes.py); [`src/chat_handler.py`](../../src/chat_handler.py); [`src/chat_processor.py`](../../src/chat_processor.py); [`src/llm_core.py`](../../src/llm_core.py); [`core/session_manager.py`](../../core/session_manager.py). -**Confirmed problems.** src/agent_loop.py annotates _resolved_tool_event_name with Any but imports no Any and does not enable postponed annotation evaluation. Python evaluates that annotation while importing the module, so this is an import-time defect at the checked baseline. +- **Known problems:** [`src/agent_loop.py`](../../src/agent_loop.py) annotates `_resolved_tool_event_name` with `Any` but imports no `Any` and does not enable postponed annotation evaluation. Python evaluates that annotation while importing the module, so this is an import-time defect at the checked baseline. -**Open questions.** No end-to-end provider or browser streaming run was performed for this map. +- **Open question:** No end-to-end provider or browser streaming run was performed for this map. ## Agents, tools, and execution -**Current implementation.** src/agent_loop.py drives multi-round tool use. src/tool_execution.py dispatches calls and binds workspace context. src/agent_tools/ contains individual implementations; src/tool_security.py and src/tool_policy.py apply role and request policies. Long-running command work is represented by src/bg_jobs.py. +- **How it works:** [`src/agent_loop.py`](../../src/agent_loop.py) drives multi-round tool use. [`src/tool_execution.py`](../../src/tool_execution.py) dispatches calls and binds workspace context. [`src/agent_tools/`](../../src/agent_tools/) contains individual implementations; [`src/tool_security.py`](../../src/tool_security.py) and [`src/tool_policy.py`](../../src/tool_policy.py) apply role and request policies. Long-running command work is represented by [`src/bg_jobs.py`](../../src/bg_jobs.py). -**Canonical code locations.** src/agent_loop.py; src/tool_execution.py; src/agent_tools/; src/tool_security.py; src/tool_policy.py; src/tool_schemas.py; src/bg_jobs.py. +- **Canonical code:** [`src/agent_loop.py`](../../src/agent_loop.py); [`src/tool_execution.py`](../../src/tool_execution.py); [`src/agent_tools/`](../../src/agent_tools/); [`src/tool_security.py`](../../src/tool_security.py); [`src/tool_policy.py`](../../src/tool_policy.py); [`src/tool_schemas.py`](../../src/tool_schemas.py); [`src/bg_jobs.py`](../../src/bg_jobs.py). -**Confirmed problems.** The import-time annotation defect above blocks the main agent/tool path. The shell is intentionally not a filesystem or network sandbox; that is an authority boundary, not by itself a vulnerability claim. +- **Known problems:** The import-time annotation defect above blocks the main agent/tool path. The shell is intentionally not a filesystem or network sandbox; that is an authority boundary, not by itself a vulnerability claim. -**Open questions.** Which native, legacy, and MCP-qualified invocation paths reach each policy gate? +- **Open question:** Which native, legacy, and MCP-qualified invocation paths reach each policy gate? ## Models, providers, and local serving -**Current implementation.** Model routes delegate to discovery, capabilities, endpoint resolution, and LLM core modules. Cookbook routes and hardware-fit services handle model lifecycle and local-serving support. +- **How it works:** Model routes delegate to discovery, capabilities, endpoint resolution, and LLM core modules. Cookbook routes and hardware-fit services handle model lifecycle and local-serving support. -**Canonical code locations.** routes/model_routes.py; src/model_discovery.py; src/model_capabilities.py; src/endpoint_resolver.py; src/llm_core.py; routes/cookbook_routes.py; src/cookbook_serve_lifecycle.py; services/hwfit/. +- **Canonical code:** [`routes/model_routes.py`](../../routes/model_routes.py); [`src/model_discovery.py`](../../src/model_discovery.py); [`src/model_capabilities.py`](../../src/model_capabilities.py); [`src/endpoint_resolver.py`](../../src/endpoint_resolver.py); [`src/llm_core.py`](../../src/llm_core.py); [`routes/cookbook_routes.py`](../../routes/cookbook_routes.py); [`src/cookbook_serve_lifecycle.py`](../../src/cookbook_serve_lifecycle.py); [`services/hwfit/`](../../services/hwfit/). -**Confirmed problems.** None recorded by this mapping. +- **Known problems:** None recorded by this mapping. -**Open questions.** Which endpoint inputs are administrator-created and permitted to use private provider addresses? +- **Open question:** Which endpoint inputs are administrator-created and permitted to use private provider addresses? ## Search and research -**Current implementation.** HTTP search routes use services/search/; research is exposed through routes/research/ and implemented in services/research/, src/deep_research.py, and related helpers. src/search/ remains an import-compatibility layer for callers not yet moved to services.search. +- **How it works:** HTTP search routes use [`services/search/`](../../services/search/); research is exposed through [`routes/research/`](../../routes/research/) and implemented in [`services/research/`](../../services/research/), [`src/deep_research.py`](../../src/deep_research.py), and related helpers. [`src/search/`](../../src/search/) remains an import-compatibility layer for callers not yet moved to `services.search`. -**Canonical code locations.** routes/search_routes.py; services/search/; routes/research/research_routes.py; services/research/; src/deep_research.py; src/search/. +- **Canonical code:** [`routes/search_routes.py`](../../routes/search_routes.py); [`services/search/`](../../services/search/); [`routes/research/research_routes.py`](../../routes/research/research_routes.py); [`services/research/`](../../services/research/); [`src/deep_research.py`](../../src/deep_research.py); [`src/search/`](../../src/search/). -**Confirmed problems.** The two search package paths are a confirmed compatibility arrangement, not two independent canonical implementations: several src/search modules forward to services.search. The remaining wrappers and compatibility imports can drift. +- **Known problems:** The two search package paths are a confirmed compatibility arrangement, not two independent canonical implementations: several [`src/search/`](../../src/search/) modules forward to [`services/search/`](../../services/search/). The remaining wrappers and compatibility imports can drift. -**Open questions.** No live provider request was made; provider configuration and network access remain unverified. +- **Open question:** No live provider request was made; provider configuration and network access remain unverified. ## Documents, retrieval, and personal knowledge -**Current implementation.** Document routes coordinate upload handling, document processing, and editor actions. Personal-document and RAG modules use Chroma and embedding clients. PDF viewing uses the optional-dependency loader in src/pdf_runtime.py; form extraction and filling live separately in src/pdf_forms.py and src/pdf_form_doc.py. +- **How it works:** Document routes coordinate upload handling, document processing, and editor actions. Personal-document and RAG modules use Chroma and embedding clients. PDF viewing uses the optional-dependency loader in [`src/pdf_runtime.py`](../../src/pdf_runtime.py); form extraction and filling live separately in [`src/pdf_forms.py`](../../src/pdf_forms.py) and [`src/pdf_form_doc.py`](../../src/pdf_form_doc.py). -**Canonical code locations.** routes/document_routes.py; src/upload_handler.py; src/document_processor.py; src/document_actions.py; src/personal_docs.py; src/rag_manager.py; src/embeddings.py; src/pdf_runtime.py; src/pdf_forms.py; src/pdf_form_doc.py. +- **Canonical code:** [`routes/document_routes.py`](../../routes/document_routes.py); [`src/upload_handler.py`](../../src/upload_handler.py); [`src/document_processor.py`](../../src/document_processor.py); [`src/document_actions.py`](../../src/document_actions.py); [`src/personal_docs.py`](../../src/personal_docs.py); [`src/rag_manager.py`](../../src/rag_manager.py); [`src/embeddings.py`](../../src/embeddings.py); [`src/pdf_runtime.py`](../../src/pdf_runtime.py); [`src/pdf_forms.py`](../../src/pdf_forms.py); [`src/pdf_form_doc.py`](../../src/pdf_form_doc.py). -**Confirmed problems.** PDF viewing/runtime loading and PDF form processing are separate implementations. That separation is confirmed and intentional in the source; it is not a defect without a reported behavioural failure. +- **Known problems:** PDF viewing/runtime loading and PDF form processing are separate implementations. That separation is confirmed and intentional in the source; it is not a defect without a reported behavioural failure. -**Open questions.** Optional PDF dependencies and representative uploaded documents were not exercised. +- **Open question:** Optional PDF dependencies and representative uploaded documents were not exercised. ## Memory and skills -**Current implementation.** Memory routes use services/memory/ and vector helpers. Skills are exposed through routes/skills_routes.py, stored and managed in services/memory/skills.py, and may be imported through services/memory/skill_importer.py. +- **How it works:** Memory routes use [`services/memory/`](../../services/memory/) and vector helpers. Skills are exposed through [`routes/skills_routes.py`](../../routes/skills_routes.py), stored and managed in [`services/memory/skills.py`](../../services/memory/skills.py), and may be imported through [`services/memory/skill_importer.py`](../../services/memory/skill_importer.py). -**Canonical code locations.** routes/memory/memory_routes.py; services/memory/; src/memory.py; src/memory_vector.py; routes/skills_routes.py; services/memory/skills.py; services/memory/skill_importer.py. +- **Canonical code:** [`routes/memory/memory_routes.py`](../../routes/memory/memory_routes.py); [`services/memory/`](../../services/memory/); [`src/memory.py`](../../src/memory.py); [`src/memory_vector.py`](../../src/memory_vector.py); [`routes/skills_routes.py`](../../routes/skills_routes.py); [`services/memory/skills.py`](../../services/memory/skills.py); [`services/memory/skill_importer.py`](../../services/memory/skill_importer.py). -**Confirmed problems.** None recorded by this mapping. +- **Known problems:** None recorded by this mapping. -**Open questions.** Which imported skill content can reach execution-capable paths, and which validation occurs before that point? +- **Open question:** Which imported skill content can reach execution-capable paths, and which validation occurs before that point? -## Personal services: email, calendar, contacts, notes, and tasks +## Email, calendar, contacts, notes, and tasks -**Current implementation.** Dedicated route modules own email, CalDAV calendar, CardDAV contacts, notes, and tasks. Supporting modules include email helpers and pollers, CalDAV sync and writeback, and the task scheduler. +- **How it works:** Dedicated route modules own email, CalDAV calendar, CardDAV contacts, notes, and tasks. Supporting modules include email helpers and pollers, CalDAV sync and writeback, and the task scheduler. -**Canonical code locations.** routes/email_routes.py; routes/calendar_routes.py; routes/contacts/contacts_routes.py; routes/note/note_routes.py; routes/task_routes.py; routes/assistant_routes.py; src/caldav_sync.py; src/caldav_writeback.py; src/task_scheduler.py. +- **Canonical code:** [`routes/email_routes.py`](../../routes/email_routes.py); [`routes/calendar_routes.py`](../../routes/calendar_routes.py); [`routes/contacts/contacts_routes.py`](../../routes/contacts/contacts_routes.py); [`routes/note/note_routes.py`](../../routes/note/note_routes.py); [`routes/task_routes.py`](../../routes/task_routes.py); [`routes/assistant_routes.py`](../../routes/assistant_routes.py); [`src/caldav_sync.py`](../../src/caldav_sync.py); [`src/caldav_writeback.py`](../../src/caldav_writeback.py); [`src/task_scheduler.py`](../../src/task_scheduler.py). -**Confirmed problems.** None recorded by this mapping. +- **Known problems:** None recorded by this mapping. -**Open questions.** External account behaviour, writeback, and delivery require controlled credentials and are not runtime-validated here. +- **Open question:** External account behaviour, writeback, and delivery require controlled credentials and are not runtime-validated here. ## Media, speech, and image work -**Current implementation.** Gallery and image routes coordinate media features. Service modules own speech and media integrations; src/generated_images.py and src/visual_report.py support artifact handling and presentation. +- **How it works:** Gallery and image routes coordinate media features. Service modules own speech and media integrations; [`src/generated_images.py`](../../src/generated_images.py) and [`src/visual_report.py`](../../src/visual_report.py) support artifact handling and presentation. -**Canonical code locations.** routes/gallery/gallery_routes.py; routes/stt_routes.py; routes/tts_routes.py; src/generated_images.py; services/stt/; services/tts/; services/faces/; src/visual_report.py. +- **Canonical code:** [`routes/gallery/gallery_routes.py`](../../routes/gallery/gallery_routes.py); [`routes/stt_routes.py`](../../routes/stt_routes.py); [`routes/tts_routes.py`](../../routes/tts_routes.py); [`src/generated_images.py`](../../src/generated_images.py); [`services/stt/`](../../services/stt/); [`services/tts/`](../../services/tts/); [`services/faces/`](../../services/faces/); [`src/visual_report.py`](../../src/visual_report.py). -**Confirmed problems.** None recorded by this mapping. +- **Known problems:** None recorded by this mapping. -**Open questions.** Hardware- and provider-dependent media workflows were not exercised. +- **Open question:** Hardware- and provider-dependent media workflows were not exercised. ## Authentication, secrets, and privileged administration -**Current implementation.** core/auth.py and core/middleware.py provide identity and request gates. src/secret_storage.py encrypts application-managed database secrets with a local Fernet key. Vault handling is separate: routes/vault_routes.py and src/tools/vault.py invoke the Bitwarden CLI and persist its session data in the application data area. +- **How it works:** [`core/auth.py`](../../core/auth.py) and [`core/middleware.py`](../../core/middleware.py) provide identity and request gates. [`src/secret_storage.py`](../../src/secret_storage.py) encrypts application-managed database secrets with a local Fernet key. Vault handling is separate: [`routes/vault_routes.py`](../../routes/vault_routes.py) and [`src/tools/vault.py`](../../src/tools/vault.py) invoke the Bitwarden CLI and persist its session data in the application data area. -**Canonical code locations.** core/auth.py; core/middleware.py; routes/auth_routes.py; routes/api_token_routes.py; src/secret_storage.py; routes/vault_routes.py; src/tools/vault.py; routes/admin_wipe/admin_wipe_routes.py. +- **Canonical code:** [`core/auth.py`](../../core/auth.py); [`core/middleware.py`](../../core/middleware.py); [`routes/auth_routes.py`](../../routes/auth_routes.py); [`routes/api_token_routes.py`](../../routes/api_token_routes.py); [`src/secret_storage.py`](../../src/secret_storage.py); [`routes/vault_routes.py`](../../routes/vault_routes.py); [`src/tools/vault.py`](../../src/tools/vault.py); [`routes/admin_wipe/admin_wipe_routes.py`](../../routes/admin_wipe/admin_wipe_routes.py). -**Confirmed problems.** Vault-command handling and local application secret storage are distinct paths with different storage mechanisms. This is a source-confirmed boundary, not evidence that either path is compromised. +- **Known problems:** Vault-command handling and local application secret storage are distinct paths with different storage mechanisms. This is a source-confirmed boundary, not evidence that either path is compromised. -**Open questions.** What are the current confidentiality, ownership, rotation, and backup semantics for vault session data? +- **Open question:** What are the current confidentiality, ownership, rotation, and backup semantics for vault session data? ## Persistence, background work, and operations -**Current implementation.** SQLite models and persistence are centred in core/database.py; managers use application data paths. The scheduler and background-job monitor can continue work outside a live browser request. Operational routes cover cleanup, backup, and administrative wipe; the repository also provides a backup script and user documentation. +- **How it works:** SQLite models and persistence are centred in [`core/database.py`](../../core/database.py); managers use application data paths. The scheduler and background-job monitor can continue work outside a live browser request. Operational routes cover cleanup, backup, and administrative wipe; the repository also provides a backup script and user documentation. -**Canonical code locations.** core/database.py; src/runtime_paths.py; src/task_scheduler.py; src/bg_jobs.py; src/bg_monitor.py; routes/backup_routes.py; routes/cleanup/cleanup_routes.py; routes/admin_wipe/admin_wipe_routes.py; scripts/odysseus-backup; docs/backup-restore.md. +- **Canonical code:** [`core/database.py`](../../core/database.py); [`src/runtime_paths.py`](../../src/runtime_paths.py); [`src/task_scheduler.py`](../../src/task_scheduler.py); [`src/bg_jobs.py`](../../src/bg_jobs.py); [`src/bg_monitor.py`](../../src/bg_monitor.py); [`routes/backup_routes.py`](../../routes/backup_routes.py); [`routes/cleanup/cleanup_routes.py`](../../routes/cleanup/cleanup_routes.py); [`routes/admin_wipe/admin_wipe_routes.py`](../../routes/admin_wipe/admin_wipe_routes.py); [`scripts/odysseus-backup`](../../scripts/odysseus-backup); [`docs/backup-restore.md`](../../docs/backup-restore.md). -**Confirmed problems.** None recorded by this mapping. +- **Known problems:** None recorded by this mapping. -**Open questions.** What current behaviour applies to background execution, cancellation, retries, and authority inheritance? +- **Open question:** What current behaviour applies to background execution, cancellation, retries, and authority inheritance? From 69041b5f50b845231417bd79a2d5afccd2d9bad3 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <alexandremagteixeira@gmail.com> Date: Sun, 26 Jul 2026 19:13:13 +0100 Subject: [PATCH 004/125] docs: correct confirmed stale guidance --- THREAT_MODEL.md | 10 +++++----- docs/setup.md | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 48665a61d..ec10cd105 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -68,14 +68,14 @@ External content that reaches the LLM is treated as untrusted via `src/prompt_se - `X-Content-Type-Options: nosniff` and `Referrer-Policy: no-referrer` everywhere. - **CSP:** nonce-based `script-src 'self' 'nonce-{nonce}' https://cdn.jsdelivr.net`. `style-src 'unsafe-inline'` is intentionally kept — `static/index.html` ships inline `<style>` blocks and JS modules set `style=""` attributes at runtime. Inline styles do not execute script so the risk is visual-only. Removing this requires templating the HTML files and auditing all JS-set style attributes. +## Token-Supplied Model Endpoints + +Direct `/api/v1/chat` requests with a token-supplied `base_url` must use a public HTTP(S) endpoint. This restriction applies only to untrusted direct values; administrator-configured endpoints may intentionally use local or LAN URLs for private model providers. + ## Known Gaps These are open, acknowledged, and contributor help is welcome: 1. **No shell/filesystem sandbox.** The agent `bash` and `read_file`/`write_file` tools run as the app process user with no network egress filtering or filesystem confinement. A successful prompt-injection reaching a shell-enabled admin session can make outbound requests to internal services. See #1058 for the sandbox proposal. -2. **SSRF via `/api/v1/chat` `base_url` parameter.** A chat-scoped API token can supply an arbitrary `base_url`; the server forwards the LLM request to that host without validating the scheme or address. PR #1039 fixes this. - -3. **`src/search/` partial consolidation.** `src.search.core` and `src.search.providers` correctly alias `services.search` via `sys.modules` replacement. `analytics`, `cache`, `content`, `query`, and `ranking` are still independent copies that can drift. The SSRF regression tests in `tests/test_webhook_ssrf_resilience.py` test `src.webhook_manager` directly (separate from search), so the safety net there is intact. See #1058. - -4. **Token scopes are coarse.** There is no way to grant a session a subset of the owning user's privileges. Companion/mobile tokens carry either `chat` or `admin` scope with no per-capability granularity. +2. **Token scopes are coarse.** There is no way to grant a session a subset of the owning user's privileges. Companion/mobile tokens carry either `chat` or `admin` scope with no per-capability granularity. diff --git a/docs/setup.md b/docs/setup.md index 53a6fb28c..61381461b 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -441,7 +441,7 @@ uv pip sync requirements.lock # reproduce it exactly la ### Outlook / Office 365 email Odysseus email accounts currently use IMAP/SMTP username-password auth. Outlook and Microsoft 365 generally require OAuth instead, so normal Microsoft mailbox -passwords will fail. See [docs/email-outlook.md](docs/email-outlook.md) for the +passwords will fail. See [email-outlook.md](email-outlook.md) for the current limitation and the planned integration direction. ## Security Notes From ab6f3fa70b7f9dc2863ed199ed710c34582cdf13 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <alexandremagteixeira@gmail.com> Date: Sun, 26 Jul 2026 19:57:49 +0100 Subject: [PATCH 005/125] docs(discovery): clarify non-canonical evidence role --- specs/discovery/README.md | 23 ++++++++++++----------- specs/discovery/safety-boundaries.md | 2 +- specs/discovery/system-map.md | 28 ++++++++++++++-------------- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/specs/discovery/README.md b/specs/discovery/README.md index 166ed3656..4343f9ec7 100644 --- a/specs/discovery/README.md +++ b/specs/discovery/README.md @@ -1,33 +1,34 @@ # Odysseus discovery maps -Compact, code-grounded maps of the cross-cutting systems in the checked-in Odysseus codebase. They are not a feature certification or substitute for normal testing, and do not become a permanent audit system. +Compact, code-grounded discovery maps of cross-cutting systems in the checked-in Odysseus codebase. They preserve investigation context and open factual questions; they are not canonical subsystem specifications, a feature certification, or a substitute for normal testing. > [!IMPORTANT] -> Code is the source of truth for current behaviour. These specifications describe only the checked-in system and its current boundaries; keep them synchronized with relevant code changes. +> Checked-in code is the source of truth for current behaviour. Mature subsystem specifications, where they exist, are the canonical documentation of accepted subsystem behaviour. Check code, tests, and configuration before reconciling a discovery finding. Discovery remains non-canonical. ## Explore the maps | Document | Purpose | |---|---| -| [Current system map](system-map.md) | Explains the current subsystem boundaries, implementation locations, confirmed problems, and factual open questions. | -| [Safety boundaries](safety-boundaries.md) | Maps broad authority, safeguards, confirmed risks or gaps, and unverified behaviour. | +| [Current system map](system-map.md) | Records evidence locations, confirmed local observations, and factual open questions about subsystem boundaries. | +| [Safety boundaries](safety-boundaries.md) | Records evidence about broad authority, safeguards, confirmed risks or gaps, and unverified behaviour. | ## Working rules - **Trace the code first.** Confirm the current path in source before recording a claim. -- **Keep specs current-state only.** Do not record intentions, design direction, refactor plans, or decision history here. -- **Synchronize with code.** Update this package when a code change alters a documented cross-cutting system, authority boundary, or canonical implementation location. +- **Promote selectively.** When an owning mature specification exists, add a fact only when it is verified, useful, and not already represented there. +- **Record missing ownership.** When no owning specification exists, retain the verified finding in discovery and record missing documentation ownership as a follow-up. +- **Retain uncertainty here.** Keep unresolved questions and useful investigation context in discovery rather than treating them as canonical truth. +- **Keep specifications current-state only.** Do not record intentions, design direction, refactor plans, decision history, priority, ownership, or sequencing here. - **Investigate with cause.** Do not exhaustively revalidate existing functionality without a report, visible failure, relevant change, or high-authority review need. -- **Keep planning in Plane.** Record confirmed problems in the relevant project within the canonical Plane workspace; keep high-level design, prioritization, ownership, and refactor discussion there. - **Review authority carefully.** Give execution, data access, external tools, credentials, destructive operations, and unattended work focused review. - **Use stable locations.** Cite modules, routes, classes, and functions instead of fragile line ranges or generated evidence tables. -## Working flow +## Reconciliation flow 1. Start with the relevant map and trace the cited code. -2. For a defect, create or update the relevant project within the canonical Plane workspace with a reproducible report and ownership. -3. Use a Plane thread for design, prioritization, ownership, or refactor discussion. -4. After implementation changes the code, update the affected map to describe the resulting current state. +2. Classify the finding against current source evidence and an owning mature specification where one exists. +3. Promote only verified, useful facts that are missing from an existing owning specification. +4. When no owning specification exists, retain the verified finding here and record missing documentation ownership as a follow-up; otherwise retain unresolved context here and correct stale wording. > [!NOTE] > This package intentionally contains no generator, validator, maturity scale, feature database, or parallel work tracker. The [architecture runtime inventory](../architecture-runtime-inventory.md) remains useful structural context, but is an explicitly draft snapshot. diff --git a/specs/discovery/safety-boundaries.md b/specs/discovery/safety-boundaries.md index ee8b67d98..4340aa8d5 100644 --- a/specs/discovery/safety-boundaries.md +++ b/specs/discovery/safety-boundaries.md @@ -1,7 +1,7 @@ # Safety boundaries > [!IMPORTANT] -> Broad authority does not by itself establish a vulnerability. This map records code-grounded safeguards, confirmed risks or gaps, and unverified behaviour. No destructive test, external connection, or real credential was used for this map. +> This non-canonical discovery map records code-grounded safeguards, confirmed risks or gaps, and unverified behaviour. Broad authority does not by itself establish a vulnerability. Verify the cited source before relying on a finding. No destructive test, external connection, or real credential was used for this map. ## Navigate the boundaries diff --git a/specs/discovery/system-map.md b/specs/discovery/system-map.md index 2f9f50eaa..ce65e2b02 100644 --- a/specs/discovery/system-map.md +++ b/specs/discovery/system-map.md @@ -1,7 +1,7 @@ # Current system map > [!NOTE] -> This map describes the checked-in code at the same commit as this document. It is a subsystem map, not an exhaustive feature catalog or runtime certification. Each section records only current implementation, canonical locations, confirmed current problems, and unresolved factual questions. +> This non-canonical discovery map is an evidence guide, not an exhaustive feature catalog or runtime certification. Verify the cited source before relying on a finding. Each section records local implementation observations, evidence locations, confirmed current problems, and unresolved factual questions. ## Navigate the system @@ -22,7 +22,7 @@ - **How it works:** [`app.py`](../../app.py) creates the application, mounts static assets, constructs shared services, registers route factories, and owns lifespan startup and shutdown. [`src/app_initializer.py`](../../src/app_initializer.py) prepares application state; [`core/`](../../core/) provides persistence, authentication, middleware, sessions, and platform helpers. -- **Canonical code:** [`app.py`](../../app.py); [`src/app_initializer.py`](../../src/app_initializer.py); [`core/database.py`](../../core/database.py); [`core/auth.py`](../../core/auth.py); [`core/middleware.py`](../../core/middleware.py); [`routes/`](../../routes/). +- **Evidence locations:** [`app.py`](../../app.py); [`src/app_initializer.py`](../../src/app_initializer.py); [`core/database.py`](../../core/database.py); [`core/auth.py`](../../core/auth.py); [`core/middleware.py`](../../core/middleware.py); [`routes/`](../../routes/). - **Known problems:** None recorded by this mapping. @@ -32,7 +32,7 @@ - **How it works:** [`static/index.html`](../../static/index.html) is served by the root and SPA deep-link routes in [`app.py`](../../app.py); [`static/app.js`](../../static/app.js), [`static/style.css`](../../static/style.css), and [`static/js/`](../../static/js/) implement the client surface. -- **Canonical code:** [`static/index.html`](../../static/index.html); [`static/app.js`](../../static/app.js); [`static/js/`](../../static/js/); [`static/style.css`](../../static/style.css); [`app.py`](../../app.py) deep-link handlers. +- **Evidence locations:** [`static/index.html`](../../static/index.html); [`static/app.js`](../../static/app.js); [`static/js/`](../../static/js/); [`static/style.css`](../../static/style.css); [`app.py`](../../app.py) deep-link handlers. - **Known problems:** The `/backgrounds` route in [`app.py`](../../app.py) calls `serve_html_with_nonce` for `static/backgrounds.html`, but that file is absent from [`static/`](../../static/). This is a confirmed broken prototype route, not evidence about the rest of the frontend. @@ -42,7 +42,7 @@ - **How it works:** [`routes/chat_routes.py`](../../routes/chat_routes.py) and [`routes/chat_helpers.py`](../../routes/chat_helpers.py) coordinate requests, session state, and SSE delivery. [`src/chat_handler.py`](../../src/chat_handler.py), [`src/chat_processor.py`](../../src/chat_processor.py), [`src/llm_core.py`](../../src/llm_core.py), and [`src/session_actions.py`](../../src/session_actions.py) provide message preparation, provider interaction, and session operations. -- **Canonical code:** [`routes/chat_routes.py`](../../routes/chat_routes.py); [`routes/chat_helpers.py`](../../routes/chat_helpers.py); [`routes/session_routes.py`](../../routes/session_routes.py); [`src/chat_handler.py`](../../src/chat_handler.py); [`src/chat_processor.py`](../../src/chat_processor.py); [`src/llm_core.py`](../../src/llm_core.py); [`core/session_manager.py`](../../core/session_manager.py). +- **Evidence locations:** [`routes/chat_routes.py`](../../routes/chat_routes.py); [`routes/chat_helpers.py`](../../routes/chat_helpers.py); [`routes/session_routes.py`](../../routes/session_routes.py); [`src/chat_handler.py`](../../src/chat_handler.py); [`src/chat_processor.py`](../../src/chat_processor.py); [`src/llm_core.py`](../../src/llm_core.py); [`core/session_manager.py`](../../core/session_manager.py). - **Known problems:** [`src/agent_loop.py`](../../src/agent_loop.py) annotates `_resolved_tool_event_name` with `Any` but imports no `Any` and does not enable postponed annotation evaluation. Python evaluates that annotation while importing the module, so this is an import-time defect at the checked baseline. @@ -52,7 +52,7 @@ - **How it works:** [`src/agent_loop.py`](../../src/agent_loop.py) drives multi-round tool use. [`src/tool_execution.py`](../../src/tool_execution.py) dispatches calls and binds workspace context. [`src/agent_tools/`](../../src/agent_tools/) contains individual implementations; [`src/tool_security.py`](../../src/tool_security.py) and [`src/tool_policy.py`](../../src/tool_policy.py) apply role and request policies. Long-running command work is represented by [`src/bg_jobs.py`](../../src/bg_jobs.py). -- **Canonical code:** [`src/agent_loop.py`](../../src/agent_loop.py); [`src/tool_execution.py`](../../src/tool_execution.py); [`src/agent_tools/`](../../src/agent_tools/); [`src/tool_security.py`](../../src/tool_security.py); [`src/tool_policy.py`](../../src/tool_policy.py); [`src/tool_schemas.py`](../../src/tool_schemas.py); [`src/bg_jobs.py`](../../src/bg_jobs.py). +- **Evidence locations:** [`src/agent_loop.py`](../../src/agent_loop.py); [`src/tool_execution.py`](../../src/tool_execution.py); [`src/agent_tools/`](../../src/agent_tools/); [`src/tool_security.py`](../../src/tool_security.py); [`src/tool_policy.py`](../../src/tool_policy.py); [`src/tool_schemas.py`](../../src/tool_schemas.py); [`src/bg_jobs.py`](../../src/bg_jobs.py). - **Known problems:** The import-time annotation defect above blocks the main agent/tool path. The shell is intentionally not a filesystem or network sandbox; that is an authority boundary, not by itself a vulnerability claim. @@ -62,7 +62,7 @@ - **How it works:** Model routes delegate to discovery, capabilities, endpoint resolution, and LLM core modules. Cookbook routes and hardware-fit services handle model lifecycle and local-serving support. -- **Canonical code:** [`routes/model_routes.py`](../../routes/model_routes.py); [`src/model_discovery.py`](../../src/model_discovery.py); [`src/model_capabilities.py`](../../src/model_capabilities.py); [`src/endpoint_resolver.py`](../../src/endpoint_resolver.py); [`src/llm_core.py`](../../src/llm_core.py); [`routes/cookbook_routes.py`](../../routes/cookbook_routes.py); [`src/cookbook_serve_lifecycle.py`](../../src/cookbook_serve_lifecycle.py); [`services/hwfit/`](../../services/hwfit/). +- **Evidence locations:** [`routes/model_routes.py`](../../routes/model_routes.py); [`src/model_discovery.py`](../../src/model_discovery.py); [`src/model_capabilities.py`](../../src/model_capabilities.py); [`src/endpoint_resolver.py`](../../src/endpoint_resolver.py); [`src/llm_core.py`](../../src/llm_core.py); [`routes/cookbook_routes.py`](../../routes/cookbook_routes.py); [`src/cookbook_serve_lifecycle.py`](../../src/cookbook_serve_lifecycle.py); [`services/hwfit/`](../../services/hwfit/). - **Known problems:** None recorded by this mapping. @@ -72,9 +72,9 @@ - **How it works:** HTTP search routes use [`services/search/`](../../services/search/); research is exposed through [`routes/research/`](../../routes/research/) and implemented in [`services/research/`](../../services/research/), [`src/deep_research.py`](../../src/deep_research.py), and related helpers. [`src/search/`](../../src/search/) remains an import-compatibility layer for callers not yet moved to `services.search`. -- **Canonical code:** [`routes/search_routes.py`](../../routes/search_routes.py); [`services/search/`](../../services/search/); [`routes/research/research_routes.py`](../../routes/research/research_routes.py); [`services/research/`](../../services/research/); [`src/deep_research.py`](../../src/deep_research.py); [`src/search/`](../../src/search/). +- **Evidence locations:** [`routes/search_routes.py`](../../routes/search_routes.py); [`services/search/`](../../services/search/); [`routes/research/research_routes.py`](../../routes/research/research_routes.py); [`services/research/`](../../services/research/); [`src/deep_research.py`](../../src/deep_research.py); [`src/search/`](../../src/search/). -- **Known problems:** The two search package paths are a confirmed compatibility arrangement, not two independent canonical implementations: several [`src/search/`](../../src/search/) modules forward to [`services/search/`](../../services/search/). The remaining wrappers and compatibility imports can drift. +- **Known problems:** None recorded by this mapping. - **Open question:** No live provider request was made; provider configuration and network access remain unverified. @@ -82,7 +82,7 @@ - **How it works:** Document routes coordinate upload handling, document processing, and editor actions. Personal-document and RAG modules use Chroma and embedding clients. PDF viewing uses the optional-dependency loader in [`src/pdf_runtime.py`](../../src/pdf_runtime.py); form extraction and filling live separately in [`src/pdf_forms.py`](../../src/pdf_forms.py) and [`src/pdf_form_doc.py`](../../src/pdf_form_doc.py). -- **Canonical code:** [`routes/document_routes.py`](../../routes/document_routes.py); [`src/upload_handler.py`](../../src/upload_handler.py); [`src/document_processor.py`](../../src/document_processor.py); [`src/document_actions.py`](../../src/document_actions.py); [`src/personal_docs.py`](../../src/personal_docs.py); [`src/rag_manager.py`](../../src/rag_manager.py); [`src/embeddings.py`](../../src/embeddings.py); [`src/pdf_runtime.py`](../../src/pdf_runtime.py); [`src/pdf_forms.py`](../../src/pdf_forms.py); [`src/pdf_form_doc.py`](../../src/pdf_form_doc.py). +- **Evidence locations:** [`routes/document_routes.py`](../../routes/document_routes.py); [`src/upload_handler.py`](../../src/upload_handler.py); [`src/document_processor.py`](../../src/document_processor.py); [`src/document_actions.py`](../../src/document_actions.py); [`src/personal_docs.py`](../../src/personal_docs.py); [`src/rag_manager.py`](../../src/rag_manager.py); [`src/embeddings.py`](../../src/embeddings.py); [`src/pdf_runtime.py`](../../src/pdf_runtime.py); [`src/pdf_forms.py`](../../src/pdf_forms.py); [`src/pdf_form_doc.py`](../../src/pdf_form_doc.py). - **Known problems:** PDF viewing/runtime loading and PDF form processing are separate implementations. That separation is confirmed and intentional in the source; it is not a defect without a reported behavioural failure. @@ -92,7 +92,7 @@ - **How it works:** Memory routes use [`services/memory/`](../../services/memory/) and vector helpers. Skills are exposed through [`routes/skills_routes.py`](../../routes/skills_routes.py), stored and managed in [`services/memory/skills.py`](../../services/memory/skills.py), and may be imported through [`services/memory/skill_importer.py`](../../services/memory/skill_importer.py). -- **Canonical code:** [`routes/memory/memory_routes.py`](../../routes/memory/memory_routes.py); [`services/memory/`](../../services/memory/); [`src/memory.py`](../../src/memory.py); [`src/memory_vector.py`](../../src/memory_vector.py); [`routes/skills_routes.py`](../../routes/skills_routes.py); [`services/memory/skills.py`](../../services/memory/skills.py); [`services/memory/skill_importer.py`](../../services/memory/skill_importer.py). +- **Evidence locations:** [`routes/memory/memory_routes.py`](../../routes/memory/memory_routes.py); [`services/memory/`](../../services/memory/); [`src/memory.py`](../../src/memory.py); [`src/memory_vector.py`](../../src/memory_vector.py); [`routes/skills_routes.py`](../../routes/skills_routes.py); [`services/memory/skills.py`](../../services/memory/skills.py); [`services/memory/skill_importer.py`](../../services/memory/skill_importer.py). - **Known problems:** None recorded by this mapping. @@ -102,7 +102,7 @@ - **How it works:** Dedicated route modules own email, CalDAV calendar, CardDAV contacts, notes, and tasks. Supporting modules include email helpers and pollers, CalDAV sync and writeback, and the task scheduler. -- **Canonical code:** [`routes/email_routes.py`](../../routes/email_routes.py); [`routes/calendar_routes.py`](../../routes/calendar_routes.py); [`routes/contacts/contacts_routes.py`](../../routes/contacts/contacts_routes.py); [`routes/note/note_routes.py`](../../routes/note/note_routes.py); [`routes/task_routes.py`](../../routes/task_routes.py); [`routes/assistant_routes.py`](../../routes/assistant_routes.py); [`src/caldav_sync.py`](../../src/caldav_sync.py); [`src/caldav_writeback.py`](../../src/caldav_writeback.py); [`src/task_scheduler.py`](../../src/task_scheduler.py). +- **Evidence locations:** [`routes/email_routes.py`](../../routes/email_routes.py); [`routes/calendar_routes.py`](../../routes/calendar_routes.py); [`routes/contacts/contacts_routes.py`](../../routes/contacts/contacts_routes.py); [`routes/note/note_routes.py`](../../routes/note/note_routes.py); [`routes/task_routes.py`](../../routes/task_routes.py); [`routes/assistant_routes.py`](../../routes/assistant_routes.py); [`src/caldav_sync.py`](../../src/caldav_sync.py); [`src/caldav_writeback.py`](../../src/caldav_writeback.py); [`src/task_scheduler.py`](../../src/task_scheduler.py). - **Known problems:** None recorded by this mapping. @@ -112,7 +112,7 @@ - **How it works:** Gallery and image routes coordinate media features. Service modules own speech and media integrations; [`src/generated_images.py`](../../src/generated_images.py) and [`src/visual_report.py`](../../src/visual_report.py) support artifact handling and presentation. -- **Canonical code:** [`routes/gallery/gallery_routes.py`](../../routes/gallery/gallery_routes.py); [`routes/stt_routes.py`](../../routes/stt_routes.py); [`routes/tts_routes.py`](../../routes/tts_routes.py); [`src/generated_images.py`](../../src/generated_images.py); [`services/stt/`](../../services/stt/); [`services/tts/`](../../services/tts/); [`services/faces/`](../../services/faces/); [`src/visual_report.py`](../../src/visual_report.py). +- **Evidence locations:** [`routes/gallery/gallery_routes.py`](../../routes/gallery/gallery_routes.py); [`routes/stt_routes.py`](../../routes/stt_routes.py); [`routes/tts_routes.py`](../../routes/tts_routes.py); [`src/generated_images.py`](../../src/generated_images.py); [`services/stt/`](../../services/stt/); [`services/tts/`](../../services/tts/); [`services/faces/`](../../services/faces/); [`src/visual_report.py`](../../src/visual_report.py). - **Known problems:** None recorded by this mapping. @@ -122,7 +122,7 @@ - **How it works:** [`core/auth.py`](../../core/auth.py) and [`core/middleware.py`](../../core/middleware.py) provide identity and request gates. [`src/secret_storage.py`](../../src/secret_storage.py) encrypts application-managed database secrets with a local Fernet key. Vault handling is separate: [`routes/vault_routes.py`](../../routes/vault_routes.py) and [`src/tools/vault.py`](../../src/tools/vault.py) invoke the Bitwarden CLI and persist its session data in the application data area. -- **Canonical code:** [`core/auth.py`](../../core/auth.py); [`core/middleware.py`](../../core/middleware.py); [`routes/auth_routes.py`](../../routes/auth_routes.py); [`routes/api_token_routes.py`](../../routes/api_token_routes.py); [`src/secret_storage.py`](../../src/secret_storage.py); [`routes/vault_routes.py`](../../routes/vault_routes.py); [`src/tools/vault.py`](../../src/tools/vault.py); [`routes/admin_wipe/admin_wipe_routes.py`](../../routes/admin_wipe/admin_wipe_routes.py). +- **Evidence locations:** [`core/auth.py`](../../core/auth.py); [`core/middleware.py`](../../core/middleware.py); [`routes/auth_routes.py`](../../routes/auth_routes.py); [`routes/api_token_routes.py`](../../routes/api_token_routes.py); [`src/secret_storage.py`](../../src/secret_storage.py); [`routes/vault_routes.py`](../../routes/vault_routes.py); [`src/tools/vault.py`](../../src/tools/vault.py); [`routes/admin_wipe/admin_wipe_routes.py`](../../routes/admin_wipe/admin_wipe_routes.py). - **Known problems:** Vault-command handling and local application secret storage are distinct paths with different storage mechanisms. This is a source-confirmed boundary, not evidence that either path is compromised. @@ -132,7 +132,7 @@ - **How it works:** SQLite models and persistence are centred in [`core/database.py`](../../core/database.py); managers use application data paths. The scheduler and background-job monitor can continue work outside a live browser request. Operational routes cover cleanup, backup, and administrative wipe; the repository also provides a backup script and user documentation. -- **Canonical code:** [`core/database.py`](../../core/database.py); [`src/runtime_paths.py`](../../src/runtime_paths.py); [`src/task_scheduler.py`](../../src/task_scheduler.py); [`src/bg_jobs.py`](../../src/bg_jobs.py); [`src/bg_monitor.py`](../../src/bg_monitor.py); [`routes/backup_routes.py`](../../routes/backup_routes.py); [`routes/cleanup/cleanup_routes.py`](../../routes/cleanup/cleanup_routes.py); [`routes/admin_wipe/admin_wipe_routes.py`](../../routes/admin_wipe/admin_wipe_routes.py); [`scripts/odysseus-backup`](../../scripts/odysseus-backup); [`docs/backup-restore.md`](../../docs/backup-restore.md). +- **Evidence locations:** [`core/database.py`](../../core/database.py); [`src/runtime_paths.py`](../../src/runtime_paths.py); [`src/task_scheduler.py`](../../src/task_scheduler.py); [`src/bg_jobs.py`](../../src/bg_jobs.py); [`src/bg_monitor.py`](../../src/bg_monitor.py); [`routes/backup_routes.py`](../../routes/backup_routes.py); [`routes/cleanup/cleanup_routes.py`](../../routes/cleanup/cleanup_routes.py); [`routes/admin_wipe/admin_wipe_routes.py`](../../routes/admin_wipe/admin_wipe_routes.py); [`scripts/odysseus-backup`](../../scripts/odysseus-backup); [`docs/backup-restore.md`](../../docs/backup-restore.md). - **Known problems:** None recorded by this mapping. From 7fe3bc64829a184cc43fd99de88b0a420c4743ac Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <alexandremagteixeira@gmail.com> Date: Sun, 26 Jul 2026 20:11:10 +0100 Subject: [PATCH 006/125] docs(discovery): move discovery to repository root --- {specs/discovery => discovery}/README.md | 2 +- .../safety-boundaries.md | 18 +-- discovery/system-map.md | 139 ++++++++++++++++++ specs/discovery/system-map.md | 139 ------------------ 4 files changed, 149 insertions(+), 149 deletions(-) rename {specs/discovery => discovery}/README.md (94%) rename {specs/discovery => discovery}/safety-boundaries.md (79%) create mode 100644 discovery/system-map.md delete mode 100644 specs/discovery/system-map.md diff --git a/specs/discovery/README.md b/discovery/README.md similarity index 94% rename from specs/discovery/README.md rename to discovery/README.md index 4343f9ec7..f4c205837 100644 --- a/specs/discovery/README.md +++ b/discovery/README.md @@ -31,4 +31,4 @@ Compact, code-grounded discovery maps of cross-cutting systems in the checked-in 4. When no owning specification exists, retain the verified finding here and record missing documentation ownership as a follow-up; otherwise retain unresolved context here and correct stale wording. > [!NOTE] -> This package intentionally contains no generator, validator, maturity scale, feature database, or parallel work tracker. The [architecture runtime inventory](../architecture-runtime-inventory.md) remains useful structural context, but is an explicitly draft snapshot. +> This package intentionally contains no generator, validator, maturity scale, feature database, or parallel work tracker. The [architecture runtime inventory](../specs/architecture-runtime-inventory.md) remains useful structural context, but is an explicitly draft snapshot. diff --git a/specs/discovery/safety-boundaries.md b/discovery/safety-boundaries.md similarity index 79% rename from specs/discovery/safety-boundaries.md rename to discovery/safety-boundaries.md index 4340aa8d5..fa3ab7f3b 100644 --- a/specs/discovery/safety-boundaries.md +++ b/discovery/safety-boundaries.md @@ -37,7 +37,7 @@ - **User-controlled inputs:** Tool paths, patches, file contents, search patterns, and workspace selection passed into the tool dispatcher. -- **Current safeguards:** [`src/tool_execution.py`](../../src/tool_execution.py) resolves paths, blocks sensitive subpaths, applies allowlist containment, and tightens paths to the active workspace when one is bound. File tools use those resolvers. +- **Current safeguards:** [`src/tool_execution.py`](../src/tool_execution.py) resolves paths, blocks sensitive subpaths, applies allowlist containment, and tightens paths to the active workspace when one is bound. File tools use those resolvers. - **Confirmed risks or gaps:** Intentional authority with safeguards. The file-tool policy does not sandbox the shell; treating a workspace as a whole-process containment boundary would be incorrect. @@ -51,7 +51,7 @@ - **User-controlled inputs:** Chat content, attached/retrieved content that may influence the model, tool arguments, per-request tool selection, and policy toggles. -- **Current safeguards:** [`src/tool_security.py`](../../src/tool_security.py) blocks protected tools for non-admin users and fails closed for malformed tool names; [`src/tool_policy.py`](../../src/tool_policy.py) supports disabled and guide-only policy; prompt-security helpers label untrusted context. +- **Current safeguards:** [`src/tool_security.py`](../src/tool_security.py) blocks protected tools for non-admin users and fails closed for malformed tool names; [`src/tool_policy.py`](../src/tool_policy.py) supports disabled and guide-only policy; prompt-security helpers label untrusted context. - **Confirmed risks or gaps:** Credible risk requiring verification: aliases, legacy text tools, native function calls, and MCP-qualified names must all reach the same policy outcome. The code has specific alias handling for email/MCP names, which makes this a sensitive compatibility seam. @@ -65,7 +65,7 @@ - **User-controlled inputs:** Server configuration, remote OAuth completion, tool arguments, and model-selected MCP calls. -- **Current safeguards:** MCP routes are registered through [`routes/mcp_routes.py`](../../routes/mcp_routes.py); MCP-qualified tools are denied to non-admin users by [`src/tool_security.py`](../../src/tool_security.py). OAuth state and token persistence are handled in [`src/mcp_oauth.py`](../../src/mcp_oauth.py). +- **Current safeguards:** MCP routes are registered through [`routes/mcp_routes.py`](../routes/mcp_routes.py); MCP-qualified tools are denied to non-admin users by [`src/tool_security.py`](../src/tool_security.py). OAuth state and token persistence are handled in [`src/mcp_oauth.py`](../src/mcp_oauth.py). - **Confirmed risks or gaps:** Credible risk requiring verification: an MCP server authority is broader than the application can infer from its tool name. This map does not establish a trust or approval model for server installation and individual tool invocation. @@ -79,7 +79,7 @@ - **User-controlled inputs:** Search/fetch URLs, imported skill URLs, webhook configuration, and some endpoint settings. -- **Current safeguards:** [`src/url_security.py`](../../src/url_security.py) validates untrusted public HTTP URLs and fails closed on unsuitable schemes or private addresses. [`services/search/content.py`](../../services/search/content.py) resolves and rejects non-public hosts, pins resolved addresses for fetches, caps bodies, and limits redirects. +- **Current safeguards:** [`src/url_security.py`](../src/url_security.py) validates untrusted public HTTP URLs and fails closed on unsuitable schemes or private addresses. [`services/search/content.py`](../services/search/content.py) resolves and rejects non-public hosts, pins resolved addresses for fetches, caps bodies, and limits redirects. - **Confirmed risks or gaps:** Intentional split: administrator-created model endpoints may target private providers, while untrusted URLs use public-address checks. That distinction is required for self-hosted deployments but needs explicit call-site review. @@ -93,9 +93,9 @@ - **User-controlled inputs:** Administrative configuration, login/unlock requests, imported settings, and agent vault tool arguments. -- **Current safeguards:** [`src/secret_storage.py`](../../src/secret_storage.py) uses a locally stored Fernet key with restrictive permissions for supported database secrets. Vault routes require an administrator, avoid passing master passwords in command arguments, and set restrictive permissions on the vault-session file. +- **Current safeguards:** [`src/secret_storage.py`](../src/secret_storage.py) uses a locally stored Fernet key with restrictive permissions for supported database secrets. Vault routes require an administrator, avoid passing master passwords in command arguments, and set restrictive permissions on the vault-session file. -- **Confirmed risks or gaps:** Confirmed current boundary: vault session data is persisted through the vault path, not through [`src/secret_storage.py`](../../src/secret_storage.py). This is an unresolved question about current security semantics, not a confirmed exposure. +- **Confirmed risks or gaps:** Confirmed current boundary: vault session data is persisted through the vault path, not through [`src/secret_storage.py`](../src/secret_storage.py). This is an unresolved question about current security semantics, not a confirmed exposure. - **Unverified behaviour:** Current encryption-at-rest, owner scope, rotation, lock/logout, backup/restore, and log/tool-result exposure behaviour remains unverified. @@ -107,7 +107,7 @@ - **User-controlled inputs:** Login/signup data, session cookies, API tokens, authentication configuration, and requests to privileged routes. -- **Current safeguards:** [`core/auth.py`](../../core/auth.py), [`core/middleware.py`](../../core/middleware.py), and route-level checks establish identity and administrator gates. [`app.py`](../../app.py) warns when localhost bypass is configured; [`SECURITY.md`](../../SECURITY.md) documents deployment requirements. +- **Current safeguards:** [`core/auth.py`](../core/auth.py), [`core/middleware.py`](../core/middleware.py), and route-level checks establish identity and administrator gates. [`app.py`](../app.py) warns when localhost bypass is configured; [`SECURITY.md`](../SECURITY.md) documents deployment requirements. - **Confirmed risks or gaps:** Intentional authority with safeguards. Security depends on deployments keeping authentication enabled and internal services private; this map does not audit reverse-proxy or environment configuration. @@ -123,7 +123,7 @@ - **Current safeguards:** Administrative wipe routes use the administrative boundary. Cleanup exposes a preview route before mutation. The documented backup tool requires explicit restore confirmation, stages the old data directory, and validates archive members before extraction. -- **Confirmed risks or gaps:** Intentional destructive authority. Backup archives contain secrets by design, as documented in [`docs/backup-restore.md`](../../docs/backup-restore.md); this is an operator confidentiality responsibility, not a code defect established here. +- **Confirmed risks or gaps:** Intentional destructive authority. Backup archives contain secrets by design, as documented in [`docs/backup-restore.md`](../docs/backup-restore.md); this is an operator confidentiality responsibility, not a code defect established here. - **Unverified behaviour:** Role-gate, confirmation, archive-rejection, staged-recovery, and owner-isolation behaviour remains unverified. No destructive runtime test was performed. @@ -135,7 +135,7 @@ - **User-controlled inputs:** Stored task prompt, schedule, model/crew selection, enabled-tool configuration, output target, and prior persisted state. -- **Current safeguards:** [`src/task_scheduler.py`](../../src/task_scheduler.py) serializes execution, records task runs, associates work with an owner, and applies the agent owner-based tool gate. [`src/bg_jobs.py`](../../src/bg_jobs.py) keeps bounded state and can terminate overlong subprocess jobs. +- **Current safeguards:** [`src/task_scheduler.py`](../src/task_scheduler.py) serializes execution, records task runs, associates work with an owner, and applies the agent owner-based tool gate. [`src/bg_jobs.py`](../src/bg_jobs.py) keeps bounded state and can terminate overlong subprocess jobs. - **Confirmed risks or gaps:** Credible risk requiring verification: authority is inherited and exercised later, so changes to roles, task configuration, and disabled tools must be checked at execution time rather than assumed from task creation. diff --git a/discovery/system-map.md b/discovery/system-map.md new file mode 100644 index 000000000..938af76f3 --- /dev/null +++ b/discovery/system-map.md @@ -0,0 +1,139 @@ +# Current system map + +> [!NOTE] +> This non-canonical discovery map is an evidence guide, not an exhaustive feature catalog or runtime certification. Verify the cited source before relying on a finding. Each section records local implementation observations, evidence locations, confirmed current problems, and unresolved factual questions. + +## Navigate the system + +- [Startup and application composition](#startup-and-application-composition) +- [Frontend shell and browser interaction](#frontend-shell-and-browser-interaction) +- [Chat, sessions, and streaming](#chat-sessions-and-streaming) +- [Agents, tools, and execution](#agents-tools-and-execution) +- [Models, providers, and local serving](#models-providers-and-local-serving) +- [Search and research](#search-and-research) +- [Documents, retrieval, and personal knowledge](#documents-retrieval-and-personal-knowledge) +- [Memory and skills](#memory-and-skills) +- [Email, calendar, contacts, notes, and tasks](#email-calendar-contacts-notes-and-tasks) +- [Media, speech, and image work](#media-speech-and-image-work) +- [Authentication, secrets, and privileged administration](#authentication-secrets-and-privileged-administration) +- [Persistence, background work, and operations](#persistence-background-work-and-operations) + +## Startup and application composition + +- **How it works:** [`app.py`](../app.py) creates the application, mounts static assets, constructs shared services, registers route factories, and owns lifespan startup and shutdown. [`src/app_initializer.py`](../src/app_initializer.py) prepares application state; [`core/`](../core/) provides persistence, authentication, middleware, sessions, and platform helpers. + +- **Evidence locations:** [`app.py`](../app.py); [`src/app_initializer.py`](../src/app_initializer.py); [`core/database.py`](../core/database.py); [`core/auth.py`](../core/auth.py); [`core/middleware.py`](../core/middleware.py); [`routes/`](../routes/). + +- **Known problems:** None recorded by this mapping. + +- **Open question:** Which component currently owns startup and shutdown for each long-lived service? + +## Frontend shell and browser interaction + +- **How it works:** [`static/index.html`](../static/index.html) is served by the root and SPA deep-link routes in [`app.py`](../app.py); [`static/app.js`](../static/app.js), [`static/style.css`](../static/style.css), and [`static/js/`](../static/js/) implement the client surface. + +- **Evidence locations:** [`static/index.html`](../static/index.html); [`static/app.js`](../static/app.js); [`static/js/`](../static/js/); [`static/style.css`](../static/style.css); [`app.py`](../app.py) deep-link handlers. + +- **Known problems:** The `/backgrounds` route in [`app.py`](../app.py) calls `serve_html_with_nonce` for `static/backgrounds.html`, but that file is absent from [`static/`](../static/). This is a confirmed broken prototype route, not evidence about the rest of the frontend. + +- **Open question:** Is `/backgrounds` currently an intentionally supported route or an obsolete prototype? + +## Chat, sessions, and streaming + +- **How it works:** [`routes/chat_routes.py`](../routes/chat_routes.py) and [`routes/chat_helpers.py`](../routes/chat_helpers.py) coordinate requests, session state, and SSE delivery. [`src/chat_handler.py`](../src/chat_handler.py), [`src/chat_processor.py`](../src/chat_processor.py), [`src/llm_core.py`](../src/llm_core.py), and [`src/session_actions.py`](../src/session_actions.py) provide message preparation, provider interaction, and session operations. + +- **Evidence locations:** [`routes/chat_routes.py`](../routes/chat_routes.py); [`routes/chat_helpers.py`](../routes/chat_helpers.py); [`routes/session_routes.py`](../routes/session_routes.py); [`src/chat_handler.py`](../src/chat_handler.py); [`src/chat_processor.py`](../src/chat_processor.py); [`src/llm_core.py`](../src/llm_core.py); [`core/session_manager.py`](../core/session_manager.py). + +- **Known problems:** [`src/agent_loop.py`](../src/agent_loop.py) annotates `_resolved_tool_event_name` with `Any` but imports no `Any` and does not enable postponed annotation evaluation. Python evaluates that annotation while importing the module, so this is an import-time defect at the checked baseline. + +- **Open question:** No end-to-end provider or browser streaming run was performed for this map. + +## Agents, tools, and execution + +- **How it works:** [`src/agent_loop.py`](../src/agent_loop.py) drives multi-round tool use. [`src/tool_execution.py`](../src/tool_execution.py) dispatches calls and binds workspace context. [`src/agent_tools/`](../src/agent_tools/) contains individual implementations; [`src/tool_security.py`](../src/tool_security.py) and [`src/tool_policy.py`](../src/tool_policy.py) apply role and request policies. Long-running command work is represented by [`src/bg_jobs.py`](../src/bg_jobs.py). + +- **Evidence locations:** [`src/agent_loop.py`](../src/agent_loop.py); [`src/tool_execution.py`](../src/tool_execution.py); [`src/agent_tools/`](../src/agent_tools/); [`src/tool_security.py`](../src/tool_security.py); [`src/tool_policy.py`](../src/tool_policy.py); [`src/tool_schemas.py`](../src/tool_schemas.py); [`src/bg_jobs.py`](../src/bg_jobs.py). + +- **Known problems:** The import-time annotation defect above blocks the main agent/tool path. The shell is intentionally not a filesystem or network sandbox; that is an authority boundary, not by itself a vulnerability claim. + +- **Open question:** Which native, legacy, and MCP-qualified invocation paths reach each policy gate? + +## Models, providers, and local serving + +- **How it works:** Model routes delegate to discovery, capabilities, endpoint resolution, and LLM core modules. Cookbook routes and hardware-fit services handle model lifecycle and local-serving support. + +- **Evidence locations:** [`routes/model_routes.py`](../routes/model_routes.py); [`src/model_discovery.py`](../src/model_discovery.py); [`src/model_capabilities.py`](../src/model_capabilities.py); [`src/endpoint_resolver.py`](../src/endpoint_resolver.py); [`src/llm_core.py`](../src/llm_core.py); [`routes/cookbook_routes.py`](../routes/cookbook_routes.py); [`src/cookbook_serve_lifecycle.py`](../src/cookbook_serve_lifecycle.py); [`services/hwfit/`](../services/hwfit/). + +- **Known problems:** None recorded by this mapping. + +- **Open question:** Which endpoint inputs are administrator-created and permitted to use private provider addresses? + +## Search and research + +- **How it works:** HTTP search routes use [`services/search/`](../services/search/); research is exposed through [`routes/research/`](../routes/research/) and implemented in [`services/research/`](../services/research/), [`src/deep_research.py`](../src/deep_research.py), and related helpers. [`src/search/`](../src/search/) remains an import-compatibility layer for callers not yet moved to `services.search`. + +- **Evidence locations:** [`routes/search_routes.py`](../routes/search_routes.py); [`services/search/`](../services/search/); [`routes/research/research_routes.py`](../routes/research/research_routes.py); [`services/research/`](../services/research/); [`src/deep_research.py`](../src/deep_research.py); [`src/search/`](../src/search/). + +- **Known problems:** None recorded by this mapping. + +- **Open question:** No live provider request was made; provider configuration and network access remain unverified. + +## Documents, retrieval, and personal knowledge + +- **How it works:** Document routes coordinate upload handling, document processing, and editor actions. Personal-document and RAG modules use Chroma and embedding clients. PDF viewing uses the optional-dependency loader in [`src/pdf_runtime.py`](../src/pdf_runtime.py); form extraction and filling live separately in [`src/pdf_forms.py`](../src/pdf_forms.py) and [`src/pdf_form_doc.py`](../src/pdf_form_doc.py). + +- **Evidence locations:** [`routes/document_routes.py`](../routes/document_routes.py); [`src/upload_handler.py`](../src/upload_handler.py); [`src/document_processor.py`](../src/document_processor.py); [`src/document_actions.py`](../src/document_actions.py); [`src/personal_docs.py`](../src/personal_docs.py); [`src/rag_manager.py`](../src/rag_manager.py); [`src/embeddings.py`](../src/embeddings.py); [`src/pdf_runtime.py`](../src/pdf_runtime.py); [`src/pdf_forms.py`](../src/pdf_forms.py); [`src/pdf_form_doc.py`](../src/pdf_form_doc.py). + +- **Known problems:** PDF viewing/runtime loading and PDF form processing are separate implementations. That separation is confirmed and intentional in the source; it is not a defect without a reported behavioural failure. + +- **Open question:** Optional PDF dependencies and representative uploaded documents were not exercised. + +## Memory and skills + +- **How it works:** Memory routes use [`services/memory/`](../services/memory/) and vector helpers. Skills are exposed through [`routes/skills_routes.py`](../routes/skills_routes.py), stored and managed in [`services/memory/skills.py`](../services/memory/skills.py), and may be imported through [`services/memory/skill_importer.py`](../services/memory/skill_importer.py). + +- **Evidence locations:** [`routes/memory/memory_routes.py`](../routes/memory/memory_routes.py); [`services/memory/`](../services/memory/); [`src/memory.py`](../src/memory.py); [`src/memory_vector.py`](../src/memory_vector.py); [`routes/skills_routes.py`](../routes/skills_routes.py); [`services/memory/skills.py`](../services/memory/skills.py); [`services/memory/skill_importer.py`](../services/memory/skill_importer.py). + +- **Known problems:** None recorded by this mapping. + +- **Open question:** Which imported skill content can reach execution-capable paths, and which validation occurs before that point? + +## Email, calendar, contacts, notes, and tasks + +- **How it works:** Dedicated route modules own email, CalDAV calendar, CardDAV contacts, notes, and tasks. Supporting modules include email helpers and pollers, CalDAV sync and writeback, and the task scheduler. + +- **Evidence locations:** [`routes/email_routes.py`](../routes/email_routes.py); [`routes/calendar_routes.py`](../routes/calendar_routes.py); [`routes/contacts/contacts_routes.py`](../routes/contacts/contacts_routes.py); [`routes/note/note_routes.py`](../routes/note/note_routes.py); [`routes/task_routes.py`](../routes/task_routes.py); [`routes/assistant_routes.py`](../routes/assistant_routes.py); [`src/caldav_sync.py`](../src/caldav_sync.py); [`src/caldav_writeback.py`](../src/caldav_writeback.py); [`src/task_scheduler.py`](../src/task_scheduler.py). + +- **Known problems:** None recorded by this mapping. + +- **Open question:** External account behaviour, writeback, and delivery require controlled credentials and are not runtime-validated here. + +## Media, speech, and image work + +- **How it works:** Gallery and image routes coordinate media features. Service modules own speech and media integrations; [`src/generated_images.py`](../src/generated_images.py) and [`src/visual_report.py`](../src/visual_report.py) support artifact handling and presentation. + +- **Evidence locations:** [`routes/gallery/gallery_routes.py`](../routes/gallery/gallery_routes.py); [`routes/stt_routes.py`](../routes/stt_routes.py); [`routes/tts_routes.py`](../routes/tts_routes.py); [`src/generated_images.py`](../src/generated_images.py); [`services/stt/`](../services/stt/); [`services/tts/`](../services/tts/); [`services/faces/`](../services/faces/); [`src/visual_report.py`](../src/visual_report.py). + +- **Known problems:** None recorded by this mapping. + +- **Open question:** Hardware- and provider-dependent media workflows were not exercised. + +## Authentication, secrets, and privileged administration + +- **How it works:** [`core/auth.py`](../core/auth.py) and [`core/middleware.py`](../core/middleware.py) provide identity and request gates. [`src/secret_storage.py`](../src/secret_storage.py) encrypts application-managed database secrets with a local Fernet key. Vault handling is separate: [`routes/vault_routes.py`](../routes/vault_routes.py) and [`src/tools/vault.py`](../src/tools/vault.py) invoke the Bitwarden CLI and persist its session data in the application data area. + +- **Evidence locations:** [`core/auth.py`](../core/auth.py); [`core/middleware.py`](../core/middleware.py); [`routes/auth_routes.py`](../routes/auth_routes.py); [`routes/api_token_routes.py`](../routes/api_token_routes.py); [`src/secret_storage.py`](../src/secret_storage.py); [`routes/vault_routes.py`](../routes/vault_routes.py); [`src/tools/vault.py`](../src/tools/vault.py); [`routes/admin_wipe/admin_wipe_routes.py`](../routes/admin_wipe/admin_wipe_routes.py). + +- **Known problems:** Vault-command handling and local application secret storage are distinct paths with different storage mechanisms. This is a source-confirmed boundary, not evidence that either path is compromised. + +- **Open question:** What are the current confidentiality, ownership, rotation, and backup semantics for vault session data? + +## Persistence, background work, and operations + +- **How it works:** SQLite models and persistence are centred in [`core/database.py`](../core/database.py); managers use application data paths. The scheduler and background-job monitor can continue work outside a live browser request. Operational routes cover cleanup, backup, and administrative wipe; the repository also provides a backup script and user documentation. + +- **Evidence locations:** [`core/database.py`](../core/database.py); [`src/runtime_paths.py`](../src/runtime_paths.py); [`src/task_scheduler.py`](../src/task_scheduler.py); [`src/bg_jobs.py`](../src/bg_jobs.py); [`src/bg_monitor.py`](../src/bg_monitor.py); [`routes/backup_routes.py`](../routes/backup_routes.py); [`routes/cleanup/cleanup_routes.py`](../routes/cleanup/cleanup_routes.py); [`routes/admin_wipe/admin_wipe_routes.py`](../routes/admin_wipe/admin_wipe_routes.py); [`scripts/odysseus-backup`](../scripts/odysseus-backup); [`docs/backup-restore.md`](../docs/backup-restore.md). + +- **Known problems:** None recorded by this mapping. + +- **Open question:** What current behaviour applies to background execution, cancellation, retries, and authority inheritance? diff --git a/specs/discovery/system-map.md b/specs/discovery/system-map.md deleted file mode 100644 index ce65e2b02..000000000 --- a/specs/discovery/system-map.md +++ /dev/null @@ -1,139 +0,0 @@ -# Current system map - -> [!NOTE] -> This non-canonical discovery map is an evidence guide, not an exhaustive feature catalog or runtime certification. Verify the cited source before relying on a finding. Each section records local implementation observations, evidence locations, confirmed current problems, and unresolved factual questions. - -## Navigate the system - -- [Startup and application composition](#startup-and-application-composition) -- [Frontend shell and browser interaction](#frontend-shell-and-browser-interaction) -- [Chat, sessions, and streaming](#chat-sessions-and-streaming) -- [Agents, tools, and execution](#agents-tools-and-execution) -- [Models, providers, and local serving](#models-providers-and-local-serving) -- [Search and research](#search-and-research) -- [Documents, retrieval, and personal knowledge](#documents-retrieval-and-personal-knowledge) -- [Memory and skills](#memory-and-skills) -- [Email, calendar, contacts, notes, and tasks](#email-calendar-contacts-notes-and-tasks) -- [Media, speech, and image work](#media-speech-and-image-work) -- [Authentication, secrets, and privileged administration](#authentication-secrets-and-privileged-administration) -- [Persistence, background work, and operations](#persistence-background-work-and-operations) - -## Startup and application composition - -- **How it works:** [`app.py`](../../app.py) creates the application, mounts static assets, constructs shared services, registers route factories, and owns lifespan startup and shutdown. [`src/app_initializer.py`](../../src/app_initializer.py) prepares application state; [`core/`](../../core/) provides persistence, authentication, middleware, sessions, and platform helpers. - -- **Evidence locations:** [`app.py`](../../app.py); [`src/app_initializer.py`](../../src/app_initializer.py); [`core/database.py`](../../core/database.py); [`core/auth.py`](../../core/auth.py); [`core/middleware.py`](../../core/middleware.py); [`routes/`](../../routes/). - -- **Known problems:** None recorded by this mapping. - -- **Open question:** Which component currently owns startup and shutdown for each long-lived service? - -## Frontend shell and browser interaction - -- **How it works:** [`static/index.html`](../../static/index.html) is served by the root and SPA deep-link routes in [`app.py`](../../app.py); [`static/app.js`](../../static/app.js), [`static/style.css`](../../static/style.css), and [`static/js/`](../../static/js/) implement the client surface. - -- **Evidence locations:** [`static/index.html`](../../static/index.html); [`static/app.js`](../../static/app.js); [`static/js/`](../../static/js/); [`static/style.css`](../../static/style.css); [`app.py`](../../app.py) deep-link handlers. - -- **Known problems:** The `/backgrounds` route in [`app.py`](../../app.py) calls `serve_html_with_nonce` for `static/backgrounds.html`, but that file is absent from [`static/`](../../static/). This is a confirmed broken prototype route, not evidence about the rest of the frontend. - -- **Open question:** Is `/backgrounds` currently an intentionally supported route or an obsolete prototype? - -## Chat, sessions, and streaming - -- **How it works:** [`routes/chat_routes.py`](../../routes/chat_routes.py) and [`routes/chat_helpers.py`](../../routes/chat_helpers.py) coordinate requests, session state, and SSE delivery. [`src/chat_handler.py`](../../src/chat_handler.py), [`src/chat_processor.py`](../../src/chat_processor.py), [`src/llm_core.py`](../../src/llm_core.py), and [`src/session_actions.py`](../../src/session_actions.py) provide message preparation, provider interaction, and session operations. - -- **Evidence locations:** [`routes/chat_routes.py`](../../routes/chat_routes.py); [`routes/chat_helpers.py`](../../routes/chat_helpers.py); [`routes/session_routes.py`](../../routes/session_routes.py); [`src/chat_handler.py`](../../src/chat_handler.py); [`src/chat_processor.py`](../../src/chat_processor.py); [`src/llm_core.py`](../../src/llm_core.py); [`core/session_manager.py`](../../core/session_manager.py). - -- **Known problems:** [`src/agent_loop.py`](../../src/agent_loop.py) annotates `_resolved_tool_event_name` with `Any` but imports no `Any` and does not enable postponed annotation evaluation. Python evaluates that annotation while importing the module, so this is an import-time defect at the checked baseline. - -- **Open question:** No end-to-end provider or browser streaming run was performed for this map. - -## Agents, tools, and execution - -- **How it works:** [`src/agent_loop.py`](../../src/agent_loop.py) drives multi-round tool use. [`src/tool_execution.py`](../../src/tool_execution.py) dispatches calls and binds workspace context. [`src/agent_tools/`](../../src/agent_tools/) contains individual implementations; [`src/tool_security.py`](../../src/tool_security.py) and [`src/tool_policy.py`](../../src/tool_policy.py) apply role and request policies. Long-running command work is represented by [`src/bg_jobs.py`](../../src/bg_jobs.py). - -- **Evidence locations:** [`src/agent_loop.py`](../../src/agent_loop.py); [`src/tool_execution.py`](../../src/tool_execution.py); [`src/agent_tools/`](../../src/agent_tools/); [`src/tool_security.py`](../../src/tool_security.py); [`src/tool_policy.py`](../../src/tool_policy.py); [`src/tool_schemas.py`](../../src/tool_schemas.py); [`src/bg_jobs.py`](../../src/bg_jobs.py). - -- **Known problems:** The import-time annotation defect above blocks the main agent/tool path. The shell is intentionally not a filesystem or network sandbox; that is an authority boundary, not by itself a vulnerability claim. - -- **Open question:** Which native, legacy, and MCP-qualified invocation paths reach each policy gate? - -## Models, providers, and local serving - -- **How it works:** Model routes delegate to discovery, capabilities, endpoint resolution, and LLM core modules. Cookbook routes and hardware-fit services handle model lifecycle and local-serving support. - -- **Evidence locations:** [`routes/model_routes.py`](../../routes/model_routes.py); [`src/model_discovery.py`](../../src/model_discovery.py); [`src/model_capabilities.py`](../../src/model_capabilities.py); [`src/endpoint_resolver.py`](../../src/endpoint_resolver.py); [`src/llm_core.py`](../../src/llm_core.py); [`routes/cookbook_routes.py`](../../routes/cookbook_routes.py); [`src/cookbook_serve_lifecycle.py`](../../src/cookbook_serve_lifecycle.py); [`services/hwfit/`](../../services/hwfit/). - -- **Known problems:** None recorded by this mapping. - -- **Open question:** Which endpoint inputs are administrator-created and permitted to use private provider addresses? - -## Search and research - -- **How it works:** HTTP search routes use [`services/search/`](../../services/search/); research is exposed through [`routes/research/`](../../routes/research/) and implemented in [`services/research/`](../../services/research/), [`src/deep_research.py`](../../src/deep_research.py), and related helpers. [`src/search/`](../../src/search/) remains an import-compatibility layer for callers not yet moved to `services.search`. - -- **Evidence locations:** [`routes/search_routes.py`](../../routes/search_routes.py); [`services/search/`](../../services/search/); [`routes/research/research_routes.py`](../../routes/research/research_routes.py); [`services/research/`](../../services/research/); [`src/deep_research.py`](../../src/deep_research.py); [`src/search/`](../../src/search/). - -- **Known problems:** None recorded by this mapping. - -- **Open question:** No live provider request was made; provider configuration and network access remain unverified. - -## Documents, retrieval, and personal knowledge - -- **How it works:** Document routes coordinate upload handling, document processing, and editor actions. Personal-document and RAG modules use Chroma and embedding clients. PDF viewing uses the optional-dependency loader in [`src/pdf_runtime.py`](../../src/pdf_runtime.py); form extraction and filling live separately in [`src/pdf_forms.py`](../../src/pdf_forms.py) and [`src/pdf_form_doc.py`](../../src/pdf_form_doc.py). - -- **Evidence locations:** [`routes/document_routes.py`](../../routes/document_routes.py); [`src/upload_handler.py`](../../src/upload_handler.py); [`src/document_processor.py`](../../src/document_processor.py); [`src/document_actions.py`](../../src/document_actions.py); [`src/personal_docs.py`](../../src/personal_docs.py); [`src/rag_manager.py`](../../src/rag_manager.py); [`src/embeddings.py`](../../src/embeddings.py); [`src/pdf_runtime.py`](../../src/pdf_runtime.py); [`src/pdf_forms.py`](../../src/pdf_forms.py); [`src/pdf_form_doc.py`](../../src/pdf_form_doc.py). - -- **Known problems:** PDF viewing/runtime loading and PDF form processing are separate implementations. That separation is confirmed and intentional in the source; it is not a defect without a reported behavioural failure. - -- **Open question:** Optional PDF dependencies and representative uploaded documents were not exercised. - -## Memory and skills - -- **How it works:** Memory routes use [`services/memory/`](../../services/memory/) and vector helpers. Skills are exposed through [`routes/skills_routes.py`](../../routes/skills_routes.py), stored and managed in [`services/memory/skills.py`](../../services/memory/skills.py), and may be imported through [`services/memory/skill_importer.py`](../../services/memory/skill_importer.py). - -- **Evidence locations:** [`routes/memory/memory_routes.py`](../../routes/memory/memory_routes.py); [`services/memory/`](../../services/memory/); [`src/memory.py`](../../src/memory.py); [`src/memory_vector.py`](../../src/memory_vector.py); [`routes/skills_routes.py`](../../routes/skills_routes.py); [`services/memory/skills.py`](../../services/memory/skills.py); [`services/memory/skill_importer.py`](../../services/memory/skill_importer.py). - -- **Known problems:** None recorded by this mapping. - -- **Open question:** Which imported skill content can reach execution-capable paths, and which validation occurs before that point? - -## Email, calendar, contacts, notes, and tasks - -- **How it works:** Dedicated route modules own email, CalDAV calendar, CardDAV contacts, notes, and tasks. Supporting modules include email helpers and pollers, CalDAV sync and writeback, and the task scheduler. - -- **Evidence locations:** [`routes/email_routes.py`](../../routes/email_routes.py); [`routes/calendar_routes.py`](../../routes/calendar_routes.py); [`routes/contacts/contacts_routes.py`](../../routes/contacts/contacts_routes.py); [`routes/note/note_routes.py`](../../routes/note/note_routes.py); [`routes/task_routes.py`](../../routes/task_routes.py); [`routes/assistant_routes.py`](../../routes/assistant_routes.py); [`src/caldav_sync.py`](../../src/caldav_sync.py); [`src/caldav_writeback.py`](../../src/caldav_writeback.py); [`src/task_scheduler.py`](../../src/task_scheduler.py). - -- **Known problems:** None recorded by this mapping. - -- **Open question:** External account behaviour, writeback, and delivery require controlled credentials and are not runtime-validated here. - -## Media, speech, and image work - -- **How it works:** Gallery and image routes coordinate media features. Service modules own speech and media integrations; [`src/generated_images.py`](../../src/generated_images.py) and [`src/visual_report.py`](../../src/visual_report.py) support artifact handling and presentation. - -- **Evidence locations:** [`routes/gallery/gallery_routes.py`](../../routes/gallery/gallery_routes.py); [`routes/stt_routes.py`](../../routes/stt_routes.py); [`routes/tts_routes.py`](../../routes/tts_routes.py); [`src/generated_images.py`](../../src/generated_images.py); [`services/stt/`](../../services/stt/); [`services/tts/`](../../services/tts/); [`services/faces/`](../../services/faces/); [`src/visual_report.py`](../../src/visual_report.py). - -- **Known problems:** None recorded by this mapping. - -- **Open question:** Hardware- and provider-dependent media workflows were not exercised. - -## Authentication, secrets, and privileged administration - -- **How it works:** [`core/auth.py`](../../core/auth.py) and [`core/middleware.py`](../../core/middleware.py) provide identity and request gates. [`src/secret_storage.py`](../../src/secret_storage.py) encrypts application-managed database secrets with a local Fernet key. Vault handling is separate: [`routes/vault_routes.py`](../../routes/vault_routes.py) and [`src/tools/vault.py`](../../src/tools/vault.py) invoke the Bitwarden CLI and persist its session data in the application data area. - -- **Evidence locations:** [`core/auth.py`](../../core/auth.py); [`core/middleware.py`](../../core/middleware.py); [`routes/auth_routes.py`](../../routes/auth_routes.py); [`routes/api_token_routes.py`](../../routes/api_token_routes.py); [`src/secret_storage.py`](../../src/secret_storage.py); [`routes/vault_routes.py`](../../routes/vault_routes.py); [`src/tools/vault.py`](../../src/tools/vault.py); [`routes/admin_wipe/admin_wipe_routes.py`](../../routes/admin_wipe/admin_wipe_routes.py). - -- **Known problems:** Vault-command handling and local application secret storage are distinct paths with different storage mechanisms. This is a source-confirmed boundary, not evidence that either path is compromised. - -- **Open question:** What are the current confidentiality, ownership, rotation, and backup semantics for vault session data? - -## Persistence, background work, and operations - -- **How it works:** SQLite models and persistence are centred in [`core/database.py`](../../core/database.py); managers use application data paths. The scheduler and background-job monitor can continue work outside a live browser request. Operational routes cover cleanup, backup, and administrative wipe; the repository also provides a backup script and user documentation. - -- **Evidence locations:** [`core/database.py`](../../core/database.py); [`src/runtime_paths.py`](../../src/runtime_paths.py); [`src/task_scheduler.py`](../../src/task_scheduler.py); [`src/bg_jobs.py`](../../src/bg_jobs.py); [`src/bg_monitor.py`](../../src/bg_monitor.py); [`routes/backup_routes.py`](../../routes/backup_routes.py); [`routes/cleanup/cleanup_routes.py`](../../routes/cleanup/cleanup_routes.py); [`routes/admin_wipe/admin_wipe_routes.py`](../../routes/admin_wipe/admin_wipe_routes.py); [`scripts/odysseus-backup`](../../scripts/odysseus-backup); [`docs/backup-restore.md`](../../docs/backup-restore.md). - -- **Known problems:** None recorded by this mapping. - -- **Open question:** What current behaviour applies to background execution, cancellation, retries, and authority inheritance? From c762efe1c97ae9df8e2f729e0488b9a400876d24 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <alexandremagteixeira@gmail.com> Date: Mon, 27 Jul 2026 16:45:48 +0100 Subject: [PATCH 007/125] docs: replace detailed roadmap with directional overview --- .github/ISSUE_TEMPLATE/feature_request.yml | 4 +- README.md | 2 +- ROADMAP.md | 118 +++++++----------- tests/test_provider_endpoints_url_building.py | 6 +- 4 files changed, 49 insertions(+), 81 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 4ee603ee9..ae8d18f3f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -8,8 +8,8 @@ body: value: | **Before submitting:** search [open issues](https://github.com/odysseus-dev/odysseus/issues) and [discussions](https://github.com/odysseus-dev/odysseus/discussions) first. - Feature requests that duplicate [ROADMAP.md](https://github.com/odysseus-dev/odysseus/blob/main/ROADMAP.md) - or an existing open issue will be closed as duplicates. + The [roadmap](https://github.com/odysseus-dev/odysseus/blob/main/ROADMAP.md) is directional rather than a complete backlog. + Feature requests that duplicate an existing issue or accepted proposal may be closed as duplicates. If your idea needs community input before it becomes a concrete proposal, start a [discussion](https://github.com/odysseus-dev/odysseus/discussions/categories/ideas) instead. diff --git a/README.md b/README.md index 705ec6b68..b8d2017d7 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ A full hover-to-play tour lives on the landing page: [`docs/index.html`](docs/in ## Contributing -Help is welcome. The best entry points are fresh-install testing, provider setup bugs, mobile/editor polish, docs, and small focused refactors. See [CONTRIBUTING.md](CONTRIBUTING.md) and [ROADMAP.md](ROADMAP.md). +Help is welcome. The best entry points are fresh-install testing, provider setup bugs, mobile/editor polish, documentation, and small focused refactors. See [CONTRIBUTING.md](CONTRIBUTING.md), the [public roadmap](ROADMAP.md), and the open [GitHub issues](https://github.com/odysseus-dev/odysseus/issues). ## Security diff --git a/ROADMAP.md b/ROADMAP.md index f5c47ae18..bcf0c155c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,87 +1,55 @@ -# Roadmap / Help Wanted +# Roadmap -Odysseus is on a voyage, but not home yet. It works great for me (lol), but this ship is moving fast and feedback/help would be appreciated! (I don't know what I'm doing, help). +This document provides a high-level view of the areas Odysseus is currently improving. -If you see weird CSS, strange layout behavior, or a suspiciously murky corner of -the codebase, you are probably right to stay away. +It is directional rather than exhaustive. Priorities may change as the project evolves, defects are discovered, and maintainers learn more from implementation work and user feedback. -## High Priority +For current implementation work, see the open [GitHub issues](https://github.com/odysseus-dev/odysseus/issues). Accepted behaviour should be documented in the repository alongside the code. -- SQUASH BUGS -- Fresh install smoke tests on Linux, macOS, and Windows. Docker, native Python, - and WSL all need coverage. +## Current priorities -- Integration audit: do integrations even work? Confirm what works, what needs setup docs, and what should be removed or hidden. -- Cookbook reliability on other computers. This is probably the area most likely to need work across different machines, GPUs, drivers, shells, and Python environments. -- Cookbook SGLang support across platforms. Make sure SGLang setup/serve works - predictably on Linux, Windows/WSL, macOS where possible, Docker, and common - NVIDIA/AMD hardware paths. -- Deep Research model presets by hardware. Recommend approved model/parameter - profiles for small, medium, and large local setups so people with different - hardware can use Deep Research without guessing. Surface this either in Deep - Research settings or as a Cookbook scan/dropdown suggestion. -- Cookbook model scan/download ranking. Prioritize newer architectures and - better hardware-fit models instead of scoring everything almost the same. - Ranking should account for architecture age, quant format, VRAM/RAM fit, - backend support, vision/mmproj requirements, and likely serve reliability. -- Cookbook error feedback and logging. Failed downloads, dependency installs, - preflights, and serve jobs should show the actual command/output/error in the - UI, with copyable logs and clear next steps instead of just "crashed". -- Agent prompt/context bloat. Agent mode is too heavy for smaller local models: - tool schemas, skills, memory, documents, and instructions can eat the context - before the user request really starts. We need slimmer prompts, better tool - selection, smaller default tool sets, and clearer guidance for models with - 4k/8k/16k context windows. -- Local model speculative decoding support. For Odysseus-tuned local models, - plan to ship or recommend a small same-tokenizer draft model when the serving - backend supports it. Early vLLM testing showed a generic `Qwen3-0.6B` draft - beside `Qwen3-8B` can materially reduce wall time, while an unsupported - DSpark conversion performed poorly. Treat this as a supported draft-model lane - first; keep MTP-specific packaging as future work only when the architecture - and runtime support are real. Judge this by time-to-success, tool correctness, - grammar, and unchanged target output, not tokens/sec alone. -- Skill/tool prompt-injection audit. User-editable skills, notes, documents, - fetched pages, and memories should be treated as untrusted data. Keep testing - whether models follow malicious instructions from those surfaces. -- Better degraded-state reporting for ChromaDB, SearXNG, email, ntfy, and provider probes. -- Email performance audit. Fetching, searching, opening, deleting, and sending - email can feel slow, especially over IMAP/SMTP providers with high latency. - Need someone who knows mail performance to profile the current flow, identify - whether the bottleneck is IMAP folder select/fetch, cache invalidation, - attachment/body loading, SMTP handshakes, or frontend refresh behavior, then - propose safer caching/prefetch/batching without breaking multi-account state. -- Provider setup/probing audit for Anthropic, Gemini, Groq, xAI, OpenRouter, OpenAI, and DeepSeek. +### Reliability and setup -## Refactor Targets -- CSS cleanup. `static/style.css` basically Calypso's island atm. -- Tour core helper. The onboarding tours have too much copy-pasted scaffolding; promote a shared `tour-core.js` helper before adding more tours. -- Modal/window positioning cleanup. Some window controls have improved, but the - underlying popup/dropdown/fixed-position behavior is still too fragile. -- Mobile media override discoverability. A lot of "CSS did not move" bugs are mobile `@media` overrides of the same selector; comments or linting around desktop/mobile paired rules would help. -- Dead code pass for old routes, stale feature flags, and unused UI states. +- Improve fresh-install and smoke-test coverage across supported environments. +- Make provider setup, probing, and failure states more predictable. +- Improve Cookbook reliability across hardware, operating systems, drivers, shells, and serving backends. +- Improve degraded-state reporting and recovery guidance when optional services are unavailable. -## Frontend +### Local model workflows -- Expand the Editor for quicker, more robust everyday use. Better file/document - handling, smoother window behavior, clearer save/export flows, stronger image - editing affordances, and fewer brittle edge cases. -- Better AI integration for Notes and Todos. Notes should be easier for the - agent to read, update, summarize, and turn into actions. Todos should be - assignable to an agent from the UI, possibly through a button, task action, - or dedicated skill/tool flow. -- Mobile gallery/editor polish. Easier to launch/download inpaint model or any missing pieces. -- Accessibility pass: keyboard navigation, focus states, contrast, reduced motion. -- Improve empty states and error messages on fresh installs. -- Tighten first-run setup, hints, and tours so they do not repeat or fight each other. -- Vendor CDN assets eventually for a more fully self-hosted/offline mode. +- Improve hardware-aware model recommendations and compatibility guidance. +- Evaluate serving optimizations, including speculative decoding, through reproducible benchmarks. +- Improve installation, preflight checks, logging, and error reporting for local model serving. +- Reduce prompt and context overhead for smaller local models. -## Backend +### Safety and resilience -- More tests around endpoint probing and provider setup. -- Better task scheduler defaults and visibility. -- Backup/restore guide and helper flow for `data/`. -- Security hardening around admin-only tools and clear docs for their risk. +- Continue hardening tool execution, filesystem access, credentials, networking, and destructive operations. +- Treat content from documents, notes, memories, skills, and fetched pages as potentially untrusted. +- Improve security-focused regression coverage and operational guidance. +- Review integrations that expand access to sensitive data or privileged operations. -## Not The Focus Right Now +### Product usability -I prob shouldnt add more themes. +- Improve first-run setup, onboarding, hints, and tours. +- Improve accessibility, keyboard navigation, focus behaviour, contrast, and reduced-motion support. +- Improve empty states, error messages, and recovery paths. +- Strengthen Notes, Todos, Editor, mobile, and everyday workspace flows. + +### Architecture and maintainability + +- Reduce duplication and technical debt through focused, reviewable refactors. +- Improve subsystem documentation as behaviour and architecture become stable. +- Remove stale code, obsolete feature flags, and unsupported integrations. +- Keep implementation decisions grounded in current code and verified behaviour. + +## Tracking work + +Concrete implementation tasks, defects, proposals, and technical investigations are tracked in: + +- [GitHub Issues](https://github.com/odysseus-dev/odysseus/issues) +- [Contributing Guide](CONTRIBUTING.md) + +Maintainers may use additional private coordination tools for ownership, planning, and unresolved decisions. + +This roadmap is not a complete backlog or a guarantee that a particular item will be delivered. diff --git a/tests/test_provider_endpoints_url_building.py b/tests/test_provider_endpoints_url_building.py index 56d129e1c..7ddbe2ba8 100644 --- a/tests/test_provider_endpoints_url_building.py +++ b/tests/test_provider_endpoints_url_building.py @@ -1,8 +1,8 @@ """Provider endpoint URL-building tests. -Covers ``build_chat_url`` and ``build_models_url`` for every provider named in -ROADMAP.md: Anthropic, Gemini, Groq, xAI, OpenRouter, OpenAI, DeepSeek, Ollama -(local + cloud). +Covers ``build_chat_url`` and ``build_models_url`` for the provider families +exercised by this module: Anthropic, Gemini, Groq, xAI, OpenRouter, OpenAI, +DeepSeek, and Ollama (local and cloud). """ import pytest From 8670aed9adb6f5a1677a4406fcc669aa2ea5d6f3 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <alexandremagteixeira@gmail.com> Date: Tue, 28 Jul 2026 07:59:37 +0100 Subject: [PATCH 008/125] docs: classify architecture inventory and refresh guidance --- discovery/README.md | 2 +- discovery/architecture-runtime-inventory.md | 331 +++++++++++++ docs/ARCHITECTURE.md | 87 ++++ docs/STYLE.md | 72 +++ docs/pr-blocker-audit.md | 4 +- specs/architecture-runtime-inventory.md | 443 ++---------------- tests/LAYOUT_INVENTORY.md | 300 +++++------- tests/OVERSIZED_TEST_SPLIT_PLAN.md | 325 +++++++------ .../tools/build_oversized_test_split_plan.py | 8 +- 9 files changed, 832 insertions(+), 740 deletions(-) create mode 100644 discovery/architecture-runtime-inventory.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/STYLE.md diff --git a/discovery/README.md b/discovery/README.md index f4c205837..234726851 100644 --- a/discovery/README.md +++ b/discovery/README.md @@ -31,4 +31,4 @@ Compact, code-grounded discovery maps of cross-cutting systems in the checked-in 4. When no owning specification exists, retain the verified finding here and record missing documentation ownership as a follow-up; otherwise retain unresolved context here and correct stale wording. > [!NOTE] -> This package intentionally contains no generator, validator, maturity scale, feature database, or parallel work tracker. The [architecture runtime inventory](../specs/architecture-runtime-inventory.md) remains useful structural context, but is an explicitly draft snapshot. +> This package intentionally contains no generator, validator, maturity scale, feature database, or parallel work tracker. The [architecture runtime inventory](./architecture-runtime-inventory.md) preserves dated structural metrics, investigation context, and historical planning as an explicitly non-canonical snapshot. diff --git a/discovery/architecture-runtime-inventory.md b/discovery/architecture-runtime-inventory.md new file mode 100644 index 000000000..22a56d9bf --- /dev/null +++ b/discovery/architecture-runtime-inventory.md @@ -0,0 +1,331 @@ +# Architecture runtime inventory + +> [!WARNING] +> This document is a dated structural snapshot, not a canonical runtime specification. +> Counts, paths, and implementation details may drift as the repository changes. +> Verify implementation-sensitive claims against the current code and tests. + +- **Branch:** `discovery` +- **Commit:** `c762efe1c97a` +- **Generated:** `2026-07-28T05:38:14+01:00` +- **Historical context:** readability and refactor planning in [#4071](https://github.com/odysseus-dev/odysseus/issues/4071) and [#4082](https://github.com/odysseus-dev/odysseus/issues/4082) + +## Disposition + +> [!NOTE] +> Reviewed for documentation classification. Stable runtime structure and +> subsystem ownership have been transferred to the proposed canonical +> destination, [`docs/ARCHITECTURE.md`](../docs/ARCHITECTURE.md), for +> maintainer review. +> +> This document retains dated metrics, rankings, investigation context, +> refactor-sensitive observations, and historical planning. Those contents are +> non-canonical and belong under `discovery/`. + +| Content | Authority and destination | +|---|---| +| Stable runtime structure | Proposed canonical destination: `docs/ARCHITECTURE.md` | +| Stable subsystem boundaries | Proposed canonical destination: `docs/ARCHITECTURE.md` | +| Frontend module organization | `static/js/MODULE_SUMMARY.md` | +| Counts, line totals, and rankings | This non-canonical inventory | +| Investigation context and open questions | `discovery/` | +| Refactor options and prioritization | Issues, Plane, or non-canonical discovery material | + +The transfer preserves the stable facts without promoting generated metrics or +historical prioritization into canonical documentation. + +## Purpose + +This inventory provides a reviewable map of the current repository structure, +large runtime modules, major subsystem boundaries, and refactor-sensitive areas. + +It does not: + +- define accepted subsystem behaviour; +- certify runtime correctness; +- prescribe a committed refactor sequence; +- replace focused specifications, tests, or source review. + +For cross-cutting implementation evidence, see the [discovery maps](./README.md). + +## Top-level runtime structure + +| Area | Role | +|---|---| +| `app.py` | FastAPI application composition and entry point | +| `launcher.py` | Application launch support | +| `setup.py` | Native setup workflow | +| `core/` | Authentication, middleware, persistence, sessions, and platform primitives | +| `routes/` | HTTP and API route handlers | +| `src/` | Application services, orchestration, tools, providers, and runtime helpers | +| `services/` | Domain-oriented service packages | +| `mcp_servers/` | Built-in MCP server implementations | +| `scripts/` | CLI tools, diagnostics, maintenance, and migration helpers | +| `static/` | No-build browser frontend and bundled assets | +| `tests/` | Automated test suite and supporting test infrastructure | + +## Directory snapshot + +| Directory | Tracked files | Tracked Python files | Direct subdirectories | +|---|---:|---:|---| +| `src/` | 143 | 143 | `agent_tools/`, `model_capability_readers/`, `search/`, `tools/` | +| `routes/` | 73 | 73 | `admin_wipe/`, `cleanup/`, `compare/`, `contacts/`, `gallery/`, `history/`, `memory/`, `note/`, `research/` | +| `core/` | 11 | 11 | None | +| `services/` | 42 | 40 | `docs/`, `faces/`, `hwfit/`, `memory/`, `research/`, `search/`, `shell/`, `stt/`, `tts/`, `youtube/` | +| `mcp_servers/` | 5 | 5 | None | +| `scripts/` | 44 | 17 | `_completion/`, `_lib/`, `demo_email/` | +| `static/js/` | 154 | 0 | `calendar/`, `color/`, `compare/`, `editor/`, `emailLibrary/`, `markdown/`, `model/`, `research/`, `util/` | +| `tests/` | 768 | 758 | `cli/`, `helpers/`, `streaming/`, `tools/` | + +> [!NOTE] +> Counts in this table use `git ls-files`, so generated caches, virtual +> environments, and other untracked local files are excluded. + +## Largest backend modules + +Large files are review signals, not proof that a module should be split. +Coupling, ownership, import compatibility, tests, and runtime authority matter more +than line count alone. + +| Rank | File | Lines | Classes | Top-level functions | Review signal | +|---:|---|---:|---:|---:|---| +| 1 | `routes/email_routes.py` | 6032 | 1 | 58 | High | +| 2 | `src/agent_loop.py` | 5248 | 0 | 63 | High | +| 3 | `routes/cookbook_routes.py` | 4545 | 0 | 16 | High | +| 4 | `mcp_servers/email_server.py` | 2920 | 0 | 77 | High | +| 5 | `src/llm_core.py` | 2895 | 3 | 85 | High | +| 6 | `src/builtin_actions.py` | 2845 | 2 | 27 | High | +| 7 | `routes/model_routes.py` | 2743 | 0 | 65 | High | +| 8 | `src/task_scheduler.py` | 2627 | 1 | 8 | Medium | +| 9 | `core/database.py` | 2562 | 28 | 67 | High | +| 10 | `routes/gallery/gallery_routes.py` | 2325 | 0 | 16 | Medium | +| 11 | `routes/chat_routes.py` | 2063 | 0 | 18 | Medium | +| 12 | `routes/shell_routes.py` | 1971 | 1 | 21 | Medium | +| 13 | `src/visual_report.py` | 1933 | 0 | 11 | Medium | +| 14 | `routes/email_helpers.py` | 1888 | 3 | 48 | Medium | +| 15 | `routes/document_routes.py` | 1810 | 0 | 5 | Medium | +| 16 | `src/tools/cookbook.py` | 1705 | 0 | 34 | Medium | +| 17 | `routes/calendar_routes.py` | 1667 | 2 | 19 | Medium | +| 18 | `routes/skills_routes.py` | 1662 | 3 | 19 | Medium | +| 19 | `src/tool_schemas.py` | 1595 | 0 | 3 | Medium | +| 20 | `routes/email_pollers.py` | 1551 | 0 | 23 | Medium | + +The largest current backend concentrations include: + +- email routing and helper logic; +- agent-loop orchestration; +- Cookbook lifecycle and serving logic; +- provider and model routing; +- task scheduling; +- shared database models and persistence helpers. + +These areas require focused ownership and compatibility analysis before structural +changes are attempted. + +## Largest frontend modules + +| Rank | File | Lines | +|---:|---|---:| +| 1 | `static/style.css` | 41132 | +| 2 | `static/js/document.js` | 11200 | +| 3 | `static/js/emailLibrary.js` | 8505 | +| 4 | `static/js/slashCommands.js` | 6520 | +| 5 | `static/js/chat.js` | 6001 | +| 6 | `static/js/settings.js` | 5819 | +| 7 | `static/js/notes.js` | 5365 | +| 8 | `static/app.js` | 4681 | +| 9 | `static/js/cookbookRunning.js` | 4433 | +| 10 | `static/js/galleryEditor.js` | 4386 | +| 11 | `static/js/cookbookServe.js` | 4305 | +| 12 | `static/js/calendar.js` | 3722 | +| 13 | `static/js/cookbook.js` | 3677 | +| 14 | `static/js/sessions.js` | 3665 | +| 15 | `static/js/documentLibrary.js` | 3422 | +| 16 | `static/js/tasks.js` | 3187 | +| 17 | `static/js/admin.js` | 3144 | +| 18 | `static/js/gallery.js` | 2958 | +| 19 | `static/js/cookbook-hwfit.js` | 2826 | +| 20 | `static/js/chatRenderer.js` | 2808 | + +The browser frontend remains a no-build ES-module application. Its current source +tree is authoritative; the maintained structural summary is available in +[`static/js/MODULE_SUMMARY.md`](../static/js/MODULE_SUMMARY.md). + +CSS modularization remains tracked separately in +[#2617](https://github.com/odysseus-dev/odysseus/issues/2617). + +## Major subsystem boundaries + +| Subsystem | Primary implementation locations | +|---|---| +| Application startup | `app.py`, `src/app_initializer.py`, `core/` | +| Authentication and sessions | `core/auth.py`, `core/middleware.py`, `core/session_manager.py`, `routes/auth_routes.py` | +| Chat and streaming | `routes/chat_routes.py`, `routes/chat_helpers.py`, `src/chat_handler.py`, `src/chat_processor.py`, `src/llm_core.py` | +| Agents and tools | `src/agent_loop.py`, `src/tool_execution.py`, `src/agent_tools/`, `src/tools/`, `src/tool_policy.py`, `src/tool_security.py` | +| Models and providers | `routes/model_routes.py`, `src/model_discovery.py`, `src/model_capabilities.py`, `src/endpoint_resolver.py`, `src/llm_core.py` | +| Cookbook and hardware fit | `routes/cookbook_routes.py`, `routes/cookbook_helpers.py`, `src/cookbook_serve_lifecycle.py`, `services/hwfit/` | +| Search and research | `routes/search_routes.py`, `services/search/`, `routes/research/`, `services/research/`, `src/deep_research.py` | +| Documents and retrieval | `routes/document_routes.py`, `src/document_processor.py`, `src/personal_docs.py`, `src/rag_manager.py`, `src/pdf_runtime.py` | +| Memory and skills | `routes/memory/`, `services/memory/`, `routes/skills_routes.py` | +| Email | `routes/email_routes.py`, `routes/email_helpers.py`, `routes/email_pollers.py`, `mcp_servers/email_server.py` | +| Calendar, contacts, notes, and tasks | `routes/calendar_routes.py`, `routes/contacts/`, `routes/note/`, `routes/task_routes.py`, `src/task_scheduler.py` | +| Media and speech | `routes/gallery/`, `routes/stt_routes.py`, `routes/tts_routes.py`, `services/stt/`, `services/tts/` | +| Persistence and operations | `core/database.py`, `src/runtime_paths.py`, `src/bg_jobs.py`, `routes/backup_routes.py`, `routes/cleanup/` | + +For a broader evidence map, see +[`system-map.md`](./system-map.md). + +## Refactor-sensitive areas + +### Shared persistence + +`core/database.py` is a central dependency containing models and shared persistence +helpers. Changes can affect routes, services, background work, tests, migrations, +and import compatibility. + +A split should not begin from file size alone. It requires: + +- an importer inventory; +- model and helper ownership decisions; +- migration compatibility checks; +- stable re-export or migration strategy; +- focused and full-suite validation. + +### Agent orchestration + +`src/agent_loop.py` coordinates model interaction, tool selection, policy decisions, +multi-round execution, and background behaviour. Extraction work must preserve tool +event semantics, policy enforcement, cancellation, and test patch points. + +Historical agent-loop modularization discussion is tracked in +[#3266](https://github.com/odysseus-dev/odysseus/issues/3266). + +### Tool implementation boundaries + +Tool implementation is no longer represented by one proposed future package alone. +Current responsibilities are distributed across: + +- `src/tool_execution.py`; +- `src/tool_schemas.py`; +- `src/tool_index.py`; +- `src/tool_policy.py`; +- `src/tool_security.py`; +- `src/agent_tools/`; +- `src/tools/`; +- remaining compatibility surfaces such as `src/tool_implementations.py`. + +Historical tool modularization work is tracked in +[#3629](https://github.com/odysseus-dev/odysseus/issues/3629). + +### Route ownership + +`routes/` now contains both flat modules and domain packages. Existing package +boundaries should be extended only through focused changes. Broad mechanical route +movement would affect registration, imports, tests, monkeypatch targets, and +compatibility paths. + +### Frontend concentration + +The no-build frontend contains several large JavaScript modules and one central CSS +file. Refactors should preserve module load order, global compatibility exports, +DOM contracts, deep-link handling, and browser behaviour. + +## Non-implemented architecture options + +> [!NOTE] +> The paths below are historical or possible design directions. They do not describe +> the current repository and are not approved implementation plans. + +Earlier planning discussed: + +- renaming `app.py` to `main.py`; +- moving agent orchestration into a new `src/agent/` package; +- introducing broad `src/domain/`, `src/infra/`, `src/api/`, or `src/pkg/` layers; +- moving all routes into domain subpackages; +- splitting database models into a new infrastructure hierarchy. + +These options should be reconsidered against the current tree rather than copied +forward as assumed targets. + +## Refactor guardrails + +- Keep structural changes behaviour-preserving. +- Change one ownership boundary at a time. +- Do not mix file movement with unrelated feature work. +- Preserve existing import and monkeypatch paths where compatibility is required. +- Identify focused tests before modifying high-authority modules. +- Validate startup, imports, and affected runtime paths. +- Avoid repository-wide package reorganizations without maintainer agreement. +- Treat generated metrics as snapshots, not architectural decisions. + +## Reproduce the snapshot + +Run these commands from the repository root. + +```bash +# Tracked directory totals +for dir in src routes core services mcp_servers scripts static/js tests; do + files="$(git ls-files "$dir" | wc -l)" + python_files="$(git ls-files "$dir" '*.py' | wc -l)" + + printf '%-14s tracked=%-5s python=%-5s\n' \ + "$dir" \ + "$files" \ + "$python_files" +done + +# Largest tracked backend files +git ls-files \ + 'app.py' \ + 'launcher.py' \ + 'setup.py' \ + 'core/*.py' \ + 'core/**/*.py' \ + 'routes/*.py' \ + 'routes/**/*.py' \ + 'services/*.py' \ + 'services/**/*.py' \ + 'src/*.py' \ + 'src/**/*.py' \ + 'mcp_servers/*.py' \ + 'scripts/*.py' \ + 'scripts/**/*.py' | +xargs wc -l | +sort -nr | +head -31 + +# Largest tracked frontend source files +git ls-files \ + 'static/*.js' \ + 'static/*.css' \ + 'static/*.html' \ + 'static/**/*.js' \ + 'static/**/*.css' \ + 'static/**/*.html' | +grep -vE '\.min\.js$' | +xargs wc -l | +sort -nr | +head -31 +``` + +## Validation for architecture changes + +Use the smallest relevant checks first, then expand according to risk: + +```bash +python3 -m compileall -q app.py core routes services src +venv/bin/python -m pytest tests/<focused-test-file>.py -q +venv/bin/python -m pytest -q +``` + +Startup, browser, Docker, and integration checks may also be required depending on +the affected boundary. + +## Related documentation + +- [Documentation style](../docs/STYLE.md) +- [Discovery maps](../discovery/README.md) +- [Current system map](../discovery/system-map.md) +- [Safety boundaries](../discovery/safety-boundaries.md) +- [Frontend module summary](../static/js/MODULE_SUMMARY.md) +- [Testing standard](../tests/TESTING_STANDARD.md) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 000000000..d00077d7e --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,87 @@ +# Architecture + +> [!NOTE] +> This document is the proposed canonical destination for stable high-level +> architecture facts. It remains subject to maintainer review. Source code, +> tests, and configuration remain authoritative for implementation-sensitive +> behaviour. + +## Purpose + +This document identifies the stable runtime boundaries and primary ownership +locations used to navigate and extend Odysseus. + +It intentionally excludes generated metrics, file-size rankings, refactor +priorities, unresolved investigation findings, and proposed package layouts. + +## Runtime structure + +| Area | Responsibility | +|---|---| +| `app.py` | FastAPI application composition and primary application entry point | +| `launcher.py` | Application launch support | +| `setup.py` | Native setup workflow | +| `core/` | Authentication, middleware, persistence, sessions, and platform primitives | +| `routes/` | HTTP and API route handlers | +| `src/` | Application orchestration, tools, providers, and runtime helpers | +| `services/` | Domain-oriented service implementations | +| `mcp_servers/` | Built-in MCP server implementations | +| `scripts/` | CLI tools, diagnostics, maintenance, and migration helpers | +| `static/` | No-build browser frontend and bundled assets | +| `tests/` | Automated tests and supporting test infrastructure | + +## Subsystem boundaries + +| Subsystem | Primary implementation locations | +|---|---| +| Application startup | `app.py`, `src/app_initializer.py`, `core/` | +| Authentication and sessions | `core/auth.py`, `core/middleware.py`, `core/session_manager.py`, `routes/auth_routes.py` | +| Chat and streaming | `routes/chat_routes.py`, `routes/chat_helpers.py`, `src/chat_handler.py`, `src/chat_processor.py`, `src/llm_core.py` | +| Agents and tools | `src/agent_loop.py`, `src/tool_execution.py`, `src/agent_tools/`, `src/tools/`, `src/tool_policy.py`, `src/tool_security.py` | +| Models and providers | `routes/model_routes.py`, `src/model_discovery.py`, `src/model_capabilities.py`, `src/endpoint_resolver.py`, `src/llm_core.py` | +| Cookbook and hardware fit | `routes/cookbook_routes.py`, `routes/cookbook_helpers.py`, `src/cookbook_serve_lifecycle.py`, `services/hwfit/` | +| Search and research | `routes/search_routes.py`, `services/search/`, `routes/research/`, `services/research/`, `src/deep_research.py` | +| Documents and retrieval | `routes/document_routes.py`, `src/document_processor.py`, `src/personal_docs.py`, `src/rag_manager.py`, `src/pdf_runtime.py` | +| Memory and skills | `routes/memory/`, `services/memory/`, `routes/skills_routes.py` | +| Email | `routes/email_routes.py`, `routes/email_helpers.py`, `routes/email_pollers.py`, `mcp_servers/email_server.py` | +| Calendar, contacts, notes, and tasks | `routes/calendar_routes.py`, `routes/contacts/`, `routes/note/`, `routes/task_routes.py`, `src/task_scheduler.py` | +| Media and speech | `routes/gallery/`, `routes/stt_routes.py`, `routes/tts_routes.py`, `services/stt/`, `services/tts/` | +| Persistence and operations | `core/database.py`, `src/runtime_paths.py`, `src/bg_jobs.py`, `routes/backup_routes.py`, `routes/cleanup/` | + +## Architectural constraints + +- Preserve established import and compatibility paths unless a focused change + explicitly migrates them. +- Keep HTTP concerns in route modules and reusable domain behaviour in runtime + or service modules. +- Treat shared persistence, agent orchestration, tool execution, and application + startup as high-authority boundaries. +- Change one ownership boundary at a time. +- Do not mix structural movement with unrelated feature behaviour. +- Validate affected imports, startup paths, compatibility surfaces, and tests. + +## Frontend + +The browser frontend is a no-build ES-module application under `static/`. + +Its maintained module-level structure is documented in +[`static/js/MODULE_SUMMARY.md`](../static/js/MODULE_SUMMARY.md). + +## Investigation and snapshots + +Non-canonical investigation material is maintained under [`discovery/`](../discovery/). + +The following documents may contain dated observations, metrics, unresolved +questions, or historical planning and must not be treated as specifications: + +- [`discovery/system-map.md`](../discovery/system-map.md) +- [`discovery/architecture-runtime-inventory.md`](../discovery/architecture-runtime-inventory.md) + +## Documentation authority + +- Code, tests, and configuration define implemented behaviour. +- Mature specifications define accepted subsystem behaviour where they exist. +- Following maintainer acceptance, this document will define the high-level + architecture map. +- Discovery documents preserve evidence and uncertainty but remain + non-canonical. diff --git a/docs/STYLE.md b/docs/STYLE.md new file mode 100644 index 000000000..23ea6d2c5 --- /dev/null +++ b/docs/STYLE.md @@ -0,0 +1,72 @@ +# Documentation style + +This guide defines the shared structure and writing conventions for Odysseus documentation. + +## Principles + +- Write for a clear audience and purpose. +- State whether a document is canonical, informational, a snapshot, or planning material. +- Prefer current behaviour over historical explanation. +- Link to source files, tests, issues, or other documentation when useful. +- Separate verified behaviour from assumptions, open questions, and future work. +- Keep headings descriptive and consistent. +- Use Markdown callouts where status or risk must be visible. +- Do not use emojis. + +## Document status + +Use a status callout near the top when the document is not normal canonical guidance. + +### Canonical documentation + +> [!IMPORTANT] +> This document describes accepted current behaviour. Verify implementation-sensitive details against the current code and tests. + +### Discovery material + +> [!NOTE] +> This is non-canonical discovery material. It records code-grounded observations and open questions. + +### Snapshot or inventory + +> [!WARNING] +> This document is a dated snapshot. Counts, paths, and implementation details may drift as the codebase changes. + +### Planning material + +> [!NOTE] +> This document records planning context. It does not define current runtime behaviour or guarantee future implementation. + +## Recommended structure + +Use the following sections where relevant: + +1. Title +2. Purpose or status callout +3. Scope +4. Current behaviour or guidance +5. Safety, limitations, or known gaps +6. Validation or evidence +7. Related documentation + +Not every document needs every section. + +## Writing style + +- Use concise sentences. +- Prefer direct language. +- Avoid jokes, filler, and informal warnings. +- Avoid repeating the same guidance across several files. +- Link to the owning document instead of duplicating large sections. +- Use lists for procedures, requirements, and comparisons. +- Use tables only when they improve scanning. +- Use fenced code blocks with an appropriate language identifier. +- Use relative repository links for internal files. + +## Authority + +The current code, tests, and configuration are the source of truth for implemented behaviour. + +Canonical documentation describes accepted behaviour and supported workflows. + +Discovery, inventory, and planning documents must identify themselves explicitly and must not silently become behavioural specifications. diff --git a/docs/pr-blocker-audit.md b/docs/pr-blocker-audit.md index b56f28cb3..75d94bc87 100644 --- a/docs/pr-blocker-audit.md +++ b/docs/pr-blocker-audit.md @@ -181,8 +181,8 @@ Dirty, blocked, conflicting, and unknown merge states are shown as risk/caution ## Validation ```bash -python3 -m py_compile scripts/pr_blocker_audit.py tests/test_pr_blocker_audit.py -python3 -m pytest tests/test_pr_blocker_audit.py -q +venv/bin/python -m py_compile scripts/pr_blocker_audit.py tests/test_pr_blocker_audit.py +venv/bin/python -m pytest tests/test_pr_blocker_audit.py -q python3 scripts/pr_blocker_audit.py --help git diff --check ``` diff --git a/specs/architecture-runtime-inventory.md b/specs/architecture-runtime-inventory.md index 5c8e4bc21..4ec156b15 100644 --- a/specs/architecture-runtime-inventory.md +++ b/specs/architecture-runtime-inventory.md @@ -1,412 +1,31 @@ -# Architecture Runtime Inventory - -> **Purpose**: Phase 0 planning baseline for codebase readability improvements (#4071). -> **Parent issue**: [#4082](https://github.com/odysseus-dev/odysseus/issues/4082) -> **Last updated**: dev@b58af42 | 2026-06-16 -> **Status**: Draft — to be reviewed before follow-up slices open. -> **Snapshot basis**: Importer / file / import-line counts are refreshed to `dev@b58af42` (2026-06-16) and are recomputable via the commands in §3.4. **Line counts** in §2.1 / §2.2 are a snapshot from an earlier baseline and drift as `dev` moves — recompute any of them with `wc -l <file>`. This inventory tracks structure and risk, not live metrics. - -This document maps the current runtime module structure, identifies high-risk boundaries, and recommends safe first refactor slices. It does **not** move files, change imports, or alter runtime behavior. - ---- - -## 1. Current Structure Overview - -### 1.1 Top-Level Layout - -``` -odysseus/ -├── app.py # FastAPI app entrypoint (1,145 lines) -├── conf/ # Configuration (config.py, settings.py, settings_scrub.py) -├── src/ # 95 flat .py files + 2 subdirectories -│ ├── agent_tools/ # Tool helpers: document, filesystem, subprocess, web -│ └── search/ # Search subsystem -├── routes/ # 54 flat .py files — HTTP route handlers -├── core/ # 10 files — database models, auth, middleware, session -├── mcp_servers/ # 5 files — MCP server implementations -├── scripts/ # CLI tools and one-shot scripts -├── static/ # Frontend HTML/CSS/JS -├── tests/ # 583 test files (~54,800 lines) -└── services/ # (exists as needed) -``` - -### 1.2 Directory Flatness Metric - -| Directory | Flat `.py` Files | Subdirectories | Concern | -|-----------|-----------------|----------------|---------| -| `src/` | **95** | 2 (`agent_tools/`, `search/`) | No domain grouping; 95 files in one directory | -| `routes/` | **54** | 0 | All route handlers in one flat directory | -| `core/` | 10 | 0 | Manageable, but `database.py` is oversized | - ---- - -## 2. Largest Runtime Modules - -### 2.1 Python Backend - -| Rank | File | Lines | Classes | Functions | Risk | -|------|------|-------|---------|-----------|------| -| 1 | `src/tool_implementations.py` | **4,032** | 0 | ~48 | **HIGH** | -| 2 | `routes/email_routes.py` | **3,245** | — | — | **MEDIUM** | -| 3 | `routes/cookbook_routes.py` | **2,969** | — | — | **MEDIUM** | -| 4 | `src/agent_loop.py` | **2,961** | 0 | ~24 | **HIGH** | -| 5 | `src/task_scheduler.py` | **2,330** | — | 5 | MEDIUM | -| 6 | `routes/model_routes.py` | **2,266** | — | — | MEDIUM | -| 7 | `core/database.py` | **2,265** | 28 | ~59 helpers | **HIGH** | -| 8 | `src/builtin_actions.py` | **2,262** | 2 | ~24 | MEDIUM | -| 9 | `src/llm_core.py` | **2,164** | — | — | MEDIUM | -| 10 | `mcp_servers/email_server.py` | 2,197 | — | — | LOW (separate process) | -| 11 | `src/visual_report.py` | 1,918 | — | — | LOW | -| 12 | `routes/gallery_routes.py` | 1,896 | — | — | LOW | -| 13 | `src/ai_interaction.py` | 1,846 | — | — | MEDIUM | -| 14 | `routes/document_routes.py` | 1,717 | — | — | LOW | -| 15 | `routes/skills_routes.py` | 1,648 | — | — | LOW | - -**Heuristic**: Files > 2,000 lines with 20+ public symbols and many importers are the highest-risk splits. Files 1,000–2,000 lines are medium-risk if tightly coupled. - -### 2.2 Frontend - -| File | Lines | Concern | -|------|-------|---------| -| `static/style.css` | **36,653** | Entire app CSS in one file (tracked separately in #2617) | -| `static/js/document.js` | **9,776** | Single JS file for document functionality | -| `static/js/slashCommands.js` | 6,498 | | -| `static/js/settings.js` | 5,266 | | -| `static/js/emailLibrary.js` | 5,217 | | -| `static/js/notes.js` | 5,124 | | -| `static/js/chat.js` | 4,985 | | -| `static/app.js` | 4,090 | | - -**Note**: Frontend modularization is tracked separately in #2617 (CSS) and is not the focus of this Phase 0 inventory. Frontend is listed here for completeness but follow-up slices should target Python backend boundaries first. - ---- - -## 3. Import Dependency Graph - -### 3.1 Who Depends on `core/database.py` - -**102 files** import from `core.database` — this is the most depended-upon module: - -- All route handlers (`routes/*.py`) -- Most `src/*.py` files -- `core/session_manager.py`, `core/auth.py` -- Multiple test files - -**Implication**: Any split of `core/database.py` is the highest-risk refactor. It should be tackled **last**, never first. - -### 3.2 Who Depends on `src/tool_implementations.py` - -**17 files** import from `src.tool_implementations`: -- `src/agent_loop.py`, `src/builtin_actions.py`, `src/tool_index.py` -- `src/task_scheduler.py`, `src/tool_policy.py` -- Various tests - -### 3.3 Who Depends on `src/agent_loop.py` - -**22 files** import from `src.agent_loop`: - -- `src/tool_policy.py`, `src/teacher_escalation.py`, `src/bg_monitor.py` -- `src/task_scheduler.py` -- Multiple test files - -### 3.4 Cross-Layer Import Violations - -**`src/` importing from `routes/`** (backwards dependency — domain logic depending on HTTP layer): - -``` -src/tool_implementations.py ──→ routes/calendar_routes.py -src/tool_implementations.py ──→ routes/cookbook_helpers.py -src/tool_implementations.py ──→ routes/email_helpers.py -src/tool_implementations.py ──→ routes/email_pollers.py -src/tool_implementations.py ──→ routes/email_routes.py -src/tool_implementations.py ──→ routes/model_routes.py -src/tool_implementations.py ──→ routes/note_routes.py -src/tool_implementations.py ──→ routes/prefs_routes.py -``` - -> These are **runtime imports** (inside function bodies, not at module top), which mitigates circular import risk but indicates fuzzy layer boundaries. Function-level inline imports from the HTTP layer into business logic are a code smell. - -**Import counts (top-level)**: -| Direction | Count | Notes | -|-----------|-------|-------| -| `routes/` → `src/` | **374** | Expected: HTTP handlers call domain logic | -| `routes/` → `core/` | **126** | Expected: handlers access DB models | -| `src/` → `routes/` | **31** | **Unexpected**: domain logic reaching into HTTP layer (direct grep of import lines referencing `routes/`) | -| `src/` → `core/` | **106** | Acceptable but could be reduced with a data-access layer | - -> **How the metrics in this document are computed** — recompute against current `dev` before treating any count as authoritative (the tree drifts; these numbers are a snapshot, not a live value): -> - `src/` flat `.py` files: `find src -maxdepth 1 -name '*.py' | wc -l` -> - `tests/` test files: `find tests -name 'test_*.py' | wc -l` -> - `core.database` importers: `grep -rlE '(from|import) +core\.database' --include='*.py' . | grep -v core/database.py | wc -l` -> - `src.agent_loop` importers: `grep -rlE '(from|import) +src\.agent_loop' --include='*.py' . | grep -v src/agent_loop.py | wc -l` -> - Cross-layer import lines: `grep -rhE '(from|import) +<pkg>' --include='*.py' <dir>/ | wc -l` (e.g. `(from|import) +routes` over `src/`) - ---- - -## 4. Route Ownership Map - -Routes can be grouped into logical feature domains. Current flat structure obscures these boundaries: - -| Domain | Route Files | Total Lines | Review Complexity | -|--------|-------------|-------------|-------------------| -| **Email** | `email_routes.py`, `email_helpers.py`, `email_pollers.py` | 5,936 | HIGH — most complex domain | -| **Chat / Agent** | `chat_routes.py`, `chat_helpers.py`, `shell_routes.py`, `codex_routes.py`, `skills_routes.py` | 6,365 | HIGH — core interaction surface | -| **Cookbook** | `cookbook_routes.py`, `cookbook_helpers.py`, `cookbook_output.py` | 4,110 | MEDIUM | -| **Model / LLM** | `model_routes.py`, `assistant_routes.py`, `copilot_routes.py` | 2,764 | MEDIUM | -| **Calendar / Contacts** | `calendar_routes.py`, `contacts_routes.py` | 2,336 | MEDIUM | -| **Documents** | `document_routes.py`, `document_helpers.py` | 1,954 | LOW | -| **Auth** | `auth_routes.py`, `api_token_routes.py`, `device_flow.py` | 1,171 | LOW | -| **Tasks** | `task_routes.py` (standalone) | 1,157 | LOW | -| **Session** | `session_routes.py` (standalone) | 1,287 | LOW | -| **Gallery** | `gallery_routes.py`, `gallery_helpers.py` | 1,896 | LOW | -| **Memory** | `memory_routes.py` | — | LOW | -| **Research** | `research_routes.py` | — | LOW | -| **MCP** | `mcp_routes.py` | — | LOW | -| **Notes** | `note_routes.py` | — | LOW | -| **Other** | `prefs_routes.py`, `upload_routes.py`, `vault_routes.py`, `webhook_routes.py`, `workspace_routes.py`, `search_routes.py`, `history_routes.py`, `hwfit_routes.py`, `preset_routes.py`, `signature_routes.py`, `backup_routes.py`, `cleanup_routes.py`, `diagnostics_routes.py`, `embedding_routes.py`, `emoji_routes.py`, `font_routes.py`, `stt_routes.py`, `tts_routes.py`, `compare_routes.py`, `personal_routes.py`, `editor_draft_routes.py`, `admin_wipe_routes.py`, `chatgpt_subscription_routes.py` | 2,000+ | LOW individual, HIGH cumulative | - ---- - -## 5. Tool Registry & Implementation Boundaries - -### 5.1 Current Tool Architecture - -| Component | File | Lines | Role | -|-----------|------|-------|------| -| Tool schemas | `src/tool_schemas.py` | 1,392 | JSON Schema tool definitions (Duck-TypedDict) | -| Tool index | `src/tool_index.py` | 542 | RAG-based tool retrieval from ChromaDB | -| Tool implementations | `src/tool_implementations.py` | 4,032 | 33 `do_*` functions — all tool execution logic | -| Tool security | `src/tool_security.py` | — | Owner-scoped tool blocking | -| Tool policy | `src/tool_policy.py` | — | Guide-only directive, plan-mode disabled tools | -| Tool utils | `src/tool_utils.py` | — | Shared tool helpers | - -### 5.2 Tool Implementation Categories - -The 33 `do_*` functions in `tool_implementations.py` fall into natural domain groups — the basis for slice 1's split in §6.2: - -| Category | `do_*` functions | Count | -|----------|------------------|-------| -| **System / config** | `do_manage_skills`, `do_manage_tasks`, `do_manage_endpoints`, `do_manage_mcp`, `do_manage_webhooks`, `do_manage_tokens`, `do_manage_settings`, `do_api_call`, `do_app_api` | 9 | -| **Cookbook / model serving** | `do_download_model`, `do_serve_model`, `do_list_served_models`, `do_stop_served_model`, `do_tail_serve_output`, `do_list_downloads`, `do_cancel_download`, `do_search_hf_models`, `do_adopt_served_model`, `do_list_cookbook_servers`, `do_list_serve_presets`, `do_serve_preset`, `do_list_cached_models` | 13 | -| **Notes** | `do_manage_notes` | 1 | -| **Calendar** | `do_manage_calendar` | 1 | -| **Search** | `do_search_chats` | 1 | -| **Research** | `do_manage_research`, `do_trigger_research` | 2 | -| **Contacts** | `do_resolve_contact`, `do_manage_contact` | 2 | -| **Vault** | `do_vault_search`, `do_vault_get`, `do_vault_unlock` | 3 | -| **Image** | `do_edit_image` | 1 | -| | **Total** | **33** | - -> Low-level tools (filesystem, subprocess, web fetch, document parsing) live in `src/agent_tools/`, **not** in `tool_implementations.py` — out of scope for this split. - ---- - -## 6. Risk Assessment & Candidate Slice Ranking - -> **Candidate proposals, not a committed plan.** The rankings, package shapes (e.g. `src/pkg/`, `src/domain/`, `src/infra/`, `src/api/`), split ordering, and route-grouping strategy below are **options for maintainer discussion**. Per #4082/#4071, slice ownership and order are settled by maintainers before any follow-up PR. §1–§3 above are the factual current-state inventory. - -### 6.1 Risk Scale - -| Level | Criteria | -|-------|----------| -| **LOW** | File has ≤3 importers AND ≤500 lines, OR is a pure refactor with clear boundaries | -| **MEDIUM** | File has 4–15 importers OR 500–1,500 lines | -| **HIGH** | File has 16+ importers OR >2,000 lines, OR has cross-layer import violations | - -### 6.2 Ranked Split Candidates - -| Priority | Target | Risk | Rationale | -|----------|--------|------|-----------| -| **1** | `src/tool_implementations.py` → `src/tools/*.py` | **MEDIUM** | 4,032 lines → ~10 files by tool category. Already has natural boundaries. 17 importers, tracked in #3629. Use `__init__.py` shim to keep existing imports working. | -| **2** | `routes/` → domain subdirectories (one domain per PR) | **MEDIUM** | 54 flat files. Done **one domain at a time** (e.g. a standalone PR for the email domain, then chat, …), not a broad reorganization — route modules carry helper imports, registration assumptions, and test import paths. | -| **3** | `src/agent_loop.py` → `src/agent/loop.py` + submodules | **MEDIUM-HIGH** | 2,961 lines, 24 functions. Can extract prompt building, classification, verification, and runaway detection. Tracked in #3266. | -| **4** | `src/` → `src/pkg/`, `src/domain/`, `src/infra/`, `src/api/` | **MEDIUM** | Structural reorganization. Split flat `src/` into layered packages. Must come after routes and tools are stable. | -| **5** | `routes/email_*.py` consolidation | **LOW** | Already grouped by filename prefix. Low-risk cleanup within the email domain. | -| **6** | `core/database.py` → `src/infra/database/models/*.py` | **HIGH** | 28 classes, 102 importers. Highest-risk split. Must be **last** in any sequence. Requires careful import shim strategy. | -| **7** | Frontend CSS modularization | **MEDIUM** | 36,653 lines. Tracked in #2617. Separate timeline from backend work. | -| **8** | Frontend JS modularization | **MEDIUM** | 9,776 lines in `document.js`. Introduce ES modules at minimum. | - -### 6.3 Candidate First 3 Behavior-Preserving Slices - -**Slice 1: Split `tool_implementations.py`** (Lowest-risk high-impact) - -- Create `src/tools/` package with one file per tool category -- Add `src/tools/__init__.py` re-exporting all symbols with current names -- Update 17 importers to use new paths (can be deferred via shim) -- Validation: `python -m pytest tests/ -x -q` + manual smoke test of tool execution -- Reference: #3629 - -**Slice 2: Group `routes/` by domain** (one domain per PR, not a broad sweep) - -Route modules carry helper imports, router registration assumptions, and test import paths, so this must be done **one domain at a time** rather than as a single reorganization PR. Example sequence (each its own PR): - -- PR 2a: move the **email** domain (`email_routes.py`, `email_helpers.py`, `email_pollers.py`) → `routes/email/` + shim -- PR 2b: move the **chat/agent** domain → `routes/chat/` + shim -- PR 2c: move the **cookbook** domain → `routes/cookbook/` + shim -- …and so on per domain from §4 - -Each PR: add `__init__.py` re-exporting old names, update `app.py` router imports, validation `python app.py` starts clean. **No behavior change** — pure file reorganization. - -**Slice 3: Extract `agent_loop.py` submodules** (Improve reviewability) - -- Move prompt assembly → `src/agent/prompt.py` -- Move request classification → `src/agent/classifier.py` -- Move sub-agent verification → `src/agent/verifier.py` -- Move runaway detection → `src/agent/runaway.py` -- Move context management → `src/agent/context.py` -- Keep `src/agent/loop.py` as the main orchestration module -- Validation: `python -m pytest tests/test_agent_loop.py tests/test_loop_breaker_runaway.py -v` - ---- - -## 7. Safety Guardrails for Follow-Up Work - -Per maintainer guidance in #4082 and #4071: - -- [ ] **One domain/slice per PR** — never mix multiple reorganizations -- [ ] **No behavior changes** mixed with file moves — pure reorganization only -- [ ] **Keep compatibility shims** — `__init__.py` re-exports for all existing import paths -- [ ] **Add or identify focused tests** before risky splits -- [ ] **Do not start with `core/database.py`** or broad route movement unless this inventory shows a safe boundary -- [ ] **Prefer small, reviewable slices** over large restructures -- [ ] **No packaging/runtime/tooling migration** mixed into file moves -- [ ] **No frontend framework migration** inside this stabilization lane -- [ ] **Validate with `python -m compileall`** — every PR must pass CI checks -- [ ] **Validate with `pytest`** — run the full test suite before opening each PR - ---- - -## 8. Validation Commands - -Each follow-up PR should be verifiable with these commands before submission: - -```bash -# Syntax check — must pass with zero errors -python -m compileall src/ routes/ core/ conf/ - -# Full test suite — must match baseline pass rate -python -m pytest tests/ -x -q - -# Import shim verification — existing import paths must still work -python -c "from src.tool_implementations import do_search_chats; print('OK')" - -# App startup smoke test (if backend touched) -timeout 5 python app.py 2>&1 | head -5 || true -``` - ---- - -## 9. Open Questions - -1. Is `#2538` (specs ground truth) the canonical behavior map baseline, and should this inventory be kept in sync with those specs once merged? -2. Should route grouping follow the domain map proposed here, or is there a different taxonomy preferred by maintainers? -3. For the `tool_implementations.py` split (#3629), is the tool categorization in §5.2 acceptable, or should it follow a different grouping? -4. Should compatibility shims (`__init__.py`) be temporary (removed in a follow-up wave) or permanent? -5. Should an ADR (Architecture Decision Record) document be started to track decisions made during this process? - ---- - -## 10. Future Direction (NOT current state) - -The following are **future refactor targets** (candidate directions **pending maintainer agreement**, not committed), recorded here so this inventory does not imply they exist today. None of them are present in the current `dev` tree: - -- `main.py` — proposed rename of the `app.py` entrypoint. Today the app boots via `app.py`. -- `src/agent/` — proposed package to hold `agent_loop.py` submodules (prompt/classifier/verifier/runaway/context). Today `agent_loop.py` is a single flat file in `src/`. -- `src/infra/`, `src/domain/`, `src/pkg/`, `src/api/` — proposed layered reorganization of the flat `src/` directory (slice 4 in §6). - -These become real only when the corresponding slices land. - ---- - -## Appendix A: File Listing - -### `src/` (95 files — 61 shown; run `ls src/*.py` for the full list) - -``` -agent_loop.py tool_implementations.py tool_schemas.py -tool_index.py tool_security.py tool_policy.py -tool_utils.py builtin_actions.py task_scheduler.py -llm_core.py model_context.py model_discovery.py -session_search.py context_budget.py context_compactor.py -ai_interaction.py action_intents.py agent_runs.py -app_helpers.py app_initializer.py config.py -database.py memory.py memory_provider.py -secret_storage.py prompt_security.py url_security.py -url_safety.py rate_limiter.py cleanup_service.py -readiness.py service_health.py exceptions.py -request_models.py assistant_log.py bg_monitor.py -builtin_mcp.py chat_helpers.py chroma_client.py -document_processor.py embedding_lanes.py deep_research.py -research_handler.py research_utils.py personal_docs.py -rag_manager.py rag_singleton.py topic_analyzer.py -visual_report.py youtube_handler.py pdf_forms.py -pdf_form_doc.py pdf_runtime.py caldav_writeback.py -email_thread_parser.py text_helpers.py user_time.py -teacher_escalation.py cookbook_serve_lifecycle.py -chatgpt_subscription.py mcp_manager.py -``` - -### `routes/` (54 files) - -``` -__init__.py _validators.py -auth_routes.py api_token_routes.py device_flow.py -chat_routes.py chat_helpers.py shell_routes.py -codex_routes.py skills_routes.py -email_routes.py email_helpers.py email_pollers.py -cookbook_routes.py cookbook_helpers.py cookbook_output.py -model_routes.py assistant_routes.py copilot_routes.py -calendar_routes.py contacts_routes.py -document_routes.py document_helpers.py -gallery_routes.py gallery_helpers.py -task_routes.py session_routes.py -note_routes.py memory_routes.py research_routes.py -mcp_routes.py search_routes.py history_routes.py -webhook_routes.py workspace_routes.py upload_routes.py -vault_routes.py prefs_routes.py preset_routes.py -signature_routes.py personal_routes.py hwfit_routes.py -backup_routes.py cleanup_routes.py diagnostics_routes.py -embedding_routes.py emoji_routes.py font_routes.py -stt_routes.py tts_routes.py compare_routes.py -editor_draft_routes.py chatgpt_subscription_routes.py admin_wipe_routes.py -``` - -### `core/` (10 files) - -``` -__init__.py constants.py database.py models.py -auth.py middleware.py session_manager.py exceptions.py -atomic_io.py platform_compat.py -``` - ---- - -## Appendix B: Key Import Relationships - -``` -core/database.py ←── 102 importers (routes/*, src/*, core/*, tests/*) - ↑ - ├── routes/auth_routes.py - ├── routes/email_routes.py - ├── src/builtin_actions.py - ├── src/task_scheduler.py - ├── src/tool_implementations.py (inline) - └── ...97 more - -src/tool_implementations.py ←── 17 importers - ↑ - ├── src/agent_loop.py - ├── src/builtin_actions.py - ├── src/tool_index.py - ├── src/task_scheduler.py - ├── src/tool_policy.py - └── ...12 more (mostly tests) - -src/agent_loop.py ←── 22 importers - ↑ - ├── src/tool_policy.py - ├── src/teacher_escalation.py - ├── src/bg_monitor.py - ├── src/task_scheduler.py - └── 18 more (incl. tests) -``` +# Architecture runtime inventory disposition + +> [!NOTE] +> The dated architecture runtime inventory has been classified and transferred +> to [`discovery/architecture-runtime-inventory.md`](../discovery/architecture-runtime-inventory.md) +> after verification of its stable facts. +> +> Stable runtime structure and subsystem boundaries were transferred to the +> proposed canonical destination, +> [`docs/ARCHITECTURE.md`](../docs/ARCHITECTURE.md), for maintainer review. + +## Classification + +| Content | Disposition | +|---|---| +| Stable runtime structure | Proposed canonical architecture document | +| Stable subsystem boundaries | Proposed canonical architecture document | +| Frontend module organization | `static/js/MODULE_SUMMARY.md` | +| Counts, rankings, and generated metrics | Non-canonical discovery inventory | +| Investigation context and unresolved questions | Non-canonical discovery material | +| Refactor options and prioritization | Issues, Plane, or discovery material | + +No dated metrics, prioritization, or investigation findings remain in this +specification path. + +## Review status + +- Findings and scope have been validated. +- Transfer verification has completed. +- The final canonical status of `docs/ARCHITECTURE.md` requires maintainer review. +- No implementation behaviour, test taxonomy, or runtime files were changed. diff --git a/tests/LAYOUT_INVENTORY.md b/tests/LAYOUT_INVENTORY.md index 34c719774..4a14961be 100644 --- a/tests/LAYOUT_INVENTORY.md +++ b/tests/LAYOUT_INVENTORY.md @@ -1,202 +1,152 @@ -# Test Layout Inventory +# Test layout inventory + +> [!NOTE] +> This document is a completed implementation snapshot for the first low-risk +> test-directory reorganization. It does not propose additional file moves and +> is not the canonical definition of the current test taxonomy. + +- **Original inventory:** #3712 +- **Parent tracker:** #2523 +- **Implemented by:** #3842 +- **Status:** completed historical snapshot ## Purpose -Inventory for the first low-risk split of the flat `tests/` directory -(issue #3712, parent #2523). This document only records *what* should move -first and *why*; it moves nothing. The actual move is a separate, mechanical -PR that relocates the listed files verbatim and changes no test content. +This document preserves the reasoning, scope, and validation contract for the +first mechanical reorganization of the formerly flat `tests/` directory. -The target layout and category definitions come from -[`TESTING_STANDARD.md`](./TESTING_STANDARD.md); the collection-time markers -come from [`_taxonomy.py`](./_taxonomy.py), which classifies by **filename -tokens only** (paths are ignored, except the `tests/helpers/` rule). A file -keeps its `area_*`/`sub_*` markers when moved into a subdirectory, and -`conftest.py` discovers marker names recursively (`rglob`), so a move does not -disturb marker registration or focused selection. +The original change moved 28 tests classified as `area_cli` into `tests/cli/` +without changing their assertions, helpers, taxonomy, or runtime behaviour. -## Current low-risk candidate groups +Current testing guidance and classification rules are maintained in: -Groups whose tests need no route/app setup and no real DB/session setup: +- [`README.md`](./README.md) +- [`TESTING_STANDARD.md`](./TESTING_STANDARD.md) +- [`_taxonomy.py`](./_taxonomy.py) -1. **CLI / script tests** (`area_cli`, 28 files) - load `scripts/` entry - points via `tests.helpers.cli_loader.load_script`; DB access is stubbed - with `tests.helpers.db_stubs` (`SessionLocal` is a plain stub attribute). - No `TestClient`, no FastAPI app import, no SQLite files. -2. **Helper self-tests** (`area_helpers`) - e.g. `test_helpers_import_state.py`, - `test_db_stubs_helper.py`. Safe but tiny (two files), and they test the - shared helpers from the #3685 audit (merged) that the rest of the suite - depends on; little payoff as a first slice. -3. **Pure unit / parsing tests** (`area_unit`) - `*_nonstring.py`, - `*_nondict.py`, parsing tests. Large and heterogeneous; some touch - provider/session modules, so the boundary is less crisp. -4. **Static checks** - e.g. `test_readme_ascii_fenced.py`, - `test_docs_no_orphan_images.py`. Safe but tiny and `uncategorized` in the - taxonomy, so a move buys little and matches no existing marker. +The current repository and test suite remain authoritative when this snapshot +and the implementation differ. -Not candidates for the first move (per #3712 guidance): security/owner-scope -tests, route/API tests, DB/session-heavy tests, auth/session concurrency -tests, and the taxonomy/runner infrastructure tests that changed recently -(#3491, #3556, #3659, #3711). +## Why CLI tests were selected -## Recommended first move +The CLI and script tests were selected as the first low-risk group because they +formed a narrow, mechanically identifiable boundary: -**CLI / script tests → `tests/cli/`** +- script entry points were loaded through shared CLI helpers; +- database access used controlled stubs rather than persistent sessions; +- the tests did not require FastAPI application or route setup; +- the tests did not require a real database; +- taxonomy classification could be derived consistently from filenames; +- absolute helper imports remained valid after relocation. -Why this group over the alternatives: +This reduced the risk relative to route, security, session, provider, and +heterogeneous unit-test groups. -- Lowest coupling: every file imports only the script under test (via - `cli_loader`) plus `tests.helpers` stubs - no app, no routes, no real DB. -- Crisp, machine-checkable boundary: the set is exactly the files classified - `area_cli` by `_taxonomy.py`, so before/after selection counts can be - compared mechanically. -- Already the planned target dir for this category in `TESTING_STANDARD.md` - (`tests/cli/`). -- Absolute imports (`from tests.helpers...`) and unique basenames mean no - import-order or module-name collisions after the move. -- Lower risk than helper self-tests (tiny group, little payoff), unit tests - (fuzzy boundary), or anything security/route/session-shaped. +## Completed implementation -## Files included in the first move +Repository history records the implementation as: -The 28 files classified `area_cli` (verified against `_taxonomy.py`): +- `83af3ca test: move area_cli tests into cli directory (#3842)` -Note: this inventory was refreshed against current `dev` after `tests/test_research_cli_status.py` was added to the `area_cli` set. +The following 28 files were included in that completed move: -- `tests/test_calendar_cli_name.py` -- `tests/test_contacts_cli_rows.py` -- `tests/test_cookbook_cli_state.py` -- `tests/test_docs_cli_content_length.py` -- `tests/test_gallery_cli_album_count.py` -- `tests/test_gallery_cli_preview.py` -- `tests/test_logs_cli_resolve_nonstring.py` -- `tests/test_mail_cli_read_empty_fetch.py` -- `tests/test_mail_cli_recipients.py` -- `tests/test_mcp_cli_env_serialize.py` -- `tests/test_mcp_cli_json.py` -- `tests/test_memory_cli_rows.py` -- `tests/test_notes_cli_items.py` -- `tests/test_personal_cli_rows.py` -- `tests/test_preset_cli_invalid_entries.py` -- `tests/test_preset_cli_set_corrupt_entry.py` -- `tests/test_preset_cli_store.py` -- `tests/test_research_cli_preview.py` -- `tests/test_research_cli_status_filter.py` -- `tests/test_research_cli_status.py` -- `tests/test_research_cli_store.py` -- `tests/test_sessions_cli.py` -- `tests/test_signature_cli_export.py` -- `tests/test_skills_cli_preview.py` -- `tests/test_skills_cli_rows.py` -- `tests/test_tasks_cli_preview.py` -- `tests/test_theme_cli_store.py` -- `tests/test_webhook_cli_mask.py` +- `tests/cli/test_calendar_cli_name.py` +- `tests/cli/test_contacts_cli_rows.py` +- `tests/cli/test_cookbook_cli_state.py` +- `tests/cli/test_docs_cli_content_length.py` +- `tests/cli/test_gallery_cli_album_count.py` +- `tests/cli/test_gallery_cli_preview.py` +- `tests/cli/test_logs_cli_resolve_nonstring.py` +- `tests/cli/test_mail_cli_read_empty_fetch.py` +- `tests/cli/test_mail_cli_recipients.py` +- `tests/cli/test_mcp_cli_env_serialize.py` +- `tests/cli/test_mcp_cli_json.py` +- `tests/cli/test_memory_cli_rows.py` +- `tests/cli/test_notes_cli_items.py` +- `tests/cli/test_personal_cli_rows.py` +- `tests/cli/test_preset_cli_invalid_entries.py` +- `tests/cli/test_preset_cli_set_corrupt_entry.py` +- `tests/cli/test_preset_cli_store.py` +- `tests/cli/test_research_cli_preview.py` +- `tests/cli/test_research_cli_status.py` +- `tests/cli/test_research_cli_status_filter.py` +- `tests/cli/test_research_cli_store.py` +- `tests/cli/test_sessions_cli.py` +- `tests/cli/test_signature_cli_export.py` +- `tests/cli/test_skills_cli_preview.py` +- `tests/cli/test_skills_cli_rows.py` +- `tests/cli/test_tasks_cli_preview.py` +- `tests/cli/test_theme_cli_store.py` +- `tests/cli/test_webhook_cli_mask.py` -## Files intentionally excluded +## Original exclusions -- `tests/test_backup_cli_security.py` - classifies as `area_security` - (security outranks cli in the taxonomy); moving it into `tests/cli/` would - make the directory disagree with its marker. It belongs with the security - group in a later phase. -- `tests/test_run_focus.py`, `tests/test_taxonomy.py` - taxonomy/runner - infrastructure tests, recently changed (#3556, #3659); they also pin - flat-layout paths (e.g. `tests/test_auth_config_lock_concurrency.py` in - `test_run_focus.py`), so they stay put. -- Script-like but `uncategorized` files - `test_pr_blocker_audit.py`, - `test_update_database_script.py`, `test_windows_update_script.py`, - `test_setup_admin_user.py`, `test_amd_gpu_check_args.py`, `test_hwfit_*.py`. - They exercise `scripts/` too, but moving them would make `tests/cli/` - diverge from the `area_cli` marker set. Reclassify or move them in a later, - separate slice. -- Everything else (security, routes, services, unit, js, helpers) - out of - scope for the first move by design. +The original inventory deliberately excluded: -## How this was verified +- `tests/test_backup_cli_security.py`, because its security classification + outranked the CLI token in its filename; +- taxonomy and focused-runner infrastructure, including + `tests/test_taxonomy.py` and `tests/test_run_focus.py`; +- script-oriented tests not classified as `area_cli`; +- route, service, security, session, JavaScript, helper, and broad unit-test + groups. -Read-only checks, run from the repo root on this branch. Note the real API is -`classify_test_path` (there is no `classify_test_file`). +These exclusions kept the implementation mechanical and prevented it from +mixing taxonomy changes, assertion changes, helper extraction, or unrelated +test-suite restructuring. + +## Current repository observation + +The current taxonomy classifies 30 files as `area_cli`: + +| Location | Files | +|---|---:| +| `tests/cli/` | 28 | +| Flat `tests/` directory | 2 | + +The two currently classified CLI tests outside `tests/cli/` are: + +- `tests/test_calendar_cli_overlap.py` +- `tests/test_memory_cli_add_nondict.py` + +These files were not part of the original 28-file implementation. + +Their presence is recorded solely to keep this snapshot accurate. This +documentation update does not recommend moving them, changing their taxonomy, +or opening implementation work. Any future test-layout change requires separate +maintainer agreement, scope, and validation. + +## Validation contract preserved by the original move + +The completed implementation was designed to preserve: + +- identical `area_cli` selection before and after relocation; +- successful direct execution of the relocated tests; +- successful whole-suite collection; +- unchanged taxonomy and focused-runner behaviour; +- no stale references to the former flat paths; +- no production-code changes. + +Relevant current verification commands include: ```bash -# Compute the area_cli set and confirm test_backup_cli_security.py is -# area_security. Expected: 28 files, then "security". -./venv/bin/python - <<'PY' -from pathlib import Path -from tests._taxonomy import classify_test_path - -cli = [p for p in sorted(Path("tests").glob("test_*.py")) - if classify_test_path(p).area == "cli"] -print(len(cli)) -for p in cli: - print(p) -print(classify_test_path("tests/test_backup_cli_security.py").area) -PY - -# Coupling check across the CLI files. Expected: the only hits are -# "SessionLocal" as stub attribute names passed to tests.helpers.db_stubs; -# no TestClient, FastAPI, create_app, sqlite, or dependency_overrides. -rg -n "TestClient|FastAPI|create_app|SessionLocal|sqlite|dependency_overrides" \ - tests/test_*cli*.py tests/test_sessions_cli.py - -# Hard-coded flat paths to the exact CLI files outside tests/. Expected: no matches. -./venv/bin/python - <<'PY2' > /tmp/area_cli_paths.txt -from pathlib import Path -from tests._taxonomy import classify_test_path - -for path in sorted(Path("tests").glob("test_*.py")): - if classify_test_path(path).area == "cli": - print(path) -PY2 - -rg -n -F -f /tmp/area_cli_paths.txt .github scripts docs \ - tests/README.md tests/TESTING_STANDARD.md pyproject.toml 2>/dev/null || true +venv/bin/python tests/run_focus.py --dry-run --area cli +venv/bin/python -m pytest -m area_cli -q +venv/bin/python -m pytest tests/cli/ -q +venv/bin/python -m pytest tests/test_taxonomy.py tests/test_run_focus.py -q +venv/bin/python -m pytest --collect-only -q ``` -Also checked by reading the code: `tests/conftest.py` registers sub-markers -from a recursive `rglob` scan, and `tests/_taxonomy.py` classifies by filename -tokens only (plus the `tests/helpers/` directory rule), so the markers of the -28 files do not change when they move into `tests/cli/`. - -## Validation for the future move PR - -Run with the project venv (`./venv/bin/python`); system `python3` may miss -pinned deps. Before the move, record the baseline; after, compare: - -```bash -# Selection must match the 28 files before and after the move. -./venv/bin/python tests/run_focus.py --dry-run --area cli -./venv/bin/python -m pytest -m area_cli -q - -# Moved files pass when targeted directly. -./venv/bin/python -m pytest tests/cli/ -q - -# Whole-suite collection still succeeds (catches import/path breakage). -./venv/bin/python -m pytest --collect-only -q - -# Taxonomy/runner infrastructure is unaffected. -./venv/bin/python -m pytest tests/test_taxonomy.py tests/test_run_focus.py -q - -# No stale flat-path references to the moved files. Expected: no matches -# outside tests/cli/ itself. -./venv/bin/python - <<'PY2' > /tmp/area_cli_paths.txt -from pathlib import Path -from tests._taxonomy import classify_test_path - -for path in sorted(Path("tests").glob("test_*.py")): - if classify_test_path(path).area == "cli": - print(path) -PY2 - -rg -n -F -f /tmp/area_cli_paths.txt .github scripts docs \ - tests/README.md tests/TESTING_STANDARD.md pyproject.toml 2>/dev/null || true -``` - -Pass criteria: identical test counts for `-m area_cli` before/after, zero -collection errors, and no changes outside the moved files. +These commands describe validation coverage. This snapshot does not claim that +they were executed as part of the present documentation update. ## Non-goals -- No file moves, renames, or deletions in this PR. -- No changes to `conftest.py`, `_taxonomy.py`, `run_focus.py`, helpers, - markers, CI workflows, or production code. -- No recommendation to split the whole suite at once; later groups get their - own inventory-then-move slices. +This document does not: + +- propose moving or renaming tests; +- redefine taxonomy classifications; +- change test behaviour or assertions; +- prescribe the next test-suite refactor; +- replace the testing standard; +- create implementation work from the two current residual files. diff --git a/tests/OVERSIZED_TEST_SPLIT_PLAN.md b/tests/OVERSIZED_TEST_SPLIT_PLAN.md index 4f81080a4..481424e58 100644 --- a/tests/OVERSIZED_TEST_SPLIT_PLAN.md +++ b/tests/OVERSIZED_TEST_SPLIT_PLAN.md @@ -1,5 +1,9 @@ # Oversized Test File Split Plan +> [!NOTE] +> This is generated planning material based on the current test tree. +> It does not define runtime behaviour or approve any test-file split. + ## Purpose This document plans future oversized test-file splits using current repo data. @@ -26,8 +30,8 @@ Future split PRs must still inspect each file manually before editing. ## Current summary -- test files scanned: 583 -- collected pytest items counted: 3586 +- test files scanned: 746 +- collected pytest items counted: 4764 - large-file threshold: 300 lines - large-collected threshold: 20 collected items @@ -35,123 +39,123 @@ Area distribution: | Value | Files | |---|---:| -| cli | 28 | +| cli | 30 | | helpers | 1 | -| js | 39 | -| routes | 23 | -| security | 77 | -| services | 144 | -| uncategorized | 234 | -| unit | 37 | +| js | 49 | +| routes | 35 | +| security | 88 | +| services | 197 | +| uncategorized | 306 | +| unit | 40 | Sub-area distribution: | Value | Files | |---|---:| -| api | 6 | +| api | 8 | | atomic | 3 | -| auth | 9 | -| calendar | 10 | -| cli | 28 | -| confinement | 7 | -| cookbook | 13 | -| document | 11 | -| email | 12 | -| embedding | 3 | -| gallery | 5 | +| auth | 11 | +| calendar | 12 | +| cli | 30 | +| confinement | 8 | +| cookbook | 20 | +| document | 13 | +| email | 23 | +| embedding | 6 | +| gallery | 6 | | history | 3 | -| js | 39 | -| llm | 16 | -| mcp | 8 | -| memory | 15 | +| js | 49 | +| llm | 21 | +| mcp | 12 | +| memory | 17 | | nondict | 7 | -| nonstring | 22 | -| owner | 14 | -| owner_scope | 23 | -| parse | 4 | -| provider | 6 | -| research | 16 | +| nonstring | 23 | +| owner | 15 | +| owner_scope | 27 | +| parse | 5 | +| provider | 16 | +| research | 17 | | route | 6 | -| routes | 9 | -| scheduler | 3 | +| routes | 19 | +| scheduler | 4 | | scope | 5 | | security | 9 | -| session | 16 | -| ssrf | 3 | -| webhook | 3 | +| session | 18 | +| ssrf | 6 | +| webhook | 4 | | xss | 5 | -Values below 2 files: 244 values covering 244 files. +Values below 2 files: 318 values covering 318 files. ## Top files by collected pytest items | File | Lines | Collected tests | Test defs | Test classes | Area | Sub-area | Signals | |---|---:|---:|---:|---:|---|---|---| -| `tests/test_model_routes.py` | 1778 | 139 | 116 | 10 | routes | routes | route/api, db/session, import-state, async/threading | -| `tests/test_security_regressions.py` | 1224 | 92 | 68 | 0 | security | security | route/api, db/session, import-state, security, filesystem, async/threading, ui/static | -| `tests/test_provider_classification.py` | 188 | 67 | 21 | 4 | services | provider | - | -| `tests/test_cookbook_helpers.py` | 912 | 65 | 65 | 0 | services | cookbook | route/api, filesystem, subprocess/script, async/threading, ui/static | -| `tests/test_shell_routes.py` | 481 | 63 | 48 | 8 | routes | routes | route/api, import-state, filesystem | +| `tests/test_model_routes.py` | 2099 | 163 | 134 | 10 | routes | routes | route/api, db/session, import-state, async/threading | +| `tests/test_security_regressions.py` | 1515 | 98 | 74 | 0 | security | security | route/api, db/session, import-state, security, filesystem, async/threading, ui/static | +| `tests/test_cookbook_helpers.py` | 958 | 69 | 69 | 0 | services | cookbook | route/api, filesystem, subprocess/script, async/threading, ui/static | +| `tests/test_shell_routes.py` | 538 | 69 | 53 | 9 | routes | routes | route/api, import-state, filesystem | +| `tests/test_endpoint_probing.py` | 497 | 61 | 36 | 7 | uncategorized | endpoint_probing | route/api, db/session, import-state | | `tests/test_pr_blocker_audit.py` | 964 | 58 | 58 | 0 | uncategorized | pr_blocker_audit | import-state, security, filesystem | -| `tests/test_provider_endpoints.py` | 241 | 58 | 18 | 1 | services | provider | subprocess/script | -| `tests/test_agent_loop.py` | 469 | 52 | 52 | 5 | uncategorized | agent_loop | db/session, import-state | -| `tests/test_service_health.py` | 472 | 47 | 42 | 0 | uncategorized | service_health | async/threading | -| `tests/test_run_focus.py` | 399 | 47 | 44 | 0 | uncategorized | run_focus | security, filesystem, subprocess/script, ui/static | -| `tests/test_llm_core_temperature.py` | 196 | 41 | 17 | 0 | services | llm | - | -| `tests/test_endpoint_probing.py` | 411 | 34 | 30 | 6 | uncategorized | endpoint_probing | route/api, db/session, import-state | +| `tests/test_research_routes_path_confinement.py` | 565 | 58 | 26 | 0 | security | confinement | route/api, security, filesystem, async/threading | +| `tests/test_run_focus.py` | 492 | 55 | 52 | 0 | uncategorized | run_focus | security, filesystem, subprocess/script, ui/static | +| `tests/test_agent_loop.py` | 500 | 54 | 54 | 5 | uncategorized | agent_loop | db/session, import-state | +| `tests/test_manage_mcp_command_allowlist.py` | 168 | 45 | 10 | 0 | services | mcp | db/session, subprocess/script, async/threading, ui/static | +| `tests/test_provider_classification.py` | 115 | 42 | 9 | 2 | services | provider | - | +| `tests/test_review_regressions.py` | 1429 | 41 | 41 | 0 | uncategorized | review_regressions | route/api, db/session, import-state, filesystem, async/threading | +| `tests/test_chat_helpers.py` | 582 | 37 | 24 | 0 | uncategorized | chat_helpers | route/api, filesystem, async/threading | +| `tests/test_model_context.py` | 314 | 36 | 34 | 4 | uncategorized | model_context | db/session, import-state | +| `tests/test_email_oauth.py` | 798 | 34 | 29 | 0 | services | email | route/api, db/session, security, async/threading | | `tests/test_llm_core_anthropic_temp_omit.py` | 94 | 32 | 6 | 0 | services | llm | db/session | -| `tests/test_chat_helpers.py` | 264 | 31 | 18 | 0 | uncategorized | chat_helpers | route/api | -| `tests/test_provider_detection.py` | 148 | 31 | 31 | 5 | services | provider | - | -| `tests/test_model_context.py` | 251 | 30 | 30 | 4 | uncategorized | model_context | db/session, import-state | -| `tests/test_endpoint_resolver.py` | 148 | 30 | 30 | 6 | uncategorized | endpoint_resolver | - | -| `tests/test_embedding_lanes.py` | 1104 | 29 | 29 | 0 | services | embedding | filesystem | +| `tests/test_redos_verdict_continuation.py` | 80 | 30 | 5 | 0 | uncategorized | redos_verdict_continuation | route/api | | `tests/test_upload_limits_centralized.py` | 110 | 29 | 5 | 0 | uncategorized | upload_limits_centralized | import-state, filesystem | -| `tests/test_email_oauth.py` | 580 | 28 | 25 | 0 | services | email | route/api, db/session, security, async/threading | -| `tests/test_review_regressions.py` | 930 | 26 | 26 | 0 | uncategorized | review_regressions | route/api, db/session, import-state, filesystem, async/threading | -| `tests/test_rename_user_owner_sync.py` | 686 | 26 | 26 | 0 | security | owner | route/api, db/session, import-state, filesystem, async/threading | +| `tests/test_cookbook_docker_access.py` | 339 | 28 | 13 | 0 | services | cookbook | route/api, filesystem, async/threading | +| `tests/test_rename_user_owner_sync.py` | 740 | 27 | 27 | 0 | security | owner | route/api, db/session, import-state, filesystem, async/threading | +| `tests/test_taxonomy.py` | 151 | 27 | 17 | 0 | uncategorized | taxonomy | security, ui/static | +| `tests/test_endpoint_resolver_urls.py` | 95 | 27 | 23 | 3 | uncategorized | endpoint_resolver_urls | - | | `tests/test_helpers_import_state.py` | 426 | 26 | 26 | 0 | helpers | helpers | route/api, db/session, import-state | -| `tests/test_taxonomy.py` | 145 | 26 | 16 | 0 | uncategorized | taxonomy | security, ui/static | -| `tests/test_tool_path_confinement.py` | 282 | 24 | 24 | 0 | security | confinement | import-state, filesystem, async/threading | -| `tests/test_copilot.py` | 170 | 23 | 16 | 0 | uncategorized | copilot | - | -| `tests/test_research_utils.py` | 97 | 23 | 23 | 2 | services | research | - | -| `tests/test_api_chat_security.py` | 401 | 22 | 8 | 0 | security | security | route/api, db/session, import-state, filesystem, async/threading | -| `tests/test_tool_support_heuristic.py` | 166 | 22 | 22 | 3 | uncategorized | tool_support_heuristic | - | -| `tests/test_platform_compat.py` | 318 | 21 | 21 | 0 | uncategorized | platform_compat | import-state, filesystem, subprocess/script | +| `tests/test_workspace_confine.py` | 527 | 25 | 25 | 0 | uncategorized | workspace_confine | route/api, filesystem, subprocess/script, async/threading | +| `tests/test_chat_route_tool_policy.py` | 344 | 25 | 23 | 0 | routes | route | filesystem, ui/static | +| `tests/test_tool_path_confinement.py` | 303 | 25 | 25 | 0 | security | confinement | import-state, filesystem, async/threading | +| `tests/test_copilot.py` | 182 | 25 | 18 | 0 | uncategorized | copilot | - | +| `tests/test_llm_core_temperature_reasoning.py` | 133 | 25 | 9 | 0 | services | llm | - | +| `tests/test_provider_endpoints_url_building.py` | 86 | 25 | 3 | 0 | services | provider | - | +| `tests/test_bg_job_tools.py` | 174 | 23 | 16 | 0 | uncategorized | bg_job_tools | filesystem, async/threading | ## Top files by physical line count | File | Lines | Collected tests | Test defs | Test classes | Area | Sub-area | Signals | |---|---:|---:|---:|---:|---|---|---| -| `tests/test_model_routes.py` | 1778 | 139 | 116 | 10 | routes | routes | route/api, db/session, import-state, async/threading | -| `tests/test_security_regressions.py` | 1224 | 92 | 68 | 0 | security | security | route/api, db/session, import-state, security, filesystem, async/threading, ui/static | -| `tests/test_embedding_lanes.py` | 1104 | 29 | 29 | 0 | services | embedding | filesystem | +| `tests/test_model_routes.py` | 2099 | 163 | 134 | 10 | routes | routes | route/api, db/session, import-state, async/threading | +| `tests/test_security_regressions.py` | 1515 | 98 | 74 | 0 | security | security | route/api, db/session, import-state, security, filesystem, async/threading, ui/static | +| `tests/test_review_regressions.py` | 1429 | 41 | 41 | 0 | uncategorized | review_regressions | route/api, db/session, import-state, filesystem, async/threading | | `tests/test_pr_blocker_audit.py` | 964 | 58 | 58 | 0 | uncategorized | pr_blocker_audit | import-state, security, filesystem | -| `tests/test_review_regressions.py` | 930 | 26 | 26 | 0 | uncategorized | review_regressions | route/api, db/session, import-state, filesystem, async/threading | -| `tests/test_cookbook_helpers.py` | 912 | 65 | 65 | 0 | services | cookbook | route/api, filesystem, subprocess/script, async/threading, ui/static | -| `tests/test_rename_user_owner_sync.py` | 686 | 26 | 26 | 0 | security | owner | route/api, db/session, import-state, filesystem, async/threading | -| `tests/test_email_oauth.py` | 580 | 28 | 25 | 0 | services | email | route/api, db/session, security, async/threading | +| `tests/test_cookbook_helpers.py` | 958 | 69 | 69 | 0 | services | cookbook | route/api, filesystem, subprocess/script, async/threading, ui/static | +| `tests/test_upload_handler_cleanup.py` | 831 | 13 | 13 | 0 | uncategorized | upload_handler_cleanup | route/api, db/session, filesystem, async/threading | +| `tests/test_email_oauth.py` | 798 | 34 | 29 | 0 | services | email | route/api, db/session, security, async/threading | +| `tests/test_rename_user_owner_sync.py` | 740 | 27 | 27 | 0 | security | owner | route/api, db/session, import-state, filesystem, async/threading | +| `tests/test_email_owner_scope.py` | 656 | 13 | 13 | 0 | security | owner_scope | route/api, db/session, filesystem, async/threading | +| `tests/test_model_capability_readers.py` | 646 | 20 | 20 | 0 | uncategorized | model_capability_readers | - | +| `tests/test_chat_helpers.py` | 582 | 37 | 24 | 0 | uncategorized | chat_helpers | route/api, filesystem, async/threading | | `tests/test_api_token_routes.py` | 578 | 17 | 17 | 0 | routes | api_routes | route/api, db/session, import-state, async/threading | -| `tests/test_shell_routes.py` | 481 | 63 | 48 | 8 | routes | routes | route/api, import-state, filesystem | -| `tests/test_email_owner_scope.py` | 474 | 9 | 9 | 0 | security | owner_scope | route/api, db/session, filesystem, async/threading | -| `tests/test_service_health.py` | 472 | 47 | 42 | 0 | uncategorized | service_health | async/threading | -| `tests/test_agent_loop.py` | 469 | 52 | 52 | 5 | uncategorized | agent_loop | db/session, import-state | +| `tests/test_research_routes_path_confinement.py` | 565 | 58 | 26 | 0 | security | confinement | route/api, security, filesystem, async/threading | +| `tests/test_shell_routes.py` | 538 | 69 | 53 | 9 | routes | routes | route/api, import-state, filesystem | +| `tests/test_workspace_confine.py` | 527 | 25 | 25 | 0 | uncategorized | workspace_confine | route/api, filesystem, subprocess/script, async/threading | +| `tests/test_agent_loop.py` | 500 | 54 | 54 | 5 | uncategorized | agent_loop | db/session, import-state | +| `tests/test_endpoint_probing.py` | 497 | 61 | 36 | 7 | uncategorized | endpoint_probing | route/api, db/session, import-state | +| `tests/test_run_focus.py` | 492 | 55 | 52 | 0 | uncategorized | run_focus | security, filesystem, subprocess/script, ui/static | +| `tests/test_tool_policy.py` | 475 | 18 | 18 | 0 | uncategorized | tool_policy | import-state, async/threading | | `tests/test_kv_cache_invalidation_2927.py` | 463 | 8 | 8 | 0 | uncategorized | kv_cache_invalidation_2927 | route/api, db/session, import-state, async/threading | +| `tests/test_email_test_connection_oauth.py` | 436 | 9 | 7 | 0 | services | email | route/api, db/session, async/threading | | `tests/test_helpers_import_state.py` | 426 | 26 | 26 | 0 | helpers | helpers | route/api, db/session, import-state | | `tests/test_endpoint_owner_scope_followup.py` | 414 | 11 | 11 | 0 | security | owner_scope | route/api, db/session, filesystem | -| `tests/test_endpoint_probing.py` | 411 | 34 | 30 | 6 | uncategorized | endpoint_probing | route/api, db/session, import-state | +| `tests/test_api_chat_security.py` | 404 | 22 | 8 | 0 | security | security | route/api, db/session, import-state, filesystem, async/threading | | `tests/test_imap_leak_fixes.py` | 404 | 15 | 15 | 0 | uncategorized | imap_leak_fixes | route/api, db/session, security, filesystem | | `tests/test_companion_readonly.py` | 402 | 17 | 17 | 0 | uncategorized | companion_readonly | db/session, import-state | -| `tests/test_api_chat_security.py` | 401 | 22 | 8 | 0 | security | security | route/api, db/session, import-state, filesystem, async/threading | | `tests/test_upload_handler_atomicity.py` | 401 | 9 | 9 | 0 | uncategorized | upload_handler_atomicity | filesystem, async/threading | -| `tests/test_run_focus.py` | 399 | 47 | 44 | 0 | uncategorized | run_focus | security, filesystem, subprocess/script, ui/static | -| `tests/test_auth_regressions.py` | 375 | 15 | 15 | 0 | security | auth | route/api, db/session, import-state, async/threading | -| `tests/test_calendar_owner_scope.py` | 345 | 7 | 7 | 0 | security | owner_scope | route/api, db/session, import-state, filesystem, async/threading, ui/static | -| `tests/test_null_owner_gates.py` | 342 | 20 | 20 | 0 | security | owner | route/api, db/session, import-state | -| `tests/test_agent_migration_manifest.py` | 340 | 15 | 15 | 0 | uncategorized | agent_migration_manifest | import-state, filesystem | -| `tests/test_calendar_recurrence.py` | 338 | 19 | 19 | 0 | services | calendar | - | -| `tests/test_tool_policy.py` | 330 | 13 | 13 | 0 | uncategorized | tool_policy | import-state, async/threading | -| `tests/test_workspace_confine.py` | 328 | 18 | 18 | 0 | uncategorized | workspace_confine | route/api, filesystem, subprocess/script, async/threading | -| `tests/test_diffusion_server_security.py` | 325 | 14 | 14 | 0 | security | security | route/api, import-state, security, filesystem, async/threading, ui/static | +| `tests/test_auth_regressions.py` | 370 | 15 | 15 | 0 | security | auth | route/api, db/session, import-state, async/threading | +| `tests/test_calendar_recurrence.py` | 356 | 20 | 20 | 0 | services | calendar | - | +| `tests/test_task_cookbook_admin_gate.py` | 350 | 10 | 10 | 0 | services | cookbook | route/api, db/session, import-state, filesystem, async/threading | ## Split planning candidates @@ -165,53 +169,69 @@ These are planning candidates only. A later split PR still needs a focused manua | File | Why included | Setup/risk signals | Suggested handling | |---|---|---|---| -| `tests/test_model_routes.py` | 1778 lines, 139 collected tests | route/api, db/session, import-state, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_security_regressions.py` | 1224 lines, 92 collected tests | route/api, db/session, import-state, security, filesystem, async/threading, ui/static | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_provider_classification.py` | 67 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | -| `tests/test_cookbook_helpers.py` | 912 lines, 65 collected tests | route/api, filesystem, subprocess/script, async/threading, ui/static | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_shell_routes.py` | 481 lines, 63 collected tests | route/api, import-state, filesystem | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_model_routes.py` | 2099 lines, 163 collected tests | route/api, db/session, import-state, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_security_regressions.py` | 1515 lines, 98 collected tests | route/api, db/session, import-state, security, filesystem, async/threading, ui/static | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_cookbook_helpers.py` | 958 lines, 69 collected tests | route/api, filesystem, subprocess/script, async/threading, ui/static | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_shell_routes.py` | 538 lines, 69 collected tests | route/api, import-state, filesystem | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_endpoint_probing.py` | 497 lines, 61 collected tests | route/api, db/session, import-state | Defer mechanical split until setup/risk boundaries are mapped. | | `tests/test_pr_blocker_audit.py` | 964 lines, 58 collected tests | import-state, security, filesystem | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_provider_endpoints.py` | 58 collected tests | subprocess/script | Good first manual-review candidate if test themes are cohesive. | -| `tests/test_agent_loop.py` | 469 lines, 52 collected tests | db/session, import-state | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_service_health.py` | 472 lines, 47 collected tests | async/threading | Good first manual-review candidate if test themes are cohesive. | -| `tests/test_run_focus.py` | 399 lines, 47 collected tests | security, filesystem, subprocess/script, ui/static | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_llm_core_temperature.py` | 41 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | -| `tests/test_endpoint_probing.py` | 411 lines, 34 collected tests | route/api, db/session, import-state | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_research_routes_path_confinement.py` | 565 lines, 58 collected tests | route/api, security, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_run_focus.py` | 492 lines, 55 collected tests | security, filesystem, subprocess/script, ui/static | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_agent_loop.py` | 500 lines, 54 collected tests | db/session, import-state | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_manage_mcp_command_allowlist.py` | 45 collected tests | db/session, subprocess/script, async/threading, ui/static | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_provider_classification.py` | 42 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | +| `tests/test_review_regressions.py` | 1429 lines, 41 collected tests | route/api, db/session, import-state, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_chat_helpers.py` | 582 lines, 37 collected tests | route/api, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_model_context.py` | 314 lines, 36 collected tests | db/session, import-state | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_email_oauth.py` | 798 lines, 34 collected tests | route/api, db/session, security, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | | `tests/test_llm_core_anthropic_temp_omit.py` | 32 collected tests | db/session | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_chat_helpers.py` | 31 collected tests | route/api | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_provider_detection.py` | 31 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | -| `tests/test_model_context.py` | 30 collected tests | db/session, import-state | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_endpoint_resolver.py` | 30 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | -| `tests/test_embedding_lanes.py` | 1104 lines, 29 collected tests | filesystem | Good first manual-review candidate if test themes are cohesive. | +| `tests/test_redos_verdict_continuation.py` | 30 collected tests | route/api | Defer mechanical split until setup/risk boundaries are mapped. | | `tests/test_upload_limits_centralized.py` | 29 collected tests | import-state, filesystem | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_email_oauth.py` | 580 lines, 28 collected tests | route/api, db/session, security, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_review_regressions.py` | 930 lines, 26 collected tests | route/api, db/session, import-state, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_rename_user_owner_sync.py` | 686 lines, 26 collected tests | route/api, db/session, import-state, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_cookbook_docker_access.py` | 339 lines, 28 collected tests | route/api, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_rename_user_owner_sync.py` | 740 lines, 27 collected tests | route/api, db/session, import-state, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_taxonomy.py` | 27 collected tests | security, ui/static | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_endpoint_resolver_urls.py` | 27 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | | `tests/test_helpers_import_state.py` | 426 lines, 26 collected tests | route/api, db/session, import-state | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_taxonomy.py` | 26 collected tests | security, ui/static | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_tool_path_confinement.py` | 24 collected tests | import-state, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_copilot.py` | 23 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | +| `tests/test_workspace_confine.py` | 527 lines, 25 collected tests | route/api, filesystem, subprocess/script, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_chat_route_tool_policy.py` | 344 lines, 25 collected tests | filesystem, ui/static | Good first manual-review candidate if test themes are cohesive. | +| `tests/test_tool_path_confinement.py` | 303 lines, 25 collected tests | import-state, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_copilot.py` | 25 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | +| `tests/test_llm_core_temperature_reasoning.py` | 25 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | +| `tests/test_provider_endpoints_url_building.py` | 25 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | +| `tests/test_bg_job_tools.py` | 23 collected tests | filesystem, async/threading | Good first manual-review candidate if test themes are cohesive. | | `tests/test_research_utils.py` | 23 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | -| `tests/test_api_chat_security.py` | 401 lines, 22 collected tests | route/api, db/session, import-state, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_api_chat_security.py` | 404 lines, 22 collected tests | route/api, db/session, import-state, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | | `tests/test_tool_support_heuristic.py` | 22 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | -| `tests/test_platform_compat.py` | 318 lines, 21 collected tests | import-state, filesystem, subprocess/script | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_platform_compat.py` | 322 lines, 21 collected tests | import-state, filesystem, subprocess/script | Defer mechanical split until setup/risk boundaries are mapped. | | `tests/test_context_compactor.py` | 21 collected tests | db/session, import-state, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | | `tests/test_prompt_security.py` | 21 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | -| `tests/test_null_owner_gates.py` | 342 lines, 20 collected tests | route/api, db/session, import-state | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_fenced_inline_args.py` | 21 collected tests | db/session, import-state | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_cache_affinity_local_only.py` | 21 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | +| `tests/test_model_capability_readers.py` | 646 lines, 20 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | +| `tests/test_calendar_recurrence.py` | 356 lines, 20 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | +| `tests/test_null_owner_gates.py` | 332 lines, 20 collected tests | route/api, db/session, import-state | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_gallery_endpoint_hardening.py` | 326 lines, 20 collected tests | route/api, security, filesystem, ui/static | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_redos_llm_parsers.py` | 20 collected tests | No obvious setup signals from static scan. | Good first manual-review candidate if test themes are cohesive. | +| `tests/test_code_nav_tools.py` | 20 collected tests | db/session, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | | `tests/test_youtube_handler_consolidation.py` | 20 collected tests | route/api, import-state | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_calendar_recurrence.py` | 338 lines | No obvious setup signals from static scan. | Plan split boundaries before editing. | -| `tests/test_workspace_confine.py` | 328 lines | route/api, filesystem, subprocess/script, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_tool_policy.py` | 475 lines | import-state, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_fenced_example_not_executed_for_native_models.py` | 345 lines | async/threading | Plan split boundaries before editing. | | `tests/test_api_token_routes.py` | 578 lines | route/api, db/session, import-state, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | | `tests/test_companion_readonly.py` | 402 lines | db/session, import-state | Defer mechanical split until setup/risk boundaries are mapped. | | `tests/test_set_admin.py` | 317 lines | route/api, import-state, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_llm_core_ollama.py` | 324 lines | async/threading | Plan split boundaries before editing. | | `tests/test_imap_leak_fixes.py` | 404 lines | route/api, db/session, security, filesystem | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_auth_regressions.py` | 375 lines | route/api, db/session, import-state, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_auth_regressions.py` | 370 lines | route/api, db/session, import-state, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | | `tests/test_agent_migration_manifest.py` | 340 lines | import-state, filesystem | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_mcp_email_decode_header_spaces.py` | 327 lines | route/api, db/session, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | | `tests/test_diffusion_server_security.py` | 325 lines | route/api, import-state, security, filesystem, async/threading, ui/static | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_tool_policy.py` | 330 lines | import-state, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_upload_handler_cleanup.py` | 831 lines | route/api, db/session, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_email_owner_scope.py` | 656 lines | route/api, db/session, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_upload_routes_owner_scope.py` | 344 lines | route/api, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | | `tests/test_endpoint_owner_scope_followup.py` | 414 lines | route/api, db/session, filesystem | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_upload_routes_owner_scope.py` | 315 lines | route/api, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | -| `tests/test_email_owner_scope.py` | 474 lines | route/api, db/session, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_composer_arrow_up_recall_js.py` | 308 lines | filesystem, subprocess/script, ui/static | Plan split boundaries before editing. | +| `tests/test_task_cookbook_admin_gate.py` | 350 lines | route/api, db/session, import-state, filesystem, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | +| `tests/test_email_test_connection_oauth.py` | 436 lines | route/api, db/session, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | | `tests/test_upload_handler_atomicity.py` | 401 lines | filesystem, async/threading | Plan split boundaries before editing. | | `tests/test_kv_cache_invalidation_2927.py` | 463 lines | route/api, db/session, import-state, async/threading | Defer mechanical split until setup/risk boundaries are mapped. | | `tests/test_calendar_owner_scope.py` | 345 lines | route/api, db/session, import-state, filesystem, async/threading, ui/static | Defer mechanical split until setup/risk boundaries are mapped. | @@ -226,28 +246,35 @@ Before using any of these files as a split target, first decide whether the taxo | File | Lines | Collected tests | Sub-area | Signals | Suggested follow-up | |---|---:|---:|---|---|---| +| `tests/test_endpoint_probing.py` | 497 | 61 | endpoint_probing | route/api, db/session, import-state | Review taxonomy and setup/risk boundaries before any split. | | `tests/test_pr_blocker_audit.py` | 964 | 58 | pr_blocker_audit | import-state, security, filesystem | Review taxonomy and setup/risk boundaries before any split. | -| `tests/test_agent_loop.py` | 469 | 52 | agent_loop | db/session, import-state | Review taxonomy and setup/risk boundaries before any split. | -| `tests/test_service_health.py` | 472 | 47 | service_health | async/threading | Review taxonomy mapping before using as a split target. | -| `tests/test_run_focus.py` | 399 | 47 | run_focus | security, filesystem, subprocess/script, ui/static | Review taxonomy and setup/risk boundaries before any split. | -| `tests/test_endpoint_probing.py` | 411 | 34 | endpoint_probing | route/api, db/session, import-state | Review taxonomy and setup/risk boundaries before any split. | -| `tests/test_chat_helpers.py` | 264 | 31 | chat_helpers | route/api | Review taxonomy and setup/risk boundaries before any split. | -| `tests/test_model_context.py` | 251 | 30 | model_context | db/session, import-state | Review taxonomy and setup/risk boundaries before any split. | -| `tests/test_endpoint_resolver.py` | 148 | 30 | endpoint_resolver | - | Review taxonomy mapping before using as a split target. | +| `tests/test_run_focus.py` | 492 | 55 | run_focus | security, filesystem, subprocess/script, ui/static | Review taxonomy and setup/risk boundaries before any split. | +| `tests/test_agent_loop.py` | 500 | 54 | agent_loop | db/session, import-state | Review taxonomy and setup/risk boundaries before any split. | +| `tests/test_review_regressions.py` | 1429 | 41 | review_regressions | route/api, db/session, import-state, filesystem, async/threading | Review taxonomy and setup/risk boundaries before any split. | +| `tests/test_chat_helpers.py` | 582 | 37 | chat_helpers | route/api, filesystem, async/threading | Review taxonomy and setup/risk boundaries before any split. | +| `tests/test_model_context.py` | 314 | 36 | model_context | db/session, import-state | Review taxonomy and setup/risk boundaries before any split. | +| `tests/test_redos_verdict_continuation.py` | 80 | 30 | redos_verdict_continuation | route/api | Review taxonomy and setup/risk boundaries before any split. | | `tests/test_upload_limits_centralized.py` | 110 | 29 | upload_limits_centralized | import-state, filesystem | Review taxonomy and setup/risk boundaries before any split. | -| `tests/test_review_regressions.py` | 930 | 26 | review_regressions | route/api, db/session, import-state, filesystem, async/threading | Review taxonomy and setup/risk boundaries before any split. | -| `tests/test_taxonomy.py` | 145 | 26 | taxonomy | security, ui/static | Review taxonomy and setup/risk boundaries before any split. | -| `tests/test_copilot.py` | 170 | 23 | copilot | - | Review taxonomy mapping before using as a split target. | +| `tests/test_taxonomy.py` | 151 | 27 | taxonomy | security, ui/static | Review taxonomy and setup/risk boundaries before any split. | +| `tests/test_endpoint_resolver_urls.py` | 95 | 27 | endpoint_resolver_urls | - | Review taxonomy mapping before using as a split target. | +| `tests/test_workspace_confine.py` | 527 | 25 | workspace_confine | route/api, filesystem, subprocess/script, async/threading | Review taxonomy and setup/risk boundaries before any split. | +| `tests/test_copilot.py` | 182 | 25 | copilot | - | Review taxonomy mapping before using as a split target. | +| `tests/test_bg_job_tools.py` | 174 | 23 | bg_job_tools | filesystem, async/threading | Review taxonomy mapping before using as a split target. | | `tests/test_tool_support_heuristic.py` | 166 | 22 | tool_support_heuristic | - | Review taxonomy mapping before using as a split target. | -| `tests/test_platform_compat.py` | 318 | 21 | platform_compat | import-state, filesystem, subprocess/script | Review taxonomy and setup/risk boundaries before any split. | +| `tests/test_platform_compat.py` | 322 | 21 | platform_compat | import-state, filesystem, subprocess/script | Review taxonomy and setup/risk boundaries before any split. | | `tests/test_context_compactor.py` | 233 | 21 | context_compactor | db/session, import-state, async/threading | Review taxonomy and setup/risk boundaries before any split. | +| `tests/test_fenced_inline_args.py` | 186 | 21 | fenced_inline_args | db/session, import-state | Review taxonomy and setup/risk boundaries before any split. | +| `tests/test_cache_affinity_local_only.py` | 104 | 21 | cache_affinity_local_only | - | Review taxonomy mapping before using as a split target. | +| `tests/test_model_capability_readers.py` | 646 | 20 | model_capability_readers | - | Review taxonomy mapping before using as a split target. | +| `tests/test_code_nav_tools.py` | 198 | 20 | code_nav_tools | db/session, filesystem, async/threading | Review taxonomy and setup/risk boundaries before any split. | | `tests/test_youtube_handler_consolidation.py` | 104 | 20 | youtube_handler_consolidation | route/api, import-state | Review taxonomy and setup/risk boundaries before any split. | -| `tests/test_workspace_confine.py` | 328 | 18 | workspace_confine | route/api, filesystem, subprocess/script, async/threading | Review taxonomy and setup/risk boundaries before any split. | +| `tests/test_tool_policy.py` | 475 | 18 | tool_policy | import-state, async/threading | Review taxonomy and setup/risk boundaries before any split. | +| `tests/test_fenced_example_not_executed_for_native_models.py` | 345 | 18 | fenced_example_not_executed_for_native_models | async/threading | Review taxonomy mapping before using as a split target. | | `tests/test_companion_readonly.py` | 402 | 17 | companion_readonly | db/session, import-state | Review taxonomy and setup/risk boundaries before any split. | | `tests/test_set_admin.py` | 317 | 17 | set_admin | route/api, import-state, filesystem, async/threading | Review taxonomy and setup/risk boundaries before any split. | | `tests/test_imap_leak_fixes.py` | 404 | 15 | imap_leak_fixes | route/api, db/session, security, filesystem | Review taxonomy and setup/risk boundaries before any split. | | `tests/test_agent_migration_manifest.py` | 340 | 15 | agent_migration_manifest | import-state, filesystem | Review taxonomy and setup/risk boundaries before any split. | -| `tests/test_tool_policy.py` | 330 | 13 | tool_policy | import-state, async/threading | Review taxonomy and setup/risk boundaries before any split. | +| `tests/test_upload_handler_cleanup.py` | 831 | 13 | upload_handler_cleanup | route/api, db/session, filesystem, async/threading | Review taxonomy and setup/risk boundaries before any split. | | `tests/test_upload_handler_atomicity.py` | 401 | 9 | upload_handler_atomicity | filesystem, async/threading | Review taxonomy mapping before using as a split target. | | `tests/test_kv_cache_invalidation_2927.py` | 463 | 8 | kv_cache_invalidation_2927 | route/api, db/session, import-state, async/threading | Review taxonomy and setup/risk boundaries before any split. | @@ -259,14 +286,16 @@ Files still in the `uncategorized` taxonomy area are listed separately below so | File | Lines | Collected tests | Area | Sub-area | Signals | Why this is a candidate | |---|---:|---:|---|---|---|---| -| `tests/test_provider_classification.py` | 188 | 67 | services | provider | - | 67 collected tests | -| `tests/test_provider_endpoints.py` | 241 | 58 | services | provider | subprocess/script | 58 collected tests | -| `tests/test_llm_core_temperature.py` | 196 | 41 | services | llm | - | 41 collected tests | -| `tests/test_provider_detection.py` | 148 | 31 | services | provider | - | 31 collected tests | -| `tests/test_embedding_lanes.py` | 1104 | 29 | services | embedding | filesystem | 1104 lines, 29 collected tests | +| `tests/test_provider_classification.py` | 115 | 42 | services | provider | - | 42 collected tests | +| `tests/test_chat_route_tool_policy.py` | 344 | 25 | routes | route | filesystem, ui/static | 344 lines, 25 collected tests | +| `tests/test_llm_core_temperature_reasoning.py` | 133 | 25 | services | llm | - | 25 collected tests | +| `tests/test_provider_endpoints_url_building.py` | 86 | 25 | services | provider | - | 25 collected tests | | `tests/test_research_utils.py` | 97 | 23 | services | research | - | 23 collected tests | | `tests/test_prompt_security.py` | 203 | 21 | security | security | - | 21 collected tests | -| `tests/test_calendar_recurrence.py` | 338 | 19 | services | calendar | - | 338 lines | +| `tests/test_calendar_recurrence.py` | 356 | 20 | services | calendar | - | 356 lines, 20 collected tests | +| `tests/test_redos_llm_parsers.py` | 201 | 20 | services | llm | - | 20 collected tests | +| `tests/test_llm_core_ollama.py` | 324 | 16 | services | llm | async/threading | 324 lines | +| `tests/test_composer_arrow_up_recall_js.py` | 308 | 11 | js | js | filesystem, subprocess/script, ui/static | 308 lines | ## High-risk candidates to defer first @@ -274,21 +303,21 @@ These files may still be split later, but not as the first implementation slice | File | Lines | Collected tests | High-risk signals | |---|---:|---:|---| -| `tests/test_model_routes.py` | 1778 | 139 | db/session, import-state, route/api | -| `tests/test_security_regressions.py` | 1224 | 92 | db/session, import-state, route/api, security | -| `tests/test_cookbook_helpers.py` | 912 | 65 | route/api | -| `tests/test_shell_routes.py` | 481 | 63 | import-state, route/api | +| `tests/test_model_routes.py` | 2099 | 163 | db/session, import-state, route/api | +| `tests/test_security_regressions.py` | 1515 | 98 | db/session, import-state, route/api, security | +| `tests/test_cookbook_helpers.py` | 958 | 69 | route/api | +| `tests/test_shell_routes.py` | 538 | 69 | import-state, route/api | +| `tests/test_endpoint_probing.py` | 497 | 61 | db/session, import-state, route/api | | `tests/test_pr_blocker_audit.py` | 964 | 58 | import-state, security | -| `tests/test_agent_loop.py` | 469 | 52 | db/session, import-state | -| `tests/test_run_focus.py` | 399 | 47 | security | -| `tests/test_endpoint_probing.py` | 411 | 34 | db/session, import-state, route/api | +| `tests/test_research_routes_path_confinement.py` | 565 | 58 | route/api, security | +| `tests/test_run_focus.py` | 492 | 55 | security | +| `tests/test_agent_loop.py` | 500 | 54 | db/session, import-state | +| `tests/test_manage_mcp_command_allowlist.py` | 168 | 45 | db/session | +| `tests/test_review_regressions.py` | 1429 | 41 | db/session, import-state, route/api | +| `tests/test_chat_helpers.py` | 582 | 37 | route/api | +| `tests/test_model_context.py` | 314 | 36 | db/session, import-state | +| `tests/test_email_oauth.py` | 798 | 34 | db/session, route/api, security | | `tests/test_llm_core_anthropic_temp_omit.py` | 94 | 32 | db/session | -| `tests/test_chat_helpers.py` | 264 | 31 | route/api | -| `tests/test_model_context.py` | 251 | 30 | db/session, import-state | -| `tests/test_upload_limits_centralized.py` | 110 | 29 | import-state | -| `tests/test_email_oauth.py` | 580 | 28 | db/session, route/api, security | -| `tests/test_review_regressions.py` | 930 | 26 | db/session, import-state, route/api | -| `tests/test_rename_user_owner_sync.py` | 686 | 26 | db/session, import-state, route/api | ## Rules for future split PRs @@ -313,7 +342,7 @@ Do not start a split PR from this planning issue alone if the file's boundaries This document was generated with: ```bash -.venv/bin/python tests/tools/build_oversized_test_split_plan.py +venv/bin/python tests/tools/build_oversized_test_split_plan.py ``` ## Freshness check @@ -321,6 +350,6 @@ This document was generated with: After editing the builder or rebasing the branch, regenerate the plan and confirm no unexpected plan drift: ```bash -.venv/bin/python tests/tools/build_oversized_test_split_plan.py +venv/bin/python tests/tools/build_oversized_test_split_plan.py git diff --exit-code -- tests/OVERSIZED_TEST_SPLIT_PLAN.md ``` diff --git a/tests/tools/build_oversized_test_split_plan.py b/tests/tools/build_oversized_test_split_plan.py index 855945c1c..671b646d5 100644 --- a/tests/tools/build_oversized_test_split_plan.py +++ b/tests/tools/build_oversized_test_split_plan.py @@ -441,6 +441,10 @@ def write_report(metrics: list[FileMetric], node_count_total: int) -> None: lines = [ "# Oversized Test File Split Plan", "", + "> [!NOTE]", + "> This is generated planning material based on the current test tree.", + "> It does not define runtime behaviour or approve any test-file split.", + "", "## Purpose", "", "This document plans future oversized test-file splits using current repo data.", @@ -508,7 +512,7 @@ def write_report(metrics: list[FileMetric], node_count_total: int) -> None: "This document was generated with:", "", "```bash", - ".venv/bin/python tests/tools/build_oversized_test_split_plan.py", + "venv/bin/python tests/tools/build_oversized_test_split_plan.py", "```", "", "## Freshness check", @@ -516,7 +520,7 @@ def write_report(metrics: list[FileMetric], node_count_total: int) -> None: "After editing the builder or rebasing the branch, regenerate the plan and confirm no unexpected plan drift:", "", "```bash", - ".venv/bin/python tests/tools/build_oversized_test_split_plan.py", + "venv/bin/python tests/tools/build_oversized_test_split_plan.py", "git diff --exit-code -- tests/OVERSIZED_TEST_SPLIT_PLAN.md", "```", "", From 01790c2f08233f1b8d15457fbe7dd0d089d7b8ef Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:11:34 +0100 Subject: [PATCH 009/125] fix(mcp): keep built-in servers on SDK v1 (#5820) --- requirements.txt | 5 ++++- tests/test_mcp_dependency_compatibility.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 tests/test_mcp_dependency_compatibility.py diff --git a/requirements.txt b/requirements.txt index be5f5d450..3c5114f53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,10 @@ python-dateutil caldav cryptography bcrypt -mcp +# Built-in servers use the v1 low-level Server decorator API. MCP SDK v2 is a +# breaking rewrite, so keep fresh installs on the maintained v1 line until the +# servers are migrated together. +mcp<2 pyotp qrcode[pil] croniter diff --git a/tests/test_mcp_dependency_compatibility.py b/tests/test_mcp_dependency_compatibility.py new file mode 100644 index 000000000..9efefe4fe --- /dev/null +++ b/tests/test_mcp_dependency_compatibility.py @@ -0,0 +1,15 @@ +"""Regression coverage for the built-in MCP servers' SDK compatibility line.""" + +from pathlib import Path + + +REQUIREMENTS = Path(__file__).resolve().parents[1] / "requirements.txt" + + +def test_mcp_requirement_excludes_breaking_v2_sdk(): + requirements = [ + line.split("#", 1)[0].strip().replace(" ", "") + for line in REQUIREMENTS.read_text(encoding="utf-8").splitlines() + ] + + assert "mcp<2" in requirements From d190980f4779108571cfc75b988ea6a5f15b6606 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <alexandremagteixeira@gmail.com> Date: Tue, 28 Jul 2026 18:28:31 +0100 Subject: [PATCH 010/125] docs: classify test planning documents --- tests/LAYOUT_INVENTORY.md | 17 +++++++++++++++++ tests/OVERSIZED_TEST_SPLIT_PLAN.md | 4 ++++ tests/tools/build_oversized_test_split_plan.py | 4 ++++ 3 files changed, 25 insertions(+) diff --git a/tests/LAYOUT_INVENTORY.md b/tests/LAYOUT_INVENTORY.md index 4a14961be..60a9b7c63 100644 --- a/tests/LAYOUT_INVENTORY.md +++ b/tests/LAYOUT_INVENTORY.md @@ -10,6 +10,23 @@ - **Implemented by:** #3842 - **Status:** completed historical snapshot +- **Durable testing policy:** [`TESTING_STANDARD.md`](./TESTING_STANDARD.md) +- **Current testing mechanics:** [`README.md`](./README.md) + +## Disposition + +This document is retained as a non-canonical historical record of the completed +CLI test relocation. + +Reusable testing rules are maintained in `TESTING_STANDARD.md`; current helper, +taxonomy-runner, and execution mechanics are maintained in `README.md`, the test +suite, and its supporting code. + +Counts, residual-file observations, and path listings in this document are +snapshot evidence only. They must not be maintained as durable repository truth. + +Any additional test relocation requires a separately approved issue or work item. + ## Purpose This document preserves the reasoning, scope, and validation contract for the diff --git a/tests/OVERSIZED_TEST_SPLIT_PLAN.md b/tests/OVERSIZED_TEST_SPLIT_PLAN.md index 481424e58..442b6c83c 100644 --- a/tests/OVERSIZED_TEST_SPLIT_PLAN.md +++ b/tests/OVERSIZED_TEST_SPLIT_PLAN.md @@ -2,6 +2,10 @@ > [!NOTE] > This is generated planning material based on the current test tree. +> +> It is non-canonical and disposable. Durable testing policy belongs in +> `tests/TESTING_STANDARD.md`; active split selection and prioritization +> belong in approved issues or the project tracker. > It does not define runtime behaviour or approve any test-file split. ## Purpose diff --git a/tests/tools/build_oversized_test_split_plan.py b/tests/tools/build_oversized_test_split_plan.py index 671b646d5..ac2499fdc 100644 --- a/tests/tools/build_oversized_test_split_plan.py +++ b/tests/tools/build_oversized_test_split_plan.py @@ -443,6 +443,10 @@ def write_report(metrics: list[FileMetric], node_count_total: int) -> None: "", "> [!NOTE]", "> This is generated planning material based on the current test tree.", + ">", + "> It is non-canonical and disposable. Durable testing policy belongs in", + "> `tests/TESTING_STANDARD.md`; active split selection and prioritization", + "> belong in approved issues or the project tracker.", "> It does not define runtime behaviour or approve any test-file split.", "", "## Purpose", From 5104a9a96710f682d641a1e10c4977ae196ab912 Mon Sep 17 00:00:00 2001 From: Boody <bodebi639@gmail.com> Date: Tue, 28 Jul 2026 21:34:03 +0300 Subject: [PATCH 011/125] feat(tts): implement TTS cache size limit and eviction policy --- .env.example | 1 + services/tts/tts_service.py | 35 +++++++++ tests/test_tts_service_enforce_cache_limit.py | 73 +++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 tests/test_tts_service_enforce_cache_limit.py diff --git a/.env.example b/.env.example index d23276eb8..4eb4695e0 100644 --- a/.env.example +++ b/.env.example @@ -189,6 +189,7 @@ SEARXNG_INSTANCE=http://localhost:8080 # ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES=26214400 # email compose attachment (25 MB) # ODYSSEUS_STT_MAX_AUDIO_BYTES=26214400 # speech-to-text audio (25 MB) # ODYSSEUS_ICS_MAX_BYTES=10485760 # calendar .ics import (10 MB) +# ODYSSEUS_TTS_CACHE_MAX_BYTES=52428800 # TTS cache (500 MB) # ============================================================ # Host Docker access (explicit opt-in) diff --git a/services/tts/tts_service.py b/services/tts/tts_service.py index 2120d7720..e1c67d4da 100644 --- a/services/tts/tts_service.py +++ b/services/tts/tts_service.py @@ -2,6 +2,7 @@ """Multi-provider TTS service — dispatches to local Kokoro, OpenAI-compatible API, or browser.""" import io +import os import wave import logging import hashlib @@ -41,6 +42,11 @@ class TTSService: self.cache_dir = Path(cache_dir) self.cache_dir.mkdir(parents=True, exist_ok=True) self._kokoro = None # lazy-init + + try: + self.max_cache_bytes = int(os.getenv("ODYSSEUS_TTS_CACHE_MAX_BYTES", 500 * 1024 * 1024)) + except ValueError: + self.max_cache_bytes = 500 * 1024 * 1024 # ── Settings ── @@ -89,6 +95,35 @@ class TTSService: ext = ".mp3" if (len(data) >= 3 and (data[:3] == b'ID3' or (data[0] == 0xff and (data[1] & 0xe0) == 0xe0))) else ".wav" (self.cache_dir / f"{key}{ext}").write_bytes(data) + self._enforce_cache_limit() + + def _enforce_cache_limit(self): + """Evicts oldest files if the cache exceeds the configured byte limit.""" + if self.max_cache_bytes <= 0: + return + + files = [f for f in self.cache_dir.glob("*.*") if f.is_file()] + total_size = sum(f.stat().st_size for f in files) + + if total_size > self.max_cache_bytes: + logger.info(f"TTS cache ({total_size} bytes) exceeded limit ({self.max_cache_bytes} bytes). Evicting oldest files.") + + # Sort files by modification time (oldest first) + files.sort(key=lambda f: f.stat().st_mtime) + + # Trim down to 80% of max capacity so we aren't constantly triggering this on every new generation + target_size = self.max_cache_bytes * 0.8 + + while files and total_size > target_size: + f = files.pop(0) + try: + size = f.stat().st_size + f.unlink() + total_size -= size + except FileNotFoundError: + # File was deleted by another process + continue + def clear_cache(self): count = 0 for f in self.cache_dir.glob("*.*"): diff --git a/tests/test_tts_service_enforce_cache_limit.py b/tests/test_tts_service_enforce_cache_limit.py new file mode 100644 index 000000000..f4a31c75b --- /dev/null +++ b/tests/test_tts_service_enforce_cache_limit.py @@ -0,0 +1,73 @@ +import os +import time +from pathlib import Path +import pytest + +# Adjust the import path if your file is directly in ./services instead of ./services/tts +from services.tts.tts_service import TTSService + +def test_cache_under_limit(tmp_path, monkeypatch): + """Test that writing a file under the size limit does not trigger eviction.""" + # Set a tiny limit: 100 bytes + monkeypatch.setenv("TTS_CACHE_MAX_BYTES", "100") + + # Initialize service with pytest's temporary directory + service = TTSService(cache_dir=str(tmp_path)) + + # Write a 40-byte file (under the 100-byte limit) + service._put_cache("test_key", b"x" * 40) + + # Verify the file was written and nothing was deleted + files = list(tmp_path.glob("*.*")) + assert len(files) == 1 + assert sum(f.stat().st_size for f in files) == 40 + +def test_cache_exceeds_limit_triggers_eviction(tmp_path, monkeypatch): + """Test that exceeding the limit evicts the oldest files down to 80% capacity.""" + # Set limit to 100 bytes. 80% target capacity will be 80 bytes. + monkeypatch.setenv("TTS_CACHE_MAX_BYTES", "100") + service = TTSService(cache_dir=str(tmp_path)) + + # 1. Setup: Manually create two older files (40 bytes each) + file1 = tmp_path / "oldest.wav" + file2 = tmp_path / "middle.wav" + + file1.write_bytes(b"a" * 40) + file2.write_bytes(b"b" * 40) + + # Spoof timestamps so file1 is explicitly older than file2 + now = time.time() + os.utime(file1, (now - 100, now - 100)) # 100 seconds ago + os.utime(file2, (now - 50, now - 50)) # 50 seconds ago + + # 2. Action: Write a 3rd file using the service method (40 bytes) + # Total cache is now 120 bytes, which exceeds 100. + # It should delete oldest (file1) to drop to 80 bytes (which matches the 80% target). + service._put_cache("newest", b"c" * 40) + + # 3. Assertions + # The newest file should exist (saved as .wav because it lacks MP3 magic bytes) + newest_file = tmp_path / "newest.wav" + + assert not file1.exists(), "The oldest file should have been evicted." + assert file2.exists(), "The middle file should still exist." + assert newest_file.exists(), "The newest file should have been saved." + + # Verify the final directory size is <= 80 bytes + total_size = sum(f.stat().st_size for f in tmp_path.glob("*.*")) + assert total_size <= 80 + +def test_cache_limit_disabled(tmp_path, monkeypatch): + """Test that setting max bytes to 0 disables eviction.""" + monkeypatch.setenv("TTS_CACHE_MAX_BYTES", "0") + service = TTSService(cache_dir=str(tmp_path)) + + # Write 3 large files that would normally trigger eviction + service._put_cache("file1", b"x" * 1000) + service._put_cache("file2", b"x" * 1000) + service._put_cache("file3", b"x" * 1000) + + # Ensure nothing was deleted + files = list(tmp_path.glob("*.*")) + assert len(files) == 3 + assert sum(f.stat().st_size for f in files) == 3000 \ No newline at end of file From 98e4d8451bdcdf2f682bb39a54cccfeb0299ab9a Mon Sep 17 00:00:00 2001 From: Boody <bodebi639@gmail.com> Date: Tue, 28 Jul 2026 22:00:54 +0300 Subject: [PATCH 012/125] fix(tests): update environment variable for TTS cache limit to include ODYSSEUS prefix --- tests/test_tts_service_enforce_cache_limit.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_tts_service_enforce_cache_limit.py b/tests/test_tts_service_enforce_cache_limit.py index f4a31c75b..8a726557b 100644 --- a/tests/test_tts_service_enforce_cache_limit.py +++ b/tests/test_tts_service_enforce_cache_limit.py @@ -9,7 +9,7 @@ from services.tts.tts_service import TTSService def test_cache_under_limit(tmp_path, monkeypatch): """Test that writing a file under the size limit does not trigger eviction.""" # Set a tiny limit: 100 bytes - monkeypatch.setenv("TTS_CACHE_MAX_BYTES", "100") + monkeypatch.setenv("ODYSSEUS_TTS_CACHE_MAX_BYTES", "100") # Initialize service with pytest's temporary directory service = TTSService(cache_dir=str(tmp_path)) @@ -25,7 +25,7 @@ def test_cache_under_limit(tmp_path, monkeypatch): def test_cache_exceeds_limit_triggers_eviction(tmp_path, monkeypatch): """Test that exceeding the limit evicts the oldest files down to 80% capacity.""" # Set limit to 100 bytes. 80% target capacity will be 80 bytes. - monkeypatch.setenv("TTS_CACHE_MAX_BYTES", "100") + monkeypatch.setenv("ODYSSEUS_TTS_CACHE_MAX_BYTES", "100") service = TTSService(cache_dir=str(tmp_path)) # 1. Setup: Manually create two older files (40 bytes each) @@ -59,7 +59,7 @@ def test_cache_exceeds_limit_triggers_eviction(tmp_path, monkeypatch): def test_cache_limit_disabled(tmp_path, monkeypatch): """Test that setting max bytes to 0 disables eviction.""" - monkeypatch.setenv("TTS_CACHE_MAX_BYTES", "0") + monkeypatch.setenv("ODYSSEUS_TTS_CACHE_MAX_BYTES", "0") service = TTSService(cache_dir=str(tmp_path)) # Write 3 large files that would normally trigger eviction From 6bc6ee9c7e9d798c5c09e82311a1f75e075b0ae7 Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <alexandremagteixeira@gmail.com> Date: Tue, 28 Jul 2026 20:26:38 +0100 Subject: [PATCH 013/125] docs: reconcile security reporting guidance --- SECURITY.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 1fa5b0b3b..38265a1da 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -37,4 +37,6 @@ Only `.env.example`, docs, source, tests, and static assets should be committed. ## Reporting -Please report vulnerabilities privately via GitHub security advisories if available, or by opening a minimal issue that does not disclose exploit details. +Report security vulnerabilities privately through [GitHub Security Advisories](https://github.com/odysseus-dev/odysseus/security/advisories/new). + +Do not open a public issue or discussion, and do not disclose exploit details publicly. From 25a4d134b1611b5329675ce212070bedcb8daad1 Mon Sep 17 00:00:00 2001 From: "Tal.Yuan" <ydonghao@users.noreply.github.com> Date: Wed, 29 Jul 2026 04:26:29 +0800 Subject: [PATCH 014/125] refactor(routes): move search domain into routes/search/ subpackage (#5779) Slice 2j of the route-domain reorganization (#4082/#4071). Moves search_routes.py into routes/search/, leaving a backward-compat sys.modules shim. Pure file reorganization, no behavior change. --- app.py | 2 +- routes/search/__init__.py | 5 ++ routes/search/search_routes.py | 111 +++++++++++++++++++++++++++++ routes/search_routes.py | 116 +++---------------------------- tests/test_search_routes_shim.py | 11 +++ 5 files changed, 137 insertions(+), 108 deletions(-) create mode 100644 routes/search/__init__.py create mode 100644 routes/search/search_routes.py create mode 100644 tests/test_search_routes_shim.py diff --git a/app.py b/app.py index e740ad518..2ae5ec761 100644 --- a/app.py +++ b/app.py @@ -692,7 +692,7 @@ from routes.history.history_routes import setup_history_routes app.include_router(setup_history_routes(session_manager, upload_handler=upload_handler)) # Search -from routes.search_routes import setup_search_routes +from routes.search.search_routes import setup_search_routes app.include_router(setup_search_routes(config)) # Presets diff --git a/routes/search/__init__.py b/routes/search/__init__.py new file mode 100644 index 000000000..ea051bbe0 --- /dev/null +++ b/routes/search/__init__.py @@ -0,0 +1,5 @@ +"""Search route domain package (slice 2j, #4082/#4071). + +Contains search_routes.py, migrated from the flat routes/ directory. +Backward-compat shim at routes/search_routes.py re-exports from here. +""" diff --git a/routes/search/search_routes.py b/routes/search/search_routes.py new file mode 100644 index 000000000..1effb7b8f --- /dev/null +++ b/routes/search/search_routes.py @@ -0,0 +1,111 @@ +"""Search routes — /api/search/config GET, /api/search POST.""" + +import logging +from typing import Dict, Any + +from fastapi import APIRouter, Request + +import time + +from services.search import get_search_config, comprehensive_web_search, PROVIDER_INFO +from services.search.core import _call_provider +from services.search.providers import _get_provider_key, _get_search_instance + +logger = logging.getLogger(__name__) + + +async def _request_values(request: Request) -> Dict[str, Any]: + """Accept JSON, form data, or query params for search endpoints. + + The browser UI posts FormData, while the agent's generic app_api tool + posts JSON. FastAPI Form(...) rejects JSON with a 422 before our handler + runs, which made the model think SearXNG was broken. + """ + values: Dict[str, Any] = dict(request.query_params) + content_type = (request.headers.get("content-type") or "").lower() + try: + if "application/json" in content_type: + body = await request.json() + if isinstance(body, dict): + values.update(body) + else: + form = await request.form() + values.update(dict(form)) + except Exception: + pass + return values + + +def setup_search_routes(config) -> APIRouter: + router = APIRouter(tags=["search"]) + + @router.get("/api/search/config") + async def get_search_settings() -> Dict[str, Any]: + return get_search_config() + + @router.post("/api/search") + async def do_web_search(request: Request) -> Dict[str, Any]: + """Standalone web search — returns context string + source list. + + Used by Compare mode to pre-search once and share results across panes. + """ + values = await _request_values(request) + query = str(values.get("query") or values.get("q") or "").strip() + if not query: + return {"context": "", "sources": [], "error": "query is required"} + time_filter = values.get("time_filter") or values.get("freshness") + if time_filter is not None: + time_filter = str(time_filter).strip() or None + try: + context, sources = comprehensive_web_search( + query, return_sources=True, time_filter=time_filter, + ) + return {"context": context, "sources": sources} + except Exception as e: + logger.error(f"Standalone web search failed: {e}") + return {"context": "", "sources": [], "error": str(e)} + + @router.get("/api/search/providers") + async def list_search_providers(): + """Return available search providers with config status.""" + providers = [] + for pid, (label, needs_key, needs_url) in PROVIDER_INFO.items(): + if pid == "disabled": + continue + available = True + if needs_key and not _get_provider_key(pid): + available = False + if needs_url and pid == "searxng" and not _get_search_instance(): + available = False + providers.append({ + "id": pid, + "label": label, + "available": available, + }) + return providers + + @router.post("/api/search/query") + async def search_with_provider(request: Request) -> Dict[str, Any]: + """Search using a specific provider. Used by compare search mode.""" + values = await _request_values(request) + query = str(values.get("query") or values.get("q") or "").strip() + provider = str(values.get("provider") or "").strip() + try: + count = int(values.get("count") or values.get("limit") or 10) + except Exception: + count = 10 + if not query: + return {"results": [], "provider": provider, "error": "query is required"} + if provider not in PROVIDER_INFO or provider == "disabled": + return {"results": [], "provider": provider, "error": "Unknown provider"} + t0 = time.time() + try: + results = _call_provider(provider, query, min(count, 20)) + elapsed = round(time.time() - t0, 2) + return {"results": results, "provider": provider, "time": elapsed} + except Exception as e: + elapsed = round(time.time() - t0, 2) + logger.error(f"Search provider {provider} failed: {e}") + return {"results": [], "provider": provider, "time": elapsed, "error": str(e)} + + return router diff --git a/routes/search_routes.py b/routes/search_routes.py index 1effb7b8f..03b94438b 100644 --- a/routes/search_routes.py +++ b/routes/search_routes.py @@ -1,111 +1,13 @@ -"""Search routes — /api/search/config GET, /api/search POST.""" +"""Backward-compat shim — canonical location is routes/search/search_routes.py. -import logging -from typing import Dict, Any +This module is replaced in ``sys.modules`` by the canonical module object so +that ``import routes.search_routes`` and ``from routes.search_routes import X`` +keep resolving to the canonical module. Keeps existing import paths working +after slice 2j (#4082/#4071). +""" -from fastapi import APIRouter, Request +import sys as _sys -import time +from routes.search import search_routes as _canonical # noqa: F401 -from services.search import get_search_config, comprehensive_web_search, PROVIDER_INFO -from services.search.core import _call_provider -from services.search.providers import _get_provider_key, _get_search_instance - -logger = logging.getLogger(__name__) - - -async def _request_values(request: Request) -> Dict[str, Any]: - """Accept JSON, form data, or query params for search endpoints. - - The browser UI posts FormData, while the agent's generic app_api tool - posts JSON. FastAPI Form(...) rejects JSON with a 422 before our handler - runs, which made the model think SearXNG was broken. - """ - values: Dict[str, Any] = dict(request.query_params) - content_type = (request.headers.get("content-type") or "").lower() - try: - if "application/json" in content_type: - body = await request.json() - if isinstance(body, dict): - values.update(body) - else: - form = await request.form() - values.update(dict(form)) - except Exception: - pass - return values - - -def setup_search_routes(config) -> APIRouter: - router = APIRouter(tags=["search"]) - - @router.get("/api/search/config") - async def get_search_settings() -> Dict[str, Any]: - return get_search_config() - - @router.post("/api/search") - async def do_web_search(request: Request) -> Dict[str, Any]: - """Standalone web search — returns context string + source list. - - Used by Compare mode to pre-search once and share results across panes. - """ - values = await _request_values(request) - query = str(values.get("query") or values.get("q") or "").strip() - if not query: - return {"context": "", "sources": [], "error": "query is required"} - time_filter = values.get("time_filter") or values.get("freshness") - if time_filter is not None: - time_filter = str(time_filter).strip() or None - try: - context, sources = comprehensive_web_search( - query, return_sources=True, time_filter=time_filter, - ) - return {"context": context, "sources": sources} - except Exception as e: - logger.error(f"Standalone web search failed: {e}") - return {"context": "", "sources": [], "error": str(e)} - - @router.get("/api/search/providers") - async def list_search_providers(): - """Return available search providers with config status.""" - providers = [] - for pid, (label, needs_key, needs_url) in PROVIDER_INFO.items(): - if pid == "disabled": - continue - available = True - if needs_key and not _get_provider_key(pid): - available = False - if needs_url and pid == "searxng" and not _get_search_instance(): - available = False - providers.append({ - "id": pid, - "label": label, - "available": available, - }) - return providers - - @router.post("/api/search/query") - async def search_with_provider(request: Request) -> Dict[str, Any]: - """Search using a specific provider. Used by compare search mode.""" - values = await _request_values(request) - query = str(values.get("query") or values.get("q") or "").strip() - provider = str(values.get("provider") or "").strip() - try: - count = int(values.get("count") or values.get("limit") or 10) - except Exception: - count = 10 - if not query: - return {"results": [], "provider": provider, "error": "query is required"} - if provider not in PROVIDER_INFO or provider == "disabled": - return {"results": [], "provider": provider, "error": "Unknown provider"} - t0 = time.time() - try: - results = _call_provider(provider, query, min(count, 20)) - elapsed = round(time.time() - t0, 2) - return {"results": results, "provider": provider, "time": elapsed} - except Exception as e: - elapsed = round(time.time() - t0, 2) - logger.error(f"Search provider {provider} failed: {e}") - return {"results": [], "provider": provider, "time": elapsed, "error": str(e)} - - return router +_sys.modules[__name__] = _canonical diff --git a/tests/test_search_routes_shim.py b/tests/test_search_routes_shim.py new file mode 100644 index 000000000..a8b278488 --- /dev/null +++ b/tests/test_search_routes_shim.py @@ -0,0 +1,11 @@ +"""Regression test for the search route shim (slice 2j, #4082/#4071).""" + +import importlib + +import routes.search_routes as _shim_search # noqa: F401 + + +def test_legacy_and_canonical_search_module_are_same_object(): + legacy = importlib.import_module("routes.search_routes") + canonical = importlib.import_module("routes.search.search_routes") + assert legacy is canonical From 7b4c83089b8437a831e44e445f857ebfa0714e2a Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <alexandremagteixeira@gmail.com> Date: Tue, 28 Jul 2026 22:15:13 +0100 Subject: [PATCH 015/125] docs: improve README and setup navigation --- README.md | 12 ++++++++---- docs/setup.md | 51 +++++++++++++++++++++++++++++++++++---------------- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index b8d2017d7..b790f4e2d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +# Odysseus + <p align="center"> <img src="docs/odysseus-wordmark.png" alt="Odysseus" width="238"> </p> @@ -9,6 +11,8 @@ <p align="center"> <a href="#quick-start">Quick Start</a> · <a href="docs/setup.md">Setup Guide</a> · + <a href="docs/ARCHITECTURE.md">Architecture</a> · + <a href="SECURITY.md">Security</a> · <a href="CONTRIBUTING.md">Contributing</a> · <a href="ROADMAP.md">Roadmap</a> </p> @@ -51,15 +55,15 @@ Native installs, GPU notes, Windows/macOS instructions, HTTPS, and configuration ## Demo -A full hover-to-play tour lives on the landing page: [`docs/index.html`](docs/index.html). +Explore the interface through the [interactive product tour](docs/index.html). ## Contributing -Help is welcome. The best entry points are fresh-install testing, provider setup bugs, mobile/editor polish, documentation, and small focused refactors. See [CONTRIBUTING.md](CONTRIBUTING.md), the [public roadmap](ROADMAP.md), and the open [GitHub issues](https://github.com/odysseus-dev/odysseus/issues). +Help is welcome. The best entry points are fresh-install testing, provider setup bugs, mobile/editor polish, documentation, and small focused refactors. Read the [contributing guide](CONTRIBUTING.md), review the [public roadmap](ROADMAP.md), and browse the open [GitHub issues](https://github.com/odysseus-dev/odysseus/issues). ## Security -Odysseus is a self-hosted workspace with powerful local tools. Keep auth enabled, keep private data out of Git, and do not expose raw model/service ports publicly. Deployment details are in the [setup guide](docs/setup.md#security-notes). +Odysseus is a self-hosted workspace with powerful local tools. Keep auth enabled, keep private data out of Git, and do not expose raw model or service ports publicly. Read the [security policy](SECURITY.md) and the [deployment security guidance](docs/setup.md#security-notes). ## Star History @@ -73,4 +77,4 @@ Odysseus is a self-hosted workspace with powerful local tools. Keep auth enabled ## License -AGPL-3.0-or-later -- see [LICENSE](LICENSE) and [ACKNOWLEDGMENTS.md](ACKNOWLEDGMENTS.md). +Licensed under AGPL-3.0-or-later. See the [license](LICENSE) and [acknowledgments](ACKNOWLEDGMENTS.md). diff --git a/docs/setup.md b/docs/setup.md index 61381461b..ab7265698 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -1,6 +1,28 @@ # Odysseus Setup Guide -This page keeps the detailed install, deployment, troubleshooting, and configuration notes out of the front README. +This guide covers installation, deployment, troubleshooting, and configuration. +For a minimal Docker installation, start with the +[repository README](../README.md#quick-start). + +## On this page + +- [Quick Start](#quick-start) + - [Docker](#docker-recommended) + - [Native Linux and macOS](#native-linux--macos) + - [Apple Silicon](#apple-silicon) + - [Native Windows](#native-windows) +- [Troubleshooting and advanced setup](#troubleshooting--advanced-setup) +- [Security notes](#security-notes) +- [Configuration](#configuration) +- [Architecture](#architecture) +- [Data and backups](#data) + +Related guidance: + +- [Security policy](../SECURITY.md) +- [Architecture overview](ARCHITECTURE.md) +- [Backup and restore guide](backup-restore.md) +- [Contributing guide](../CONTRIBUTING.md) ## Quick Start @@ -15,8 +37,8 @@ On first setup, Odysseus creates an admin account (`admin` unless For Docker installs, the same line is in `docker compose logs odysseus`. Use that for the first login, then change it in **Settings**. -Contributing? See [CONTRIBUTING.md](../CONTRIBUTING.md) for setup, testing, and -pull request guidelines. +Contributing? See the [contributing guide](../CONTRIBUTING.md) for development +setup, testing, and pull request guidelines. ### Docker (recommended) ```bash @@ -441,8 +463,8 @@ uv pip sync requirements.lock # reproduce it exactly la ### Outlook / Office 365 email Odysseus email accounts currently use IMAP/SMTP username-password auth. Outlook and Microsoft 365 generally require OAuth instead, so normal Microsoft mailbox -passwords will fail. See [email-outlook.md](email-outlook.md) for the -current limitation and the planned integration direction. +passwords will fail. See the [Outlook and Microsoft 365 email guide](email-outlook.md) +for the current limitation and planned integration direction. ## Security Notes Odysseus is a self-hosted workspace with powerful local tools: shell access, file uploads, model downloads, web research, email/calendar integrations, and API tokens. Treat it like an admin console. @@ -530,19 +552,16 @@ npx -y @playwright/mcp@latest --version That installs `@playwright/mcp` plus Playwright (~300MB total). Restart Odysseus and the server will register at startup. ## Architecture -``` -app.py # FastAPI entry point -core/ auth, database, middleware, constants -src/ llm_core, agent_loop, agent_tools, chat_processor, search/ -routes/ chat, session, document, memory, model … endpoints -services/ docs, memory, search, hwfit (Cookbook) … -static/ index.html + app.js + style.css + js/ (modular front-end) -docs/ landing page (index.html) + preview clips -``` + +For stable high-level runtime structure, subsystem boundaries, and documentation +authority, see the [architecture overview](ARCHITECTURE.md). + +Source code, tests, and configuration remain authoritative for +implementation-sensitive behaviour. ## Data All user data lives in `data/` (gitignored): `app.db` (sessions, messages, documents), `memory.json`, `presets.json`, `uploads/`, `personal_docs/`, `chroma/`, `settings.json`. -To back up or restore everything in `data/`, see the -[Backup & Restore guide](backup-restore.md). +To protect or recover this data, follow the +[backup and restore guide](backup-restore.md). From 53ce95049db686588544776191c83ea5469232ad Mon Sep 17 00:00:00 2001 From: Alexandre Teixeira <alexandremagteixeira@gmail.com> Date: Tue, 28 Jul 2026 23:02:40 +0100 Subject: [PATCH 016/125] docs: validate attachment lifecycle contract --- docs/attachments.md | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/attachments.md b/docs/attachments.md index 93f9e0ffe..4c0aec5fb 100644 --- a/docs/attachments.md +++ b/docs/attachments.md @@ -1,7 +1,13 @@ # Attachment References and Upload Storage -Odysseus stores uploaded bytes once under the configured upload directory and -passes stable references through chat history, tools, and future artifact work. +> [!NOTE] +> This document records the current attachment-reference and upload-lifecycle +> contract proposed for maintainer acceptance. Source code, tests, and +> configuration remain authoritative for implementation-sensitive behaviour. + +Odysseus stores chat and document attachment bytes under the configured upload +directory and passes stable references through chat history, document flows, and +tool context. The goal is to avoid duplicating large inline media payloads in `chat_messages.content` or the SQLite FTS index. @@ -54,6 +60,32 @@ External MCP/custom tools should treat the URI and attachment ID as the stable contract and request bytes through an owner-checked server path, not by assuming host filesystem layout. +## Implementation evidence + +The current contract is implemented primarily through: + +- `src/upload_handler.py` for upload metadata, owner-aware resolution, + reservations, cleanup, and deletion; +- `src/attachment_refs.py` for compact persisted references and search-index + sanitization; +- `src/document_processor.py` for resolving attachments into chat/model context; +- `src/tool_execution.py` for attachment manifests exposed to tools; +- `routes/upload_routes.py` and `routes/document_helpers.py` for upload and + retrieval paths. + +Focused regression coverage includes: + +- `tests/test_attachment_refs.py`; +- `tests/test_upload_handler_cleanup.py`; +- `tests/test_replace_messages_upload_reservations.py`; +- `tests/test_resolve_upload_path_nondict.py`; +- `tests/test_chat_preprocess_tool_policy.py`; +- the upload, attachment, and PDF-marker cases in + `tests/test_security_regressions.py`. + +These tests cover compact persistence, owner isolation, path containment, +cleanup safety, reservation-before-write behaviour, and traversal resistance. + ## Retention and Deletion Current retention behavior is conservative: From 61c138d9e7f50eaaf73185d1249f8debe809094b Mon Sep 17 00:00:00 2001 From: Boody <bodebi639@gmail.com> Date: Wed, 29 Jul 2026 12:26:09 +0300 Subject: [PATCH 017/125] fixed .env.example ODYSSEUS_TTS_CACHE_MAX_BYTES into correct 500 MBs --- .env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 4eb4695e0..2d1be3373 100644 --- a/.env.example +++ b/.env.example @@ -189,7 +189,7 @@ SEARXNG_INSTANCE=http://localhost:8080 # ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES=26214400 # email compose attachment (25 MB) # ODYSSEUS_STT_MAX_AUDIO_BYTES=26214400 # speech-to-text audio (25 MB) # ODYSSEUS_ICS_MAX_BYTES=10485760 # calendar .ics import (10 MB) -# ODYSSEUS_TTS_CACHE_MAX_BYTES=52428800 # TTS cache (500 MB) +# ODYSSEUS_TTS_CACHE_MAX_BYTES=524288000 # TTS cache (500 MB) # ============================================================ # Host Docker access (explicit opt-in) From 46905ab9b0f4a2ecf85996b23f47f678501792e4 Mon Sep 17 00:00:00 2001 From: Boody <bodebi639@gmail.com> Date: Wed, 29 Jul 2026 12:32:43 +0300 Subject: [PATCH 018/125] added ODYSSEUS_TTS_CACHE_MAX_BYTES env variable to docker compose files --- docker-compose.gpu-amd.yml | 1 + docker-compose.gpu-nvidia.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/docker-compose.gpu-amd.yml b/docker-compose.gpu-amd.yml index 91e223e05..9699fc038 100644 --- a/docker-compose.gpu-amd.yml +++ b/docker-compose.gpu-amd.yml @@ -67,6 +67,7 @@ services: - ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES=${ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES:-26214400} - ODYSSEUS_STT_MAX_AUDIO_BYTES=${ODYSSEUS_STT_MAX_AUDIO_BYTES:-26214400} - ODYSSEUS_ICS_MAX_BYTES=${ODYSSEUS_ICS_MAX_BYTES:-10485760} + - ODYSSEUS_TTS_CACHE_MAX_BYTES=${ODYSSEUS_TTS_CACHE_MAX_BYTES} - DATA_BRAVE_API_KEY=${DATA_BRAVE_API_KEY:-} - GOOGLE_API_KEY=${GOOGLE_API_KEY:-} - GOOGLE_PSE_CX=${GOOGLE_PSE_CX:-} diff --git a/docker-compose.gpu-nvidia.yml b/docker-compose.gpu-nvidia.yml index e8c2fd032..804a0a14e 100644 --- a/docker-compose.gpu-nvidia.yml +++ b/docker-compose.gpu-nvidia.yml @@ -66,6 +66,7 @@ services: - ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES=${ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES:-26214400} - ODYSSEUS_STT_MAX_AUDIO_BYTES=${ODYSSEUS_STT_MAX_AUDIO_BYTES:-26214400} - ODYSSEUS_ICS_MAX_BYTES=${ODYSSEUS_ICS_MAX_BYTES:-10485760} + - ODYSSEUS_TTS_CACHE_MAX_BYTES=${ODYSSEUS_TTS_CACHE_MAX_BYTES} - DATA_BRAVE_API_KEY=${DATA_BRAVE_API_KEY:-} - GOOGLE_API_KEY=${GOOGLE_API_KEY:-} - GOOGLE_PSE_CX=${GOOGLE_PSE_CX:-} From 9914651cc9f1756fd1610952af42de8848b6015d Mon Sep 17 00:00:00 2001 From: Boody <bodebi639@gmail.com> Date: Wed, 29 Jul 2026 12:41:31 +0300 Subject: [PATCH 019/125] improve cache eviction logic to handle file access errors and ensure stability --- services/tts/tts_service.py | 64 ++++++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 23 deletions(-) diff --git a/services/tts/tts_service.py b/services/tts/tts_service.py index e1c67d4da..c7f787954 100644 --- a/services/tts/tts_service.py +++ b/services/tts/tts_service.py @@ -98,31 +98,49 @@ class TTSService: self._enforce_cache_limit() def _enforce_cache_limit(self): - """Evicts oldest files if the cache exceeds the configured byte limit.""" - if self.max_cache_bytes <= 0: - return + """Evicts oldest files if the cache exceeds the configured byte limit.""" + if self.max_cache_bytes <= 0: + return - files = [f for f in self.cache_dir.glob("*.*") if f.is_file()] - total_size = sum(f.stat().st_size for f in files) + try: + files = [] + total_size = 0 - if total_size > self.max_cache_bytes: - logger.info(f"TTS cache ({total_size} bytes) exceeded limit ({self.max_cache_bytes} bytes). Evicting oldest files.") - - # Sort files by modification time (oldest first) - files.sort(key=lambda f: f.stat().st_mtime) - - # Trim down to 80% of max capacity so we aren't constantly triggering this on every new generation - target_size = self.max_cache_bytes * 0.8 - - while files and total_size > target_size: - f = files.pop(0) - try: - size = f.stat().st_size - f.unlink() - total_size -= size - except FileNotFoundError: - # File was deleted by another process - continue + # Safely scan files and sum sizes, ignoring files deleted mid-scan + for f in self.cache_dir.glob("*.*"): + try: + if f.is_file(): + files.append(f) + total_size += f.stat().st_size + except OSError: + continue + + if total_size > self.max_cache_bytes: + logger.info( + f"TTS cache ({total_size} bytes) exceeded limit ({self.max_cache_bytes} bytes). Evicting oldest files." + ) + + # Sort files by modification time (oldest first) + try: + files.sort(key=lambda f: f.stat().st_mtime) + except OSError as e: + logger.warning(f"Failed to sort cache files by mtime: {e}") + + # Trim down to 80% of max capacity + target_size = self.max_cache_bytes * 0.8 + + while files and total_size > target_size: + f = files.pop(0) + try: + size = f.stat().st_size + f.unlink() + total_size -= size + except OSError as e: + logger.warning(f"Failed to evict cache file {f}: {e}") + continue + + except Exception as e: + logger.warning(f"Error enforcing TTS cache limit: {e}", exc_info=True) def clear_cache(self): count = 0 From d183fe545b25766badbf5ada731ccd0fe7b0c594 Mon Sep 17 00:00:00 2001 From: Boody <bodebi639@gmail.com> Date: Wed, 29 Jul 2026 12:42:20 +0300 Subject: [PATCH 020/125] add test for cache eviction handling unlink errors gracefully --- tests/test_tts_service_enforce_cache_limit.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_tts_service_enforce_cache_limit.py b/tests/test_tts_service_enforce_cache_limit.py index 8a726557b..1da9d16c0 100644 --- a/tests/test_tts_service_enforce_cache_limit.py +++ b/tests/test_tts_service_enforce_cache_limit.py @@ -70,4 +70,28 @@ def test_cache_limit_disabled(tmp_path, monkeypatch): # Ensure nothing was deleted files = list(tmp_path.glob("*.*")) assert len(files) == 3 - assert sum(f.stat().st_size for f in files) == 3000 \ No newline at end of file + assert sum(f.stat().st_size for f in files) == 3000 + +def test_cache_eviction_handles_unlink_error_gracefully(tmp_path, monkeypatch): + """Test that if unlinking a file fails, _put_cache still succeeds without raising.""" + service = TTSService(cache_dir=str(tmp_path)) + service.max_cache_bytes = 50 + + # Create a file to evict + old_file = tmp_path / "old.wav" + old_file.write_bytes(b"x" * 40) + + # Monkeypatch unlink on Path objects to simulate a PermissionError / file-lock failure + def mock_unlink(self_path): + raise OSError("Permission denied / file locked") + + monkeypatch.setattr(Path, "unlink", mock_unlink) + + # Writing a new file triggers eviction which encounters the mocked unlink error + try: + service._put_cache("new_key", b"y" * 40) + except Exception as e: + pytest.fail(f"_put_cache raised an exception during failed eviction: {e}") + + # The new file should still be written successfully + assert (tmp_path / "new_key.wav").exists() \ No newline at end of file From 2e631ad8160c76c42f757471d372cffa59ac88e4 Mon Sep 17 00:00:00 2001 From: Boody <bodebi639@gmail.com> Date: Wed, 29 Jul 2026 12:47:55 +0300 Subject: [PATCH 021/125] improve cache size calculation by filtering file types --- services/tts/tts_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/tts/tts_service.py b/services/tts/tts_service.py index c7f787954..dd37865a7 100644 --- a/services/tts/tts_service.py +++ b/services/tts/tts_service.py @@ -107,9 +107,9 @@ class TTSService: total_size = 0 # Safely scan files and sum sizes, ignoring files deleted mid-scan - for f in self.cache_dir.glob("*.*"): + for f in self.cache_dir.iterdir(): try: - if f.is_file(): + if f.is_file() and f.suffix.lower() in (".mp3", ".wav"): files.append(f) total_size += f.stat().st_size except OSError: From 9297bed5b9574ea5ba13039821a51d77a2afaccd Mon Sep 17 00:00:00 2001 From: Boody <bodebi639@gmail.com> Date: Wed, 29 Jul 2026 12:54:55 +0300 Subject: [PATCH 022/125] add ODYSSEUS_TTS_CACHE_MAX_BYTES environment variable to docker-compose --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index b1f2c37ee..b0efb4439 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,6 +55,7 @@ services: - ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES=${ODYSSEUS_EMAIL_COMPOSE_UPLOAD_MAX_BYTES:-26214400} - ODYSSEUS_STT_MAX_AUDIO_BYTES=${ODYSSEUS_STT_MAX_AUDIO_BYTES:-26214400} - ODYSSEUS_ICS_MAX_BYTES=${ODYSSEUS_ICS_MAX_BYTES:-10485760} + - ODYSSEUS_TTS_CACHE_MAX_BYTES=${ODYSSEUS_TTS_CACHE_MAX_BYTES} - DATA_BRAVE_API_KEY=${DATA_BRAVE_API_KEY:-} - GOOGLE_API_KEY=${GOOGLE_API_KEY:-} - GOOGLE_PSE_CX=${GOOGLE_PSE_CX:-} From 3250a4ce68ffb05e6d5023ca55c6c2189d548686 Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:04:28 +0100 Subject: [PATCH 023/125] fix(ci): clear review label when issues close (#5813) The issue-close lifecycle change is narrowly scoped and correct. Closed issues remove the stale \`ready for review\` label and return before normal validation can restore it. Focused regressions cover closure and subsequent edits to a closed issue. The branch was updated onto current \`dev\`. The focused test, merged-result validation, diff checks, and GitHub CI passed. No blocking review threads remain. --- .github/scripts/check-issue-description.js | 13 ++- .github/workflows/issue-description-check.yml | 2 +- tests/test_issue_description_check.py | 86 +++++++++++++++++++ 3 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 tests/test_issue_description_check.py diff --git a/.github/scripts/check-issue-description.js b/.github/scripts/check-issue-description.js index a76ca29ab..63162b0d7 100644 --- a/.github/scripts/check-issue-description.js +++ b/.github/scripts/check-issue-description.js @@ -153,6 +153,16 @@ module.exports = async ({ github, context, core }) => { } } + const LABEL_BAD = 'needs more info'; + const LABEL_GOOD = 'ready for review'; + + // Closed issues are no longer awaiting review. + // This also prevents later edits to closed issues from restoring the label. + if (issue.state === 'closed') { + await dropLabel(LABEL_GOOD); + return; + } + // ── Find existing bot comment to update in-place ────────────────────────── const MARKER = '<!-- issue-description-check -->'; const { data: comments } = await github.rest.issues.listComments({ @@ -160,9 +170,6 @@ module.exports = async ({ github, context, core }) => { }); const existing = comments.find(c => c.user.type === 'Bot' && c.body.includes(MARKER)); - const LABEL_BAD = 'needs more info'; - const LABEL_GOOD = 'ready for review'; - if (failures.length === 0) { if (existing) { await github.rest.issues.deleteComment({ owner, repo, comment_id: existing.id }); diff --git a/.github/workflows/issue-description-check.yml b/.github/workflows/issue-description-check.yml index 52e9dddae..5ce6037f0 100644 --- a/.github/workflows/issue-description-check.yml +++ b/.github/workflows/issue-description-check.yml @@ -2,7 +2,7 @@ name: ci / issue description check on: issues: - types: [opened, edited, reopened] + types: [opened, edited, reopened, closed] permissions: issues: write diff --git a/tests/test_issue_description_check.py b/tests/test_issue_description_check.py new file mode 100644 index 000000000..196f21cfc --- /dev/null +++ b/tests/test_issue_description_check.py @@ -0,0 +1,86 @@ +"""Regression coverage for issue-description label lifecycle events.""" + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + + +_REPO = Path(__file__).resolve().parent.parent +_CHECKER = _REPO / ".github" / "scripts" / "check-issue-description.js" +_WORKFLOW = _REPO / ".github" / "workflows" / "issue-description-check.yml" +pytestmark = pytest.mark.skipif(not shutil.which("node"), reason="node not on PATH") + + +def _run_closed_issue(action): + harness = r""" +const checkIssueDescription = require(process.argv[1]); +const action = process.argv[2]; +const calls = []; +const unexpected = (name) => async () => { + throw new Error(`${name} should not be called for a closed issue`); +}; + +const github = { + rest: { + issues: { + removeLabel: async (params) => calls.push({ method: 'removeLabel', params }), + getLabel: unexpected('getLabel'), + addLabels: unexpected('addLabels'), + listComments: unexpected('listComments'), + createComment: unexpected('createComment'), + updateComment: unexpected('updateComment'), + deleteComment: unexpected('deleteComment'), + }, + }, +}; +const context = { + payload: { + action, + issue: { number: 42, state: 'closed', body: '', labels: [] }, + }, + repo: { owner: 'odysseus-dev', repo: 'odysseus' }, +}; +const core = { + warning: unexpected('core.warning'), + setFailed: unexpected('core.setFailed'), +}; + +checkIssueDescription({ github, context, core }) + .then(() => process.stdout.write(JSON.stringify(calls))) + .catch((error) => { + console.error(error); + process.exitCode = 1; + }); +""" + proc = subprocess.run( + ["node", "-e", harness, str(_CHECKER), action], + capture_output=True, + text=True, + cwd=str(_REPO), + timeout=30, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_workflow_handles_issue_closures(): + workflow = _WORKFLOW.read_text() + assert "types: [opened, edited, reopened, closed]" in workflow + + +@pytest.mark.parametrize("action", ["closed", "edited"]) +def test_closed_issue_only_drops_ready_for_review(action): + assert _run_closed_issue(action) == [ + { + "method": "removeLabel", + "params": { + "owner": "odysseus-dev", + "repo": "odysseus", + "issue_number": 42, + "name": "ready for review", + }, + } + ] From 6a84398e75835e899d792953e0b4e55ac40d9ccf Mon Sep 17 00:00:00 2001 From: holden093 <kevin@nixit.it> Date: Thu, 30 Jul 2026 10:06:31 +0200 Subject: [PATCH 024/125] fix(skills): use utility model for skill tests instead of chat default (#5746) Skill tests are background automation tasks (like auto-naming and memory audit) and should use the configured utility model. Previously they resolved via resolve_endpoint("default") which returned the chat model, bypassing the utility model entirely. This completes the sweep started in PR #4027 which fixed auto-naming and memory audit but missed skill tests. --- routes/skills_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/skills_routes.py b/routes/skills_routes.py index 711baa2e5..00bef589f 100644 --- a/routes/skills_routes.py +++ b/routes/skills_routes.py @@ -1409,7 +1409,7 @@ def setup_skills_routes(skills_manager: SkillsManager) -> APIRouter: # Prefer the configured DEFAULT (→ Utility) model — not the current chat # session's model. Fall back to the caller's session model only if unset. - url, model, headers = resolve_endpoint("default", owner=user) + url, model, headers = resolve_endpoint("utility", owner=user) if not url or not model: url = url or ((body.get("endpoint_url") or "").strip() or None) model = model or ((body.get("model") or "").strip() or None) From f23221420fea7df82d9ca532cf6b16a4868b1136 Mon Sep 17 00:00:00 2001 From: Husam <husamemad60@gmail.com> Date: Thu, 30 Jul 2026 11:54:59 +0300 Subject: [PATCH 025/125] fix(skills): replace deprecated utcnow in skill timestamp helper (#5777) * fix(skills): replace deprecated utcnow in skill timestamp helper _now_iso() builds the 'created' value in skill frontmatter. datetime.utcnow() returns a naive datetime and has been deprecated since Python 3.12, scheduled for removal. Switch to the timezone-aware datetime.now(timezone.utc), keeping the serialized YYYY-MM-DDTHH:MM:SSZ shape unchanged so existing skill files keep parsing. timezone.utc is used rather than the datetime.UTC alias, which is 3.11+ only. Adds regression tests covering the deprecation, the serialized shape, and UTC correctness under a non-UTC local timezone -- the last guards against a bare datetime.now(), which yields the same shape but local wall time. Fixes #5697 * test(skills): skip timezone mutation where unsupported --------- Co-authored-by: Alexandre Teixeira <alexandremagteixeira@gmail.com> --- services/memory/skill_format.py | 4 +- tests/test_skill_format_timestamp.py | 58 ++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 tests/test_skill_format_timestamp.py diff --git a/services/memory/skill_format.py b/services/memory/skill_format.py index 2b2dfb1b3..628474b04 100644 --- a/services/memory/skill_format.py +++ b/services/memory/skill_format.py @@ -50,7 +50,7 @@ import json import logging import re from dataclasses import dataclass, field -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Dict, List, Optional logger = logging.getLogger(__name__) @@ -441,4 +441,4 @@ class Skill: def _now_iso() -> str: - return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") diff --git a/tests/test_skill_format_timestamp.py b/tests/test_skill_format_timestamp.py new file mode 100644 index 000000000..a9309bdc1 --- /dev/null +++ b/tests/test_skill_format_timestamp.py @@ -0,0 +1,58 @@ +"""Regression for issue #5697 — skill timestamps must not use ``datetime.utcnow()``. + +``_now_iso()`` builds the ``created`` value in skill frontmatter. ``utcnow()`` +returns a *naive* datetime and has been deprecated since Python 3.12, scheduled +for removal. The replacement must stay timezone-aware while keeping the +serialized ``YYYY-MM-DDTHH:MM:SSZ`` shape, so skill files written by older +versions keep parsing. + +The UTC check matters on its own: a bare ``datetime.now()`` also produces the +right shape, but emits local wall time, which would silently backdate or +postdate skills for every user outside UTC. +""" + +import os +import re +import time +import warnings +from datetime import datetime, timezone + +import pytest + +from services.memory.skill_format import _now_iso + +_ISO_Z = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$") + + +def test_now_iso_keeps_serialized_shape(): + assert _ISO_Z.match(_now_iso()) + + +def test_now_iso_emits_no_deprecation_warning(): + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + _now_iso() + assert not [w for w in caught if issubclass(w.category, DeprecationWarning)] + + +@pytest.mark.skipif( + not hasattr(time, "tzset"), + reason="time.tzset is unavailable on this platform", +) +def test_now_iso_is_utc_not_local_time(): + """Pin UTC under a non-UTC local timezone, where the two visibly diverge.""" + original_tz = os.environ.get("TZ") + os.environ["TZ"] = "Asia/Amman" # UTC+3, never UTC + time.tzset() + try: + emitted = datetime.strptime(_now_iso(), "%Y-%m-%dT%H:%M:%SZ").replace( + tzinfo=timezone.utc + ) + drift = abs((emitted - datetime.now(timezone.utc)).total_seconds()) + assert drift < 60, f"timestamp is {drift}s off UTC — local time leaked in" + finally: + if original_tz is None: + os.environ.pop("TZ", None) + else: + os.environ["TZ"] = original_tz + time.tzset() From 578312200ac1828bc3bae039eac3c827036fdc69 Mon Sep 17 00:00:00 2001 From: Husam <husamemad60@gmail.com> Date: Thu, 30 Jul 2026 12:48:31 +0300 Subject: [PATCH 026/125] fix(markdown): restore extracted blocks verbatim so $& and $$ survive (#5768) The placeholder-restore pass in mdToHtml put code, math, mermaid and allowed-HTML blocks back with a string replacement, so String.replace read `$&`, `` $` ``, `$'` and `$$` in the *replacement* as substitution patterns. A fenced block containing them rendered corrupted: `$&` re-inserted the placeholder (`perl -pe 's/world/$& again/'` became `s/world/___CODE_BLOCK_0___amp; again/`), `` $` `` and `$'` spliced in the surrounding document, and `$$` collapsed to a single `$`. Pass a function replacer at all four sites, matching the inline-code site below them, which was already fixed this way. A function's return value is inserted verbatim with no `$` interpretation. The inline-code comment claimed `echo $1` would be read as a back-reference; with a string search value there are no capture groups, so `$1` is already literal. Reworded to name the four sequences that do corrupt. Fixes #5663 --- static/js/markdown.js | 20 ++++++++----- tests/test_markdown_rendering_js.py | 44 +++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/static/js/markdown.js b/static/js/markdown.js index 8735b83e7..f249facc9 100644 --- a/static/js/markdown.js +++ b/static/js/markdown.js @@ -758,30 +758,36 @@ export function mdToHtml(src, opts) { // Remove empty paragraphs s = s.replace(/<p><\/p>/g, ''); + // Every restore below passes a function replacer rather than the block string + // itself. With a string replacement, `String.replace` reads `$&`, `` $` ``, + // `$'` and `$$` in the *replacement* as substitution patterns, so a restored + // block containing them is corrupted: `$&` re-inserts the placeholder, `` $` `` + // and `$'` splice in the surrounding document, and `$$` collapses to `$`. Those + // sequences are ordinary content in fenced code (`perl -pe 's/x/$& y/'`, + // `echo "$$USD"`). A function replacer inserts its return value verbatim. + // CRITICAL: Restore allowed HTML blocks first allowedHtmlBlocks.forEach((block, index) => { - s = s.replace(`___ALLOWED_HTML_${index}___`, block); + s = s.replace(`___ALLOWED_HTML_${index}___`, () => block); }); // Restore math blocks mathBlocks.forEach((block, index) => { - s = s.replace(`___MATH_BLOCK_${index}___`, block); + s = s.replace(`___MATH_BLOCK_${index}___`, () => block); }); // Restore mermaid diagram blocks mermaidBlocks.forEach((block, index) => { - s = s.replace(`___MERMAID_BLOCK_${index}___`, block); + s = s.replace(`___MERMAID_BLOCK_${index}___`, () => block); }); // CRITICAL: Restore code blocks at the end codeBlocks.forEach((block, index) => { - s = s.replace(`___CODE_BLOCK_${index}___`, block); + s = s.replace(`___CODE_BLOCK_${index}___`, () => block); }); // Restore inline code spans last, so placeholders carried inside restored - // <a>/allowed-HTML blocks are resolved too. The function replacer keeps the - // escaped code literal — e.g. a shell snippet like `echo $1` is not treated - // as a regex back-reference. + // <a>/allowed-HTML blocks are resolved too. inlineCodeBlocks.forEach((block, index) => { s = s.replace(`___INLINE_CODE_${index}___`, () => block); }); diff --git a/tests/test_markdown_rendering_js.py b/tests/test_markdown_rendering_js.py index 2ffe8914f..536789b89 100644 --- a/tests/test_markdown_rendering_js.py +++ b/tests/test_markdown_rendering_js.py @@ -214,6 +214,50 @@ def test_inline_code_content_is_html_escaped(node_available): assert "<b>" not in html +def test_fenced_code_keeps_dollar_ampersand(node_available): + # Issue #5663: the block-restore pass used a string replacement, so `$&` in a + # restored block was read as "the matched text" and re-inserted the + # placeholder. `perl -pe 's/world/$& again/'` rendered as + # "s/world/___CODE_BLOCK_0___amp; again/" — the trailing "amp;" is the orphan + # left behind after `$&` consumed the `$&` of the escaped `$&`. + html = _run_markdown_case( + "```sh\necho \"hello world\" | perl -pe 's/world/$& again/'\n```" + ) + + assert "___CODE_BLOCK_" not in html + assert "s/world/$& again/" in html + assert "amp; again" not in html.replace("$& again", "") + + +def test_fenced_code_keeps_dollar_backtick_and_quote(node_available): + # `` $` `` and `$'` splice the text before/after the placeholder into the + # block. Unlike `$&` these leave no placeholder behind — the characters just + # vanish — so assert the content survives verbatim. + html = _run_markdown_case("```sh\nsed \"s/$`/x/\" && sed \"s/$'/y/\"\n```") + + assert "___CODE_BLOCK_" not in html + assert "s/$`/x/" in html + assert "s/$'/y/" in html + + +def test_fenced_code_keeps_double_dollar(node_available): + # `$$` collapsed to a single `$` in the restored block. + html = _run_markdown_case('```sh\necho "$$USD and $$"\n```') + + assert "$$USD and $$" in html + + +def test_mermaid_block_keeps_dollar_ampersand(node_available): + # The mermaid restore site had the same hazard: a node label containing `$&` + # re-inserted the ___MERMAID_BLOCK_n___ placeholder into the diagram source, + # which then fails to parse. The math and allowed-HTML sites are fixed the + # same way; they need KaTeX/sanitizer conditions this harness doesn't set up. + html = _run_markdown_case('```mermaid\ngraph TD; A["$&"] --> B;\n```') + + assert "___MERMAID_BLOCK_" not in html + assert "$&" in html + + def test_currency_dollar_amounts_are_not_rendered_as_math(node_available): # "$5 to $10" used to pair the two dollar signs as inline-math delimiters # and render "5 to" through KaTeX. Pandoc-style rules now reject it: the From 84709a00d979c396dfb34f5d60b2992efb5a9284 Mon Sep 17 00:00:00 2001 From: Husam <husamemad60@gmail.com> Date: Thu, 30 Jul 2026 13:30:00 +0300 Subject: [PATCH 027/125] fix(llm): omit temperature for major-only Opus ids (claude-opus-5) (#5761) The version pattern in _anthropic_rejects_temperature() required a minor component, so major-only ids like `claude-opus-5` never matched and the guard reported that the model accepts `temperature`. Anthropic rejects the field outright on Opus 4.7+, so every such call returned HTTP 400 and the stream aborted with zero tokens ("the model returned an empty response"). Make the minor optional and read a missing minor as `.0`. The major is also capped at 1-2 digits with a no-trailing-digit lookahead, mirroring the minor: once the minor is optional, a greedy major would swallow the date in `claude-3-opus-20240229` and read it as version 20240229, dropping temperature from a model that accepts it. Fixes #5753 Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- src/llm_core.py | 26 ++++++++++++++++------ tests/test_llm_core_anthropic_temp_omit.py | 26 +++++++++++++++++++++- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/llm_core.py b/src/llm_core.py index 4dec32376..3e84c1060 100644 --- a/src/llm_core.py +++ b/src/llm_core.py @@ -1237,15 +1237,27 @@ def _anthropic_rejects_temperature(model: str) -> bool: return False # `(?<![a-z])` anchors "opus" to a word boundary so a substring match like # `oct-opus`/`octopus-4-8` can't be read as Opus (it would otherwise strip - # temperature). Cap the minor at 1-2 digits and forbid a trailing digit so a - # dated id like `claude-opus-4-20250514` (Opus 4.0) parses as major-only (no - # minor match, kept) instead of reading the date `20250514` as a giant minor - # that would falsely test >= 4.7. Dated 4.7+ snapshots (`claude-opus-4-7- - # 20260201`) keep their explicit minor and are still matched. - match = re.search(r"(?<![a-z])opus[-_]?(\d+)[-_.](\d{1,2})(?!\d)", model.lower()) + # temperature). Both version components are capped at 1-2 digits and forbid a + # trailing digit, so an 8-digit date can never be read as a version number: + # `claude-opus-4-20250514` (Opus 4.0) parses as major-only rather than reading + # `20250514` as a giant minor, and `claude-3-opus-20240229` (legacy Claude 3 + # Opus, date directly after "opus-") fails to match at all rather than reading + # the date as a giant major. Dated 4.7+ snapshots (`claude-opus-4-7-20260201`) + # keep their explicit minor and are still matched. + # + # The minor is optional and a missing minor reads as `.0`, so major-only ids + # like `claude-opus-5` are correctly treated as >= 4.7 (issue #5753). Without + # this, every Opus 5 call kept `temperature` and failed with HTTP 400 — visible + # only on paths that pass a temperature, e.g. scheduled tasks inheriting + # `stream_agent_loop`'s 0.3 default, which returned empty responses. + match = re.search( + r"(?<![a-z])opus[-_]?(\d{1,2})(?!\d)(?:[-_.](\d{1,2})(?!\d))?", model.lower() + ) if not match: return False - return (int(match.group(1)), int(match.group(2))) >= (4, 7) + major = int(match.group(1)) + minor = int(match.group(2)) if match.group(2) else 0 + return (major, minor) >= (4, 7) # Reasoning effort level sent to Mistral thinking-capable models. Mistral's # API accepts "high", "medium", "low", "none" — see diff --git a/tests/test_llm_core_anthropic_temp_omit.py b/tests/test_llm_core_anthropic_temp_omit.py index 2274f1dc9..f7d26aef0 100644 --- a/tests/test_llm_core_anthropic_temp_omit.py +++ b/tests/test_llm_core_anthropic_temp_omit.py @@ -29,6 +29,13 @@ from src.llm_core import _anthropic_rejects_temperature, _build_anthropic_payloa "anthropic/claude-opus-4-7", # tolerate a provider-prefixed id "claude-opus-4-10", # future minor still >= 4.7 "claude-opus-5-0", # future major + # Major-only ids: a missing minor reads as `.0`, so these are >= 4.7 too + # (issue #5753). Before the fix the version pattern required a minor, so + # these fell through to "accepts temperature" and every call 400'd. + "claude-opus-5", + "claude-opus-5-20260101", # major-only + dated snapshot + "anthropic/claude-opus-5", # major-only behind a provider prefix + "claude-opus-6", # future major-only ], ) def test_opus_47_plus_rejects_temperature(model): @@ -48,7 +55,10 @@ def test_opus_47_plus_rejects_temperature(model): "claude-opus-4-6-20251201", # dated 4.6 snapshot — older, still keeps temperature "claude-sonnet-4-6", "claude-3-5-sonnet", - "claude-3-opus-20240229", # legacy Claude 3 Opus — no opus-N-M pattern, kept + "claude-3-opus-20240229", # legacy Claude 3 Opus — date directly after + # "opus-", so the major must not swallow it as version 20240229 (that is + # what makes capping the major at 1-2 digits necessary once the minor + # became optional in #5753). "claude-haiku-4-5", "claude-x", "octopus-4-8", # "opus" only as a substring of another word — must not match @@ -87,6 +97,20 @@ def test_payload_keeps_temperature_for_older_models(): assert _payload("claude-3-5-sonnet", 1.2)["temperature"] == 1.0 +def test_payload_omits_temperature_for_major_only_opus_5(): + # Issue #5753: the scheduled-task path calls stream_agent_loop() without a + # temperature and inherits its 0.3 default, so `claude-opus-5` 400'd on every + # run and surfaced as "the model returned an empty response". Interactive chat + # leaves temperature None and never hit it. + assert "temperature" not in _payload("claude-opus-5", 0.3) + + +def test_payload_keeps_temperature_for_legacy_claude_3_opus(): + # Guards the major-digit cap: `opus-20240229` must not parse as version + # 20240229, or Claude 3 Opus would silently lose the caller's temperature. + assert _payload("claude-3-opus-20240229", 0.5)["temperature"] == 0.5 + + def test_payload_keeps_temperature_for_dated_opus_4_0(): # Anthropic's dated id for Opus 4.0 (claude-opus-4-20250514) is in this repo's # ANTHROPIC_MODELS list. The date must not be misread as a >= 4.7 minor, or the From 28c333e64780ae2fbb7f0757d752e8bb5fb74990 Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Thu, 30 Jul 2026 12:24:39 +0100 Subject: [PATCH 028/125] fix(email): preserve OAuth SMTP security (#5802) --- static/js/settings.js | 2 ++ tests/test_email_oauth_connect_smtp_security.py | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 tests/test_email_oauth_connect_smtp_security.py diff --git a/static/js/settings.js b/static/js/settings.js index 72936adee..3819c83fa 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -3031,12 +3031,14 @@ async function initEmailAccountsSettings() { const body = { name: el('eaf-name').value.trim() || el('eaf-from').value.trim(), from_address: el('eaf-from').value.trim(), + display_name: el('eaf-display-name').value.trim(), imap_host: el('eaf-imap-host').value.trim(), imap_port: parseInt(el('eaf-imap-port').value) || 993, imap_user: el('eaf-imap-user').value.trim(), imap_starttls: el('eaf-imap-starttls').checked, smtp_host: el('eaf-smtp-host').value.trim(), smtp_port: parseInt(el('eaf-smtp-port').value) || 587, + smtp_security: el('eaf-smtp-security').value, smtp_user: el('eaf-imap-user').value.trim(), }; if (!body.name) { el('eaf-msg').textContent = 'Enter a Name or Email first'; el('eaf-msg').style.color = 'var(--red)'; return; } diff --git a/tests/test_email_oauth_connect_smtp_security.py b/tests/test_email_oauth_connect_smtp_security.py new file mode 100644 index 000000000..21c4224a6 --- /dev/null +++ b/tests/test_email_oauth_connect_smtp_security.py @@ -0,0 +1,15 @@ +"""Regression coverage for SMTP security saved before Google OAuth.""" + +from pathlib import Path + + +_REPO = Path(__file__).resolve().parents[1] + + +def test_email_tab_oauth_connect_persists_selected_smtp_security(): + source = (_REPO / "static" / "js" / "settings.js").read_text(encoding="utf-8") + start = source.index("el('eaf-oauth-btn').addEventListener") + handler_body = source[start:source.index("if (!body.name)", start)] + + assert "smtp_security: el('eaf-smtp-security').value" in handler_body + assert "display_name: el('eaf-display-name').value.trim()" in handler_body From 25c9e735ef5ce605f47f8f666ac6689056d2c10c Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Thu, 30 Jul 2026 14:57:07 +0100 Subject: [PATCH 029/125] fix(email): open settings after OAuth callback (#5803) --- static/js/settings.js | 45 +++++++++++---------- tests/test_email_oauth_settings_redirect.py | 19 +++++++++ 2 files changed, 42 insertions(+), 22 deletions(-) create mode 100644 tests/test_email_oauth_settings_redirect.py diff --git a/static/js/settings.js b/static/js/settings.js index 3819c83fa..540acff00 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -5790,29 +5790,30 @@ export function close() { window.history.replaceState(null, '', clean); const success = sp.has('email_oauth_success'); const errMsg = sp.get('email_oauth_error') || ''; - // Open settings → integrations after the app has initialised. - function _tryOpen() { - if (window.settingsModule && typeof window.settingsModule.open === 'function') { - window.settingsModule.open('integrations'); - // Brief toast-style banner. - const banner = document.createElement('div'); - banner.textContent = success - ? '✓ Google account connected — email is ready' - : `Google OAuth failed: ${errMsg || 'unknown error'}`; - Object.assign(banner.style, { - position: 'fixed', bottom: '24px', left: '50%', transform: 'translateX(-50%)', - background: success ? 'var(--accent, #50fa7b)' : 'var(--red, #ff5555)', - color: '#000', padding: '8px 18px', borderRadius: '6px', fontSize: '12px', - fontWeight: '600', zIndex: '99999', pointerEvents: 'none', - boxShadow: '0 2px 12px rgba(0,0,0,0.3)', - }); - document.body.appendChild(banner); - setTimeout(() => banner.remove(), 4000); - } else { - setTimeout(_tryOpen, 100); - } + // Open settings → integrations once the document is ready. This module owns + // the open() API, so it does not need to wait for a window-level alias. + function _showResult() { + open('integrations'); + // Brief toast-style banner. + const banner = document.createElement('div'); + banner.textContent = success + ? 'Google account connected — email is ready' + : `Google OAuth failed: ${errMsg || 'unknown error'}`; + Object.assign(banner.style, { + position: 'fixed', bottom: '24px', left: '50%', transform: 'translateX(-50%)', + background: success ? 'var(--accent, #50fa7b)' : 'var(--red, #ff5555)', + color: '#000', padding: '8px 18px', borderRadius: '6px', fontSize: '12px', + fontWeight: '600', zIndex: '99999', pointerEvents: 'none', + boxShadow: '0 2px 12px rgba(0,0,0,0.3)', + }); + document.body.appendChild(banner); + setTimeout(() => banner.remove(), 4000); + } + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', _showResult, { once: true }); + } else { + _showResult(); } - _tryOpen(); })(); const settingsModule = { open, close, initIntegrations, initUnifiedIntegrations, syncAdminVisibility, refreshAiModelEndpoints }; diff --git a/tests/test_email_oauth_settings_redirect.py b/tests/test_email_oauth_settings_redirect.py new file mode 100644 index 000000000..f7d588132 --- /dev/null +++ b/tests/test_email_oauth_settings_redirect.py @@ -0,0 +1,19 @@ +"""Regression coverage for the settings UI after Google OAuth redirects.""" + +from pathlib import Path + + +_REPO = Path(__file__).resolve().parents[1] + + +def test_oauth_redirect_uses_the_module_local_settings_api(): + source = (_REPO / "static" / "js" / "settings.js").read_text(encoding="utf-8") + handler = source[ + source.index("(function _handleOauthRedirect"): + source.index("const settingsModule =") + ] + + assert "open('integrations');" in handler + assert "window.settingsModule" not in handler + assert "window.__odysseusAppStarted" not in handler + assert "document.addEventListener('DOMContentLoaded', _showResult, { once: true })" in handler From 0de76c4056a0f0fb8f24b3bf2972fa5a185d7b0c Mon Sep 17 00:00:00 2001 From: "Tal.Yuan" <ydonghao@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:44:00 +0800 Subject: [PATCH 030/125] refactor(routes): move vault domain into routes/vault/ subpackage (#5780) Slice 2k of the route-domain reorganization (#4082/#4071). Moves vault_routes.py into routes/vault/, leaving a backward-compat sys.modules shim. Pure file reorganization, no behavior change. --- app.py | 2 +- routes/vault/__init__.py | 5 + routes/vault/vault_routes.py | 242 +++++++++++++++++++++++++++++++ routes/vault_routes.py | 246 ++------------------------------ tests/test_vault_routes_shim.py | 11 ++ 5 files changed, 268 insertions(+), 238 deletions(-) create mode 100644 routes/vault/__init__.py create mode 100644 routes/vault/vault_routes.py create mode 100644 tests/test_vault_routes_shim.py diff --git a/app.py b/app.py index 2ae5ec761..5fb2da54d 100644 --- a/app.py +++ b/app.py @@ -852,7 +852,7 @@ app.include_router(setup_codex_routes( )) app.include_router(setup_claude_routes()) -from routes.vault_routes import setup_vault_routes +from routes.vault.vault_routes import setup_vault_routes app.include_router(setup_vault_routes()) # Contacts (CardDAV) diff --git a/routes/vault/__init__.py b/routes/vault/__init__.py new file mode 100644 index 000000000..8aa82701d --- /dev/null +++ b/routes/vault/__init__.py @@ -0,0 +1,5 @@ +"""Vault route domain package (slice 2k, #4082/#4071). + +Contains vault_routes.py, migrated from the flat routes/ directory. +Backward-compat shim at routes/vault_routes.py re-exports from here. +""" diff --git a/routes/vault/vault_routes.py b/routes/vault/vault_routes.py new file mode 100644 index 000000000..7e97500f0 --- /dev/null +++ b/routes/vault/vault_routes.py @@ -0,0 +1,242 @@ +""" +vault_routes.py + +Vaultwarden / Bitwarden CLI integration — config and unlock endpoints. +Stores the BW_SESSION key in data/vault.json with restrictive permissions. +""" + +import json +import logging +import os +import shutil +import asyncio +from pathlib import Path +from datetime import datetime +from fastapi import APIRouter, Request +from pydantic import BaseModel + +from core.middleware import require_admin +from core.platform_compat import IS_WINDOWS, safe_chmod, which_tool +from src.constants import VAULT_FILE as _VAULT_FILE + +logger = logging.getLogger(__name__) + +VAULT_FILE = Path(_VAULT_FILE) + + +def _find_bw() -> str: + """Locate the bw binary, checking PATH and common npm-global locations. + + On Windows the Bitwarden CLI shim is `bw.cmd`/`bw.exe`, resolved by + which_tool via PATHEXT. + """ + p = which_tool("bw") + if p: + return p + if IS_WINDOWS: + appdata = os.environ.get("APPDATA", os.path.expanduser("~")) + for candidate in ( + os.path.join(appdata, "npm", "bw.cmd"), + os.path.join(appdata, "npm", "bw.exe"), + ): + if os.path.isfile(candidate): + return candidate + return "bw" + home = os.path.expanduser("~") + for candidate in ( + f"{home}/.npm-global/bin/bw", + f"{home}/.nvm/versions/node/*/bin/bw", + "/usr/local/bin/bw", + "/opt/homebrew/bin/bw", + ): + if "*" in candidate: + import glob + for m in glob.glob(candidate): + if os.path.isfile(m) and os.access(m, os.X_OK): + return m + elif os.path.isfile(candidate) and os.access(candidate, os.X_OK): + return candidate + return "bw" # fall back to PATH lookup (will FileNotFoundError, handled below) + + +def _load_config() -> dict: + if VAULT_FILE.exists(): + try: + data = json.loads(VAULT_FILE.read_text(encoding="utf-8")) + return data if isinstance(data, dict) else {} + except Exception: + pass + return {} + + +def _save_config(cfg: dict): + VAULT_FILE.parent.mkdir(parents=True, exist_ok=True) + VAULT_FILE.write_text(json.dumps(cfg, indent=2), encoding="utf-8") + # POSIX: restrict the BW_SESSION store to 0o600. Windows: no-op (profile dir + # is ACL-restricted already). + safe_chmod(str(VAULT_FILE), 0o600) + + +async def _run_bw(args: list, session: str = None, input_text: str = None, + bw_password: str = None) -> tuple: + env = {} + env.update(os.environ) + if session: + env["BW_SESSION"] = session + # Secrets must never be passed as argv — process arguments are world-readable + # via `ps` / `/proc/<pid>/cmdline` to any local user. Keep --passwordenv + # support for bw commands that need it; unlock/login callers should prefer + # stdin so the master password is not left in the child environment either. + if bw_password is not None: + env["BW_PASSWORD"] = bw_password + bw_path = _find_bw() + try: + proc = await asyncio.create_subprocess_exec( + bw_path, *args, + stdin=asyncio.subprocess.PIPE if input_text else None, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env=env, + ) + except FileNotFoundError: + return "", "bw CLI not installed (install `nodejs-bitwarden-cli` or `bitwarden-cli`)", 127 + except Exception as e: + return "", f"Failed to launch bw: {e}", 1 + try: + stdout, stderr = await proc.communicate(input=input_text.encode() if input_text else None) + except Exception as e: + return "", f"bw subprocess error: {e}", 1 + return stdout.decode(errors="replace").strip(), stderr.decode(errors="replace").strip(), proc.returncode + + +class VaultConfig(BaseModel): + server_url: str = "" + email: str = "" + + +class VaultUnlockRequest(BaseModel): + master_password: str + + +class VaultLoginRequest(BaseModel): + email: str + master_password: str + + +def setup_vault_routes(): + router = APIRouter(prefix="/api/vault", tags=["vault"]) + + @router.get("/config") + async def get_config(request: Request): + """Return vault config (no sensitive fields).""" + require_admin(request) + cfg = _load_config() + return { + "server_url": cfg.get("server_url", ""), + "email": cfg.get("email", ""), + "unlocked": bool(cfg.get("session")), + "unlocked_at": cfg.get("unlocked_at", ""), + "bw_installed": await _check_bw_installed(), + } + + @router.post("/config") + async def save_config(req: VaultConfig, request: Request): + """Save vault URL + email. Runs 'bw config server' to point at Vaultwarden.""" + require_admin(request) + cfg = _load_config() + cfg["server_url"] = req.server_url.strip().rstrip("/") + cfg["email"] = req.email.strip() + + if cfg["server_url"]: + _, stderr, rc = await _run_bw(["config", "server", cfg["server_url"]]) + if rc != 0: + return {"ok": False, "error": f"bw config failed: {stderr[:300]}"} + + _save_config(cfg) + return {"ok": True} + + @router.post("/login") + async def login(req: VaultLoginRequest, request: Request): + """Log in to Vaultwarden (required once per account).""" + require_admin(request) + cfg = _load_config() + # Update email + cfg["email"] = req.email + _save_config(cfg) + + stdout, stderr, rc = await _run_bw( + ["login", req.email, "--raw"], + input_text=req.master_password + "\n", + ) + if rc != 0: + # Already logged in is OK + if "already logged in" in stderr.lower(): + return {"ok": True, "already": True} + return {"ok": False, "error": f"Login failed: {stderr[:300]}"} + # bw login --raw prints session key on success (when 2FA disabled) + if stdout: + cfg["session"] = stdout + cfg["unlocked_at"] = datetime.utcnow().isoformat() + _save_config(cfg) + return {"ok": True} + + @router.post("/unlock") + async def unlock(req: VaultUnlockRequest, request: Request): + """Unlock the vault and save the session key.""" + require_admin(request) + # Pass the master password on stdin, not argv. argv is visible through + # `ps` / /proc/<pid>/cmdline; stdin also avoids leaving the secret in + # the child process environment. + stdout, stderr, rc = await _run_bw( + ["unlock", "--raw"], + input_text=req.master_password + "\n", + ) + if rc != 0: + return {"ok": False, "error": f"Unlock failed: {stderr[:300]}"} + session = stdout.strip() + if not session: + return {"ok": False, "error": "bw returned empty session"} + cfg = _load_config() + cfg["session"] = session + cfg["unlocked_at"] = datetime.utcnow().isoformat() + _save_config(cfg) + return {"ok": True, "message": "Vault unlocked"} + + @router.post("/lock") + async def lock(request: Request): + """Lock the vault (clear session from config).""" + require_admin(request) + cfg = _load_config() + cfg.pop("session", None) + cfg.pop("unlocked_at", None) + _save_config(cfg) + # Also tell bw to lock + await _run_bw(["lock"]) + return {"ok": True, "message": "Vault locked"} + + @router.post("/logout") + async def logout(request: Request): + """Log out of the Bitwarden CLI completely.""" + require_admin(request) + await _run_bw(["logout"]) + cfg = _load_config() + cfg.pop("session", None) + cfg.pop("email", None) + cfg.pop("unlocked_at", None) + _save_config(cfg) + return {"ok": True} + + return router + + +async def _check_bw_installed() -> bool: + try: + proc = await asyncio.create_subprocess_exec( + _find_bw(), "--version", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + await proc.communicate() + return proc.returncode == 0 + except Exception: + return False diff --git a/routes/vault_routes.py b/routes/vault_routes.py index 7e97500f0..cfed2ba39 100644 --- a/routes/vault_routes.py +++ b/routes/vault_routes.py @@ -1,242 +1,14 @@ -""" -vault_routes.py +"""Backward-compat shim — canonical location is routes/vault/vault_routes.py. -Vaultwarden / Bitwarden CLI integration — config and unlock endpoints. -Stores the BW_SESSION key in data/vault.json with restrictive permissions. +This module is replaced in ``sys.modules`` by the canonical module object so +that ``import routes.vault_routes``, ``from routes.vault_routes import X``, +and the ``import ... as vr`` + ``monkeypatch.setattr(vr, ...)`` pattern used +by test_vault_password_not_in_argv.py all operate on the *same* object. +Keeps existing import paths working after slice 2k (#4082/#4071). """ -import json -import logging -import os -import shutil -import asyncio -from pathlib import Path -from datetime import datetime -from fastapi import APIRouter, Request -from pydantic import BaseModel +import sys as _sys -from core.middleware import require_admin -from core.platform_compat import IS_WINDOWS, safe_chmod, which_tool -from src.constants import VAULT_FILE as _VAULT_FILE +from routes.vault import vault_routes as _canonical # noqa: F401 -logger = logging.getLogger(__name__) - -VAULT_FILE = Path(_VAULT_FILE) - - -def _find_bw() -> str: - """Locate the bw binary, checking PATH and common npm-global locations. - - On Windows the Bitwarden CLI shim is `bw.cmd`/`bw.exe`, resolved by - which_tool via PATHEXT. - """ - p = which_tool("bw") - if p: - return p - if IS_WINDOWS: - appdata = os.environ.get("APPDATA", os.path.expanduser("~")) - for candidate in ( - os.path.join(appdata, "npm", "bw.cmd"), - os.path.join(appdata, "npm", "bw.exe"), - ): - if os.path.isfile(candidate): - return candidate - return "bw" - home = os.path.expanduser("~") - for candidate in ( - f"{home}/.npm-global/bin/bw", - f"{home}/.nvm/versions/node/*/bin/bw", - "/usr/local/bin/bw", - "/opt/homebrew/bin/bw", - ): - if "*" in candidate: - import glob - for m in glob.glob(candidate): - if os.path.isfile(m) and os.access(m, os.X_OK): - return m - elif os.path.isfile(candidate) and os.access(candidate, os.X_OK): - return candidate - return "bw" # fall back to PATH lookup (will FileNotFoundError, handled below) - - -def _load_config() -> dict: - if VAULT_FILE.exists(): - try: - data = json.loads(VAULT_FILE.read_text(encoding="utf-8")) - return data if isinstance(data, dict) else {} - except Exception: - pass - return {} - - -def _save_config(cfg: dict): - VAULT_FILE.parent.mkdir(parents=True, exist_ok=True) - VAULT_FILE.write_text(json.dumps(cfg, indent=2), encoding="utf-8") - # POSIX: restrict the BW_SESSION store to 0o600. Windows: no-op (profile dir - # is ACL-restricted already). - safe_chmod(str(VAULT_FILE), 0o600) - - -async def _run_bw(args: list, session: str = None, input_text: str = None, - bw_password: str = None) -> tuple: - env = {} - env.update(os.environ) - if session: - env["BW_SESSION"] = session - # Secrets must never be passed as argv — process arguments are world-readable - # via `ps` / `/proc/<pid>/cmdline` to any local user. Keep --passwordenv - # support for bw commands that need it; unlock/login callers should prefer - # stdin so the master password is not left in the child environment either. - if bw_password is not None: - env["BW_PASSWORD"] = bw_password - bw_path = _find_bw() - try: - proc = await asyncio.create_subprocess_exec( - bw_path, *args, - stdin=asyncio.subprocess.PIPE if input_text else None, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - env=env, - ) - except FileNotFoundError: - return "", "bw CLI not installed (install `nodejs-bitwarden-cli` or `bitwarden-cli`)", 127 - except Exception as e: - return "", f"Failed to launch bw: {e}", 1 - try: - stdout, stderr = await proc.communicate(input=input_text.encode() if input_text else None) - except Exception as e: - return "", f"bw subprocess error: {e}", 1 - return stdout.decode(errors="replace").strip(), stderr.decode(errors="replace").strip(), proc.returncode - - -class VaultConfig(BaseModel): - server_url: str = "" - email: str = "" - - -class VaultUnlockRequest(BaseModel): - master_password: str - - -class VaultLoginRequest(BaseModel): - email: str - master_password: str - - -def setup_vault_routes(): - router = APIRouter(prefix="/api/vault", tags=["vault"]) - - @router.get("/config") - async def get_config(request: Request): - """Return vault config (no sensitive fields).""" - require_admin(request) - cfg = _load_config() - return { - "server_url": cfg.get("server_url", ""), - "email": cfg.get("email", ""), - "unlocked": bool(cfg.get("session")), - "unlocked_at": cfg.get("unlocked_at", ""), - "bw_installed": await _check_bw_installed(), - } - - @router.post("/config") - async def save_config(req: VaultConfig, request: Request): - """Save vault URL + email. Runs 'bw config server' to point at Vaultwarden.""" - require_admin(request) - cfg = _load_config() - cfg["server_url"] = req.server_url.strip().rstrip("/") - cfg["email"] = req.email.strip() - - if cfg["server_url"]: - _, stderr, rc = await _run_bw(["config", "server", cfg["server_url"]]) - if rc != 0: - return {"ok": False, "error": f"bw config failed: {stderr[:300]}"} - - _save_config(cfg) - return {"ok": True} - - @router.post("/login") - async def login(req: VaultLoginRequest, request: Request): - """Log in to Vaultwarden (required once per account).""" - require_admin(request) - cfg = _load_config() - # Update email - cfg["email"] = req.email - _save_config(cfg) - - stdout, stderr, rc = await _run_bw( - ["login", req.email, "--raw"], - input_text=req.master_password + "\n", - ) - if rc != 0: - # Already logged in is OK - if "already logged in" in stderr.lower(): - return {"ok": True, "already": True} - return {"ok": False, "error": f"Login failed: {stderr[:300]}"} - # bw login --raw prints session key on success (when 2FA disabled) - if stdout: - cfg["session"] = stdout - cfg["unlocked_at"] = datetime.utcnow().isoformat() - _save_config(cfg) - return {"ok": True} - - @router.post("/unlock") - async def unlock(req: VaultUnlockRequest, request: Request): - """Unlock the vault and save the session key.""" - require_admin(request) - # Pass the master password on stdin, not argv. argv is visible through - # `ps` / /proc/<pid>/cmdline; stdin also avoids leaving the secret in - # the child process environment. - stdout, stderr, rc = await _run_bw( - ["unlock", "--raw"], - input_text=req.master_password + "\n", - ) - if rc != 0: - return {"ok": False, "error": f"Unlock failed: {stderr[:300]}"} - session = stdout.strip() - if not session: - return {"ok": False, "error": "bw returned empty session"} - cfg = _load_config() - cfg["session"] = session - cfg["unlocked_at"] = datetime.utcnow().isoformat() - _save_config(cfg) - return {"ok": True, "message": "Vault unlocked"} - - @router.post("/lock") - async def lock(request: Request): - """Lock the vault (clear session from config).""" - require_admin(request) - cfg = _load_config() - cfg.pop("session", None) - cfg.pop("unlocked_at", None) - _save_config(cfg) - # Also tell bw to lock - await _run_bw(["lock"]) - return {"ok": True, "message": "Vault locked"} - - @router.post("/logout") - async def logout(request: Request): - """Log out of the Bitwarden CLI completely.""" - require_admin(request) - await _run_bw(["logout"]) - cfg = _load_config() - cfg.pop("session", None) - cfg.pop("email", None) - cfg.pop("unlocked_at", None) - _save_config(cfg) - return {"ok": True} - - return router - - -async def _check_bw_installed() -> bool: - try: - proc = await asyncio.create_subprocess_exec( - _find_bw(), "--version", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - await proc.communicate() - return proc.returncode == 0 - except Exception: - return False +_sys.modules[__name__] = _canonical diff --git a/tests/test_vault_routes_shim.py b/tests/test_vault_routes_shim.py new file mode 100644 index 000000000..9577395f7 --- /dev/null +++ b/tests/test_vault_routes_shim.py @@ -0,0 +1,11 @@ +"""Regression test for the vault route shim (slice 2k, #4082/#4071).""" + +import importlib + +import routes.vault_routes as _shim_vault # noqa: F401 + + +def test_legacy_and_canonical_vault_module_are_same_object(): + legacy = importlib.import_module("routes.vault_routes") + canonical = importlib.import_module("routes.vault.vault_routes") + assert legacy is canonical From fb8c391a8893be254a9ce4e2e954ea297c573554 Mon Sep 17 00:00:00 2001 From: "Tal.Yuan" <ydonghao@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:44:31 +0800 Subject: [PATCH 031/125] refactor(routes): move webhook domain into routes/webhook/ subpackage (#5781) Slice 2l of the route-domain reorganization (#4082/#4071). Moves webhook_routes.py into routes/webhook/, leaving a backward-compat sys.modules shim. Pure file reorganization, no behavior change. One source-introspection test repointed (test_api_chat_security.py). --- app.py | 2 +- routes/webhook/__init__.py | 5 + routes/webhook/webhook_routes.py | 395 +++++++++++++++++++++++++++++ routes/webhook_routes.py | 403 +----------------------------- tests/test_api_chat_security.py | 2 +- tests/test_webhook_routes_shim.py | 11 + 6 files changed, 425 insertions(+), 393 deletions(-) create mode 100644 routes/webhook/__init__.py create mode 100644 routes/webhook/webhook_routes.py create mode 100644 tests/test_webhook_routes_shim.py diff --git a/app.py b/app.py index 5fb2da54d..c85d425fb 100644 --- a/app.py +++ b/app.py @@ -820,7 +820,7 @@ set_ai_rag_manager(rag_manager, personal_docs_mgr) logger.info("AI interaction tools initialized (session, memory, RAG, UI control)") # Webhooks -from routes.webhook_routes import setup_webhook_routes +from routes.webhook.webhook_routes import setup_webhook_routes app.include_router(setup_webhook_routes(webhook_manager, auth_manager, session_manager, api_key_manager)) # API Tokens diff --git a/routes/webhook/__init__.py b/routes/webhook/__init__.py new file mode 100644 index 000000000..e51389e3a --- /dev/null +++ b/routes/webhook/__init__.py @@ -0,0 +1,5 @@ +"""Webhook route domain package (slice 2l, #4082/#4071). + +Contains webhook_routes.py, migrated from the flat routes/ directory. +Backward-compat shim at routes/webhook_routes.py re-exports from here. +""" diff --git a/routes/webhook/webhook_routes.py b/routes/webhook/webhook_routes.py new file mode 100644 index 000000000..8d3a704c6 --- /dev/null +++ b/routes/webhook/webhook_routes.py @@ -0,0 +1,395 @@ +"""Webhook, API Token, and sync chat routes.""" + +import uuid +import logging +from typing import Optional + +import httpx +from fastapi import APIRouter, HTTPException, Request, Form +from pydantic import BaseModel, Field + +from core.database import SessionLocal, Webhook, ModelEndpoint +from src.auth_helpers import owner_filter +from src.url_security import validate_public_http_url +from src.webhook_manager import WebhookManager, validate_webhook_url, validate_events + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api", tags=["webhooks"]) + +# Input limits +MAX_NAME_LEN = 100 +MAX_URL_LEN = 2048 +MAX_SECRET_LEN = 256 +MAX_MESSAGE_LEN = 32_000 + + +from core.middleware import require_admin as _require_admin + + +def _select_api_chat_fallback_endpoint(db, token_owner: Optional[str]): + """First enabled ModelEndpoint visible to token_owner — their own rows plus + legacy null-owner ("shared") rows. Owner-scoped: an unscoped .first() would + let a chat-scoped token fall back onto another user's private endpoint and + silently spend that owner's API key/quota. Prefer owner rows before shared + rows. Fails closed to null-owner rows only when token_owner is absent. + Does not validate base_url — admin-configured local/LAN endpoints remain allowed. + """ + query = db.query(ModelEndpoint).filter(ModelEndpoint.is_enabled == True) # noqa: E712 + if token_owner: + query = owner_filter(query, ModelEndpoint, token_owner) + return query.order_by(ModelEndpoint.owner.desc(), ModelEndpoint.created_at).first() + return query.filter(ModelEndpoint.owner == None).order_by(ModelEndpoint.created_at).first() # noqa: E711 + + +def _caller_owns_session(sess_owner, caller) -> bool: + """Strict session-ownership gate for the token-authenticated sync-chat + endpoint (`POST /api/v1/chat`). + + Mirrors ``_verify_session_owner`` in session_routes.py and the null-owner + gates in notes/calendar/gallery: a caller may resume a session ONLY when + its owner matches them exactly. A null/empty session owner (legacy or + migrated rows) is deliberately NOT resumable by an arbitrary token — the + old ``sess_owner and sess_owner != caller`` form skipped the check whenever + ``sess_owner`` was falsy, so any chat-scoped token (e.g. a paired mobile + device) could resume such a session, inject a message, and read back its + history and reuse the owner's endpoint credentials. Fail closed: an + unresolvable caller also returns False. + """ + if not caller: + return False + return sess_owner == caller + + +def setup_webhook_routes( + webhook_manager: WebhookManager, + auth_manager, + session_manager=None, + api_key_manager=None, +) -> APIRouter: + + @router.get("/webhooks") + def list_webhooks(request: Request): + _require_admin(request) + db = SessionLocal() + try: + hooks = db.query(Webhook).all() + return [ + { + "id": w.id, + "name": w.name, + "url": w.url, + "has_secret": bool(w.secret), + "events": w.events.split(",") if w.events else [], + "is_active": w.is_active, + "last_triggered_at": w.last_triggered_at.isoformat() if w.last_triggered_at else None, + "last_status_code": w.last_status_code, + "last_error": w.last_error, + "created_at": w.created_at.isoformat() if w.created_at else None, + } + for w in hooks + ] + finally: + db.close() + + @router.post("/webhooks") + def create_webhook( + request: Request, + name: str = Form(""), + url: str = Form(""), + secret: str = Form(""), + events: str = Form(""), + ): + _require_admin(request) + name = name.strip()[:MAX_NAME_LEN] + if not name: + raise HTTPException(400, "Webhook name is required") + try: + url = validate_webhook_url(url) + except ValueError as e: + raise HTTPException(400, str(e)) + try: + events = validate_events(events) + except ValueError as e: + raise HTTPException(400, str(e)) + + secret_val = secret.strip()[:MAX_SECRET_LEN] or None + # Encrypt the secret at rest using the same Fernet key as API keys + encrypted_secret = None + if secret_val and api_key_manager: + encrypted_secret = api_key_manager.encrypt_api_key(secret_val) + elif secret_val: + encrypted_secret = secret_val # Fallback if no encryption available + + webhook_id = str(uuid.uuid4())[:8] + db = SessionLocal() + try: + db.add(Webhook( + id=webhook_id, + name=name, + url=url, + secret=encrypted_secret, + events=events, + is_active=True, + )) + db.commit() + finally: + db.close() + + return {"id": webhook_id, "name": name} + + @router.post("/webhooks/{webhook_id}/test") + async def test_webhook(request: Request, webhook_id: str): + _require_admin(request) + db = SessionLocal() + try: + wh = db.query(Webhook).filter(Webhook.id == webhook_id).first() + if not wh: + raise HTTPException(404, "Webhook not found") + url, secret = wh.url, wh.secret + finally: + db.close() + + await webhook_manager.deliver_test(webhook_id, url, secret) + return {"status": "sent"} + + @router.patch("/webhooks/{webhook_id}") + def toggle_webhook(request: Request, webhook_id: str): + _require_admin(request) + db = SessionLocal() + try: + wh = db.query(Webhook).filter(Webhook.id == webhook_id).first() + if not wh: + raise HTTPException(404, "Webhook not found") + wh.is_active = not wh.is_active + db.commit() + return {"id": webhook_id, "is_active": wh.is_active} + finally: + db.close() + + @router.delete("/webhooks/{webhook_id}") + def delete_webhook(request: Request, webhook_id: str): + _require_admin(request) + db = SessionLocal() + try: + deleted = db.query(Webhook).filter(Webhook.id == webhook_id).delete() + db.commit() + if not deleted: + raise HTTPException(404, "Webhook not found") + finally: + db.close() + return {"status": "deleted"} + + # ================================================================ + # Sync Chat Endpoint (for n8n / Make / Activepieces) + # ================================================================ + + # Known provider base URLs — auto-resolved from api_key prefix or model name + KNOWN_PROVIDERS = { + "deepseek": "https://api.deepseek.com/v1", + "openai": "https://api.openai.com/v1", + "mistral": "https://api.mistral.ai/v1", + "groq": "https://api.groq.com/openai/v1", + "together": "https://api.together.xyz/v1", + "openrouter": "https://openrouter.ai/api/v1", + "ollama": "https://ollama.com/api", + "opencode-zen": "https://opencode.ai/zen/v1", + "opencode-go": "https://opencode.ai/zen/go/v1", + "fireworks": "https://api.fireworks.ai/inference/v1", + "venice": "https://api.venice.ai/api/v1", + "kimi-code": "https://api.kimi.com/coding/v1", + "kimicode": "https://api.kimi.com/coding/v1", + } + + # Model prefix → provider mapping for auto-detection + MODEL_PROVIDER_MAP = { + "deepseek": "deepseek", + "gpt-": "openai", + "o1": "openai", + "o3": "openai", + "o4": "openai", + "mistral": "mistral", + "llama": "groq", + "mixtral": "groq", + "kimi-for-coding": "kimi-code", + "kimi": "kimi-code", + } + + def _resolve_base_url(model: Optional[str], provider: Optional[str]) -> Optional[str]: + """Try to auto-resolve a base URL from provider name or model prefix.""" + if provider and provider.lower() in KNOWN_PROVIDERS: + return KNOWN_PROVIDERS[provider.lower()] + if model: + model_lower = model.lower() + for prefix, prov in MODEL_PROVIDER_MAP.items(): + if model_lower.startswith(prefix): + return KNOWN_PROVIDERS[prov] + return None + + class SyncChatRequest(BaseModel): + message: str = Field(..., max_length=MAX_MESSAGE_LEN) + model: Optional[str] = Field(None, max_length=200) + session: Optional[str] = Field(None, max_length=100) + api_key: Optional[str] = Field(None, max_length=256) + base_url: Optional[str] = Field(None, max_length=MAX_URL_LEN) + provider: Optional[str] = Field(None, max_length=50) + + @router.post("/v1/chat") + async def sync_chat(request: Request, body: SyncChatRequest): + if not getattr(request.state, "api_token", False): + raise HTTPException(403, "This endpoint requires an API token") + scopes = set(getattr(request.state, "api_token_scopes", []) or []) + if "chat" not in scopes: + raise HTTPException(403, "API token is not scoped for chat") + token_owner = getattr(request.state, "api_token_owner", None) + + from core.models import ChatMessage + from src.llm_core import llm_call_async + from src.endpoint_resolver import build_chat_url, build_headers, build_models_url, normalize_base + + message = body.message.strip() + if not message: + raise HTTPException(400, "Message is required") + + session_id = body.session + sess = None + + # --- Case 1: Resume an existing session --- + if session_id and session_manager: + try: + sess = session_manager.get_session(session_id) + except (KeyError, Exception): + raise HTTPException(404, "Session not found") + # SECURITY: verify the API-token's user owns this session — without + # this any token holder could resume any user's chat by passing its + # ID. The token's user is on request.state.user (set by API-token + # middleware); fall back to require_user if not present. + try: + from src.auth_helpers import get_current_user as _gcu + _tok_user = token_owner or getattr(request.state, "user", None) or _gcu(request) + except Exception: + _tok_user = None + # Strict ownership (see _caller_owns_session): fail closed so a + # null-owner / cross-owner session can't be resumed by an arbitrary + # chat-scoped token. + _sess_owner = getattr(sess, "owner", None) + if not _caller_owns_session(_sess_owner, _tok_user): + raise HTTPException(404, "Session not found") + + # --- Case 2: Direct API key + model (no pre-configured endpoint needed) --- + if not sess and body.api_key: + api_key = body.api_key.strip() + model = body.model or "deepseek-chat" + + # Validate only token-supplied direct base_url; auto-resolved known-provider + # URLs are not subject to extra local/LAN blocking beyond existing provider logic. + direct_base_url = body.base_url.strip().rstrip("/") if body.base_url else None + if direct_base_url: + try: + base_url = validate_public_http_url(direct_base_url) + except ValueError as e: + detail = str(e).replace("URL", "base_url", 1) + raise HTTPException(400, detail) + else: + base_url = _resolve_base_url(model, body.provider) + if not base_url: + raise HTTPException(400, + "Could not auto-detect provider. Pass base_url (e.g. 'https://api.deepseek.com/v1') " + "or provider ('deepseek', 'openai', 'groq', etc.)") + base_url = normalize_base(base_url) + endpoint_url = build_chat_url(base_url) + + if not session_manager: + raise HTTPException(500, "Session manager not available") + + sid = str(uuid.uuid4()) + sess = session_manager.create_session( + session_id=sid, name="API Chat", endpoint_url=endpoint_url, + model=model, owner=token_owner, + ) + sess.headers = build_headers(api_key, base_url) + session_manager.save_sessions() + session_id = sid + + # --- Case 3: Fall back to first configured ModelEndpoint --- + if not sess: + db = SessionLocal() + try: + ep = _select_api_chat_fallback_endpoint(db, token_owner) + finally: + db.close() + + if not ep: + raise HTTPException(400, + "No session, api_key, or configured endpoints. " + "Pass api_key + model, or configure an endpoint in Admin.") + + base_url = normalize_base(ep.base_url) + endpoint_url = build_chat_url(base_url) + model = body.model or "auto" + api_key = ep.api_key + if getattr(ep, "provider_auth_id", None): + try: + from src.endpoint_resolver import resolve_endpoint_runtime + base_url, api_key = resolve_endpoint_runtime(ep, owner=token_owner) + endpoint_url = build_chat_url(base_url) + except Exception: + raise HTTPException(500, "Could not resolve endpoint credentials") + + if model == "auto": + try: + async with httpx.AsyncClient(timeout=5) as client: + models_url = build_models_url(base_url) + hdrs = build_headers(api_key, base_url) + if models_url: + resp = await client.get(models_url, headers=hdrs) + resp.raise_for_status() + data = resp.json() + items = data if isinstance(data, list) else (data.get("data") or []) + ids = [m.get("id") for m in items if isinstance(m, dict) and m.get("id")] + if not ids and isinstance(data, dict): + ids = [ + m.get("name") or m.get("model") + for m in (data.get("models") or []) + if m.get("name") or m.get("model") + ] + else: + import json as _json + ids = _json.loads(ep.cached_models or "[]") + model = ids[0] if ids else "auto" + except Exception: + raise HTTPException(500, "Could not discover models from endpoint") + + if not session_manager: + raise HTTPException(500, "Session manager not available") + + sid = str(uuid.uuid4()) + sess = session_manager.create_session( + session_id=sid, name="API Chat", endpoint_url=endpoint_url, + model=model, owner=token_owner, + ) + if api_key: + sess.headers = build_headers(api_key, base_url) + session_manager.save_sessions() + session_id = sid + + # --- Send message and get response --- + sess.add_message(ChatMessage("user", message)) + + messages = [{"role": m.role, "content": m.content} for m in sess.history] + + reply = await llm_call_async( + sess.endpoint_url, sess.model, messages, + headers=sess.headers, timeout=120, + ) + sess.add_message(ChatMessage("assistant", reply)) + session_manager.save_sessions() + + webhook_manager.fire_and_forget("chat.completed", { + "session_id": session_id, "model": sess.model, + "user_message": message[:2000], "response": reply[:2000], + }) + + return {"response": reply, "session_id": session_id, "model": sess.model} + + return router diff --git a/routes/webhook_routes.py b/routes/webhook_routes.py index 8d3a704c6..7c5e0453e 100644 --- a/routes/webhook_routes.py +++ b/routes/webhook_routes.py @@ -1,395 +1,16 @@ -"""Webhook, API Token, and sync chat routes.""" +"""Backward-compat shim — canonical location is routes/webhook/webhook_routes.py. -import uuid -import logging -from typing import Optional +This module is replaced in ``sys.modules`` by the canonical module object so +that ``import routes.webhook_routes``, ``from routes.webhook_routes import X``, +``importlib.import_module("routes.webhook_routes")``, and the +``__import__("routes.webhook_routes", fromlist=[...])`` + ``setattr(wh_mod, +...)`` pattern used by test_null_owner_gates.py all operate on the *same* +object. Keeps existing import paths working after slice 2l (#4082/#4071). +Source-introspection tests read the canonical file by path. +""" -import httpx -from fastapi import APIRouter, HTTPException, Request, Form -from pydantic import BaseModel, Field +import sys as _sys -from core.database import SessionLocal, Webhook, ModelEndpoint -from src.auth_helpers import owner_filter -from src.url_security import validate_public_http_url -from src.webhook_manager import WebhookManager, validate_webhook_url, validate_events +from routes.webhook import webhook_routes as _canonical # noqa: F401 -logger = logging.getLogger(__name__) - -router = APIRouter(prefix="/api", tags=["webhooks"]) - -# Input limits -MAX_NAME_LEN = 100 -MAX_URL_LEN = 2048 -MAX_SECRET_LEN = 256 -MAX_MESSAGE_LEN = 32_000 - - -from core.middleware import require_admin as _require_admin - - -def _select_api_chat_fallback_endpoint(db, token_owner: Optional[str]): - """First enabled ModelEndpoint visible to token_owner — their own rows plus - legacy null-owner ("shared") rows. Owner-scoped: an unscoped .first() would - let a chat-scoped token fall back onto another user's private endpoint and - silently spend that owner's API key/quota. Prefer owner rows before shared - rows. Fails closed to null-owner rows only when token_owner is absent. - Does not validate base_url — admin-configured local/LAN endpoints remain allowed. - """ - query = db.query(ModelEndpoint).filter(ModelEndpoint.is_enabled == True) # noqa: E712 - if token_owner: - query = owner_filter(query, ModelEndpoint, token_owner) - return query.order_by(ModelEndpoint.owner.desc(), ModelEndpoint.created_at).first() - return query.filter(ModelEndpoint.owner == None).order_by(ModelEndpoint.created_at).first() # noqa: E711 - - -def _caller_owns_session(sess_owner, caller) -> bool: - """Strict session-ownership gate for the token-authenticated sync-chat - endpoint (`POST /api/v1/chat`). - - Mirrors ``_verify_session_owner`` in session_routes.py and the null-owner - gates in notes/calendar/gallery: a caller may resume a session ONLY when - its owner matches them exactly. A null/empty session owner (legacy or - migrated rows) is deliberately NOT resumable by an arbitrary token — the - old ``sess_owner and sess_owner != caller`` form skipped the check whenever - ``sess_owner`` was falsy, so any chat-scoped token (e.g. a paired mobile - device) could resume such a session, inject a message, and read back its - history and reuse the owner's endpoint credentials. Fail closed: an - unresolvable caller also returns False. - """ - if not caller: - return False - return sess_owner == caller - - -def setup_webhook_routes( - webhook_manager: WebhookManager, - auth_manager, - session_manager=None, - api_key_manager=None, -) -> APIRouter: - - @router.get("/webhooks") - def list_webhooks(request: Request): - _require_admin(request) - db = SessionLocal() - try: - hooks = db.query(Webhook).all() - return [ - { - "id": w.id, - "name": w.name, - "url": w.url, - "has_secret": bool(w.secret), - "events": w.events.split(",") if w.events else [], - "is_active": w.is_active, - "last_triggered_at": w.last_triggered_at.isoformat() if w.last_triggered_at else None, - "last_status_code": w.last_status_code, - "last_error": w.last_error, - "created_at": w.created_at.isoformat() if w.created_at else None, - } - for w in hooks - ] - finally: - db.close() - - @router.post("/webhooks") - def create_webhook( - request: Request, - name: str = Form(""), - url: str = Form(""), - secret: str = Form(""), - events: str = Form(""), - ): - _require_admin(request) - name = name.strip()[:MAX_NAME_LEN] - if not name: - raise HTTPException(400, "Webhook name is required") - try: - url = validate_webhook_url(url) - except ValueError as e: - raise HTTPException(400, str(e)) - try: - events = validate_events(events) - except ValueError as e: - raise HTTPException(400, str(e)) - - secret_val = secret.strip()[:MAX_SECRET_LEN] or None - # Encrypt the secret at rest using the same Fernet key as API keys - encrypted_secret = None - if secret_val and api_key_manager: - encrypted_secret = api_key_manager.encrypt_api_key(secret_val) - elif secret_val: - encrypted_secret = secret_val # Fallback if no encryption available - - webhook_id = str(uuid.uuid4())[:8] - db = SessionLocal() - try: - db.add(Webhook( - id=webhook_id, - name=name, - url=url, - secret=encrypted_secret, - events=events, - is_active=True, - )) - db.commit() - finally: - db.close() - - return {"id": webhook_id, "name": name} - - @router.post("/webhooks/{webhook_id}/test") - async def test_webhook(request: Request, webhook_id: str): - _require_admin(request) - db = SessionLocal() - try: - wh = db.query(Webhook).filter(Webhook.id == webhook_id).first() - if not wh: - raise HTTPException(404, "Webhook not found") - url, secret = wh.url, wh.secret - finally: - db.close() - - await webhook_manager.deliver_test(webhook_id, url, secret) - return {"status": "sent"} - - @router.patch("/webhooks/{webhook_id}") - def toggle_webhook(request: Request, webhook_id: str): - _require_admin(request) - db = SessionLocal() - try: - wh = db.query(Webhook).filter(Webhook.id == webhook_id).first() - if not wh: - raise HTTPException(404, "Webhook not found") - wh.is_active = not wh.is_active - db.commit() - return {"id": webhook_id, "is_active": wh.is_active} - finally: - db.close() - - @router.delete("/webhooks/{webhook_id}") - def delete_webhook(request: Request, webhook_id: str): - _require_admin(request) - db = SessionLocal() - try: - deleted = db.query(Webhook).filter(Webhook.id == webhook_id).delete() - db.commit() - if not deleted: - raise HTTPException(404, "Webhook not found") - finally: - db.close() - return {"status": "deleted"} - - # ================================================================ - # Sync Chat Endpoint (for n8n / Make / Activepieces) - # ================================================================ - - # Known provider base URLs — auto-resolved from api_key prefix or model name - KNOWN_PROVIDERS = { - "deepseek": "https://api.deepseek.com/v1", - "openai": "https://api.openai.com/v1", - "mistral": "https://api.mistral.ai/v1", - "groq": "https://api.groq.com/openai/v1", - "together": "https://api.together.xyz/v1", - "openrouter": "https://openrouter.ai/api/v1", - "ollama": "https://ollama.com/api", - "opencode-zen": "https://opencode.ai/zen/v1", - "opencode-go": "https://opencode.ai/zen/go/v1", - "fireworks": "https://api.fireworks.ai/inference/v1", - "venice": "https://api.venice.ai/api/v1", - "kimi-code": "https://api.kimi.com/coding/v1", - "kimicode": "https://api.kimi.com/coding/v1", - } - - # Model prefix → provider mapping for auto-detection - MODEL_PROVIDER_MAP = { - "deepseek": "deepseek", - "gpt-": "openai", - "o1": "openai", - "o3": "openai", - "o4": "openai", - "mistral": "mistral", - "llama": "groq", - "mixtral": "groq", - "kimi-for-coding": "kimi-code", - "kimi": "kimi-code", - } - - def _resolve_base_url(model: Optional[str], provider: Optional[str]) -> Optional[str]: - """Try to auto-resolve a base URL from provider name or model prefix.""" - if provider and provider.lower() in KNOWN_PROVIDERS: - return KNOWN_PROVIDERS[provider.lower()] - if model: - model_lower = model.lower() - for prefix, prov in MODEL_PROVIDER_MAP.items(): - if model_lower.startswith(prefix): - return KNOWN_PROVIDERS[prov] - return None - - class SyncChatRequest(BaseModel): - message: str = Field(..., max_length=MAX_MESSAGE_LEN) - model: Optional[str] = Field(None, max_length=200) - session: Optional[str] = Field(None, max_length=100) - api_key: Optional[str] = Field(None, max_length=256) - base_url: Optional[str] = Field(None, max_length=MAX_URL_LEN) - provider: Optional[str] = Field(None, max_length=50) - - @router.post("/v1/chat") - async def sync_chat(request: Request, body: SyncChatRequest): - if not getattr(request.state, "api_token", False): - raise HTTPException(403, "This endpoint requires an API token") - scopes = set(getattr(request.state, "api_token_scopes", []) or []) - if "chat" not in scopes: - raise HTTPException(403, "API token is not scoped for chat") - token_owner = getattr(request.state, "api_token_owner", None) - - from core.models import ChatMessage - from src.llm_core import llm_call_async - from src.endpoint_resolver import build_chat_url, build_headers, build_models_url, normalize_base - - message = body.message.strip() - if not message: - raise HTTPException(400, "Message is required") - - session_id = body.session - sess = None - - # --- Case 1: Resume an existing session --- - if session_id and session_manager: - try: - sess = session_manager.get_session(session_id) - except (KeyError, Exception): - raise HTTPException(404, "Session not found") - # SECURITY: verify the API-token's user owns this session — without - # this any token holder could resume any user's chat by passing its - # ID. The token's user is on request.state.user (set by API-token - # middleware); fall back to require_user if not present. - try: - from src.auth_helpers import get_current_user as _gcu - _tok_user = token_owner or getattr(request.state, "user", None) or _gcu(request) - except Exception: - _tok_user = None - # Strict ownership (see _caller_owns_session): fail closed so a - # null-owner / cross-owner session can't be resumed by an arbitrary - # chat-scoped token. - _sess_owner = getattr(sess, "owner", None) - if not _caller_owns_session(_sess_owner, _tok_user): - raise HTTPException(404, "Session not found") - - # --- Case 2: Direct API key + model (no pre-configured endpoint needed) --- - if not sess and body.api_key: - api_key = body.api_key.strip() - model = body.model or "deepseek-chat" - - # Validate only token-supplied direct base_url; auto-resolved known-provider - # URLs are not subject to extra local/LAN blocking beyond existing provider logic. - direct_base_url = body.base_url.strip().rstrip("/") if body.base_url else None - if direct_base_url: - try: - base_url = validate_public_http_url(direct_base_url) - except ValueError as e: - detail = str(e).replace("URL", "base_url", 1) - raise HTTPException(400, detail) - else: - base_url = _resolve_base_url(model, body.provider) - if not base_url: - raise HTTPException(400, - "Could not auto-detect provider. Pass base_url (e.g. 'https://api.deepseek.com/v1') " - "or provider ('deepseek', 'openai', 'groq', etc.)") - base_url = normalize_base(base_url) - endpoint_url = build_chat_url(base_url) - - if not session_manager: - raise HTTPException(500, "Session manager not available") - - sid = str(uuid.uuid4()) - sess = session_manager.create_session( - session_id=sid, name="API Chat", endpoint_url=endpoint_url, - model=model, owner=token_owner, - ) - sess.headers = build_headers(api_key, base_url) - session_manager.save_sessions() - session_id = sid - - # --- Case 3: Fall back to first configured ModelEndpoint --- - if not sess: - db = SessionLocal() - try: - ep = _select_api_chat_fallback_endpoint(db, token_owner) - finally: - db.close() - - if not ep: - raise HTTPException(400, - "No session, api_key, or configured endpoints. " - "Pass api_key + model, or configure an endpoint in Admin.") - - base_url = normalize_base(ep.base_url) - endpoint_url = build_chat_url(base_url) - model = body.model or "auto" - api_key = ep.api_key - if getattr(ep, "provider_auth_id", None): - try: - from src.endpoint_resolver import resolve_endpoint_runtime - base_url, api_key = resolve_endpoint_runtime(ep, owner=token_owner) - endpoint_url = build_chat_url(base_url) - except Exception: - raise HTTPException(500, "Could not resolve endpoint credentials") - - if model == "auto": - try: - async with httpx.AsyncClient(timeout=5) as client: - models_url = build_models_url(base_url) - hdrs = build_headers(api_key, base_url) - if models_url: - resp = await client.get(models_url, headers=hdrs) - resp.raise_for_status() - data = resp.json() - items = data if isinstance(data, list) else (data.get("data") or []) - ids = [m.get("id") for m in items if isinstance(m, dict) and m.get("id")] - if not ids and isinstance(data, dict): - ids = [ - m.get("name") or m.get("model") - for m in (data.get("models") or []) - if m.get("name") or m.get("model") - ] - else: - import json as _json - ids = _json.loads(ep.cached_models or "[]") - model = ids[0] if ids else "auto" - except Exception: - raise HTTPException(500, "Could not discover models from endpoint") - - if not session_manager: - raise HTTPException(500, "Session manager not available") - - sid = str(uuid.uuid4()) - sess = session_manager.create_session( - session_id=sid, name="API Chat", endpoint_url=endpoint_url, - model=model, owner=token_owner, - ) - if api_key: - sess.headers = build_headers(api_key, base_url) - session_manager.save_sessions() - session_id = sid - - # --- Send message and get response --- - sess.add_message(ChatMessage("user", message)) - - messages = [{"role": m.role, "content": m.content} for m in sess.history] - - reply = await llm_call_async( - sess.endpoint_url, sess.model, messages, - headers=sess.headers, timeout=120, - ) - sess.add_message(ChatMessage("assistant", reply)) - session_manager.save_sessions() - - webhook_manager.fire_and_forget("chat.completed", { - "session_id": session_id, "model": sess.model, - "user_message": message[:2000], "response": reply[:2000], - }) - - return {"response": reply, "session_id": session_id, "model": sess.model} - - return router +_sys.modules[__name__] = _canonical diff --git a/tests/test_api_chat_security.py b/tests/test_api_chat_security.py index 7dcec324e..d92a31620 100644 --- a/tests/test_api_chat_security.py +++ b/tests/test_api_chat_security.py @@ -76,7 +76,7 @@ def _load_webhook_routes_for_test(monkeypatch): module_name = "routes.webhook_routes_under_test" spec = importlib.util.spec_from_file_location( module_name, - Path(__file__).resolve().parent.parent / "routes" / "webhook_routes.py", + Path(__file__).resolve().parent.parent / "routes" / "webhook" / "webhook_routes.py", ) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) diff --git a/tests/test_webhook_routes_shim.py b/tests/test_webhook_routes_shim.py new file mode 100644 index 000000000..f6312e8e6 --- /dev/null +++ b/tests/test_webhook_routes_shim.py @@ -0,0 +1,11 @@ +"""Regression test for the webhook route shim (slice 2l, #4082/#4071).""" + +import importlib + +import routes.webhook_routes as _shim_webhook # noqa: F401 + + +def test_legacy_and_canonical_webhook_module_are_same_object(): + legacy = importlib.import_module("routes.webhook_routes") + canonical = importlib.import_module("routes.webhook.webhook_routes") + assert legacy is canonical From bb719f217a77b19d89d26f96168cf463ab73b6ba Mon Sep 17 00:00:00 2001 From: "Tal.Yuan" <ydonghao@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:54:55 +0800 Subject: [PATCH 032/125] refactor(routes): move document domain into routes/document/ subpackage (#5885) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 2m of the route-domain reorganization (#4082/#4071, per specs/architecture-runtime-inventory.md §6.3). Moves document_routes.py (1810 lines) and document_helpers.py (243 lines) into routes/document/, leaving backward-compat sys.modules shims at the old paths. Pure file reorganization, no behavior change. Both shims use sys.modules replacement so the `import ... as droutes` + `droutes.SessionLocal = ...` / `monkeypatch.setattr(droutes, ...)` pattern in multiple tests, and the `sys.modules.pop("routes.document_helpers")` + re-import pattern in test_security_regressions.py, all reach the canonical modules. The canonical document_routes.py imports helpers from the canonical path (routes.document.document_helpers), not the legacy shim. Three source-introspection test sites repointed to the new canonical path: - test_imap_mailbox_quoting.py - test_model_helper_owner_scope.py - test_vision_owner_scope.py (shared with other domains; document entry repointed) Adds tests/test_document_routes_shim.py to pin the sys.modules shim contract for both modules. Verified: compileall clean; full suite 4789 passed, 3 skipped. --- app.py | 2 +- routes/document/__init__.py | 6 + routes/document/document_helpers.py | 243 ++++ routes/document/document_routes.py | 1810 +++++++++++++++++++++++ routes/document_helpers.py | 249 +--- routes/document_routes.py | 1819 +----------------------- tests/test_document_routes_shim.py | 29 + tests/test_imap_mailbox_quoting.py | 2 +- tests/test_model_helper_owner_scope.py | 2 +- tests/test_vision_owner_scope.py | 2 +- 10 files changed, 2115 insertions(+), 2049 deletions(-) create mode 100644 routes/document/__init__.py create mode 100644 routes/document/document_helpers.py create mode 100644 routes/document/document_routes.py create mode 100644 tests/test_document_routes_shim.py diff --git a/app.py b/app.py index c85d425fb..8363ba4e9 100644 --- a/app.py +++ b/app.py @@ -739,7 +739,7 @@ app.include_router(setup_stt_routes(stt_service)) logger.info("STT service initialized (provider managed via settings)") # Documents (artifacts/canvas) -from routes.document_routes import setup_document_routes +from routes.document.document_routes import setup_document_routes document_router = setup_document_routes(session_manager, upload_handler) app.include_router(document_router) diff --git a/routes/document/__init__.py b/routes/document/__init__.py new file mode 100644 index 000000000..7f79ce1bb --- /dev/null +++ b/routes/document/__init__.py @@ -0,0 +1,6 @@ +"""Document route domain package (slice 2m, #4082/#4071). + +Contains document_routes.py and document_helpers.py, migrated from the flat +routes/ directory. Backward-compat shims at routes/document_routes.py and +routes/document_helpers.py re-export from here. +""" diff --git a/routes/document/document_helpers.py b/routes/document/document_helpers.py new file mode 100644 index 000000000..a0c2d08eb --- /dev/null +++ b/routes/document/document_helpers.py @@ -0,0 +1,243 @@ +"""document_helpers.py — Pydantic models, doc serializers, owner gating, file-locator helpers shared with document_routes.py.""" + +"""Document routes — CRUD for living documents with version history.""" + +import logging +import os +import re +from typing import Any, Dict, Optional + +from fastapi import HTTPException, Request +from pydantic import BaseModel + +from core.database import Document, DocumentVersion +from core.database import Session as DbSession +from src.auth_helpers import _auth_disabled +from src.upload_handler import UploadHandler + +logger = logging.getLogger(__name__) + + +# ---- Request schemas ---- + +class DocumentCreate(BaseModel): + session_id: Optional[str] = None + title: str = "Untitled" + language: Optional[str] = None + content: str = "" + +class DocumentUpdate(BaseModel): + content: str + summary: Optional[str] = None + force_version: bool = False + +class DocumentPatch(BaseModel): + title: Optional[str] = None + language: Optional[str] = None + session_id: Optional[str] = None # link/unlink document to a session + + +# ---- Helpers ---- + +def _doc_to_dict(doc: Document) -> Dict[str, Any]: + return { + "id": doc.id, + "session_id": doc.session_id, + "title": doc.title, + "language": doc.language, + "current_content": doc.current_content, + "version_count": doc.version_count, + "is_active": doc.is_active, + "archived": bool(getattr(doc, "archived", False)), + "created_at": (doc.created_at.isoformat() + "Z") if doc.created_at else None, + "updated_at": (doc.updated_at.isoformat() + "Z") if doc.updated_at else None, + # Source-email provenance (set when doc was created from an email + # attachment) — drives the "Send signed reply" menu item. + "source_email_uid": getattr(doc, "source_email_uid", None), + "source_email_folder": getattr(doc, "source_email_folder", None), + "source_email_account_id": getattr(doc, "source_email_account_id", None), + "source_email_message_id": getattr(doc, "source_email_message_id", None), + } + +def _version_to_dict(v: DocumentVersion) -> Dict[str, Any]: + return { + "id": v.id, + "document_id": v.document_id, + "version_number": v.version_number, + "content": v.content, + "summary": v.summary, + "source": v.source, + "created_at": v.created_at.isoformat() if v.created_at else None, + } + + +def _verify_doc_owner(db, doc: Document, user: str): + """Verify `user` owns this document. Raise 404 if not. + + Documents now carry their own `owner` column, so a doc whose session + was deleted (session_id → NULL) can still prove ownership and stay + openable / cloneable. We trust that column first and only fall back to + the session join for any not-yet-backfilled legacy row. + """ + if user is None: + if _auth_disabled(): + return # Single-user / no-auth mode: allow access + raise HTTPException(403, "Authentication required") + if doc.owner is not None: + if doc.owner != user: + raise HTTPException(404, "Document not found") + return + # Legacy fallback: derive ownership from the linked session. + if not doc.session_id: + raise HTTPException(404, "Document not found") + session = db.query(DbSession).filter(DbSession.id == doc.session_id).first() + if not session or session.owner != user: + raise HTTPException(404, "Document not found") + + +def _owner_session_filter(q, user): + """Restrict a documents query to those owned by `user`. + + Documents now carry their own `owner` column (backfilled at boot from + the linked session, or assigned to the admin user for legacy/orphaned + docs). We filter on that directly rather than on a session join, so a + document whose session was deleted (session_id → NULL) still shows up + for its owner instead of silently vanishing from the Library + search. + + The owner backfill runs in init_db before the app serves requests, so + by the time this filter is live there are no NULL-owner rows to leak; + we therefore match the owner strictly for authenticated callers.""" + if not user: + if user == "" or _auth_disabled(): + return q + return q.filter(False) + return q.filter(Document.owner == user) + + + +def _slug(name: str) -> str: + """Filesystem-friendly version of a document title. + + Whitespace becomes underscores; other unsafe punctuation is dropped. + Preserves letters, digits, dot, hyphen, underscore. Idempotent. + """ + import re as _re + s = (name or "").strip() + # Drop the trailing extension if the title happens to include one + s = _re.sub(r'\.pdf$', '', s, flags=_re.IGNORECASE) + s = _re.sub(r'\s+', '_', s) + s = _re.sub(r'[^A-Za-z0-9._-]', '', s) + s = _re.sub(r'_+', '_', s).strip('_') + return s or "form" + + +# DPI scale for the interactive PDF view. ~150 DPI (2x of 72 PDF user-units). +_PDF_RENDER_SCALE = 2.0 + + +def _upload_path_inside(upload_dir: str, path: str) -> bool: + base = os.path.realpath(upload_dir) + p = os.path.realpath(path) + try: + return os.path.commonpath([base, p]) == base + except Exception: + return False + + +def _resolve_user_upload_path( + upload_handler: Any, + upload_id: str, + owner: Optional[str], + auth_manager=None, +) -> Optional[str]: + """Resolve an upload id to a filesystem path the caller may read.""" + if upload_handler is None: + return None + resolved = upload_handler.resolve_upload( + upload_id, + owner=owner, + auth_manager=auth_manager, + ) + if not isinstance(resolved, dict) or not resolved: + return None + path = resolved.get("path") + upload_dir = getattr(upload_handler, "upload_dir", None) + if path and upload_dir and not _upload_path_inside(upload_dir, path): + logger.warning("Upload path outside upload directory: %s", path) + return None + return path + + +def _locate_upload( + upload_dir: str, + file_id: str, + owner: Optional[str] = None, + auth_manager=None, + upload_handler: Any = None, +): + """Find an upload by its filename ID via UploadHandler.resolve_upload.""" + if upload_handler is None: + from src.upload_handler import UploadHandler + + base_dir = os.path.dirname(os.path.abspath(upload_dir)) + upload_handler = UploadHandler(base_dir, upload_dir) + return _resolve_user_upload_path(upload_handler, file_id, owner, auth_manager) + + +def _assert_pdf_marker_upload_owned( + request: Request, + content: str, + user: Optional[str], + upload_handler: Any, +) -> None: + """Reject document content whose pdf_source marker points at another user's upload.""" + if upload_handler is None: + return + from src.pdf_form_doc import find_source_upload_id + + upload_id = find_source_upload_id(content or "") + if not upload_id: + return + auth_manager = getattr(getattr(request.app, "state", None), "auth_manager", None) + if not _resolve_user_upload_path(upload_handler, upload_id, user, auth_manager): + raise HTTPException( + 400, + "Document PDF marker references an upload you do not own", + ) + + +def _derive_title(content: str) -> str: + """Derive a title from document content.""" + import re + if not isinstance(content, str): + return "Untitled" + text = content.strip() + if not text: + return "Untitled" + + # Markdown header + md = re.match(r'^#{1,3}\s+(.+)', text, re.MULTILINE) + if md: + title = md.group(1).strip() + if len(title) > 50: + title = title[:48] + "…" + return title + + # HTML heading + html = re.search(r'<h[1-3][^>]*>([^<]+)</h[1-3]>', text, re.IGNORECASE) + if html: + title = html.group(1).strip() + if len(title) > 50: + title = title[:48] + "…" + return title + + # First non-empty line (if short enough) + for line in text.split('\n'): + line = line.strip() + if line and 2 <= len(line) <= 60: + title = re.sub(r'[:#*`]+$', '', line).strip() + if title and len(title) > 50: + title = title[:48] + "…" + return title or "Untitled" + + return "Untitled" diff --git a/routes/document/document_routes.py b/routes/document/document_routes.py new file mode 100644 index 000000000..dae8b09fa --- /dev/null +++ b/routes/document/document_routes.py @@ -0,0 +1,1810 @@ +"""Document routes — CRUD for living documents with version history.""" + +import uuid +import logging +from datetime import datetime, timezone +from typing import Dict, Any, List, Optional + +from fastapi import APIRouter, HTTPException, Query, Request, UploadFile, File, Form + +from sqlalchemy import case, func, or_ +from core.database import SessionLocal, Document, DocumentVersion +from core.database import Session as DbSession +from src.auth_helpers import get_current_user, _auth_disabled +from src.constants import MAIL_ATTACHMENTS_DIR +from src.upload_handler import reserve_upload_references + +logger = logging.getLogger(__name__) + + +def _get_session_or_404(db, session_id: str, user: Optional[str]): + session = db.query(DbSession).filter(DbSession.id == session_id).first() + if not session: + raise HTTPException(404, "Session not found") + if user and session.owner != user: + raise HTTPException(404, "Session not found") + return session + + +def _aggregate_language_facets(lang_rows): + """Sum document counts per display language for the library facet. + + NULL-language and explicit "text" rows share the "text" bucket (the + language filter treats them as one), so they must be ADDED. The old dict + comprehension keyed both to "text", silently overwriting one group and + undercounting the facet versus what the filter actually returns. + """ + out = {} + for lang, cnt in lang_rows: + key = lang or "text" + out[key] = out.get(key, 0) + cnt + return out + + +def _library_language_for_document(doc: Document) -> str: + """Return the display language used by the document library. + + PDF documents are stored as markdown wrappers so the editor can preserve + extracted text, form fields, and annotations. The library should still + identify them as PDFs instead of exposing that internal wrapper format. + """ + from src.pdf_form_doc import find_source_upload_id + + if find_source_upload_id(doc.current_content or ""): + return "pdf" + return doc.language or "text" + + +def _email_source_key(content: str) -> tuple[str, str]: + """Return the source email identity embedded in an email draft document.""" + import re + + text = content or "" + uid_m = re.search(r"(?im)^X-Source-UID:\s*(.+?)\s*$", text) + folder_m = re.search(r"(?im)^X-Source-Folder:\s*(.+?)\s*$", text) + uid = (uid_m.group(1).strip() if uid_m else "") + folder = (folder_m.group(1).strip() if folder_m else "INBOX") + return uid, folder + + +from routes.document_helpers import ( + DocumentCreate, DocumentUpdate, DocumentPatch, + _doc_to_dict, _version_to_dict, + _verify_doc_owner, _owner_session_filter, + _slug, _resolve_user_upload_path, _assert_pdf_marker_upload_owned, _derive_title, + _PDF_RENDER_SCALE, +) + + +def setup_document_routes(session_manager, upload_handler=None) -> APIRouter: + router = APIRouter(tags=["documents"]) + + def _reserve_document_uploads(user: Optional[str], content: str) -> None: + missing_id = reserve_upload_references(upload_handler, user, content) + if missing_id: + raise HTTPException( + 409, + f"Referenced upload is no longer available: {missing_id}", + ) + + def _locate_current_user_upload(request: Request, upload_id: str, user: Optional[str]): + if upload_handler is None: + return None + auth_manager = getattr(getattr(request.app, "state", None), "auth_manager", None) + return _resolve_user_upload_path(upload_handler, upload_id, user, auth_manager) + + def _load_pdf_viewer_fitz(): + from src.pdf_runtime import load_pymupdf_for_pdf_viewer + + try: + return load_pymupdf_for_pdf_viewer() + except RuntimeError as exc: + raise HTTPException(503, str(exc)) from exc + + # ---- POST /api/document ---- + @router.post("/api/document") + async def create_document(request: Request, req: DocumentCreate) -> Dict[str, Any]: + from src.auth_helpers import require_privilege + user = require_privilege(request, "can_use_documents") + db = SessionLocal() + try: + # session_id is optional: a doc can be a session-less "library" doc + # (e.g. files imported from the library) — session_id is nullable and + # the doc is owner-stamped, so it lives in the library on its own. + session = None + if req.session_id: + # Match the lenient ownership model the rest of the app uses + # (see _owner_filter): only block when an AUTHENTICATED user is + # writing into a DIFFERENT user's session. In single-user / + # unconfigured / localhost-bypass mode, falsey users preserve + # the existing lenient path. + session = _get_session_or_404(db, req.session_id, user) + + # If no language was supplied (e.g. cloning a doc whose language + # was never set), detect it from the content rather than storing + # NULL — which made the editor fall back to plain text. Defaults + # to markdown for prose. + language = req.language + if not language: + from src.agent_tools.document_tools import _looks_like_email_document, _sniff_doc_language, _coerce_email_document_content + language = _sniff_doc_language(req.content) + else: + from src.agent_tools.document_tools import _looks_like_email_document, _coerce_email_document_content + if _looks_like_email_document(req.content, req.title): + language = "email" + + _reserve_document_uploads(user, req.content) + _assert_pdf_marker_upload_owned(request, req.content, user, upload_handler) + + # Reply drafts are keyed to the source email. If a UI/tool path tries + # to create a second draft for the same email in the same chat, + # update the existing draft instead so quoted thread history stays + # attached to the visible document. + if language == "email" and req.session_id: + source_uid, source_folder = _email_source_key(req.content) + if source_uid: + candidates = ( + db.query(Document) + .filter(Document.session_id == req.session_id) + .filter(Document.is_active == True) + .filter(Document.language == "email") + .order_by(Document.updated_at.desc()) + .limit(25) + .all() + ) + for existing in candidates: + old_uid, old_folder = _email_source_key(existing.current_content or "") + if old_uid != source_uid or old_folder != source_folder: + continue + merged = _coerce_email_document_content(existing.current_content or "", req.content) + if existing.current_content != merged: + new_ver = (existing.version_count or 1) + 1 + existing.current_content = merged + existing.title = req.title or existing.title + existing.version_count = new_ver + db.add(DocumentVersion( + id=str(uuid.uuid4()), + document_id=existing.id, + version_number=new_ver, + content=merged, + summary="Updated existing email draft", + source="user", + )) + db.commit() + db.refresh(existing) + return _doc_to_dict(existing) + + doc_id = str(uuid.uuid4()) + ver_id = str(uuid.uuid4()) + + doc = Document( + id=doc_id, + session_id=req.session_id, + title=req.title, + language=language, + current_content=req.content, + version_count=1, + is_active=True, + # Stamp ownership directly so the doc survives its session + # being deleted. Fall back to the session's owner when the + # request is unauthenticated (single-user / localhost bypass). + owner=user or (session.owner if session else None), + ) + ver = DocumentVersion( + id=ver_id, + document_id=doc_id, + version_number=1, + content=req.content, + summary="Initial version", + source="user", + ) + db.add(doc) + db.add(ver) + db.commit() + db.refresh(doc) + try: + from src.event_bus import fire_event + fire_event("document_created", doc.owner) + except Exception: + logger.debug("document_created event dispatch failed", exc_info=True) + return _doc_to_dict(doc) + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Failed to create document: {e}") + raise HTTPException(500, f"Failed to create document: {e}") + finally: + db.close() + + # ---- POST /api/documents/import-pdf ---- + @router.post("/api/documents/import-pdf") + async def import_pdf( + request: Request, + file: UploadFile = File(...), + session_id: Optional[str] = Form(None), + ) -> Dict[str, Any]: + """Upload a PDF and create the matching Document. + + Detects AcroForm fields — if any, creates a form-backed markdown doc + (clickable inputs in the PDF view). Otherwise creates a plain PDF doc + with a `pdf_source` marker so the viewer renders the pages without + overlays. + """ + from src.pdf_forms import has_form_fields, extract_fields + from src.pdf_form_doc import ( + save_field_sidecar, + create_form_markdown_document, + create_plain_pdf_document, + ) + from src.document_processor import _process_pdf, strip_pdf_content_marker + import os + + from src.auth_helpers import require_privilege + user = require_privilege(request, "can_use_documents") + + # session_id is optional — a library import isn't tied to a chat. When + # given, validate it; otherwise the PDF becomes a session-less library + # doc (the doc creators below already handle a missing session). + if session_id: + db = SessionLocal() + try: + _get_session_or_404(db, session_id, user) + finally: + db.close() + + if upload_handler is None: + raise HTTPException(500, "Upload handler not configured") + + client_ip = request.client.host if request.client else "unknown" + try: + meta = upload_handler.save_upload(file, client_ip, owner=user) + except HTTPException: + raise + except Exception as e: + logger.error(f"PDF import save_upload failed: {e}") + raise HTTPException(500, f"Upload failed: {e}") + + upload_id = meta["id"] + pdf_path = _locate_current_user_upload(request, upload_id, user) + if not pdf_path: + raise HTTPException(500, "Saved PDF could not be located") + + title = os.path.splitext(meta.get("original_name") or meta.get("name") or upload_id)[0] + try: + body_text = strip_pdf_content_marker(_process_pdf(pdf_path, owner=user)) + except Exception: + body_text = None + + is_form = False + try: + is_form = has_form_fields(pdf_path) + except Exception as e: + logger.warning(f"has_form_fields failed for {pdf_path}: {e}") + + if is_form: + fields = extract_fields(pdf_path) + save_field_sidecar(pdf_path, fields) + doc_id = create_form_markdown_document( + session_id=session_id, + fields=fields, + upload_id=upload_id, + title=title, + intro_text=body_text, + ) + else: + doc_id = create_plain_pdf_document( + session_id=session_id, + upload_id=upload_id, + title=title, + body_text=body_text, + ) + + if not doc_id: + raise HTTPException(500, "Failed to create document for PDF") + + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(500, "Created document not found") + # The PDF doc creators stamp owner from the session only; a + # session-less library import leaves owner NULL, which the Library's + # owner filter then hides. Stamp the requesting user so it shows. + if not doc.owner and user: + doc.owner = user + db.commit() + db.refresh(doc) + return _doc_to_dict(doc) + finally: + db.close() + + # ---- GET /api/documents/library ---- + @router.get("/api/documents/library") + async def documents_library( + request: Request, + search: Optional[str] = Query(None), + language: Optional[str] = Query(None), + sort: str = Query("recent"), + offset: int = Query(0, ge=0), + limit: int = Query(20, ge=1, le=50), + archived: bool = Query(False), + ) -> Dict[str, Any]: + user = get_current_user(request) + db = SessionLocal() + try: + from sqlalchemy import or_ + pdf_marker_cond = or_( + Document.current_content.like('%<!-- pdf_source upload_id="%'), + Document.current_content.like('%<!-- pdf_form_source upload_id="%'), + ) + library_language_expr = case( + (pdf_marker_cond, "pdf"), + (Document.language.is_(None), "text"), + else_=Document.language, + ) + # Archived view shows ONLY archived docs; the default view excludes + # them (NULL = legacy rows that predate the column = not archived). + _arch_cond = (Document.archived == True) if archived else or_( + Document.archived == False, Document.archived.is_(None)) + # Language facet counts (owner-filtered). PDF documents are stored + # as markdown wrappers, so group by the library display language + # instead of the raw stored language. + lang_q = ( + db.query(library_language_expr, func.count(Document.id)) + .outerjoin(DbSession, Document.session_id == DbSession.id) + .filter(Document.is_active == True).filter(_arch_cond) + ) + lang_q = _owner_session_filter(lang_q, user) + lang_rows = lang_q.group_by(library_language_expr).all() + languages = _aggregate_language_facets(lang_rows) + + # Session count (owner-filtered) + sc_q = ( + db.query(func.count(func.distinct(Document.session_id))) + .outerjoin(DbSession, Document.session_id == DbSession.id) + .filter(Document.is_active == True).filter(_arch_cond) + ) + sc_q = _owner_session_filter(sc_q, user) + session_count = sc_q.scalar() + + # Base query + q = ( + db.query(Document, DbSession.name) + .outerjoin(DbSession, Document.session_id == DbSession.id) + .filter(Document.is_active == True).filter(_arch_cond) + ) + q = _owner_session_filter(q, user) + + # Search filter — split on whitespace and require EACH term to + # match (title OR content). A single `%foo bar%` LIKE only matched + # the exact adjacent phrase, so any multi-word query with a space + # silently returned nothing. Per-term AND makes "machine learning" + # match docs containing both words regardless of position/order. + if search: + for tok in search.split(): + term = f"%{tok}%" + q = q.filter( + Document.title.ilike(term) | Document.current_content.ilike(term) + ) + + # Language filter. "pdf" is a display language derived from the + # source marker; "markdown" excludes those wrappers. + if language: + if language == "text": + q = q.filter((Document.language == None) | (Document.language == "text")) + elif language == "pdf": + q = q.filter(pdf_marker_cond) + else: + q = q.filter(Document.language == language) + if language == "markdown": + q = q.filter(~pdf_marker_cond) + + # Total before pagination + total = q.count() + + # Sorting + if sort == "oldest": + q = q.order_by(Document.created_at.asc()) + elif sort == "edits": + q = q.order_by(Document.version_count.desc()) + elif sort == "alpha": + q = q.order_by(Document.title.asc()) + else: # recent + q = q.order_by(Document.updated_at.desc()) + + rows = q.offset(offset).limit(limit).all() + + documents = [] + for doc, session_name in rows: + documents.append({ + "id": doc.id, + "session_id": doc.session_id, + "session_name": session_name, + "title": doc.title, + "language": _library_language_for_document(doc), + "preview": (doc.current_content or "")[:500], + "version_count": doc.version_count, + "created_at": (doc.created_at.isoformat() + "Z") if doc.created_at else None, + "updated_at": (doc.updated_at.isoformat() + "Z") if doc.updated_at else None, + }) + + return { + "documents": documents, + "total": total, + "languages": languages, + "session_count": session_count, + } + except Exception as e: + logger.error(f"Failed to fetch document library: {e}") + raise HTTPException(500, f"Failed to fetch document library: {e}") + finally: + db.close() + + # ---- GET /api/documents/{session_id} ---- + @router.get("/api/documents/{session_id}") + async def list_documents(request: Request, session_id: str) -> List[Dict[str, Any]]: + user = get_current_user(request) + db = SessionLocal() + try: + if not user: + if not _auth_disabled(): + raise HTTPException(403, "Authentication required") + # v2 review HIGH-9: raise 403 explicitly when the caller + # can't see this session, instead of returning [] which the + # UI treats identically to "no docs" and silently masks + # auth failures. + _get_session_or_404(db, session_id, user) + q = db.query(Document).filter( + Document.session_id == session_id + ) + if user: + q = q.filter(or_(Document.owner == user, Document.owner.is_(None))) + docs = q.order_by(Document.created_at.desc()).all() + return [_doc_to_dict(d) for d in docs] + finally: + db.close() + + # ---- GET /api/document/{doc_id} ---- + @router.get("/api/document/{doc_id}") + async def get_document(request: Request, doc_id: str) -> Dict[str, Any]: + user = get_current_user(request) + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + return _doc_to_dict(doc) + finally: + db.close() + + # ---- POST /api/document/{doc_id}/archive — soft-archive / restore ---- + @router.post("/api/document/{doc_id}/archive") + async def archive_document(request: Request, doc_id: str, archived: bool = Query(True)) -> Dict[str, Any]: + user = get_current_user(request) + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + doc.archived = bool(archived) + db.commit() + return {"ok": True, "id": doc_id, "archived": doc.archived} + finally: + db.close() + + # ---- POST /api/document/{doc_id}/extract-pdf-text ---- + @router.post("/api/document/{doc_id}/extract-pdf-text") + async def extract_pdf_text(request: Request, doc_id: str) -> Dict[str, Any]: + """Re-run pypdf+VL text extraction against the PDF linked to this doc + and merge the result into the doc's markdown content. Idempotent — the + existing body (everything below the title heading) is replaced. + + Lets the AI see PDF contents for old docs that were imported before + text extraction was wired, plus for scanned/image-only PDFs where the + VL model picks up text the basic pypdf path missed.""" + import re + from src.document_processor import _process_pdf, strip_pdf_content_marker + from src.pdf_form_doc import find_source_upload_id + + user = get_current_user(request) + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + + content = doc.current_content or "" + upload_id = find_source_upload_id(content) + if not upload_id: + raise HTTPException(400, "Document is not a PDF — no pdf_source marker found") + + pdf_path = _locate_current_user_upload(request, upload_id, user) + if not pdf_path: + raise HTTPException(404, "Source PDF could not be located") + + try: + body_text = strip_pdf_content_marker(_process_pdf(pdf_path, owner=user)) + except Exception as e: + logger.error(f"extract_pdf_text failed for {pdf_path}: {e}") + raise HTTPException(500, f"Extraction failed: {e}") + + if not body_text: + return {"ok": True, "id": doc_id, "extracted": False, "reason": "No readable content"} + + # Preserve everything up through the title (front-matter marker + + # first H1) and replace the rest with the freshly extracted text. + head_re = re.compile(r'^(<!--[^>]+-->\s*\n+#[^\n]*\n+)', re.MULTILINE) + head_match = head_re.match(content) + head = head_match.group(1) if head_match else (content.splitlines()[0] + "\n\n# " + (doc.title or "PDF") + "\n\n") + doc.current_content = head + body_text.strip() + "\n" + doc.version_count = (doc.version_count or 1) + 1 + db.add(DocumentVersion( + id=str(__import__("uuid").uuid4()), + document_id=doc_id, + version_number=doc.version_count, + content=doc.current_content, + summary="PDF text re-extracted (OCR)", + source="ocr", + )) + db.commit() + return {"ok": True, "id": doc_id, "extracted": True, "chars": len(body_text)} + finally: + db.close() + + # ---- POST /api/documents/export-zip — bundle selected docs into a .zip ---- + @router.post("/api/documents/export-zip") + async def documents_export_zip(request: Request): + """Zip the selected documents (each as a text file with the right + extension) — mirrors the gallery's bulk download-zip so multi-export + is one file instead of a blocked flood of individual downloads.""" + user = get_current_user(request) + try: + data = await request.json() + except Exception as e: + logger.warning("Failed to parse export request body, defaulting to empty", exc_info=e) + data = {} + ids = data.get("ids") or [] + if not ids: + raise HTTPException(400, "No documents specified") + _ext = { + "javascript": ".js", "python": ".py", "html": ".html", "css": ".css", + "markdown": ".md", "json": ".json", "yaml": ".yml", "bash": ".sh", + "sql": ".sql", "rust": ".rs", "go": ".go", "java": ".java", "c": ".c", + "cpp": ".cpp", "typescript": ".ts", "ruby": ".rb", "php": ".php", + "text": ".txt", "xml": ".xml", "toml": ".toml", "ini": ".ini", + } + db = SessionLocal() + try: + import io + import re + import zipfile + from fastapi import Response + docs = db.query(Document).filter(Document.id.in_(ids)).all() + buf = io.BytesIO() + used = set() + wrote = 0 + with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: + for doc in docs: + try: + _verify_doc_owner(db, doc, user) + except HTTPException: + continue # skip docs the user doesn't own + ext = _ext.get(doc.language or "text", ".txt") + base = (doc.title or "document").strip() or "document" + base = re.sub(r"[^\w\-. ]+", "", base)[:60].strip() or doc.id + name = base if "." in base else base + ext + i = 1 + while name in used: + name = f"{base}-{i}" + ("" if "." in base else ext) + i += 1 + used.add(name) + zf.writestr(name, doc.current_content or "") + wrote += 1 + if not wrote: + raise HTTPException(404, "No documents found") + return Response( + content=buf.getvalue(), + media_type="application/zip", + headers={"Content-Disposition": 'attachment; filename="documents.zip"'}, + ) + finally: + db.close() + + # ---- PUT /api/document/{doc_id} — user manual edit ---- + # Coalesce window: if the last user version was saved within this many + # seconds, update it in-place (user is still actively editing). + # Once the gap exceeds this, the next save creates a new version. + VERSION_COALESCE_SECONDS = 60 + + @router.put("/api/document/{doc_id}") + async def update_document(request: Request, doc_id: str, req: DocumentUpdate) -> Dict[str, Any]: + user = get_current_user(request) + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + + incoming_content = req.content + from src.agent_tools.document_tools import _coerce_email_document_content, _looks_like_email_document + is_email_doc = ( + (doc.language or "").lower() == "email" + or _looks_like_email_document(doc.current_content or "", doc.title or "") + or _looks_like_email_document(req.content or "", doc.title or "") + ) + if is_email_doc: + incoming_content = _coerce_email_document_content(doc.current_content or "", req.content) + doc.language = "email" + + # Skip if content is identical unless the caller explicitly wants + # a checkpoint version from the current editor state. + if doc.current_content == incoming_content and not req.force_version: + return _doc_to_dict(doc) + + _reserve_document_uploads(user, incoming_content) + _assert_pdf_marker_upload_owned(request, incoming_content, user, upload_handler) + + # Check if we can coalesce with the latest version + latest_ver = db.query(DocumentVersion).filter( + DocumentVersion.document_id == doc_id, + ).order_by(DocumentVersion.version_number.desc()).first() + + now = datetime.now(timezone.utc) + coalesced = False + if latest_ver and latest_ver.source == "user" and not req.force_version: + ver_time = latest_ver.created_at + if ver_time.tzinfo is None: + ver_time = ver_time.replace(tzinfo=timezone.utc) + age = (now - ver_time).total_seconds() + if age < VERSION_COALESCE_SECONDS: + # Update the existing version in-place + latest_ver.content = incoming_content + latest_ver.created_at = now + if req.summary: + latest_ver.summary = req.summary + coalesced = True + + if not coalesced: + new_ver = doc.version_count + 1 + ver = DocumentVersion( + id=str(uuid.uuid4()), + document_id=doc_id, + version_number=new_ver, + content=incoming_content, + summary=req.summary or "Manual edit", + source="user", + ) + doc.version_count = new_ver + db.add(ver) + + doc.current_content = incoming_content + db.commit() + db.refresh(doc) + return _doc_to_dict(doc) + except HTTPException: + raise + except Exception as e: + db.rollback() + raise HTTPException(500, f"Failed to update document: {e}") + finally: + db.close() + + # ---- PATCH /api/document/{doc_id} — metadata only ---- + @router.patch("/api/document/{doc_id}") + async def patch_document(request: Request, doc_id: str, req: DocumentPatch) -> Dict[str, Any]: + user = get_current_user(request) + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + if req.title is not None: + doc.title = req.title + if req.language is not None: + doc.language = req.language + if req.session_id is not None: + # Empty string = unlink from session + if req.session_id: + _get_session_or_404(db, req.session_id, user) + doc.session_id = req.session_id if req.session_id else None + if not req.session_id: + # Tab closed / doc detached from its session — drop the + # in-memory active-doc pointer so the last-resort injection + # path doesn't re-surface this doc in a later chat (#1160). + try: + from src.agent_tools.document_tools import clear_active_document + clear_active_document(doc_id) + except Exception as e: + logger.warning("Failed to clear active document %r on detach", doc_id, exc_info=e) + db.commit() + db.refresh(doc) + return _doc_to_dict(doc) + except HTTPException: + raise + except Exception as e: + db.rollback() + raise HTTPException(500, str(e)) + finally: + db.close() + + # ---- DELETE /api/document/{doc_id} — soft delete ---- + @router.delete("/api/document/{doc_id}") + async def delete_document(request: Request, doc_id: str) -> Dict[str, str]: + user = get_current_user(request) + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + doc.is_active = False + # Closed/deleted — drop the in-memory active-doc pointer so it isn't + # re-injected into a later, unrelated chat (#1160). + try: + from src.agent_tools.document_tools import clear_active_document + clear_active_document(doc_id) + except Exception: + pass + db.commit() + return {"status": "deleted", "id": doc_id} + except HTTPException: + raise + except Exception as e: + db.rollback() + raise HTTPException(500, str(e)) + finally: + db.close() + + # ---- GET /api/document/{doc_id}/versions ---- + @router.get("/api/document/{doc_id}/versions") + async def list_versions(request: Request, doc_id: str) -> List[Dict[str, Any]]: + user = get_current_user(request) + db = SessionLocal() + try: + # Verify ownership before listing versions + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + versions = db.query(DocumentVersion).filter( + DocumentVersion.document_id == doc_id + ).order_by(DocumentVersion.version_number.desc()).all() + return [{ + "id": v.id, + "version_number": v.version_number, + "content": v.content, + "summary": v.summary, + "source": v.source, + "created_at": v.created_at.isoformat() if v.created_at else None, + } for v in versions] + finally: + db.close() + + # ---- GET /api/document/{doc_id}/version/{num} ---- + @router.get("/api/document/{doc_id}/version/{num}") + async def get_version(request: Request, doc_id: str, num: int) -> Dict[str, Any]: + user = get_current_user(request) + db = SessionLocal() + try: + # Verify ownership + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + ver = db.query(DocumentVersion).filter( + DocumentVersion.document_id == doc_id, + DocumentVersion.version_number == num, + ).first() + if not ver: + raise HTTPException(404, "Version not found") + return _version_to_dict(ver) + finally: + db.close() + + # ---- POST /api/document/{doc_id}/restore/{num} ---- + @router.post("/api/document/{doc_id}/restore/{num}") + async def restore_version(request: Request, doc_id: str, num: int) -> Dict[str, Any]: + user = get_current_user(request) + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + + old_ver = db.query(DocumentVersion).filter( + DocumentVersion.document_id == doc_id, + DocumentVersion.version_number == num, + ).first() + if not old_ver: + raise HTTPException(404, "Version not found") + + new_ver_num = doc.version_count + 1 + ver = DocumentVersion( + id=str(uuid.uuid4()), + document_id=doc_id, + version_number=new_ver_num, + content=old_ver.content, + summary=f"Restored from v{num}", + source="user", + ) + doc.current_content = old_ver.content + doc.version_count = new_ver_num + db.add(ver) + db.commit() + db.refresh(doc) + return _doc_to_dict(doc) + except HTTPException: + raise + except Exception as e: + db.rollback() + raise HTTPException(500, str(e)) + finally: + db.close() + + # ---- POST /api/documents/tidy — clean up broken/empty documents ---- + @router.post("/api/documents/tidy") + async def tidy_documents(request: Request) -> Dict[str, Any]: + """Fix empty titles and remove broken/empty documents (user's docs only).""" + user = get_current_user(request) + db = SessionLocal() + try: + q = ( + db.query(Document) + .outerjoin(DbSession, Document.session_id == DbSession.id) + .filter(Document.is_active == True) + .filter((Document.archived == False) | (Document.archived.is_(None))) + ) + q = _owner_session_filter(q, user) + docs = q.all() + fixed_titles = 0 + deleted = 0 + + # Same junk-detection logic as the scheduled tidy_documents + # action (src/document_actions.py). Keep these two in sync. + import re as _re + from src.document_actions import _JUNK_TITLES + + to_delete = [] + now = datetime.now(timezone.utc) + for doc in docs: + created = doc.created_at + if created and created.tzinfo is None: + created = created.replace(tzinfo=timezone.utc) + + # Skip freshly created documents to avoid deleting them while the user is actively editing + if created and (now - created).total_seconds() < 900: # 15 minutes + continue + + content = (doc.current_content or "").strip() + title_raw = (doc.title or "").strip() + title = title_raw.lower() + is_fresh_empty = ( + not content + and created is not None + and (now - created).total_seconds() < 1800 + ) + if is_fresh_empty: + continue + + # Strip markdown noise to get a "real" character count + stripped = _re.sub(r"^#{1,6}\s+", "", content, flags=_re.MULTILINE) + stripped = _re.sub(r"[*_`>\-=]+", "", stripped) + stripped = _re.sub(r"\s+", " ", stripped).strip() + real_len = len(stripped) + + # Detect email-scaffold stubs: "To: \nSubject: \n---\n" style + # bodies with nothing typed in. Stub = every meaningful line + # is a header label (To:/From:/Subject:/...) with no real + # value (blank, "empty", "(empty)", "-", "none", "n/a"). + _is_email_stub = False + _HEADER_RE = _re.compile(r"^(to|from|cc|bcc|subject|reply-to):\s*(.*)$", _re.I) + _PLACEHOLDER_VALS = {"", "empty", "(empty)", "-", "—", "none", "n/a", "na", "tbd"} + if title in ("new email", "new mail", "new message") or doc.language == "email": + body_lines = [ln.strip() for ln in content.split("\n") + if ln.strip() and ln.strip() != "---"] + def _is_filler(ln): + m = _HEADER_RE.match(ln) + if not m: + return False + val = (m.group(2) or "").strip().lower() + return val in _PLACEHOLDER_VALS + has_real_body = any(not _is_filler(ln) for ln in body_lines) + if body_lines and not has_real_body: + _is_email_stub = True + + # Hard-delete obviously empty / junk documents + if not content or content in ("", "# Untitled"): + to_delete.append(doc); deleted += 1; continue + if _is_email_stub: + to_delete.append(doc); deleted += 1; continue + if title in _JUNK_TITLES: + to_delete.append(doc); deleted += 1; continue + + # Fix empty or placeholder titles on survivors + if not title_raw or title_raw == "Untitled": + new_title = _derive_title(content) + if new_title and new_title != "Untitled": + doc.title = new_title + fixed_titles += 1 + + for doc in to_delete: + db.delete(doc) + + # Also clean up inactive empty docs from previous soft-deletes + inactive_q = ( + db.query(Document) + .outerjoin(DbSession, Document.session_id == DbSession.id) + .filter(Document.is_active == False) + .filter((Document.current_content == None) | (Document.current_content == "")) + ) + inactive_q = _owner_session_filter(inactive_q, user) + inactive_docs = inactive_q.all() + for doc in inactive_docs: + db.delete(doc) + deleted += len(inactive_docs) + + db.commit() + return { + "fixed_titles": fixed_titles, + "deleted": deleted, + "message": f"Fixed {fixed_titles} title{'s' if fixed_titles != 1 else ''}, removed {deleted} empty document{'s' if deleted != 1 else ''}", + } + except Exception as e: + db.rollback() + logger.error(f"Document tidy failed: {e}") + raise HTTPException(500, f"Tidy failed: {e}") + finally: + db.close() + + # ---- POST /api/documents/ai-tidy — AI-powered cleanup of junk/test documents ---- + @router.post("/api/documents/ai-tidy") + async def ai_tidy_documents(request: Request) -> Dict[str, Any]: + """Use AI to judge if documents are junk/test/accidental, then delete them. + Caches verdicts so previously-reviewed docs are skipped.""" + from src.task_endpoint import resolve_task_endpoint + from src.endpoint_resolver import resolve_endpoint + from src.llm_core import llm_call_async + + user = get_current_user(request) + url, model, headers = resolve_task_endpoint(owner=user or None) + if not url or not model: + # Fall back to default endpoint + url, model, headers = resolve_endpoint("default", owner=user or None) + if not url or not model: + raise HTTPException(500, "No endpoint configured for AI tidy") + + db = SessionLocal() + try: + q = ( + db.query(Document) + .outerjoin(DbSession, Document.session_id == DbSession.id) + .filter(Document.is_active == True) + .filter((Document.archived == False) | (Document.archived.is_(None))) + ) + q = _owner_session_filter(q, user) + docs = q.all() + + # Only review docs that haven't been reviewed yet + to_review = [d for d in docs if not d.tidy_verdict] + if not to_review: + return {"deleted": 0, "reviewed": 0, "message": "All documents already reviewed"} + + # Build a batch prompt — review up to 30 at a time + batch = to_review[:30] + doc_list = [] + for i, doc in enumerate(batch): + preview = (doc.current_content or "")[:300].strip() + doc_list.append(f"[{i}] title=\"{doc.title}\" lang={doc.language or 'text'} content_preview=\"{preview}\"") + + prompt = ( + "You are a document library cleaner. For each document below, decide if it is JUNK " + "(test, accidental, placeholder, empty-ish, tool-test, throwaway) or KEEP (real content worth saving).\n\n" + "Respond with ONLY a JSON array of verdicts, one per document, like: [\"junk\",\"keep\",\"junk\",...]\n" + "No explanation, no markdown, just the JSON array.\n\n" + + "\n".join(doc_list) + ) + + response = await llm_call_async( + url, model, + [{"role": "system", "content": "You classify documents as junk or keep. Respond only with a JSON array."}, + {"role": "user", "content": prompt}], + temperature=0.1, + max_tokens=200, + headers=headers, + timeout=30, + ) + + # Parse verdicts + import re + match = re.search(r'\[.*?\]', response, re.DOTALL) + if not match: + raise HTTPException(500, "AI returned invalid response") + + import json as _json + verdicts = _json.loads(match.group()) + + deleted = 0 + reviewed = 0 + for i, doc in enumerate(batch): + if i >= len(verdicts): + break + verdict = str(verdicts[i] or "").lower().strip() + if verdict == "junk": + doc.tidy_verdict = "junk" + db.delete(doc) + deleted += 1 + else: + doc.tidy_verdict = "keep" + reviewed += 1 + + db.commit() + return { + "deleted": deleted, + "reviewed": reviewed, + "remaining": len(to_review) - len(batch), + "message": f"Reviewed {reviewed}, removed {deleted} junk document{'s' if deleted != 1 else ''}", + } + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"AI tidy failed: {e}") + raise HTTPException(500, f"AI tidy failed: {e}") + finally: + db.close() + + # ---- POST /api/document/{doc_id}/export-pdf/preview ---- + @router.post("/api/document/{doc_id}/export-pdf/preview") + async def export_pdf_preview(doc_id: str, request: Request) -> Dict[str, Any]: + """Return the field-value mapping that would be written to the PDF. + + Frontend shows this in a confirmation modal so the user can spot/fix + any wrong values before triggering the actual download. + """ + from src.pdf_form_doc import find_source_upload_id, parse_markdown_to_values, load_field_sidecar + + user = get_current_user(request) + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + + upload_id = find_source_upload_id(doc.current_content or "") + if not upload_id: + raise HTTPException(400, "Document is not linked to a source PDF") + + pdf_path = _locate_current_user_upload(request, upload_id, user) + if not pdf_path: + raise HTTPException(404, f"Source PDF {upload_id} not found in uploads") + + fields = load_field_sidecar(pdf_path) + if not fields: + raise HTTPException(404, "Field schema sidecar missing for source PDF") + + values = parse_markdown_to_values(doc.current_content or "") + field_meta = {f["name"]: f for f in fields} + + preview = [] + for name, current in values.items(): + meta = field_meta.get(name) + if not meta: + continue + preview.append({ + "name": name, + "label": meta.get("label") or name, + "type": meta.get("type"), + "options": meta.get("options") or [], + "page": meta.get("page"), + "value": current, + }) + + unknown = [ + name for name in values + if name not in field_meta + ] + return { + "doc_id": doc_id, + "upload_id": upload_id, + "fields": preview, + "unknown_fields": unknown, + "total": len(fields), + "filled": sum(1 for p in preview if p["value"] not in ("", False, None)), + } + finally: + db.close() + + # ---- GET /api/document/{doc_id}/render-pages ---- + @router.get("/api/document/{doc_id}/render-pages") + async def render_pages(doc_id: str, request: Request) -> Dict[str, Any]: + """Return per-page metadata for the interactive PDF view. + + Each page entry has its rendered-image dimensions (matching what + /page/{n}.png returns at the same DPI) plus the list of form fields + on that page with their rects translated to image-pixel coordinates. + Frontend overlays HTML form controls at those positions. + """ + from src.pdf_form_doc import find_source_upload_id, parse_markdown_to_values, load_field_sidecar + + user = get_current_user(request) + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + upload_id = find_source_upload_id(doc.current_content or "") + if not upload_id: + raise HTTPException(400, "Document is not linked to a source PDF") + pdf_path = _locate_current_user_upload(request, upload_id, user) + if not pdf_path: + raise HTTPException(404, f"Source PDF {upload_id} not found") + + fitz = _load_pdf_viewer_fitz() + schema = load_field_sidecar(pdf_path) or [] + values = parse_markdown_to_values(doc.current_content or "") + + # Group fields by page + by_page: Dict[int, list] = {} + for f in schema: + by_page.setdefault(f["page"], []).append(f) + + scale = _PDF_RENDER_SCALE + pdf_doc = fitz.open(pdf_path) + try: + pages_out = [] + for page_index in range(pdf_doc.page_count): + page = pdf_doc[page_index] + page_no = page_index + 1 + pw, ph = page.rect.width, page.rect.height + img_w = int(pw * scale) + img_h = int(ph * scale) + fields_out = [] + for f in by_page.get(page_no, []): + x0, y0, x1, y1 = f["rect"] + fields_out.append({ + "name": f["name"], + "type": f["type"], + "label": f.get("label") or "", + "options": f.get("options") or [], + "value": values.get(f["name"], f.get("value", "")), + "rect_px": [ + int(x0 * scale), int(y0 * scale), + int(x1 * scale), int(y1 * scale), + ], + }) + pages_out.append({ + "page": page_no, + "width": img_w, + "height": img_h, + "fields": fields_out, + }) + return {"doc_id": doc_id, "scale": scale, "pages": pages_out} + finally: + pdf_doc.close() + finally: + db.close() + + # ---- GET /api/document/{doc_id}/page/{n}.png ---- + @router.get("/api/document/{doc_id}/page/{page_no}.png") + async def render_page_png(doc_id: str, page_no: int, request: Request): + """Render one page of the source PDF as a PNG (no values stamped — the + frontend overlays HTML form inputs on top).""" + from fastapi.responses import Response + from src.pdf_form_doc import find_source_upload_id + + user = get_current_user(request) + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + upload_id = find_source_upload_id(doc.current_content or "") + if not upload_id: + raise HTTPException(400, "Document is not linked to a source PDF") + pdf_path = _locate_current_user_upload(request, upload_id, user) + if not pdf_path: + raise HTTPException(404, "Source PDF not found") + finally: + db.close() + + fitz = _load_pdf_viewer_fitz() + pdf_doc = fitz.open(pdf_path) + try: + if page_no < 1 or page_no > pdf_doc.page_count: + raise HTTPException(404, "Page out of range") + page = pdf_doc[page_no - 1] + mat = fitz.Matrix(_PDF_RENDER_SCALE, _PDF_RENDER_SCALE) + pix = page.get_pixmap(matrix=mat, alpha=False) + png_bytes = pix.tobytes("png") + return Response( + content=png_bytes, + media_type="image/png", + headers={"Cache-Control": "public, max-age=3600"}, + ) + finally: + pdf_doc.close() + + # ---- POST /api/document/{doc_id}/ai-fill-annotations ---- + @router.post("/api/document/{doc_id}/ai-fill-annotations") + async def ai_fill_annotations(doc_id: str, request: Request) -> Dict[str, Any]: + """Ask a vision-capable LLM to locate fillable areas on a flat PDF and + propose annotation values for each, given a free-form user instruction. + + Returns a list of annotations: [{page, x, y, w, h, value}] where x/y/w/h + are page-percentages (0–100) — same coordinate system as the freeform + annotations the frontend already renders. + """ + import base64 + import json + import fitz + from src.pdf_form_doc import find_source_upload_id + from src.document_processor import _resolve_vl_model, _load_vl_settings + from src.llm_core import llm_call_async + + body = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {} + instruction = (body or {}).get("instruction", "").strip() + if not instruction: + raise HTTPException(400, "instruction is required") + + user = get_current_user(request) + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + upload_id = find_source_upload_id(doc.current_content or "") + if not upload_id: + raise HTTPException(400, "Document is not linked to a source PDF") + pdf_path = _locate_current_user_upload(request, upload_id, user) + if not pdf_path: + raise HTTPException(404, "Source PDF not found") + finally: + db.close() + + # Resolve VL model (admin-configured or auto-detected vision-capable) + settings = _load_vl_settings() + vl_model = settings.get("vision_model", "") + try: + url, model_id, headers = _resolve_vl_model(vl_model, owner=user) + except Exception as e: + raise HTTPException(503, f"No vision model available: {e}") + + system_prompt = ( + "You analyze rendered PDF page images and propose values to fill in. " + "For each blank line, box, underscore, or labeled space on the page that " + "should be filled given the user's instruction, output one annotation. " + "Coordinates are percentages (0-100) of the page width/height with the " + "origin at top-left. Width/height should match the visible blank box. " + "Return ONLY a JSON array, no prose, no markdown fences. Each entry: " + '{"x": number, "y": number, "w": number, "h": number, "value": string}. ' + "If a region should not be filled, omit it. If nothing should be filled, " + "return []." + ) + + all_annotations = [] + pdf_doc = fitz.open(pdf_path) + try: + for page_index in range(pdf_doc.page_count): + page = pdf_doc[page_index] + mat = fitz.Matrix(_PDF_RENDER_SCALE, _PDF_RENDER_SCALE) + pix = page.get_pixmap(matrix=mat, alpha=False) + png_bytes = pix.tobytes("png") + b64 = base64.b64encode(png_bytes).decode("ascii") + + messages = [ + {"role": "system", "content": system_prompt}, + { + "role": "user", + "content": [ + { + "type": "text", + "text": ( + f"User instruction:\n{instruction}\n\n" + f"This is page {page_index + 1} of {pdf_doc.page_count}. " + "Return JSON array of annotations to add to this page." + ), + }, + { + "type": "image_url", + "image_url": {"url": f"data:image/png;base64,{b64}"}, + }, + ], + }, + ] + try: + raw = await llm_call_async( + url, model_id, messages, + temperature=0.1, max_tokens=2000, headers=headers, + ) + except Exception as e: + logger.error(f"VL call failed on page {page_index + 1}: {e}") + continue + + raw = (raw or "").strip() + if raw.startswith("```"): + raw = raw.split("\n", 1)[-1].rsplit("```", 1)[0].strip() + try: + parsed = json.loads(raw) + except Exception: + logger.warning(f"AI fill: page {page_index + 1} returned non-JSON: {raw[:200]}") + continue + if not isinstance(parsed, list): + continue + for item in parsed: + if not isinstance(item, dict): + continue + try: + x = float(item.get("x", 0)) + y = float(item.get("y", 0)) + w = float(item.get("w", 0)) + h = float(item.get("h", 0)) + value = str(item.get("value", "") or "") + except Exception: + continue + # Clamp + reject zero-size entries + if w <= 0.5 or h <= 0.3: + continue + x = max(0.0, min(99.0, x)) + y = max(0.0, min(99.0, y)) + w = max(0.5, min(100.0 - x, w)) + h = max(0.3, min(100.0 - y, h)) + if not value.strip(): + continue + all_annotations.append({ + "page": page_index + 1, + "x": round(x, 2), + "y": round(y, 2), + "w": round(w, 2), + "h": round(h, 2), + "value": value, + }) + finally: + pdf_doc.close() + + return {"annotations": all_annotations} + + # ---- GET /api/document/{doc_id}/render-pdf ---- + @router.get("/api/document/{doc_id}/render-pdf") + async def render_pdf(doc_id: str, request: Request): + """Inline PDF preview filled with the current markdown values. + + Same plumbing as the export route, but no signature stamping and + served inline (Content-Disposition: inline) so the browser can + embed it in an iframe. Cache-busted by the caller via query string. + """ + import base64 + import os + import tempfile + from fastapi.responses import FileResponse + from starlette.background import BackgroundTask + from src.pdf_form_doc import find_source_upload_id, parse_markdown_to_values, parse_markdown_annotations + from src.pdf_forms import fill_fields, stamp_annotations + from core.database import Signature + + # Track temp files for this request so they get unlinked AFTER + # the response is fully sent (BackgroundTask runs post-send). + _to_unlink: list[str] = [] + def _cleanup_temps(): + for _p in _to_unlink: + try: + os.unlink(_p) + except FileNotFoundError: + pass + except Exception as _e: + logger.warning(f"Could not unlink temp PDF {_p}: {_e}") + + user = get_current_user(request) + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + upload_id = find_source_upload_id(doc.current_content or "") + if not upload_id: + raise HTTPException(400, "Document is not linked to a source PDF") + pdf_path = _locate_current_user_upload(request, upload_id, user) + if not pdf_path: + raise HTTPException(404, f"Source PDF {upload_id} not found") + + # Fail fast with a clear 503 if the optional PyMuPDF dependency + # is missing — fill_fields/stamp_annotations will otherwise + # raise RuntimeError deep inside and bubble out as a 500. + # Mirrors the convention in _load_pdf_viewer_fitz above. + _load_pdf_viewer_fitz() + + values = parse_markdown_to_values(doc.current_content or "") + out_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name + _to_unlink.append(out_path) + try: + fill_fields(pdf_path, out_path, values) + except Exception as e: + logger.error(f"render_pdf fill_fields failed for {doc_id}: {e}") + _cleanup_temps() + raise HTTPException(500, f"PDF render failed: {e}") + + annotations = parse_markdown_annotations(doc.current_content or "") + if annotations: + ann_sig_ids = [ + a["value"][len("signature:"):].strip() + for a in annotations + if a.get("kind") == "signature" + and isinstance(a.get("value"), str) + and a["value"].startswith("signature:") + ] + ann_signature_pngs: dict[str, bytes] = {} + if ann_sig_ids: + # SECURITY: filter by owner so a caller can't reference + # someone else's signature ID from doc markdown and have + # it stamped/exported. + _sig_q = db.query(Signature).filter(Signature.id.in_(ann_sig_ids)) + if user: + _sig_q = _sig_q.filter(Signature.owner == user) + sig_rows = _sig_q.all() + for s in sig_rows: + try: + ann_signature_pngs[s.id] = base64.b64decode(s.data_png) + except Exception as e: + logger.warning(f"Bad annotation signature data for {s.id}: {e}") + annotated_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name + _to_unlink.append(annotated_path) + try: + stamp_annotations(out_path, annotated_path, annotations, ann_signature_pngs) + out_path = annotated_path + except Exception as e: + logger.error(f"stamp_annotations (render) failed for {doc_id}: {e}") + + return FileResponse( + out_path, + media_type="application/pdf", + headers={"Content-Disposition": "inline"}, + background=BackgroundTask(_cleanup_temps), + ) + finally: + db.close() + + # ---- GET /api/document/{doc_id}/export-pdf ---- + @router.get("/api/document/{doc_id}/export-pdf") + async def export_pdf(doc_id: str, request: Request): + """Stream the filled PDF for download. + + Reads field values and signature selections from the markdown — there + is no separate confirmation step. Signature fields contain their + chosen signature ID encoded as `signature:<id>` in the value. + """ + import base64 + import os + import tempfile + from fastapi.responses import FileResponse + from starlette.background import BackgroundTask + from src.pdf_form_doc import find_source_upload_id, parse_markdown_to_values, load_field_sidecar, parse_markdown_annotations + from src.pdf_forms import fill_fields, stamp_signatures, stamp_annotations + from core.database import Signature + + _to_unlink: list[str] = [] + def _cleanup_temps(): + for _p in _to_unlink: + try: + os.unlink(_p) + except FileNotFoundError: + pass + except Exception as _e: + logger.warning(f"Could not unlink temp PDF {_p}: {_e}") + + user = get_current_user(request) + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + + upload_id = find_source_upload_id(doc.current_content or "") + if not upload_id: + raise HTTPException(400, "Document is not linked to a source PDF") + + pdf_path = _locate_current_user_upload(request, upload_id, user) + if not pdf_path: + raise HTTPException(404, f"Source PDF {upload_id} not found in uploads") + + schema = load_field_sidecar(pdf_path) or [] + sig_field_names = {f["name"] for f in schema if f.get("type") == "signature"} + + all_values = parse_markdown_to_values(doc.current_content or "") + # Split: signature fields go to stamps, everything else to fill_fields + text_values: dict = {} + sig_ids: dict[str, str] = {} + for name, raw in all_values.items(): + if name in sig_field_names and isinstance(raw, str) and raw.startswith("signature:"): + sig_ids[name] = raw[len("signature:"):].strip() + elif name not in sig_field_names: + text_values[name] = raw + + stamps: dict = {} + if sig_ids: + # SECURITY: filter by owner — same reason as render_pdf. + _sig_q2 = db.query(Signature).filter(Signature.id.in_(list(sig_ids.values()))) + if user: + _sig_q2 = _sig_q2.filter(Signature.owner == user) + rows = _sig_q2.all() + by_id = {s.id: s for s in rows} + for field_name, sid in sig_ids.items(): + s = by_id.get(sid) + if not s: + continue + try: + stamps[field_name] = base64.b64decode(s.data_png) + except Exception as e: + logger.warning(f"Bad signature data for {sid}: {e}") + + filled_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name + _to_unlink.append(filled_path) + try: + fill_fields(pdf_path, filled_path, text_values) + except Exception as e: + logger.error(f"fill_fields failed for doc {doc_id}: {e}") + _cleanup_temps() + raise HTTPException(500, f"PDF fill failed: {e}") + + out_path = filled_path + if stamps: + stamped_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name + _to_unlink.append(stamped_path) + try: + stamp_signatures(filled_path, stamped_path, stamps) + out_path = stamped_path + except Exception as e: + logger.error(f"stamp_signatures failed for doc {doc_id}: {e}") + + # Burn freeform annotations (Text/Check/Sign drops) on top. + annotations = parse_markdown_annotations(doc.current_content or "") + if annotations: + # Resolve any signature annotations to their PNG bytes. + ann_sig_ids = [ + a["value"][len("signature:"):].strip() + for a in annotations + if a.get("kind") == "signature" + and isinstance(a.get("value"), str) + and a["value"].startswith("signature:") + ] + ann_signature_pngs: dict[str, bytes] = {} + if ann_sig_ids: + # SECURITY: filter by owner so a caller can't reference + # someone else's signature ID from doc markdown and have + # it stamped/exported. + _sig_q = db.query(Signature).filter(Signature.id.in_(ann_sig_ids)) + if user: + _sig_q = _sig_q.filter(Signature.owner == user) + sig_rows = _sig_q.all() + for s in sig_rows: + try: + ann_signature_pngs[s.id] = base64.b64decode(s.data_png) + except Exception as e: + logger.warning(f"Bad annotation signature data for {s.id}: {e}") + annotated_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name + _to_unlink.append(annotated_path) + try: + stamp_annotations(out_path, annotated_path, annotations, ann_signature_pngs) + out_path = annotated_path + except Exception as e: + logger.error(f"stamp_annotations failed for doc {doc_id}: {e}") + + download_name = _slug(doc.title or "form") + "_annotated.pdf" + return FileResponse( + out_path, + media_type="application/pdf", + filename=download_name, + background=BackgroundTask(_cleanup_temps), + ) + finally: + db.close() + + # ---- POST /api/document/{doc_id}/prepare-signed-reply ---- + @router.post("/api/document/{doc_id}/prepare-signed-reply") + async def prepare_signed_reply(doc_id: str, request: Request): + """Bake the current PDF state (form fields + signature stamps + + annotations) into a flattened PDF, drop it in COMPOSE_UPLOADS_DIR + and return the reply context (To/Subject/threading headers) so the + frontend can open a reply draft with this attachment pre-loaded. + + Requires the document to have source_email_* metadata (set when the + doc was created via /api/email/attachment-as-doc). Otherwise 400. + """ + import base64 + import tempfile + import shutil + import uuid as _uuid + import email as _email_mod + from src.pdf_form_doc import ( + find_source_upload_id, parse_markdown_to_values, + load_field_sidecar, parse_markdown_annotations, + ) + from src.pdf_forms import fill_fields, stamp_signatures, stamp_annotations + from core.database import Signature + # COMPOSE_UPLOADS_DIR lives in email_routes — re-derive here so we + # don't import from a routes file (cycle-prone). Same env override + # as email_routes (ODYSSEUS_MAIL_ATTACHMENTS_DIR). + from pathlib import Path as _Path + _COMPOSE_DIR = _Path(MAIL_ATTACHMENTS_DIR) / "_compose" + _COMPOSE_DIR.mkdir(parents=True, exist_ok=True) + + user = get_current_user(request) + db = SessionLocal() + try: + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + raise HTTPException(404, "Document not found") + _verify_doc_owner(db, doc, user) + + if not (doc.source_email_uid and doc.source_email_folder): + raise HTTPException(400, "Document has no source email — cannot reply") + + # 1) Build the flattened PDF (same pipeline as export_pdf) + upload_id = find_source_upload_id(doc.current_content or "") + if not upload_id: + raise HTTPException(400, "Document is not linked to a source PDF") + pdf_path = _locate_current_user_upload(request, upload_id, user) + if not pdf_path: + raise HTTPException(404, f"Source PDF {upload_id} not found") + + schema = load_field_sidecar(pdf_path) or [] + sig_field_names = {f["name"] for f in schema if f.get("type") == "signature"} + all_values = parse_markdown_to_values(doc.current_content or "") + text_values: dict = {} + sig_ids: dict[str, str] = {} + for name, raw in all_values.items(): + if name in sig_field_names and isinstance(raw, str) and raw.startswith("signature:"): + sig_ids[name] = raw[len("signature:"):].strip() + elif name not in sig_field_names: + text_values[name] = raw + + stamps: dict = {} + if sig_ids: + # SECURITY: filter by owner — same reason as render_pdf. + _sig_q2 = db.query(Signature).filter(Signature.id.in_(list(sig_ids.values()))) + if user: + _sig_q2 = _sig_q2.filter(Signature.owner == user) + rows = _sig_q2.all() + by_id = {s.id: s for s in rows} + for fname, sid in sig_ids.items(): + s = by_id.get(sid) + if not s: + continue + try: + stamps[fname] = base64.b64decode(s.data_png) + except Exception: + pass + + import os + _to_unlink: list[str] = [] + filled_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name + _to_unlink.append(filled_path) + fill_fields(pdf_path, filled_path, text_values) + out_path = filled_path + if stamps: + stamped_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name + _to_unlink.append(stamped_path) + try: + stamp_signatures(filled_path, stamped_path, stamps) + out_path = stamped_path + except Exception as e: + logger.warning(f"stamp_signatures failed for {doc_id}: {e}") + + annotations = parse_markdown_annotations(doc.current_content or "") + if annotations: + ann_sig_ids = [ + a["value"][len("signature:"):].strip() + for a in annotations + if a.get("kind") == "signature" + and isinstance(a.get("value"), str) + and a["value"].startswith("signature:") + ] + ann_signature_pngs: dict[str, bytes] = {} + if ann_sig_ids: + # SECURITY: filter by owner so a caller can't reference + # someone else's signature ID from doc markdown and have + # it stamped/exported. + _sig_q = db.query(Signature).filter(Signature.id.in_(ann_sig_ids)) + if user: + _sig_q = _sig_q.filter(Signature.owner == user) + sig_rows = _sig_q.all() + for s in sig_rows: + try: + ann_signature_pngs[s.id] = base64.b64decode(s.data_png) + except Exception: + pass + annotated_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name + _to_unlink.append(annotated_path) + try: + stamp_annotations(out_path, annotated_path, annotations, ann_signature_pngs) + out_path = annotated_path + except Exception as e: + logger.warning(f"stamp_annotations failed for {doc_id}: {e}") + + # 2) Move/copy into COMPOSE_UPLOADS_DIR with the token format + # `<uuid>_<original_name>` that /api/email/send expects. + filename = _slug(doc.title or "signed") + "_signed.pdf" + token = f"{_uuid.uuid4().hex}_{filename}" + dest = _COMPOSE_DIR / token + shutil.copyfile(out_path, str(dest)) + # Unlink the intermediate temp PDFs now that they've been + # copied into COMPOSE_UPLOADS_DIR. + for _p in _to_unlink: + try: + os.unlink(_p) + except FileNotFoundError: + pass + except Exception as _e: + logger.warning(f"Could not unlink temp PDF {_p}: {_e}") + + # 3) Fetch the source email's headers so we can build a clean reply + # context (To/Subject/In-Reply-To/References). + try: + from routes.email_routes import _imap, _decode_header + from routes.email_helpers import _q + except Exception: + _imap = None + _decode_header = lambda x: x or "" + _q = lambda x: x or "" + + to_addr = "" + from_name = "" + subject = "" + in_reply_to = doc.source_email_message_id or "" + references = in_reply_to + if _imap: + try: + with _imap(doc.source_email_account_id or None) as conn: + conn.select(_q(doc.source_email_folder), readonly=True) + status, data = conn.fetch(doc.source_email_uid.encode(), "(RFC822.HEADER)") + if status == "OK" and data and data[0]: + raw_hdr = data[0][1] + m = _email_mod.message_from_bytes(raw_hdr) + sender = _decode_header(m.get("From", "")) + from_name, to_addr = _email_mod.utils.parseaddr(sender) + if not to_addr: + to_addr = sender + subject = _decode_header(m.get("Subject", "") or "") + if subject and not subject.lower().startswith("re:"): + subject = "Re: " + subject + msg_refs = (m.get("References") or "").strip() + msg_in_reply = (m.get("Message-ID") or "").strip() or in_reply_to + in_reply_to = msg_in_reply + references = (msg_refs + " " + msg_in_reply).strip() if msg_refs else msg_in_reply + except Exception as e: + logger.warning(f"prepare-signed-reply header fetch failed: {e}") + + return { + "ok": True, + "attachment": { + "token": token, + "filename": filename, + "size": dest.stat().st_size, + }, + "reply": { + "to": to_addr, + "to_name": from_name, + "subject": subject, + "in_reply_to": in_reply_to, + "references": references, + "account_id": doc.source_email_account_id or None, + "source_uid": doc.source_email_uid, + "source_folder": doc.source_email_folder, + "source_message_id": doc.source_email_message_id, + }, + } + finally: + db.close() + + return router diff --git a/routes/document_helpers.py b/routes/document_helpers.py index a0c2d08eb..c1f68ca51 100644 --- a/routes/document_helpers.py +++ b/routes/document_helpers.py @@ -1,243 +1,14 @@ -"""document_helpers.py — Pydantic models, doc serializers, owner gating, file-locator helpers shared with document_routes.py.""" +"""Backward-compat shim — canonical location is routes/document/document_helpers.py. -"""Document routes — CRUD for living documents with version history.""" +This module is replaced in ``sys.modules`` by the canonical module object so +that ``import routes.document_helpers``, ``from routes.document_helpers import +X``, and the ``sys.modules.pop("routes.document_helpers")`` + re-import +pattern used by test_security_regressions.py all operate on the *same* object. +Keeps existing import paths working after slice 2m (#4082/#4071). +""" -import logging -import os -import re -from typing import Any, Dict, Optional +import sys as _sys -from fastapi import HTTPException, Request -from pydantic import BaseModel +from routes.document import document_helpers as _canonical # noqa: F401 -from core.database import Document, DocumentVersion -from core.database import Session as DbSession -from src.auth_helpers import _auth_disabled -from src.upload_handler import UploadHandler - -logger = logging.getLogger(__name__) - - -# ---- Request schemas ---- - -class DocumentCreate(BaseModel): - session_id: Optional[str] = None - title: str = "Untitled" - language: Optional[str] = None - content: str = "" - -class DocumentUpdate(BaseModel): - content: str - summary: Optional[str] = None - force_version: bool = False - -class DocumentPatch(BaseModel): - title: Optional[str] = None - language: Optional[str] = None - session_id: Optional[str] = None # link/unlink document to a session - - -# ---- Helpers ---- - -def _doc_to_dict(doc: Document) -> Dict[str, Any]: - return { - "id": doc.id, - "session_id": doc.session_id, - "title": doc.title, - "language": doc.language, - "current_content": doc.current_content, - "version_count": doc.version_count, - "is_active": doc.is_active, - "archived": bool(getattr(doc, "archived", False)), - "created_at": (doc.created_at.isoformat() + "Z") if doc.created_at else None, - "updated_at": (doc.updated_at.isoformat() + "Z") if doc.updated_at else None, - # Source-email provenance (set when doc was created from an email - # attachment) — drives the "Send signed reply" menu item. - "source_email_uid": getattr(doc, "source_email_uid", None), - "source_email_folder": getattr(doc, "source_email_folder", None), - "source_email_account_id": getattr(doc, "source_email_account_id", None), - "source_email_message_id": getattr(doc, "source_email_message_id", None), - } - -def _version_to_dict(v: DocumentVersion) -> Dict[str, Any]: - return { - "id": v.id, - "document_id": v.document_id, - "version_number": v.version_number, - "content": v.content, - "summary": v.summary, - "source": v.source, - "created_at": v.created_at.isoformat() if v.created_at else None, - } - - -def _verify_doc_owner(db, doc: Document, user: str): - """Verify `user` owns this document. Raise 404 if not. - - Documents now carry their own `owner` column, so a doc whose session - was deleted (session_id → NULL) can still prove ownership and stay - openable / cloneable. We trust that column first and only fall back to - the session join for any not-yet-backfilled legacy row. - """ - if user is None: - if _auth_disabled(): - return # Single-user / no-auth mode: allow access - raise HTTPException(403, "Authentication required") - if doc.owner is not None: - if doc.owner != user: - raise HTTPException(404, "Document not found") - return - # Legacy fallback: derive ownership from the linked session. - if not doc.session_id: - raise HTTPException(404, "Document not found") - session = db.query(DbSession).filter(DbSession.id == doc.session_id).first() - if not session or session.owner != user: - raise HTTPException(404, "Document not found") - - -def _owner_session_filter(q, user): - """Restrict a documents query to those owned by `user`. - - Documents now carry their own `owner` column (backfilled at boot from - the linked session, or assigned to the admin user for legacy/orphaned - docs). We filter on that directly rather than on a session join, so a - document whose session was deleted (session_id → NULL) still shows up - for its owner instead of silently vanishing from the Library + search. - - The owner backfill runs in init_db before the app serves requests, so - by the time this filter is live there are no NULL-owner rows to leak; - we therefore match the owner strictly for authenticated callers.""" - if not user: - if user == "" or _auth_disabled(): - return q - return q.filter(False) - return q.filter(Document.owner == user) - - - -def _slug(name: str) -> str: - """Filesystem-friendly version of a document title. - - Whitespace becomes underscores; other unsafe punctuation is dropped. - Preserves letters, digits, dot, hyphen, underscore. Idempotent. - """ - import re as _re - s = (name or "").strip() - # Drop the trailing extension if the title happens to include one - s = _re.sub(r'\.pdf$', '', s, flags=_re.IGNORECASE) - s = _re.sub(r'\s+', '_', s) - s = _re.sub(r'[^A-Za-z0-9._-]', '', s) - s = _re.sub(r'_+', '_', s).strip('_') - return s or "form" - - -# DPI scale for the interactive PDF view. ~150 DPI (2x of 72 PDF user-units). -_PDF_RENDER_SCALE = 2.0 - - -def _upload_path_inside(upload_dir: str, path: str) -> bool: - base = os.path.realpath(upload_dir) - p = os.path.realpath(path) - try: - return os.path.commonpath([base, p]) == base - except Exception: - return False - - -def _resolve_user_upload_path( - upload_handler: Any, - upload_id: str, - owner: Optional[str], - auth_manager=None, -) -> Optional[str]: - """Resolve an upload id to a filesystem path the caller may read.""" - if upload_handler is None: - return None - resolved = upload_handler.resolve_upload( - upload_id, - owner=owner, - auth_manager=auth_manager, - ) - if not isinstance(resolved, dict) or not resolved: - return None - path = resolved.get("path") - upload_dir = getattr(upload_handler, "upload_dir", None) - if path and upload_dir and not _upload_path_inside(upload_dir, path): - logger.warning("Upload path outside upload directory: %s", path) - return None - return path - - -def _locate_upload( - upload_dir: str, - file_id: str, - owner: Optional[str] = None, - auth_manager=None, - upload_handler: Any = None, -): - """Find an upload by its filename ID via UploadHandler.resolve_upload.""" - if upload_handler is None: - from src.upload_handler import UploadHandler - - base_dir = os.path.dirname(os.path.abspath(upload_dir)) - upload_handler = UploadHandler(base_dir, upload_dir) - return _resolve_user_upload_path(upload_handler, file_id, owner, auth_manager) - - -def _assert_pdf_marker_upload_owned( - request: Request, - content: str, - user: Optional[str], - upload_handler: Any, -) -> None: - """Reject document content whose pdf_source marker points at another user's upload.""" - if upload_handler is None: - return - from src.pdf_form_doc import find_source_upload_id - - upload_id = find_source_upload_id(content or "") - if not upload_id: - return - auth_manager = getattr(getattr(request.app, "state", None), "auth_manager", None) - if not _resolve_user_upload_path(upload_handler, upload_id, user, auth_manager): - raise HTTPException( - 400, - "Document PDF marker references an upload you do not own", - ) - - -def _derive_title(content: str) -> str: - """Derive a title from document content.""" - import re - if not isinstance(content, str): - return "Untitled" - text = content.strip() - if not text: - return "Untitled" - - # Markdown header - md = re.match(r'^#{1,3}\s+(.+)', text, re.MULTILINE) - if md: - title = md.group(1).strip() - if len(title) > 50: - title = title[:48] + "…" - return title - - # HTML heading - html = re.search(r'<h[1-3][^>]*>([^<]+)</h[1-3]>', text, re.IGNORECASE) - if html: - title = html.group(1).strip() - if len(title) > 50: - title = title[:48] + "…" - return title - - # First non-empty line (if short enough) - for line in text.split('\n'): - line = line.strip() - if line and 2 <= len(line) <= 60: - title = re.sub(r'[:#*`]+$', '', line).strip() - if title and len(title) > 50: - title = title[:48] + "…" - return title or "Untitled" - - return "Untitled" +_sys.modules[__name__] = _canonical diff --git a/routes/document_routes.py b/routes/document_routes.py index dae8b09fa..dd13e3c60 100644 --- a/routes/document_routes.py +++ b/routes/document_routes.py @@ -1,1810 +1,17 @@ -"""Document routes — CRUD for living documents with version history.""" +"""Backward-compat shim — canonical location is routes/document/document_routes.py. -import uuid -import logging -from datetime import datetime, timezone -from typing import Dict, Any, List, Optional +This module is replaced in ``sys.modules`` by the canonical module object so +that ``import routes.document_routes``, ``from routes.document_routes import +X``, ``importlib.import_module("routes.document_routes")``, and the +``import ... as droutes`` + ``droutes.SessionLocal = ...`` / +``monkeypatch.setattr(droutes, ...)`` pattern used by multiple tests all +operate on the *same* object the application actually uses. Keeps existing +import paths working after slice 2m (#4082/#4071). Source-introspection tests +read the canonical file by path. +""" -from fastapi import APIRouter, HTTPException, Query, Request, UploadFile, File, Form +import sys as _sys -from sqlalchemy import case, func, or_ -from core.database import SessionLocal, Document, DocumentVersion -from core.database import Session as DbSession -from src.auth_helpers import get_current_user, _auth_disabled -from src.constants import MAIL_ATTACHMENTS_DIR -from src.upload_handler import reserve_upload_references +from routes.document import document_routes as _canonical # noqa: F401 -logger = logging.getLogger(__name__) - - -def _get_session_or_404(db, session_id: str, user: Optional[str]): - session = db.query(DbSession).filter(DbSession.id == session_id).first() - if not session: - raise HTTPException(404, "Session not found") - if user and session.owner != user: - raise HTTPException(404, "Session not found") - return session - - -def _aggregate_language_facets(lang_rows): - """Sum document counts per display language for the library facet. - - NULL-language and explicit "text" rows share the "text" bucket (the - language filter treats them as one), so they must be ADDED. The old dict - comprehension keyed both to "text", silently overwriting one group and - undercounting the facet versus what the filter actually returns. - """ - out = {} - for lang, cnt in lang_rows: - key = lang or "text" - out[key] = out.get(key, 0) + cnt - return out - - -def _library_language_for_document(doc: Document) -> str: - """Return the display language used by the document library. - - PDF documents are stored as markdown wrappers so the editor can preserve - extracted text, form fields, and annotations. The library should still - identify them as PDFs instead of exposing that internal wrapper format. - """ - from src.pdf_form_doc import find_source_upload_id - - if find_source_upload_id(doc.current_content or ""): - return "pdf" - return doc.language or "text" - - -def _email_source_key(content: str) -> tuple[str, str]: - """Return the source email identity embedded in an email draft document.""" - import re - - text = content or "" - uid_m = re.search(r"(?im)^X-Source-UID:\s*(.+?)\s*$", text) - folder_m = re.search(r"(?im)^X-Source-Folder:\s*(.+?)\s*$", text) - uid = (uid_m.group(1).strip() if uid_m else "") - folder = (folder_m.group(1).strip() if folder_m else "INBOX") - return uid, folder - - -from routes.document_helpers import ( - DocumentCreate, DocumentUpdate, DocumentPatch, - _doc_to_dict, _version_to_dict, - _verify_doc_owner, _owner_session_filter, - _slug, _resolve_user_upload_path, _assert_pdf_marker_upload_owned, _derive_title, - _PDF_RENDER_SCALE, -) - - -def setup_document_routes(session_manager, upload_handler=None) -> APIRouter: - router = APIRouter(tags=["documents"]) - - def _reserve_document_uploads(user: Optional[str], content: str) -> None: - missing_id = reserve_upload_references(upload_handler, user, content) - if missing_id: - raise HTTPException( - 409, - f"Referenced upload is no longer available: {missing_id}", - ) - - def _locate_current_user_upload(request: Request, upload_id: str, user: Optional[str]): - if upload_handler is None: - return None - auth_manager = getattr(getattr(request.app, "state", None), "auth_manager", None) - return _resolve_user_upload_path(upload_handler, upload_id, user, auth_manager) - - def _load_pdf_viewer_fitz(): - from src.pdf_runtime import load_pymupdf_for_pdf_viewer - - try: - return load_pymupdf_for_pdf_viewer() - except RuntimeError as exc: - raise HTTPException(503, str(exc)) from exc - - # ---- POST /api/document ---- - @router.post("/api/document") - async def create_document(request: Request, req: DocumentCreate) -> Dict[str, Any]: - from src.auth_helpers import require_privilege - user = require_privilege(request, "can_use_documents") - db = SessionLocal() - try: - # session_id is optional: a doc can be a session-less "library" doc - # (e.g. files imported from the library) — session_id is nullable and - # the doc is owner-stamped, so it lives in the library on its own. - session = None - if req.session_id: - # Match the lenient ownership model the rest of the app uses - # (see _owner_filter): only block when an AUTHENTICATED user is - # writing into a DIFFERENT user's session. In single-user / - # unconfigured / localhost-bypass mode, falsey users preserve - # the existing lenient path. - session = _get_session_or_404(db, req.session_id, user) - - # If no language was supplied (e.g. cloning a doc whose language - # was never set), detect it from the content rather than storing - # NULL — which made the editor fall back to plain text. Defaults - # to markdown for prose. - language = req.language - if not language: - from src.agent_tools.document_tools import _looks_like_email_document, _sniff_doc_language, _coerce_email_document_content - language = _sniff_doc_language(req.content) - else: - from src.agent_tools.document_tools import _looks_like_email_document, _coerce_email_document_content - if _looks_like_email_document(req.content, req.title): - language = "email" - - _reserve_document_uploads(user, req.content) - _assert_pdf_marker_upload_owned(request, req.content, user, upload_handler) - - # Reply drafts are keyed to the source email. If a UI/tool path tries - # to create a second draft for the same email in the same chat, - # update the existing draft instead so quoted thread history stays - # attached to the visible document. - if language == "email" and req.session_id: - source_uid, source_folder = _email_source_key(req.content) - if source_uid: - candidates = ( - db.query(Document) - .filter(Document.session_id == req.session_id) - .filter(Document.is_active == True) - .filter(Document.language == "email") - .order_by(Document.updated_at.desc()) - .limit(25) - .all() - ) - for existing in candidates: - old_uid, old_folder = _email_source_key(existing.current_content or "") - if old_uid != source_uid or old_folder != source_folder: - continue - merged = _coerce_email_document_content(existing.current_content or "", req.content) - if existing.current_content != merged: - new_ver = (existing.version_count or 1) + 1 - existing.current_content = merged - existing.title = req.title or existing.title - existing.version_count = new_ver - db.add(DocumentVersion( - id=str(uuid.uuid4()), - document_id=existing.id, - version_number=new_ver, - content=merged, - summary="Updated existing email draft", - source="user", - )) - db.commit() - db.refresh(existing) - return _doc_to_dict(existing) - - doc_id = str(uuid.uuid4()) - ver_id = str(uuid.uuid4()) - - doc = Document( - id=doc_id, - session_id=req.session_id, - title=req.title, - language=language, - current_content=req.content, - version_count=1, - is_active=True, - # Stamp ownership directly so the doc survives its session - # being deleted. Fall back to the session's owner when the - # request is unauthenticated (single-user / localhost bypass). - owner=user or (session.owner if session else None), - ) - ver = DocumentVersion( - id=ver_id, - document_id=doc_id, - version_number=1, - content=req.content, - summary="Initial version", - source="user", - ) - db.add(doc) - db.add(ver) - db.commit() - db.refresh(doc) - try: - from src.event_bus import fire_event - fire_event("document_created", doc.owner) - except Exception: - logger.debug("document_created event dispatch failed", exc_info=True) - return _doc_to_dict(doc) - except HTTPException: - raise - except Exception as e: - db.rollback() - logger.error(f"Failed to create document: {e}") - raise HTTPException(500, f"Failed to create document: {e}") - finally: - db.close() - - # ---- POST /api/documents/import-pdf ---- - @router.post("/api/documents/import-pdf") - async def import_pdf( - request: Request, - file: UploadFile = File(...), - session_id: Optional[str] = Form(None), - ) -> Dict[str, Any]: - """Upload a PDF and create the matching Document. - - Detects AcroForm fields — if any, creates a form-backed markdown doc - (clickable inputs in the PDF view). Otherwise creates a plain PDF doc - with a `pdf_source` marker so the viewer renders the pages without - overlays. - """ - from src.pdf_forms import has_form_fields, extract_fields - from src.pdf_form_doc import ( - save_field_sidecar, - create_form_markdown_document, - create_plain_pdf_document, - ) - from src.document_processor import _process_pdf, strip_pdf_content_marker - import os - - from src.auth_helpers import require_privilege - user = require_privilege(request, "can_use_documents") - - # session_id is optional — a library import isn't tied to a chat. When - # given, validate it; otherwise the PDF becomes a session-less library - # doc (the doc creators below already handle a missing session). - if session_id: - db = SessionLocal() - try: - _get_session_or_404(db, session_id, user) - finally: - db.close() - - if upload_handler is None: - raise HTTPException(500, "Upload handler not configured") - - client_ip = request.client.host if request.client else "unknown" - try: - meta = upload_handler.save_upload(file, client_ip, owner=user) - except HTTPException: - raise - except Exception as e: - logger.error(f"PDF import save_upload failed: {e}") - raise HTTPException(500, f"Upload failed: {e}") - - upload_id = meta["id"] - pdf_path = _locate_current_user_upload(request, upload_id, user) - if not pdf_path: - raise HTTPException(500, "Saved PDF could not be located") - - title = os.path.splitext(meta.get("original_name") or meta.get("name") or upload_id)[0] - try: - body_text = strip_pdf_content_marker(_process_pdf(pdf_path, owner=user)) - except Exception: - body_text = None - - is_form = False - try: - is_form = has_form_fields(pdf_path) - except Exception as e: - logger.warning(f"has_form_fields failed for {pdf_path}: {e}") - - if is_form: - fields = extract_fields(pdf_path) - save_field_sidecar(pdf_path, fields) - doc_id = create_form_markdown_document( - session_id=session_id, - fields=fields, - upload_id=upload_id, - title=title, - intro_text=body_text, - ) - else: - doc_id = create_plain_pdf_document( - session_id=session_id, - upload_id=upload_id, - title=title, - body_text=body_text, - ) - - if not doc_id: - raise HTTPException(500, "Failed to create document for PDF") - - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(500, "Created document not found") - # The PDF doc creators stamp owner from the session only; a - # session-less library import leaves owner NULL, which the Library's - # owner filter then hides. Stamp the requesting user so it shows. - if not doc.owner and user: - doc.owner = user - db.commit() - db.refresh(doc) - return _doc_to_dict(doc) - finally: - db.close() - - # ---- GET /api/documents/library ---- - @router.get("/api/documents/library") - async def documents_library( - request: Request, - search: Optional[str] = Query(None), - language: Optional[str] = Query(None), - sort: str = Query("recent"), - offset: int = Query(0, ge=0), - limit: int = Query(20, ge=1, le=50), - archived: bool = Query(False), - ) -> Dict[str, Any]: - user = get_current_user(request) - db = SessionLocal() - try: - from sqlalchemy import or_ - pdf_marker_cond = or_( - Document.current_content.like('%<!-- pdf_source upload_id="%'), - Document.current_content.like('%<!-- pdf_form_source upload_id="%'), - ) - library_language_expr = case( - (pdf_marker_cond, "pdf"), - (Document.language.is_(None), "text"), - else_=Document.language, - ) - # Archived view shows ONLY archived docs; the default view excludes - # them (NULL = legacy rows that predate the column = not archived). - _arch_cond = (Document.archived == True) if archived else or_( - Document.archived == False, Document.archived.is_(None)) - # Language facet counts (owner-filtered). PDF documents are stored - # as markdown wrappers, so group by the library display language - # instead of the raw stored language. - lang_q = ( - db.query(library_language_expr, func.count(Document.id)) - .outerjoin(DbSession, Document.session_id == DbSession.id) - .filter(Document.is_active == True).filter(_arch_cond) - ) - lang_q = _owner_session_filter(lang_q, user) - lang_rows = lang_q.group_by(library_language_expr).all() - languages = _aggregate_language_facets(lang_rows) - - # Session count (owner-filtered) - sc_q = ( - db.query(func.count(func.distinct(Document.session_id))) - .outerjoin(DbSession, Document.session_id == DbSession.id) - .filter(Document.is_active == True).filter(_arch_cond) - ) - sc_q = _owner_session_filter(sc_q, user) - session_count = sc_q.scalar() - - # Base query - q = ( - db.query(Document, DbSession.name) - .outerjoin(DbSession, Document.session_id == DbSession.id) - .filter(Document.is_active == True).filter(_arch_cond) - ) - q = _owner_session_filter(q, user) - - # Search filter — split on whitespace and require EACH term to - # match (title OR content). A single `%foo bar%` LIKE only matched - # the exact adjacent phrase, so any multi-word query with a space - # silently returned nothing. Per-term AND makes "machine learning" - # match docs containing both words regardless of position/order. - if search: - for tok in search.split(): - term = f"%{tok}%" - q = q.filter( - Document.title.ilike(term) | Document.current_content.ilike(term) - ) - - # Language filter. "pdf" is a display language derived from the - # source marker; "markdown" excludes those wrappers. - if language: - if language == "text": - q = q.filter((Document.language == None) | (Document.language == "text")) - elif language == "pdf": - q = q.filter(pdf_marker_cond) - else: - q = q.filter(Document.language == language) - if language == "markdown": - q = q.filter(~pdf_marker_cond) - - # Total before pagination - total = q.count() - - # Sorting - if sort == "oldest": - q = q.order_by(Document.created_at.asc()) - elif sort == "edits": - q = q.order_by(Document.version_count.desc()) - elif sort == "alpha": - q = q.order_by(Document.title.asc()) - else: # recent - q = q.order_by(Document.updated_at.desc()) - - rows = q.offset(offset).limit(limit).all() - - documents = [] - for doc, session_name in rows: - documents.append({ - "id": doc.id, - "session_id": doc.session_id, - "session_name": session_name, - "title": doc.title, - "language": _library_language_for_document(doc), - "preview": (doc.current_content or "")[:500], - "version_count": doc.version_count, - "created_at": (doc.created_at.isoformat() + "Z") if doc.created_at else None, - "updated_at": (doc.updated_at.isoformat() + "Z") if doc.updated_at else None, - }) - - return { - "documents": documents, - "total": total, - "languages": languages, - "session_count": session_count, - } - except Exception as e: - logger.error(f"Failed to fetch document library: {e}") - raise HTTPException(500, f"Failed to fetch document library: {e}") - finally: - db.close() - - # ---- GET /api/documents/{session_id} ---- - @router.get("/api/documents/{session_id}") - async def list_documents(request: Request, session_id: str) -> List[Dict[str, Any]]: - user = get_current_user(request) - db = SessionLocal() - try: - if not user: - if not _auth_disabled(): - raise HTTPException(403, "Authentication required") - # v2 review HIGH-9: raise 403 explicitly when the caller - # can't see this session, instead of returning [] which the - # UI treats identically to "no docs" and silently masks - # auth failures. - _get_session_or_404(db, session_id, user) - q = db.query(Document).filter( - Document.session_id == session_id - ) - if user: - q = q.filter(or_(Document.owner == user, Document.owner.is_(None))) - docs = q.order_by(Document.created_at.desc()).all() - return [_doc_to_dict(d) for d in docs] - finally: - db.close() - - # ---- GET /api/document/{doc_id} ---- - @router.get("/api/document/{doc_id}") - async def get_document(request: Request, doc_id: str) -> Dict[str, Any]: - user = get_current_user(request) - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - return _doc_to_dict(doc) - finally: - db.close() - - # ---- POST /api/document/{doc_id}/archive — soft-archive / restore ---- - @router.post("/api/document/{doc_id}/archive") - async def archive_document(request: Request, doc_id: str, archived: bool = Query(True)) -> Dict[str, Any]: - user = get_current_user(request) - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - doc.archived = bool(archived) - db.commit() - return {"ok": True, "id": doc_id, "archived": doc.archived} - finally: - db.close() - - # ---- POST /api/document/{doc_id}/extract-pdf-text ---- - @router.post("/api/document/{doc_id}/extract-pdf-text") - async def extract_pdf_text(request: Request, doc_id: str) -> Dict[str, Any]: - """Re-run pypdf+VL text extraction against the PDF linked to this doc - and merge the result into the doc's markdown content. Idempotent — the - existing body (everything below the title heading) is replaced. - - Lets the AI see PDF contents for old docs that were imported before - text extraction was wired, plus for scanned/image-only PDFs where the - VL model picks up text the basic pypdf path missed.""" - import re - from src.document_processor import _process_pdf, strip_pdf_content_marker - from src.pdf_form_doc import find_source_upload_id - - user = get_current_user(request) - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - - content = doc.current_content or "" - upload_id = find_source_upload_id(content) - if not upload_id: - raise HTTPException(400, "Document is not a PDF — no pdf_source marker found") - - pdf_path = _locate_current_user_upload(request, upload_id, user) - if not pdf_path: - raise HTTPException(404, "Source PDF could not be located") - - try: - body_text = strip_pdf_content_marker(_process_pdf(pdf_path, owner=user)) - except Exception as e: - logger.error(f"extract_pdf_text failed for {pdf_path}: {e}") - raise HTTPException(500, f"Extraction failed: {e}") - - if not body_text: - return {"ok": True, "id": doc_id, "extracted": False, "reason": "No readable content"} - - # Preserve everything up through the title (front-matter marker + - # first H1) and replace the rest with the freshly extracted text. - head_re = re.compile(r'^(<!--[^>]+-->\s*\n+#[^\n]*\n+)', re.MULTILINE) - head_match = head_re.match(content) - head = head_match.group(1) if head_match else (content.splitlines()[0] + "\n\n# " + (doc.title or "PDF") + "\n\n") - doc.current_content = head + body_text.strip() + "\n" - doc.version_count = (doc.version_count or 1) + 1 - db.add(DocumentVersion( - id=str(__import__("uuid").uuid4()), - document_id=doc_id, - version_number=doc.version_count, - content=doc.current_content, - summary="PDF text re-extracted (OCR)", - source="ocr", - )) - db.commit() - return {"ok": True, "id": doc_id, "extracted": True, "chars": len(body_text)} - finally: - db.close() - - # ---- POST /api/documents/export-zip — bundle selected docs into a .zip ---- - @router.post("/api/documents/export-zip") - async def documents_export_zip(request: Request): - """Zip the selected documents (each as a text file with the right - extension) — mirrors the gallery's bulk download-zip so multi-export - is one file instead of a blocked flood of individual downloads.""" - user = get_current_user(request) - try: - data = await request.json() - except Exception as e: - logger.warning("Failed to parse export request body, defaulting to empty", exc_info=e) - data = {} - ids = data.get("ids") or [] - if not ids: - raise HTTPException(400, "No documents specified") - _ext = { - "javascript": ".js", "python": ".py", "html": ".html", "css": ".css", - "markdown": ".md", "json": ".json", "yaml": ".yml", "bash": ".sh", - "sql": ".sql", "rust": ".rs", "go": ".go", "java": ".java", "c": ".c", - "cpp": ".cpp", "typescript": ".ts", "ruby": ".rb", "php": ".php", - "text": ".txt", "xml": ".xml", "toml": ".toml", "ini": ".ini", - } - db = SessionLocal() - try: - import io - import re - import zipfile - from fastapi import Response - docs = db.query(Document).filter(Document.id.in_(ids)).all() - buf = io.BytesIO() - used = set() - wrote = 0 - with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: - for doc in docs: - try: - _verify_doc_owner(db, doc, user) - except HTTPException: - continue # skip docs the user doesn't own - ext = _ext.get(doc.language or "text", ".txt") - base = (doc.title or "document").strip() or "document" - base = re.sub(r"[^\w\-. ]+", "", base)[:60].strip() or doc.id - name = base if "." in base else base + ext - i = 1 - while name in used: - name = f"{base}-{i}" + ("" if "." in base else ext) - i += 1 - used.add(name) - zf.writestr(name, doc.current_content or "") - wrote += 1 - if not wrote: - raise HTTPException(404, "No documents found") - return Response( - content=buf.getvalue(), - media_type="application/zip", - headers={"Content-Disposition": 'attachment; filename="documents.zip"'}, - ) - finally: - db.close() - - # ---- PUT /api/document/{doc_id} — user manual edit ---- - # Coalesce window: if the last user version was saved within this many - # seconds, update it in-place (user is still actively editing). - # Once the gap exceeds this, the next save creates a new version. - VERSION_COALESCE_SECONDS = 60 - - @router.put("/api/document/{doc_id}") - async def update_document(request: Request, doc_id: str, req: DocumentUpdate) -> Dict[str, Any]: - user = get_current_user(request) - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - - incoming_content = req.content - from src.agent_tools.document_tools import _coerce_email_document_content, _looks_like_email_document - is_email_doc = ( - (doc.language or "").lower() == "email" - or _looks_like_email_document(doc.current_content or "", doc.title or "") - or _looks_like_email_document(req.content or "", doc.title or "") - ) - if is_email_doc: - incoming_content = _coerce_email_document_content(doc.current_content or "", req.content) - doc.language = "email" - - # Skip if content is identical unless the caller explicitly wants - # a checkpoint version from the current editor state. - if doc.current_content == incoming_content and not req.force_version: - return _doc_to_dict(doc) - - _reserve_document_uploads(user, incoming_content) - _assert_pdf_marker_upload_owned(request, incoming_content, user, upload_handler) - - # Check if we can coalesce with the latest version - latest_ver = db.query(DocumentVersion).filter( - DocumentVersion.document_id == doc_id, - ).order_by(DocumentVersion.version_number.desc()).first() - - now = datetime.now(timezone.utc) - coalesced = False - if latest_ver and latest_ver.source == "user" and not req.force_version: - ver_time = latest_ver.created_at - if ver_time.tzinfo is None: - ver_time = ver_time.replace(tzinfo=timezone.utc) - age = (now - ver_time).total_seconds() - if age < VERSION_COALESCE_SECONDS: - # Update the existing version in-place - latest_ver.content = incoming_content - latest_ver.created_at = now - if req.summary: - latest_ver.summary = req.summary - coalesced = True - - if not coalesced: - new_ver = doc.version_count + 1 - ver = DocumentVersion( - id=str(uuid.uuid4()), - document_id=doc_id, - version_number=new_ver, - content=incoming_content, - summary=req.summary or "Manual edit", - source="user", - ) - doc.version_count = new_ver - db.add(ver) - - doc.current_content = incoming_content - db.commit() - db.refresh(doc) - return _doc_to_dict(doc) - except HTTPException: - raise - except Exception as e: - db.rollback() - raise HTTPException(500, f"Failed to update document: {e}") - finally: - db.close() - - # ---- PATCH /api/document/{doc_id} — metadata only ---- - @router.patch("/api/document/{doc_id}") - async def patch_document(request: Request, doc_id: str, req: DocumentPatch) -> Dict[str, Any]: - user = get_current_user(request) - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - if req.title is not None: - doc.title = req.title - if req.language is not None: - doc.language = req.language - if req.session_id is not None: - # Empty string = unlink from session - if req.session_id: - _get_session_or_404(db, req.session_id, user) - doc.session_id = req.session_id if req.session_id else None - if not req.session_id: - # Tab closed / doc detached from its session — drop the - # in-memory active-doc pointer so the last-resort injection - # path doesn't re-surface this doc in a later chat (#1160). - try: - from src.agent_tools.document_tools import clear_active_document - clear_active_document(doc_id) - except Exception as e: - logger.warning("Failed to clear active document %r on detach", doc_id, exc_info=e) - db.commit() - db.refresh(doc) - return _doc_to_dict(doc) - except HTTPException: - raise - except Exception as e: - db.rollback() - raise HTTPException(500, str(e)) - finally: - db.close() - - # ---- DELETE /api/document/{doc_id} — soft delete ---- - @router.delete("/api/document/{doc_id}") - async def delete_document(request: Request, doc_id: str) -> Dict[str, str]: - user = get_current_user(request) - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - doc.is_active = False - # Closed/deleted — drop the in-memory active-doc pointer so it isn't - # re-injected into a later, unrelated chat (#1160). - try: - from src.agent_tools.document_tools import clear_active_document - clear_active_document(doc_id) - except Exception: - pass - db.commit() - return {"status": "deleted", "id": doc_id} - except HTTPException: - raise - except Exception as e: - db.rollback() - raise HTTPException(500, str(e)) - finally: - db.close() - - # ---- GET /api/document/{doc_id}/versions ---- - @router.get("/api/document/{doc_id}/versions") - async def list_versions(request: Request, doc_id: str) -> List[Dict[str, Any]]: - user = get_current_user(request) - db = SessionLocal() - try: - # Verify ownership before listing versions - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - versions = db.query(DocumentVersion).filter( - DocumentVersion.document_id == doc_id - ).order_by(DocumentVersion.version_number.desc()).all() - return [{ - "id": v.id, - "version_number": v.version_number, - "content": v.content, - "summary": v.summary, - "source": v.source, - "created_at": v.created_at.isoformat() if v.created_at else None, - } for v in versions] - finally: - db.close() - - # ---- GET /api/document/{doc_id}/version/{num} ---- - @router.get("/api/document/{doc_id}/version/{num}") - async def get_version(request: Request, doc_id: str, num: int) -> Dict[str, Any]: - user = get_current_user(request) - db = SessionLocal() - try: - # Verify ownership - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - ver = db.query(DocumentVersion).filter( - DocumentVersion.document_id == doc_id, - DocumentVersion.version_number == num, - ).first() - if not ver: - raise HTTPException(404, "Version not found") - return _version_to_dict(ver) - finally: - db.close() - - # ---- POST /api/document/{doc_id}/restore/{num} ---- - @router.post("/api/document/{doc_id}/restore/{num}") - async def restore_version(request: Request, doc_id: str, num: int) -> Dict[str, Any]: - user = get_current_user(request) - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - - old_ver = db.query(DocumentVersion).filter( - DocumentVersion.document_id == doc_id, - DocumentVersion.version_number == num, - ).first() - if not old_ver: - raise HTTPException(404, "Version not found") - - new_ver_num = doc.version_count + 1 - ver = DocumentVersion( - id=str(uuid.uuid4()), - document_id=doc_id, - version_number=new_ver_num, - content=old_ver.content, - summary=f"Restored from v{num}", - source="user", - ) - doc.current_content = old_ver.content - doc.version_count = new_ver_num - db.add(ver) - db.commit() - db.refresh(doc) - return _doc_to_dict(doc) - except HTTPException: - raise - except Exception as e: - db.rollback() - raise HTTPException(500, str(e)) - finally: - db.close() - - # ---- POST /api/documents/tidy — clean up broken/empty documents ---- - @router.post("/api/documents/tidy") - async def tidy_documents(request: Request) -> Dict[str, Any]: - """Fix empty titles and remove broken/empty documents (user's docs only).""" - user = get_current_user(request) - db = SessionLocal() - try: - q = ( - db.query(Document) - .outerjoin(DbSession, Document.session_id == DbSession.id) - .filter(Document.is_active == True) - .filter((Document.archived == False) | (Document.archived.is_(None))) - ) - q = _owner_session_filter(q, user) - docs = q.all() - fixed_titles = 0 - deleted = 0 - - # Same junk-detection logic as the scheduled tidy_documents - # action (src/document_actions.py). Keep these two in sync. - import re as _re - from src.document_actions import _JUNK_TITLES - - to_delete = [] - now = datetime.now(timezone.utc) - for doc in docs: - created = doc.created_at - if created and created.tzinfo is None: - created = created.replace(tzinfo=timezone.utc) - - # Skip freshly created documents to avoid deleting them while the user is actively editing - if created and (now - created).total_seconds() < 900: # 15 minutes - continue - - content = (doc.current_content or "").strip() - title_raw = (doc.title or "").strip() - title = title_raw.lower() - is_fresh_empty = ( - not content - and created is not None - and (now - created).total_seconds() < 1800 - ) - if is_fresh_empty: - continue - - # Strip markdown noise to get a "real" character count - stripped = _re.sub(r"^#{1,6}\s+", "", content, flags=_re.MULTILINE) - stripped = _re.sub(r"[*_`>\-=]+", "", stripped) - stripped = _re.sub(r"\s+", " ", stripped).strip() - real_len = len(stripped) - - # Detect email-scaffold stubs: "To: \nSubject: \n---\n" style - # bodies with nothing typed in. Stub = every meaningful line - # is a header label (To:/From:/Subject:/...) with no real - # value (blank, "empty", "(empty)", "-", "none", "n/a"). - _is_email_stub = False - _HEADER_RE = _re.compile(r"^(to|from|cc|bcc|subject|reply-to):\s*(.*)$", _re.I) - _PLACEHOLDER_VALS = {"", "empty", "(empty)", "-", "—", "none", "n/a", "na", "tbd"} - if title in ("new email", "new mail", "new message") or doc.language == "email": - body_lines = [ln.strip() for ln in content.split("\n") - if ln.strip() and ln.strip() != "---"] - def _is_filler(ln): - m = _HEADER_RE.match(ln) - if not m: - return False - val = (m.group(2) or "").strip().lower() - return val in _PLACEHOLDER_VALS - has_real_body = any(not _is_filler(ln) for ln in body_lines) - if body_lines and not has_real_body: - _is_email_stub = True - - # Hard-delete obviously empty / junk documents - if not content or content in ("", "# Untitled"): - to_delete.append(doc); deleted += 1; continue - if _is_email_stub: - to_delete.append(doc); deleted += 1; continue - if title in _JUNK_TITLES: - to_delete.append(doc); deleted += 1; continue - - # Fix empty or placeholder titles on survivors - if not title_raw or title_raw == "Untitled": - new_title = _derive_title(content) - if new_title and new_title != "Untitled": - doc.title = new_title - fixed_titles += 1 - - for doc in to_delete: - db.delete(doc) - - # Also clean up inactive empty docs from previous soft-deletes - inactive_q = ( - db.query(Document) - .outerjoin(DbSession, Document.session_id == DbSession.id) - .filter(Document.is_active == False) - .filter((Document.current_content == None) | (Document.current_content == "")) - ) - inactive_q = _owner_session_filter(inactive_q, user) - inactive_docs = inactive_q.all() - for doc in inactive_docs: - db.delete(doc) - deleted += len(inactive_docs) - - db.commit() - return { - "fixed_titles": fixed_titles, - "deleted": deleted, - "message": f"Fixed {fixed_titles} title{'s' if fixed_titles != 1 else ''}, removed {deleted} empty document{'s' if deleted != 1 else ''}", - } - except Exception as e: - db.rollback() - logger.error(f"Document tidy failed: {e}") - raise HTTPException(500, f"Tidy failed: {e}") - finally: - db.close() - - # ---- POST /api/documents/ai-tidy — AI-powered cleanup of junk/test documents ---- - @router.post("/api/documents/ai-tidy") - async def ai_tidy_documents(request: Request) -> Dict[str, Any]: - """Use AI to judge if documents are junk/test/accidental, then delete them. - Caches verdicts so previously-reviewed docs are skipped.""" - from src.task_endpoint import resolve_task_endpoint - from src.endpoint_resolver import resolve_endpoint - from src.llm_core import llm_call_async - - user = get_current_user(request) - url, model, headers = resolve_task_endpoint(owner=user or None) - if not url or not model: - # Fall back to default endpoint - url, model, headers = resolve_endpoint("default", owner=user or None) - if not url or not model: - raise HTTPException(500, "No endpoint configured for AI tidy") - - db = SessionLocal() - try: - q = ( - db.query(Document) - .outerjoin(DbSession, Document.session_id == DbSession.id) - .filter(Document.is_active == True) - .filter((Document.archived == False) | (Document.archived.is_(None))) - ) - q = _owner_session_filter(q, user) - docs = q.all() - - # Only review docs that haven't been reviewed yet - to_review = [d for d in docs if not d.tidy_verdict] - if not to_review: - return {"deleted": 0, "reviewed": 0, "message": "All documents already reviewed"} - - # Build a batch prompt — review up to 30 at a time - batch = to_review[:30] - doc_list = [] - for i, doc in enumerate(batch): - preview = (doc.current_content or "")[:300].strip() - doc_list.append(f"[{i}] title=\"{doc.title}\" lang={doc.language or 'text'} content_preview=\"{preview}\"") - - prompt = ( - "You are a document library cleaner. For each document below, decide if it is JUNK " - "(test, accidental, placeholder, empty-ish, tool-test, throwaway) or KEEP (real content worth saving).\n\n" - "Respond with ONLY a JSON array of verdicts, one per document, like: [\"junk\",\"keep\",\"junk\",...]\n" - "No explanation, no markdown, just the JSON array.\n\n" - + "\n".join(doc_list) - ) - - response = await llm_call_async( - url, model, - [{"role": "system", "content": "You classify documents as junk or keep. Respond only with a JSON array."}, - {"role": "user", "content": prompt}], - temperature=0.1, - max_tokens=200, - headers=headers, - timeout=30, - ) - - # Parse verdicts - import re - match = re.search(r'\[.*?\]', response, re.DOTALL) - if not match: - raise HTTPException(500, "AI returned invalid response") - - import json as _json - verdicts = _json.loads(match.group()) - - deleted = 0 - reviewed = 0 - for i, doc in enumerate(batch): - if i >= len(verdicts): - break - verdict = str(verdicts[i] or "").lower().strip() - if verdict == "junk": - doc.tidy_verdict = "junk" - db.delete(doc) - deleted += 1 - else: - doc.tidy_verdict = "keep" - reviewed += 1 - - db.commit() - return { - "deleted": deleted, - "reviewed": reviewed, - "remaining": len(to_review) - len(batch), - "message": f"Reviewed {reviewed}, removed {deleted} junk document{'s' if deleted != 1 else ''}", - } - except HTTPException: - raise - except Exception as e: - db.rollback() - logger.error(f"AI tidy failed: {e}") - raise HTTPException(500, f"AI tidy failed: {e}") - finally: - db.close() - - # ---- POST /api/document/{doc_id}/export-pdf/preview ---- - @router.post("/api/document/{doc_id}/export-pdf/preview") - async def export_pdf_preview(doc_id: str, request: Request) -> Dict[str, Any]: - """Return the field-value mapping that would be written to the PDF. - - Frontend shows this in a confirmation modal so the user can spot/fix - any wrong values before triggering the actual download. - """ - from src.pdf_form_doc import find_source_upload_id, parse_markdown_to_values, load_field_sidecar - - user = get_current_user(request) - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - - upload_id = find_source_upload_id(doc.current_content or "") - if not upload_id: - raise HTTPException(400, "Document is not linked to a source PDF") - - pdf_path = _locate_current_user_upload(request, upload_id, user) - if not pdf_path: - raise HTTPException(404, f"Source PDF {upload_id} not found in uploads") - - fields = load_field_sidecar(pdf_path) - if not fields: - raise HTTPException(404, "Field schema sidecar missing for source PDF") - - values = parse_markdown_to_values(doc.current_content or "") - field_meta = {f["name"]: f for f in fields} - - preview = [] - for name, current in values.items(): - meta = field_meta.get(name) - if not meta: - continue - preview.append({ - "name": name, - "label": meta.get("label") or name, - "type": meta.get("type"), - "options": meta.get("options") or [], - "page": meta.get("page"), - "value": current, - }) - - unknown = [ - name for name in values - if name not in field_meta - ] - return { - "doc_id": doc_id, - "upload_id": upload_id, - "fields": preview, - "unknown_fields": unknown, - "total": len(fields), - "filled": sum(1 for p in preview if p["value"] not in ("", False, None)), - } - finally: - db.close() - - # ---- GET /api/document/{doc_id}/render-pages ---- - @router.get("/api/document/{doc_id}/render-pages") - async def render_pages(doc_id: str, request: Request) -> Dict[str, Any]: - """Return per-page metadata for the interactive PDF view. - - Each page entry has its rendered-image dimensions (matching what - /page/{n}.png returns at the same DPI) plus the list of form fields - on that page with their rects translated to image-pixel coordinates. - Frontend overlays HTML form controls at those positions. - """ - from src.pdf_form_doc import find_source_upload_id, parse_markdown_to_values, load_field_sidecar - - user = get_current_user(request) - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - upload_id = find_source_upload_id(doc.current_content or "") - if not upload_id: - raise HTTPException(400, "Document is not linked to a source PDF") - pdf_path = _locate_current_user_upload(request, upload_id, user) - if not pdf_path: - raise HTTPException(404, f"Source PDF {upload_id} not found") - - fitz = _load_pdf_viewer_fitz() - schema = load_field_sidecar(pdf_path) or [] - values = parse_markdown_to_values(doc.current_content or "") - - # Group fields by page - by_page: Dict[int, list] = {} - for f in schema: - by_page.setdefault(f["page"], []).append(f) - - scale = _PDF_RENDER_SCALE - pdf_doc = fitz.open(pdf_path) - try: - pages_out = [] - for page_index in range(pdf_doc.page_count): - page = pdf_doc[page_index] - page_no = page_index + 1 - pw, ph = page.rect.width, page.rect.height - img_w = int(pw * scale) - img_h = int(ph * scale) - fields_out = [] - for f in by_page.get(page_no, []): - x0, y0, x1, y1 = f["rect"] - fields_out.append({ - "name": f["name"], - "type": f["type"], - "label": f.get("label") or "", - "options": f.get("options") or [], - "value": values.get(f["name"], f.get("value", "")), - "rect_px": [ - int(x0 * scale), int(y0 * scale), - int(x1 * scale), int(y1 * scale), - ], - }) - pages_out.append({ - "page": page_no, - "width": img_w, - "height": img_h, - "fields": fields_out, - }) - return {"doc_id": doc_id, "scale": scale, "pages": pages_out} - finally: - pdf_doc.close() - finally: - db.close() - - # ---- GET /api/document/{doc_id}/page/{n}.png ---- - @router.get("/api/document/{doc_id}/page/{page_no}.png") - async def render_page_png(doc_id: str, page_no: int, request: Request): - """Render one page of the source PDF as a PNG (no values stamped — the - frontend overlays HTML form inputs on top).""" - from fastapi.responses import Response - from src.pdf_form_doc import find_source_upload_id - - user = get_current_user(request) - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - upload_id = find_source_upload_id(doc.current_content or "") - if not upload_id: - raise HTTPException(400, "Document is not linked to a source PDF") - pdf_path = _locate_current_user_upload(request, upload_id, user) - if not pdf_path: - raise HTTPException(404, "Source PDF not found") - finally: - db.close() - - fitz = _load_pdf_viewer_fitz() - pdf_doc = fitz.open(pdf_path) - try: - if page_no < 1 or page_no > pdf_doc.page_count: - raise HTTPException(404, "Page out of range") - page = pdf_doc[page_no - 1] - mat = fitz.Matrix(_PDF_RENDER_SCALE, _PDF_RENDER_SCALE) - pix = page.get_pixmap(matrix=mat, alpha=False) - png_bytes = pix.tobytes("png") - return Response( - content=png_bytes, - media_type="image/png", - headers={"Cache-Control": "public, max-age=3600"}, - ) - finally: - pdf_doc.close() - - # ---- POST /api/document/{doc_id}/ai-fill-annotations ---- - @router.post("/api/document/{doc_id}/ai-fill-annotations") - async def ai_fill_annotations(doc_id: str, request: Request) -> Dict[str, Any]: - """Ask a vision-capable LLM to locate fillable areas on a flat PDF and - propose annotation values for each, given a free-form user instruction. - - Returns a list of annotations: [{page, x, y, w, h, value}] where x/y/w/h - are page-percentages (0–100) — same coordinate system as the freeform - annotations the frontend already renders. - """ - import base64 - import json - import fitz - from src.pdf_form_doc import find_source_upload_id - from src.document_processor import _resolve_vl_model, _load_vl_settings - from src.llm_core import llm_call_async - - body = await request.json() if request.headers.get("content-type", "").startswith("application/json") else {} - instruction = (body or {}).get("instruction", "").strip() - if not instruction: - raise HTTPException(400, "instruction is required") - - user = get_current_user(request) - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - upload_id = find_source_upload_id(doc.current_content or "") - if not upload_id: - raise HTTPException(400, "Document is not linked to a source PDF") - pdf_path = _locate_current_user_upload(request, upload_id, user) - if not pdf_path: - raise HTTPException(404, "Source PDF not found") - finally: - db.close() - - # Resolve VL model (admin-configured or auto-detected vision-capable) - settings = _load_vl_settings() - vl_model = settings.get("vision_model", "") - try: - url, model_id, headers = _resolve_vl_model(vl_model, owner=user) - except Exception as e: - raise HTTPException(503, f"No vision model available: {e}") - - system_prompt = ( - "You analyze rendered PDF page images and propose values to fill in. " - "For each blank line, box, underscore, or labeled space on the page that " - "should be filled given the user's instruction, output one annotation. " - "Coordinates are percentages (0-100) of the page width/height with the " - "origin at top-left. Width/height should match the visible blank box. " - "Return ONLY a JSON array, no prose, no markdown fences. Each entry: " - '{"x": number, "y": number, "w": number, "h": number, "value": string}. ' - "If a region should not be filled, omit it. If nothing should be filled, " - "return []." - ) - - all_annotations = [] - pdf_doc = fitz.open(pdf_path) - try: - for page_index in range(pdf_doc.page_count): - page = pdf_doc[page_index] - mat = fitz.Matrix(_PDF_RENDER_SCALE, _PDF_RENDER_SCALE) - pix = page.get_pixmap(matrix=mat, alpha=False) - png_bytes = pix.tobytes("png") - b64 = base64.b64encode(png_bytes).decode("ascii") - - messages = [ - {"role": "system", "content": system_prompt}, - { - "role": "user", - "content": [ - { - "type": "text", - "text": ( - f"User instruction:\n{instruction}\n\n" - f"This is page {page_index + 1} of {pdf_doc.page_count}. " - "Return JSON array of annotations to add to this page." - ), - }, - { - "type": "image_url", - "image_url": {"url": f"data:image/png;base64,{b64}"}, - }, - ], - }, - ] - try: - raw = await llm_call_async( - url, model_id, messages, - temperature=0.1, max_tokens=2000, headers=headers, - ) - except Exception as e: - logger.error(f"VL call failed on page {page_index + 1}: {e}") - continue - - raw = (raw or "").strip() - if raw.startswith("```"): - raw = raw.split("\n", 1)[-1].rsplit("```", 1)[0].strip() - try: - parsed = json.loads(raw) - except Exception: - logger.warning(f"AI fill: page {page_index + 1} returned non-JSON: {raw[:200]}") - continue - if not isinstance(parsed, list): - continue - for item in parsed: - if not isinstance(item, dict): - continue - try: - x = float(item.get("x", 0)) - y = float(item.get("y", 0)) - w = float(item.get("w", 0)) - h = float(item.get("h", 0)) - value = str(item.get("value", "") or "") - except Exception: - continue - # Clamp + reject zero-size entries - if w <= 0.5 or h <= 0.3: - continue - x = max(0.0, min(99.0, x)) - y = max(0.0, min(99.0, y)) - w = max(0.5, min(100.0 - x, w)) - h = max(0.3, min(100.0 - y, h)) - if not value.strip(): - continue - all_annotations.append({ - "page": page_index + 1, - "x": round(x, 2), - "y": round(y, 2), - "w": round(w, 2), - "h": round(h, 2), - "value": value, - }) - finally: - pdf_doc.close() - - return {"annotations": all_annotations} - - # ---- GET /api/document/{doc_id}/render-pdf ---- - @router.get("/api/document/{doc_id}/render-pdf") - async def render_pdf(doc_id: str, request: Request): - """Inline PDF preview filled with the current markdown values. - - Same plumbing as the export route, but no signature stamping and - served inline (Content-Disposition: inline) so the browser can - embed it in an iframe. Cache-busted by the caller via query string. - """ - import base64 - import os - import tempfile - from fastapi.responses import FileResponse - from starlette.background import BackgroundTask - from src.pdf_form_doc import find_source_upload_id, parse_markdown_to_values, parse_markdown_annotations - from src.pdf_forms import fill_fields, stamp_annotations - from core.database import Signature - - # Track temp files for this request so they get unlinked AFTER - # the response is fully sent (BackgroundTask runs post-send). - _to_unlink: list[str] = [] - def _cleanup_temps(): - for _p in _to_unlink: - try: - os.unlink(_p) - except FileNotFoundError: - pass - except Exception as _e: - logger.warning(f"Could not unlink temp PDF {_p}: {_e}") - - user = get_current_user(request) - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - upload_id = find_source_upload_id(doc.current_content or "") - if not upload_id: - raise HTTPException(400, "Document is not linked to a source PDF") - pdf_path = _locate_current_user_upload(request, upload_id, user) - if not pdf_path: - raise HTTPException(404, f"Source PDF {upload_id} not found") - - # Fail fast with a clear 503 if the optional PyMuPDF dependency - # is missing — fill_fields/stamp_annotations will otherwise - # raise RuntimeError deep inside and bubble out as a 500. - # Mirrors the convention in _load_pdf_viewer_fitz above. - _load_pdf_viewer_fitz() - - values = parse_markdown_to_values(doc.current_content or "") - out_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name - _to_unlink.append(out_path) - try: - fill_fields(pdf_path, out_path, values) - except Exception as e: - logger.error(f"render_pdf fill_fields failed for {doc_id}: {e}") - _cleanup_temps() - raise HTTPException(500, f"PDF render failed: {e}") - - annotations = parse_markdown_annotations(doc.current_content or "") - if annotations: - ann_sig_ids = [ - a["value"][len("signature:"):].strip() - for a in annotations - if a.get("kind") == "signature" - and isinstance(a.get("value"), str) - and a["value"].startswith("signature:") - ] - ann_signature_pngs: dict[str, bytes] = {} - if ann_sig_ids: - # SECURITY: filter by owner so a caller can't reference - # someone else's signature ID from doc markdown and have - # it stamped/exported. - _sig_q = db.query(Signature).filter(Signature.id.in_(ann_sig_ids)) - if user: - _sig_q = _sig_q.filter(Signature.owner == user) - sig_rows = _sig_q.all() - for s in sig_rows: - try: - ann_signature_pngs[s.id] = base64.b64decode(s.data_png) - except Exception as e: - logger.warning(f"Bad annotation signature data for {s.id}: {e}") - annotated_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name - _to_unlink.append(annotated_path) - try: - stamp_annotations(out_path, annotated_path, annotations, ann_signature_pngs) - out_path = annotated_path - except Exception as e: - logger.error(f"stamp_annotations (render) failed for {doc_id}: {e}") - - return FileResponse( - out_path, - media_type="application/pdf", - headers={"Content-Disposition": "inline"}, - background=BackgroundTask(_cleanup_temps), - ) - finally: - db.close() - - # ---- GET /api/document/{doc_id}/export-pdf ---- - @router.get("/api/document/{doc_id}/export-pdf") - async def export_pdf(doc_id: str, request: Request): - """Stream the filled PDF for download. - - Reads field values and signature selections from the markdown — there - is no separate confirmation step. Signature fields contain their - chosen signature ID encoded as `signature:<id>` in the value. - """ - import base64 - import os - import tempfile - from fastapi.responses import FileResponse - from starlette.background import BackgroundTask - from src.pdf_form_doc import find_source_upload_id, parse_markdown_to_values, load_field_sidecar, parse_markdown_annotations - from src.pdf_forms import fill_fields, stamp_signatures, stamp_annotations - from core.database import Signature - - _to_unlink: list[str] = [] - def _cleanup_temps(): - for _p in _to_unlink: - try: - os.unlink(_p) - except FileNotFoundError: - pass - except Exception as _e: - logger.warning(f"Could not unlink temp PDF {_p}: {_e}") - - user = get_current_user(request) - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - - upload_id = find_source_upload_id(doc.current_content or "") - if not upload_id: - raise HTTPException(400, "Document is not linked to a source PDF") - - pdf_path = _locate_current_user_upload(request, upload_id, user) - if not pdf_path: - raise HTTPException(404, f"Source PDF {upload_id} not found in uploads") - - schema = load_field_sidecar(pdf_path) or [] - sig_field_names = {f["name"] for f in schema if f.get("type") == "signature"} - - all_values = parse_markdown_to_values(doc.current_content or "") - # Split: signature fields go to stamps, everything else to fill_fields - text_values: dict = {} - sig_ids: dict[str, str] = {} - for name, raw in all_values.items(): - if name in sig_field_names and isinstance(raw, str) and raw.startswith("signature:"): - sig_ids[name] = raw[len("signature:"):].strip() - elif name not in sig_field_names: - text_values[name] = raw - - stamps: dict = {} - if sig_ids: - # SECURITY: filter by owner — same reason as render_pdf. - _sig_q2 = db.query(Signature).filter(Signature.id.in_(list(sig_ids.values()))) - if user: - _sig_q2 = _sig_q2.filter(Signature.owner == user) - rows = _sig_q2.all() - by_id = {s.id: s for s in rows} - for field_name, sid in sig_ids.items(): - s = by_id.get(sid) - if not s: - continue - try: - stamps[field_name] = base64.b64decode(s.data_png) - except Exception as e: - logger.warning(f"Bad signature data for {sid}: {e}") - - filled_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name - _to_unlink.append(filled_path) - try: - fill_fields(pdf_path, filled_path, text_values) - except Exception as e: - logger.error(f"fill_fields failed for doc {doc_id}: {e}") - _cleanup_temps() - raise HTTPException(500, f"PDF fill failed: {e}") - - out_path = filled_path - if stamps: - stamped_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name - _to_unlink.append(stamped_path) - try: - stamp_signatures(filled_path, stamped_path, stamps) - out_path = stamped_path - except Exception as e: - logger.error(f"stamp_signatures failed for doc {doc_id}: {e}") - - # Burn freeform annotations (Text/Check/Sign drops) on top. - annotations = parse_markdown_annotations(doc.current_content or "") - if annotations: - # Resolve any signature annotations to their PNG bytes. - ann_sig_ids = [ - a["value"][len("signature:"):].strip() - for a in annotations - if a.get("kind") == "signature" - and isinstance(a.get("value"), str) - and a["value"].startswith("signature:") - ] - ann_signature_pngs: dict[str, bytes] = {} - if ann_sig_ids: - # SECURITY: filter by owner so a caller can't reference - # someone else's signature ID from doc markdown and have - # it stamped/exported. - _sig_q = db.query(Signature).filter(Signature.id.in_(ann_sig_ids)) - if user: - _sig_q = _sig_q.filter(Signature.owner == user) - sig_rows = _sig_q.all() - for s in sig_rows: - try: - ann_signature_pngs[s.id] = base64.b64decode(s.data_png) - except Exception as e: - logger.warning(f"Bad annotation signature data for {s.id}: {e}") - annotated_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name - _to_unlink.append(annotated_path) - try: - stamp_annotations(out_path, annotated_path, annotations, ann_signature_pngs) - out_path = annotated_path - except Exception as e: - logger.error(f"stamp_annotations failed for doc {doc_id}: {e}") - - download_name = _slug(doc.title or "form") + "_annotated.pdf" - return FileResponse( - out_path, - media_type="application/pdf", - filename=download_name, - background=BackgroundTask(_cleanup_temps), - ) - finally: - db.close() - - # ---- POST /api/document/{doc_id}/prepare-signed-reply ---- - @router.post("/api/document/{doc_id}/prepare-signed-reply") - async def prepare_signed_reply(doc_id: str, request: Request): - """Bake the current PDF state (form fields + signature stamps + - annotations) into a flattened PDF, drop it in COMPOSE_UPLOADS_DIR - and return the reply context (To/Subject/threading headers) so the - frontend can open a reply draft with this attachment pre-loaded. - - Requires the document to have source_email_* metadata (set when the - doc was created via /api/email/attachment-as-doc). Otherwise 400. - """ - import base64 - import tempfile - import shutil - import uuid as _uuid - import email as _email_mod - from src.pdf_form_doc import ( - find_source_upload_id, parse_markdown_to_values, - load_field_sidecar, parse_markdown_annotations, - ) - from src.pdf_forms import fill_fields, stamp_signatures, stamp_annotations - from core.database import Signature - # COMPOSE_UPLOADS_DIR lives in email_routes — re-derive here so we - # don't import from a routes file (cycle-prone). Same env override - # as email_routes (ODYSSEUS_MAIL_ATTACHMENTS_DIR). - from pathlib import Path as _Path - _COMPOSE_DIR = _Path(MAIL_ATTACHMENTS_DIR) / "_compose" - _COMPOSE_DIR.mkdir(parents=True, exist_ok=True) - - user = get_current_user(request) - db = SessionLocal() - try: - doc = db.query(Document).filter(Document.id == doc_id).first() - if not doc: - raise HTTPException(404, "Document not found") - _verify_doc_owner(db, doc, user) - - if not (doc.source_email_uid and doc.source_email_folder): - raise HTTPException(400, "Document has no source email — cannot reply") - - # 1) Build the flattened PDF (same pipeline as export_pdf) - upload_id = find_source_upload_id(doc.current_content or "") - if not upload_id: - raise HTTPException(400, "Document is not linked to a source PDF") - pdf_path = _locate_current_user_upload(request, upload_id, user) - if not pdf_path: - raise HTTPException(404, f"Source PDF {upload_id} not found") - - schema = load_field_sidecar(pdf_path) or [] - sig_field_names = {f["name"] for f in schema if f.get("type") == "signature"} - all_values = parse_markdown_to_values(doc.current_content or "") - text_values: dict = {} - sig_ids: dict[str, str] = {} - for name, raw in all_values.items(): - if name in sig_field_names and isinstance(raw, str) and raw.startswith("signature:"): - sig_ids[name] = raw[len("signature:"):].strip() - elif name not in sig_field_names: - text_values[name] = raw - - stamps: dict = {} - if sig_ids: - # SECURITY: filter by owner — same reason as render_pdf. - _sig_q2 = db.query(Signature).filter(Signature.id.in_(list(sig_ids.values()))) - if user: - _sig_q2 = _sig_q2.filter(Signature.owner == user) - rows = _sig_q2.all() - by_id = {s.id: s for s in rows} - for fname, sid in sig_ids.items(): - s = by_id.get(sid) - if not s: - continue - try: - stamps[fname] = base64.b64decode(s.data_png) - except Exception: - pass - - import os - _to_unlink: list[str] = [] - filled_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name - _to_unlink.append(filled_path) - fill_fields(pdf_path, filled_path, text_values) - out_path = filled_path - if stamps: - stamped_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name - _to_unlink.append(stamped_path) - try: - stamp_signatures(filled_path, stamped_path, stamps) - out_path = stamped_path - except Exception as e: - logger.warning(f"stamp_signatures failed for {doc_id}: {e}") - - annotations = parse_markdown_annotations(doc.current_content or "") - if annotations: - ann_sig_ids = [ - a["value"][len("signature:"):].strip() - for a in annotations - if a.get("kind") == "signature" - and isinstance(a.get("value"), str) - and a["value"].startswith("signature:") - ] - ann_signature_pngs: dict[str, bytes] = {} - if ann_sig_ids: - # SECURITY: filter by owner so a caller can't reference - # someone else's signature ID from doc markdown and have - # it stamped/exported. - _sig_q = db.query(Signature).filter(Signature.id.in_(ann_sig_ids)) - if user: - _sig_q = _sig_q.filter(Signature.owner == user) - sig_rows = _sig_q.all() - for s in sig_rows: - try: - ann_signature_pngs[s.id] = base64.b64decode(s.data_png) - except Exception: - pass - annotated_path = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False).name - _to_unlink.append(annotated_path) - try: - stamp_annotations(out_path, annotated_path, annotations, ann_signature_pngs) - out_path = annotated_path - except Exception as e: - logger.warning(f"stamp_annotations failed for {doc_id}: {e}") - - # 2) Move/copy into COMPOSE_UPLOADS_DIR with the token format - # `<uuid>_<original_name>` that /api/email/send expects. - filename = _slug(doc.title or "signed") + "_signed.pdf" - token = f"{_uuid.uuid4().hex}_{filename}" - dest = _COMPOSE_DIR / token - shutil.copyfile(out_path, str(dest)) - # Unlink the intermediate temp PDFs now that they've been - # copied into COMPOSE_UPLOADS_DIR. - for _p in _to_unlink: - try: - os.unlink(_p) - except FileNotFoundError: - pass - except Exception as _e: - logger.warning(f"Could not unlink temp PDF {_p}: {_e}") - - # 3) Fetch the source email's headers so we can build a clean reply - # context (To/Subject/In-Reply-To/References). - try: - from routes.email_routes import _imap, _decode_header - from routes.email_helpers import _q - except Exception: - _imap = None - _decode_header = lambda x: x or "" - _q = lambda x: x or "" - - to_addr = "" - from_name = "" - subject = "" - in_reply_to = doc.source_email_message_id or "" - references = in_reply_to - if _imap: - try: - with _imap(doc.source_email_account_id or None) as conn: - conn.select(_q(doc.source_email_folder), readonly=True) - status, data = conn.fetch(doc.source_email_uid.encode(), "(RFC822.HEADER)") - if status == "OK" and data and data[0]: - raw_hdr = data[0][1] - m = _email_mod.message_from_bytes(raw_hdr) - sender = _decode_header(m.get("From", "")) - from_name, to_addr = _email_mod.utils.parseaddr(sender) - if not to_addr: - to_addr = sender - subject = _decode_header(m.get("Subject", "") or "") - if subject and not subject.lower().startswith("re:"): - subject = "Re: " + subject - msg_refs = (m.get("References") or "").strip() - msg_in_reply = (m.get("Message-ID") or "").strip() or in_reply_to - in_reply_to = msg_in_reply - references = (msg_refs + " " + msg_in_reply).strip() if msg_refs else msg_in_reply - except Exception as e: - logger.warning(f"prepare-signed-reply header fetch failed: {e}") - - return { - "ok": True, - "attachment": { - "token": token, - "filename": filename, - "size": dest.stat().st_size, - }, - "reply": { - "to": to_addr, - "to_name": from_name, - "subject": subject, - "in_reply_to": in_reply_to, - "references": references, - "account_id": doc.source_email_account_id or None, - "source_uid": doc.source_email_uid, - "source_folder": doc.source_email_folder, - "source_message_id": doc.source_email_message_id, - }, - } - finally: - db.close() - - return router +_sys.modules[__name__] = _canonical diff --git a/tests/test_document_routes_shim.py b/tests/test_document_routes_shim.py new file mode 100644 index 000000000..68d049a62 --- /dev/null +++ b/tests/test_document_routes_shim.py @@ -0,0 +1,29 @@ +"""Regression test for the document route shim (slice 2m, #4082/#4071). + +The backward-compat shims at ``routes/document_routes.py`` and +``routes/document_helpers.py`` use ``sys.modules`` replacement so the legacy +import paths and the canonical ``routes.document.*`` paths resolve to the +*same* module objects. This is required because multiple tests do +``import routes.document_routes as droutes`` followed by +``droutes.SessionLocal = ...`` / ``monkeypatch.setattr(droutes, ...)`` and +``sys.modules.pop("routes.document_helpers")`` + re-import — for those to +take effect at runtime, the legacy and canonical module objects must be +identical. +""" + +import importlib + +import routes.document_routes as _shim_routes # noqa: F401 +import routes.document_helpers as _shim_helpers # noqa: F401 + + +def test_legacy_and_canonical_routes_are_same_object(): + legacy = importlib.import_module("routes.document_routes") + canonical = importlib.import_module("routes.document.document_routes") + assert legacy is canonical + + +def test_legacy_and_canonical_helpers_are_same_object(): + legacy = importlib.import_module("routes.document_helpers") + canonical = importlib.import_module("routes.document.document_helpers") + assert legacy is canonical diff --git a/tests/test_imap_mailbox_quoting.py b/tests/test_imap_mailbox_quoting.py index 7c5bb1645..636270a56 100644 --- a/tests/test_imap_mailbox_quoting.py +++ b/tests/test_imap_mailbox_quoting.py @@ -87,7 +87,7 @@ def test_known_imap_mailbox_call_sites_are_quoted(): assert "conn.select(sent_name" not in pollers assert "imap.append(sent_folder" not in pollers - document_routes = Path("routes/document_routes.py").read_text() + document_routes = Path("routes/document/document_routes.py").read_text() assert "conn.select(doc.source_email_folder" not in document_routes diff --git a/tests/test_model_helper_owner_scope.py b/tests/test_model_helper_owner_scope.py index dafbad594..f48a1f7e2 100644 --- a/tests/test_model_helper_owner_scope.py +++ b/tests/test_model_helper_owner_scope.py @@ -14,7 +14,7 @@ def _function_source(path: str, name: str) -> str: def test_document_ai_tidy_resolves_with_owner_scope(): - body = _function_source("routes/document_routes.py", "ai_tidy_documents") + body = _function_source("routes/document/document_routes.py", "ai_tidy_documents") assert "resolve_task_endpoint(owner=user or None)" in body assert 'resolve_endpoint("default", owner=user or None)' in body diff --git a/tests/test_vision_owner_scope.py b/tests/test_vision_owner_scope.py index f0d3a184d..29de101a3 100644 --- a/tests/test_vision_owner_scope.py +++ b/tests/test_vision_owner_scope.py @@ -88,7 +88,7 @@ def test_request_vision_call_sites_pass_owner(): chat_source = (ROOT / "src" / "chat_handler.py").read_text() processor_source = (ROOT / "src" / "document_processor.py").read_text() upload_source = (ROOT / "routes" / "upload_routes.py").read_text() - document_source = (ROOT / "routes" / "document_routes.py").read_text() + document_source = (ROOT / "routes" / "document" / "document_routes.py").read_text() gallery_source = (ROOT / "routes" / "gallery" / "gallery_routes.py").read_text() memory_source = (ROOT / "routes" / "memory" / "memory_routes.py").read_text() From 9d686180dd20e6ef842f0c23f9ef2f0ce39cee4f Mon Sep 17 00:00:00 2001 From: Ashvin <76151462+ashvinctrl@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:47:41 +0530 Subject: [PATCH 033/125] fix(integrations): pin api_call to the SSRF-validated IP (#5727) * fix(integrations): pin api_call to the SSRF-validated IP execute_api_call runs check_outbound_url on the target, but that guard only resolves the host to answer (ok, reason) and hands back no address. The request right after it opened a plain httpx.AsyncClient, which resolves the host again at connect time. A base_url host on a low TTL can pass the guard as a public IP and then flip to 169.254.169.254 for the connect, so the call lands on cloud metadata with the integration's stored auth headers attached. Resolve once, remember the IPs the guard actually validated, and pin the client's socket to that set through a small AnyIO-backed transport. SNI and the Host header still come from the URL, so TLS and vhost routing are unchanged; connect-time fallback stays inside the approved address set over one shared deadline. This is the same pinning the webhook sender and web-fetch paths already do -- api_call was the last outbound path that skipped it. Fixes #5513 * fix(integrations): de-duplicate the pinned IP list _default_resolver calls getaddrinfo(host, None) with no socktype filter, so glibc returns one record per socktype and a single-homed host comes back three times over. _validated_ips kept every entry, so the transport pinned the same address repeatedly and the connect fallback could spend its shared deadline retrying one dead address instead of moving on to a genuinely different one. Windows getaddrinfo collapses those duplicate records, which is why the ip-literal pin test only failed on CI and not locally. --- src/integrations.py | 175 ++++++++++++- tests/test_integration_api_call_ssrf.py | 240 ++++++++++++++++++ .../test_integrations_api_call_truncation.py | 14 +- 3 files changed, 420 insertions(+), 9 deletions(-) diff --git a/src/integrations.py b/src/integrations.py index aa6c4982e..52dd4b2d1 100644 --- a/src/integrations.py +++ b/src/integrations.py @@ -1,11 +1,14 @@ +import ipaddress import json import os +import time import uuid import logging import re from typing import Dict, List, Optional, Any from urllib.parse import urljoin, urlparse, urlunparse +import httpcore import httpx from fastapi import HTTPException @@ -354,6 +357,152 @@ def _find_integration(identifier: str) -> Optional[Dict[str, Any]]: return None +# httpcore raises its own exception hierarchy; map the ones a simple request can +# surface back to their httpx equivalents so the caller's `except httpx.*` blocks +# below behave exactly as they did with the default transport. +_HTTPCORE_TO_HTTPX_EXC = { + httpcore.ConnectError: httpx.ConnectError, + httpcore.ConnectTimeout: httpx.ConnectTimeout, + httpcore.NetworkError: httpx.NetworkError, + httpcore.PoolTimeout: httpx.PoolTimeout, + httpcore.ProtocolError: httpx.ProtocolError, + httpcore.ReadError: httpx.ReadError, + httpcore.ReadTimeout: httpx.ReadTimeout, + httpcore.RemoteProtocolError: httpx.RemoteProtocolError, + httpcore.TimeoutException: httpx.TimeoutException, + httpcore.WriteError: httpx.WriteError, + httpcore.WriteTimeout: httpx.WriteTimeout, +} + + +class _PinnedAsyncBackend(httpcore.AsyncNetworkBackend): + """Network backend that connects only to the pre-validated IPs, in order. + + Every address here came out of the single SSRF resolution, so moving to the + next one after a connect failure is not re-resolution — it's ordinary + multi-address fallback restricted to the set the guard already approved. + httpcore takes TLS SNI and the ``Host`` header from the request URL rather + than the connect host, so pinning the socket destination leaves certificate + validation and vhost routing pointed at the original hostname. + """ + + def __init__(self, ips: List[ipaddress._BaseAddress]): + self._ips = [str(ip) for ip in ips] + self._real = httpcore.AnyIOBackend() + + async def connect_tcp(self, host, port, timeout=None, local_address=None, + socket_options=None): + # One shared connect budget: each attempt gets the time left until the + # original deadline, so N dead addresses can't stretch the connect + # phase to N * timeout. + deadline = None if timeout is None else time.monotonic() + timeout + last_exc: Optional[Exception] = None + for ip in self._ips: + remaining = None if deadline is None else max(0.0, deadline - time.monotonic()) + try: + return await self._real.connect_tcp( + ip, port, remaining, local_address, socket_options + ) + except (httpcore.ConnectError, httpcore.ConnectTimeout) as exc: + last_exc = exc + if deadline is not None and time.monotonic() >= deadline: + break + raise last_exc + + async def connect_unix_socket(self, path, timeout=None, socket_options=None): + return await self._real.connect_unix_socket(path, timeout, socket_options) + + async def sleep(self, seconds: float) -> None: + return await self._real.sleep(seconds) + + +class _PinnedAsyncTransport(httpx.AsyncBaseTransport): + """httpx transport that pins the TCP connect to the pre-resolved IP(s). + + Kept local, mirroring the per-module pinned transports web fetch and + webhook delivery already carry, rather than coupling api_call to the + webhook subsystem. The request URL passes through unchanged, so SNI and the + ``Host`` header stay the original hostname; only the socket destination is + pinned, which is what closes the rebinding window. + """ + + def __init__(self, ips: List[ipaddress._BaseAddress]): + self._pinned_ips = list(ips) + self._pool = httpcore.AsyncConnectionPool( + # Reuse the CA trust the default httpx client would build (certifi + # plus SSL_CERT_FILE / SSL_CERT_DIR when trust_env is set) so + # swapping in this transport doesn't quietly change which chains + # verify. ssl.create_default_context() would use system roots. + ssl_context=httpx.create_ssl_context(), + http1=True, + http2=False, + network_backend=_PinnedAsyncBackend(ips), + ) + + async def handle_async_request(self, request: httpx.Request) -> httpx.Response: + core_req = httpcore.Request( + method=request.method, + url=httpcore.URL( + scheme=request.url.raw_scheme, + host=request.url.raw_host, + port=request.url.port, + target=request.url.raw_path, + ), + headers=request.headers.raw, + content=request.stream, + extensions=request.extensions, + ) + try: + core_resp = await self._pool.handle_async_request(core_req) + content = b"".join([chunk async for chunk in core_resp.aiter_stream()]) + await core_resp.aclose() + except Exception as exc: + mapped = _HTTPCORE_TO_HTTPX_EXC.get(type(exc)) + if mapped is not None: + raise mapped(str(exc)) from exc + raise + return httpx.Response( + status_code=core_resp.status, + headers=core_resp.headers, + content=content, + extensions=core_resp.extensions, + ) + + async def aclose(self) -> None: + await self._pool.aclose() + + +def _validated_ips(raw_ips: List[str]) -> List[ipaddress._BaseAddress]: + """Return every entry that parses as an IP address, de-duplicated, order + preserved. + + check_outbound_url only reports ok when *all* of these classify as safe, so + the whole list is guard-approved and any of them is a legitimate connect + target. Skipping unparseable entries mirrors how the guard walks the same + resolver output. + + De-duplication matters because the resolver is getaddrinfo(host, None) with + no socktype filter, so glibc reports the same address once per socktype + (SOCK_STREAM/SOCK_DGRAM/SOCK_RAW) — a single-homed host comes back three + times. Without this, the connect fallback would spend the shared deadline + retrying one dead address instead of moving on to a genuinely different one. + """ + ips: List[ipaddress._BaseAddress] = [] + seen = set() + for raw in raw_ips: + if not isinstance(raw, str): + continue + try: + ip = ipaddress.ip_address(raw.split("%")[0]) # strip IPv6 zone id + except ValueError: + continue + if ip in seen: + continue + seen.add(ip) + ips.append(ip) + return ips + + async def execute_api_call( integration_id: str, method: str, @@ -409,13 +558,31 @@ async def execute_api_call( # loopback for locked-down deployments. Private stays allowed by default # because LAN integrations (Home Assistant, Miniflux, ntfy) are the # primary use case. - from src.url_safety import check_outbound_url + from src.url_safety import check_outbound_url, _default_resolver block_private = os.getenv( "INTEGRATION_API_BLOCK_PRIVATE_IPS", "false" ).lower() == "true" - ok, reason = check_outbound_url(url, block_private=block_private) + # Resolve the host exactly once and remember the IPs the guard validated so + # the request below can be pinned to them. check_outbound_url only reports + # (ok, reason); a plain httpx client re-resolves the host at connect time, + # which reopens a DNS-rebinding TOCTOU — a base_url host that answers with a + # public IP for the guard and then flips to 169.254.169.254 for the connect + # would reach cloud metadata with the integration's auth headers attached. + resolved_ips: List[str] = [] + + def _recording_resolver(host: str) -> List[str]: + ips = _default_resolver(host) + resolved_ips[:] = ips + return ips + + ok, reason = check_outbound_url( + url, block_private=block_private, resolver=_recording_resolver + ) if not ok: return {"error": f"URL rejected: {reason}", "exit_code": 1} + pinned_ips = _validated_ips(resolved_ips) + if not pinned_ips: + return {"error": "URL rejected: host did not resolve to a usable address", "exit_code": 1} method = method.upper() @@ -455,7 +622,9 @@ async def execute_api_call( auth = httpx.BasicAuth(parts[0], parts[1]) try: - async with httpx.AsyncClient(timeout=30.0) as client: + async with httpx.AsyncClient( + timeout=30.0, transport=_PinnedAsyncTransport(pinned_ips) + ) as client: response = await client.request( method, url, diff --git a/tests/test_integration_api_call_ssrf.py b/tests/test_integration_api_call_ssrf.py index 53dc671c5..f23cc40de 100644 --- a/tests/test_integration_api_call_ssrf.py +++ b/tests/test_integration_api_call_ssrf.py @@ -9,8 +9,13 @@ link-local/metadata is always rejected; RFC-1918/loopback only when INTEGRATION_API_BLOCK_PRIVATE_IPS=true (LAN integrations are the primary use case, so private stays allowed by default). """ +import asyncio +import ipaddress +import ssl from unittest.mock import AsyncMock, MagicMock, patch +import httpcore +import httpx import pytest from src import integrations @@ -97,3 +102,238 @@ async def test_private_base_url_allowed_by_default_blocked_with_knob(monkeypatch assert result["exit_code"] == 1 assert "rejected" in result["error"].lower() client.request.assert_not_called() + + +async def _call_capturing_transport(base_url, path="/items"): + """Drive execute_api_call and return (result, transport) where transport is + the object passed to httpx.AsyncClient(transport=...).""" + resp = MagicMock() + resp.status_code = 200 + resp.headers = {"content-type": "application/json"} + resp.json.return_value = {"ok": True} + resp.text = '{"ok": true}' + + client = AsyncMock() + client.__aenter__ = AsyncMock(return_value=client) + client.__aexit__ = AsyncMock(return_value=None) + client.request = AsyncMock(return_value=resp) + + captured = {} + + def _fake_async_client(*args, **kwargs): + captured.update(kwargs) + return client + + with ( + patch.object(integrations, "_find_integration", + return_value=_integration(base_url)), + patch("httpx.AsyncClient", side_effect=_fake_async_client), + ): + result = await integrations.execute_api_call("test_integ", "GET", path) + return result, captured.get("transport"), client + + +@pytest.mark.asyncio +async def test_connection_is_pinned_to_the_validated_ip(monkeypatch): + """DNS-rebinding defense: the guard resolves the host once to a benign + public IP, and the request must be pinned to *that* IP so a host that + rebinds to the metadata range at connect time can't be reached with the + integration's auth headers. Static resolution passing the guard is not + enough — a plain client would re-resolve at connect.""" + monkeypatch.setattr("src.url_safety._default_resolver", + lambda host: ["93.184.216.34"]) + result, transport, client = await _call_capturing_transport( + "http://rebinding.attacker.example") + + assert result.get("exit_code") == 0 + client.request.assert_called_once() + assert isinstance(transport, integrations._PinnedAsyncTransport) + assert [str(ip) for ip in transport._pinned_ips] == ["93.184.216.34"] + + +@pytest.mark.asyncio +async def test_pin_carries_the_whole_validated_ip_set(monkeypatch): + """When a host resolves to several records the transport keeps all of them + (check_outbound_url validated every one), in resolver order, so it can fall + back past a dead first address instead of failing the whole call.""" + monkeypatch.setattr("src.url_safety._default_resolver", + lambda host: ["93.184.216.34", "198.51.100.7"]) + result, transport, _ = await _call_capturing_transport("http://multi.example") + + assert result.get("exit_code") == 0 + assert [str(ip) for ip in transport._pinned_ips] == ["93.184.216.34", "198.51.100.7"] + + +class _FakeStream: + """Stand-in for the connected socket the real backend returns.""" + + +class _RecordingBackend: + """Fake httpcore backend: connect_tcp fails for the addresses in `dead` + and succeeds for the rest, recording the order it was asked to connect.""" + + def __init__(self, dead): + self.dead = set(dead) + self.attempts = [] + + async def connect_tcp(self, host, port, timeout=None, local_address=None, + socket_options=None): + self.attempts.append((host, timeout)) + if host in self.dead: + raise httpcore.ConnectError(f"connection refused: {host}") + return _FakeStream() + + +def _pinned_backend(ips, dead): + """A _PinnedAsyncBackend whose underlying connect is the recording fake.""" + backend = integrations._PinnedAsyncBackend(ips) + backend._real = _RecordingBackend(dead) + return backend + + +@pytest.mark.asyncio +async def test_connect_falls_back_from_dead_first_to_live_second(): + """first-dead / second-live: the pinned backend must try the next validated + address when the first refuses, rather than surfacing the failure. It also + ignores the `host` httpcore passes (the original hostname) and connects to + the pinned IPs, which is what keeps TLS SNI / Host on the real hostname.""" + ips = [ipaddress.ip_address("203.0.113.10"), ipaddress.ip_address("198.51.100.7")] + backend = _pinned_backend(ips, dead={"203.0.113.10"}) + + stream = await backend.connect_tcp("original.hostname.example", 443, timeout=5.0) + + assert isinstance(stream, _FakeStream) + # Tried the dead address first, then the live one — never the hostname. + assert [host for host, _ in backend._real.attempts] == ["203.0.113.10", "198.51.100.7"] + # Fallback shared one budget: the second attempt got the time left, not a fresh 5s. + assert backend._real.attempts[1][1] <= 5.0 + + +@pytest.mark.asyncio +async def test_connect_raises_when_every_validated_address_is_dead(): + ips = [ipaddress.ip_address("203.0.113.10"), ipaddress.ip_address("198.51.100.7")] + backend = _pinned_backend(ips, dead={"203.0.113.10", "198.51.100.7"}) + + with pytest.raises(httpcore.ConnectError): + await backend.connect_tcp("original.hostname.example", 443, timeout=5.0) + assert [host for host, _ in backend._real.attempts] == ["203.0.113.10", "198.51.100.7"] + + +@pytest.mark.asyncio +async def test_pinned_transport_reuses_httpx_ca_trust(monkeypatch): + """TLS trust must come from the same builder the default httpx client uses + (certifi + SSL_CERT_FILE / SSL_CERT_DIR via trust_env), not from + ssl.create_default_context()'s system roots — otherwise chains that verified + under the old default client can silently stop verifying.""" + sentinel = ssl.create_default_context() + calls = [] + + def _fake_create(*args, **kwargs): + calls.append(kwargs) + return sentinel + + monkeypatch.setattr(httpx, "create_ssl_context", _fake_create) + transport = integrations._PinnedAsyncTransport([ipaddress.ip_address("93.184.216.34")]) + try: + assert calls, "transport did not build its context via httpx.create_ssl_context" + assert transport._pool._ssl_context is sentinel + finally: + await transport.aclose() + + +@pytest.mark.asyncio +async def test_real_socket_falls_back_from_dead_first_to_live_second(): + """End-to-end over real loopback sockets: pin [127.0.0.2 (nothing + listening), 127.0.0.1 (live)], and the request must succeed by falling back + to the second address while the Host header stays the original hostname — + i.e. only the socket destination moved, vhost/SNI routing did not.""" + captured = {} + + async def handle(reader, writer): + request = await reader.read(4096) + for line in request.split(b"\r\n"): + if line.lower().startswith(b"host:"): + captured["host"] = line.split(b":", 1)[1].strip().decode() + writer.write(b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\nConnection: close\r\n\r\nhi") + await writer.drain() + writer.close() + + server = await asyncio.start_server(handle, "127.0.0.1", 0) + port = server.sockets[0].getsockname()[1] + async with server: + await server.start_serving() + transport = integrations._PinnedAsyncTransport( + [ipaddress.ip_address("127.0.0.2"), ipaddress.ip_address("127.0.0.1")] + ) + try: + async with httpx.AsyncClient(transport=transport) as client: + resp = await client.get(f"http://pinned.example:{port}/health") + finally: + await transport.aclose() + + assert resp.status_code == 200 + assert resp.text == "hi" + assert captured.get("host") == f"pinned.example:{port}" + + +@pytest.mark.asyncio +async def test_ip_literal_base_url_still_pins_and_is_not_rejected(): + """A base_url that is already an IP has nothing to rebind, but it must not + trip the "did not resolve" guard either. + + check_outbound_url resolves even a literal (getaddrinfo returns the address + itself), so the captured list is populated and the pin is a no-op rather + than a rejection. Uses the real resolver on purpose — no monkeypatch — so + this would catch the fail-closed branch firing on a literal. + """ + result, transport, client = await _call_capturing_transport( + "http://93.184.216.34") + + assert result.get("exit_code") == 0 + assert isinstance(transport, integrations._PinnedAsyncTransport) + assert [str(ip) for ip in transport._pinned_ips] == ["93.184.216.34"] + + +@pytest.mark.asyncio +async def test_ipv6_base_url_pins_every_validated_address(monkeypatch): + """IPv6 goes down the same path as v4. + + Resolution is stubbed rather than using a literal so this doesn't depend on + the runner having IPv6 configured. + """ + v6 = "2606:2800:220:1:248:1893:25c8:1946" + monkeypatch.setattr("src.url_safety._default_resolver", lambda host: [v6]) + result, transport, client = await _call_capturing_transport("http://v6.example") + + assert result.get("exit_code") == 0 + assert isinstance(transport, integrations._PinnedAsyncTransport) + assert [str(ip) for ip in transport._pinned_ips] == [v6] + + +def test_validated_ips_strips_zone_id_and_drops_junk(): + """getaddrinfo can hand back a scoped v6 address like 'fe80::1%eth0'.""" + got = integrations._validated_ips( + ["93.184.216.34", "fe80::1%eth0", "not-an-ip", None, "2001:db8::5"] + ) + assert [str(ip) for ip in got] == ["93.184.216.34", "fe80::1", "2001:db8::5"] + + +def test_validated_ips_deduplicates_repeated_addresses(): + """The resolver is getaddrinfo(host, None) with no socktype filter, so glibc + returns one record per socktype and a single-homed host arrives three times + over. Duplicates must collapse (first-seen order kept) or the connect + fallback wastes its shared deadline retrying one dead address.""" + got = integrations._validated_ips( + ["93.184.216.34", "93.184.216.34", "93.184.216.34"] + ) + assert [str(ip) for ip in got] == ["93.184.216.34"] + + # Order is first-seen, and distinct addresses all survive. + got = integrations._validated_ips( + ["198.51.100.7", "93.184.216.34", "198.51.100.7", "2001:db8::5"] + ) + assert [str(ip) for ip in got] == ["198.51.100.7", "93.184.216.34", "2001:db8::5"] + + # A zone-id variant is the same address once stripped, so it collapses too. + got = integrations._validated_ips(["fe80::1%eth0", "fe80::1%eth1", "fe80::1"]) + assert [str(ip) for ip in got] == ["fe80::1"] diff --git a/tests/test_integrations_api_call_truncation.py b/tests/test_integrations_api_call_truncation.py index bf1ec7d05..a0ad61b4a 100644 --- a/tests/test_integrations_api_call_truncation.py +++ b/tests/test_integrations_api_call_truncation.py @@ -83,9 +83,10 @@ async def _call(json_data, status=200): with ( patch.object(integrations, "_find_integration", return_value=DUMMY_INTEGRATION), patch("httpx.AsyncClient", return_value=mock_client), - # api.example.com doesn't resolve; the SSRF guard would fail closed. - # These tests are about truncation, so stub the guard open. - patch("src.url_safety.check_outbound_url", return_value=(True, "ok")), + # api.example.com doesn't resolve. Point the resolver at a public + # address instead of stubbing the guard open, so the real check (and + # the connect-IP pinning that reads its result) still runs. + patch("src.url_safety._default_resolver", lambda host: ["93.184.216.34"]), ): return await integrations.execute_api_call("test_integ", "GET", "/items") @@ -101,9 +102,10 @@ async def _call_with_integration(integration, path="/items"): with ( patch.object(integrations, "_find_integration", return_value=integration), patch("httpx.AsyncClient", return_value=mock_client), - # api.example.com doesn't resolve; the SSRF guard would fail closed. - # These tests are about URL joining, so stub the guard open. - patch("src.url_safety.check_outbound_url", return_value=(True, "ok")), + # api.example.com doesn't resolve. Point the resolver at a public + # address instead of stubbing the guard open, so the real check (and + # the connect-IP pinning that reads its result) still runs. + patch("src.url_safety._default_resolver", lambda host: ["93.184.216.34"]), ): result = await integrations.execute_api_call("test_integ", "GET", path) return result, mock_client From 20e7fc0164286e1521569d9edc17a4ae4d0d2e22 Mon Sep 17 00:00:00 2001 From: adabarbulescu <94562950+adabarbulescu@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:17:45 +0300 Subject: [PATCH 034/125] fix(skills): require manage_skills action (#5856) --- src/tools/system.py | 6 ++++-- tests/test_manage_skills_action_required.py | 24 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 tests/test_manage_skills_action_required.py diff --git a/src/tools/system.py b/src/tools/system.py index 813d57df2..c2eb9ceab 100644 --- a/src/tools/system.py +++ b/src/tools/system.py @@ -46,7 +46,9 @@ async def do_manage_skills(content: str, owner: Optional[str] = None) -> Dict: except ValueError: return {"error": "Invalid JSON arguments", "exit_code": 1} - action = (args.get("action") or "").lower() + action = (args.get("action") or "").strip().lower() + if not action: + return {"error": "action is required (list|view|view_ref|add|edit|patch|publish|delete|search)", "exit_code": 1} from services.memory.skills import SkillsManager from services.memory.skill_format import Skill, slugify from src.constants import DATA_DIR @@ -55,7 +57,7 @@ async def do_manage_skills(content: str, owner: Optional[str] = None) -> Dict: # Accept legacy `skill_id` as an alias for `name`. name = (args.get("name") or args.get("skill_id") or "").strip() - if action in ("list", "index", ""): + if action in ("list", "index"): all_skills = sm.load(owner=owner) if not all_skills: return {"results": "No skills yet. Create one with action='add'."} diff --git a/tests/test_manage_skills_action_required.py b/tests/test_manage_skills_action_required.py new file mode 100644 index 000000000..4efae8026 --- /dev/null +++ b/tests/test_manage_skills_action_required.py @@ -0,0 +1,24 @@ +import json + +import pytest + +from src.tools.system import do_manage_skills + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "payload", + [ + {}, + {"action": ""}, + {"action": " "}, + {"name": "demo", "description": "x", "procedure": ["step"]}, + ], +) +async def test_manage_skills_requires_action(payload): + result = await do_manage_skills(json.dumps(payload), owner="test") + + assert result == { + "error": "action is required (list|view|view_ref|add|edit|patch|publish|delete|search)", + "exit_code": 1, + } From c8a012d4d2db27142196a9a7b290687323979b5b Mon Sep 17 00:00:00 2001 From: Ashvin <76151462+ashvinctrl@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:03:50 +0530 Subject: [PATCH 035/125] fix(memory): don't let an unreadable store get overwritten with an empty one (#5831) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(memory): don't let an unreadable store get overwritten with an empty one load_all() answered a failed read the same way it answered an empty store: with []. Every mutation path is a read-modify-write (load the whole file, change it, save it back), so a failed read became load_all() -> [] -> [].append(new) -> save([new]) and save() is atomic, so the replacement stuck. The case that actually destroys data is a store that is READABLE but not parseable - a truncated file, or one holding {} instead of []. Nothing obstructs the write, so adding a memory returns HTTP 200 and every memory already stored is gone. Verified end-to-end against a running instance: on the current code a truncated memory.json plus one add leaves the file holding only the new entry. Truncation is reachable - core/database.py rewrites memory.json during migration with a plain open(.., "w") + json.dump, which is not atomic. A live exclusive lock is not the dangerous case: it blocks the read and the os.replace alike, so the save fails too and the store survives. That path currently 500s and loses nothing. _read_entries() now returns [] only when the file genuinely does not exist and raises MemoryStoreUnreadable for every other failure, including a store that parses but is not a JSON array. load_all() keeps the old lenient behaviour so display, search and context injection still degrade quietly instead of breaking chat. The read-modify-write callers switch to load_all_for_update(), which propagates the error: the memory routes turn it into a 503 and change nothing, backup import refuses rather than saving only the incoming rows, and auto-extraction and the audit merge skip the write. The audit merge mattered most - it rebuilds the whole file from one owner's slice plus everyone else's rows, so an empty read there dropped every other tenant's memories. The corrupt-JSON path still gets its one shot at the legacy memory.txt migration before raising, so that recovery is unchanged. The two updated fakes gained load_all_for_update because the real class has it; MagicMock would otherwise hand the import path a Mock instead of the seeded list. Fixes #5673 * fix(memory): fail closed on the remaining read-modify-write add paths The strict loader landed with the routes, the backup import and the extractor converted, but three read-modify-write sinks still called load_all(), which degrades an unreadable store to []. Two of them are the paths users actually reach, so the data loss in #5673 stayed reproducible: - src/ai_interaction.py do_manage_memory, action "add" — reached from ordinary chat via src/tool_execution.py:793 -> dispatch_ai_tool. "Remember that I prefer X" against an unreadable store wrote a one-entry file over it and reported success. - mcp_servers/memory_server.py, action "add" — the same shape through _scope_entries(), registered as a built-in in src/builtin_mcp.py. - src/memory_provider.py NativeMemoryProvider.remember and .delete — wired into app state in src/app_initializer.py but not consumed outside tests yet, converted here so the pattern is uniform before it goes live. The MCP server takes _scope_entries(for_update=True) so list keeps the lenient read. The edit and delete branches on both tool paths were already fail-closed by accident — an empty view matches nothing and returns before the save — so they are left alone. The three new tests drive the real entry points rather than replaying the shape, and use a truncated store, which is the case that reads back fine so nothing stops the save. Each asserts memory.json is byte-identical afterwards; all three fail on the previous commit with the store overwritten. --- mcp_servers/memory_server.py | 26 +- routes/backup_routes.py | 11 +- routes/memory/memory_routes.py | 26 +- services/memory/__init__.py | 3 +- services/memory/memory.py | 14 +- services/memory/memory_extractor.py | 23 +- src/ai_interaction.py | 11 +- src/memory.py | 90 ++++++- src/memory_provider.py | 11 +- tests/test_backup_import_cross_user_dedup.py | 3 + ...st_memory_extractor_vector_cross_tenant.py | 6 + tests/test_memory_store_unreadable_no_wipe.py | 255 ++++++++++++++++++ 12 files changed, 451 insertions(+), 28 deletions(-) create mode 100644 tests/test_memory_store_unreadable_no_wipe.py diff --git a/mcp_servers/memory_server.py b/mcp_servers/memory_server.py index fafbcfc2b..fd574fd1f 100644 --- a/mcp_servers/memory_server.py +++ b/mcp_servers/memory_server.py @@ -17,6 +17,8 @@ from mcp.types import Tool, TextContent sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) +from src.memory import MemoryStoreUnreadable + server = Server("memory") # Late-initialized managers (set during first tool call) @@ -29,6 +31,10 @@ _OWNER_SCOPE_ERROR = ( "Error: Memory MCP owner is not configured for an owner-scoped memory store. " "Set ODYSSEUS_MCP_MEMORY_OWNER for this server or use the owner-aware native memory tool." ) +_UNREADABLE_STORE_ERROR = ( + "Error: Memory store is temporarily unreadable — nothing was saved. " + "Repair or restore memory.json, then retry." +) def _configured_owner() -> str | None: @@ -51,9 +57,21 @@ def _owner_scoped_store(entries: list[dict]) -> bool: return any(_entry_owner(entry) for entry in entries if isinstance(entry, dict)) -def _scope_entries() -> tuple[str | None, list[dict], list[dict], str | None]: - """Return configured owner, all entries, visible entries, and optional error.""" - entries = _memory_manager.load_all() +def _scope_entries(for_update: bool = False) -> tuple[str | None, list[dict], list[dict], str | None]: + """Return configured owner, all entries, visible entries, and optional error. + + ``for_update=True`` is for read-modify-write callers. They save the ``all + entries`` list back, so an unreadable store must be reported as an error + instead of degrading to ``[]`` — otherwise the save writes their one new + entry over the whole store (issue #5673). + """ + if for_update: + try: + entries = _memory_manager.load_all_for_update() + except MemoryStoreUnreadable as e: + return None, [], [], f"{_UNREADABLE_STORE_ERROR} ({e})" + else: + entries = _memory_manager.load_all() owner = _configured_owner() if owner is None and _owner_scoped_store(entries): return None, entries, [], _OWNER_SCOPE_ERROR @@ -161,7 +179,7 @@ async def call_tool(name: str, arguments: dict) -> list[TextContent]: category = arguments.get("category", "fact") if not text: return _text_result("Error: Memory text cannot be empty") - owner, memories, _visible, scope_error = _scope_entries() + owner, memories, _visible, scope_error = _scope_entries(for_update=True) if scope_error: return _text_result(scope_error) entry = _memory_manager.add_entry(text, source="ai_agent", category=category, owner=owner) diff --git a/routes/backup_routes.py b/routes/backup_routes.py index 313369370..4ecf4f165 100644 --- a/routes/backup_routes.py +++ b/routes/backup_routes.py @@ -6,6 +6,7 @@ from datetime import datetime from fastapi import APIRouter, HTTPException, Request, Response from core.middleware import require_admin +from services.memory import MemoryStoreUnreadable from src.auth_helpers import get_current_user from src.settings import load_settings, save_settings, load_features, save_features @@ -76,7 +77,15 @@ def setup_backup_routes(memory_manager, preset_manager, skills_manager) -> APIRo # ── Memories ── if "memories" in body and isinstance(body["memories"], list): - existing = memory_manager.load_all() + # Strict load: importing on top of an unreadable store would write + # only the incoming rows back and drop everything already saved. + try: + existing = memory_manager.load_all_for_update() + except MemoryStoreUnreadable as e: + logger.error("Refusing to import memories: %s", e) + raise HTTPException( + 503, "Memory store is temporarily unreadable — nothing was imported." + ) # Dedup against THIS user's own memories only. Using every tenant's # rows (load_all) meant a memory whose text matched any other # user's was silently skipped, so the importing user lost their own diff --git a/routes/memory/memory_routes.py b/routes/memory/memory_routes.py index d290046ec..c4232bec4 100644 --- a/routes/memory/memory_routes.py +++ b/routes/memory/memory_routes.py @@ -21,7 +21,7 @@ def _strip_list_prefix(text: str) -> str: return text return _LIST_PREFIX_RE.sub("", text, count=1).strip() -from services.memory import MemoryManager +from services.memory import MemoryManager, MemoryStoreUnreadable from core.session_manager import SessionManager from src.request_models import MemoryAddRequest from core.database import SessionLocal @@ -35,6 +35,22 @@ from src.upload_limits import read_upload_limited, MEMORY_IMPORT_MAX_BYTES logger = logging.getLogger(__name__) +def _load_for_update(memory_manager) -> List[Dict[str, Any]]: + """Load the whole store for a read-modify-write cycle. + + A transient read failure must not look like an empty store: the caller + would append to ``[]`` and save that back, atomically destroying every + existing memory (issue #5673). Surface it as a 503 and change nothing. + """ + try: + return memory_manager.load_all_for_update() + except MemoryStoreUnreadable as e: + logger.error("Refusing to rewrite the memory store: %s", e) + raise HTTPException( + 503, "Memory store is temporarily unreadable — no changes were made." + ) + + def setup_memory_routes(memory_manager: MemoryManager, session_manager: SessionManager, memory_vector=None): """Set up memory-related routes.""" router = APIRouter(prefix="/api/memory", tags=["memory"]) @@ -116,7 +132,7 @@ def setup_memory_routes(memory_manager: MemoryManager, session_manager: SessionM new_entry = memory_manager.add_entry(text, memory_data.source, memory_data.category, owner=user) if memory_data.session_id: new_entry["session_id"] = memory_data.session_id - all_mem = memory_manager.load_all() + all_mem = _load_for_update(memory_manager) all_mem.append(new_entry) memory_manager.save(all_mem) # Sync vector index @@ -487,7 +503,7 @@ def setup_memory_routes(memory_manager: MemoryManager, session_manager: SessionM def pin_memory(request: Request, memory_id: str, pinned: bool = Form(True)): """Pin or unpin a memory. Pinned memories are always included in context.""" user = _owner(request) - all_mem = memory_manager.load_all() + all_mem = _load_for_update(memory_manager) for i, memory in enumerate(all_mem): if memory["id"] == memory_id: _verify_memory_owner(memory, user) @@ -512,7 +528,7 @@ def setup_memory_routes(memory_manager: MemoryManager, session_manager: SessionM def update_memory(request: Request, memory_id: str, text: str = Form(...), category: str = Form(None)): """Update an existing memory item with new text and optional category.""" user = _owner(request) - all_mem = memory_manager.load_all() + all_mem = _load_for_update(memory_manager) for i, memory in enumerate(all_mem): if memory["id"] == memory_id: _verify_memory_owner(memory, user) @@ -534,7 +550,7 @@ def setup_memory_routes(memory_manager: MemoryManager, session_manager: SessionM def delete_memory(request: Request, memory_id: str): """Delete a memory item by its ID.""" user = _owner(request) - all_mem = memory_manager.load_all() + all_mem = _load_for_update(memory_manager) # Find and verify ownership before deleting target = next((m for m in all_mem if m["id"] == memory_id), None) diff --git a/services/memory/__init__.py b/services/memory/__init__.py index 53fc80bd8..31fa1d5fa 100644 --- a/services/memory/__init__.py +++ b/services/memory/__init__.py @@ -2,7 +2,7 @@ """Memory service — persistent memory storage and retrieval.""" from .service import MemoryService, Memory, MemorySearchResult -from .memory import MemoryManager +from .memory import MemoryManager, MemoryStoreUnreadable from .memory_vector import MemoryVectorStore __all__ = [ @@ -10,5 +10,6 @@ __all__ = [ "Memory", "MemorySearchResult", "MemoryManager", + "MemoryStoreUnreadable", "MemoryVectorStore", ] diff --git a/services/memory/memory.py b/services/memory/memory.py index 031c13ac4..b9aaaa2a8 100644 --- a/services/memory/memory.py +++ b/services/memory/memory.py @@ -5,6 +5,16 @@ application runtime instantiates ``src.memory.MemoryManager``, so keeping a parallel implementation here risks silent drift between import paths. """ -from src.memory import MemoryManager, get_text_similarity, tokenize +from src.memory import ( + MemoryManager, + MemoryStoreUnreadable, + get_text_similarity, + tokenize, +) -__all__ = ["MemoryManager", "get_text_similarity", "tokenize"] +__all__ = [ + "MemoryManager", + "MemoryStoreUnreadable", + "get_text_similarity", + "tokenize", +] diff --git a/services/memory/memory_extractor.py b/services/memory/memory_extractor.py index e5f609250..11539263b 100644 --- a/services/memory/memory_extractor.py +++ b/services/memory/memory_extractor.py @@ -17,6 +17,8 @@ import os import re from typing import Optional +from src.memory import MemoryStoreUnreadable + logger = logging.getLogger(__name__) @@ -387,7 +389,13 @@ async def extract_and_store( # Get owner from session _owner = getattr(session, 'owner', None) - existing = memory_manager.load_all() + # Strict load: this is a read-modify-write. Degrading to [] here would + # save only the newly extracted facts and drop the entire store. + try: + existing = memory_manager.load_all_for_update() + except MemoryStoreUnreadable as e: + logger.error("Skipping auto memory extraction, store unreadable: %s", e) + return added = 0 for fact in facts: @@ -626,7 +634,18 @@ async def audit_memories( # Merge audited entries back with other users' entries if owner: - all_entries = memory_manager.load_all() + # Strict load: the merge below reconstructs the whole file. If this + # degraded to [] we would save only this owner's audited slice and + # destroy every other tenant's memories. + try: + all_entries = memory_manager.load_all_for_update() + except MemoryStoreUnreadable as e: + logger.error("Aborting memory audit save, store unreadable: %s", e) + return { + "before": before_count, + "after": before_count, + "error": "store_unreadable", + } audited_ids = {e["id"] for e in final_entries} other_entries = [e for e in all_entries if e.get("owner") != owner and (e.get("owner") is not None)] # Also keep legacy entries that weren't part of this audit diff --git a/src/ai_interaction.py b/src/ai_interaction.py index 9ee97368f..e777ca32a 100644 --- a/src/ai_interaction.py +++ b/src/ai_interaction.py @@ -22,6 +22,7 @@ import time from typing import Any, Awaitable, Callable, Dict, Optional, Tuple from src.constants import GENERATED_IMAGES_DIR +from src.memory import MemoryStoreUnreadable logger = logging.getLogger(__name__) @@ -384,7 +385,15 @@ async def do_manage_memory(content: str, session_id: Optional[str] = None, owner return {"error": "Memory text cannot be empty"} entry = _memory_manager.add_entry(text, source="ai_agent", category=category, owner=owner) - memories = _memory_manager.load_all() + # Strict load: this is a read-modify-write, and it is the path an + # ordinary "remember that I prefer X" takes. Degrading to [] here would + # save just this one entry over a store we only failed to read, + # atomically destroying every memory in it (issue #5673). + try: + memories = _memory_manager.load_all_for_update() + except MemoryStoreUnreadable as e: + logger.error("Refusing to add memory, store unreadable: %s", e) + return {"error": "Memory store is temporarily unreadable — nothing was saved."} memories.append(entry) _memory_manager.save(memories) diff --git a/src/memory.py b/src/memory.py index 1d8cdbc1e..92efbf5b2 100644 --- a/src/memory.py +++ b/src/memory.py @@ -10,6 +10,18 @@ from datetime import datetime logger = logging.getLogger(__name__) + +class MemoryStoreUnreadable(RuntimeError): + """memory.json exists on disk but could not be read or parsed. + + "The contents are unknown" is categorically different from "there are no + memories". A read-modify-write caller that conflates the two appends to an + empty view and then persists it, destroying the whole store — the writes + are atomic, so the loss is durable. Raised by + :meth:`MemoryManager.load_all_for_update` so those callers fail closed. + """ + + def tokenize(text: str) -> List[str]: """Simple tokenizer that splits on whitespace and removes punctuation.""" return [word.strip('.,!?";') for word in text.split()] @@ -110,21 +122,69 @@ class MemoryManager: with open(self.memory_file, 'w', encoding='utf-8') as f: json.dump([], f, ensure_ascii=False, indent=2) - def load_all(self) -> List[Dict]: - """Load all memory entries from JSON file (unfiltered).""" + def _read_entries(self) -> List[Dict]: + """Parse the store, or raise :class:`MemoryStoreUnreadable`. + + Returns ``[]`` only when the file genuinely does not exist. Every other + failure mode raises, so callers can tell "no memories" apart from + "couldn't read the memories". + """ if not os.path.exists(self.memory_file): return [] try: with open(self.memory_file, "r", encoding="utf-8") as f: data = json.load(f) - if isinstance(data, list): - return self._validate_entries(data) - except (json.JSONDecodeError, PermissionError) as e: - logger.error("Error loading memory.json: %s", e) - return self._migrate_from_legacy() + except OSError as e: + # PermissionError is an OSError (a scanner holding the file, a + # permissions problem, bad media). + raise MemoryStoreUnreadable( + f"cannot read {self.memory_file}: {e}" + ) from e + except json.JSONDecodeError as e: + # This is the branch that actually destroyed stores: the file reads + # back fine, so nothing stops the save that follows. A truncated + # memory.json is reachable because core/database.py rewrites it with + # a plain open(..,"w") + json.dump during migration. + # + # Preserved behaviour: a corrupt store still gets one shot at the + # pre-JSON memory.txt migration. Only raise when that finds nothing, + # so we never report "empty" for a store we simply failed to parse. + legacy = self._migrate_from_legacy() + if legacy: + return legacy + raise MemoryStoreUnreadable( + f"{self.memory_file} is not valid JSON: {e}" + ) from e - return [] + if not isinstance(data, list): + raise MemoryStoreUnreadable( + f"{self.memory_file} is not a JSON array (got {type(data).__name__})" + ) + return self._validate_entries(data) + + def load_all(self) -> List[Dict]: + """Load all memory entries from JSON file (unfiltered). + + Lenient by design: this feeds display, search, and context-injection + paths, so an unreadable store degrades to an empty list rather than + breaking chat. Never build a value from this that you intend to save + back — use :meth:`load_all_for_update` for that. + """ + try: + return self._read_entries() + except MemoryStoreUnreadable as e: + logger.error("Error loading memory.json: %s", e) + return [] + + def load_all_for_update(self) -> List[Dict]: + """Load for a read-modify-write cycle. + + Propagates :class:`MemoryStoreUnreadable` instead of degrading to ``[]`` + so a caller can never append to an empty view and persist it over a + store that was only temporarily unreadable (issue #5673). + """ + return self._read_entries() def load(self, owner: str = None) -> List[Dict]: """Load memory entries, optionally filtered by owner.""" @@ -135,7 +195,12 @@ class MemoryManager: def claim_ownerless(self, owner: str): """Assign all ownerless memory entries to the given owner.""" - entries = self.load_all() + try: + entries = self.load_all_for_update() + except MemoryStoreUnreadable as e: + # Skip the sweep rather than rewrite the store from an unknown view. + logger.error("Skipping ownerless claim, memory store unreadable: %s", e) + return changed = False claimed = 0 for entry in entries: @@ -235,7 +300,12 @@ class MemoryManager: if not ids: return id_set = set(ids) - entries = self.load_all() + try: + entries = self.load_all_for_update() + except MemoryStoreUnreadable as e: + # Best-effort counter; never worth rewriting the store blind. + logger.error("Skipping uses bump, memory store unreadable: %s", e) + return changed = False for e in entries: if e.get("id") in id_set: diff --git a/src/memory_provider.py b/src/memory_provider.py index 925c59192..8974a6e84 100644 --- a/src/memory_provider.py +++ b/src/memory_provider.py @@ -157,7 +157,11 @@ class NativeMemoryProvider(MemoryProvider): if metadata: entry["metadata"] = dict(metadata) - memories = self.memory_manager.load_all() + # Strict load: read-modify-write. `load_all` degrades an unreadable + # store to [], which would save this single entry over everything + # already stored (issue #5673). The provider API has no error channel, + # so MemoryStoreUnreadable propagates to the caller. + memories = self.memory_manager.load_all_for_update() memories.append(entry) self.memory_manager.save(memories) @@ -223,7 +227,10 @@ class NativeMemoryProvider(MemoryProvider): ] async def delete(self, memory_id: str, *, owner: Optional[str] = None) -> bool: - memories = self.memory_manager.load_all() + # Strict load for the same reason: `remaining` is derived from this + # list and saved back, so it must never be built from a store we + # failed to read. + memories = self.memory_manager.load_all_for_update() remaining = [] deleted_id = None diff --git a/tests/test_backup_import_cross_user_dedup.py b/tests/test_backup_import_cross_user_dedup.py index 2df5936ef..135be78ee 100644 --- a/tests/test_backup_import_cross_user_dedup.py +++ b/tests/test_backup_import_cross_user_dedup.py @@ -27,6 +27,9 @@ def _setup(monkeypatch, store, user="alice"): mem = MagicMock() mem.load_all.return_value = list(store) + # import_data reads through the strict loader so a store it cannot read is + # never overwritten (#5673); the double has to offer the same entry point. + mem.load_all_for_update.return_value = list(store) saved = {} mem.save.side_effect = lambda entries: saved.__setitem__("entries", entries) diff --git a/tests/test_memory_extractor_vector_cross_tenant.py b/tests/test_memory_extractor_vector_cross_tenant.py index 49702c17f..06ca31667 100644 --- a/tests/test_memory_extractor_vector_cross_tenant.py +++ b/tests/test_memory_extractor_vector_cross_tenant.py @@ -67,6 +67,12 @@ class FakeMemoryManager: def load_all(self): return list(self.rows) + def load_all_for_update(self): + # Mirrors the real MemoryManager: extraction is a read-modify-write and + # goes through the strict loader (#5673). A healthy store behaves the + # same as load_all. + return list(self.rows) + def load(self, owner=None): return [r for r in self.rows if r.get("owner") == owner] diff --git a/tests/test_memory_store_unreadable_no_wipe.py b/tests/test_memory_store_unreadable_no_wipe.py new file mode 100644 index 000000000..4b9076065 --- /dev/null +++ b/tests/test_memory_store_unreadable_no_wipe.py @@ -0,0 +1,255 @@ +"""A memory store that cannot be READ must never be overwritten (issue #5673). + +`MemoryManager.save` is atomic, and the add/import/extract paths are all +read-modify-write: load the whole store, append, save it back. `load_all` +used to answer a *failed read* with `[]` — indistinguishable from "no +memories" — so a failed read turned into + + load_all() -> [] -> [].append(new) -> save([new]) + +which atomically replaced the entire store with one entry. + +The trigger that actually bites is a store that is **readable but not +parseable** — a truncated file, or one holding `{}` instead of `[]`. Nothing +obstructs the write, so the request succeeds with HTTP 200 and every existing +memory is destroyed silently. Truncation is reachable: `core/database.py` +rewrites memory.json during migration with a plain `open(..., "w")` + +`json.dump`, which is not atomic. + +A live exclusive lock is NOT the dangerous case: it blocks the read and the +`os.replace` alike, so the save fails too and the store survives (verified +end-to-end — clean dev returns 500 there and loses nothing). + +`load_all_for_update` is the strict loader those callers now use: it raises +`MemoryStoreUnreadable` rather than reporting an empty store. +""" + +import asyncio +import builtins +import json +import os + +import pytest + +from src.memory import MemoryManager, MemoryStoreUnreadable + +_SEED = [ + {"id": "m1", "text": "user prefers dark mode", "owner": "alice"}, + {"id": "m2", "text": "user lives in Berlin", "owner": "alice"}, + {"id": "m3", "text": "bob's cat is called Mila", "owner": "bob"}, +] + + +def _seeded(tmp_path): + m = MemoryManager(str(tmp_path)) + m.save([dict(e) for e in _SEED]) + return m + + +def _break_reads_of(monkeypatch, target, exc): + """Make open() raise `exc` for `target` only, leaving every other path alone.""" + real_open = builtins.open + + def fake_open(file, mode="r", *args, **kwargs): + if os.path.abspath(str(file)) == os.path.abspath(target) and "r" in mode: + raise exc + return real_open(file, mode, *args, **kwargs) + + monkeypatch.setattr(builtins, "open", fake_open) + + +# ── the strict loader signals, rather than reporting "empty" ────────────── + +def test_strict_load_raises_on_permission_error(tmp_path, monkeypatch): + m = _seeded(tmp_path) + _break_reads_of(monkeypatch, m.memory_file, PermissionError(13, "locked")) + with pytest.raises(MemoryStoreUnreadable): + m.load_all_for_update() + + +def test_strict_load_raises_on_corrupt_json(tmp_path): + m = _seeded(tmp_path) + with open(m.memory_file, "w", encoding="utf-8") as f: + f.write('[{"id": "m1", "text": "truncated mid-writ') + with pytest.raises(MemoryStoreUnreadable): + m.load_all_for_update() + + +def test_strict_load_raises_when_store_is_not_a_list(tmp_path): + # A file holding `{}` or `null` is not an empty store, it is a broken one. + m = _seeded(tmp_path) + with open(m.memory_file, "w", encoding="utf-8") as f: + json.dump({}, f) + with pytest.raises(MemoryStoreUnreadable): + m.load_all_for_update() + + +def test_strict_load_returns_entries_when_healthy(tmp_path): + m = _seeded(tmp_path) + assert {e["id"] for e in m.load_all_for_update()} == {"m1", "m2", "m3"} + + +def test_strict_load_returns_empty_when_file_genuinely_absent(tmp_path): + m = _seeded(tmp_path) + os.remove(m.memory_file) + # Absent is the one case that legitimately means "no memories yet". + assert m.load_all_for_update() == [] + + +# ── read paths stay lenient, so an unreadable store can't break chat ────── + +def test_read_path_still_degrades_to_empty(tmp_path, monkeypatch): + m = _seeded(tmp_path) + _break_reads_of(monkeypatch, m.memory_file, PermissionError(13, "locked")) + # Context injection / search must not raise; they just see nothing. + assert m.load_all() == [] + assert m.load(owner="alice") == [] + + +# ── the actual #5673 regression: the store survives ─────────────────────── + +def test_add_cycle_under_transient_read_error_does_not_wipe(tmp_path, monkeypatch): + """Mirrors routes/memory/memory_routes.py api_add_memory exactly.""" + m = _seeded(tmp_path) + new_entry = m.add_entry("a brand new fact", owner="alice") + + with monkeypatch.context() as mp: + _break_reads_of(mp, m.memory_file, PermissionError(13, "locked")) + with pytest.raises(MemoryStoreUnreadable): + all_mem = m.load_all_for_update() + all_mem.append(new_entry) + m.save(all_mem) + + # Reads work again; every original memory is still there and the file was + # never replaced by the single new entry. + assert {e["id"] for e in m.load_all()} == {"m1", "m2", "m3"} + + +def test_audit_merge_cannot_drop_other_tenants(tmp_path, monkeypatch): + """The audit path rebuilds the whole file from load_all + one owner's slice. + + Reading [] there would save only the audited owner's entries and destroy + every other tenant's memories, so it has to fail closed too. + """ + m = _seeded(tmp_path) + alice_slice = [e for e in _SEED if e["owner"] == "alice"] + + with monkeypatch.context() as mp: + _break_reads_of(mp, m.memory_file, PermissionError(13, "locked")) + with pytest.raises(MemoryStoreUnreadable): + all_entries = m.load_all_for_update() + others = [e for e in all_entries if e.get("owner") != "alice"] + m.save(alice_slice + others) + + assert any(e["id"] == "m3" for e in m.load_all()), "bob's memory was destroyed" + + +def test_uses_bump_skips_write_when_unreadable(tmp_path, monkeypatch): + m = _seeded(tmp_path) + with monkeypatch.context() as mp: + _break_reads_of(mp, m.memory_file, PermissionError(13, "locked")) + m.increment_uses(["m1"]) # must not raise, must not write + assert {e["id"] for e in m.load_all()} == {"m1", "m2", "m3"} + + +def test_claim_ownerless_skips_write_when_unreadable(tmp_path, monkeypatch): + m = _seeded(tmp_path) + with monkeypatch.context() as mp: + _break_reads_of(mp, m.memory_file, PermissionError(13, "locked")) + m.claim_ownerless("alice") + assert {e["id"] for e in m.load_all()} == {"m1", "m2", "m3"} + + +# ── the add sinks users actually reach ──────────────────────────────────── +# +# The tests above replay the read-modify-write shape. These drive the real +# entry points end to end, because those are what #5673 reports: "remember +# that I prefer X" in ordinary chat (src/ai_interaction.py do_manage_memory, +# routed from src/tool_execution.py) and the built-in memory MCP server +# (mcp_servers/memory_server.py, registered in src/builtin_mcp.py). +# +# They use a truncated store rather than a read error on purpose: it reads +# fine, so nothing stops the save, which is the case that silently destroyed +# stores. The assertion is that the file is left byte-identical — still broken, +# but still holding the user's memories, so it can be repaired by hand. + + +def _truncated_store(tmp_path): + """Seed a store that reads back fine but no longer parses.""" + m = _seeded(tmp_path) + good = json.dumps([dict(e) for e in _SEED], indent=2) + with open(m.memory_file, "w", encoding="utf-8") as f: + f.write(good[:good.rindex("]")]) # drop the closing bracket only + with open(m.memory_file, "rb") as f: + return m, f.read() + + +def _on_disk(manager) -> bytes: + with open(manager.memory_file, "rb") as f: + return f.read() + + +def test_agent_memory_add_does_not_overwrite_unreadable_store(tmp_path, monkeypatch): + """src/ai_interaction.py do_manage_memory, action "add".""" + from src import ai_interaction + + manager, before = _truncated_store(tmp_path) + monkeypatch.setattr(ai_interaction, "_memory_manager", manager) + monkeypatch.setattr(ai_interaction, "_memory_vector", None) + + result = asyncio.run(ai_interaction.do_manage_memory("add\nuser prefers tabs")) + + assert _on_disk(manager) == before, "the unreadable store was overwritten" + assert b"m3" in _on_disk(manager) + assert "error" in result, "the add reported success over an unreadable store" + + +def test_mcp_memory_add_does_not_overwrite_unreadable_store(tmp_path, monkeypatch): + """mcp_servers/memory_server.py, action "add".""" + import mcp_servers.memory_server as memory_server + + manager, before = _truncated_store(tmp_path) + monkeypatch.setattr(memory_server, "_memory_manager", manager) + monkeypatch.setattr(memory_server, "_memory_vector", None) + monkeypatch.setattr(memory_server, "_initialized", True) + for key in memory_server._OWNER_ENV_KEYS: + monkeypatch.delenv(key, raising=False) + + result = asyncio.run(memory_server.call_tool( + "manage_memory", {"action": "add", "text": "user prefers tabs"} + )) + + assert _on_disk(manager) == before, "the unreadable store was overwritten" + assert b"m3" in _on_disk(manager) + assert result[0].text.startswith("Error:") + + +def test_native_provider_remember_does_not_overwrite_unreadable_store(tmp_path): + """src/memory_provider.py NativeMemoryProvider.remember. + + Registered into app state in src/app_initializer.py but not yet consumed + outside tests, so this is the pattern held in place before it goes live. + """ + from src.memory_provider import NativeMemoryProvider + + manager, before = _truncated_store(tmp_path) + provider = NativeMemoryProvider(manager) + + with pytest.raises(MemoryStoreUnreadable): + asyncio.run(provider.remember("user prefers tabs", owner="alice")) + + assert _on_disk(manager) == before + + +# ── the legacy memory.txt migration is preserved ────────────────────────── + +def test_corrupt_store_still_migrates_from_legacy_txt(tmp_path): + m = _seeded(tmp_path) + with open(m.memory_file, "w", encoding="utf-8") as f: + f.write("{ not json") + legacy = os.path.join(str(tmp_path), "memory.txt") + with open(legacy, "w", encoding="utf-8") as f: + f.write("recovered fact one\nrecovered fact two\n") + + entries = m.load_all_for_update() + assert [e["text"] for e in entries] == ["recovered fact one", "recovered fact two"] From 45fc3938e0debef94ce485843ea2cf7e787ac975 Mon Sep 17 00:00:00 2001 From: Mubelotix <mubelotix@gmail.com> Date: Thu, 6 Aug 2026 19:18:56 +0200 Subject: [PATCH 036/125] Fixes Star History section in README Fixes part of #5563 --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 705ec6b68..098645b07 100644 --- a/README.md +++ b/README.md @@ -63,11 +63,11 @@ Odysseus is a self-hosted workspace with powerful local tools. Keep auth enabled ## Star History -<a href="https://www.star-history.com/?repos=odysseus-dev%2Fodysseus&type=date&legend=top-left"> +<a href="https://star-history.dera.page/#odysseus-dev/odysseus&type=date&legend=top-left"> <picture> - <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=odysseus-dev/odysseus&type=date&theme=dark&legend=top-left" /> - <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=odysseus-dev/odysseus&type=date&legend=top-left" /> - <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=odysseus-dev/odysseus&type=date&legend=top-left" /> + <source media="(prefers-color-scheme: dark)" srcset="https://star-history.dera.page/svg?repos=odysseus-dev/odysseus&type=date&theme=dark&legend=top-left" /> + <source media="(prefers-color-scheme: light)" srcset="https://star-history.dera.page/svg?repos=odysseus-dev/odysseus&type=date&legend=top-left" /> + <img alt="Star History Chart" src="https://star-history.dera.page/svg?repos=odysseus-dev/odysseus&type=date&legend=top-left" /> </picture> </a> From 5ddef23d949b0ebb5ed90d6ae4904798d1619bca Mon Sep 17 00:00:00 2001 From: adabarbulescu <94562950+adabarbulescu@users.noreply.github.com> Date: Fri, 7 Aug 2026 20:12:21 +0300 Subject: [PATCH 037/125] fix(welcome): rotate startup tips (#5871) --- static/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/index.html b/static/index.html index 8257660fe..0136f2316 100644 --- a/static/index.html +++ b/static/index.html @@ -1005,7 +1005,7 @@ var tips = mobile ? phone : desktop; var el = document.getElementById('welcome-tip'); if (el) { - el.textContent = 'Pick a model if you want, or just type.'; + el.textContent = tips[Math.floor(Math.random() * tips.length)]; } fetch('/api/version').then(function(r){return r.json()}).then(function(d){ if (d.version) window._appVersion = d.version; From 36d409842177e18017dac2fa4bbc5266bb451ac7 Mon Sep 17 00:00:00 2001 From: Jakub Grula <ramsters110@gmail.com> Date: Fri, 7 Aug 2026 19:15:50 +0200 Subject: [PATCH 038/125] fix: Edit box formatting was removing triple tick boxes (#5737) --- static/js/chat.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/static/js/chat.js b/static/js/chat.js index ea2d8c1bb..ca583c5fc 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -4787,7 +4787,8 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr if (msgIndex < 0) return; const bodyEl = userMsgElement.querySelector('.body'); - const currentText = bodyEl ? bodyEl.textContent.trim().replace(/\s*\[\d+ attachment\(s\)\]$/, '') : ''; + let currentText = (userMsgElement.dataset.raw || (bodyEl ? bodyEl.textContent : '') || '').trim(); + currentText = currentText.replace(/\s*\[\d+ attachment\(s\)\]$/, ''); // Replace body with an editable textarea const editor = document.createElement('textarea'); From f1e96d102e5692fca3a91b1a40f46e22028e7ecb Mon Sep 17 00:00:00 2001 From: Husam <husamemad60@gmail.com> Date: Fri, 7 Aug 2026 20:33:14 +0300 Subject: [PATCH 039/125] fix(tool_parsing): require a pipe on the Qwen bare end marker (#5829) The `end` branch of _QWEN_BARE_MARKER_RE had both pipes optional (`\|?end\|?`), so it also matched a bare `end` between whitespace and replaced it with a space. Messages containing Ruby, Lua or shell code that closes a block with a lone `end` had those lines deleted, and ordinary prose lost the word too. Require at least one pipe so only real turn markers match; `|end`, `end|`, `|end|` and `/|end|` strip exactly as before. Applied to the duplicated pattern in static/js/chatRenderer.js as well. Fixes #5547 --- src/tool_parsing.py | 6 +- static/js/chatRenderer.js | 5 +- tests/test_tool_parsing_bare_end_marker.py | 96 ++++++++++++++++++++++ 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 tests/test_tool_parsing_bare_end_marker.py diff --git a/src/tool_parsing.py b/src/tool_parsing.py index 2885cc00f..98dc1b5f6 100644 --- a/src/tool_parsing.py +++ b/src/tool_parsing.py @@ -187,8 +187,12 @@ _FUNCTION_MODEL_NAME_RE = re.compile( _FUNCTION_MODEL_PARAMS_OPEN_RE = re.compile(r"<parameters>\s*", re.IGNORECASE) _FUNCTION_MODEL_PARAMS_CLOSE_RE = re.compile(r"</parameters>", re.IGNORECASE) _QWEN_ROLE_MARKER_RE = re.compile(r"</?\|(?:assistant|assistan|user|system|tool)\|>?|</\|end\|>?", re.IGNORECASE) +# At least one pipe is required around `end`. Both pipes used to be optional +# (`\|?end\|?`), which also matched a bare `end` on its own line and deleted it +# from ordinary prose and from Ruby/Lua/shell snippets that close blocks with +# one; see #5547. `|end`, `end|`, `|end|` and `/|end|` still strip as before. _QWEN_BARE_MARKER_RE = re.compile( - r"(?:^|[\t\r\n ])(?:\|?end\|?|/?\|end\|)(?=[\t\r\n ]|$)|" + r"(?:^|[\t\r\n ])(?:/?\|end\||\|end|end\|)(?=[\t\r\n ]|$)|" r"(?:^|[\t\r\n ])assistan(?:t)?(?=[\t\r\n ]|$)", re.IGNORECASE, ) diff --git a/static/js/chatRenderer.js b/static/js/chatRenderer.js index 10709679d..1d6e2e4a9 100644 --- a/static/js/chatRenderer.js +++ b/static/js/chatRenderer.js @@ -478,7 +478,10 @@ const DSML_STRAY_RE = /<\s*\/?\s*[||]+\s*DSML\s*[||]+[^>]*>/gi; const DSML_INVOKE_RE = /<\s*[||]+\s*DSML\s*[||]+\s*invoke\b[^>]*>[\s\S]*?(?:<\s*\/\s*[||]+\s*DSML\s*[||]+\s*invoke\s*>|$)/gi; const RAW_OPENAI_TOOL_JSON_RE = /(?:\[\s*)?\{\s*"function"\s*:\s*\{[\s\S]*?\}\s*,\s*"id"\s*:\s*"[^"]*"\s*,\s*"type"\s*:\s*"function"\s*\}\s*\]?/gi; const QWEN_ROLE_MARKER_RE = /<\/?\|(?:assistant|assistan|user|system|tool)\|>?|<\/\|end\|>?/gi; -const QWEN_BARE_MARKER_RE = /(?:^|[\t\r\n ])(?:\|?end\|?|\/?\|end\|)(?=[\t\r\n ]|$)|(?:^|[\t\r\n ])assistan(?:t)?(?=[\t\r\n ]|$)/gi; +// Keep in sync with _QWEN_BARE_MARKER_RE in src/tool_parsing.py. At least one +// pipe is required around `end`: with both optional (`\|?end\|?`) this also ate +// a bare `end` on its own line, breaking Ruby/Lua/shell snippets (#5547). +const QWEN_BARE_MARKER_RE = /(?:^|[\t\r\n ])(?:\/?\|end\||\|end|end\|)(?=[\t\r\n ]|$)|(?:^|[\t\r\n ])assistan(?:t)?(?=[\t\r\n ]|$)/gi; // Self-narration about tool results (model echoing stdout/exit_code) const TOOL_NARRATION_RE = /(?:The (?:result|output) shows?:?\s*)?-?\s*(?:stdout|stderr|exit_code):\s*.+/gi; diff --git a/tests/test_tool_parsing_bare_end_marker.py b/tests/test_tool_parsing_bare_end_marker.py new file mode 100644 index 000000000..6167c8dde --- /dev/null +++ b/tests/test_tool_parsing_bare_end_marker.py @@ -0,0 +1,96 @@ +"""Regression: the Qwen bare-marker scrub must not eat a lone `end` (#5547). + +`_QWEN_BARE_MARKER_RE` cleans Qwen turn markers that leak into content. Its +`end` branch was `\\|?end\\|?` — both pipes optional — so it also matched a bare +`end` surrounded by whitespace and replaced it with a space. Any message +containing Ruby, Lua or shell code that closes a block with a lone `end` had +those lines silently deleted, in the stored text and in the rendered message. + +Requiring at least one pipe keeps every real marker (`|end`, `end|`, `|end|`, +`/|end|`) stripping as before. The same pattern is duplicated in +static/js/chatRenderer.js, so the JS copy is checked here too — the two must +not drift. +""" +import json +import re +import shutil +import subprocess +from pathlib import Path + +import pytest + +import src.agent_tools # noqa: F401 (break agent_tools<->tool_parsing import cycle) +from src.tool_parsing import strip_tool_blocks + +_REPO = Path(__file__).resolve().parent.parent +_CHAT_RENDERER = _REPO / "static" / "js" / "chatRenderer.js" + +# Inputs that must survive untouched, and the substring that proves they did. +KEPT = [ + ("loop do\n puts \"yo\"\nend\n", "\nend"), # the reported Ruby case + ("if x then\nend", "\nend"), + ("function f()\nend\n", "\nend"), + ("a end b", "a end b"), + ("append end", "append end"), + ("END", "END"), + ("\nEnd\n", "End"), +] + +# Real markers — at least one pipe, plus the role word — with the exact output +# they must still produce. Asserted as equality rather than "marker not in out" +# so narrowing the pattern can't pass by deleting more than it should. +STRIPPED = [ + ("a |end| b", "a b"), + ("a /|end| b", "a b"), + ("a |end b", "a b"), + ("a end| b", "a b"), + ("x assistant y", "x y"), +] + + +@pytest.mark.parametrize("text,kept", KEPT) +def test_bare_end_survives_stripping(text, kept): + assert kept in strip_tool_blocks(text) + + +@pytest.mark.parametrize("text,expected", STRIPPED) +def test_piped_end_markers_are_still_stripped(text, expected): + assert strip_tool_blocks(text) == expected + + +def test_bare_end_inside_a_fenced_block_survives(): + """The scrub runs over the whole message, fenced regions included.""" + out = strip_tool_blocks("Here:\n```ruby\nloop do\n puts 1\nend\n```\nDone.") + assert "\nend\n" in out + + +def _js_bare_marker_regex_source(): + src = _CHAT_RENDERER.read_text(encoding="utf-8") + m = re.search(r"^const QWEN_BARE_MARKER_RE = (/.*/[gimsuy]*);$", src, re.MULTILINE) + assert m, "QWEN_BARE_MARKER_RE literal not found in chatRenderer.js" + return m.group(1) + + +def test_js_copy_of_the_pattern_matches_the_python_one(): + """Guard the duplication: the JS branch must require a pipe too.""" + if shutil.which("node") is None: + pytest.skip("node binary not on PATH") + + cases = [text for text, _ in KEPT] + [text for text, _ in STRIPPED] + script = ( + "const RE = %s;\n" + "const cases = JSON.parse(process.argv[1]);\n" + "console.log(JSON.stringify(cases.map(c => c.replace(RE, ' '))));" + % _js_bare_marker_regex_source() + ) + result = subprocess.run( + ["node", "--input-type=module", "-e", script, json.dumps(cases)], + cwd=_REPO, capture_output=True, timeout=15, text=True, + ) + assert result.returncode == 0, f"node failed:\n{result.stderr}" + got = json.loads(result.stdout.splitlines()[-1]) + + for (text, kept), out in zip(KEPT, got): + assert kept in out, f"JS regex dropped {kept!r} from {text!r}" + for (text, expected), out in zip(STRIPPED, got[len(KEPT):]): + assert out == expected, f"JS regex: {text!r} -> {out!r}, expected {expected!r}" From 99566d28b53cdd53efaecff05a61ef3b7b5daa06 Mon Sep 17 00:00:00 2001 From: Husam <husamemad60@gmail.com> Date: Fri, 7 Aug 2026 20:34:50 +0300 Subject: [PATCH 040/125] fix(chat): stop ArrowUp from eating an unsent multi-line prompt (#5875) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit static/app.js carried a near-verbatim copy of the prompt-recall logic in static/js/composerArrowUpRecall.js, wired as a second capture-phase keydown listener on the same #message textarea. The copy omitted the draft guard the module has: it called preventDefault() and stopImmediatePropagation() unconditionally, then recalled history[0] over whatever the user had typed. Because it stopped immediate propagation, the copy won regardless of registration order — if it ran first the module never saw the event, and if it ran second the module had already declined to stop propagation on an unmatched draft. The guard at composerArrowUpRecall.js:109 was unreachable on the real page, so ArrowUp on a multi-line draft replaced it with the last sent prompt instead of moving the caret up a line. Delete the duplicate. The module keeps ownership of ArrowUp/ArrowDown recall, which is the behavior MODULE_SUMMARY.md documents ("on an empty composer") and the behavior tests/test_composer_arrow_up_recall_js.py already pins via test_non_empty_composer_does_not_recall and test_multiline_caret_navigation_preserved. Also correct a stale comment in the module that described the deleted behavior and contradicted the guard 35 lines above it, and add a regression test asserting app.js does not reintroduce a second handler. Fixes #5862 --- static/app.js | 83 ++--------------------- static/js/composerArrowUpRecall.js | 6 +- tests/test_composer_arrow_up_recall_js.py | 21 ++++++ 3 files changed, 28 insertions(+), 82 deletions(-) diff --git a/static/app.js b/static/app.js index 97f0ae77e..c9e3a567f 100644 --- a/static/app.js +++ b/static/app.js @@ -3908,85 +3908,10 @@ function startOdysseusApp() { const messageInput = el('message'); const modelPickerWrap = document.getElementById('model-picker-wrap'); - function _readComposerPromptHistory() { - const chatBox = document.getElementById('chat-history'); - if (!chatBox) return []; - return Array.from(chatBox.querySelectorAll('.msg-user')) - .reverse() - .map(msg => { - const body = msg.querySelector('.body'); - return msg.dataset?.raw || (body ? body.textContent : '') || ''; - }) - .filter(Boolean); - } - - if (messageInput && !messageInput._odysseusPromptRecallCapture) { - messageInput._odysseusPromptRecallCapture = true; - let recallHistory = []; - let recallIndex = -1; - let lastRecalled = ''; - const norm = (v) => String(v || '').replace(/\r\n/g, '\n').trimEnd(); - messageInput.addEventListener('input', () => { - if (norm(messageInput.value) === norm(lastRecalled)) return; - recallHistory = []; - recallIndex = -1; - lastRecalled = ''; - try { delete messageInput.dataset.odysseusRecallIndex; } catch {} - }, true); - messageInput.addEventListener('keydown', (e) => { - if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; - if (e.shiftKey || e.altKey || e.ctrlKey || e.metaKey || e.isComposing) return; - if (window._ghostAutocomplete?.isActive?.()) return; - const fresh = _readComposerPromptHistory(); - const history = fresh.length ? fresh : recallHistory; - if (!history.length) return; - const current = norm(messageInput.value); - let currentIndex = current ? history.findIndex(item => norm(item) === current) : -1; - if (current && currentIndex < 0 && current === norm(lastRecalled)) currentIndex = recallIndex; - if (current && currentIndex < 0) { - const markedIndex = Number(messageInput.dataset.odysseusRecallIndex); - if (Number.isInteger(markedIndex) && markedIndex >= 0 && markedIndex < history.length) { - currentIndex = markedIndex; - } - } - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - if (e.key === 'ArrowDown') { - if (currentIndex < 0) return; - const nextIndex = currentIndex - 1; - if (nextIndex < 0) { - recallHistory = history; - recallIndex = -1; - lastRecalled = ''; - try { delete messageInput.dataset.odysseusRecallIndex; } catch {} - messageInput.value = ''; - try { messageInput.selectionStart = messageInput.selectionEnd = 0; } catch {} - try { uiModule.autoResize(messageInput); } catch {} - return; - } - const recalled = history[nextIndex]; - recallHistory = history; - recallIndex = nextIndex; - lastRecalled = recalled; - try { messageInput.dataset.odysseusRecallIndex = String(nextIndex); } catch {} - messageInput.value = recalled; - try { messageInput.selectionStart = messageInput.selectionEnd = recalled.length; } catch {} - try { uiModule.autoResize(messageInput); } catch {} - return; - } - const nextIndex = currentIndex >= 0 ? Math.min(currentIndex + 1, history.length - 1) : 0; - const recalled = history[nextIndex]; - if (!recalled) return; - recallHistory = history; - recallIndex = nextIndex; - lastRecalled = recalled; - try { messageInput.dataset.odysseusRecallIndex = String(nextIndex); } catch {} - messageInput.value = recalled; - try { messageInput.selectionStart = messageInput.selectionEnd = recalled.length; } catch {} - try { uiModule.autoResize(messageInput); } catch {} - }, true); - } + // ArrowUp/ArrowDown prompt recall on #message lives in + // static/js/composerArrowUpRecall.js (wired from chat.js). Do not re-add a + // copy here: two capture-phase listeners on the same textarea meant the one + // without the draft guard won and ate unsent multi-line prompts (#5862). const _sendIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>'; const _micIcon = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'; diff --git a/static/js/composerArrowUpRecall.js b/static/js/composerArrowUpRecall.js index e0b20d6b4..83141bfe9 100644 --- a/static/js/composerArrowUpRecall.js +++ b/static/js/composerArrowUpRecall.js @@ -143,9 +143,9 @@ export function wireArrowUpRecall(composer, getUserMessages, options = {}) { return; } - // ArrowUp owns prompt history in the chat composer. If the current text - // is not already a recalled prompt, start from newest instead of letting - // the browser move the caret inside the textarea. + // ArrowUp walks older prompts. An unmatched draft already returned above, + // so reaching here means the composer is empty or holds a recalled prompt + // — the caret-navigation case is never hijacked. const nextIndex = currentIndex >= 0 ? Math.min(currentIndex + 1, history.length - 1) : 0; const recalled = history[nextIndex]; if (!recalled) { diff --git a/tests/test_composer_arrow_up_recall_js.py b/tests/test_composer_arrow_up_recall_js.py index eadc3bc94..022fcbc02 100644 --- a/tests/test_composer_arrow_up_recall_js.py +++ b/tests/test_composer_arrow_up_recall_js.py @@ -306,3 +306,24 @@ def test_integration_recalls_from_chat_history_dom(): ) assert proc.returncode == 0, proc.stderr assert json.loads(proc.stdout.strip()) == {"value": "stored prompt", "prevented": True} + + +def test_prompt_recall_is_not_duplicated_in_app_js(): + """Only composerArrowUpRecall.js may own ArrowUp on #message (issue #5862). + + static/app.js once carried a near-verbatim copy of this recall logic, wired + as a second capture-phase listener on the same textarea. That copy lacked + the draft guard here, and because it called stopImmediatePropagation it won + regardless of registration order — so a typed multi-line prompt was replaced + by the last sent one instead of the caret moving up a line. + """ + app_js = (_REPO / "static" / "app.js").read_text(encoding="utf-8") + for marker in ( + "_odysseusPromptRecallCapture", + "_readComposerPromptHistory", + "odysseusRecallIndex", + ): + assert marker not in app_js, ( + f"static/app.js reintroduces prompt recall ({marker!r}); " + "it belongs to static/js/composerArrowUpRecall.js alone" + ) From f06a0a30a80e6739a7f2d7f9b23ed38a8ffb21fd Mon Sep 17 00:00:00 2001 From: Samy <12219635+touzenesmy@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:04:53 -0400 Subject: [PATCH 041/125] fix(session): restore session URL hash writes (removed in cf4e240a) (#5872) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix: restore session URL hash writes (removed in cf4e240a) Restores history.replaceState() calls in selectSession() and materializePendingSession() that were dropped during the July 23 merge. Without these, chat URLs never update the address bar hash, making sessions unshareable and causing bare-URL reloads to land on the welcome screen instead of restoring the last active chat. Root cause: selectSession() had its hash-write deliberately removed; materializePendingSession() lost its during a larger refactor that added the stale-response and incognito guards. Fixes #5870 (upstream) * fix: session URL hash lost when sending message mid-stream Two independent bugs caused the session hash to disappear from the URL: Bug 1 — ReferenceError in catch block silently killed error recovery In handleChatSubmit, two const variables (streamingTTS at line 1922 and abortCtrl at line 1741) were declared inside the try block but referenced in the catch block. Since const is block-scoped in JavaScript, they were undefined in catch, causing a ReferenceError that silently aborted the error handler. This prevented materializePendingSession() from ever being called, so no hash was written to the URL. Fix: Hoisted both as let declarations before the try { block. Bug 2 — Dual sessions.js ES module instances with mismatched state app.js imported sessions.js with a version query string (?v=20260722ctxheader4) while every other module imported ./sessions.js without one. The browser treated them as different URLs, creating two separate module instances with independent _pendingChat and currentSessionId state. createDirectChat() set pending on one instance while handleChatSubmit() checked hasPendingChat() on the other — so the pending session never materialized. Fix: Removed the version query string from the sessions.js import in app.js and from the modulepreload + script tags in index.html. All modules now share a single sessions.js instance. Bonus guard: _adoptOpenedSessionBeforeAutoCreate() now checks hasPendingChat() before adopting a stale DOM-active session, preventing the send path from landing in the wrong session when a New Chat is pending. --------- Co-authored-by: samy <samy@users.noreply.github.com> --- static/app.js | 4 ++-- static/index.html | 8 ++++---- static/js/chat.js | 9 +++++++-- static/js/sessions.js | 5 +++++ 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/static/app.js b/static/app.js index c9e3a567f..9d05d8991 100644 --- a/static/app.js +++ b/static/app.js @@ -10,14 +10,14 @@ import modelsModule from './js/models.js?v=20260715startupcalm2'; import ragModule from './js/rag.js'; import presetsModule from './js/presets.js'; import searchModule from './js/search.js'; -import chatModule from './js/chat.js?v=20260722ctxheader4'; +import chatModule from './js/chat.js?v=20260801fix1'; import compareModule from './js/compare/index.js?v=20260723compareicon2'; import documentModule from './js/document.js?v=20260722emailfastindex1'; import searchChatModule from './js/search-chat.js'; import { makeWindowDraggable } from './js/windowDrag.js'; import markdownModule from './js/markdown.js'; import chatRenderer from './js/chatRenderer.js?v=20260722emailfastindex1'; -import sessionModule from './js/sessions.js?v=20260722ctxheader4'; +import sessionModule from './js/sessions.js'; import memoryModule from './js/memory.js?v=20260722memoryloading1'; import voiceRecorderModule from './js/voiceRecorder.js'; import censorModule from './js/censor.js'; diff --git a/static/index.html b/static/index.html index 0136f2316..0fea836ba 100644 --- a/static/index.html +++ b/static/index.html @@ -250,9 +250,9 @@ </script> <link rel="stylesheet" href="/static/style.css?v=20260723tasksbulkfeedback1"> <link rel="modulepreload" href="/static/app.js?v=20260723tasksbulkfeedback1"> - <link rel="modulepreload" href="/static/js/chat.js?v=20260722ctxheader4"> + <link rel="modulepreload" href="/static/js/chat.js?v=20260801fix1"> <link rel="modulepreload" href="/static/js/ui.js"> - <link rel="modulepreload" href="/static/js/sessions.js?v=20260722ctxheader4"> + <link rel="modulepreload" href="/static/js/sessions.js"> <link rel="modulepreload" href="/static/js/markdown.js"> </head> <body> @@ -2504,7 +2504,7 @@ <script type="module" src="/static/js/ui.js"></script> <script type="module" src="/static/js/markdown.js"></script> <script type="module" src="/static/js/dragSort.js"></script> -<script type="module" src="/static/js/sessions.js?v=20260722ctxheader4"></script> +<script type="module" src="/static/js/sessions.js"></script> <script type="module" src="/static/js/memory.js?v=20260722memoryloading1"></script> <script type="module" src="/static/js/skills.js"></script> <script type="module" src="/static/js/tourHints.js"></script> @@ -2522,7 +2522,7 @@ <script type="module" src="/static/js/chatRenderer.js?v=20260722emailfastindex1"></script> <script type="module" src="/static/js/codeRunner.js"></script> <script type="module" src="/static/js/chatStream.js?v=20260722emailfastindex1"></script> - <script type="module" src="/static/js/chat.js?v=20260722ctxheader4"></script> + <script type="module" src="/static/js/chat.js?v=20260801fix1"></script> <script type="module" src="/static/js/cookbook.js"></script> <script src="/static/js/cookbookSchedule.js"></script> <script type="module" src="/static/js/search-chat.js"></script> diff --git a/static/js/chat.js b/static/js/chat.js index ca583c5fc..3c8bbe850 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -349,6 +349,9 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr async function _adoptOpenedSessionBeforeAutoCreate() { if (!sessionModule || !sessionModule.getCurrentSessionId || sessionModule.getCurrentSessionId()) return true; + // Don't adopt a stale session when the user explicitly started a New Chat + // (pending state set) — the send path must materialize the pending session. + if (sessionModule.hasPendingChat && sessionModule.hasPendingChat()) return false; const activeRowId = document.querySelector('.list-item.active-session[data-session-id], .session-item.active[data-session-id]')?.dataset?.sessionId || ''; const hashId = _hashSessionCandidate(); const lastSelectedId = String(window.__odysseusLastSelectedSessionId || '').trim(); @@ -1403,6 +1406,8 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr currentAccumulated = ''; currentHolder = null; + let abortCtrl = null; + let streamingTTS = false; try { // Re-enable auto-scroll when user sends a message uiModule.setAutoScroll(true); @@ -1716,7 +1721,7 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr } - const abortCtrl = new AbortController(); + abortCtrl = new AbortController(); abortCtrl._reason = ''; currentAbort = abortCtrl; @@ -1897,7 +1902,7 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr let isThinking = false; let thinkingStartTime = null; // Streaming TTS: synthesize sentence-by-sentence during streaming - const streamingTTS = !!(window.aiTTSManager && window.aiTTSManager.autoPlay && window.aiTTSManager.available); + streamingTTS = !!(window.aiTTSManager && window.aiTTSManager.autoPlay && window.aiTTSManager.available); if (streamingTTS) window.aiTTSManager.streamingStart(); // Multi-bubble agent tracking let roundHolder = holder; // Current AI text bubble (changes per round) diff --git a/static/js/sessions.js b/static/js/sessions.js index cf59d478c..edf83c8a4 100644 --- a/static/js/sessions.js +++ b/static/js/sessions.js @@ -1847,6 +1847,10 @@ export async function selectSession(id, { keepSidebar = false, showLoading = tru const _isTransientChat = !!_meta && (_meta.folder === 'Assistant' || _meta.folder === 'Tasks'); if (!_isTransientChat) { Storage.set('lastSessionId', id); + // Update URL hash without triggering hashchange handler + if (window.location.hash !== '#' + id) { + history.replaceState(null, '', '#' + id); + } } // Restore character preset for persistent chats try { @@ -2313,6 +2317,7 @@ export async function materializePendingSession() { currentSessionId = payload.id; if (!isIncognito) { Storage.set('lastSessionId', payload.id); + history.replaceState(null, '', '#' + payload.id); } // Reload the sidebar in the background. Awaiting this used to block the first From 378518f6dfb994481a8ec5fbb0032d4a8c23c4e1 Mon Sep 17 00:00:00 2001 From: Samy <12219635+touzenesmy@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:06:17 -0400 Subject: [PATCH 042/125] Fix #5870: stale skills panel data on tab reopen (#5876) Remove early-return guard in loadSkills() that skipped both API re-fetch and renderSkillsList() when the Skills tab was reopened after first load. The cascade entrance animation is already handled inside renderSkillsList() via _cascadeNext, so the guard was unnecessary and caused deleted/edited skills to remain visible until a full page reload. Co-authored-by: samy <samy@users.noreply.github.com> --- static/js/skills.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/static/js/skills.js b/static/js/skills.js index 84974d446..b45403570 100644 --- a/static/js/skills.js +++ b/static/js/skills.js @@ -83,11 +83,9 @@ export async function loadSkills(cascade = false) { // Play the domino-in entrance on this load (set when the tab is opened, // not for the silent re-loads after an edit/delete). if (cascade) _cascadeNext = true; - if (cascade && loaded && !_loadPromise && _playSkillsCascade()) { - _cascadeNext = false; - updateCount(); - return; - } + // Always re-fetch when the tab is explicitly opened — the cascade + // animation is handled inside renderSkillsList() via _cascadeNext. + // Skipping the fetch here caused stale data on panel close/reopen (#5870). if (_loadPromise) return _loadPromise; _loadPromise = (async () => { try { From e4fa4ae5dd1d709ce4168397bd1d200fec1b2494 Mon Sep 17 00:00:00 2001 From: Wes Huber <wesleybaxterhuber@gmail.com> Date: Fri, 7 Aug 2026 13:07:07 -0700 Subject: [PATCH 043/125] fix(brain): give the Add Memory form a submit button and reliable Enter handling (#5830) The Brain > Add tab rendered only a text input and category select with no submit control, and Enter submission relied on a deprecated keypress listener that is not guaranteed to fire, so the form could not be submitted at all (#5828). Add a labelled submit button styled like the neighbouring Skill Import button (theme-io-btn, inline SVG icon), switch the Enter handler to keydown with preventDefault, ignore IME composition, and pin both submit paths with a source-level regression test. Fixes #5828 Co-authored-by: Claude Fable 5 <noreply@anthropic.com> --- static/app.js | 12 ++++- static/index.html | 1 + tests/test_memory_add_submit_regression.py | 54 ++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 tests/test_memory_add_submit_regression.py diff --git a/static/app.js b/static/app.js index 9d05d8991..2f1e8d4bf 100644 --- a/static/app.js +++ b/static/app.js @@ -1689,12 +1689,20 @@ function initializeEventListeners() { const newMemoryInput = el('new-memory-input'); if (newMemoryInput) { - newMemoryInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { + // keydown, not the deprecated keypress: keypress is not guaranteed to + // fire for Enter everywhere, which left the Add Memory form with no + // working submit path (#5828). + newMemoryInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.isComposing) { + e.preventDefault(); memoryModule.addNewMemory(); } }); } + const newMemoryAddBtn = el('new-memory-add-btn'); + if (newMemoryAddBtn) { + newMemoryAddBtn.addEventListener('click', () => memoryModule.addNewMemory()); + } // Voice recording is handled by the dual-purpose send/mic button (see below) diff --git a/static/index.html b/static/index.html index 0fea836ba..fea4e20ac 100644 --- a/static/index.html +++ b/static/index.html @@ -365,6 +365,7 @@ <span class="skill-rich-ph"><span class="k">Add a memory</span> — e.g. 'I prefer concise replies' <svg class="k" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-left:4px;" aria-hidden="true"><polyline points="9 10 4 15 9 20"/><path d="M20 4v7a4 4 0 0 1-4 4H4"/></svg></span> </div> <select id="new-memory-category" class="memory-edit-cat-select" aria-label="Memory category"></select> + <button type="button" id="new-memory-add-btn" class="theme-io-btn" title="Save this memory" style="flex:none;height:28px;font-size:12px;"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>Add</button> </div> </div> <div class="admin-card"> diff --git a/tests/test_memory_add_submit_regression.py b/tests/test_memory_add_submit_regression.py new file mode 100644 index 000000000..450d63003 --- /dev/null +++ b/tests/test_memory_add_submit_regression.py @@ -0,0 +1,54 @@ +"""The Brain > Add Memory form must be submittable (#5828). + +The form previously had no submit button and relied on a deprecated +``keypress`` listener for Enter, which is not guaranteed to fire on all +platforms — leaving the form with no working submit path. Pins: + +- a visible, keyboard-accessible submit button next to the category select; +- the button wired to ``memoryModule.addNewMemory()``; +- Enter handled via ``keydown`` with ``preventDefault()`` (and no lingering + ``keypress`` handler on the input). +""" +from pathlib import Path + +APP_JS = Path("static/app.js") +INDEX_HTML = Path("static/index.html") + + +def _add_memory_row(html): + start = html.index('id="new-memory-input"') + end = html.index("</div>", html.index('id="new-memory-add-btn"', start)) + return html[start:end] + + +def test_add_memory_form_renders_a_submit_button(): + html = INDEX_HTML.read_text() + row = _add_memory_row(html) + + assert 'id="new-memory-category"' in row, "button must sit in the same row as the form fields" + btn_start = row.index('id="new-memory-add-btn"') + btn_tag = row[row.rindex("<button", 0, btn_start):row.index(">", btn_start)] + assert 'type="button"' in btn_tag, "must not rely on implicit submit semantics" + + +def _new_memory_wiring_block(source): + start = source.index("const newMemoryInput = el('new-memory-input');") + end = source.index("// Voice recording", start) + return source[start:end] + + +def test_submit_button_is_wired_to_add_new_memory(): + block = _new_memory_wiring_block(APP_JS.read_text()) + + assert "el('new-memory-add-btn')" in block + assert "addEventListener('click', () => memoryModule.addNewMemory())" in block + + +def test_enter_uses_keydown_with_prevent_default(): + block = _new_memory_wiring_block(APP_JS.read_text()) + + assert "addEventListener('keydown'" in block + assert "addEventListener('keypress'" not in block, "keypress is deprecated and unreliable for Enter" + assert "e.preventDefault();" in block + assert "!e.isComposing" in block, "IME composition must not submit the form" + assert "memoryModule.addNewMemory();" in block From 48cf08328f7b1fca078b15422a77733b6f113cef Mon Sep 17 00:00:00 2001 From: adabarbulescu <94562950+adabarbulescu@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:46:48 +0300 Subject: [PATCH 044/125] fix(agent): use Git Bash for Windows workspace shell --- src/agent_tools/subprocess_tools.py | 44 ++++++++-- tests/test_agent_bash_windows.py | 128 ++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+), 8 deletions(-) create mode 100644 tests/test_agent_bash_windows.py diff --git a/src/agent_tools/subprocess_tools.py b/src/agent_tools/subprocess_tools.py index 15041c76e..1c407b112 100644 --- a/src/agent_tools/subprocess_tools.py +++ b/src/agent_tools/subprocess_tools.py @@ -6,6 +6,7 @@ import sys import time import collections from typing import Optional, Callable, Awaitable, Tuple, Dict +from core.platform_compat import IS_WINDOWS, find_bash from src.constants import MAX_OUTPUT_CHARS DEFAULT_BASH_TIMEOUT = 60 * 60 # 1 hour @@ -16,6 +17,27 @@ PROGRESS_TAIL_LINES = 12 TMUX_CAPTURE_LINES = 2000 +async def _create_bash_subprocess(command: str, **kwargs): + """Start the agent shell with Bash semantics on every supported OS. + + ``asyncio.create_subprocess_shell`` delegates to ``cmd.exe`` on native + Windows. That contradicts the Bash tool contract and makes POSIX commands + such as ``pwd``, ``ls -la``, and ``cat`` unreliable even when the launcher + has found Git Bash. Pass the selected workspace as a structural ``cwd`` + argument; Git Bash inherits that native Windows directory and exposes it + using its normal ``/c/...`` representation. + """ + if IS_WINDOWS: + bash = find_bash() + if not bash: + raise RuntimeError( + "Git Bash is required for the Bash tool on Windows; " + "install Git for Windows and restart Odysseus" + ) + return await asyncio.create_subprocess_exec(bash, "-c", command, **kwargs) + return await asyncio.create_subprocess_shell(command, **kwargs) + + def _tmux_session_name(session_id: Optional[str]) -> str: raw = re.sub(r"[^A-Za-z0-9_.-]+", "-", str(session_id or "default")).strip("-") return f"ody-agent-{raw[:80] or 'default'}" @@ -280,7 +302,10 @@ class BashTool: progress_cb = ctx.get("progress_cb") _subproc_env = ctx.get("subproc_env") session_id = ctx.get("session_id") - if session_id and shutil.which("tmux"): + # tmux is a POSIX persistence path. A stray MSYS/Cygwin tmux.exe on + # native Windows must not bypass the Git Bash launcher below: the tmux + # setup hard-codes /bin/bash and cannot safely consume a native cwd. + if session_id and not IS_WINDOWS and shutil.which("tmux"): stdout, stderr, rc, timed_out = await _run_tmux_bash( content, session_id=str(session_id), @@ -307,13 +332,16 @@ class BashTool: "tmux_session": _tmux_session_name(str(session_id)), } - proc = await asyncio.create_subprocess_shell( - content, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - env=_subproc_env, - cwd=agent_cwd(), - ) + try: + proc = await _create_bash_subprocess( + content, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + env=_subproc_env, + cwd=agent_cwd(), + ) + except RuntimeError as e: + return {"error": f"bash: {e}", "exit_code": 1} stdout, stderr, rc, timed_out = await _run_subprocess_streaming( proc, timeout=DEFAULT_BASH_TIMEOUT, diff --git a/tests/test_agent_bash_windows.py b/tests/test_agent_bash_windows.py new file mode 100644 index 000000000..888c906b3 --- /dev/null +++ b/tests/test_agent_bash_windows.py @@ -0,0 +1,128 @@ +"""Windows execution contract for the agent Bash tool.""" + +import pytest + +from src.agent_tools import subprocess_tools + + +@pytest.mark.asyncio +async def test_windows_bash_uses_git_bash_with_structural_cwd(monkeypatch): + captured = {} + bash = r"C:\Program Files\Git\bin\bash.exe" + workspace = r"D:\Workspaces\Project with spaces" + process = object() + + monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True) + monkeypatch.setattr(subprocess_tools, "find_bash", lambda: bash) + + async def fake_exec(*argv, **kwargs): + captured["argv"] = argv + captured["kwargs"] = kwargs + return process + + async def fail_shell(*_args, **_kwargs): + pytest.fail("native Windows Bash must not execute through cmd.exe") + + monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_exec", fake_exec) + monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_shell", fail_shell) + + result = await subprocess_tools._create_bash_subprocess( + "pwd; cat package.json", + cwd=workspace, + env={"HOME": r"C:\Odysseus\data"}, + ) + + assert result is process + assert captured["argv"] == (bash, "-c", "pwd; cat package.json") + assert captured["kwargs"]["cwd"] == workspace + + +@pytest.mark.asyncio +async def test_windows_bash_without_git_bash_fails_clearly(monkeypatch): + monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True) + monkeypatch.setattr(subprocess_tools, "find_bash", lambda: None) + + async def fail_spawn(*_args, **_kwargs): + pytest.fail("no subprocess should start without Git Bash") + + monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_exec", fail_spawn) + monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_shell", fail_spawn) + + with pytest.raises(RuntimeError, match="Git Bash is required"): + await subprocess_tools._create_bash_subprocess("pwd", cwd=r"C:\Work") + + +@pytest.mark.asyncio +async def test_bash_tool_returns_install_hint_when_git_bash_is_missing(monkeypatch): + monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True) + monkeypatch.setattr(subprocess_tools, "find_bash", lambda: None) + + result = await subprocess_tools.BashTool().execute( + "pwd", + {"subproc_env": {}, "session_id": None}, + ) + + assert result["exit_code"] == 1 + assert "install Git for Windows" in result["error"] + + +@pytest.mark.asyncio +async def test_windows_bash_does_not_use_a_stray_tmux_executable(monkeypatch): + captured = {} + workspace = r"D:\Workspaces\Project with spaces" + + monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", True) + monkeypatch.setattr( + subprocess_tools.shutil, + "which", + lambda name: r"C:\msys64\usr\bin\tmux.exe", + ) + monkeypatch.setattr("src.tool_execution.agent_cwd", lambda: workspace) + + async def fail_tmux(*_args, **_kwargs): + pytest.fail("native Windows must not enter the POSIX tmux path") + + async def fake_create(command, **kwargs): + captured["command"] = command + captured["kwargs"] = kwargs + return object() + + async def fake_stream(_process, **_kwargs): + return "ok", "", 0, False + + monkeypatch.setattr(subprocess_tools, "_run_tmux_bash", fail_tmux) + monkeypatch.setattr(subprocess_tools, "_create_bash_subprocess", fake_create) + monkeypatch.setattr(subprocess_tools, "_run_subprocess_streaming", fake_stream) + + result = await subprocess_tools.BashTool().execute( + "pwd", + {"subproc_env": {}, "session_id": "chat-1"}, + ) + + assert result == {"output": "ok", "exit_code": 0} + assert captured["command"] == "pwd" + assert captured["kwargs"]["cwd"] == workspace + + +@pytest.mark.asyncio +async def test_posix_bash_keeps_existing_shell_path(monkeypatch): + captured = {} + process = object() + + monkeypatch.setattr(subprocess_tools, "IS_WINDOWS", False) + + async def fake_shell(command, **kwargs): + captured["command"] = command + captured["kwargs"] = kwargs + return process + + async def fail_exec(*_args, **_kwargs): + pytest.fail("POSIX behavior must continue through create_subprocess_shell") + + monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_shell", fake_shell) + monkeypatch.setattr(subprocess_tools.asyncio, "create_subprocess_exec", fail_exec) + + result = await subprocess_tools._create_bash_subprocess("pwd", cwd="/tmp/work") + + assert result is process + assert captured == {"command": "pwd", "kwargs": {"cwd": "/tmp/work"}} From 42da399b4d9d814400b8e19365dffedad5b66ab3 Mon Sep 17 00:00:00 2001 From: Matyas Gosztonyi <matyas@fenyves.net> Date: Sat, 8 Aug 2026 23:06:41 +0200 Subject: [PATCH 045/125] fix(email): route summaries through shared LLM adapter (#5841) * fix(email): route summaries through shared llm adapter * chore(ci): refresh PR checks * fix(email): preserve scheduled summary safeguards --------- Co-authored-by: Matyas Fenyves <16389204+uhhgoat@users.noreply.github.com> --- routes/email_helpers.py | 120 +++++++ routes/email_pollers.py | 32 +- routes/email_routes.py | 94 +++--- static/js/emailLibrary.js | 7 +- static/js/emailLibrary/utils.js | 19 ++ tests/test_email_summary_error_ui_js.py | 52 +++ tests/test_email_summary_llm.py | 406 ++++++++++++++++++++++++ 7 files changed, 670 insertions(+), 60 deletions(-) create mode 100644 tests/test_email_summary_error_ui_js.py create mode 100644 tests/test_email_summary_llm.py diff --git a/routes/email_helpers.py b/routes/email_helpers.py index c8639e1c7..257f5f921 100644 --- a/routes/email_helpers.py +++ b/routes/email_helpers.py @@ -247,6 +247,7 @@ import re as _re_reply _REPLY_OPEN_RE = _re_reply.compile(r"<<<\s*(?:REPLY|SUMMARY|OUTPUT)\s*>>+", _re_reply.I) _REPLY_CLOSE_RE = _re_reply.compile(r"<<<\s*END\s*>>+", _re_reply.I) _REPLY_ROLE_MARKER_RE = _re_reply.compile(r"</?\|(?:assistant|assistan|user|system|tool)\|>?|</\|end\|>?", _re_reply.I) +_SUMMARY_BULLET_RE = _re_reply.compile(r"^(?:[-*\u2022]\s+|\d+[.)]\s+)") def _extract_reply(text: str) -> str: @@ -277,6 +278,125 @@ def _extract_reply(text: str) -> str: return _strip_think(t).strip() +def _build_email_summary_messages(sender: str, subject: str, body_for_llm: str) -> list[dict[str, str]]: + return [ + { + "role": "system", + "content": ( + "You are an email summarizer. Format: 1-3 short bullet points " + "(use '- '). Cover: main point, action items, deadlines. If the " + "email has attachments (marked '--- ATTACHMENTS ---'), USE THEIR " + "CONTENTS - pull invoice totals, deadlines, key clauses, concrete " + "numbers/dates from PDFs/docs into the bullets. Be terse.\n\n" + "OUTPUT FORMAT: Put ONLY the bullet points between these exact " + "markers, each on its own line:\n" + "<<<SUMMARY>>>\n" + "- ...\n" + "<<<END>>>\n" + "Any reasoning must come BEFORE <<<SUMMARY>>> (ideally inside " + "<think>...</think>). Only the text between the markers is kept." + ), + }, + { + "role": "user", + "content": ( + f"From: {sender}\nSubject: {subject}\n\n{body_for_llm[:12000]}" + "\n\n---\n\nSummarize the email. Output the bullets between " + "<<<SUMMARY>>> and <<<END>>>." + ), + }, + ] + + +async def _generate_email_summary( + url: str, + model: str, + sender: str, + subject: str, + body_for_llm: str, + *, + headers: dict | None = None, + max_tokens: int = 8192, + timeout: int = 180, +) -> str: + """Generate an interactive email summary through the shared LLM adapter.""" + from src.llm_core import llm_call_async + + raw = await llm_call_async( + url=url, + model=model, + messages=_build_email_summary_messages(sender, subject, body_for_llm), + temperature=0.3, + max_tokens=max_tokens, + headers=headers, + timeout=timeout, + workload="foreground", + ) + return _normalize_email_summary(raw) + + +async def _generate_scheduled_email_summary( + url: str, + model: str, + sender: str, + subject: str, + body_for_llm: str, + *, + headers: dict | None = None, + owner: str | None = None, + max_tokens: int = 8192, + timeout: int = 180, +) -> str: + """Generate a scheduled summary through the background task candidate chain.""" + from src.task_endpoint import task_llm_call_async + + raw = await task_llm_call_async( + messages=_build_email_summary_messages(sender, subject, body_for_llm), + fallback_url=url, + fallback_model=model, + fallback_headers=headers, + owner=owner, + temperature=0.3, + max_tokens=max_tokens, + timeout=timeout, + ) + return _normalize_email_summary(raw) + + +def _normalize_email_summary(raw) -> str: + """Extract a stable cache/UI summary from provider output.""" + raw_text = raw or "" + if _REPLY_OPEN_RE.search(raw_text): + summary = _extract_reply(raw_text) + if summary: + return summary + + cleaned = _strip_think(raw_text).strip() + bullets = [ + line.strip() + for line in cleaned.splitlines() + if _SUMMARY_BULLET_RE.match(line.strip()) + ] + if bullets: + return "\n".join(bullets) + return cleaned.strip() + + +EMAIL_SUMMARY_ERROR_CODE = "email_summary_unavailable" +EMAIL_SUMMARY_ERROR_MESSAGE = "Failed to summarize" + + +def _email_summary_failure_log_detail(exc: BaseException) -> str: + """Return useful provider-failure metadata without echoing exception text.""" + detail = f"type={type(exc).__name__}" + status = getattr(exc, "status_code", None) + if status is None: + status = getattr(getattr(exc, "response", None), "status_code", None) + if isinstance(status, int): + detail += f" status={status}" + return detail + + def _apply_email_style_mechanics(text: str) -> str: """Enforce deterministic writing-style mechanics that models often miss.""" if not text: diff --git a/routes/email_pollers.py b/routes/email_pollers.py index 5d96bd0f9..a2507989d 100644 --- a/routes/email_pollers.py +++ b/routes/email_pollers.py @@ -40,6 +40,7 @@ from routes.email_helpers import ( _pre_retrieve_context, _attach_compose_uploads, _cleanup_compose_uploads, _q, SCHEDULED_DB, _EMAIL_REPLY_SYS_PROMPT_BASE, _email_cache_owner_clause, + _generate_scheduled_email_summary, _email_summary_failure_log_detail, ) logger = logging.getLogger(__name__) @@ -653,6 +654,7 @@ async def _auto_summarize_pass_single(days_back: int = 1, account_id: str | None no_msgid = 0 examined = 0 _summaries_created = 0 + _summary_failed = 0 _events_created = 0 _replies_drafted = 0 _reply_failed = 0 @@ -785,16 +787,17 @@ async def _auto_summarize_pass_single(days_back: int = 1, account_id: str | None if need_sum: try: - summary = await task_llm_call_async( - messages=[ - {"role": "system", "content": "You are an email summarizer. Format: 1-3 short bullet points (use '- '). Cover: main point, action items, deadlines. If the email has attachments (marked '--- ATTACHMENTS ---'), USE THEIR CONTENTS — pull out invoice totals, deadlines, key clauses, any concrete numbers/dates in PDFs/docs, and reflect them in the bullets. Be terse.\n\nOUTPUT FORMAT: Put ONLY the bullet points between these exact markers, each on its own line:\n<<<SUMMARY>>>\n- ...\n<<<END>>>\nAny reasoning or planning must come BEFORE <<<SUMMARY>>> (ideally inside <think>...</think>). Only the text between the markers is kept."}, - {"role": "user", "content": f"From: {sender}\nSubject: {subject}\n\n{body_for_llm[:12000]}\n\n---\n\nSummarize the email. Output the bullets between <<<SUMMARY>>> and <<<END>>>."}, - ], - fallback_url=url, fallback_model=model, fallback_headers=headers, + summary = await _generate_scheduled_email_summary( + url=url, + model=model, + sender=sender, + subject=subject, + body_for_llm=body_for_llm, + headers=req_headers, owner=account_owner or None, - temperature=0.3, max_tokens=16384, timeout=240, + max_tokens=16384, + timeout=240, ) - summary = _extract_reply((summary or "").strip()) if summary: _c = _sql3.connect(SCHEDULED_DB) _c.execute(""" @@ -808,10 +811,19 @@ async def _auto_summarize_pass_single(days_back: int = 1, account_id: str | None _summaries_created += 1 _uid_text = uid.decode() if isinstance(uid, bytes) else str(uid) _detail_lines.append(f"summary · {_folder}#{_uid_text} · {subject or '(no subject)'} — {sender or '(unknown sender)'}") + else: + _summary_failed += 1 + _uid_text = uid.decode() if isinstance(uid, bytes) else str(uid) + _detail_lines.append(f"summary empty · {_folder}#{_uid_text} · {subject or '(no subject)'} — {sender or '(unknown sender)'}") except Exception as e: + _summary_failed += 1 _uid_text = uid.decode() if isinstance(uid, bytes) else str(uid) _detail_lines.append(f"summary failed · {_folder}#{_uid_text} · {subject or '(no subject)'} — {sender or '(unknown sender)'}") - logger.warning(f"Auto-summary {uid} failed: {e}") + logger.warning( + "Auto-summary uid=%s failed %s", + _uid_text, + _email_summary_failure_log_detail(e), + ) if need_reply: await _emit_progress(progress_cb, f"Drafting reply {processed + 1}/{_max_process} · checked {examined}/{len(uid_list)}") @@ -1320,6 +1332,8 @@ async def _auto_summarize_pass_single(days_back: int = 1, account_id: str | None parts.append(f"processed {processed} new") if auto_sum: parts.append(f"summarized {_summaries_created}") + if _summary_failed: + parts.append(f"{_summary_failed} summary failed") if auto_reply_draft: parts.append(f"drafted {_replies_drafted} repl" + ("y" if _replies_drafted == 1 else "ies")) if _reply_failed: diff --git a/routes/email_routes.py b/routes/email_routes.py index 3c8e407bd..76a744ce1 100644 --- a/routes/email_routes.py +++ b/routes/email_routes.py @@ -57,7 +57,8 @@ from routes.email_helpers import ( _extract_attachment_to_disk, _extract_html, _extract_text, _fetch_sender_thread_context, _pre_retrieve_context, _EMAIL_REPLY_SYS_PROMPT_BASE, _POOL_HOOKS, - _friendly_email_auth_error, + _friendly_email_auth_error, _email_summary_failure_log_detail, + _generate_email_summary, EMAIL_SUMMARY_ERROR_CODE, EMAIL_SUMMARY_ERROR_MESSAGE, SendEmailRequest, ExtractStyleRequest, ATTACHMENTS_DIR, COMPOSE_UPLOADS_DIR, SCHEDULED_DB, attachment_extract_dir, _email_cache_owner_clause, email_translation_body_hash, @@ -4766,8 +4767,6 @@ def setup_email_routes(): """Generate a quick AI summary of an email body.""" try: from src.endpoint_resolver import resolve_endpoint - from src.llm_core import _uses_max_completion_tokens, _restricts_temperature - import requests as _req body = data.get("body", "") subject = data.get("subject", "") @@ -4778,7 +4777,11 @@ def setup_email_routes(): if account_id: _assert_owns_account(account_id, owner) if not body: - return {"success": False, "error": "No body provided"} + return { + "success": False, + "error": "No body provided", + "error_code": "email_summary_missing_body", + } # If we know which UID this is, fetch the raw message and pull # attachment text so the summary can reference invoice totals, @@ -4807,53 +4810,43 @@ def setup_email_routes(): if not url: url, model, headers = resolve_endpoint("default", owner=owner) if not url or not model: - return {"success": False, "error": "No LLM endpoint configured"} + return { + "success": False, + "error": "No model configured for email summaries", + "error_code": "email_summary_not_configured", + } req_headers = {"Content-Type": "application/json"} if headers: req_headers.update(headers) - tok_key = "max_completion_tokens" if _uses_max_completion_tokens(model) else "max_tokens" - payload = { - "model": model, - "messages": [ - {"role": "system", "content": "You are an email summarizer. Format: 1-3 short bullet points (use '- '). Cover: main point, action items, deadlines. If the email has attachments (marked '--- ATTACHMENTS ---'), USE THEIR CONTENTS — pull invoice totals, deadlines, key clauses, concrete numbers/dates from PDFs/docs into the bullets. Be terse.\n\nOUTPUT FORMAT: Put ONLY the bullet points between these exact markers, each on its own line:\n<<<SUMMARY>>>\n- ...\n<<<END>>>\nAny reasoning must come BEFORE <<<SUMMARY>>> (ideally inside <think>...</think>). Only the text between the markers is kept."}, - {"role": "user", "content": f"From: {sender}\nSubject: {subject}\n\n{body_for_llm[:12000]}\n\n---\n\nSummarize the email. Output the bullets between <<<SUMMARY>>> and <<<END>>>."}, - ], - tok_key: 8192, - "temperature": 0.3, - "stream": False, - } - # Reasoning models (o1/o3/o4/gpt-5) reject an explicit temperature. - if _restricts_temperature(model): - payload.pop("temperature", None) - resp = await asyncio.to_thread( - _req.post, url, json=payload, headers=req_headers, timeout=180 - ) - if not resp.ok: - return {"success": False, "error": f"LLM HTTP {resp.status_code}"} - rdata = resp.json() - msg = (rdata.get("choices") or [{}])[0].get("message", {}) - content = (msg.get("content") or "").strip() - content = _extract_reply(content) + try: + content = await _generate_email_summary( + url=url, + model=model, + sender=sender, + subject=subject, + body_for_llm=body_for_llm, + headers=req_headers, + max_tokens=8192, + timeout=180, + ) + except Exception as e: + logger.warning( + "Email summary LLM call failed %s", + _email_summary_failure_log_detail(e), + ) + return { + "success": False, + "error": EMAIL_SUMMARY_ERROR_MESSAGE, + "error_code": EMAIL_SUMMARY_ERROR_CODE, + } if not content: - # Model put everything in reasoning_content — extract bullet points - rc = (msg.get("reasoning_content") or "").strip() - # Find bullet-point style output (lines starting with -, •, *, or numbered) - bullet_lines = [] - for line in rc.split("\n"): - stripped = line.strip() - if re.match(r"^[-•*]\s+|^\d+[.)]\s+", stripped): - bullet_lines.append(stripped) - if bullet_lines: - content = "\n".join(bullet_lines) - else: - # Last resort: take the last paragraph - paragraphs = [p.strip() for p in rc.split("\n\n") if p.strip()] - content = paragraphs[-1] if paragraphs else rc[:500] - - if not content: - return {"success": False, "error": "Empty response from model"} + return { + "success": False, + "error": "The model returned an empty summary", + "error_code": "email_summary_empty", + } # Cache the summary if we have a message_id mid = data.get("message_id", "") @@ -4876,8 +4869,15 @@ def setup_email_routes(): return {"success": True, "summary": content, "model_used": model} except Exception as e: - logger.error(f"Failed to summarize: {e}") - return {"success": False, "error": "Mail operation failed"} + logger.error( + "Email summary route failed %s", + _email_summary_failure_log_detail(e), + ) + return { + "success": False, + "error": EMAIL_SUMMARY_ERROR_MESSAGE, + "error_code": EMAIL_SUMMARY_ERROR_CODE, + } @router.post("/translate") async def translate_email(data: dict, owner: str = Depends(require_owner)): diff --git a/static/js/emailLibrary.js b/static/js/emailLibrary.js index 6a0d3e294..32b906ddc 100644 --- a/static/js/emailLibrary.js +++ b/static/js/emailLibrary.js @@ -13,7 +13,7 @@ import { makeWindowDraggable } from './windowDrag.js'; import { _esc, _escLinkify, _extractName, _parseTurnMeta, _formatBubbleDate, _formatRecipients, _senderColor, _initials, - _sanitizeHtml, + _sanitizeHtml, _renderEmailSummaryError, _TALON_WROTE, _TALON_FROM, _TALON_SENT, _TALON_SUBJ, _TALON_TO, _TALON_ORIG_RE, _SIG_BLOAT_MIN_CHARS, } from './emailLibrary/utils.js'; @@ -7259,12 +7259,11 @@ async function _generateSummary(reader, data, btn) { if (label) label.textContent = 'Summary'; } } else { - content.innerHTML = `<span style="color:var(--red)">${_esc(result.error || 'Failed to summarize')}</span>`; - panel.remove(); + _renderEmailSummaryError(content, result); } } catch (e) { sp.destroy(); - panel.remove(); + _renderEmailSummaryError(content, null); if (uiModule) uiModule.showError?.('Failed to summarize'); } finally { if (btn) btn.disabled = false; diff --git a/static/js/emailLibrary/utils.js b/static/js/emailLibrary/utils.js index 82a5c86ec..f634c9949 100644 --- a/static/js/emailLibrary/utils.js +++ b/static/js/emailLibrary/utils.js @@ -30,6 +30,25 @@ export function _esc(text) { return div.innerHTML; } +const _EMAIL_SUMMARY_ERROR_MESSAGES = Object.freeze({ + email_summary_missing_body: 'No email body to summarize', + email_summary_not_configured: 'No model configured for email summaries', + email_summary_empty: 'The model returned an empty summary', + email_summary_unavailable: 'Failed to summarize', +}); + +export function _emailSummaryErrorMessage(result) { + const code = String(result?.error_code || ''); + return _EMAIL_SUMMARY_ERROR_MESSAGES[code] || 'Failed to summarize'; +} + +export function _renderEmailSummaryError(container, result) { + const message = container.ownerDocument.createElement('span'); + message.style.color = 'var(--red)'; + message.textContent = _emailSummaryErrorMessage(result); + container.replaceChildren(message); +} + function _attrEsc(text) { return String(text ?? '') .replace(/"/g, '"') diff --git a/tests/test_email_summary_error_ui_js.py b/tests/test_email_summary_error_ui_js.py new file mode 100644 index 000000000..1afc3bec9 --- /dev/null +++ b/tests/test_email_summary_error_ui_js.py @@ -0,0 +1,52 @@ +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + + +_REPO = Path(__file__).resolve().parent.parent +_UTILS = (_REPO / "static" / "js" / "emailLibrary" / "utils.js").as_posix() +_HAS_NODE = shutil.which("node") is not None + +pytestmark = pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") + + +def test_email_summary_renderer_ignores_untrusted_provider_error_text(): + secret = ( + "endpoint=https://private.example.internal/v1 provider=ollama " + "model=private-model response_body=private-response " + "Authorization: Bearer token-secret-value" + ) + script = f""" + import {{ _renderEmailSummaryError }} from '{_UTILS}'; + const host = {{ + ownerDocument: {{ + createElement() {{ return {{ style: {{}}, textContent: '' }}; }}, + }}, + replaceChildren(node) {{ this.child = node; }}, + }}; + _renderEmailSummaryError(host, {{ + error_code: 'email_summary_unavailable', + error: {json.dumps(secret)}, + }}); + console.log(JSON.stringify({{ + text: host.child.textContent, + color: host.child.style.color, + }})); + """ + + proc = subprocess.run( + ["node", "--input-type=module"], + input=script, + capture_output=True, + text=True, + cwd=str(_REPO), + timeout=30, + ) + + assert proc.returncode == 0, proc.stderr + rendered = json.loads(proc.stdout) + assert rendered == {"text": "Failed to summarize", "color": "var(--red)"} + assert secret not in proc.stdout diff --git a/tests/test_email_summary_llm.py b/tests/test_email_summary_llm.py new file mode 100644 index 000000000..b0ab7b3be --- /dev/null +++ b/tests/test_email_summary_llm.py @@ -0,0 +1,406 @@ +import asyncio +import json +import logging +import os +import sqlite3 +import sys +import tempfile +from pathlib import Path + +import pytest + + +_TMP_DATA = Path(tempfile.mkdtemp(prefix="odysseus-email-summary-")) +os.environ.setdefault("DATA_DIR", str(_TMP_DATA)) +os.environ.setdefault("DATABASE_URL", f"sqlite:///{_TMP_DATA / 'app.db'}") + +PROJECT_ROOT = Path(__file__).resolve().parent.parent +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + + +def _route_endpoint(router, path: str, method: str): + method = method.upper() + for route in router.routes: + if route.path == path and method in getattr(route, "methods", set()): + return route.endpoint + raise AssertionError(f"route not found: {method} {path}") + + +@pytest.mark.asyncio +async def test_generate_email_summary_uses_shared_llm_adapter(monkeypatch): + import routes.email_helpers as email_helpers + import src.llm_core as llm_core + + calls = {} + + async def fake_llm_call_async(url, model, messages, **kwargs): + calls["url"] = url + calls["model"] = model + calls["messages"] = messages + calls["kwargs"] = kwargs + return "thinking before marker\n<<<SUMMARY>>>\n- Pay the invoice by Friday.\n<<<END>>>" + + monkeypatch.setattr(llm_core, "llm_call_async", fake_llm_call_async) + + summary = await email_helpers._generate_email_summary( + url="https://chatgpt.com/backend-api/codex/responses", + model="gpt-5.5", + sender="Billing <billing@example.com>", + subject="Invoice due", + body_for_llm="Please pay invoice 123 by Friday.", + headers={"Authorization": "Bearer test"}, + max_tokens=1234, + timeout=45, + ) + + assert summary == "- Pay the invoice by Friday." + assert calls["url"] == "https://chatgpt.com/backend-api/codex/responses" + assert calls["model"] == "gpt-5.5" + assert calls["kwargs"]["headers"] == {"Authorization": "Bearer test"} + assert calls["kwargs"]["temperature"] == 0.3 + assert calls["kwargs"]["max_tokens"] == 1234 + assert calls["kwargs"]["timeout"] == 45 + assert calls["kwargs"]["workload"] == "foreground" + assert calls["messages"][0]["role"] == "system" + assert calls["messages"][1]["role"] == "user" + + +@pytest.mark.asyncio +async def test_scheduled_email_summary_uses_background_fallback_chain(monkeypatch): + import routes.email_helpers as email_helpers + import src.llm_core as llm_core + import src.task_endpoint as task_endpoint + + candidates = [ + ("http://primary.invalid/v1", "primary-model", {"X-Candidate": "primary"}), + ("http://fallback.invalid/v1", "fallback-model", {"X-Candidate": "fallback"}), + ] + resolve_calls = [] + wait_calls = [] + llm_calls = [] + + def fake_resolve_task_candidates(**kwargs): + resolve_calls.append(kwargs) + return candidates + + async def fake_wait_for_interactive_quiet(label): + wait_calls.append(label) + return False + + async def fake_llm_call_async(url, model, messages, **kwargs): + llm_calls.append((url, model, messages, kwargs)) + if model == "primary-model": + raise RuntimeError("primary unavailable") + return "<<<SUMMARY>>>\n- Used the fallback model.\n<<<END>>>" + + monkeypatch.setattr(task_endpoint, "resolve_task_candidates", fake_resolve_task_candidates) + monkeypatch.setattr(task_endpoint, "wait_for_interactive_quiet", fake_wait_for_interactive_quiet) + monkeypatch.setattr(llm_core, "llm_call_async", fake_llm_call_async) + + summary = await email_helpers._generate_scheduled_email_summary( + url="http://caller-fallback.invalid/v1", + model="caller-fallback-model", + sender="Sender <sender@example.com>", + subject="Scheduled subject", + body_for_llm="Please summarize this scheduled email.", + headers={"Authorization": "Bearer test"}, + owner="alice", + max_tokens=321, + timeout=54, + ) + + assert summary == "- Used the fallback model." + assert resolve_calls == [{ + "fallback_url": "http://caller-fallback.invalid/v1", + "fallback_model": "caller-fallback-model", + "fallback_headers": {"Authorization": "Bearer test"}, + "owner": "alice", + }] + assert wait_calls == ["background task LLM"] + assert [call[1] for call in llm_calls] == ["primary-model", "fallback-model"] + assert all(call[3]["workload"] == "background" for call in llm_calls) + assert all(call[3]["max_tokens"] == 321 for call in llm_calls) + assert all(call[3]["timeout"] == 54 for call in llm_calls) + + +@pytest.mark.asyncio +async def test_scheduled_local_summary_is_preempted_by_foreground_call(monkeypatch): + import routes.email_helpers as email_helpers + import src.llm_core as llm_core + import src.task_endpoint as task_endpoint + + local_url = "http://127.0.0.1:11434/v1/chat/completions" + background_started = asyncio.Event() + never_release = asyncio.Event() + observed_workloads = [] + + monkeypatch.setenv("ODYSSEUS_LOCAL_MODEL_GATE", "true") + monkeypatch.setenv("BACKGROUND_TASK_FOREGROUND_GATE", "false") + monkeypatch.setattr(llm_core, "_LOCAL_MODEL_LOCK", asyncio.Lock()) + monkeypatch.setattr(llm_core, "_LOCAL_MODEL_CURRENT", {}) + monkeypatch.setattr(llm_core, "_LOCAL_MODEL_WAITING_FOREGROUND", 0) + monkeypatch.setattr( + task_endpoint, + "resolve_task_candidates", + lambda **_kwargs: [(local_url, "scheduled-model", {})], + ) + + async def fake_wait_for_interactive_quiet(_label): + return False + + async def gated_llm_call(url, model, messages, **kwargs): + assert messages + workload = kwargs.get("workload") + observed_workloads.append(workload) + async with llm_core._local_model_slot(url, model, workload=workload): + background_started.set() + await never_release.wait() + return "unreachable" + + monkeypatch.setattr(task_endpoint, "wait_for_interactive_quiet", fake_wait_for_interactive_quiet) + monkeypatch.setattr(llm_core, "llm_call_async", gated_llm_call) + + background_task = asyncio.create_task(email_helpers._generate_scheduled_email_summary( + url=local_url, + model="scheduled-model", + sender="Sender", + subject="Scheduled", + body_for_llm="Scheduled body", + owner="alice", + )) + foreground_task = None + try: + await asyncio.wait_for(background_started.wait(), timeout=1) + + async def run_foreground(): + async with llm_core._local_model_slot( + local_url, + "interactive-model", + workload="foreground", + ): + return True + + foreground_task = asyncio.create_task(run_foreground()) + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(background_task, timeout=1) + assert await asyncio.wait_for(foreground_task, timeout=1) is True + assert observed_workloads == ["background"] + finally: + for task in (background_task, foreground_task): + if task is not None and not task.done(): + task.cancel() + + +@pytest.mark.asyncio +async def test_manual_email_summary_uses_shared_helper_and_caches(tmp_path, monkeypatch): + import routes.email_helpers as email_helpers + import routes.email_routes as email_routes + import src.endpoint_resolver as endpoint_resolver + + db_path = tmp_path / "scheduled_emails.db" + monkeypatch.setattr(email_helpers, "SCHEDULED_DB", db_path) + monkeypatch.setattr(email_routes, "SCHEDULED_DB", db_path) + email_helpers._init_scheduled_db() + + resolve_calls = [] + + def fake_resolve_endpoint(kind, owner=None): + resolve_calls.append((kind, owner)) + assert kind == "utility" + assert owner == "alice" + return ( + "https://chatgpt.com/backend-api/codex/responses", + "gpt-5.5", + {"Authorization": "Bearer test"}, + ) + + helper_calls = {} + + async def fake_generate_email_summary(**kwargs): + helper_calls.update(kwargs) + return "- Manual summary" + + monkeypatch.setattr(endpoint_resolver, "resolve_endpoint", fake_resolve_endpoint) + monkeypatch.setattr(email_routes, "_generate_email_summary", fake_generate_email_summary) + + router = email_routes.setup_email_routes() + summarize = _route_endpoint(router, "/api/email/summarize", "POST") + + result = await summarize( + { + "body": "This is a long enough email body for manual summary.", + "subject": "Manual subject", + "from": "Sender <sender@example.com>", + "message_id": "<manual@example.com>", + "folder": "INBOX", + }, + owner="alice", + ) + + assert result == { + "success": True, + "summary": "- Manual summary", + "model_used": "gpt-5.5", + } + assert resolve_calls == [("utility", "alice")] + assert helper_calls["url"] == "https://chatgpt.com/backend-api/codex/responses" + assert helper_calls["model"] == "gpt-5.5" + assert helper_calls["headers"]["Authorization"] == "Bearer test" + assert helper_calls["headers"]["Content-Type"] == "application/json" + + conn = sqlite3.connect(db_path) + try: + row = conn.execute( + "SELECT owner, summary, model_used FROM email_summaries WHERE message_id=?", + ("<manual@example.com>",), + ).fetchone() + finally: + conn.close() + assert row == ("alice", "- Manual summary", "gpt-5.5") + + +@pytest.mark.asyncio +@pytest.mark.parametrize("exception_kind", ["http", "runtime"]) +async def test_manual_email_summary_never_exposes_provider_exception( + monkeypatch, + caplog, + exception_kind, +): + from fastapi import HTTPException + import routes.email_routes as email_routes + import src.endpoint_resolver as endpoint_resolver + + secret_detail = ( + "endpoint=https://private.example.internal/v1 provider=ollama " + "model=private-model response_body=private-response " + "Authorization: Bearer token-secret-value" + ) + + def fake_resolve_endpoint(kind, owner=None): + assert kind == "utility" + assert owner == "alice" + return ( + "https://private.example.internal/v1", + "private-model", + {"Authorization": "Bearer token-secret-value"}, + ) + + async def fail_summary(**_kwargs): + if exception_kind == "http": + raise HTTPException(status_code=502, detail=secret_detail) + raise RuntimeError(secret_detail) + + monkeypatch.setattr(endpoint_resolver, "resolve_endpoint", fake_resolve_endpoint) + monkeypatch.setattr(email_routes, "_generate_email_summary", fail_summary) + caplog.set_level(logging.WARNING, logger=email_routes.__name__) + + router = email_routes.setup_email_routes() + summarize = _route_endpoint(router, "/api/email/summarize", "POST") + result = await summarize( + { + "body": "This email body is long enough to summarize.", + "subject": "Sensitive provider failure", + "from": "Sender <sender@example.com>", + }, + owner="alice", + ) + + assert result == { + "success": False, + "error": "Failed to summarize", + "error_code": "email_summary_unavailable", + } + exposed = json.dumps(result) + caplog.text + for marker in ( + "private.example.internal", + "ollama", + "private-model", + "private-response", + "token-secret-value", + ): + assert marker not in exposed + assert f"type={'HTTPException' if exception_kind == 'http' else 'RuntimeError'}" in caplog.text + + +@pytest.mark.asyncio +async def test_scheduled_email_summary_uses_shared_helper_and_caches(tmp_path, monkeypatch): + import routes.email_helpers as email_helpers + import routes.email_pollers as email_pollers + + db_path = tmp_path / "scheduled_emails.db" + monkeypatch.setattr(email_helpers, "SCHEDULED_DB", db_path) + monkeypatch.setattr(email_pollers, "SCHEDULED_DB", db_path) + email_helpers._init_scheduled_db() + + raw_email = ( + b"From: Sender <sender@example.com>\r\n" + b"To: Alice <alice@example.com>\r\n" + b"Subject: Scheduled subject\r\n" + b"Message-ID: <scheduled@example.com>\r\n" + b"Date: Tue, 01 Jan 2026 12:00:00 +0000\r\n" + b"Content-Type: text/plain; charset=utf-8\r\n" + b"\r\n" + + (b"Please review this scheduled summary email. " * 8) + ) + + class FakeImap: + def __init__(self): + self.logout_calls = 0 + + def select(self, _folder, readonly=True): + return "OK", [] + + def uid(self, command, *args): + if command == "SEARCH": + return "OK", [b"1"] + if command == "FETCH": + return "OK", [(b"1 (RFC822)", raw_email)] + raise AssertionError(f"unexpected uid command: {command!r} {args!r}") + + def logout(self): + self.logout_calls += 1 + + fake_conn = FakeImap() + + def fake_resolve_task_candidates(owner=None): + assert owner == "alice" + return [( + "https://chatgpt.com/backend-api/codex/responses", + "gpt-5.5", + {"Authorization": "Bearer test"}, + )] + + helper_calls = {} + + async def fake_generate_email_summary(**kwargs): + helper_calls.update(kwargs) + return "- Scheduled summary" + + monkeypatch.setattr(email_pollers, "_load_settings", lambda: {"email_auto_summarize": True}) + monkeypatch.setattr(email_pollers, "_owner_for_email_account", lambda _account_id: "alice") + monkeypatch.setattr(email_pollers, "_imap_connect", lambda account_id=None, owner="": fake_conn) + monkeypatch.setattr(email_pollers, "_get_email_config", lambda account_id=None, owner="": {"from_address": "alice@example.com"}) + monkeypatch.setattr(email_pollers, "resolve_task_candidates", fake_resolve_task_candidates) + monkeypatch.setattr(email_pollers, "_generate_scheduled_email_summary", fake_generate_email_summary) + + result = await email_pollers._auto_summarize_pass_single(account_id="acct-alice") + + assert "summarized 1" in result + assert "summary failed" not in result + assert helper_calls["url"] == "https://chatgpt.com/backend-api/codex/responses" + assert helper_calls["model"] == "gpt-5.5" + assert helper_calls["headers"]["Authorization"] == "Bearer test" + assert helper_calls["headers"]["Content-Type"] == "application/json" + assert helper_calls["owner"] == "alice" + assert fake_conn.logout_calls == 1 + + conn = sqlite3.connect(db_path) + try: + row = conn.execute( + "SELECT owner, summary, model_used FROM email_summaries WHERE message_id=?", + ("<scheduled@example.com>",), + ).fetchone() + finally: + conn.close() + assert row == ("alice", "- Scheduled summary", "gpt-5.5") From 8f2f483725c8f2e2fcbb41d41f3cc65bf270bcb5 Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Mon, 10 Aug 2026 18:45:34 +0100 Subject: [PATCH 046/125] fix(email): make unread opens one authoritative IMAP operation (#5923) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(email): mark opened messages seen in one IMAP operation * fix(email): collapse unread opens and ignore stale responses * fix(email): send seen flags as an IMAP flag list Wrap the authoritative \\Seen STORE operand in parentheses so strict IMAP servers such as GreenMail accept both cache-miss and cached-open transitions. Tighten the focused fake IMAP contract to reject the previously emitted bare flag atom. * fix(email): guard stale authoritative opens * fix(email): report a failed \Seen instead of withholding the message The authoritative-open contract made a failed STORE fatal to the read: the cold path raised after the body was already fetched and parsed, and the cached path discarded an in-memory message to return {"error": "Failed to mark email read"}. A transient IMAP failure therefore turned a readable message into one that could not be opened at all. Being authoritative should mean the reported flag state is truthful, not that the body is withheld. The read now always returns the message and carries mark_seen_failed so the client can roll its optimistic unread marker back: - _read_email_sync logs and reports a rejected STORE rather than raising, and only writes the local index/list-cache transition when the provider accepted it, so local state cannot drift ahead of the mailbox. - A mailbox that refuses a read-write SELECT (shared archives, some provider folders) falls back to a read-only selection and reports the flag failure instead of failing the open. - The route strips mark_seen_failed before caching, so a one-off failure is never replayed to later readers. - mark_seen now defaults to False on _read_email_sync. It was inert before this branch and now mutates provider state; the one caller that wants it off already passes it explicitly. emailInbox and emailLibrary keep the message rendered when mark_seen_failed is set and restore the unread state, rather than showing a failed reader. --------- Co-authored-by: Léo <leograndcontact@gmail.com> --- routes/email_routes.py | 131 ++++++++++---- static/js/emailInbox.js | 59 ++++-- static/js/emailLibrary.js | 144 ++++++++++++--- tests/test_email_open_dedup_js.py | 231 ++++++++++++++++++++++++ tests/test_email_read_mark_seen.py | 278 +++++++++++++++++++++++++++++ 5 files changed, 776 insertions(+), 67 deletions(-) create mode 100644 tests/test_email_open_dedup_js.py create mode 100644 tests/test_email_read_mark_seen.py diff --git a/routes/email_routes.py b/routes/email_routes.py index 76a744ce1..81136e36e 100644 --- a/routes/email_routes.py +++ b/routes/email_routes.py @@ -2861,13 +2861,22 @@ def setup_email_routes(): return indexed_response return {"emails": [], "total": 0, "error": "Mail operation failed"} - def _read_email_sync(uid, folder, account_id, owner, mark_seen=True, full=False): + def _read_email_sync(uid, folder, account_id, owner, mark_seen=False, full=False): """Sync IMAP read — wrapped in to_thread by the async handler. The normal reader path fetches the headers plus a bounded body prefix. That avoids downloading multi-megabyte attachments just to open a message. Full-message fetch remains available for flows that need attachment metadata immediately, such as forwarding. + + `mark_seen` defaults to False because it mutates provider state: it + selects the mailbox read-write and issues a STORE. Only a foreground + open should ask for it, and it has to ask explicitly. + + A failed \\Seen transition is reported as `mark_seen_failed` on an + otherwise normal response, never as an error. The body has already been + fetched at that point, so refusing to return it would turn a cosmetic + flag failure into an unreadable message. """ import time as _t _t0 = _t.monotonic() @@ -2875,9 +2884,28 @@ def setup_email_routes(): preview_bytes = 384 * 1024 _t_select = 0.0 _t_fetch = 0.0 + mark_seen_failed = False try: with _imap(account_id, owner=owner) as conn: - conn.select(_q(folder), readonly=True) + # A foreground open owns both the body fetch and the \Seen + # transition. Keep them on one read-write IMAP selection so the + # route never schedules a second connection that can race the + # response. Prefetch/read-only callers retain BODY.PEEK and a + # read-only mailbox selection. + try: + conn.select(_q(folder), readonly=not mark_seen) + except Exception as select_exc: + if not mark_seen: + raise + # Read-only mailboxes (shared archives, some provider + # folders) reject a read-write SELECT. Serve the message + # read-only and report the flag failure. + logger.warning( + f"read-write SELECT rejected for {folder!r}; " + f"serving read-only without \\Seen: {select_exc}" + ) + conn.select(_q(folder), readonly=True) + mark_seen_failed = True _t_select = _t.monotonic() - _t0 fetch_query = "(BODY.PEEK[])" if full else f"(BODY.PEEK[HEADER] BODY.PEEK[TEXT]<0.{preview_bytes}>)" status, msg_data = _imap_uid_fetch(conn, uid, fetch_query) @@ -2903,22 +2931,44 @@ def setup_email_routes(): header_part = msg_data[0][1] or b"" raw = header_part + b"\r\n" + text_part - msg = email_mod.message_from_bytes(raw) + # Parse the fetched payload before mutating provider state. If + # the message is malformed enough that the reader cannot build + # a response, the caller gets an error while the message stays + # unread instead of receiving a false optimistic rollback. + msg = email_mod.message_from_bytes(raw) - subject = _decode_header(msg.get("Subject", "(no subject)")) - sender = _decode_header(msg.get("From", "unknown")) - to = _decode_header(msg.get("To", "")) - cc = _decode_header(msg.get("Cc", "")) - date_str = msg.get("Date", "") - message_id = msg.get("Message-ID", "") - in_reply_to = msg.get("In-Reply-To", "") - references = msg.get("References", "") - body = _extract_text(msg) - body_html = _extract_html(msg) + subject = _decode_header(msg.get("Subject", "(no subject)")) + sender = _decode_header(msg.get("From", "unknown")) + to = _decode_header(msg.get("To", "")) + cc = _decode_header(msg.get("Cc", "")) + date_str = msg.get("Date", "") + message_id = msg.get("Message-ID", "") + in_reply_to = msg.get("In-Reply-To", "") + references = msg.get("References", "") + body = _extract_text(msg) + body_html = _extract_html(msg) + + sender_name, sender_addr = email.utils.parseaddr(sender) + parsed_date = email.utils.parsedate_to_datetime(date_str) if date_str else None + attachments = _list_attachments_from_msg(msg) if full else (_email_attachment_meta_cache_get(owner, account_id, folder, uid) or []) + + if mark_seen and not mark_seen_failed: + seen_status, _ = conn.uid("STORE", _uid_bytes(uid), "+FLAGS", "(\\Seen)") + if seen_status != "OK": + # Report, don't raise. The parsed body below is still a + # valid response; only the flag claim is untrue. + logger.warning( + f"IMAP STORE \\Seen failed for UID {uid} in {folder!r}: {seen_status}" + ) + mark_seen_failed = True + + # Only record the local flag transition when the provider actually + # accepted it, so the index and list cache cannot drift ahead of + # the mailbox. + if mark_seen and not mark_seen_failed: + _email_index_update_flags(owner, account_id, folder, uid, "\\Seen", True) + _update_list_cache_seen(account_id, folder, uid, True) - sender_name, sender_addr = email.utils.parseaddr(sender) - parsed_date = email.utils.parsedate_to_datetime(date_str) if date_str else None - attachments = _list_attachments_from_msg(msg) if full else (_email_attachment_meta_cache_get(owner, account_id, folder, uid) or []) related_attachments = [] if full and not _has_visible_attachments(msg): related_attachments = _related_thread_attachments_sync( @@ -3039,20 +3089,29 @@ def setup_email_routes(): "boundaries": cached_boundaries, "thread_turns": cached_turns, "sender_signature": cached_sender_sig, + # Per-request, not part of the message: the route strips this + # before caching so a one-off flag failure is never replayed to + # later readers. + "mark_seen_failed": mark_seen_failed, } except Exception as e: logger.error(f"Failed to read email {uid}: {e}") return {"error": "Mail operation failed"} def _mark_email_seen_sync(uid, folder, account_id, owner): + """Synchronously mark a cached email seen and report success.""" try: with _imap(account_id, owner=owner) as conn: - conn.select(_q(folder)) - conn.uid("STORE", _uid_bytes(uid), "+FLAGS", "\\Seen") + conn.select(_q(folder), readonly=False) + status, _ = conn.uid("STORE", _uid_bytes(uid), "+FLAGS", "(\\Seen)") + if status != "OK": + return False _email_index_update_flags(owner, account_id, folder, uid, "\\Seen", True) _update_list_cache_seen(account_id, folder, uid, True) + return True except Exception as e: - logger.debug(f"mark-seen after cached read failed uid={uid}: {e}") + logger.warning(f"mark-seen after cached read failed uid={uid}: {e}") + return False @router.get("/read/{uid}") async def read_email_by_uid( @@ -3078,32 +3137,32 @@ def setup_email_routes(): if cached.get("attachment_version") != EMAIL_READ_ATTACHMENT_VERSION: cached = None if cached is not None: - if mark_seen: - try: - _asyncio.create_task(_asyncio.to_thread(_mark_email_seen_sync, uid, folder, account_id, owner)) - except RuntimeError: - pass + # A cache hit already holds a complete, valid message. Await the + # STORE so the response reports the real flag state, but never let + # a failed STORE withhold a body we are holding in memory. + if mark_seen and not await _asyncio.to_thread( + _mark_email_seen_sync, uid, folder, account_id, owner + ): + return {**cached, "mark_seen_failed": True} return cached if not full: persisted = _email_preview_cache_get(owner, account_id, folder, uid) if persisted and persisted.get("attachment_version") == EMAIL_READ_ATTACHMENT_VERSION: _read_cache_put(ck, persisted) - if mark_seen: - try: - _asyncio.create_task(_asyncio.to_thread(_mark_email_seen_sync, uid, folder, account_id, owner)) - except RuntimeError: - pass + if mark_seen and not await _asyncio.to_thread( + _mark_email_seen_sync, uid, folder, account_id, owner + ): + return {**persisted, "mark_seen_failed": True} return persisted result = await _asyncio.to_thread(_read_email_sync, uid, folder, account_id, owner, mark_seen, full) if result and not result.get("error"): - _read_cache_put(ck, result) + # `mark_seen_failed` describes this request, not the message, so it + # must not enter either cache — a later reader would otherwise be + # told a STORE failed that it never issued. + cacheable = {k: v for k, v in result.items() if k != "mark_seen_failed"} + _read_cache_put(ck, cacheable) if not full: - _email_preview_cache_put(owner, account_id, folder, uid, result) - if mark_seen: - try: - _asyncio.create_task(_asyncio.to_thread(_mark_email_seen_sync, uid, folder, account_id, owner)) - except RuntimeError: - pass + _email_preview_cache_put(owner, account_id, folder, uid, cacheable) return result def _schedule_recent_email_warm(emails: list, folder: str, account_id: str | None, owner: str): diff --git a/static/js/emailInbox.js b/static/js/emailInbox.js index 605a5ff61..cfee9ff90 100644 --- a/static/js/emailInbox.js +++ b/static/js/emailInbox.js @@ -149,6 +149,7 @@ let _loading = false; let _expanded = false; let _docModule = null; let _listSpinner = null; +let _openEmailRequestSeq = 0; let _senderFilter = null; // email address (lowercased) to filter by, or null let _senderFilterLabel = null; // display label for the active filter chip let _showEmailTags = localStorage.getItem('odysseus.email.showTags') !== '0'; @@ -187,7 +188,7 @@ export function init(documentModule) { } catch (_) {} if (opts.compose) { _composeNew(); return; } if (opts.email) { - await _openEmail(opts.email, null, opts.emailData, opts.mode || 'reply', opts.noteHint || ''); + await _openEmail(opts.email, null, opts.emailData, opts.mode || 'reply', opts.noteHint || '', '', opts.mailboxContext || null); } }, }); @@ -751,7 +752,21 @@ function _createEmailItem(em) { return item; } -async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply', noteHint = '', prefilledBody = '') { +async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply', noteHint = '', prefilledBody = '', mailboxContext = null) { + const openRequestSeq = ++_openEmailRequestSeq; + const folderAtStart = mailboxContext?.messageFolder || _currentFolder; + const accountAtStart = mailboxContext?.accountId ?? (window.__odysseusActiveEmailAccount || ''); + const accountQueryAtStart = accountAtStart ? `&account_id=${encodeURIComponent(accountAtStart)}` : ''; + const mailboxContextIsCurrent = typeof mailboxContext?.isCurrent === 'function' + ? mailboxContext.isCurrent + : () => ( + folderAtStart === _currentFolder && + accountAtStart === (window.__odysseusActiveEmailAccount || '') + ); + const isCurrentOpen = () => ( + openRequestSeq === _openEmailRequestSeq && + mailboxContextIsCurrent() + ); const aiReplyMode = mode === 'ai-reply-fast' ? 'fast' : ''; const wantsAiReply = mode === 'ai-reply' || !!aiReplyMode; // Body pre-fill from the agent's open_email_reply tool call takes the @@ -780,9 +795,10 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply', note let data = preloadedData; if (!data) { const fullQS = mode === 'forward' ? '&full=1' : ''; - const res = await fetch(`${API_BASE}/api/email/read/${em.uid}?folder=${encodeURIComponent(_currentFolder)}${_acct()}${fullQS}`); + const res = await fetch(`${API_BASE}/api/email/read/${em.uid}?folder=${encodeURIComponent(folderAtStart)}${accountQueryAtStart}&mark_seen=true${fullQS}`); data = await res.json(); } + if (!isCurrentOpen()) return; if (data.error) { console.error('Failed to read email:', data.error); return; @@ -808,7 +824,7 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply', note message_id: _fallback(data.message_id, em.message_id), }; if (wantsAiReply) { - const activeReplyAccount = data.account_id || em.account_id || window.__odysseusActiveEmailAccount || ''; + const activeReplyAccount = data.account_id || em.account_id || accountAtStart; if (data.cached_ai_reply && !noteHint && !activeReplyAccount) { aiSuggestedBody = _cleanAiReplyText(data.cached_ai_reply); } else { @@ -834,7 +850,7 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply', note session_id: currentSessionId, message_id: data.message_id || '', uid: String(em.uid || ''), - folder: _currentFolder, + folder: folderAtStart, account_id: activeReplyAccount, fast: true, user_hint: (noteHint || '').trim() || undefined, @@ -842,6 +858,7 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply', note }); const result = await res.json(); if (draftToastTimer) clearTimeout(draftToastTimer); + if (!isCurrentOpen()) return; if (result.success && result.reply) { aiSuggestedBody = _cleanAiReplyText(result.reply); } else { @@ -855,6 +872,7 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply', note } } catch (e) { if (draftToastTimer) clearTimeout(draftToastTimer); + if (!isCurrentOpen()) return; console.error('AI reply generation failed:', e); import('./ui.js').then(m => m.showError && m.showError('AI reply failed: ' + (e.message || e))).catch(() => {}); return; @@ -862,8 +880,12 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply', note } } - em.is_read = true; - if (itemEl) itemEl.classList.remove('email-unread'); + if (!isCurrentOpen()) return; + // Only claim the message is read when the provider accepted the \Seen + // transition. A failed STORE still opens the message; it just stays unread. + const markedSeen = !data.mark_seen_failed; + em.is_read = markedSeen; + if (itemEl) itemEl.classList.toggle('email-unread', !markedSeen); // Addresses to exclude from Reply All. Prefer the full set of configured // accounts (so a multi-account user's other mailboxes are excluded too), @@ -911,7 +933,7 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply', note if (mode !== 'forward' && data.message_id) content += `\nIn-Reply-To: ${data.message_id}`; if (mode !== 'forward' && data.message_id) content += `\nReferences: ${data.references ? data.references + ' ' + data.message_id : data.message_id}`; content += `\nX-Source-UID: ${em.uid}`; - content += `\nX-Source-Folder: ${_currentFolder}`; + content += `\nX-Source-Folder: ${folderAtStart}`; if (data.attachments && data.attachments.length > 0) { const attStr = data.attachments.map(a => `${a.index}:${a.filename}:${a.size}`).join('|'); content += `\nX-Attachments: ${attStr}`; @@ -980,21 +1002,27 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply', note // and block Send on long threads. const reuseExisting = mode !== 'forward' && !!aiSuggestedBody; const existingDocId = (reuseExisting && _docModule.findEmailDocId) - ? _docModule.findEmailDocId(em.uid, _currentFolder) + ? _docModule.findEmailDocId(em.uid, folderAtStart) : null; if (existingDocId) { if (!_docModule.isPanelOpen()) _docModule.openPanel(); await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))); + if (!isCurrentOpen()) return; await _docModule.loadDocument(existingDocId); + if (!isCurrentOpen()) return; if (typeof _docModule.ensureEmailDraftEnvelope === 'function') { await _docModule.ensureEmailDraftEnvelope(existingDocId, content); + if (!isCurrentOpen()) return; } if (aiSuggestedBody && typeof _docModule.replaceEmailReplyBody === 'function') { await _docModule.replaceEmailReplyBody(existingDocId, aiSuggestedBody, { force: false }); + if (!isCurrentOpen()) return; } _bringEmailReplyDraftToFrontOnMobile(); } else { + if (!isCurrentOpen()) return; let activeSid = await _createEmailChat(data, { forceNew: true }); + if (!isCurrentOpen()) return; if (!activeSid) { console.error('reply: could not obtain a session_id'); import('./ui.js').then(m => m.showError && m.showError('Could not start a reply chat.')).catch(() => {}); @@ -1012,13 +1040,20 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply', note }), }); let docRes = await createReplyDoc(activeSid); + if (!isCurrentOpen()) return; if (docRes.status === 404) { console.warn('[reply-debug] draft session rejected; retrying in a fresh email chat', activeSid); + if (!isCurrentOpen()) return; activeSid = await _createEmailChat(data, { forceNew: true }); - if (activeSid) docRes = await createReplyDoc(activeSid); + if (!isCurrentOpen()) return; + if (activeSid) { + docRes = await createReplyDoc(activeSid); + if (!isCurrentOpen()) return; + } } if (!docRes.ok) { const errText = await docRes.text(); + if (!isCurrentOpen()) return; console.error('[reply-debug] POST /api/document failed', docRes.status, errText); // uiModule isn't statically imported here — use the dynamic // import pattern the rest of this file uses. (Previously this @@ -1028,10 +1063,12 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply', note return; } const doc = await docRes.json(); + if (!isCurrentOpen()) return; if (doc.id) { const wasOpen = _docModule.isPanelOpen(); if (!wasOpen) _docModule.openPanel(); await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))); + if (!isCurrentOpen()) return; // Use the doc dict from the POST directly — avoids a 404 race // when the GET fires before the new row is visible to the read // connection (or when caching is interfering). loadDocument's @@ -1040,12 +1077,14 @@ async function _openEmail(em, itemEl, preloadedData = null, mode = 'reply', note _docModule.injectFreshDoc(doc); } else { await _docModule.loadDocument(doc.id); + if (!isCurrentOpen()) return; } _bringEmailReplyDraftToFrontOnMobile(); } } } } catch (e) { + if (!isCurrentOpen()) return; console.error('Failed to open email:', e); // Surface the failure so a silent throw in the reply flow doesn't // look like "nothing happened". Dynamic import — uiModule isn't a diff --git a/static/js/emailLibrary.js b/static/js/emailLibrary.js index 32b906ddc..9425a0181 100644 --- a/static/js/emailLibrary.js +++ b/static/js/emailLibrary.js @@ -30,6 +30,10 @@ import { bindMenuDismiss, dismissOrRemove } from './escMenuStack.js'; const API_BASE = window.location.origin; let _emailUnreadChipClickWired = false; let _libLoadSeq = 0; +let _emailMailboxGeneration = 0; +let _emailCardOpenSeq = 0; +let _emailReadMutationSeq = 0; +const _emailReadMutations = new Map(); let _libFolderSeq = 0; let _libSearchSeq = 0; let _libSearchHadResults = false; @@ -837,14 +841,41 @@ document.addEventListener('keydown', (e) => { e.stopImmediatePropagation?.(); }, true); -function _syncEmailReadState(uid, isRead = true) { +function _emailReadContextKey(context) { + return [context.accountId, context.folder, context.uid].map(value => String(value || '')).join('\u0000'); +} + +function _emailReadContextIsCurrent(context) { + if (!context) return true; + return ( + String(state._libAccountId || '') === context.accountId && + String(state._libFolder || 'INBOX') === context.libraryFolder && + _emailMailboxGeneration === context.mailboxGeneration + ); +} + +function _emailMatchesReadContext(email, context) { + if (String(email?.uid || '') !== context.uid) return false; + const accountId = String(email?.account_id || context.accountId); + const folder = String(email?.folder || context.folder); + return accountId === context.accountId && folder === context.folder; +} + +function _syncEmailReadState(uid, isRead = true, context = null) { if (uid == null) return; const uidStr = String(uid); const read = !!isRead; - const match = (state._libEmails || []).find(x => String(x.uid) === uidStr); + if (context && (!_emailReadContextIsCurrent(context) || uidStr !== context.uid)) return; + const match = (state._libEmails || []).find(x => ( + context ? _emailMatchesReadContext(x, context) : String(x.uid) === uidStr + )); if (match) match.is_read = read; document.querySelectorAll('.doclib-card[data-uid="' + CSS.escape(uidStr) + '"]').forEach(card => { + if (context && ( + String(card.dataset.emailAccount || '') !== context.accountId || + String(card.dataset.emailFolder || '') !== context.folder + )) return; card.classList.toggle('email-card-unread', !read); const titleRow = card.querySelector('.email-card-titlerow'); if (read) { @@ -1908,6 +1939,7 @@ function _resetEmailListForFreshLoad({ useCache = true } = {}) { _exitEmailReaderModeForList(); _resetBulkSelectionForContextChange(); state._libOffset = 0; + _emailMailboxGeneration += 1; _libLoadSeq += 1; const ck = _libCacheKey(); const cached = useCache ? _libCacheGet(ck) : null; @@ -2286,7 +2318,25 @@ function _publishActiveAccount() { export function initEmailLibrary(config) { state._docModule = config.documentModule; - state._onEmailClick = config.onEmailClick; + const onEmailClick = config.onEmailClick; + state._onEmailClick = typeof onEmailClick === 'function' ? (options = {}) => { + const accountId = String(state._libAccountId || ''); + const libraryFolder = String(state._libFolder || 'INBOX'); + const messageFolder = String(options.email?.folder || libraryFolder); + const mailboxGeneration = _emailMailboxGeneration; + const mailboxContext = Object.freeze({ + accountId, + libraryFolder, + messageFolder, + mailboxGeneration, + isCurrent: () => ( + String(state._libAccountId || '') === accountId && + String(state._libFolder || 'INBOX') === libraryFolder && + _emailMailboxGeneration === mailboxGeneration + ), + }); + return onEmailClick({ ...options, mailboxContext }); + } : null; } export function isOpen() { return state._libOpen; } @@ -2303,6 +2353,7 @@ export function openEmailLibrary(opts = {}) { document.removeEventListener('keydown', state._libEscHandler, true); state._libEscHandler = null; } + _emailMailboxGeneration += 1; state._libOpen = true; // On mobile the sidebar overlays content — close it so the email view isn't // opened behind it (same pattern as session-switch/delete). @@ -4836,6 +4887,8 @@ function _createCard(em) { else if (!em.is_read) cls += ' email-card-unread'; card.className = cls; card.dataset.uid = String(em.uid); + card.dataset.emailAccount = String(em.account_id || state._libAccountId || ''); + card.dataset.emailFolder = String(em.folder || state._libFolder || 'INBOX'); if (state._selectMode && state._selectedUids.has(em.uid)) card.classList.add('selected'); // Checkbox in select mode @@ -5162,6 +5215,25 @@ async function _toggleCardPreview(card, em) { // currently-selected folder for normal inbox cards. const folderAtStart = (em && em.folder) || libraryFolderAtStart; const uidAtStart = String(em?.uid || card?.dataset?.uid || ''); + const wasReadAtStart = !!em?.is_read; + const openGeneration = ++_emailCardOpenSeq; + const readContext = Object.freeze({ + accountId: String(accountAtStart), + libraryFolder: String(libraryFolderAtStart), + folder: String(folderAtStart), + uid: uidAtStart, + mailboxGeneration: _emailMailboxGeneration, + }); + const readContextKey = _emailReadContextKey(readContext); + const isCurrentOpen = () => ( + openGeneration === _emailCardOpenSeq && + _emailReadContextIsCurrent(readContext) && + accountAtStart === (state._libAccountId || '') && + libraryFolderAtStart === (state._libFolder || 'INBOX') && + uidAtStart === String(card?.dataset?.uid || '') && + card.isConnected && + card.classList.contains('email-card-expanded') + ); const grid = card.closest('.doclib-grid'); const gridRect = grid?.getBoundingClientRect?.(); const modal = document.getElementById('email-lib-modal'); @@ -5186,6 +5258,30 @@ async function _toggleCardPreview(card, em) { return; } + // Every authoritative open supersedes any older optimistic mutation for the + // same immutable mailbox identity. Carry the original unread state forward + // so a close/reopen followed by failure still rolls back exactly once, while + // a late failure from the superseded request cannot undo a newer success. + const previousMutation = _emailReadMutations.get(readContextKey); + const readMutation = { + generation: ++_emailReadMutationSeq, + rollbackUnread: !wasReadAtStart || !!previousMutation?.rollbackUnread, + }; + _emailReadMutations.set(readContextKey, readMutation); + const restoreUnreadState = () => { + if (_emailReadMutations.get(readContextKey)?.generation !== readMutation.generation) return; + _emailReadMutations.delete(readContextKey); + if (readMutation.rollbackUnread) _syncEmailReadState(uidAtStart, false, readContext); + }; + const commitReadState = () => { + // A successful STORE/mark_seen is authoritative for this immutable + // mailbox identity even when a newer open is still pending. Retire that + // newer rollback token too, otherwise its later failure could restore an + // unread state that no longer exists at the provider. + _emailReadMutations.delete(readContextKey); + _syncEmailReadState(uidAtStart, true, readContext); + }; + // Collapse any other expanded card if (grid) { grid.querySelectorAll('.email-card-expanded').forEach(c => { @@ -5207,10 +5303,10 @@ async function _toggleCardPreview(card, em) { requestAnimationFrame(() => { try { card.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (_) {} }); - if (!em.is_read) { - _syncEmailReadState(em.uid, true); - fetch(`${API_BASE}/api/email/mark-read/${em.uid}?folder=${encodeURIComponent(folderAtStart)}${_acct()}`, { method: 'POST' }) - .catch(err => console.error('Failed to mark email read:', err)); + if (!wasReadAtStart) { + // Keep the current optimistic visual update, but let the read request below + // own the provider-side \Seen transition. A failure restores unread state. + _syncEmailReadState(uidAtStart, true, readContext); } // Class hook on the modal so the header-hide / padding rules work on // browsers without :has() support (Firefox mobile) — the :has() versions @@ -5239,25 +5335,28 @@ async function _toggleCardPreview(card, em) { } catch (_) {} }; + let authoritativeReadSucceeded = false; try { - const res = await fetch(`${API_BASE}/api/email/read/${em.uid}?folder=${encodeURIComponent(folderAtStart)}${_acct()}`); + const accountQueryAtStart = accountAtStart ? `&account_id=${encodeURIComponent(accountAtStart)}` : ''; + const res = await fetch(`${API_BASE}/api/email/read/${encodeURIComponent(uidAtStart)}?folder=${encodeURIComponent(folderAtStart)}${accountQueryAtStart}&mark_seen=true`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); - if ( - accountAtStart !== (state._libAccountId || '') || - libraryFolderAtStart !== (state._libFolder || 'INBOX') || - uidAtStart !== String(card?.dataset?.uid || '') || - !card.isConnected || - !card.classList.contains('email-card-expanded') - ) { - return; - } if (data.error) { - showFailedReader(`Failed to load email: ${data.error}`); + restoreUnreadState(); + if (isCurrentOpen()) showFailedReader(`Failed to load email: ${data.error}`); return; } - // Mark as read locally - _syncEmailReadState(em.uid, true); + if (data.mark_seen_failed) { + // The body is authoritative even when the provider refused the \Seen + // transition. Render the message and roll the unread marker back so the + // list keeps telling the truth, rather than refusing to open a message + // we successfully read. + restoreUnreadState(); + } else { + authoritativeReadSucceeded = true; + commitReadState(); + } + if (!isCurrentOpen()) return; _stampReaderContext(reader, { ...em, ...data }, state._libFolder, state._libAccountId); // Build the attachments wrap using the shared helper so the signature- @@ -5439,7 +5538,10 @@ async function _toggleCardPreview(card, em) { // Always stop bubbling so the card's click doesn't fire while reading. reader.addEventListener('click', (ev) => { ev.stopPropagation(); }); } catch (e) { - showFailedReader(e?.message ? `Failed to load email: ${e.message}` : 'Failed to load email'); + if (!authoritativeReadSucceeded) restoreUnreadState(); + if (isCurrentOpen()) { + showFailedReader(e?.message ? `Failed to load email: ${e.message}` : 'Failed to load email'); + } } } diff --git a/tests/test_email_open_dedup_js.py b/tests/test_email_open_dedup_js.py new file mode 100644 index 000000000..cc6c431f7 --- /dev/null +++ b/tests/test_email_open_dedup_js.py @@ -0,0 +1,231 @@ +"""Focused browser-side regression coverage for authoritative email opens.""" + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + + +_REPO = Path(__file__).resolve().parent.parent +_INBOX_JS = _REPO / "static" / "js" / "emailInbox.js" +_LIBRARY_JS = _REPO / "static" / "js" / "emailLibrary.js" +_HAS_NODE = shutil.which("node") is not None + + +def _extract_between(source: str, signature: str, next_marker: str) -> str: + start = source.index(signature) + end = source.index(next_marker, start) + return source[start:end].rstrip() + + +def test_library_unread_preview_has_one_authoritative_request_and_rollback(): + source = _LIBRARY_JS.read_text(encoding="utf-8") + function = _extract_between(source, "async function _toggleCardPreview", "\n/**\n * Wrap a probable signature block") + + assert function.count("/api/email/read/") == 1 + assert "/api/email/mark-read/" not in function + assert "&mark_seen=true" in function + assert "_syncEmailReadState(uidAtStart, true, readContext)" in function + assert "_syncEmailReadState(uidAtStart, false, readContext)" in function + assert "openGeneration === _emailCardOpenSeq" in function + assert "_emailReadMutations.get(readContextKey)?.generation !== readMutation.generation" in function + assert "authoritativeReadSucceeded = true;" in function + assert "if (!authoritativeReadSucceeded) restoreUnreadState();" in function + assert "if (!isCurrentOpen()) return" in function + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_library_authoritative_success_defeats_newer_rollback_in_either_order(): + source = _LIBRARY_JS.read_text(encoding="utf-8") + function = _extract_between(source, "async function _toggleCardPreview", "\n/**\n * Wrap a probable signature block") + settlements = _extract_between( + function, + " const restoreUnreadState = () => {", + "\n\n // Collapse any other expanded card", + ) + + harness = f""" +const _emailReadMutations = new Map(); +const readContextKey = 'same-mailbox-message'; +const uidAtStart = '1'; +const readContext = {{ accountId: 'acct-a', folder: 'INBOX', uid: '1' }}; +const readUpdates = []; +function _syncEmailReadState(uid, isRead, context) {{ + readUpdates.push({{ uid, isRead, context }}); +}} +function createSettlers(readMutation) {{ +{settlements} + return {{ restoreUnreadState, commitReadState }}; +}} +function runRace(successFirst) {{ + _emailReadMutations.clear(); + readUpdates.length = 0; + const mutationA = {{ generation: 1, rollbackUnread: true }}; + _emailReadMutations.set(readContextKey, mutationA); + const settlersA = createSettlers(mutationA); + const mutationB = {{ generation: 2, rollbackUnread: true }}; + _emailReadMutations.set(readContextKey, mutationB); + const settlersB = createSettlers(mutationB); + if (successFirst) {{ + settlersA.commitReadState(); + settlersB.restoreUnreadState(); + }} else {{ + settlersB.restoreUnreadState(); + settlersA.commitReadState(); + }} + return {{ + hasMutation: _emailReadMutations.has(readContextKey), + readUpdates: readUpdates.map(update => update.isRead), + }}; +}} +console.log(JSON.stringify({{ + successFirst: runRace(true), + failureFirst: runRace(false), +}})); +""" + proc = subprocess.run( + ["node", "--input-type=module"], + input=harness, + capture_output=True, + text=True, + cwd=str(_REPO), + timeout=30, + ) + assert proc.returncode == 0, f"node failed: {proc.stderr}\n---\n{harness}" + assert json.loads(proc.stdout.strip()) == { + "successFirst": {"hasMutation": False, "readUpdates": [True]}, + "failureFirst": {"hasMutation": False, "readUpdates": [False, True]}, + } + + +def test_library_reply_open_carries_immutable_mailbox_context(): + library_source = _LIBRARY_JS.read_text(encoding="utf-8") + inbox_source = _INBOX_JS.read_text(encoding="utf-8") + + assert "const mailboxGeneration = _emailMailboxGeneration;" in library_source + assert "messageFolder = String(options.email?.folder || libraryFolder)" in library_source + assert "return onEmailClick({ ...options, mailboxContext });" in library_source + assert "mailboxContext?.messageFolder || _currentFolder" in inbox_source + assert "mailboxContextIsCurrent()" in inbox_source + assert "if (!isCurrentOpen()) return;\n let activeSid = await _createEmailChat" in inbox_source + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_inbox_late_read_response_cannot_apply_after_newer_open(): + source = _INBOX_JS.read_text(encoding="utf-8") + function = _extract_between(source, "async function _openEmail", "\nfunction _showEmailMenu") + assert "let _openEmailRequestSeq = 0;" in source + + harness = f""" +const realLog = console.log; +console.error = () => {{}}; +const API_BASE = 'https://odysseus.invalid'; +const window = {{ __odysseusActiveEmailAccount: 'acct-a' }}; +let _currentFolder = 'INBOX'; +const _acct = () => '&account_id=acct-a'; +let _openEmailRequestSeq = 0; +let _docModule = null; +const spinnerModule = {{ createWhirlpool() {{ throw new Error('spinner should not run'); }} }}; +const sessionModule = null; +let firstResolve; +const calls = []; +async function fetch(url) {{ + calls.push(String(url)); + if (calls.length === 1) {{ + return await new Promise((resolve) => {{ + firstResolve = () => resolve({{ json: async () => ({{ uid: '1', subject: 'old' }}) }}); + }}); + }} + return {{ json: async () => ({{ error: 'newer open completed test' }}) }}; +}} +{function} +const oldEmail = {{ uid: '1', is_read: false }}; +const newerEmail = {{ uid: '2', is_read: false }}; +const first = _openEmail(oldEmail, null); +await Promise.resolve(); +const second = _openEmail(newerEmail, null); +await second; +firstResolve(); +await first; +realLog(JSON.stringify({{ calls, oldRead: oldEmail.is_read, newerRead: newerEmail.is_read }})); +""" + proc = subprocess.run( + ["node", "--input-type=module"], + input=harness, + capture_output=True, + text=True, + cwd=str(_REPO), + timeout=30, + ) + assert proc.returncode == 0, f"node failed: {proc.stderr}\n---\n{harness}" + result = json.loads(proc.stdout.strip()) + assert len(result["calls"]) == 2 + assert all("mark_seen=true" in url for url in result["calls"]) + assert result["oldRead"] is False + assert result["newerRead"] is False + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +@pytest.mark.parametrize("context_change", ["account", "folder", "library"]) +def test_inbox_late_read_response_cannot_apply_after_mailbox_switch(context_change): + source = _INBOX_JS.read_text(encoding="utf-8") + function = _extract_between(source, "async function _openEmail", "\nfunction _showEmailMenu") + + changes = { + "account": "window.__odysseusActiveEmailAccount = 'acct-b';", + "folder": "_currentFolder = 'Archive';", + "library": "libraryCurrent = false;", + } + change = changes[context_change] + open_call = ( + "_openEmail(email, null, null, 'reply', '', '', mailboxContext)" + if context_change == "library" + else "_openEmail(email, null)" + ) + harness = f""" +const realLog = console.log; +console.error = () => {{}}; +const API_BASE = 'https://odysseus.invalid'; +const window = {{ __odysseusActiveEmailAccount: 'acct-a' }}; +let _currentFolder = 'INBOX'; +const _acct = () => '&account_id=acct-a'; +let _openEmailRequestSeq = 0; +let libraryCurrent = true; +const mailboxContext = {{ + accountId: 'acct-a', + messageFolder: 'Archive', + isCurrent: () => libraryCurrent, +}}; +let createCalls = 0; +let _docModule = {{}}; +async function _createEmailChat() {{ createCalls += 1; return 'stale-session'; }} +const spinnerModule = {{ createWhirlpool() {{ throw new Error('spinner should not run'); }} }}; +const sessionModule = null; +let resolveRead; +async function fetch() {{ + return await new Promise((resolve) => {{ + resolveRead = () => resolve({{ json: async () => ({{ uid: '1', subject: 'old' }}) }}); + }}); +}} +{function} +const email = {{ uid: '1', is_read: false }}; +const pending = {open_call}; +await Promise.resolve(); +{change} +resolveRead(); +await pending; +realLog(JSON.stringify({{ createCalls, isRead: email.is_read }})); +""" + proc = subprocess.run( + ["node", "--input-type=module"], + input=harness, + capture_output=True, + text=True, + cwd=str(_REPO), + timeout=30, + ) + assert proc.returncode == 0, f"node failed: {proc.stderr}\n---\n{harness}" + result = json.loads(proc.stdout.strip()) + assert result == {"createCalls": 0, "isRead": False} diff --git a/tests/test_email_read_mark_seen.py b/tests/test_email_read_mark_seen.py new file mode 100644 index 000000000..2d9faa273 --- /dev/null +++ b/tests/test_email_read_mark_seen.py @@ -0,0 +1,278 @@ +import asyncio +from contextlib import contextmanager + +import pytest + + +RAW_EMAIL = ( + b"From: Sender <sender@example.com>\r\n" + b"To: Alice <alice@example.com>\r\n" + b"Subject: Single authoritative open\r\n" + b"Message-ID: <single-open@example.com>\r\n" + b"Date: Tue, 04 Aug 2026 12:00:00 +0000\r\n" + b"Content-Type: text/plain; charset=utf-8\r\n" + b"\r\n" + b"Body" +) + + +def _route_endpoint(router, path: str, method: str): + method = method.upper() + for route in router.routes: + if route.path == path and method in getattr(route, "methods", set()): + return route.endpoint + raise AssertionError(f"route not found: {method} {path}") + + +class FakeImap: + def __init__(self, store_status="OK", readonly_mailbox=False): + self.store_status = store_status + # Shared archives and some provider folders reject a read-write SELECT. + self.readonly_mailbox = readonly_mailbox + self.selects = [] + self.commands = [] + + def select(self, mailbox, readonly=False): + self.selects.append((mailbox, readonly)) + if self.readonly_mailbox and not readonly: + raise OSError("[READ-ONLY] Mailbox is read-only") + return "OK", [b"1"] + + def uid(self, command, uid, *args): + self.commands.append((command, uid, *args)) + if command == "FETCH": + header, body = RAW_EMAIL.split(b"\r\n\r\n", 1) + return "OK", [ + (b"1 (UID 42 BODY[HEADER])", header + b"\r\n\r\n"), + (b"1 (UID 42 BODY[TEXT]<0>)", body), + ] + if command == "STORE": + # RFC 3501 STORE takes a parenthesized flag-list. GreenMail rejects + # the formerly emitted bare ``\Seen`` atom with BAD, so keep the + # fake strict enough to catch that provider-compatibility failure. + if args != ("+FLAGS", "(\\Seen)"): + return "BAD", [b"Expected:'(' found:'\\'"] + return self.store_status, [] + raise AssertionError(f"unexpected IMAP command: {command}") + + +def _install_fakes(monkeypatch, tmp_path, *, store_status="OK", readonly_mailbox=False): + import routes.email_helpers as email_helpers + import routes.email_routes as email_routes + + db_path = tmp_path / "email.db" + monkeypatch.setattr(email_helpers, "SCHEDULED_DB", db_path) + monkeypatch.setattr(email_routes, "SCHEDULED_DB", db_path) + email_helpers._init_scheduled_db() + + connections = [] + indexed_updates = [] + + @contextmanager + def fake_imap(account_id=None, owner=""): + conn = FakeImap(store_status=store_status, readonly_mailbox=readonly_mailbox) + connections.append(conn) + yield conn + + monkeypatch.setattr(email_routes, "_start_poller", lambda: None) + monkeypatch.setattr(email_routes, "_imap", fake_imap) + monkeypatch.setattr(email_routes, "_email_preview_cache_get", lambda *_args, **_kwargs: None) + monkeypatch.setattr(email_routes, "_email_preview_cache_put", lambda *_args, **_kwargs: None) + monkeypatch.setattr(email_routes, "_email_attachment_meta_cache_get", lambda *_args, **_kwargs: None) + monkeypatch.setattr(email_routes, "_email_attachment_meta_cache_put", lambda *_args, **_kwargs: None) + monkeypatch.setattr( + email_routes, + "_email_index_update_flags", + lambda *args, **_kwargs: indexed_updates.append(args), + ) + return email_routes, connections, indexed_updates + + +@pytest.mark.asyncio +@pytest.mark.parametrize("mark_seen", [True, False]) +async def test_read_email_seen_contract_uses_one_imap_connection(monkeypatch, tmp_path, mark_seen): + email_routes, connections, indexed_updates = _install_fakes(monkeypatch, tmp_path) + router = email_routes.setup_email_routes() + read_email = _route_endpoint(router, "/api/email/read/{uid}", "GET") + + result = await read_email( + "42", + folder="INBOX", + account_id="acct-a", + mark_seen=mark_seen, + full=False, + owner="alice", + ) + + assert result["uid"] == "42" + assert len(connections) == 1 + conn = connections[0] + assert conn.selects == [(conn.selects[0][0], not mark_seen)] + assert [command[0] for command in conn.commands] == ( + ["FETCH", "STORE"] if mark_seen else ["FETCH"] + ) + assert "BODY.PEEK[HEADER]" in conn.commands[0][2] + if mark_seen: + assert conn.commands[1][2:] == ("+FLAGS", "(\\Seen)") + assert indexed_updates == [("alice", "acct-a", "INBOX", "42", "\\Seen", True)] + else: + assert indexed_updates == [] + + +@pytest.mark.asyncio +async def test_cached_read_awaits_one_seen_store_without_refetch(monkeypatch, tmp_path): + email_routes, connections, indexed_updates = _install_fakes(monkeypatch, tmp_path) + router = email_routes.setup_email_routes() + read_email = _route_endpoint(router, "/api/email/read/{uid}", "GET") + + first = await read_email( + "42", folder="INBOX", account_id="acct-a", mark_seen=False, full=False, owner="alice" + ) + monkeypatch.setattr( + asyncio, + "create_task", + lambda *_args, **_kwargs: (_ for _ in ()).throw( + AssertionError("cached mark-seen must be awaited, not scheduled") + ), + ) + second = await read_email( + "42", folder="INBOX", account_id="acct-a", mark_seen=True, full=False, owner="alice" + ) + + assert first["message_id"] == second["message_id"] + assert len(connections) == 2 + assert [command[0] for command in connections[0].commands] == ["FETCH"] + assert [command[0] for command in connections[1].commands] == ["STORE"] + assert connections[1].commands[0][2:] == ("+FLAGS", "(\\Seen)") + assert connections[1].selects[0][1] is False + assert indexed_updates == [("alice", "acct-a", "INBOX", "42", "\\Seen", True)] + + +@pytest.mark.asyncio +async def test_seen_store_failure_returns_the_body_and_reports_the_failure(monkeypatch, tmp_path): + """A failed STORE must not cost the reader the message. + + The body was fetched successfully before the flag update was attempted, so + the response stays a normal read and carries `mark_seen_failed` for the + client to roll its optimistic unread marker back. + """ + email_routes, connections, indexed_updates = _install_fakes( + monkeypatch, tmp_path, store_status="NO" + ) + router = email_routes.setup_email_routes() + read_email = _route_endpoint(router, "/api/email/read/{uid}", "GET") + + result = await read_email( + "42", folder="INBOX", account_id="acct-a", mark_seen=True, full=False, owner="alice" + ) + + assert "error" not in result + assert result["uid"] == "42" + assert result["mark_seen_failed"] is True + assert len(connections) == 1 + assert [command[0] for command in connections[0].commands] == ["FETCH", "STORE"] + # The local index must not claim a transition the provider rejected. + assert indexed_updates == [] + + +@pytest.mark.asyncio +async def test_read_only_mailbox_serves_the_message_without_marking_seen(monkeypatch, tmp_path): + """A mailbox that refuses a read-write SELECT is still readable. + + Opening the message is the user's actual goal; the \\Seen transition is a + side effect of it. A folder that cannot accept flag changes must therefore + fall back to a read-only selection rather than failing the open. + """ + email_routes, connections, indexed_updates = _install_fakes( + monkeypatch, tmp_path, readonly_mailbox=True + ) + router = email_routes.setup_email_routes() + read_email = _route_endpoint(router, "/api/email/read/{uid}", "GET") + + result = await read_email( + "42", folder="Archive", account_id="acct-a", mark_seen=True, full=False, owner="alice" + ) + + assert "error" not in result + assert result["uid"] == "42" + assert result["mark_seen_failed"] is True + # Read-write attempt first, then the read-only retry on the same connection. + assert [readonly for _mailbox, readonly in connections[0].selects] == [False, True] + # No STORE is attempted once the mailbox is known to be read-only. + assert [command[0] for command in connections[0].commands] == ["FETCH"] + assert indexed_updates == [] + + +@pytest.mark.asyncio +async def test_failed_seen_state_is_not_replayed_from_cache(monkeypatch, tmp_path): + """`mark_seen_failed` describes one request, not the stored message. + + A second read that does not ask to mark seen must come back clean, or every + later reader would inherit a STORE failure it never issued. + """ + email_routes, connections, _ = _install_fakes(monkeypatch, tmp_path, store_status="NO") + router = email_routes.setup_email_routes() + read_email = _route_endpoint(router, "/api/email/read/{uid}", "GET") + + failed = await read_email( + "42", folder="INBOX", account_id="acct-a", mark_seen=True, full=False, owner="alice" + ) + replayed = await read_email( + "42", folder="INBOX", account_id="acct-a", mark_seen=False, full=False, owner="alice" + ) + + assert failed["mark_seen_failed"] is True + assert replayed.get("mark_seen_failed", False) is False + assert replayed["uid"] == "42" + + +@pytest.mark.asyncio +async def test_unparseable_read_does_not_mark_seen(monkeypatch, tmp_path): + email_routes, connections, indexed_updates = _install_fakes(monkeypatch, tmp_path) + monkeypatch.setattr( + email_routes.email_mod, + "message_from_bytes", + lambda *_args, **_kwargs: (_ for _ in ()).throw(ValueError("malformed message")), + ) + router = email_routes.setup_email_routes() + read_email = _route_endpoint(router, "/api/email/read/{uid}", "GET") + + result = await read_email( + "42", folder="INBOX", account_id="acct-a", mark_seen=True, full=False, owner="alice" + ) + + assert result == {"error": "Mail operation failed"} + assert len(connections) == 1 + assert [command[0] for command in connections[0].commands] == ["FETCH"] + assert indexed_updates == [] + + +@pytest.mark.asyncio +async def test_cached_seen_store_failure_returns_the_cached_body(monkeypatch, tmp_path): + """A cache hit already holds a complete message; a failed STORE cannot take it away. + + This is the path where withholding the body would be least defensible — the + response is served from memory and needed no network at all. + """ + email_routes, connections, indexed_updates = _install_fakes( + monkeypatch, tmp_path, store_status="NO" + ) + router = email_routes.setup_email_routes() + read_email = _route_endpoint(router, "/api/email/read/{uid}", "GET") + + first = await read_email( + "42", folder="INBOX", account_id="acct-a", mark_seen=False, full=False, owner="alice" + ) + second = await read_email( + "42", folder="INBOX", account_id="acct-a", mark_seen=True, full=False, owner="alice" + ) + + assert first["uid"] == "42" + assert "error" not in second + assert second["uid"] == "42" + assert second["body"] == first["body"] + assert second["mark_seen_failed"] is True + assert len(connections) == 2 + assert [command[0] for command in connections[0].commands] == ["FETCH"] + assert [command[0] for command in connections[1].commands] == ["STORE"] + assert indexed_updates == [] From 96aca5209485a124d63ccb3798f10ce7967b604c Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:14:10 +0100 Subject: [PATCH 047/125] perf(email): make library prewarm idle and bounded (#5925) * perf(email): make library prewarm idle and bounded * fix(email): preserve idle prewarm and prioritize foreground * fix(email): retry interrupted idle prewarm safely --- static/js/emailLibrary.js | 429 +++++++++++++++++++--------- tests/test_email_library_prewarm.py | 414 +++++++++++++++++++++++++++ 2 files changed, 711 insertions(+), 132 deletions(-) create mode 100644 tests/test_email_library_prewarm.py diff --git a/static/js/emailLibrary.js b/static/js/emailLibrary.js index 9425a0181..c56109196 100644 --- a/static/js/emailLibrary.js +++ b/static/js/emailLibrary.js @@ -1793,11 +1793,18 @@ function _rememberedEmailAccountId() { // results and __scheduled__ are deliberately not cached. const _libListCache = new Map(); const _LIB_CACHE_MAX = 24; +const _LIB_INITIAL_PAGE_SIZE = 100; const _LIB_SESSION_CACHE_PREFIX = 'odysseus.email.list.'; const _LIB_SESSION_CACHE_TTL_MS = 10 * 60 * 1000; const _LIB_LAST_ACCOUNT_KEY = 'odysseus.email.lastAccountId'; -let _libPrewarmTimer = null; +const _LIB_PREWARM_COOLDOWN_MS = 5 * 60 * 1000; +let _libPrewarmDelayTimer = null; +let _libPrewarmIdleHandle = null; let _libPrewarmPromise = null; +let _libPrewarmResolve = null; +let _libPrewarmAbortController = null; +let _libPrewarmDetachPriorityListeners = null; +let _libPrewarmGeneration = 0; let _libLastPrewarmAt = 0; let _libUnreadPrewarmKey = ''; let _libUnreadPrewarmAt = 0; @@ -2108,162 +2115,319 @@ function _isChatInteractionBusy() { } } -function _loadEmailsWhenChatIdle({ delay = 50, retries = 180, options = {} } = {}) { - const run = () => { - if (!state._libOpen || !document.getElementById('email-lib-modal')) return; - if (_isChatInteractionBusy() && retries > 0) { - setTimeout(() => _loadEmailsWhenChatIdle({ delay: 1000, retries: retries - 1, options }), 1000); +function _canRunEmailPrewarm() { + if (state._libOpen || state._libLoading || _libSearchInFlight) return false; + if (document.visibilityState && document.visibilityState !== 'visible') return false; + return !_isChatInteractionBusy(); +} + +function _isEmailPrewarmTemporarilyBlocked() { + if (state._libOpen || state._libLoading || _libSearchInFlight) return false; + if (document.visibilityState && document.visibilityState !== 'visible') return false; + return _isChatInteractionBusy(); +} + +function _isEmailPrewarmCurrent(generation, signal) { + return generation === _libPrewarmGeneration + && !signal?.aborted + && _canRunEmailPrewarm(); +} + +function _settleEmailPrewarm(generation, value = false) { + if (generation !== _libPrewarmGeneration) return; + const resolve = _libPrewarmResolve; + const detachPriorityListeners = _libPrewarmDetachPriorityListeners; + _libPrewarmDelayTimer = null; + _libPrewarmIdleHandle = null; + _libPrewarmPromise = null; + _libPrewarmResolve = null; + _libPrewarmAbortController = null; + _libPrewarmDetachPriorityListeners = null; + detachPriorityListeners?.(); + resolve?.(value); +} + +function _cancelEmailPrewarm() { + const resolve = _libPrewarmResolve; + const detachPriorityListeners = _libPrewarmDetachPriorityListeners; + _libPrewarmGeneration += 1; + if (_libPrewarmDelayTimer !== null) { + clearTimeout(_libPrewarmDelayTimer); + } + if (_libPrewarmIdleHandle !== null && typeof window.cancelIdleCallback === 'function') { + try { window.cancelIdleCallback(_libPrewarmIdleHandle); } catch (_) {} + } + try { _libPrewarmAbortController?.abort(); } catch (_) {} + _libPrewarmDelayTimer = null; + _libPrewarmIdleHandle = null; + _libPrewarmPromise = null; + _libPrewarmResolve = null; + _libPrewarmAbortController = null; + _libPrewarmDetachPriorityListeners = null; + detachPriorityListeners?.(); + resolve?.(false); +} + +function _scheduleEmailPrewarm(task, { delay = 0 } = {}) { + if (_libPrewarmPromise) return _libPrewarmPromise; + // Do not disguise a timer as idle work. Browsers without the genuine idle + // callback simply skip this optional optimization and load on demand. + if (typeof window.requestIdleCallback !== 'function') return Promise.resolve(false); + + const generation = ++_libPrewarmGeneration; + _libPrewarmPromise = new Promise(resolve => { _libPrewarmResolve = resolve; }); + const promise = _libPrewarmPromise; + let attemptPending = false; + let retryRequested = false; + + function clearScheduledAttempt() { + if (_libPrewarmDelayTimer !== null) clearTimeout(_libPrewarmDelayTimer); + if (_libPrewarmIdleHandle !== null && typeof window.cancelIdleCallback === 'function') { + try { window.cancelIdleCallback(_libPrewarmIdleHandle); } catch (_) {} + } + _libPrewarmDelayTimer = null; + _libPrewarmIdleHandle = null; + } + + function scheduleIdleRetry(delay = 500) { + if (generation !== _libPrewarmGeneration) return; + retryRequested = true; + if (attemptPending || _libPrewarmDelayTimer !== null || _libPrewarmIdleHandle !== null) return; + if (document.visibilityState && document.visibilityState !== 'visible') return; + _libPrewarmDelayTimer = setTimeout(requestIdle, Math.max(50, Number(delay) || 500)); + } + + function handlePriorityChange() { + if (generation !== _libPrewarmGeneration) return; + if (_canRunEmailPrewarm()) { + scheduleIdleRetry(50); return; } - _loadEmails(options); + + const priorityBlocked = _isChatInteractionBusy() + || (document.visibilityState && document.visibilityState !== 'visible'); + if (!priorityBlocked) return; + + retryRequested = true; + clearScheduledAttempt(); + const controller = _libPrewarmAbortController; + _libPrewarmAbortController = null; + try { controller?.abort(); } catch (_) {} + // A hidden page waits for visibilitychange. Chat priority also retains the + // timer fallback for busy-until windows whose final transition has no event. + if (!document.visibilityState || document.visibilityState === 'visible') { + scheduleIdleRetry(); + } + } + + window.addEventListener('odysseus:chat-busy-change', handlePriorityChange); + document.addEventListener('visibilitychange', handlePriorityChange); + _libPrewarmDetachPriorityListeners = () => { + window.removeEventListener('odysseus:chat-busy-change', handlePriorityChange); + document.removeEventListener('visibilitychange', handlePriorityChange); }; - setTimeout(run, Math.max(0, Number(delay) || 0)); + + function requestIdle() { + if (generation !== _libPrewarmGeneration) return; + _libPrewarmDelayTimer = null; + try { + _libPrewarmIdleHandle = window.requestIdleCallback((deadline) => { + if (generation !== _libPrewarmGeneration) return; + _libPrewarmIdleHandle = null; + const hasIdleBudget = Boolean( + deadline + && !deadline.didTimeout + && typeof deadline.timeRemaining === 'function' + && deadline.timeRemaining() > 0 + ); + if (!_canRunEmailPrewarm()) { + if (_isEmailPrewarmTemporarilyBlocked()) { + scheduleIdleRetry(); + } else { + _settleEmailPrewarm(generation, false); + } + return; + } + if (!hasIdleBudget) { + scheduleIdleRetry(); + return; + } + if (generation !== _libPrewarmGeneration) { + _settleEmailPrewarm(generation, false); + return; + } + const controller = new AbortController(); + _libPrewarmAbortController = controller; + attemptPending = true; + retryRequested = false; + Promise.resolve() + .then(() => task({ signal: controller.signal, generation })) + .then(value => { + if (controller !== _libPrewarmAbortController || controller.signal.aborted) return; + _settleEmailPrewarm(generation, Boolean(value)); + }) + .catch(() => { + if (controller !== _libPrewarmAbortController || controller.signal.aborted) return; + _settleEmailPrewarm(generation, false); + }) + .finally(() => { + attemptPending = false; + if (generation !== _libPrewarmGeneration) return; + if (retryRequested) scheduleIdleRetry(); + }); + }); + } catch (_) { + _settleEmailPrewarm(generation, false); + } + } + + const wait = Math.max(0, Number(delay) || 0); + if (wait > 0) _libPrewarmDelayTimer = setTimeout(requestIdle, wait); + else requestIdle(); + return promise; } export function prewarmEmailLibrary({ delay = 2500 } = {}) { - if (_libPrewarmTimer || _libPrewarmPromise) return; + if (_libPrewarmPromise) return _libPrewarmPromise; const elapsed = Date.now() - _libLastPrewarmAt; - if (elapsed >= 0 && elapsed < 5 * 60 * 1000) return; - _libPrewarmTimer = setTimeout(() => { - _libPrewarmTimer = null; - _libPrewarmPromise = _prewarmEmailViews() - .catch(() => {}) - .finally(() => { _libPrewarmPromise = null; }); - }, Math.max(0, Number(delay) || 0)); + if (elapsed >= 0 && elapsed < _LIB_PREWARM_COOLDOWN_MS) return Promise.resolve(false); + return _scheduleEmailPrewarm(_prewarmEmailViews, { delay }); } -async function _ensureEmailAccountsForPrewarm() { +function _chooseEmailPrewarmAccountId(accounts) { + const enabled = Array.isArray(accounts) ? accounts.filter(a => a && a.enabled !== false) : []; + const remembered = _rememberedEmailAccountId(); + const current = String(state._libAccountId || '').trim(); + const chosen = enabled.find(a => String(a.id || '') === remembered) + || enabled.find(a => String(a.id || '') === current) + || enabled.find(a => a.is_default) + || enabled[0] + || null; + return String(chosen?.id || '').trim(); +} + +async function _ensureEmailAccountsForPrewarm({ signal, generation } = {}) { + if (!_isEmailPrewarmCurrent(generation, signal)) return null; const accountsFresh = _libAccountsLoadedAt && (Date.now() - _libAccountsLoadedAt) < _LIB_ACCOUNTS_TTL_MS; - if (Array.isArray(state._libAccounts) && state._libAccounts.length && accountsFresh) { - if (!state._libAccountId) { - const def = state._libAccounts.find(a => a.is_default) || state._libAccounts[0]; - state._libAccountId = def?.id || null; - _publishActiveAccount(); - } - return; - } - try { - const accountsRes = await fetch(`${API_BASE}/api/email/accounts`, { credentials: 'same-origin' }); - if (!accountsRes.ok) return; - const accountsData = await accountsRes.json().catch(() => ({})); - if (Array.isArray(accountsData.accounts)) { - state._libAccounts = accountsData.accounts; - _libAccountsLoadedAt = Date.now(); - if (!state._libAccountId && state._libAccounts.length) { - const def = state._libAccounts.find(a => a.is_default) || state._libAccounts[0]; - state._libAccountId = def?.id || null; - _publishActiveAccount(); + if (!(Array.isArray(state._libAccounts) && state._libAccounts.length && accountsFresh)) { + try { + const accountsRes = await fetch(`${API_BASE}/api/email/accounts`, { + credentials: 'same-origin', + signal, + }); + if (!_isEmailPrewarmCurrent(generation, signal)) return null; + if (accountsRes.ok) { + const accountsData = await accountsRes.json().catch(() => ({})); + if (!_isEmailPrewarmCurrent(generation, signal)) return null; + if (Array.isArray(accountsData.accounts)) { + state._libAccounts = accountsData.accounts; + _libAccountsLoadedAt = Date.now(); + } } + } catch (err) { + if (err?.name === 'AbortError') return null; } - } catch (_) {} + } + + const accountId = _chooseEmailPrewarmAccountId(state._libAccounts); + if (!_isEmailPrewarmCurrent(generation, signal)) return null; + if (!accountId) return null; + if (accountId && state._libAccountId !== accountId) { + state._libAccountId = accountId; + _publishActiveAccount(); + } + return accountId; } -export async function prewarmUnreadEmails({ limit = 8, maxUid = 0 } = {}) { - if (state._libOpen) return; - await _ensureEmailAccountsForPrewarm(); - if (state._libOpen) return; - const accountId = state._libAccountId || ''; +export function prewarmUnreadEmails({ limit = 8, maxUid = 0 } = {}) { + return _scheduleEmailPrewarm( + context => _prewarmUnreadEmailsNow({ limit, maxUid }, context), + { delay: 0 } + ); +} + +async function _prewarmUnreadEmailsNow({ limit = 8, maxUid = 0 } = {}, { signal, generation } = {}) { + if (!_isEmailPrewarmCurrent(generation, signal)) return false; + const accountId = await _ensureEmailAccountsForPrewarm({ signal, generation }); + if (accountId === null || !_isEmailPrewarmCurrent(generation, signal)) return false; const n = Math.max(1, Math.min(20, Number(limit) || 8)); const key = `${accountId}|${maxUid || 0}|${n}`; - if (_libUnreadPrewarmKey === key && (Date.now() - _libUnreadPrewarmAt) < 60 * 1000) return; - _libUnreadPrewarmKey = key; - _libUnreadPrewarmAt = Date.now(); + if (_libUnreadPrewarmKey === key && (Date.now() - _libUnreadPrewarmAt) < 60 * 1000) return true; try { const folder = 'INBOX'; - const res = await fetch(emailApiUrl('/api/email/list', { - folder, - limit: n, - offset: 0, - filter: 'unread', - account_id: accountId || undefined, - }), { credentials: 'same-origin' }); - if (state._libOpen) return; - if (!res.ok) return; + const res = await fetch(emailApiUrl('/api/email/list', { + folder, + limit: n, + offset: 0, + filter: 'unread', + account_id: accountId || undefined, + }), { + credentials: 'same-origin', + signal, + }); + if (!_isEmailPrewarmCurrent(generation, signal) || !res.ok) return false; const data = await res.json().catch(() => null); - if (!data || data.error || !Array.isArray(data.emails) || !data.emails.length) return; + if (!_isEmailPrewarmCurrent(generation, signal)) return false; + if (!data || data.error || !Array.isArray(data.emails) || !data.emails.length) return false; const sync = data.sync || {}; _libCachePut(_libCacheKeyFor(accountId, folder, 'unread', false), { emails: data.emails, total: data.total || data.emails.length, sync, }); - } catch (_) {} + _libUnreadPrewarmKey = key; + _libUnreadPrewarmAt = Date.now(); + return true; + } catch (_) { + return false; + } } -function _sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -async function _prewarmEmailViews() { - if (state._libOpen) return; - _libLastPrewarmAt = Date.now(); +async function _prewarmEmailViews({ signal, generation } = {}) { + if (!_isEmailPrewarmCurrent(generation, signal)) return false; _setEmailSyncStatus({ warming: true }); const folder = 'INBOX'; const filter = 'all'; - - // The accounts request is cheap and warms the account strip for first open. - // Then folder/list requests warm both the client cache and the backend - // IMAP/read caches. Failure stays silent: no configured mail should not nag. try { - const accountsRes = await fetch(`${API_BASE}/api/email/accounts`, { credentials: 'same-origin' }); - if (accountsRes.ok) { - const accountsData = await accountsRes.json().catch(() => ({})); - if (Array.isArray(accountsData.accounts)) { - state._libAccounts = accountsData.accounts; - _libAccountsLoadedAt = Date.now(); - } + const accountId = await _ensureEmailAccountsForPrewarm({ signal, generation }); + if (accountId === null || !_isEmailPrewarmCurrent(generation, signal)) return false; + const ck = _libCacheKeyFor(accountId, folder, filter, false); + if (_libCacheGet(ck)) { + _libLastPrewarmAt = Date.now(); + return true; } - } catch (_) {} - const accounts = Array.isArray(state._libAccounts) ? state._libAccounts.filter(a => a && a.enabled !== false) : []; - const preferred = state._libAccountId - || (accounts.find(a => a.is_default)?.id) - || (accounts[0]?.id) - || ''; - if (!state._libAccountId && preferred) { - state._libAccountId = preferred; - _publishActiveAccount(); - } - const orderedAccountIds = [ - preferred, - ...accounts.map(a => a.id).filter(id => id && id !== preferred), - ].filter((id, idx, arr) => arr.indexOf(id) === idx); - if (!orderedAccountIds.length) orderedAccountIds.push(''); - - try { - for (const accountId of orderedAccountIds.slice(0, 4)) { - if (state._libOpen) return; - const ck = _libCacheKeyFor(accountId, folder, filter, false); - if (_libCacheGet(ck)) continue; - await fetch(emailApiUrl('/api/email/folders', { account_id: accountId || undefined }), { credentials: 'same-origin' }).catch(() => null); - await fetch(emailApiUrl('/api/email/unread-state', { folder, account_id: accountId || undefined }), { credentials: 'same-origin' }).catch(() => null); - const res = await fetch(emailApiUrl('/api/email/list', { - folder, - limit: 100, - offset: 0, - filter, - account_id: accountId || undefined, - }), { - credentials: 'same-origin', - }); - if (res.ok) { - const data = await res.json().catch(() => null); - if (data && !data.error) { - const sync = data.sync || {}; - _libCachePut(ck, { - emails: data.emails || [], - total: data.total || 0, - sync, - }); - _setEmailSyncStatus({ - updatedAt: sync.updated_at || new Date().toISOString(), - source: sync.source || '', - warming: true, - }); - } - } - await _sleep(900); - } + // One optional first-page request only. Folder metadata, unread state, and + // other accounts remain demand-driven so startup cannot fan out into IMAP. + const res = await fetch(emailApiUrl('/api/email/list', { + folder, + limit: _LIB_INITIAL_PAGE_SIZE, + offset: 0, + filter, + account_id: accountId || undefined, + }), { + credentials: 'same-origin', + signal, + }); + if (!_isEmailPrewarmCurrent(generation, signal) || !res.ok) return false; + const data = await res.json().catch(() => null); + if (!_isEmailPrewarmCurrent(generation, signal)) return false; + if (!data || data.error || !Array.isArray(data.emails)) return false; + const sync = data.sync || {}; + _libCachePut(ck, { + emails: data.emails, + total: data.total || 0, + sync, + }); + _libLastPrewarmAt = Date.now(); + _setEmailSyncStatus({ + updatedAt: sync.updated_at || new Date().toISOString(), + source: sync.source || '', + warming: true, + }); + return true; + } catch (_) { + return false; } finally { _setEmailSyncStatus({ warming: false }); } @@ -2342,10 +2506,10 @@ export function initEmailLibrary(config) { export function isOpen() { return state._libOpen; } export function openEmailLibrary(opts = {}) { - if (_libPrewarmTimer) { - clearTimeout(_libPrewarmTimer); - _libPrewarmTimer = null; - } + // Foreground email always wins: cancel a delayed/idle callback and abort the + // one optional request if it has already started. Generation checks make a + // non-abortable response harmless if it races this transition. + _cancelEmailPrewarm(); // Force-clean any stale state from previous attempts const existing = document.getElementById('email-lib-modal'); if (existing) existing.remove(); @@ -2977,7 +3141,7 @@ export function openEmailLibrary(opts = {}) { } const fastAccountAtOpen = state._libAccountId || ''; if (fastAccountAtOpen) { - _loadEmailsWhenChatIdle({ delay: 0 }); + _loadEmails({ useCache: true }); } // If we already know the previous/default account, paint that inbox first // from the durable index and validate accounts in parallel. Cold refreshes @@ -2987,7 +3151,7 @@ export function openEmailLibrary(opts = {}) { _loadFolders(); _loadEmailReminderBellVisibility(); if (!fastAccountAtOpen || fastAccountAtOpen !== (state._libAccountId || '')) { - _loadEmailsWhenChatIdle(); + _loadEmails({ useCache: true }); } })(); } @@ -3172,6 +3336,7 @@ export async function openEmailLibrarySettings() { } export function closeEmailLibrary() { + _cancelEmailPrewarm(); const modal = document.getElementById('email-lib-modal'); if (modal) modal.remove(); if (_libSyncTicker) { @@ -4605,7 +4770,7 @@ async function _loadEmails({ force = false, useCache = true } = {}) { const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 450); try { - const fastRes = await fetch(`${API_BASE}/api/email/list?folder=${encodeURIComponent(folderAtStart)}${accountQS}&limit=100&offset=${offsetAtStart}&filter=${filterAtStart}${attQS}&cached_only=1`, { + const fastRes = await fetch(`${API_BASE}/api/email/list?folder=${encodeURIComponent(folderAtStart)}${accountQS}&limit=${_LIB_INITIAL_PAGE_SIZE}&offset=${offsetAtStart}&filter=${filterAtStart}${attQS}&cached_only=1`, { signal: ctrl.signal, }); const fastData = await fastRes.json().catch(() => null); @@ -4632,7 +4797,7 @@ async function _loadEmails({ force = false, useCache = true } = {}) { // opens omit it so rapid close/reopen returns instantly; the // Refresh button passes `force: true` to add it back. const buster = force ? `&_=${Date.now()}` : ''; - const res = await fetch(`${API_BASE}/api/email/list?folder=${encodeURIComponent(folderAtStart)}${accountQS}&limit=100&offset=${offsetAtStart}&filter=${filterAtStart}${attQS}${buster}`); + const res = await fetch(`${API_BASE}/api/email/list?folder=${encodeURIComponent(folderAtStart)}${accountQS}&limit=${_LIB_INITIAL_PAGE_SIZE}&offset=${offsetAtStart}&filter=${filterAtStart}${attQS}${buster}`); const data = await res.json(); if (seq !== _libLoadSeq || accountAtStart !== (state._libAccountId || '')) return; if (data.error) throw new Error(data.error); diff --git a/tests/test_email_library_prewarm.py b/tests/test_email_library_prewarm.py new file mode 100644 index 000000000..42dee3bc6 --- /dev/null +++ b/tests/test_email_library_prewarm.py @@ -0,0 +1,414 @@ +import json +from pathlib import Path +import shutil +import subprocess + +import pytest + + +_REPO = Path(__file__).resolve().parents[1] +_EMAIL_LIBRARY = _REPO / "static" / "js" / "emailLibrary.js" + + +def _source() -> str: + return _EMAIL_LIBRARY.read_text(encoding="utf-8") + + +def _function_source(name: str) -> str: + """Return one top-level JS function using balanced braces.""" + text = _source() + markers = (f"function {name}", f"async function {name}", f"export function {name}", f"export async function {name}") + starts = [text.find(marker) for marker in markers] + starts = [start for start in starts if start >= 0] + assert starts, f"missing function {name}" + start = min(starts) + paren = text.index("(", start) + paren_depth = 0 + quote = None + escaped = False + for index in range(paren, len(text)): + char = text[index] + if quote: + if escaped: + escaped = False + elif char == "\\": + escaped = True + elif char == quote: + quote = None + continue + if char in ("'", '"', "`"): + quote = char + elif char == "(": + paren_depth += 1 + elif char == ")": + paren_depth -= 1 + if paren_depth == 0: + brace = text.index("{", index) + break + else: + raise AssertionError(f"unterminated signature {name}") + depth = 0 + quote = None + escaped = False + template_depth = 0 + for index in range(brace, len(text)): + char = text[index] + if quote: + if escaped: + escaped = False + elif char == "\\": + escaped = True + elif char == quote and template_depth == 0: + quote = None + elif quote == "`" and char == "$" and index + 1 < len(text) and text[index + 1] == "{": + template_depth += 1 + elif quote == "`" and char == "}" and template_depth: + template_depth -= 1 + continue + if char in ("'", '"', "`"): + quote = char + elif char == "{": + depth += 1 + elif char == "}": + depth -= 1 + if depth == 0: + return text[start:index + 1] + raise AssertionError(f"unterminated function {name}") + + +def _run_scheduler_scenario(scenario: str): + node = shutil.which("node") + if not node: + pytest.skip("node not on PATH") + functions = "\n".join( + _function_source(name) + for name in ( + "_isChatInteractionBusy", + "_canRunEmailPrewarm", + "_isEmailPrewarmTemporarilyBlocked", + "_settleEmailPrewarm", + "_cancelEmailPrewarm", + "_scheduleEmailPrewarm", + ) + ) + script = f""" + let now = 0; + Date.now = () => now; + const state = {{ _libOpen: false, _libLoading: false }}; + let _libSearchInFlight = false; + let _libPrewarmDelayTimer = null; + let _libPrewarmIdleHandle = null; + let _libPrewarmPromise = null; + let _libPrewarmResolve = null; + let _libPrewarmAbortController = null; + let _libPrewarmDetachPriorityListeners = null; + let _libPrewarmGeneration = 0; + let nextHandle = 1; + const timers = new Map(); + const idleCallbacks = new Map(); + let idleRequestCount = 0; + function eventTarget(target) {{ + const listeners = new Map(); + target.addEventListener = (type, callback) => {{ + if (!listeners.has(type)) listeners.set(type, new Set()); + listeners.get(type).add(callback); + }}; + target.removeEventListener = (type, callback) => listeners.get(type)?.delete(callback); + target.dispatchEvent = (event) => {{ + for (const callback of [...(listeners.get(event.type) || [])]) callback(event); + }}; + target.listenerCount = (type) => listeners.get(type)?.size || 0; + return target; + }} + const document = eventTarget({{ visibilityState: 'visible' }}); + const window = {{ + __odysseusChatBusy: false, + __odysseusChatBusyUntil: 0, + requestIdleCallback(callback) {{ + const handle = nextHandle++; + idleRequestCount += 1; + idleCallbacks.set(handle, callback); + return handle; + }}, + cancelIdleCallback(handle) {{ idleCallbacks.delete(handle); }}, + }}; + eventTarget(window); + function setTimeout(callback, delay) {{ + const handle = nextHandle++; + timers.set(handle, {{ callback, at: now + Number(delay || 0) }}); + return handle; + }} + function clearTimeout(handle) {{ timers.delete(handle); }} + async function flushMicrotasks() {{ + for (let i = 0; i < 6; i += 1) await Promise.resolve(); + }} + async function advanceTo(target) {{ + while (true) {{ + const pending = [...timers.entries()] + .filter(([, timer]) => timer.at <= target) + .sort((a, b) => a[1].at - b[1].at)[0]; + if (!pending) break; + const [handle, timer] = pending; + timers.delete(handle); + now = timer.at; + timer.callback(); + await flushMicrotasks(); + }} + now = target; + await flushMicrotasks(); + }} + async function fireNextIdle(budget = 5) {{ + const pending = idleCallbacks.entries().next().value; + if (!pending) throw new Error('no idle callback pending'); + const [handle, callback] = pending; + idleCallbacks.delete(handle); + callback({{ didTimeout: false, timeRemaining: () => budget }}); + await flushMicrotasks(); + }} + {functions} + {scenario} + """ + proc = subprocess.run( + [node, "--input-type=module"], + input=script, + capture_output=True, + text=True, + cwd=str(_REPO), + timeout=30, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout.strip()) + + +def test_prewarm_is_genuine_idle_only_and_single_flight(): + scheduler = _function_source("_scheduleEmailPrewarm") + + assert "if (_libPrewarmPromise) return _libPrewarmPromise;" in scheduler + assert "typeof window.requestIdleCallback !== 'function'" in scheduler + assert "return Promise.resolve(false);" in scheduler + assert "window.requestIdleCallback((deadline)" in scheduler + assert "!deadline.didTimeout" in scheduler + assert "deadline.timeRemaining() > 0" in scheduler + + idle_callback = scheduler.index("window.requestIdleCallback((deadline)") + assert "Promise.resolve()" in scheduler + task_start = scheduler.index("task({ signal: controller.signal, generation })") + assert idle_callback < task_start, "network work must only be reachable from the idle callback" + + +def test_temporary_chat_priority_retries_one_single_flight_until_idle(): + out = _run_scheduler_scenario(""" + window.__odysseusChatBusyUntil = 10000; + let taskCalls = 0; + const task = async () => { taskCalls += 1; return true; }; + const first = _scheduleEmailPrewarm(task, { delay: 1800 }); + const joined = _scheduleEmailPrewarm(task, { delay: 0 }); + const samePromise = first === joined; + await advanceTo(1800); + await fireNextIdle(7); + const callsWhileBusy = taskCalls; + while (now < 10300) { + await advanceTo(now + 500); + await fireNextIdle(7); + } + const result = await first; + console.log(JSON.stringify({ + result, samePromise, callsWhileBusy, taskCalls, idleRequestCount, + timers: timers.size, idleCallbacks: idleCallbacks.size, + })); + """) + assert out == { + "result": True, + "samePromise": True, + "callsWhileBusy": 0, + "taskCalls": 1, + "idleRequestCount": 18, + "timers": 0, + "idleCallbacks": 0, + } + + +def test_cancelled_prewarm_cannot_issue_a_delayed_duplicate(): + out = _run_scheduler_scenario(""" + let taskCalls = 0; + const pending = _scheduleEmailPrewarm(async () => { taskCalls += 1; return true; }, { delay: 1800 }); + await advanceTo(1400); + _cancelEmailPrewarm(); + await advanceTo(12000); + const result = await pending; + console.log(JSON.stringify({ + result, taskCalls, idleRequestCount, + timers: timers.size, idleCallbacks: idleCallbacks.size, + })); + """) + assert out == { + "result": False, + "taskCalls": 0, + "idleRequestCount": 0, + "timers": 0, + "idleCallbacks": 0, + } + + +@pytest.mark.parametrize("transition", ["busy", "hidden"]) +def test_active_prewarm_is_aborted_and_retried_once_after_priority_transition(transition): + block = ( + "window.__odysseusChatBusy = true; " + "window.dispatchEvent({ type: 'odysseus:chat-busy-change' });" + if transition == "busy" + else "document.visibilityState = 'hidden'; document.dispatchEvent({ type: 'visibilitychange' });" + ) + unblock = ( + "window.__odysseusChatBusy = false; window.__odysseusChatBusyUntil = now; " + "window.dispatchEvent({ type: 'odysseus:chat-busy-change' });" + if transition == "busy" + else "document.visibilityState = 'visible'; document.dispatchEvent({ type: 'visibilitychange' });" + ) + out = _run_scheduler_scenario(f""" + let taskCalls = 0; + let firstSignal = null; + let finishFirst; + const firstAttempt = new Promise(resolve => {{ finishFirst = resolve; }}); + const pending = _scheduleEmailPrewarm(async ({{ signal }}) => {{ + taskCalls += 1; + if (taskCalls === 1) {{ firstSignal = signal; return firstAttempt; }} + return true; + }}); + await fireNextIdle(7); + {block} + const aborted = firstSignal.aborted; + {unblock} + const callsBeforeLateResult = taskCalls; + finishFirst(true); + await flushMicrotasks(); + const stillPendingAfterLateResult = _libPrewarmPromise === pending; + await advanceTo(now + 500); + await fireNextIdle(7); + const result = await pending; + console.log(JSON.stringify({{ + result, aborted, callsBeforeLateResult, taskCalls, + stillPendingAfterLateResult, + timers: timers.size, idleCallbacks: idleCallbacks.size, + chatListeners: window.listenerCount('odysseus:chat-busy-change'), + visibilityListeners: document.listenerCount('visibilitychange'), + }})); + """) + assert out == { + "result": True, + "aborted": True, + "callsBeforeLateResult": 1, + "taskCalls": 2, + "stillPendingAfterLateResult": True, + "timers": 0, + "idleCallbacks": 0, + "chatListeners": 0, + "visibilityListeners": 0, + } + + +def test_prewarm_skips_hidden_and_foreground_work(): + guard = _function_source("_canRunEmailPrewarm") + + assert "state._libOpen" in guard + assert "state._libLoading" in guard + assert "_libSearchInFlight" in guard + assert "document.visibilityState !== 'visible'" in guard + assert "!_isChatInteractionBusy()" in guard + + +def test_prewarm_selects_only_last_used_or_default_account(): + chooser = _function_source("_chooseEmailPrewarmAccountId") + prewarm = _function_source("_prewarmEmailViews") + + assert "_rememberedEmailAccountId()" in chooser + assert "a.enabled !== false" in chooser + assert "a.is_default" in chooser + assert "enabled[0]" in chooser + + assert "for (" not in prewarm + assert "orderedAccountIds" not in prewarm + assert "slice(0, 4)" not in prewarm + assert "/api/email/folders" not in prewarm + assert "/api/email/unread-state" not in prewarm + assert prewarm.count("/api/email/list") == 1 + + +def test_prewarm_account_chooser_rejects_disabled_or_empty_authoritative_inventory(): + node = shutil.which("node") + if not node: + pytest.skip("node not on PATH") + chooser = _function_source("_chooseEmailPrewarmAccountId") + script = f""" + const state = {{ _libAccountId: 'disabled-current' }}; + function _rememberedEmailAccountId() {{ return 'disabled-remembered'; }} + {chooser} + const onlyDisabled = _chooseEmailPrewarmAccountId([ + {{ id: 'disabled-remembered', enabled: false, is_default: true }}, + {{ id: 'disabled-current', enabled: false }}, + ]); + const empty = _chooseEmailPrewarmAccountId([]); + const mixed = _chooseEmailPrewarmAccountId([ + {{ id: 'disabled-remembered', enabled: false, is_default: true }}, + {{ id: 'enabled-default', enabled: true, is_default: true }}, + ]); + console.log(JSON.stringify({{ onlyDisabled, empty, mixed }})); + """ + proc = subprocess.run( + [node, "--input-type=module"], + input=script, + capture_output=True, + text=True, + cwd=str(_REPO), + timeout=30, + ) + assert proc.returncode == 0, proc.stderr + assert json.loads(proc.stdout.strip()) == { + "onlyDisabled": "", + "empty": "", + "mixed": "enabled-default", + } + + ensure_accounts = _function_source("_ensureEmailAccountsForPrewarm") + assert "if (!accountId) return null;" in ensure_accounts + assert ensure_accounts.index("if (!accountId) return null;") < ensure_accounts.index("_publishActiveAccount();") + + +def test_prewarm_is_bounded_to_the_interactive_initial_page_size(): + text = _source() + prewarm = _function_source("_prewarmEmailViews") + + assert "const _LIB_INITIAL_PAGE_SIZE = 100;" in text + assert "limit: _LIB_INITIAL_PAGE_SIZE" in prewarm + assert text.count("limit=${_LIB_INITIAL_PAGE_SIZE}&offset=${offsetAtStart}") == 2 + assert "limit: 100" not in prewarm + + +def test_open_cancels_scheduled_or_inflight_prewarm_first(): + text = _source() + cancel = _function_source("_cancelEmailPrewarm") + open_library = _function_source("openEmailLibrary") + + assert "clearTimeout(_libPrewarmDelayTimer)" in cancel + assert "window.cancelIdleCallback(_libPrewarmIdleHandle)" in cancel + assert "_libPrewarmAbortController?.abort()" in cancel + assert "_libPrewarmGeneration += 1" in cancel + assert open_library.index("_cancelEmailPrewarm();") < open_library.index("state._libOpen = true;") + assert "_loadEmailsWhenChatIdle" not in text + assert text.count("_loadEmails({ useCache: true });") >= 2 + + +def test_close_cancels_pending_prewarm_cleanup(): + close_library = _function_source("closeEmailLibrary") + + assert close_library.index("_cancelEmailPrewarm();") < close_library.index("state._libOpen = false;") + + +def test_unread_warm_joins_the_same_idle_single_flight_gate(): + unread_entry = _function_source("prewarmUnreadEmails") + unread_work = _function_source("_prewarmUnreadEmailsNow") + + assert "_scheduleEmailPrewarm(" in unread_entry + assert "fetch(" not in unread_entry + assert "_ensureEmailAccountsForPrewarm({ signal, generation })" in unread_work + assert "signal" in unread_work + assert "Math.min(20" in unread_work From dbeed4b63f2169da5365a95ca402930bb9648455 Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:37:21 +0100 Subject: [PATCH 048/125] perf(ui): stop session loading from blocking shell (#5927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(ui): stop session loading from blocking shell * fix(startup): open routes on their own data, retire the loader for good Follow-up to review on #5927. - Route openers are now classified by the data they actually read. Only /email touches the hydrated session list (its new-chat path falls back to the most recent session's model when no default chat is set), so every other route opens as soon as module wiring completes instead of queueing behind /api/sessions. This is the deferred-route half of #5926, which the first pass left unimplemented. - index.html's 5s fallback removes the loader node again. Leaving it in the DOM indefinitely kept _shouldPreserveStartupComposer true forever on a hung /api/sessions, so the composer stopped clearing on session switch. - A missing session module settles hydration instead of leaving the sidebar on "Loading chats…" and dropping the user's route on the floor. - Startup sequencing moved to static/js/startupShell.js so it can be run by tests. The source-text assertions in test_startup_shell_session_loading.py are replaced by node-driven behavioural tests, per tests/TESTING_STANDARD.md. - Reverted the unrequested loader a11y rework, removed the duplicated inert writes (the module stops the wave interval through a callback), and moved the bootstrap row's inline styles into .session-list-bootstrap. * fix: preserve session bootstrap failure state --------- Co-authored-by: Léo <leograndcontact@gmail.com> --- static/app.js | 47 +-- static/index.html | 22 +- static/js/sessions.js | 21 +- static/js/startupShell.js | 153 +++++++++ static/style.css | 6 + tests/test_startup_session_bootstrap_js.py | 356 +++++++++++++++++++ tests/test_startup_shell_js.py | 377 +++++++++++++++++++++ 7 files changed, 957 insertions(+), 25 deletions(-) create mode 100644 static/js/startupShell.js create mode 100644 tests/test_startup_session_bootstrap_js.py create mode 100644 tests/test_startup_shell_js.py diff --git a/static/app.js b/static/app.js index 2f1e8d4bf..514d59d8b 100644 --- a/static/app.js +++ b/static/app.js @@ -15,6 +15,12 @@ import compareModule from './js/compare/index.js?v=20260723compareicon2'; import documentModule from './js/document.js?v=20260722emailfastindex1'; import searchChatModule from './js/search-chat.js'; import { makeWindowDraggable } from './js/windowDrag.js'; +import { + revealApplicationShellAfterPaint, + runDeferredRouteOpener, + deferRouteOpener, + settleSessionHydration +} from './js/startupShell.js'; import markdownModule from './js/markdown.js'; import chatRenderer from './js/chatRenderer.js?v=20260722emailfastindex1'; import sessionModule from './js/sessions.js'; @@ -1217,12 +1223,13 @@ function initializeEventListeners() { '/library': () => sessionModule && sessionModule.openLibrary && sessionModule.openLibrary(), }; const _opener = _routeOpen[urlPath]; - // Defer the opener — at this point in init, the modules whose handlers - // we trigger (#rail-new-session click handler, the email-section header - // click handler in emailInbox, sessionModule's loaded session list) are - // still being wired up further down in this same function. Stash the - // opener so it runs from sessionModule.loadSessions().finally() below. - if (_opener) window._odysseusRouteOpener = _opener; + // Defer the opener — at this point in init, the modules whose handlers we + // trigger (#rail-new-session click handler, the email-section header click + // handler in emailInbox, sessionModule) are still being wired up further + // down in this same function. startupShell decides when it can run: as soon + // as wiring completes, or — for the routes that read the session list — + // once /api/sessions has settled. + deferRouteOpener(urlPath, _opener); // Archive browser tool button const toolLibraryBtn = el('tool-library-btn'); @@ -4315,6 +4322,10 @@ function startOdysseusApp() { // Load initial data presetsModule.loadPresets(uiModule.showError); + // Core wiring is complete for this turn — reveal the shell independently of + // the session-list request. + revealApplicationShellAfterPaint(); + if (sessionModule) { sessionModule.initDependencies({ API_BASE: API_BASE, @@ -4326,21 +4337,19 @@ function startOdysseusApp() { scrollHistory: uiModule.scrollHistoryInstant }); - // Load sessions first (critical path) — remove loader when done - sessionModule.loadSessions() - .catch(e => console.warn('loadSessions error:', e)) - .finally(() => { - const loader = document.getElementById('app-loader'); - if (loader) { loader.style.opacity = '0'; setTimeout(() => loader.remove(), 300); } - // Fire any URL route opener now that sessions + module wiring are - // ready. Deferred from up top of init for exactly this reason. - if (window._odysseusRouteOpener) { - try { window._odysseusRouteOpener(); } catch (_) {} - window._odysseusRouteOpener = null; - } - }); + // sessionModule is now wired, so every route opener has the modules it + // drives. The ones that read no session data open here rather than + // queueing behind /api/sessions. + runDeferredRouteOpener(); + + // The shell is already usable at this point; session hydration is + // sidebar-local and settles on its own schedule. + settleSessionHydration(() => sessionModule.loadSessions()); } else { console.error('Session module not loaded!'); + // Nothing will hydrate. Settle immediately so the sidebar exposes the + // failure; session-dependent routes must remain unopened without data. + settleSessionHydration(null); } const runNonCriticalStartup = (fn, delay = 4000) => { diff --git a/static/index.html b/static/index.html index fea4e20ac..c73d9a890 100644 --- a/static/index.html +++ b/static/index.html @@ -248,8 +248,8 @@ }, { once: true }); })(); </script> - <link rel="stylesheet" href="/static/style.css?v=20260723tasksbulkfeedback1"> - <link rel="modulepreload" href="/static/app.js?v=20260723tasksbulkfeedback1"> + <link rel="stylesheet" href="/static/style.css?v=20260808startupshell1"> + <link rel="modulepreload" href="/static/app.js?v=20260808startupshell1"> <link rel="modulepreload" href="/static/js/chat.js?v=20260801fix1"> <link rel="modulepreload" href="/static/js/ui.js"> <link rel="modulepreload" href="/static/js/sessions.js"> @@ -286,7 +286,13 @@ if(!document.getElementById('app-loader')){clearInterval(iv);return} render(); },150); - setTimeout(function(){var l=document.getElementById('app-loader');if(l){l.style.opacity='0';setTimeout(function(){l.remove()},300)}},5000); + // startupShell.js hides the loader as soon as the shell is wired; it calls + // back here to stop the wave because this interval is owned by this script. + window.__odysseusLoaderWaveStop=function(){clearInterval(iv)}; + // Last-resort fallback for a boot that never reaches app.js at all. Must + // still REMOVE the node: sessions.js reads its presence as "startup in + // progress" and stops clearing the composer while it is around. + setTimeout(function(){var l=document.getElementById('app-loader');if(l){clearInterval(iv);l.style.opacity='0';setTimeout(function(){l.remove()},300)}},5000); })(); </script> <!-- Memory Management Modal --> @@ -813,7 +819,13 @@ <button class="session-bulk-btn" id="session-bulk-cancel" title="Cancel"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button> </div> </div> - <div id="session-list" role="listbox"></div> + <div id="session-list" role="listbox"> + <!-- Sidebar-local bootstrap state. renderSessionList() replaces the + whole list on first hydration, so this row is transient. --> + <div id="session-list-loading" class="list-item session-list-bootstrap" role="option" aria-disabled="true" aria-live="polite" aria-atomic="true"> + <span class="grow muted" data-session-list-status>Loading chats…</span> + </div> + </div> </div> <!-- Hidden dropdown for session actions --> <div id="session-actions-dropdown" class="dropdown hidden"> @@ -2531,7 +2543,7 @@ <script type="module" src="/static/js/censor.js"></script> <script type="module" src="/static/js/settings.js?v=20260723compareicon1"></script> <script type="module" src="/static/js/assistant.js"></script> -<script type="module" src="/static/app.js?v=20260723tasksbulkfeedback1"></script> <!-- app.js must be LAST --> +<script type="module" src="/static/app.js?v=20260808startupshell1"></script> <!-- app.js must be LAST --> <script type="module" src="/static/js/init.js?v=20260715freshroot3"></script> <script type="module" src="/static/js/a11y.js"></script> <script nonce="{{CSP_NONCE}}">if('serviceWorker' in navigator){navigator.serviceWorker.register('/static/sw.js').catch(()=>{});}</script> diff --git a/static/js/sessions.js b/static/js/sessions.js index edf83c8a4..1fc47a976 100644 --- a/static/js/sessions.js +++ b/static/js/sessions.js @@ -1683,8 +1683,21 @@ export async function loadSessions() { url += `?active_incognito_id=${encodeURIComponent(currentSessionId)}`; } const res = await fetch(url); + if (!res.ok) { + let detail = ''; + try { + const payload = await res.json(); + detail = payload?.detail || payload?.error || ''; + } catch (_) {} + const error = new Error(detail || `Session request failed (HTTP ${res.status})`); + error.status = res.status; + throw error; + } fetched = await res.json(); } + if (!Array.isArray(fetched)) { + throw new Error('Session request returned an invalid response'); + } sessions = _normalizeSessionsList(fetched); renderSessionList(); @@ -1807,9 +1820,15 @@ export async function loadSessions() { _autoCreateInProgress = false; } } + return true; } catch (error) { console.error('Error in loadSessions:', error); - uiModule.showError('Failed to load sessions: ' + error.message); + // app.js's global fetch wrapper owns expired-auth navigation. Avoid + // flashing a redundant session error while that 401 redirect is pending. + if (error?.status !== 401) { + uiModule.showError('Failed to load sessions: ' + error.message); + } + return false; } } diff --git a/static/js/startupShell.js b/static/js/startupShell.js new file mode 100644 index 000000000..0879ff517 --- /dev/null +++ b/static/js/startupShell.js @@ -0,0 +1,153 @@ +// Odysseus UI — startup shell sequencing +// ES6 module — no application dependencies, DOM only. +// +// Revealing the application shell, retiring the boot loader, settling the +// sidebar's own loading state, and firing a deferred URL route are separate +// startup concerns that used to sit inline in app.js behind a single promise. +// They live here so each step has one owner and so the whole contract can be +// exercised directly (tests/test_startup_shell_js.py) without booting the app. + +const LOADER_ID = 'app-loader'; +const SESSION_BOOTSTRAP_ROW_ID = 'session-list-loading'; + +// Route openers that read the hydrated session list. Everything else only +// needs module wiring and must not wait on /api/sessions. `/email` spawns a +// fresh chat, and that path falls back to the most recent session's model +// (_createDirectChatFromPreferredModel in app.js) when there is no default +// chat configured, so it genuinely needs the list. +const ROUTES_NEEDING_SESSIONS = new Set(['/email']); + +let _routeOpener = null; +let _routeOpenerNeedsSessions = false; + +function _loader() { + return document.getElementById(LOADER_ID); +} + +/** Run `fn` after the next paint has committed (two animation frames). */ +export function afterNextPaint(fn) { + requestAnimationFrame(() => requestAnimationFrame(fn)); +} + +// The loader node stays in the DOM while sessions hydrate — sidebar-layout.js +// and sessions.js both read its presence as a "still starting up" sentinel — +// but it must stop covering, announcing, and animating over a usable shell. +function _makeLoaderInert(loader) { + if (!loader || loader.dataset.shellRevealed === 'true') return; + loader.dataset.shellRevealed = 'true'; + loader.setAttribute('aria-hidden', 'true'); + loader.style.pointerEvents = 'none'; + loader.style.opacity = '0'; + // index.html's inline bootstrap animates the wave on a 150ms interval. + // Nothing of it is visible any more, so stop rendering into it. + try { window.__odysseusLoaderWaveStop?.(); } catch (_) {} +} + +/** + * Hand the shell to the user once core wiring is done. Deferred by one paint + * so the first frame lands with the app already laid out. + */ +export function revealApplicationShellAfterPaint() { + const loader = _loader(); + if (!loader || loader.dataset.shellRevealScheduled === 'true') return; + loader.dataset.shellRevealScheduled = 'true'; + afterNextPaint(() => _makeLoaderInert(_loader())); +} + +/** Retire the loader node for good. Safe to call after a reveal. */ +export function removeApplicationLoader() { + const loader = _loader(); + if (!loader) return; + _makeLoaderInert(loader); + setTimeout(() => loader.remove(), 300); +} + +/** + * Turn the sidebar's bootstrap row into a failure row. The write is delayed + * until the session renderer's frame has committed so a late success cannot + * leave stale failure text behind. + */ +export function markSessionListUnavailableIfStillBootstrapping() { + afterNextPaint(() => { + const row = document.getElementById(SESSION_BOOTSTRAP_ROW_ID); + if (!row) return; + const status = row.querySelector('[data-session-list-status]') || row; + status.textContent = 'Chats unavailable'; + }); +} + +/** True when `path`'s route opener reads the hydrated session list. */ +export function routeNeedsSessionData(path) { + return ROUTES_NEEDING_SESSIONS.has(path); +} + +/** + * Stash a URL route opener for later. At the point app.js resolves the route, + * the modules its handlers drive (the rail new-chat handler, the email + * section header handler, sessionModule) are still being wired further down + * the same init pass, so the opener cannot run inline. + */ +export function deferRouteOpener(path, opener) { + if (!opener) return; + _routeOpener = opener; + _routeOpenerNeedsSessions = routeNeedsSessionData(path); +} + +/** + * Fire the deferred route opener if its data is ready. Called once when + * wiring completes and again after authoritative session hydration; a route + * that needs no session data takes the first call, one that does takes the + * second. + * + * @returns {boolean} whether an opener ran. + */ +export function runDeferredRouteOpener({ sessionsSettled = false } = {}) { + if (!_routeOpener) return false; + if (_routeOpenerNeedsSessions && !sessionsSettled) return false; + const opener = _routeOpener; + _routeOpener = null; + _routeOpenerNeedsSessions = false; + try { opener(); } catch (e) { console.warn('route opener failed:', e); } + return true; +} + +/** + * Drive session hydration and everything that hangs off it settling: the + * sidebar's failure row, the loader node, and any session-dependent route. + * + * @param {(() => Promise<boolean>)|null} loadSessions — resolves true only + * after the session list was authoritatively loaded and applied. Null means + * the session module failed to load. + */ +export function settleSessionHydration(loadSessions) { + const settle = (succeeded) => { + if (!succeeded) { + markSessionListUnavailableIfStillBootstrapping(); + // A later unrelated caller must not be able to release a stale startup + // opener against unknown session state. + _routeOpener = null; + _routeOpenerNeedsSessions = false; + } + removeApplicationLoader(); + if (succeeded) runDeferredRouteOpener({ sessionsSettled: true }); + return succeeded; + }; + if (!loadSessions) { + return Promise.resolve(settle(false)); + } + // Kick the request off synchronously — a microtask hop here would delay the + // fetch this whole change exists to get off the critical path. + let pending; + try { + pending = loadSessions(); + } catch (e) { + console.warn('loadSessions error:', e); + return Promise.resolve(settle(false)); + } + return Promise.resolve(pending) + .then(result => settle(result === true)) + .catch(e => { + console.warn('loadSessions error:', e); + return settle(false); + }); +} diff --git a/static/style.css b/static/style.css index 73fdbcd5b..d2116bcbd 100644 --- a/static/style.css +++ b/static/style.css @@ -38015,6 +38015,12 @@ button.cal-add-btn.cal-add-btn-text.cal-add-btn-sm:hover .cal-add-label { outline-offset: 2px; border-radius: 5px; } +/* Bootstrap row shown while the session list hydrates, and on load failure. + Reads as a normal list row but is not selectable. */ +.session-list-bootstrap { + cursor: default; + pointer-events: none; +} #email-lib-grid .date-section-header { padding: 10px 5px 3px; } diff --git a/tests/test_startup_session_bootstrap_js.py b/tests/test_startup_session_bootstrap_js.py new file mode 100644 index 000000000..184a720e3 --- /dev/null +++ b/tests/test_startup_session_bootstrap_js.py @@ -0,0 +1,356 @@ +"""Exercise sessions.js and startupShell.js together at the bootstrap seam. + +The dependency-heavy session module is copied unchanged except for redirecting +its static imports to tiny browser stubs. The real loadSessions implementation +and the real startup-shell coordinator then run together under Node. +""" + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + + +_REPO = Path(__file__).resolve().parent.parent +_SESSIONS = _REPO / "static" / "js" / "sessions.js" +_SHELL_URL = (_REPO / "static" / "js" / "startupShell.js").as_uri() +_HAS_NODE = shutil.which("node") is not None + +_IMPORT_REWRITES = { + "import Storage from './storage.js';": "import Storage from './storage.mjs';", + "import uiModule, { autoResize, styledPrompt } from './ui.js';": ( + "import uiModule, { autoResize, styledPrompt } from './ui.mjs';" + ), + "import chatRenderer from './chatRenderer.js?v=20260722ctxheader1';": ( + "import chatRenderer from './chatRenderer.mjs';" + ), + "import { providerLogo } from './providers.js';": ( + "import { providerLogo } from './providers.mjs';" + ), + "import { initModelPicker, updateModelPicker } from './modelPicker.js?v=20260722ctxheader1';": ( + "import { initModelPicker, updateModelPicker } from './modelPicker.mjs';" + ), + "import themeModule from './theme.js';": "import themeModule from './theme.mjs';", + "import spinnerModule from './spinner.js';": "import spinnerModule from './spinner.mjs';", +} + +_STUBS = { + "storage.mjs": r""" +const Storage = { + get: (key, fallback = null) => localStorage.getItem(key) ?? fallback, + set: (key, value) => localStorage.setItem(key, value), + remove: (key) => localStorage.removeItem(key), + getJSON: (key, fallback) => { + try { return JSON.parse(localStorage.getItem(key) ?? JSON.stringify(fallback)); } + catch (_) { return fallback; } + }, + setJSON: (key, value) => localStorage.setItem(key, JSON.stringify(value)), +}; +export default Storage; +""", + "ui.mjs": r""" +export const autoResize = () => {}; +export const styledPrompt = async () => null; +const ui = { + el: (id) => document.getElementById(id), + showError: (message) => globalThis.__sessionErrors.push(String(message)), + showToast: () => {}, + styledConfirm: async () => true, +}; +export default ui; +""", + "chatRenderer.mjs": ( + "export default { addMessage: () => null, hideWelcomeScreen: () => {} };\n" + ), + "providers.mjs": "export const providerLogo = () => '';\n", + "modelPicker.mjs": ( + "export const initModelPicker = () => {};\n" + "export const updateModelPicker = () => {};\n" + ), + "theme.mjs": "export default {};\n", + "spinner.mjs": "export default {};\n", +} + +_HARNESS = r""" +const SESSIONS_URL = 'SESSIONS_PATH'; +const SHELL_URL = 'SHELL_PATH'; + +function makeStore() { + const values = new Map(); + return { + getItem(key) { return values.has(key) ? values.get(key) : null; }, + setItem(key, value) { values.set(key, String(value)); }, + removeItem(key) { values.delete(key); }, + }; +} + +function makeClassList() { + const values = new Set(); + return { + add(...names) { names.forEach(name => values.add(name)); }, + remove(...names) { names.forEach(name => values.delete(name)); }, + contains(name) { return values.has(name); }, + toggle(name, force) { + const enabled = force === undefined ? !values.has(name) : !!force; + if (enabled) values.add(name); else values.delete(name); + return enabled; + }, + }; +} + +function makeWorld() { + const byId = new Map(); + const frames = []; + const cancelledFrames = new Set(); + const timers = []; + let nextFrame = 1; + let historyWrites = 0; + + function makeElement(id = '') { + let html = ''; + const element = { + id, + dataset: {}, + style: {}, + classList: makeClassList(), + children: [], + status: null, + value: '', + disabled: false, + removed: false, + addEventListener() {}, + removeEventListener() {}, + setAttribute(name, value) { this[name] = value; }, + getAttribute(name) { return this[name] ?? null; }, + appendChild(child) { this.children.push(child); return child; }, + insertBefore(child) { this.children.unshift(child); return child; }, + contains() { return false; }, + closest() { return null; }, + querySelector(selector) { + if (selector === '[data-session-list-status]') return this.status; + return null; + }, + querySelectorAll() { return []; }, + focus() { document.activeElement = this; }, + remove() { this.removed = true; if (this.id) byId.delete(this.id); }, + }; + Object.defineProperty(element, 'innerHTML', { + get() { return html; }, + set(value) { + html = String(value); + if (id === 'session-list' && html === '') { + const row = byId.get('session-list-loading'); + if (row) row.remove(); + } + }, + }); + return element; + } + + const document = { + activeElement: null, + getElementById: (id) => byId.get(id) || null, + querySelector: () => null, + querySelectorAll: () => [], + createElement: (tag) => makeElement(tag), + createDocumentFragment: () => makeElement('fragment'), + addEventListener() {}, + }; + globalThis.document = document; + globalThis.localStorage = makeStore(); + globalThis.sessionStorage = makeStore(); + Object.defineProperty(globalThis, 'navigator', { + value: { platform: 'Linux' }, + configurable: true, + }); + globalThis.history = { replaceState() { historyWrites += 1; } }; + globalThis.window = { + document, + innerWidth: 1024, + innerHeight: 768, + location: { origin: 'http://odysseus.test', hash: '', pathname: '/', href: '/' }, + addEventListener() {}, + removeEventListener() {}, + chatModule: { + detachCurrentStream() {}, + showWelcomeScreen() {}, + }, + __odysseusDefaultChat: { + endpoint_url: 'http://model.test', + model: 'test/model', + endpoint_id: 'endpoint-1', + }, + }; + globalThis.location = window.location; + globalThis.requestAnimationFrame = (fn) => { + const id = nextFrame++; + frames.push({ id, fn }); + return id; + }; + globalThis.cancelAnimationFrame = (id) => cancelledFrames.add(id); + globalThis.setTimeout = (fn, ms) => { timers.push({ fn, ms }); return timers.length; }; + globalThis.clearTimeout = () => {}; + globalThis.__sessionErrors = []; + + return { + add(id, options = {}) { + const element = makeElement(id); + if (options.statusText !== undefined) { + element.status = { textContent: options.statusText }; + } + if (options.value !== undefined) element.value = options.value; + byId.set(id, element); + return element; + }, + paint(rounds = 1) { + for (let i = 0; i < rounds; i += 1) { + const due = frames.splice(0, frames.length); + for (const frame of due) { + if (!cancelledFrames.has(frame.id)) frame.fn(); + } + } + }, + runTimers() { + const due = timers.splice(0, timers.length); + for (const timer of due) timer.fn(); + }, + byId, + historyWrites: () => historyWrites, + resetHistoryWrites: () => { historyWrites = 0; }, + }; +} + +const world = makeWorld(); +world.add('session-list'); +world.add('sessions-section'); +const message = world.add('message', { value: 'draft before seed' }); + +const responses = [ + { + ok: true, + status: 200, + json: async () => [{ id: 'existing', name: 'Existing', folder: 'Assistant', archived: false }], + }, + { + ok: false, + status: 503, + json: async () => ({ detail: 'temporarily unavailable' }), + }, +]; +let fetchCount = 0; +globalThis.fetch = async () => { + fetchCount += 1; + const response = responses.shift(); + if (!response) throw new Error('unexpected fetch'); + return response; +}; + +const sessions = await import(SESSIONS_URL + '?bootstrap'); +const shell = await import(SHELL_URL + '?bootstrap'); + +const seeded = await sessions.loadSessions(); +world.paint(1); +localStorage.setItem('lastSessionId', 'existing'); +message.value = 'draft must survive'; +document.activeElement = null; +world.resetHistoryWrites(); +const loader = world.add('app-loader'); +const row = world.add('session-list-loading', { statusText: 'Loading chats…' }); +let opened = 0; +shell.deferRouteOpener('/email', () => { opened += 1; }); + +const hydrated = await shell.settleSessionHydration(() => sessions.loadSessions()); +const beforePaint = row.status.textContent; +world.paint(2); +world.runTimers(); +const staleRouteRan = shell.runDeferredRouteOpener({ sessionsSettled: true }); + +const errorsBeforeAuth = __sessionErrors.length; +globalThis.fetch = async () => { + fetchCount += 1; + const response = { ok: false, status: 401, json: async () => ({ detail: 'expired' }) }; + window.location.href = '/login'; // app.js global fetch-wrapper behaviour + return response; +}; +const authResult = await sessions.loadSessions(); + +console.log(JSON.stringify({ + seeded, + hydrated, + beforePaint, + afterPaint: row.status.textContent, + rowStillPresent: world.byId.has('session-list-loading'), + loaderRemoved: loader.removed, + opened, + staleRouteRan, + fetchCount, + sessionIds: sessions.getSessions().map(session => session.id), + pendingChat: sessions.hasPendingChat(), + draft: message.value, + lastSessionId: localStorage.getItem('lastSessionId'), + historyWrites: world.historyWrites(), + errors: __sessionErrors, + authResult, + authRedirect: window.location.href, + authAddedError: __sessionErrors.length !== errorsBeforeAuth, +})); +""" + + +@pytest.fixture(scope="module") +def results(tmp_path_factory): + if not _HAS_NODE: + pytest.skip("node is not installed") + + module_dir = tmp_path_factory.mktemp("session-bootstrap-js") + source = _SESSIONS.read_text(encoding="utf-8") + for original, replacement in _IMPORT_REWRITES.items(): + assert original in source, f"sessions import changed: {original}" + source = source.replace(original, replacement, 1) + sessions_module = module_dir / "sessions.mjs" + sessions_module.write_text(source, encoding="utf-8") + for name, stub in _STUBS.items(): + (module_dir / name).write_text(stub, encoding="utf-8") + + harness = _HARNESS.replace("SESSIONS_PATH", sessions_module.as_uri()).replace( + "SHELL_PATH", _SHELL_URL + ) + proc = subprocess.run( + ["node", "--input-type=module", "-e", harness], + capture_output=True, + text=True, + timeout=60, + ) + assert proc.returncode == 0, f"node harness failed:\n{proc.stderr}" + return json.loads(proc.stdout.strip().splitlines()[-1]) + + +def test_fulfilled_503_is_not_applied_as_an_empty_session_list(results): + assert results["seeded"] is True + assert results["hydrated"] is False + assert results["sessionIds"] == ["existing"] + assert results["pendingChat"] is False, "failure created a default direct chat" + assert results["draft"] == "draft must survive" + assert results["lastSessionId"] == "existing" + assert results["historyWrites"] == 0 + + +def test_fulfilled_503_keeps_failure_state_and_route_deferred(results): + assert results["beforePaint"] == "Loading chats…" + assert results["afterPaint"] == "Chats unavailable" + assert results["rowStillPresent"] is True + assert results["loaderRemoved"] is True + assert results["opened"] == 0 + assert results["staleRouteRan"] is False + assert results["errors"] == [ + "Failed to load sessions: temporarily unavailable", + ] + + +def test_401_keeps_global_auth_redirect_contract(results): + assert results["authResult"] is False + assert results["authRedirect"] == "/login" + assert results["authAddedError"] is False + assert results["sessionIds"] == ["existing"] diff --git a/tests/test_startup_shell_js.py b/tests/test_startup_shell_js.py new file mode 100644 index 000000000..8dcd6f90b --- /dev/null +++ b/tests/test_startup_shell_js.py @@ -0,0 +1,377 @@ +"""Pin the startup shell contract (static/js/startupShell.js). + +Driven through `node --input-type=module` against a stub DOM and a manually +pumped frame/timer clock, so the real module runs without a browser (same +approach as test_composer_arrow_up_recall_js.py). Skips when `node` is absent. + +Locks in the behaviour #5926 asks for: the shell is revealed one paint after +wiring and does not wait on /api/sessions; the loader node survives hydration +as a startup sentinel but is always retired once hydration settles; the sidebar +owns its own loading/failure row and a successful zero-session render never +shows a false failure; and a URL route opens only after the data it actually +needs is authoritatively available. +""" +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +_REPO = Path(__file__).resolve().parent.parent +_MODULE = _REPO / "static" / "js" / "startupShell.js" +_MODULE_URL = _MODULE.as_uri() +_HAS_NODE = shutil.which("node") is not None + +_HARNESS = r""" +const MODULE_URL = 'MODULE_PATH'; + +// ── Stub DOM + a clock we pump by hand ──────────────────────────────────── +function makeWorld() { + const byId = new Map(); + const frames = []; + const timers = []; + const world = { + byId, + waveStops: 0, + addElement(id, { statusText = null } = {}) { + const el = { + id, + dataset: {}, + style: {}, + attrs: {}, + removed: false, + status: null, + setAttribute(k, v) { this.attrs[k] = v; }, + getAttribute(k) { return this.attrs[k]; }, + remove() { this.removed = true; byId.delete(this.id); }, + querySelector(sel) { + return sel === '[data-session-list-status]' ? this.status : null; + }, + }; + if (statusText !== null) el.status = { textContent: statusText }; + byId.set(id, el); + return el; + }, + // One "paint" = one round of already-queued rAF callbacks. afterNextPaint + // chains two, so a committed paint takes two rounds. + paint(rounds = 1) { + for (let i = 0; i < rounds; i++) { + const due = frames.splice(0, frames.length); + for (const fn of due) fn(); + } + }, + runTimers() { + const due = timers.splice(0, timers.length); + for (const t of due) t.fn(); + }, + pendingTimers() { return timers.length; }, + }; + globalThis.document = { getElementById: (id) => byId.get(id) || null }; + globalThis.window = { __odysseusLoaderWaveStop: () => { world.waveStops += 1; } }; + globalThis.requestAnimationFrame = (fn) => { frames.push(fn); return frames.length; }; + globalThis.setTimeout = (fn, ms) => { timers.push({ fn, ms }); return timers.length; }; + return world; +} + +// Fresh module instance per case so deferred-route state cannot leak. +let _instance = 0; +async function loadModule() { + _instance += 1; + return import(MODULE_URL + '?case=' + _instance); +} + +function loaderSnapshot(loader) { + return { + revealed: loader.dataset.shellRevealed === 'true', + opacity: loader.style.opacity ?? null, + pointerEvents: loader.style.pointerEvents ?? null, + ariaHidden: loader.getAttribute('aria-hidden') ?? null, + removed: loader.removed, + }; +} + +const cases = {}; + +cases.reveal_waits_one_paint_then_keeps_node = async () => { + const w = makeWorld(); + const loader = w.addElement('app-loader'); + const shell = await loadModule(); + shell.revealApplicationShellAfterPaint(); + const beforePaint = loaderSnapshot(loader); + w.paint(1); + const afterOneFrame = loaderSnapshot(loader); + w.paint(1); + return { + beforePaint, + afterOneFrame, + afterPaint: loaderSnapshot(loader), + waveStops: w.waveStops, + stillInDocument: w.byId.has('app-loader'), + }; +}; + +cases.reveal_is_idempotent = async () => { + const w = makeWorld(); + const loader = w.addElement('app-loader'); + const shell = await loadModule(); + shell.revealApplicationShellAfterPaint(); + shell.revealApplicationShellAfterPaint(); + w.paint(2); + shell.revealApplicationShellAfterPaint(); + w.paint(2); + return { waveStops: w.waveStops, snapshot: loaderSnapshot(loader) }; +}; + +cases.remove_retires_the_loader_node = async () => { + const w = makeWorld(); + const loader = w.addElement('app-loader'); + const shell = await loadModule(); + shell.removeApplicationLoader(); + const beforeTimers = loaderSnapshot(loader); + w.runTimers(); + return { beforeTimers, afterTimers: loaderSnapshot(loader) }; +}; + +cases.failed_hydration_marks_sidebar_row = async () => { + const w = makeWorld(); + w.addElement('app-loader'); + const row = w.addElement('session-list-loading', { statusText: 'Loading chats…' }); + const shell = await loadModule(); + await shell.settleSessionHydration(() => Promise.reject(new Error('boom'))); + const beforePaint = row.status.textContent; + w.paint(2); + w.runTimers(); + return { + beforePaint, + afterPaint: row.status.textContent, + loaderRemoved: !w.byId.has('app-loader'), + }; +}; + +// A successful load with zero sessions must not schedule a failure write. +cases.zero_session_success_shows_no_failure = async () => { + const w = makeWorld(); + w.addElement('app-loader'); + const row = w.addElement('session-list-loading', { statusText: 'Loading chats…' }); + const shell = await loadModule(); + await shell.settleSessionHydration(() => Promise.resolve(true)); + w.paint(1); + row.remove(); // renderSessionList() clearing #session-list + w.paint(1); + return { statusText: row.status.textContent, rowRemoved: row.removed }; +}; + +// The whole point is getting /api/sessions off the critical path, not later. +cases.hydration_starts_synchronously = async () => { + const w = makeWorld(); + w.addElement('app-loader'); + const shell = await loadModule(); + let started = false; + const done = shell.settleSessionHydration(() => { started = true; return Promise.resolve(true); }); + const startedBeforeAwait = started; + await done; + return { startedBeforeAwait }; +}; + +cases.synchronous_load_failure_still_settles = async () => { + const w = makeWorld(); + w.addElement('app-loader'); + const row = w.addElement('session-list-loading', { statusText: 'Loading chats…' }); + const shell = await loadModule(); + let opened = 0; + shell.deferRouteOpener('/email', () => { opened += 1; }); + let threw = false; + let succeeded = true; + try { + succeeded = await shell.settleSessionHydration(() => { throw new Error('module blew up'); }); + } catch (_) { threw = true; } + w.paint(2); + w.runTimers(); + return { + threw, + succeeded, + opened, + statusText: row.status.textContent, + loaderRemoved: !w.byId.has('app-loader'), + ranAfterFailure: shell.runDeferredRouteOpener({ sessionsSettled: true }), + }; +}; + +cases.route_without_session_data_opens_before_hydration = async () => { + const w = makeWorld(); + w.addElement('app-loader'); + const shell = await loadModule(); + let opened = 0; + shell.deferRouteOpener('/notes', () => { opened += 1; }); + const ranEarly = shell.runDeferredRouteOpener(); + const openedAfterEarly = opened; + const ranAgain = shell.runDeferredRouteOpener({ sessionsSettled: true }); + return { ranEarly, openedAfterEarly, ranAgain, opened }; +}; + +cases.route_with_session_data_waits_for_hydration = async () => { + const w = makeWorld(); + w.addElement('app-loader'); + const shell = await loadModule(); + let opened = 0; + shell.deferRouteOpener('/email', () => { opened += 1; }); + const ranEarly = shell.runDeferredRouteOpener(); + const openedAfterEarly = opened; + const succeeded = await shell.settleSessionHydration(() => Promise.resolve(true)); + return { + ranEarly, + openedAfterEarly, + openedAfterHydration: opened, + succeeded, + needsSessions: [shell.routeNeedsSessionData('/email'), shell.routeNeedsSessionData('/notes')], + }; +}; + +cases.missing_session_module_keeps_route_deferred = async () => { + const w = makeWorld(); + w.addElement('app-loader'); + const row = w.addElement('session-list-loading', { statusText: 'Loading chats…' }); + const shell = await loadModule(); + let opened = 0; + shell.deferRouteOpener('/email', () => { opened += 1; }); + const succeeded = await shell.settleSessionHydration(null); + w.paint(2); + w.runTimers(); + return { + opened, + succeeded, + statusText: row.status.textContent, + loaderRemoved: !w.byId.has('app-loader'), + ranAfterFailure: shell.runDeferredRouteOpener({ sessionsSettled: true }), + }; +}; + +cases.throwing_route_opener_is_contained = async () => { + const w = makeWorld(); + w.addElement('app-loader'); + const shell = await loadModule(); + shell.deferRouteOpener('/notes', () => { throw new Error('opener blew up'); }); + let threw = false; + let ran = false; + try { ran = shell.runDeferredRouteOpener(); } catch (_) { threw = true; } + return { threw, ran, ranAgain: shell.runDeferredRouteOpener({ sessionsSettled: true }) }; +}; + +const out = {}; +for (const [name, fn] of Object.entries(cases)) out[name] = await fn(); +console.log(JSON.stringify(out)); +""".replace("MODULE_PATH", _MODULE_URL) + + +@pytest.fixture(scope="module") +def results(): + if not _HAS_NODE: + pytest.skip("node is not installed") + proc = subprocess.run( + ["node", "--input-type=module", "-e", _HARNESS], + capture_output=True, + text=True, + timeout=60, + ) + assert proc.returncode == 0, f"node harness failed:\n{proc.stderr}" + return json.loads(proc.stdout.strip().splitlines()[-1]) + + +def test_module_exists(): + assert _MODULE.is_file(), f"missing {_MODULE}" + + +def test_shell_is_revealed_one_paint_after_wiring(results): + r = results["reveal_waits_one_paint_then_keeps_node"] + assert r["beforePaint"]["revealed"] is False, "revealed before any frame ran" + assert r["afterOneFrame"]["revealed"] is False, "revealed before the paint committed" + assert r["afterPaint"] == { + "revealed": True, + "opacity": "0", + "pointerEvents": "none", + "ariaHidden": "true", + "removed": False, + } + assert r["waveStops"] == 1, "loader wave interval kept running after reveal" + + +def test_revealed_loader_stays_as_startup_sentinel(results): + # sessions.js / sidebar-layout.js read #app-loader as "startup in progress". + r = results["reveal_waits_one_paint_then_keeps_node"] + assert r["stillInDocument"] is True + assert r["afterPaint"]["removed"] is False + + +def test_reveal_is_idempotent(results): + r = results["reveal_is_idempotent"] + assert r["waveStops"] == 1, "reveal ran its side effects more than once" + assert r["snapshot"]["revealed"] is True + + +def test_loader_node_is_retired_after_the_fade(results): + r = results["remove_retires_the_loader_node"] + assert r["beforeTimers"]["revealed"] is True, "removal should hide immediately" + assert r["beforeTimers"]["removed"] is False, "removal should wait for the fade" + assert r["afterTimers"]["removed"] is True, "loader node outlived hydration" + + +def test_failed_session_load_marks_the_sidebar_row(results): + r = results["failed_hydration_marks_sidebar_row"] + assert r["beforePaint"] == "Loading chats…", "failure written before the render frame" + assert r["afterPaint"] == "Chats unavailable" + assert r["loaderRemoved"] is True, "a failed load must still free the shell" + + +def test_zero_session_success_never_shows_a_failure(results): + r = results["zero_session_success_shows_no_failure"] + assert r["rowRemoved"] is True + assert r["statusText"] == "Loading chats…", "false 'Chats unavailable' on empty success" + + +def test_hydration_request_starts_synchronously(results): + r = results["hydration_starts_synchronously"] + assert r["startedBeforeAwait"] is True, "/api/sessions start was deferred a microtask" + + +def test_synchronous_load_failure_still_settles(results): + r = results["synchronous_load_failure_still_settles"] + assert r["threw"] is False, "a throwing loadSessions must not escape" + assert r["succeeded"] is False + assert r["opened"] == 0, "session-dependent route opened without session data" + assert r["ranAfterFailure"] is False, "failed startup left a stale route opener" + assert r["statusText"] == "Chats unavailable" + assert r["loaderRemoved"] is True + + +def test_route_needing_no_session_data_opens_before_hydration(results): + r = results["route_without_session_data_opens_before_hydration"] + assert r["ranEarly"] is True, "/notes waited on /api/sessions it does not read" + assert r["openedAfterEarly"] == 1 + assert r["ranAgain"] is False, "route opener fired twice" + assert r["opened"] == 1 + + +def test_route_needing_session_data_waits_for_hydration(results): + r = results["route_with_session_data_waits_for_hydration"] + assert r["ranEarly"] is False, "/email opened before the session list was there" + assert r["openedAfterEarly"] == 0 + assert r["openedAfterHydration"] == 1 + assert r["succeeded"] is True + assert r["needsSessions"] == [True, False] + + +def test_missing_session_module_still_settles_without_opening_data_route(results): + r = results["missing_session_module_keeps_route_deferred"] + assert r["succeeded"] is False + assert r["opened"] == 0, "route opened without the session module it depends on" + assert r["ranAfterFailure"] is False, "missing module left a stale route opener" + assert r["statusText"] == "Chats unavailable" + assert r["loaderRemoved"] is True + + +def test_throwing_route_opener_is_contained(results): + r = results["throwing_route_opener_is_contained"] + assert r["threw"] is False + assert r["ran"] is True + assert r["ranAgain"] is False, "a failed opener must not be retried" From d449a9d431784289f302e45b8acf031ee14bcb6b Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Mon, 10 Aug 2026 19:39:21 +0100 Subject: [PATCH 049/125] fix(history): defer full transcript hydration to model sends (#5929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(history): defer full hydration to model sends * fix(session): key hydration on real rows, fork through get_session Two regressions from the display/model-context split, both reproducible against dev. The hydration gate compared the cached transcript against the denormalized sessions.message_count column. That column drifts in normal operation — _persist_message swallows a failed insert while add_message has already appended in memory, so the next successful persist writes rows+1 — and _db_to_session re-read the same column after each reload, so the shortfall never closed. Every send, edit, delete and truncate on a warm session re-selected the whole message table: the cost this change set out to remove, relocated onto the hot path. The other direction was just as bad — a persist for an uncached session writes message_count = 0, and a stale-low counter with a partly filled cache meant no hydration at all and a silently truncated transcript for the model. sync_session_metadata now reconciles message_count against COUNT(*) on chat_messages (one indexed count inside the connection it already opens), and _db_to_session trusts the rows it just loaded. A hydrate always closes the gap, so the next read is a cache hit. fork_session read session_manager.sessions directly and never hydrated. keep_count indexes into source.history, and display pagination no longer fills that cache, so forking after a restart returned HTTP 200 with an empty conversation and no error surfaced. It goes through get_session now. _hydrate_session_history_from_db is gone with its helper: get_session is the hydration seam, and rebuilding session.history from raw rows in the display fallback overwrote the parsed multimodal content and the _db_id edit/delete keys that had just been set. Tests drive a real SessionManager over a temp DB instead of a stub that only proved the stub hydrates — both drift directions, the send path warm and cold, and a fork taken after a restart. All five fail without this change. The brittle SQL-text assertions are dropped; the page bounds are already proven by the response body. * fix(history): route pagination through canonical handler --------- Co-authored-by: Léo <leograndcontact@gmail.com> --- core/session_manager.py | 53 +- routes/history/history_routes.py | 74 +-- routes/session_routes.py | 9 - tests/test_fork_session_metadata.py | 5 + tests/test_history_db_fallback_hidden.py | 6 +- tests/test_history_display_model_hydration.py | 549 ++++++++++++++++++ 6 files changed, 614 insertions(+), 82 deletions(-) create mode 100644 tests/test_history_display_model_hydration.py diff --git a/core/session_manager.py b/core/session_manager.py index 6eb493e95..6e65226d3 100644 --- a/core/session_manager.py +++ b/core/session_manager.py @@ -194,7 +194,12 @@ class SessionManager: is_important=getattr(db_session, 'is_important', False) or False, ) - session.message_count = getattr(db_session, 'message_count', len(history)) + # The rows just loaded are the whole transcript, so they — not the + # denormalized sessions.message_count column — are the truth for this + # cached object. get_session's hydration gate compares against this + # number; seeding it from a drifted column would ask for a reload that + # can never close the gap. + session.message_count = len(history) return session # ------------------------------------------------------------------ @@ -398,30 +403,50 @@ class SessionManager: # ------------------------------------------------------------------ def get_session(self, session_id: str) -> Session: - """Get a session by ID, loading from DB if needed. + """Get a session by ID, loading complete DB history when needed. - Sessions seeded by `load_sessions` start with empty history. The - first read here hydrates them with the message rows. + Sessions seeded by ``load_sessions`` start with empty history, and a + cached session can also become partially stale. Refresh metadata first, + then hydrate whenever the cached transcript is short of the stored rows. + Model-send routes enter through this method before building context, + while paginated display history reads SQLite directly. + + The gate compares against ``sync_session_metadata``'s reconciled count + (the real ``chat_messages`` total), never the denormalized column, so a + hydrate always closes the gap and the next read is a cache hit. """ if session_id not in self.sessions: self._load_session_from_db(session_id) - else: - cached = self.sessions[session_id] - # Lazy hydrate: metadata-only entries get their messages on first read. - if not cached.history and getattr(cached, "message_count", 0) > 0: - self._load_session_from_db(session_id) # Keep model/endpoint metadata fresh. Endpoint deletion can clear the - # DB row while a session object is still cached in RAM. + # DB row while a session object is still cached in RAM. Refreshing first + # also exposes the authoritative message count before completeness is + # checked. self.sync_session_metadata(session_id) + cached = self.sessions[session_id] + cached_count = len(cached.history or []) + stored_count = int(getattr(cached, "message_count", 0) or 0) + if cached_count < stored_count: + self._load_session_from_db(session_id) + # Update last_accessed self._touch_session(session_id) return self.sessions[session_id] def sync_session_metadata(self, session_id: str) -> bool: - """Refresh non-message session fields from the DB into the cached object.""" + """Refresh non-message session fields from the DB into the cached object. + + ``message_count`` is reconciled against the real ``chat_messages`` rows + rather than copied from the denormalized ``sessions.message_count`` + column. That column drifts in normal operation — ``_persist_message`` + swallows a failed insert but ``add_message`` has already appended in + memory, so the next successful persist writes rows+1, and a persist for + an uncached session writes 0. Hydration keys off this number: a + drifted-high column would reload the whole transcript on every warm + read, and a drifted-low one would leave the model a truncated one. + """ session = self.sessions.get(session_id) if session is None: return False @@ -444,7 +469,11 @@ class SessionManager: session.archived = db_session.archived session.owner = getattr(db_session, "owner", None) session.is_important = getattr(db_session, "is_important", False) or False - session.message_count = getattr(db_session, "message_count", session.message_count) or 0 + session.message_count = ( + db.query(DbChatMessage) + .filter(DbChatMessage.session_id == session_id) + .count() + ) return True except Exception as e: logger.error(f"Error syncing session metadata {session_id}: {e}") diff --git a/routes/history/history_routes.py b/routes/history/history_routes.py index f9fa3bd5a..4a6208e33 100644 --- a/routes/history/history_routes.py +++ b/routes/history/history_routes.py @@ -137,44 +137,6 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter: entry["metadata"] = meta return entry - def _db_message_metadata(m: DbChatMessage) -> Dict[str, Any]: - meta = {} - if m.meta_data: - try: - meta = json.loads(m.meta_data) or {} - except (json.JSONDecodeError, ValueError): - meta = {} - if m.timestamp and "timestamp" not in meta: - meta["timestamp"] = m.timestamp.isoformat() + "Z" - return meta - - def _hydrate_session_history_from_db(session_id: str, rows: list[DbChatMessage]) -> None: - """Rebuild in-memory context from raw DB rows after a history load. - - The browser history endpoint can return paged/display-trimmed messages, - but the next model call reads ``session.history``. After a restart or a - stale in-memory session, selecting an old chat through the paged endpoint - used to show the transcript while the model only saw fresh context. - """ - if not rows: - return - try: - session = session_manager.get_session(session_id) - except KeyError: - return - session.history = [ - ChatMessage(role=m.role, content=m.content, metadata=_db_message_metadata(m) or None) - for m in rows - ] - session.message_count = len(session.history) - - def _session_needs_db_history_hydration(session_id: str, total: int) -> bool: - try: - session = session_manager.get_session(session_id) - except KeyError: - return False - return len(session.history or []) < int(total or 0) - @router.get("/api/history/{session_id}") async def get_session_history( request: Request, @@ -198,6 +160,8 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter: ) page_offset = int(offset) if offset is not None else max(total - page_limit, 0) page_offset = max(0, min(page_offset, total)) + # Keep display pagination page-scoped. ``get_session`` is the + # full model-context hydration seam and must not be entered here. rows = ( db.query(DbChatMessage) .filter(DbChatMessage.session_id == session_id) @@ -206,14 +170,6 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter: .limit(page_limit) .all() ) - if _session_needs_db_history_hydration(session_id, total): - full_rows = ( - db.query(DbChatMessage) - .filter(DbChatMessage.session_id == session_id) - .order_by(DbChatMessage.timestamp) - .all() - ) - _hydrate_session_history_from_db(session_id, full_rows) history_dict = [ entry for entry in (_db_history_entry(m) for m in rows) if not (entry.get("metadata") or {}).get("hidden") @@ -258,7 +214,10 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter: entry["metadata"] = msg["metadata"] history_dict.append(entry) - # Fallback: load from DB if in-memory is empty + # Fallback: load from DB if in-memory renders empty. Display only — + # get_session above is the hydration seam, so nothing here writes back + # into session.history — rebuilding it from raw rows would overwrite + # parsed multimodal content and the _db_id edit/delete keys it just set. if not history_dict: db = SessionLocal() try: @@ -268,17 +227,10 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter: .order_by(DbChatMessage.timestamp) .all() ) - db_history = [] - for m in db_messages: - db_history.append(_db_history_entry(m)) - if db_history: - # Rebuild in-memory history from the full set so hidden - # messages (e.g. compaction summaries) are kept for AI context. - _hydrate_session_history_from_db(session_id, db_messages) # Response excludes hidden messages, matching the in-memory path. history_dict = [ - m for m in db_history - if not (m.get("metadata") or {}).get("hidden") + entry for entry in (_db_history_entry(m) for m in db_messages) + if not (entry.get("metadata") or {}).get("hidden") ] except Exception as e: logger.error(f"DB fallback failed for {session_id}: {e}") @@ -645,8 +597,14 @@ def setup_history_routes(session_manager, upload_handler=None) -> APIRouter: body = await request.json() keep_count = body.get("keep_count", 0) - # Get the source session - source = session_manager.sessions.get(session_id) + # Get the source session. keep_count indexes into source.history, + # so this must go through get_session — reading the cache directly + # forks an empty transcript out of a metadata-only session after a + # restart (display pagination no longer hydrates it). + try: + source = session_manager.get_session(session_id) + except KeyError: + raise HTTPException(404, "Session not found") if not source: raise HTTPException(404, "Session not found") diff --git a/routes/session_routes.py b/routes/session_routes.py index dc29a64e4..b1d79f7fe 100644 --- a/routes/session_routes.py +++ b/routes/session_routes.py @@ -801,15 +801,6 @@ def setup_session_routes( finally: db.close() - @router.get("/history/{sid}") - def get_history(request: Request, sid: str): - _verify_session_owner(request, sid) - try: - session = session_manager.get_session(sid) - except KeyError: - raise HTTPException(404, f"Session {sid} not found") - return {"history": [msg.to_dict() for msg in session.history]} - @router.get("/session/{sid}/export") def export_session(request: Request, sid: str, fmt: str = "md", filename: str = ""): """Export conversation history as a downloadable file. diff --git a/tests/test_fork_session_metadata.py b/tests/test_fork_session_metadata.py index cd278da1d..0f1ddc8b2 100644 --- a/tests/test_fork_session_metadata.py +++ b/tests/test_fork_session_metadata.py @@ -34,6 +34,11 @@ class _FakeSessionManager: self.sessions = {"src-id": source} self.created = None + def get_session(self, session_id): + # Fork looks the source up through get_session — the hydration seam — + # so a session only present in the DB still forks a real transcript. + return self.sessions[session_id] + def create_session(self, session_id=None, name=None, endpoint_url=None, model=None, rag=False, owner=None): self.created = _FakeSession(name=name, owner=owner) diff --git a/tests/test_history_db_fallback_hidden.py b/tests/test_history_db_fallback_hidden.py index 855d68c74..4265a3be1 100644 --- a/tests/test_history_db_fallback_hidden.py +++ b/tests/test_history_db_fallback_hidden.py @@ -5,9 +5,9 @@ The in-memory branch skips messages whose metadata has ``hidden`` (e.g. compaction summaries that are kept for AI context but not shown to the user). The DB fallback (taken when the in-memory history is empty, e.g. after a restart) built the client response from every DB row with no such filter, so -hidden messages leaked to the client on DB-served sessions. The rebuilt -in-memory ``session.history`` must still keep them, though, so only the response -is filtered. +hidden messages leaked to the client on DB-served sessions. Hydration of +``session.history`` belongs to ``get_session``; this fallback only shapes the +response, so only the response is filtered. get_session_history depends on the DB, the session manager and a FastAPI request, so this pins the regression at the source level (as other route tests diff --git a/tests/test_history_display_model_hydration.py b/tests/test_history_display_model_hydration.py new file mode 100644 index 000000000..b2bf95b7f --- /dev/null +++ b/tests/test_history_display_model_hydration.py @@ -0,0 +1,549 @@ +"""Display pagination must stay separate from full model-context hydration.""" + +import json +from datetime import datetime, timedelta + +import pytest +from fastapi import APIRouter, FastAPI +from fastapi.testclient import TestClient +from starlette.requests import Request +from sqlalchemy import create_engine, event +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import StaticPool + +from core.database import Base, ChatMessage as DbChatMessage, Session as DbSession +from core.models import ChatMessage, Session +from core.session_manager import SessionManager +from routes import chat_routes +from routes.history import history_routes +from routes import session_routes +from src.request_models import ChatRequest + + +def _database(): + engine = create_engine( + "sqlite://", + connect_args={"check_same_thread": False}, + poolclass=StaticPool, + ) + Base.metadata.create_all( + engine, + tables=[DbSession.__table__, DbChatMessage.__table__], + ) + return engine, sessionmaker(bind=engine, autocommit=False, autoflush=False) + + +def _seed_session(db_factory, *, session_id="session-1", message_count=6, stored_count=None): + """Seed `message_count` real rows; `stored_count` overrides the denormalized + sessions.message_count column so drift can be reproduced.""" + db = db_factory() + try: + db.add( + DbSession( + id=session_id, + name="Long chat", + endpoint_url="http://model.test/v1", + model="test-model", + owner="alice", + message_count=message_count if stored_count is None else stored_count, + ) + ) + start = datetime(2026, 1, 1, 12, 0, 0) + for index in range(message_count): + db.add( + DbChatMessage( + id=f"message-{index}", + session_id=session_id, + role="user" if index % 2 == 0 else "assistant", + content=f"content-{index}", + timestamp=start + timedelta(seconds=index), + ) + ) + db.commit() + finally: + db.close() + + +def _chat_message_selects(statements): + return [ + " ".join(statement.lower().split()) + for statement in statements + if statement.lstrip().lower().startswith("select") + and "chat_messages" in statement.lower() + ] + + +def _manager(db_factory, monkeypatch, sessions=None): + """A real SessionManager bound to the temp DB, with load counting.""" + monkeypatch.setattr("core.session_manager.SessionLocal", db_factory) + manager = object.__new__(SessionManager) + manager.upload_handler = None + manager.sessions = sessions if sessions is not None else {} + manager.full_loads = 0 + + original_load = manager._load_session_from_db + + def counting_load(session_id): + manager.full_loads += 1 + return original_load(session_id) + + manager._load_session_from_db = counting_load + return manager + + +def test_paginated_history_reads_only_count_and_requested_page(monkeypatch): + engine, db_factory = _database() + _seed_session(db_factory) + + class DisplayOnlyManager: + def get_session(self, _session_id): + raise AssertionError("paginated display history must not hydrate model context") + + monkeypatch.setattr(history_routes, "SessionLocal", db_factory) + monkeypatch.setattr(history_routes, "_verify_session_owner", lambda *_args: None) + + app = FastAPI() + app.include_router(history_routes.setup_history_routes(DisplayOnlyManager())) + + statements = [] + + def capture_sql(_conn, _cursor, statement, _parameters, _context, _executemany): + statements.append(statement) + + event.listen(engine, "before_cursor_execute", capture_sql) + try: + response = TestClient(app).get("/api/history/session-1?limit=2") + finally: + event.remove(engine, "before_cursor_execute", capture_sql) + engine.dispose() + + assert response.status_code == 200 + payload = response.json() + assert [message["content"] for message in payload["history"]] == [ + "content-4", + "content-5", + ] + assert payload["total"] == 6 + assert payload["offset"] == 4 + assert payload["has_more_before"] is True + assert payload["has_more_after"] is False + + # One COUNT for the total plus one page read — never a full-transcript + # select. The page bounds are asserted through the response above rather + # than by matching SQL text. + chat_selects = _chat_message_selects(statements) + assert len(chat_selects) == 2, chat_selects + assert sum("count(" in statement for statement in chat_selects) == 1 + + +def test_production_router_order_reaches_bounded_canonical_history(monkeypatch): + """The assembled app must not shadow canonical history with session routes.""" + engine, db_factory = _database() + _seed_session(db_factory, message_count=1200) + + class DisplayOnlyManager: + def get_session(self, _session_id): + raise AssertionError("bounded initial history must not hydrate all messages") + + manager = DisplayOnlyManager() + monkeypatch.setattr( + session_routes, + "router", + APIRouter(prefix="/api", tags=["sessions"]), + ) + monkeypatch.setattr(history_routes, "SessionLocal", db_factory) + monkeypatch.setattr(history_routes, "_verify_session_owner", lambda *_args: None) + + app = FastAPI() + app.include_router(session_routes.setup_session_routes(manager, {})) + app.include_router(history_routes.setup_history_routes(manager)) + + try: + response = TestClient(app).get("/api/history/session-1?limit=24") + finally: + engine.dispose() + + assert response.status_code == 200 + assert response.request.url.params["limit"] == "24" + payload = response.json() + displayed = len(payload["history"]) + assert 0 < displayed <= payload["limit"] <= 100 + assert payload["total"] >= 1200 + assert payload["has_more_before"] is True + assert displayed < payload["total"] + + +def test_incomplete_cached_history_hydrates_once_for_model_context(monkeypatch): + engine, db_factory = _database() + raw_multimodal = json.dumps( + [ + {"type": "text", "text": "look at the source image"}, + { + "type": "image_url", + "image_url": {"url": "data:image/png;base64,AAAA"}, + }, + ] + ) + db = db_factory() + try: + db.add( + DbSession( + id="session-1", + name="Long chat", + endpoint_url="http://model.test/v1", + model="test-model", + owner="alice", + message_count=3, + ) + ) + start = datetime(2026, 1, 1, 12, 0, 0) + db.add_all( + [ + DbChatMessage( + id="message-0", + session_id="session-1", + role="user", + content=raw_multimodal, + meta_data=json.dumps( + { + "attachments": [ + { + "id": "upload-1", + "filename": "source.png", + "content_type": "image/png", + } + ] + } + ), + timestamp=start, + ), + DbChatMessage( + id="message-1", + session_id="session-1", + role="assistant", + content="answer", + timestamp=start + timedelta(seconds=1), + ), + DbChatMessage( + id="message-2", + session_id="session-1", + role="system", + content="compaction summary", + meta_data=json.dumps({"hidden": True}), + timestamp=start + timedelta(seconds=2), + ), + ] + ) + db.commit() + finally: + db.close() + + manager = _manager( + db_factory, + monkeypatch, + sessions={ + "session-1": Session( + id="session-1", + name="Long chat", + endpoint_url="http://model.test/v1", + model="test-model", + owner="alice", + history=[ChatMessage("user", "stale partial cache")], + # Deliberately stale too: get_session must refresh metadata before + # checking whether the cached transcript is complete. + message_count=1, + ) + }, + ) + + try: + hydrated = manager.get_session("session-1") + first_full_loads = manager.full_loads + warm = manager.get_session("session-1") + second_full_loads = manager.full_loads + finally: + engine.dispose() + + assert hydrated is warm + assert len(hydrated.history) == 3 + assert first_full_loads == 1 + assert second_full_loads == first_full_loads + + context = hydrated.get_context_messages() + assert len(context) == 3 + assert context[0]["content"][1]["image_url"]["url"] == "data:image/png;base64,AAAA" + assert context[0]["metadata"]["attachments"] == [ + { + "id": "upload-1", + "filename": "source.png", + "content_type": "image/png", + } + ] + hidden_summary = next(message for message in context if message["role"] == "system") + assert hidden_summary["content"] == "compaction summary" + assert hidden_summary["metadata"]["hidden"] is True + + +def test_inflated_message_count_column_does_not_reload_warm_sessions(monkeypatch): + """A drifted-high sessions.message_count must not reload on every read. + + `_persist_message` swallows a failed insert while `add_message` has already + appended in memory, so the next successful persist writes rows+1. Keyed on + that column, the hydration gate would stay true forever and re-select the + whole transcript on every send, edit, delete and truncate. + """ + engine, db_factory = _database() + _seed_session(db_factory, message_count=6, stored_count=8) + + manager = _manager(db_factory, monkeypatch) + try: + session = manager.get_session("session-1") + cold_loads = manager.full_loads + for _ in range(3): + manager.get_session("session-1") + finally: + engine.dispose() + + assert len(session.history) == 6 + assert cold_loads == 1 + assert manager.full_loads == 1 + + +def test_stale_low_message_count_column_still_hydrates_for_the_model(monkeypatch): + """The other drift direction must not hand the model a truncated transcript. + + `_persist_message` writes message_count = 0 when the session is not cached. + A partly-filled cache plus that stale-low column previously left the send + path with whatever RAM happened to hold. + """ + engine, db_factory = _database() + _seed_session(db_factory, message_count=6, stored_count=0) + + manager = _manager( + db_factory, + monkeypatch, + sessions={ + "session-1": Session( + id="session-1", + name="Long chat", + endpoint_url="http://model.test/v1", + model="test-model", + owner="alice", + history=[ChatMessage("user", "content-0")], + message_count=0, + ) + }, + ) + try: + session = manager.get_session("session-1") + manager.get_session("session-1") + finally: + engine.dispose() + + assert [message.content for message in session.history] == [ + f"content-{index}" for index in range(6) + ] + assert manager.full_loads == 1 + + +def test_fork_after_restart_copies_the_real_transcript(monkeypatch): + """Forking reads source.history, so it must hydrate through get_session. + + Display pagination no longer fills the cache, so a fork taken after a + restart used to return HTTP 200 with an empty conversation. + """ + engine, db_factory = _database() + _seed_session(db_factory, message_count=6) + + # Restart state: metadata-only cache entry, exactly what load_sessions seeds. + manager = _manager(db_factory, monkeypatch) + manager.load_sessions() + + monkeypatch.setattr(history_routes, "SessionLocal", db_factory) + monkeypatch.setattr(history_routes, "_verify_session_owner", lambda *_args: None) + monkeypatch.setattr("core.models._SESSION_MANAGER_INSTANCE", manager) + + app = FastAPI() + app.include_router(history_routes.setup_history_routes(manager)) + client = TestClient(app) + + try: + page = client.get("/api/history/session-1?limit=2") + assert page.status_code == 200 + assert len(manager.sessions["session-1"].history) == 0 + + response = client.post("/api/session/session-1/fork", json={"keep_count": 4}) + assert response.status_code == 200 + payload = response.json() + assert payload["kept"] == 4 + + forked = manager.get_session(payload["id"]) + assert [message.content for message in forked.history] == [ + f"content-{index}" for index in range(4) + ] + finally: + engine.dispose() + + +class _ContextBuildReached(Exception): + pass + + +class _ToolPolicy: + block_all_tool_calls = False + + def blocks(self, _tool_name): + return False + + +class _ChatHandler: + async def handle_memory_command(self, _session, _message): + return None + + +def _json_request(path, payload): + raw = json.dumps(payload).encode() + sent = False + + async def receive(): + nonlocal sent + if sent: + return {"type": "http.request", "body": b"", "more_body": False} + sent = True + return {"type": "http.request", "body": raw, "more_body": False} + + scope = { + "type": "http", + "asgi": {"version": "3.0"}, + "http_version": "1.1", + "method": "POST", + "scheme": "http", + "path": path, + "raw_path": path.encode(), + "root_path": "", + "query_string": b"", + "headers": [(b"content-type", b"application/json")], + "client": ("127.0.0.1", 1234), + "server": ("testserver", 80), + } + return Request(scope, receive) + + +def _route_endpoint(router, path): + return next(route.endpoint for route in router.routes if route.path == path) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("path", ["/api/chat", "/api/chat_stream"]) +async def test_model_send_routes_hydrate_before_context_build(monkeypatch, path): + # A real SessionManager over a real (temp) DB — a stub here would only + # assert that the stub hydrates, not that SessionManager does. + engine, db_factory = _database() + _seed_session(db_factory, message_count=6, stored_count=8) + manager = _manager(db_factory, monkeypatch) + manager.load_sessions() # restart state: metadata only, no messages cached + contexts_built = [] + + async def assert_complete_context(session, *_args, **_kwargs): + contexts_built.append(session) + assert [message.content for message in session.history] == [ + f"content-{index}" for index in range(6) + ] + raise _ContextBuildReached + + monkeypatch.setattr(chat_routes, "_set_user_time_from_request", lambda *_args: None) + monkeypatch.setattr(chat_routes, "_verify_session_owner", lambda *_args: None) + monkeypatch.setattr(chat_routes, "effective_user", lambda *_args: "alice") + monkeypatch.setattr( + chat_routes, + "_clear_orphaned_session_endpoint", + lambda *_args, **_kwargs: False, + ) + monkeypatch.setattr( + chat_routes, + "_recover_empty_session_model", + lambda *_args, **_kwargs: False, + ) + monkeypatch.setattr(chat_routes, "_enforce_chat_privileges", lambda *_args: None) + monkeypatch.setattr( + chat_routes, + "build_effective_tool_policy", + lambda **_kwargs: _ToolPolicy(), + ) + monkeypatch.setattr(chat_routes, "build_chat_context", assert_complete_context) + monkeypatch.setattr( + chat_routes, + "_resolve_request_workspace", + lambda *_args: (None, False), + ) + monkeypatch.setattr(chat_routes, "_classify_tool_intent", lambda *_args: None) + monkeypatch.setattr( + chat_routes, + "_is_contextual_web_followup", + lambda *_args: False, + ) + monkeypatch.setattr( + chat_routes, + "_is_contextual_browser_followup", + lambda *_args: False, + ) + monkeypatch.setattr( + chat_routes, + "_resolve_workspace_from_message_path", + lambda *_args: (None, None), + ) + monkeypatch.setattr( + chat_routes, + "_reconcile_selected_route_from_request", + lambda *_args, **_kwargs: False, + ) + monkeypatch.setattr(chat_routes, "resolve_session_auth", lambda *_args, **_kwargs: None) + monkeypatch.setattr(chat_routes, "get_session_mode", lambda *_args: "chat") + monkeypatch.setattr( + chat_routes, + "_is_image_generation_session", + lambda *_args, **_kwargs: False, + ) + monkeypatch.setattr(chat_routes, "web_search_enabled_for_turn", lambda *_args: False) + + router = chat_routes.setup_chat_routes( + manager, + _ChatHandler(), + object(), + object(), + object(), + object(), + ) + endpoint = _route_endpoint(router, path) + + async def send(): + if path == "/api/chat": + await endpoint( + _json_request(path, {}), + ChatRequest(message="hello", session="session-1"), + ) + else: + await endpoint( + _json_request( + path, + {"message": "hello", "session": "session-1"}, + ) + ) + + try: + with pytest.raises(_ContextBuildReached): + await send() + first_loads = manager.full_loads + + # Second send on the now-warm session: the transcript is complete, so + # it must be served from RAM even though sessions.message_count is + # still drifted high in the DB. + with pytest.raises(_ContextBuildReached): + await send() + finally: + engine.dispose() + + assert len(contexts_built) == 2 + assert contexts_built[0] is contexts_built[1] + assert first_loads == 1 + assert manager.full_loads == 1 From 651bf714de4ced0cdeb28b9f1820d35037be9c2a Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:11:48 +0100 Subject: [PATCH 050/125] perf(chat): batch live thinking rendering and bound timer updates (#5931) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * perf(chat): batch live thinking DOM updates * test(chat): cover live thinking scheduler lifecycle * fix(chat): guard background stop-state, restore live thinking text, drop source-text tests - _closeOpenThinkingMarkup no longer overwrites currentAccumulated for backgrounded streams. It now mirrors the guard the delta path already uses (`if (!_isBg) currentAccumulated = accumulated`). Without it a backgrounded stream's text is written into the foreground session's stop-state, which abortCurrentRequest and detachCurrentStream then put in the wrong bubble. - Split _extractLiveThinkingText into _liveThinkingText (strip every think tag) and _closedThinkingText (via extractThinkingBlocks). Slicing from the first <think> to the first </think> pinned the live box to "The" for the rest of the stream on the `<think>The</think>` + untagged-thinking pattern that the hasUnclosedThink detection deliberately keeps streaming through. - The background transition now flushes with rich:true, so a stream that backgrounds mid-thinking isn't left as pre-wrap plain text permanently. - Move the throttle to static/js/liveThinkingThrottle.js and import it. The .mjs suite imports the module instead of slicing it out of chat.js with vm.runInNewContext and marker comments. - Replace the source-text assertions in tests/test_live_thinking_scheduler_js.py with behavioral coverage, per tests/TESTING_STANDARD.md. The .mjs suite grows from 3 to 6 cases. - Collapse the duplicated tool_start/agent_step finalizers into one _endLiveThinkingSection(). * fix(chat): hoist thinking teardown out of the try block so catch can reach it In an ES module a function declared inside `try { }` is scoped to that block, and `catch` is a sibling scope rather than a nested one. _closeOpenThinkingMarkup was declared inside the try and called from catch, so the call threw ReferenceError and killed the rest of the error path: the stream never finalized and the thinking block was never torn down. Declare _closeOpenThinkingMarkup and a new _endThinkingOnTerminalPath next to the existing _flushLiveThinking / _cancelLiveThinkingWork outer lets and assign them inside the try, which is the pattern those two already use for exactly this reason. Verified against a live stream in a browser: before, clicking stop mid-thinking logged "_closeOpenThinkingMarkup is not defined" and left no finalized thinking section; after, the block collapses to "View thinking process" correctly. * perf(chat): extract live thinking at commit cadence * fix(chat): bound live thinking work * test(chat): update stream invariant assertions --------- Co-authored-by: Léo <leograndcontact@gmail.com> --- static/js/MODULE_SUMMARY.md | 1 + static/js/chat.js | 743 +++++++++++++------ static/js/liveThinkingThrottle.js | 206 +++++ tests/live_thinking_scheduler.test.mjs | 277 +++++++ tests/test_chat_stream_scope.py | 3 +- tests/test_live_thinking_chat_integration.py | 145 ++++ tests/test_live_thinking_scheduler_js.py | 29 + 7 files changed, 1173 insertions(+), 231 deletions(-) create mode 100644 static/js/liveThinkingThrottle.js create mode 100644 tests/live_thinking_scheduler.test.mjs create mode 100644 tests/test_live_thinking_chat_integration.py create mode 100644 tests/test_live_thinking_scheduler_js.py diff --git a/static/js/MODULE_SUMMARY.md b/static/js/MODULE_SUMMARY.md index df5b0cb33..c0f88c824 100644 --- a/static/js/MODULE_SUMMARY.md +++ b/static/js/MODULE_SUMMARY.md @@ -61,6 +61,7 @@ The largest and most central subsystem. Chat submission → backend SSE → prog | **`chatRenderer.js`** | Message DOM construction: `addMessage`, role labels, model route labels, color coding, footers, metrics, code blocks, sources boxes (`web`/`research`/`RAG`), findings box, images, report links, ask-user cards, welcome screen, and transcript utilities. | | **`streamingRenderer.js`** | Incremental streaming renderer used by `chat.js`. Freezes finalized DOM blocks and only re-renders the growing tail to avoid flicker and O(N²) re-parsing. | | **`streamingSegmenter.js`** | Splits a token stream into display units (text vs code fences) for `streamingRenderer.js`. | +| **`liveThinkingThrottle.js`** | Trailing-edge coalescer for the live thinking block in `chat.js`: one DOM commit per 100 ms carrying the latest reasoning text, with `flush`/`cancel` for terminal and session-switch paths. | | **`slashCommands.js`** | Slash-command registry (`/help`, `/setup`, etc.), parsing, and dispatch handlers. Exported functions are consumed by `chat.js` and `slashAutocomplete.js`. | | **`slashAutocomplete.js`** | Composer autocomplete popup for `/` commands. | | **`composerArrowUpRecall.js`** | Recall last user message with `↑` on an empty composer. | diff --git a/static/js/chat.js b/static/js/chat.js index 3c8bbe850..ccf3c83a1 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -23,6 +23,12 @@ import slashCommands, { initSlashCommands, isCommand, handleSlashCommand, handle import createResearchSynapse from './researchSynapse.js'; import { createStreamRenderer } from './streamingRenderer.js'; import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArrowUpRecall.js?v=20260714promptrecall'; +import { + createIncrementalDisplayProjector, + createLiveThinkingThrottle, + createThinkingAnalysisGate, + stripLiveThinkingTags, +} from './liveThinkingThrottle.js'; const RESEARCH_TIMEOUT_MS = 360000; const DEFAULT_TIMEOUT_MS = 120000; @@ -562,7 +568,7 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr // Background streaming support const _backgroundStreams = new Map(); // sessionId -> { status, accumulated, sourcesHtml, abortCtrl, query, metrics } - const _activeStreams = new Map(); // sessionId -> { abortCtrl, holder, query, startedAt } + const _activeStreams = new Map(); // sessionId -> { abortCtrl, holder, query, startedAt, cancelViewWork, finalizeView } const _resumingStreams = new Set(); // sessionId -> a resumeStream() reader is live (re-attach lock) let _streamSessionId = null; // Session ID for the currently active reader loop let _lastReaderActivity = 0; // Timestamp of last reader.read() success — used to detect frozen streams @@ -1070,19 +1076,23 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr } // Render whatever was accumulated so far if (currentHolder && currentAccumulated) { - // Store accumulated in a closure variable before it gets cleared - const stoppedContent = currentAccumulated; - - // Store raw content in dataset for consistency with other messages - currentHolder.dataset.raw = stoppedContent; - - currentHolder.querySelector('.body').innerHTML = markdownModule.processWithThinking( - markdownModule.squashOutsideCode(stoppedContent) - ); + const _activeStopStream = _getForegroundStreamState(); + const _terminalView = _activeStopStream?.finalizeView?.() || null; + const _stoppedViewHolder = _terminalView?.holder || currentHolder; + const _viewPreparedByStream = !!_terminalView; + // The stream finalizer may close a synthetic reasoning tag. Capture the + // durable raw value only after that canonical terminal preparation. + const stoppedContent = _terminalView?.raw || currentAccumulated; + _stoppedViewHolder.dataset.raw = stoppedContent; + if (!_viewPreparedByStream) { + _stoppedViewHolder.querySelector('.body').innerHTML = markdownModule.processWithThinking( + markdownModule.squashOutsideCode(stoppedContent) + ); + } // Highlight code blocks if (window.hljs) { - currentHolder.querySelectorAll('pre code').forEach((block) => { + _stoppedViewHolder.querySelectorAll('pre code').forEach((block) => { window.hljs.highlightElement(block); }); } @@ -1097,7 +1107,7 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr continueBtn.className = 'continue-btn'; continueBtn.title = 'Continue'; continueBtn.textContent = '\u25B8'; - const _stoppedHolder = currentHolder; // capture before it gets cleared + const _stoppedHolder = _stoppedViewHolder; // capture before globals are cleared continueBtn.addEventListener('click', () => { stoppedIndicator.remove(); _hideUserBubble = true; @@ -1111,16 +1121,16 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr } }); stoppedIndicator.appendChild(continueBtn); - currentHolder.querySelector('.body').appendChild(stoppedIndicator); + _stoppedViewHolder.querySelector('.body').appendChild(stoppedIndicator); // Tell server to mark this message as stopped const _sid = sessionModule.getCurrentSessionId(); if (_sid) fetch(`${API_BASE}/api/session/${_sid}/mark-stopped`, { method: 'POST' }).catch(e => console.warn('mark-stopped failed:', e)); // Add footer with copy/regen if not already present - if (!currentHolder.querySelector('.msg-footer')) { - currentHolder.dataset.raw = stoppedContent; - currentHolder.appendChild(createMsgFooter(currentHolder)); + if (!_stoppedViewHolder.querySelector('.msg-footer')) { + _stoppedViewHolder.dataset.raw = stoppedContent; + _stoppedViewHolder.appendChild(createMsgFooter(_stoppedViewHolder)); } uiModule.scrollHistory(); @@ -1368,8 +1378,19 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr let processingProbeTimer = null; let processingProbeAbort = null; let _renderStream = () => {}; + let _finalizeRoundRender = () => {}; + let _finalizeInterruptedView = () => null; let _cancelThinkingTimer = () => {}; let _removeThinkingSpinner = () => {}; + let _flushLiveThinking = () => ''; + let _cancelLiveThinkingWork = () => {}; + // Declared out here, not inside the try: in an ES module a function declared + // in the try block is scoped to that block, so `catch` (a sibling scope) + // cannot see it. Calling one from catch throws ReferenceError and kills the + // rest of the error path — the stream never finalizes and the partial + // message is lost. Assigned below, alongside the two helpers above. + let _closeOpenThinkingMarkup = () => {}; + let _endThinkingOnTerminalPath = () => {}; let timeoutId = null; let responseTimeoutCleared = false; let clearResponseTimeout = () => {}; @@ -1763,6 +1784,10 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr query: streamQuery, startedAt: Date.now(), lastActivity: Date.now(), + // Resolve the mutable closure at call time: live-thinking helpers are + // installed after the stream entry is registered. + cancelViewWork: () => _cancelLiveThinkingWork(), + finalizeView: () => _finalizeInterruptedView(), }); _syncForegroundStreamGlobals(); holder._researchQuery = msg; // Store query for notification text @@ -1907,9 +1932,12 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr // Multi-bubble agent tracking let roundHolder = holder; // Current AI text bubble (changes per round) let roundText = ''; // Text accumulated for current round + let roundReplyText = null; // Reply-only text after a thinking transition let currentToolBubble = null; // Current tool execution bubble let lastToolThread = null; // Visible tool timeline for tool-only turns let roundFinalized = false; // Whether current round's text is finalized + let roundFinalization = null; // Terminal owner/result for the current round + let lastContentRoundHolder = null; // Last non-empty round for an empty continuation Stop let _sourcesHtml = ''; // Sources box HTML to prepend to body let _sourcesExpanded = false; // Track if user expanded sources during stream let _sourcesData = null; // Raw sources data for rebuilding @@ -1970,7 +1998,19 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr if (lastToolThread && lastToolThread.isConnected) lastToolThread.classList.add('has-bottom'); roundHolder = newWrap; roundText = ''; + roundReplyText = null; roundFinalized = false; + roundFinalization = null; + isThinking = false; + _thinkingMode = null; + _cancelThinkingGrace(); + _thinkingAnalysisGate.reset(); + _roundDisplayProjector.reset(); + _replyDisplayProjector.reset(); + _docFenceOpened = false; + _docFenceContentStart = -1; + _docFenceCandidateStart = -1; + _docFenceCandidateMarker = ''; } const esc = uiModule.esc; // Remove thinking spinner helper @@ -2061,6 +2101,16 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr // Document streaming state (text-fence detection) let _docFenceOpened = false; let _docFenceContentStart = -1; + let _docFenceCandidateStart = -1; + let _docFenceCandidateMarker = ''; + const _thinkingAnalysisGate = createThinkingAnalysisGate({ + startsWithReasoningPrefix: markdownModule.startsWithReasoningPrefix, + }); + const _roundDisplayProjector = createIncrementalDisplayProjector(_streamDisplayText); + const _replyDisplayProjector = createIncrementalDisplayProjector(_streamDisplayText); + let _thinkingMode = null; + let _thinkingRecheckAt = 0; + let _thinkingGraceTimer = null; let _liveThinkSection = null; let _liveThinkContent = null; let _liveThinkInner = null; @@ -2070,6 +2120,10 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr let _liveThinkTokenCount = 0; let _liveThinkToggle = null; let _liveThinkDomId = null; + let _liveThinkRenderThrottle = null; + let _liveThinkLatestText = ''; + let _liveThinkTimerId = null; + let _liveThinkReducedMotion = false; function _estimateThinkingTokens(text) { const clean = (text || '').trim(); @@ -2083,6 +2137,259 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr return time && tokens ? time + ' · ' + tokens : (time || tokens); } + function _stripThinkingWrappers(text) { + return text + .replace(/<\|channel>thought\s*\n?/gi, '') + .replace(/<\|channel>response\s*\n?/gi, '') + .replace(/<channel\|>/gi, '') + .replace(/^\s*Thinking(?:\s+Process)?:\s*/i, ''); + } + + // While thinking is still open, every think tag in the round is noise, so + // strip them all. Do NOT slice from the first <think> to the first </think>: + // the false-close detection below deliberately keeps us in the thinking + // state for `<think>The</think>` followed by real thinking left untagged, + // and slicing would pin the live box to "The" for the rest of the stream. + function _liveThinkingText(text) { + const normalized = markdownModule.normalizeThinkingMarkup(_streamDisplayText(text || '')); + return _stripThinkingWrappers(stripLiveThinkingTags(normalized)); + } + + // Once thinking has closed, the reply that follows </think> must not leak + // into the thinking box, so go through extractThinkingBlocks — it already + // collapses the false-close pattern and merges every block into one. + function _closedThinkingText(text) { + const normalized = markdownModule.normalizeThinkingMarkup(_streamDisplayText(text || '')); + const blocks = markdownModule.extractThinkingBlocks + ? markdownModule.extractThinkingBlocks(normalized)?.thinkingBlocks + : null; + if (blocks?.length) return _stripThinkingWrappers(blocks.join('\n\n')); + return _liveThinkingText(text); + } + + function _commitLiveThinkingText(text) { + _liveThinkLatestText = String(text ?? ''); + _liveThinkTokenCount = _estimateThinkingTokens(_liveThinkLatestText); + const target = _liveThinkInner; + if (!target || !target.isConnected) return; + const thinkBox = target.closest('.thinking-content'); + const nearBottom = !thinkBox || thinkBox.scrollHeight - thinkBox.clientHeight - thinkBox.scrollTop < 80; + target.style.whiteSpace = 'pre-wrap'; + target.textContent = _liveThinkLatestText; + if (thinkBox && nearBottom) thinkBox.scrollTop = thinkBox.scrollHeight; + if (nearBottom) uiModule.scrollHistory(); + } + + function _ensureLiveThinkingThrottle() { + if (!_liveThinkRenderThrottle) { + _liveThinkRenderThrottle = createLiveThinkingThrottle(_commitLiveThinkingText, { + prepare: ({ text, prepared }) => prepared ? String(text ?? '') : _liveThinkingText(text), + }); + } + return _liveThinkRenderThrottle; + } + + function _stopLiveThinkTimer() { + if (_liveThinkTimerId !== null) clearInterval(_liveThinkTimerId); + _liveThinkTimerId = null; + } + + function _startLiveThinkTimer() { + if (_liveThinkTimerId !== null || !_liveThinkTimerEl) return; + _liveThinkReducedMotion = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); + const cadence = _liveThinkReducedMotion ? 1000 : 250; + _liveThinkTimerId = setInterval(() => { + if (!_liveThinkTimerEl || !_liveThinkTimerEl.isConnected) { + _stopLiveThinkTimer(); + return; + } + const elapsed = (Date.now() - thinkingStartTime) / 1000; + const seconds = elapsed.toFixed(_liveThinkReducedMotion ? 0 : 1); + _liveThinkTimerEl.textContent = _formatThinkStats(seconds, _liveThinkTokenCount); + }, cadence); + } + + function _queueLiveThinking(text, prepared = false) { + _ensureLiveThinkingThrottle().update({ text, prepared }); + _startLiveThinkTimer(); + } + + _flushLiveThinking = ({ text = null, rich = false } = {}) => { + if (text !== null) _queueLiveThinking(text, true); + if (_liveThinkRenderThrottle) _liveThinkRenderThrottle.flush(); + if (rich && _liveThinkInner && _liveThinkInner.isConnected) { + _liveThinkInner.style.whiteSpace = ''; + _liveThinkInner.innerHTML = markdownModule.mdToHtml(_liveThinkLatestText); + } + return _liveThinkLatestText; + }; + + _cancelLiveThinkingWork = () => { + if (_liveThinkRenderThrottle) _liveThinkRenderThrottle.cancel(); + _liveThinkRenderThrottle = null; + _stopLiveThinkTimer(); + _cancelThinkingGrace(); + }; + + function _finalizeLiveThinking(text, rich = true) { + const finalText = _flushLiveThinking({ text, rich }); + _cancelLiveThinkingWork(); + return finalText; + } + + // Close the synthetic <think> we opened around vLLM reasoning deltas, so a + // stream that ends mid-thinking doesn't persist an unclosed tag. + // `currentAccumulated` is the FOREGROUND stop-state text — mirror the guard + // the delta path uses (`if (!_isBg) currentAccumulated = accumulated`), or a + // backgrounded stream overwrites the visible session's stop-state and + // abortCurrentRequest/detachCurrentStream write it into the wrong bubble. + _closeOpenThinkingMarkup = (isBackground) => { + if (!_thinkOpen) return; + accumulated += '</think>'; + roundText += '</think>'; + if (!isBackground) currentAccumulated = accumulated; + _thinkOpen = false; + }; + + // Terminal finalize used by the catch path, which cannot see the + // block-scoped helpers below. + _endThinkingOnTerminalPath = ({ rich = true } = {}) => { + if (isThinking) { + isThinking = false; + _thinkingMode = null; + _thinkingRecheckAt = 0; + _finalizeLiveThinking(_closedThinkingText(roundText), rich); + } else { + _cancelLiveThinkingWork(); + } + }; + + // Shared teardown for the terminal paths that end thinking without the + // normal </think> transition (tool_start, agent_step, [DONE], errors). + function _endLiveThinkingSection({ rich = true } = {}) { + isThinking = false; + _thinkingMode = null; + _thinkingRecheckAt = 0; + _finalizeLiveThinking(_closedThinkingText(roundText), rich); + const elapsed = thinkingStartTime ? ((Date.now() - thinkingStartTime) / 1000).toFixed(1) : null; + if (_liveThinkHeader) _liveThinkHeader.textContent = 'View thinking process'; + if (_liveThinkTimerEl) _liveThinkTimerEl.textContent = elapsed ? _formatThinkStats(elapsed, _liveThinkTokenCount) : ''; + if (_liveThinkSpinnerSlot) _liveThinkSpinnerSlot.remove(); + } + + function _cancelThinkingGrace() { + if (_thinkingGraceTimer !== null) clearTimeout(_thinkingGraceTimer); + _thinkingGraceTimer = null; + _thinkingRecheckAt = 0; + } + + function _finishLiveThinkingTransition() { + if (!isThinking) return; + isThinking = false; + _thinkingMode = null; + _cancelThinkingGrace(); + const closedText = _closedThinkingText(roundText); + const thinkTextLen = closedText.trim().length; + _finalizeLiveThinking(closedText, thinkTextLen >= 20); + + // Models sometimes emit a trivial marker such as <think>The</think>. + if (thinkTextLen < 20 && _liveThinkSection) { + _liveThinkSection.remove(); + _liveThinkSection = null; + _liveThinkContent = null; + _liveThinkInner = null; + _liveThinkHeader = null; + _liveThinkSpinnerSlot = null; + _liveThinkTimerEl = null; + _liveThinkTokenCount = 0; + _liveThinkToggle = null; + _liveThinkDomId = null; + if (spinner && spinner.element) spinner.destroy(); + _renderStream({ knownNormal: true, displayText: _roundDisplayProjector.current() }); + _scheduleThinkingSpinner(); + return; + } + + const elapsed = thinkingStartTime ? ((Date.now() - thinkingStartTime) / 1000).toFixed(1) : null; + if (elapsed) { + accumulated = accumulated.replace(/<think>/i, '<think time="' + elapsed + '">'); + roundText = roundText.replace(/<think>/i, '<think time="' + elapsed + '">'); + } + if (_liveThinkHeader) _liveThinkHeader.textContent = 'View thinking process'; + if (_liveThinkSpinnerSlot) _liveThinkSpinnerSlot.remove(); + if (_liveThinkTimerEl && elapsed) { + _liveThinkTimerEl.textContent = _formatThinkStats(elapsed, _liveThinkTokenCount); + _liveThinkTimerEl.style.marginLeft = 'auto'; + _liveThinkTimerEl.style.marginRight = '5px'; + const headerRow = _liveThinkTimerEl.closest('.thinking-header'); + if (headerRow) { + if (_liveThinkToggle && _liveThinkToggle.parentElement === headerRow) headerRow.insertBefore(_liveThinkTimerEl, _liveThinkToggle); + else headerRow.appendChild(_liveThinkTimerEl); + } + } + + const thinkingId = 'think-' + Date.now(); + const liveHeader = _liveThinkSection && _liveThinkSection.querySelector('.thinking-header'); + if (liveHeader) liveHeader.dataset.thinkingId = thinkingId; + if (_liveThinkContent) _liveThinkContent.id = thinkingId; + if (_liveThinkToggle) _liveThinkToggle.id = thinkingId + '-toggle'; + + const streamElement = _liveThinkSection ? _liveThinkSection.parentElement : roundHolder.querySelector('.stream-content'); + const replyHost = streamElement || roundHolder.querySelector('.body'); + if (replyHost && !replyHost.querySelector('.live-reply-content')) { + const replyElement = document.createElement('div'); + replyElement.className = 'live-reply-content'; + replyHost.appendChild(replyElement); + } + _renderStream(); + } + + function _scheduleThinkingGrace() { + if (_thinkingGraceTimer !== null || !_thinkingRecheckAt) return; + const delay = Math.max(0, _thinkingRecheckAt - Date.now()); + _thinkingGraceTimer = setTimeout(() => { + _thinkingGraceTimer = null; + if (!isThinking || !roundHolder?.isConnected || abortCtrl?.signal?.aborted) return; + _finishLiveThinkingTransition(); + }, delay); + } + + // Terminal paths replace the whole round, so they should perform exactly + // one rich markdown render instead of richly finalizing thinking, then + // rendering the reply, then replacing both again. + _finalizeRoundRender = () => { + if (roundFinalized) return roundFinalization; + const terminalHolder = roundHolder || holder; + const dt = markdownModule.normalizeThinkingMarkup(_streamDisplayText(roundText)); + if (!dt.trim()) { + terminalHolder.style.display = 'none'; + roundFinalized = true; + roundFinalization = { rendered: true, holder: terminalHolder, hasContent: false }; + return roundFinalization; + } + const body = terminalHolder.querySelector('.body'); + const content = _ensureStreamLayout(body); + content.style.minHeight = ''; + content.innerHTML = markdownModule.processWithThinking(markdownModule.squashOutsideCode(dt)); + if (window.hljs) terminalHolder.querySelectorAll('pre code').forEach((block) => window.hljs.highlightElement(block)); + roundFinalized = true; + lastContentRoundHolder = terminalHolder; + roundFinalization = { rendered: true, holder: terminalHolder, hasContent: true }; + return roundFinalization; + }; + _finalizeInterruptedView = () => { + _closeOpenThinkingMarkup(false); + _endThinkingOnTerminalPath({ rich: false }); + const finalization = _finalizeRoundRender(); + return { + rendered: !!finalization?.rendered, + holder: finalization?.hasContent + ? finalization.holder + : (lastContentRoundHolder || finalization?.holder || roundHolder || holder), + raw: accumulated, + }; + }; + function _replyAfterClosedThinking(text) { text = markdownModule.normalizeThinkingMarkup(text || ''); const closeRe = /<\/(?:think(?:ing)?|thought)>|<channel\|>/gi; @@ -2094,8 +2401,7 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr } // Direct render helper for streaming text - _renderStream = () => { - let dt = markdownModule.normalizeThinkingMarkup(_streamDisplayText(roundText)); + _renderStream = ({ knownNormal = false, displayText = null, replyText = null } = {}) => { const bodyEl = roundHolder.querySelector('.body'); const contentEl = _ensureStreamLayout(bodyEl); @@ -2103,14 +2409,16 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr let liveReply = contentEl.querySelector('.live-reply-content'); if (liveReply) { // Extract reply text — handle native <think> tags and non-tag patterns - const closedThinkReply = _replyAfterClosedThinking(dt); - const { thinkingBlocks, content: replyText } = closedThinkReply - ? { thinkingBlocks: [''], content: closedThinkReply } - : markdownModule.extractThinkingBlocks(dt); - let replyTrimmed = ''; - if (thinkingBlocks.length) { - replyTrimmed = (replyText || '').trim(); - } else { + let replyTrimmed = replyText === null ? '' : String(replyText); + if (replyText === null) { + const dt = markdownModule.normalizeThinkingMarkup(_streamDisplayText(roundText)); + const closedThinkReply = _replyAfterClosedThinking(dt); + const { thinkingBlocks, content: extractedReply } = closedThinkReply + ? { thinkingBlocks: [''], content: closedThinkReply } + : markdownModule.extractThinkingBlocks(dt); + if (thinkingBlocks.length) { + replyTrimmed = (extractedReply || '').trim(); + } else { // Non-tag: check for garbled <think> (reasoning\n<think>reply) const _gm = dt.match(/^[\s\S]+?<(?:think(?:ing)?|thought)(?:\s+[^>]*)?>\s*([\s\S]*?)(?:<\/(?:think(?:ing)?|thought)>)?\s*$/i); if (_gm && _gm[1].trim()) { @@ -2119,7 +2427,7 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr // Pure non-tag: find reply boundary const _rPrefixes = markdownModule.startsWithReasoningPrefix; const _rpStarts = ['Hey', 'Hi ', 'Hi!', 'Hello', 'Sure', 'Yes', 'No ', 'No,', 'Yo', 'OK', 'Here', 'Absolutely', 'Of course', 'Great', 'Alright', 'Thanks', 'Welcome', 'Good ', "I'm happy", "I'd be"]; - const _rt = (replyText || '').trimStart(); + const _rt = (extractedReply || '').trimStart(); if (_rPrefixes(_rt)) { const _rLines = _rt.split('\n'); for (let _ri = 1; _ri < _rLines.length; _ri++) { @@ -2136,6 +2444,12 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr } } } + } + } + if (replyText === null) { + roundReplyText = replyTrimmed; + _replyDisplayProjector.reset(); + replyTrimmed = _replyDisplayProjector.append(replyTrimmed, roundReplyText); } if (replyTrimmed) { const r = liveReply._streamRenderer || @@ -2150,8 +2464,18 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr return; } + // Thinking compatibility normalization and display stripping are + // intentionally omitted from the known-normal path. The incremental + // projector already handled the newly appended boundary, so repeating + // the full-round regex chains per delta would restore O(N^2) work. + let dt = displayText === null + ? (knownNormal + ? _roundDisplayProjector.current() + : markdownModule.normalizeThinkingMarkup(_streamDisplayText(roundText))) + : String(displayText); + // If thinking is still streaming (unclosed <think>), show indicator instead of raw text - if (markdownModule.hasUnclosedThinkTag && markdownModule.hasUnclosedThinkTag(dt)) { + if (!knownNormal && markdownModule.hasUnclosedThinkTag && markdownModule.hasUnclosedThinkTag(dt)) { const thinkStart = dt.search(/<(?:think(?:ing)?|thought)(?:\s+[^>]*)?>|<\|channel>thought/i); const thinkContent = dt.substring(Math.max(thinkStart, 0)) .replace(/<(?:think(?:ing)?|thought)(?:\s+[^>]*)?>|<\|channel>thought\s*\n?/i, '') @@ -2224,6 +2548,10 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr // On first transition to background, store state in map if (_isBg && !_backgroundStreams.has(streamSessionId)) { + // Leave the block in its finished shape (rich, no pre-wrap) rather + // than frozen as plain text — the user may navigate back to it. + _flushLiveThinking({ rich: true }); + _cancelLiveThinkingWork(); _backgroundStreams.set(streamSessionId, { status: 'running', accumulated: accumulated, @@ -2240,6 +2568,7 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr if (data === '[DONE]') { _streamSawDone = true; + _closeOpenThinkingMarkup(_isBg); // Always update background map if entry exists (even if user switched back) var bgDone = _backgroundStreams.get(streamSessionId); if (bgDone && !_isBg) { @@ -2270,7 +2599,9 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr // Force-close thinking if still open (model never output boundary) if (isThinking) { isThinking = false; - cancelAnimationFrame(_thinkTimerRAF); + // The final round render below is authoritative and will render + // the complete thinking + reply markup once. + _finalizeLiveThinking(_closedThinkingText(roundText), false); var _elapsedDone = thinkingStartTime ? ((Date.now() - thinkingStartTime) / 1000).toFixed(1) : null; if (_elapsedDone) { accumulated = accumulated.replace(/<think>/i, '<think time="' + _elapsedDone + '">'); @@ -2298,14 +2629,6 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr if (_liveHdrDone) _liveHdrDone.dataset.thinkingId = _thinkIdDone; if (_liveThinkContent) _liveThinkContent.id = _thinkIdDone; if (_liveThinkToggle) _liveThinkToggle.id = _thinkIdDone + '-toggle'; - // Create live-reply container so final render preserves thinking bar - var _streamElDone = _liveThinkSection ? _liveThinkSection.parentElement : roundHolder.querySelector('.stream-content'); - if (!_streamElDone) _streamElDone = roundHolder.querySelector('.body'); - if (_streamElDone && !_streamElDone.querySelector('.live-reply-content')) { - var _replyElDone = document.createElement('div'); - _replyElDone.className = 'live-reply-content'; - _streamElDone.appendChild(_replyElDone); - } } // Normal foreground completion — metrics will be displayed in the final render block below break; @@ -2372,22 +2695,37 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr } _ensureVisibleRoundForDelta(); roundText += _delta; + _roundDisplayProjector.append(_delta, roundText); // --- Text-fence doc streaming (for models that don't use native tool calls) --- - if (!_docFenceOpened && documentModule && (roundText.includes('```create_document\n') || roundText.includes('```document\n') || roundText.includes('```documen\n'))) { - const fenceMarker = roundText.includes('```document\n') ? '```document\n' : (roundText.includes('```documen\n') ? '```documen\n' : '```create_document\n'); - const fenceIdx = roundText.indexOf(fenceMarker); - const afterFence = roundText.slice(fenceIdx + fenceMarker.length); - const fenceLines = afterFence.split('\n'); - if (fenceLines.length >= 1 && fenceLines[0].trim()) { - _docFenceOpened = true; - const title = fenceLines[0].trim(); - // Keep in sync with backend _KNOWN_LANGS in src/tool_implementations.py - const knownLangs = ['python','py','javascript','js','typescript','ts','html','css','json','yaml','bash','sql','rust','go','java','c','cpp','markdown','text','plain','ruby','swift','kotlin','php','email','csv','xml','toml','ini']; - const isLang = fenceLines.length >= 2 && knownLangs.includes(fenceLines[1].trim().toLowerCase()); - const lang = isLang ? fenceLines[1].trim() : ''; - _docFenceContentStart = fenceIdx + fenceMarker.length + title.length + 1 + (isLang ? fenceLines[1].length + 1 : 0); - documentModule.streamDocOpen(title, lang); + if (!_docFenceOpened && documentModule) { + // Only inspect the newly appended boundary. Re-scanning the + // full round for every reasoning delta is quadratic even + // before thinking normalization runs. + const fenceMarkers = ['```document\n', '```documen\n', '```create_document\n']; + const fenceScanStart = Math.max(0, roundText.length - _delta.length - 24); + if (_docFenceCandidateStart < 0) { + for (const candidate of fenceMarkers) { + const candidateIdx = roundText.indexOf(candidate, fenceScanStart); + if (candidateIdx >= 0 && (_docFenceCandidateStart < 0 || candidateIdx < _docFenceCandidateStart)) { + _docFenceCandidateMarker = candidate; + _docFenceCandidateStart = candidateIdx; + } + } + } + if (_docFenceCandidateStart >= 0) { + const afterFence = roundText.slice(_docFenceCandidateStart + _docFenceCandidateMarker.length); + const fenceLines = afterFence.split('\n'); + if (fenceLines.length >= 1 && fenceLines[0].trim()) { + _docFenceOpened = true; + const title = fenceLines[0].trim(); + // Keep in sync with backend _KNOWN_LANGS in src/tool_implementations.py + const knownLangs = ['python','py','javascript','js','typescript','ts','html','css','json','yaml','bash','sql','rust','go','java','c','cpp','markdown','text','plain','ruby','swift','kotlin','php','email','csv','xml','toml','ini']; + const isLang = fenceLines.length >= 2 && knownLangs.includes(fenceLines[1].trim().toLowerCase()); + const lang = isLang ? fenceLines[1].trim() : ''; + _docFenceContentStart = _docFenceCandidateStart + _docFenceCandidateMarker.length + title.length + 1 + (isLang ? fenceLines[1].length + 1 : 0); + documentModule.streamDocOpen(title, lang); + } } } if (_docFenceOpened && _docFenceContentStart > 0 && documentModule) { @@ -2401,6 +2739,30 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr // 1. Normal: <think>...no closing tag yet // 2. Malformed: <think></think>\n...text but no second </think> yet // 3. Qwen3.5: "Thinking Process:" without <think> tags + // Most deltas cannot change thinking state. Analyze cumulative + // text only for a fresh tag/channel/reply boundary, an initial + // reasoning prefix, or an expired false-close grace period. + if (!_thinkingAnalysisGate.shouldAnalyze(roundText, { + isThinking, + nonTagThinking: _thinkingMode === 'prefix', + recheckAt: _thinkingRecheckAt, + })) { + if (isThinking) { + _queueLiveThinking(roundText); + } else { + if (spinner && spinner.element) spinner.destroy(); + if (roundReplyText !== null) { + roundReplyText += _delta; + const replyDisplayText = _replyDisplayProjector.append(_delta, roundReplyText); + _renderStream({ replyText: replyDisplayText }); + } else { + _renderStream({ knownNormal: true, displayText: _roundDisplayProjector.current() }); + } + _scheduleThinkingSpinner(); + if (streamingTTS) window.aiTTSManager.streamingUpdate(roundText); + } + continue; + } const normalizedRoundText = markdownModule.normalizeThinkingMarkup(roundText); let hasUnclosedThink = markdownModule.hasUnclosedThinkTag(normalizedRoundText); // Detect non-tag thinking patterns: "Thinking:", "Thinking Process:", Gemma-style reasoning @@ -2432,34 +2794,39 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr } } } - if (!hasUnclosedThink && /^<(?:think(?:ing)?|thought)(?:\s+[^>]*)?>\s*<\/(?:think(?:ing)?|thought)>/i.test(normalizedRoundText)) { - // Empty <think></think> — the model likely put thinking outside the tags - const afterEmpty = normalizedRoundText.replace(/^<(?:think(?:ing)?|thought)(?:\s+[^>]*)?>\s*<\/(?:think(?:ing)?|thought)>/i, '').trim(); - const closeTags = (afterEmpty.match(/<\/(?:think(?:ing)?|thought)>/gi) || []).length; - if (closeTags === 0 && afterEmpty.length > 0) { - hasUnclosedThink = true; // still waiting for real closing tag - } - } // Detect false close: <think>short</think> where real thinking follows untagged - // Only applies when there's a second </think> later (model leaked thinking outside tags) - // Do NOT trigger if the text after </think> contains tool calls (that's real content) - if (!hasUnclosedThink && isThinking) { + // Do NOT require a prior unclosed delta: providers can emit the + // short open+close and leaked reasoning in one chunk. + let _falseCloseDeadline = 0; + if (!hasUnclosedThink) { const _thinkMatch = normalizedRoundText.match(/<(?:think(?:ing)?|thought)(?:\s+[^>]*)?>([\s\S]*?)<\/(?:think(?:ing)?|thought)>/i); const _thinkLen = _thinkMatch ? _thinkMatch[1].trim().length : 0; - if (_thinkLen < 20) { + if (_thinkMatch && _thinkLen < 20) { const _afterClose = normalizedRoundText.replace(/<(?:think(?:ing)?|thought)(?:\s+[^>]*)?>([\s\S]*?)<\/(?:think(?:ing)?|thought)>/i, '').trim(); // Only keep waiting if there's trailing text that looks like thinking (not tool calls) const _hasToolCall = /```(?:bash|python|web_search|read_file|write_file|create_document|edit_document|manage_|generate_image)/i.test(_afterClose); const _hasOrphanClose = /<\/(?:think(?:ing)?|thought)>/i.test(_afterClose); - if (!_hasToolCall && (_hasOrphanClose || (Date.now() - thinkingStartTime) < 500)) { - hasUnclosedThink = true; // keep waiting for real </think> + const _falseCloseStart = thinkingStartTime || Date.now(); + if (_afterClose && !_hasToolCall && !_hasOrphanClose && (Date.now() - _falseCloseStart) < 500) { + hasUnclosedThink = true; + _falseCloseDeadline = _falseCloseStart + 500; + if (isThinking) { + _thinkingRecheckAt = _falseCloseDeadline; + _scheduleThinkingGrace(); + } + } else if (isThinking) { + _cancelThinkingGrace(); } } } if (hasUnclosedThink && !isThinking) { isThinking = true; + _thinkingMode = /<(?:think(?:ing)?|thought)(?:\s+[^>]*)?>|<\|channel>thought/i.test(normalizedRoundText) + ? 'tag' + : 'prefix'; thinkingStartTime = Date.now(); + _thinkingRecheckAt = _falseCloseDeadline || 0; if (spinner && spinner.element) spinner.destroy(); // Create a live thinking box — starts expanded so content streams visibly @@ -2486,16 +2853,9 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr _liveThinkSpinnerSlot = thinkContent.querySelector('.live-think-spinner-slot'); _liveThinkTimerEl = thinkContent.querySelector('.live-think-timer'); _liveThinkToggle = thinkContent.querySelector('.live-think-toggle'); - // Live timer - var _thinkTimerStart = Date.now(); - var _thinkTimerRAF = 0; - function _tickThinkTimer() { - if (!_liveThinkTimerEl || !_liveThinkTimerEl.isConnected) return; - var s = ((Date.now() - _thinkTimerStart) / 1000).toFixed(1); - _liveThinkTimerEl.textContent = _formatThinkStats(s, _liveThinkTokenCount); - _thinkTimerRAF = requestAnimationFrame(_tickThinkTimer); - } - _thinkTimerRAF = requestAnimationFrame(_tickThinkTimer); + _liveThinkLatestText = ''; + _cancelLiveThinkingWork(); + _queueLiveThinking(roundText); // Whirlpool spinner if (_liveThinkSpinnerSlot) { var _wp = spinnerModule.createWhirlpool(12); @@ -2505,104 +2865,22 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr _wp.element.style.transform = 'translateY(-1px)'; // align the whirlpool with the header text _liveThinkSpinnerSlot.appendChild(_wp.element); } + if (_thinkingRecheckAt) _scheduleThinkingGrace(); } else if (hasUnclosedThink && isThinking) { - if (_liveThinkInner) { - // Extract raw thinking text (strip known thinking wrappers and prefixes) - var thinkText = markdownModule.normalizeThinkingMarkup(_streamDisplayText(roundText)) - .replace(/<\/?(?:think(?:ing)?|thought)(?:\s+[^>]*)?>/gi, '') - .replace(/<\|channel>thought\s*\n?/gi, '') - .replace(/<\|channel>response\s*\n?/gi, '') - .replace(/<channel\|>/gi, ''); - thinkText = thinkText.replace(/^\s*Thinking(?:\s+Process)?:\s*/i, ''); - _liveThinkTokenCount = _estimateThinkingTokens(thinkText); - _liveThinkInner.innerHTML = markdownModule.mdToHtml(thinkText); - if (_liveThinkTimerEl) { - var _elapsedLive = thinkingStartTime ? ((Date.now() - thinkingStartTime) / 1000).toFixed(1) : ''; - _liveThinkTimerEl.textContent = _formatThinkStats(_elapsedLive, _liveThinkTokenCount); - } - // Keep thinking box scrolled to bottom, but let user scroll up - var _followThinking = true; - var thinkBox = _liveThinkInner.closest('.thinking-content'); - if (thinkBox) { - var nearBottom = thinkBox.scrollHeight - thinkBox.clientHeight - thinkBox.scrollTop < 80; - if (nearBottom) thinkBox.scrollTop = thinkBox.scrollHeight; - _followThinking = nearBottom; - } - } - if (_followThinking) uiModule.scrollHistory(); + _queueLiveThinking(roundText); continue; } else if (!hasUnclosedThink && isThinking) { - isThinking = false; - var _thinkTextLen = _liveThinkInner ? _liveThinkInner.textContent.trim().length : 0; - - // If thinking was trivially short (< 20 chars), remove the section entirely - // Models sometimes emit <think>The</think> or similar noise - if (_thinkTextLen < 20 && _liveThinkSection) { - _liveThinkSection.remove(); - _liveThinkSection = null; - _liveThinkContent = null; - _liveThinkInner = null; - _liveThinkHeader = null; - _liveThinkSpinnerSlot = null; - _liveThinkTimerEl = null; - _liveThinkTokenCount = 0; - _liveThinkToggle = null; - _liveThinkDomId = null; - // Fall through to normal streaming - if (spinner && spinner.element) spinner.destroy(); - _renderStream(); - _scheduleThinkingSpinner(); - continue; - } - - // Thinking ended — smooth transition: update header, pause, then collapse - // Stop live timer and spinner - cancelAnimationFrame(_thinkTimerRAF); - var elapsed = thinkingStartTime ? ((Date.now() - thinkingStartTime) / 1000).toFixed(1) : null; - // Embed thinking time in the <think> tag for persistence on reload - if (elapsed) { - accumulated = accumulated.replace(/<think>/i, '<think time="' + elapsed + '">'); - roundText = roundText.replace(/<think>/i, '<think time="' + elapsed + '">'); - } - if (_liveThinkHeader) _liveThinkHeader.textContent = 'View thinking process'; - if (_liveThinkSpinnerSlot) _liveThinkSpinnerSlot.remove(); - // Move timer to right side of header - if (_liveThinkTimerEl && elapsed) { - _liveThinkTimerEl.textContent = _formatThinkStats(elapsed, _liveThinkTokenCount); - _liveThinkTimerEl.style.marginLeft = 'auto'; - _liveThinkTimerEl.style.marginRight = '5px'; - var _hdrRow = _liveThinkTimerEl.closest('.thinking-header'); - // Chevron furthest right, timer to its left — insert before - // the toggle (appending would put the timer after it). - if (_hdrRow) { - if (_liveThinkToggle && _liveThinkToggle.parentElement === _hdrRow) - _hdrRow.insertBefore(_liveThinkTimerEl, _liveThinkToggle); - else _hdrRow.appendChild(_liveThinkTimerEl); - } - } - - // Assign stable IDs (for click-toggle handler in markdown.js) - var _thinkId = 'think-' + Date.now(); - var _liveHdr = _liveThinkSection && _liveThinkSection.querySelector('.thinking-header'); - if (_liveHdr) _liveHdr.dataset.thinkingId = _thinkId; - if (_liveThinkContent) _liveThinkContent.id = _thinkId; - if (_liveThinkToggle) _liveThinkToggle.id = _thinkId + '-toggle'; - - // Append a container for the reply text that follows thinking - var _streamEl = _liveThinkSection ? _liveThinkSection.parentElement : roundHolder.querySelector('.stream-content'); - if (!_streamEl) _streamEl = roundHolder.querySelector('.body'); - if (_streamEl) { - var _replyEl = document.createElement('div'); - _replyEl.className = 'live-reply-content'; - _streamEl.appendChild(_replyEl); - } - - // Render any reply text that arrived with the closing </think> token - _renderStream(); + _finishLiveThinkingTransition(); } else { // Normal streaming if (spinner && spinner.element) spinner.destroy(); - _renderStream(); + if (roundReplyText !== null) { + roundReplyText += _delta; + const replyDisplayText = _replyDisplayProjector.append(_delta, roundReplyText); + _renderStream({ replyText: replyDisplayText }); + } else { + _renderStream({ knownNormal: true, displayText: _roundDisplayProjector.current() }); + } _scheduleThinkingSpinner(); // Feed streaming TTS with accumulated text if (streamingTTS) window.aiTTSManager.streamingUpdate(roundText); @@ -2973,40 +3251,17 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr if (holder && json.id) holder.dataset.dbId = json.id; } else if (json.type === 'tool_start') { + _closeOpenThinkingMarkup(_isBg); if (_isBg) continue; _cancelThinkingTimer(); _removeThinkingSpinner(); // Force-close thinking if still open — tools are real content, not thinking if (isThinking) { - isThinking = false; - cancelAnimationFrame(_thinkTimerRAF); - var _elapsed2 = thinkingStartTime ? ((Date.now() - thinkingStartTime) / 1000).toFixed(1) : null; - if (_liveThinkHeader) _liveThinkHeader.textContent = 'View thinking process'; - if (_liveThinkTimerEl) _liveThinkTimerEl.textContent = _elapsed2 ? _formatThinkStats(_elapsed2, _liveThinkTokenCount) : ''; - if (_liveThinkSpinnerSlot) _liveThinkSpinnerSlot.remove(); - // Assign stable IDs - var _thinkId2 = 'think-' + Date.now(); - var _liveHdr2 = _liveThinkSection && _liveThinkSection.querySelector('.thinking-header'); - if (_liveHdr2) _liveHdr2.dataset.thinkingId = _thinkId2; - if (_liveThinkContent) _liveThinkContent.id = _thinkId2; - if (_liveThinkToggle) _liveThinkToggle.id = _thinkId2 + '-toggle'; + _endLiveThinkingSection({ rich: false }); } - _renderStream(); // --- Finalize current text bubble (only once per round) --- - if (!roundFinalized) { - roundFinalized = true; - if (spinner && spinner.element) spinner.destroy(); - const dt = markdownModule.normalizeThinkingMarkup(_streamDisplayText(roundText)); - if (dt.trim()) { - var _body3 = roundHolder.querySelector('.body'); - var _contentEl3 = _ensureStreamLayout(_body3); - _contentEl3.style.minHeight = ''; // clear streaming inflate - _contentEl3.innerHTML = markdownModule.processWithThinking(markdownModule.squashOutsideCode(dt)); - if (window.hljs) roundHolder.querySelectorAll('pre code').forEach((b) => window.hljs.highlightElement(b)); - } else { - roundHolder.style.display = 'none'; - } - } + if (spinner && spinner.element) spinner.destroy(); + _finalizeRoundRender(); // Track tool name for contextual spinner labels _lastToolName = json.tool || ''; @@ -3324,10 +3579,16 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr if (_pu) _setStoredPlan(_pu); } else if (json.type === 'agent_step') { + _closeOpenThinkingMarkup(_isBg); if (_isBg) continue; _cancelThinkingTimer(); _removeThinkingSpinner(); - _renderStream(); + if (isThinking) { + _endLiveThinkingSection({ rich: false }); + } else { + _cancelLiveThinkingWork(); + } + _finalizeRoundRender(); // Mark thread as connected to bubble below const _activeThread = document.querySelector('.agent-thread.streaming'); if (_activeThread) { @@ -3336,9 +3597,18 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr // --- New round: create fresh AI bubble with spinner --- currentToolBubble = null; roundFinalized = false; + roundFinalization = null; isThinking = false; + roundReplyText = null; + _thinkingMode = null; + _thinkingRecheckAt = 0; + _thinkingAnalysisGate.reset(); + _roundDisplayProjector.reset(); + _replyDisplayProjector.reset(); _docFenceOpened = false; _docFenceContentStart = -1; + _docFenceCandidateStart = -1; + _docFenceCandidateMarker = ''; const box = document.getElementById('chat-history'); const newWrap = document.createElement('div'); newWrap.className = 'msg msg-ai msg-continuation streaming'; @@ -3412,6 +3682,7 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr roundHolder = null; roundText = ''; roundFinalized = false; + roundFinalization = null; currentToolBubble = null; uiModule.scrollHistory(); @@ -3458,7 +3729,10 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr throw new Error('Stream closed before completion'); } - _renderStream(); + // The final foreground render below is authoritative. Cancel any delayed + // live-view work instead of parsing and rendering the full round once + // here and then immediately replacing it. + _cancelLiveThinkingWork(); if (spinner && spinner.element) { try { spinner.destroy(); } catch (_) {} spinner = null; } _cancelThinkingTimer(); _removeThinkingSpinner(); @@ -3739,14 +4013,27 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr } // end if (!_isBgFinal) } catch (err) { - _renderStream(); + // Check if this stream was running in background — needed before any + // stop-state write, so an errored background stream can't clobber the + // foreground session's text. + const _isBgCatch = (sessionModule.getCurrentSessionId() !== streamSessionId) || _backgroundStreams.has(streamSessionId); + let _catchTerminalView = null; + _closeOpenThinkingMarkup(_isBgCatch); + if (_isBgCatch) { + _cancelLiveThinkingWork(); + } else if (accumulated) { + _catchTerminalView = _finalizeInterruptedView(); + } else { + // Empty terminal views are owned by _renderCancelledBubble; do not run + // the rich round renderer first because it hides an empty holder. + _endThinkingOnTerminalPath({ rich: false }); + } + const _catchViewHolder = _catchTerminalView?.holder || holder; // Clean up any active spinner (e.g. "Generating response" during tool calls) if (spinner && spinner.element) spinner.destroy(); _cancelThinkingTimer(); _removeThinkingSpinner(); document.querySelectorAll('.agent-thread.streaming').forEach(t => t.classList.remove('streaming')); - // Check if this stream was running in background - const _isBgCatch = (sessionModule.getCurrentSessionId() !== streamSessionId) || _backgroundStreams.has(streamSessionId); if (_isBgCatch) { // Error happened while backgrounded — update map, don't touch DOM @@ -3779,12 +4066,12 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr if (holder && !accumulated) { holder.querySelector('.body').innerHTML = `<div style="color: var(--color-error); font-style: italic; padding: 4px 0;">[${timeoutMsg}]</div>`; - } else if (holder && accumulated) { + } else if (_catchViewHolder && accumulated) { const timeoutNote = document.createElement('div'); timeoutNote.className = 'stopped-indicator'; timeoutNote.innerHTML = `<span style="color: var(--color-error);">[${timeoutMsg}]</span>`; - holder.querySelector('.body').appendChild(timeoutNote); + _catchViewHolder.querySelector('.body').appendChild(timeoutNote); } if (currentAbort === abortCtrl) currentAbort = null; return; @@ -3795,12 +4082,12 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr if (holder && !accumulated) { holder.querySelector('.body').innerHTML = `<div style="color: var(--color-error); font-style: italic; padding: 4px 0;">[${offlineMsg}]</div>`; - } else if (holder && accumulated) { + } else if (_catchViewHolder && accumulated) { const offlineNote = document.createElement('div'); offlineNote.className = 'stopped-indicator'; offlineNote.innerHTML = `<span style="color: var(--color-error);">[${offlineMsg}]</span>`; - holder.querySelector('.body').appendChild(offlineNote); + _catchViewHolder.querySelector('.body').appendChild(offlineNote); } if (currentAbort === abortCtrl) currentAbort = null; return; @@ -3811,12 +4098,12 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr if (holder && !accumulated) { holder.querySelector('.body').innerHTML = `<div style="color: var(--color-error); font-style: italic; padding: 4px 0;">[${recoveryMsg}]</div>`; - } else if (holder && accumulated) { + } else if (_catchViewHolder && accumulated) { const recoveryNote = document.createElement('div'); recoveryNote.className = 'stopped-indicator'; recoveryNote.innerHTML = `<span style="color: var(--color-error);">[${recoveryMsg}]</span>`; - holder.querySelector('.body').appendChild(recoveryNote); + _catchViewHolder.querySelector('.body').appendChild(recoveryNote); } if (currentAbort === abortCtrl) currentAbort = null; return; @@ -3827,11 +4114,11 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr if (holder && !accumulated) { holder.querySelector('.body').innerHTML = `<div style="opacity:0.7;font-style:italic;padding:4px 0;">[${staleMsg}]</div>`; - } else if (holder && accumulated) { + } else if (_catchViewHolder && accumulated) { const staleNote = document.createElement('div'); staleNote.className = 'stopped-indicator'; staleNote.innerHTML = `<span style="opacity:0.7;">[${staleMsg}]</span>`; - holder.querySelector('.body').appendChild(staleNote); + _catchViewHolder.querySelector('.body').appendChild(staleNote); } if (currentAbort === abortCtrl) currentAbort = null; return; @@ -3844,19 +4131,11 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr _renderCancelledBubble(holder); } - // But just in case the stop button didn't render it, render it here - if (holder && accumulated && !currentHolder) { - holder.dataset.raw = accumulated; - holder.querySelector('.body').innerHTML = markdownModule.processWithThinking( - markdownModule.squashOutsideCode(accumulated) - ); - - if (window.hljs) { - holder.querySelectorAll('pre code').forEach((block) => { - window.hljs.highlightElement(block); - }); - } - + // Navigation and non-button aborts do not pass through the synchronous + // Stop renderer. The catch render above owns markdown; add only the + // interruption controls here so each terminal path renders once. + if (_catchViewHolder && accumulated && currentHolder) { + _catchViewHolder.dataset.raw = accumulated; const stoppedIndicator = document.createElement('div'); stoppedIndicator.className = 'stopped-indicator'; const stoppedLabel = document.createElement('span'); @@ -3869,7 +4148,7 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr continueBtn.addEventListener('click', () => { stoppedIndicator.remove(); _hideUserBubble = true; - _pendingContinue = holder; + _pendingContinue = _catchViewHolder; const cutoff = accumulated; const msgInput = uiModule.el('message'); if (msgInput) { @@ -3879,14 +4158,14 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr } }); stoppedIndicator.appendChild(continueBtn); - holder.querySelector('.body').appendChild(stoppedIndicator); + _catchViewHolder.querySelector('.body').appendChild(stoppedIndicator); // Tell server to mark this message as stopped const _sid2 = sessionModule.getCurrentSessionId(); if (_sid2) fetch(`${API_BASE}/api/session/${_sid2}/mark-stopped`, { method: 'POST' }).catch(e => console.warn('mark-stopped failed:', e)); - if (!holder.querySelector('.msg-footer')) { - holder.appendChild(createMsgFooter(holder)); + if (!_catchViewHolder.querySelector('.msg-footer')) { + _catchViewHolder.appendChild(createMsgFooter(_catchViewHolder)); } uiModule.scrollHistory(); @@ -3912,8 +4191,8 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr // cap. Only auto-recover from connection-class failures; deterministic // errors (unsupported tools, 4xx/5xx, parse failures) surface right away // instead of burning the nudge budget on a guaranteed-to-fail retry. - if (!(_isRecoverableStreamErr(err) && _tryAutoRecover(holder, accumulated, streamSessionId))) { - const errorHolder = document.querySelector('.msg-ai:last-of-type .body'); + if (!(_isRecoverableStreamErr(err) && _tryAutoRecover(_catchViewHolder, accumulated, streamSessionId))) { + const errorHolder = _catchViewHolder?.querySelector('.body') || document.querySelector('.msg-ai:last-of-type .body'); if (errorHolder) { let errMsg = `Error: ${err.message}`; // Add hint for tool-call errors @@ -3926,6 +4205,7 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr } } } finally { + _cancelLiveThinkingWork(); clearResponseTimeout(); clearProcessingProbe(); clearFirstTokenWaitTimers(); @@ -4080,10 +4360,6 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr _autoNudges++; if (holder && accumulated) { holder.dataset.raw = accumulated; - try { - holder.querySelector('.body').innerHTML = - markdownModule.processWithThinking(markdownModule.squashOutsideCode(accumulated)); - } catch (_) {} } _pendingContinue = holder || null; // merge the continuation into the same bubble _hideUserBubble = true; // no user bubble for the handshake @@ -4206,7 +4482,10 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr * Called from both abort paths when no tokens had streamed yet. */ function _renderCancelledBubble(holder) { if (!holder) return; + if (holder.dataset.cancelledRendered === '1') return; + holder.dataset.cancelledRendered = '1'; holder.dataset.raw = ''; + holder.style.display = ''; const body = holder.querySelector('.body'); if (body) { body.innerHTML = ''; @@ -4262,6 +4541,10 @@ import { wireArrowUpRecall, getUserMessagesFromChatHistory } from './composerArr abortCurrentRequest(); return; } + // Detachment deliberately keeps the network stream alive, but the outgoing + // view must stop all delayed rendering immediately. The reader loop may not + // receive another SSE line for an arbitrary amount of time. + if (active.cancelViewWork) active.cancelViewWork(); // Store background stream state _backgroundStreams.set(sessionId, { status: 'running', diff --git a/static/js/liveThinkingThrottle.js b/static/js/liveThinkingThrottle.js new file mode 100644 index 000000000..ca73abc10 --- /dev/null +++ b/static/js/liveThinkingThrottle.js @@ -0,0 +1,206 @@ +// liveThinkingThrottle.js +// +// Pure trailing-edge coalescer for the live "thinking" block in chat.js. +// +// A reasoning stream delivers deltas far faster than a human can read them, and +// the only thing that matters on screen is the LATEST cumulative text. Committing +// every delta to the DOM makes the work grow with the length of the stream. This +// throttle collapses a burst of updates into one commit per `delay` ms, always +// carrying the most recent value. +// +// Timers are injected so the behaviour is testable without a browser or a clock: +// +// const throttle = createLiveThinkingThrottle(commit, { prepare, schedule, cancel }); +// +// Lifecycle contract, which the terminal paths in chat.js depend on: +// +// update(value) queue `value`; schedule a commit if one is not already pending +// flush() commit any pending value NOW and drop the timer; returns whether +// a commit happened, so a clean flush cannot duplicate a commit +// cancel() drop the timer AND the pending value — nothing lands later +// +// `cancel()` is what stops a finished (or backgrounded) stream from mutating a +// view the user has since navigated away to. + +export function stripLiveThinkingTags(text) { + return String(text ?? '').replace( + /<\/?(?:think(?:ing)?|thought)(?:\s+[^>]*)?>/gi, + '', + ); +} + +const THINKING_BOUNDARY_RE = /<\/?(?:(?:mm:)?think(?:ing)?|thought)(?:\s+[^>]*)?>|<\|channel>(?:thought|response)|<channel\|>/gi; +const REPLY_PREFIX_SOURCE = "(?:Hey|Hi |Hi!|Hello|Sure|Yes|No |No,|Yo|OK|Here|Absolutely|Of course|Great|Alright|Thanks|Welcome|Good |I'm happy|I'd be)"; +const REPLY_LINE_RE = new RegExp('(?:^|\\n)\\s*' + REPLY_PREFIX_SOURCE, 'gi'); +const REPLY_INLINE_RE = new RegExp('[.!?]\\s*' + REPLY_PREFIX_SOURCE, 'gi'); +const REASONING_PREFIX_CANDIDATES = [ + 'thinking:', 'thinking process:', 'the user ', 'user wants', 'we need ', + 'i need ', 'i should ', 'i will ', "i'll ", 'i am going ', 'let me think', + 'let me look', 'let me see', 'let me check', 'let me read', 'let me review', + 'let me analyze', 'let me parse', 'let me figure', 'let me draft', 'let me write', + 'they are ', 'the question ', 'i can ', +]; + +const DISPLAY_FILTER_BOUNDARY_RE = /\[\/?TOOL_CALL\]|```(?:create_document|documen(?:t)?)(?:\s|$)|```[\w-]+[ \t]*[\[{]|<(?:[\w]+:)?(?:tool_call|function_call)>|<invoke\b|<\s*\/?\s*[||]+\s*DSML\s*[||]+|(?:\[\s*)?\{\s*"function"\s*:|<\/?\|(?:assistant|assistan|user|system|tool|end)\|?>|(?:^|[\r\n])\s*(?:stdout|stderr|exit_code):/i; + +function hasFreshMatch(text, regex, cursor, minStart = 0) { + regex.lastIndex = 0; + for (const match of text.matchAll(regex)) { + const end = match.index + match[0].length; + if (end > cursor && match.index >= minStart) return true; + } + return false; +} + +// Incrementally decides when chat.js needs its compatibility-heavy cumulative +// thinking analysis. The gate inspects only a short overlap plus the new text; +// ordinary answer/reasoning deltas therefore stay O(delta) while split tags, +// namespaced tags, non-tag reply boundaries, and false-close grace deadlines +// still request the canonical full analysis. +export function createThinkingAnalysisGate({ + startsWithReasoningPrefix = () => false, + now = () => Date.now(), + overlap = 512, +} = {}) { + let cursor = 0; + let prefixSettled = false; + let prefixProbe = ''; + + return { + shouldAnalyze(text, { + isThinking = false, + nonTagThinking = false, + recheckAt = 0, + } = {}) { + const fullText = String(text ?? ''); + if (fullText.length < cursor) { + cursor = 0; + prefixSettled = false; + prefixProbe = ''; + } + const previousCursor = cursor; + if (!prefixSettled && prefixProbe.length < overlap) { + // Build the initial probe from deltas so arbitrary leading whitespace + // cannot strand the gate in its undecided state. The retained state is + // bounded even if a provider emits a very large whitespace prefix. + prefixProbe = (prefixProbe + fullText.slice(previousCursor)) + .trimStart() + .slice(0, overlap); + } + const scanStart = Math.max(0, previousCursor - overlap); + const freshText = fullText.slice(scanStart); + const relativeCursor = previousCursor - scanStart; + const hasBoundary = hasFreshMatch(freshText, THINKING_BOUNDARY_RE, relativeCursor); + const hasReplyBoundary = nonTagThinking && ( + hasFreshMatch(freshText, REPLY_LINE_RE, relativeCursor) + || hasFreshMatch(freshText, REPLY_INLINE_RE, relativeCursor, Math.max(0, 20 - scanStart)) + ); + cursor = fullText.length; + + if (hasBoundary || hasReplyBoundary) return true; + if (isThinking) return recheckAt > 0 && now() >= recheckAt; + if (prefixSettled) return false; + + if (!prefixProbe) return false; + if (startsWithReasoningPrefix(prefixProbe)) { + prefixSettled = true; + return true; + } + const lowerProbe = prefixProbe.toLowerCase(); + if (REASONING_PREFIX_CANDIDATES.some((candidate) => candidate.startsWith(lowerProbe))) { + return false; + } + prefixSettled = true; + return false; + }, + reset() { + cursor = 0; + prefixSettled = false; + prefixProbe = ''; + }, + }; +} + +// Keep the common prose path append-only. At the first structured/tool +// boundary, filter only the preceding visible prefix and hide the structured +// tail until the authoritative terminal render. +export function createIncrementalDisplayProjector(filter, { overlap = 512 } = {}) { + let projected = ''; + let boundaryTail = ''; + let rawLength = 0; + let structuredTailHidden = false; + + return { + append(delta, fullText) { + const chunk = String(delta ?? ''); + const raw = String(fullText ?? ''); + if (raw.length < rawLength) this.reset(); + const boundaryProbe = boundaryTail + chunk; + const boundaryMatch = !structuredTailHidden + ? DISPLAY_FILTER_BOUNDARY_RE.exec(boundaryProbe) + : null; + if (boundaryMatch) { + // Filter the visible prefix, not the incomplete marker itself: several + // compatibility regexes intentionally match only completed blocks. + const boundaryStart = Math.max(0, raw.length - boundaryProbe.length + boundaryMatch.index); + structuredTailHidden = true; + projected = String(filter(raw.slice(0, boundaryStart)) ?? ''); + } else if (!structuredTailHidden) { + projected += chunk; + } + boundaryTail = (boundaryTail + chunk).slice(-overlap); + rawLength = raw.length; + return projected; + }, + current() { + return projected; + }, + reset() { + projected = ''; + boundaryTail = ''; + rawLength = 0; + structuredTailHidden = false; + }, + }; +} + +export function createLiveThinkingThrottle(commit, { + delay = 100, + prepare = (value) => String(value ?? ''), + schedule = (callback, ms) => setTimeout(callback, ms), + cancel = (timer) => clearTimeout(timer), +} = {}) { + let timer = null; + let latest = null; + let dirty = false; + + const commitLatest = () => { + timer = null; + if (!dirty) return false; + dirty = false; + commit(prepare(latest)); + return true; + }; + + return { + update(value) { + latest = value; + dirty = true; + if (timer === null) timer = schedule(commitLatest, delay); + }, + flush() { + if (timer !== null) { + cancel(timer); + timer = null; + } + return commitLatest(); + }, + cancel() { + if (timer !== null) cancel(timer); + timer = null; + dirty = false; + }, + }; +} + +export default createLiveThinkingThrottle; diff --git a/tests/live_thinking_scheduler.test.mjs b/tests/live_thinking_scheduler.test.mjs new file mode 100644 index 000000000..e0da83945 --- /dev/null +++ b/tests/live_thinking_scheduler.test.mjs @@ -0,0 +1,277 @@ +// Tests for the live-thinking throttle that bounds DOM work during long +// reasoning streams (see static/js/liveThinkingThrottle.js). +// +// The throttle's contract is what the terminal paths in chat.js lean on: +// a burst of deltas becomes ONE commit carrying the latest text; flush() +// lands trailing text synchronously and cannot double-commit; cancel() +// guarantees nothing lands after a stream is finished or backgrounded. +// +// Timers are injected, so this runs with no DOM and no real clock. +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + createIncrementalDisplayProjector, + createLiveThinkingThrottle, + createThinkingAnalysisGate, + stripLiveThinkingTags, +} from '../static/js/liveThinkingThrottle.js'; + +function fakeTimers() { + let nextId = 1; + const callbacks = new Map(); + const delays = []; + return { + schedule(callback, delay) { + const id = nextId++; + callbacks.set(id, callback); + delays.push(delay); + return id; + }, + cancel(id) { + callbacks.delete(id); + }, + run(id) { + const callback = callbacks.get(id); + assert.ok(callback, `missing timer ${id}`); + callbacks.delete(id); + callback(); + }, + pendingIds() { + return [...callbacks.keys()]; + }, + delays, + }; +} + +test('coalesces a burst and commits only the latest text after 100 ms', () => { + const timers = fakeTimers(); + const commits = []; + const throttle = createLiveThinkingThrottle((value) => commits.push(value), timers); + + throttle.update('a'); + throttle.update('ab'); + throttle.update('abc'); + + assert.deepEqual(commits, []); + assert.deepEqual(timers.delays, [100], 'a burst must schedule exactly one commit'); + const [timer] = timers.pendingIds(); + timers.run(timer); + assert.deepEqual(commits, ['abc']); +}); + +test('commit count stays flat as the stream grows', () => { + const timers = fakeTimers(); + const commits = []; + const throttle = createLiveThinkingThrottle((value) => commits.push(value), timers); + + // 500 deltas arriving inside one window is the regression this guards: + // the old code committed once per delta, so work grew with stream length. + let text = ''; + for (let i = 0; i < 500; i++) { + text += 'token '; + throttle.update(text); + } + assert.deepEqual(commits, []); + assert.equal(timers.pendingIds().length, 1); + timers.run(timers.pendingIds()[0]); + assert.equal(commits.length, 1); + assert.equal(commits[0], text); +}); + +test('prepares a 200K cumulative stream only at scheduled commit cadence', () => { + const timers = fakeTimers(); + const commits = []; + let prepareCalls = 0; + let scannedCharacters = 0; + const throttle = createLiveThinkingThrottle((value) => commits.push(value), { + ...timers, + prepare(value) { + prepareCalls += 1; + scannedCharacters += value.length; + return stripLiveThinkingTags(value); + }, + }); + + const delta = 'reasoning '.repeat(10); // 100 characters + let cumulative = ''; + for (let i = 0; i < 2000; i++) { + cumulative += delta; + throttle.update(cumulative); + } + + assert.equal(cumulative.length, 200_000); + assert.equal(prepareCalls, 0, 'cumulative extraction must not run per delta'); + assert.equal(timers.pendingIds().length, 1); + timers.run(timers.pendingIds()[0]); + assert.equal(prepareCalls, 1); + assert.equal(scannedCharacters, 200_000); + assert.deepEqual(commits, [cumulative]); +}); + +test('ordinary answers and reasoning deltas do not request cumulative analysis', () => { + const startsReasoning = (text) => /^\s*thinking(?:\s+process)?\s*:/i.test(text); + const ordinaryGate = createThinkingAnalysisGate({ startsWithReasoningPrefix: startsReasoning }); + let ordinary = ''; + let ordinaryAnalyses = 0; + for (let i = 0; i < 2000; i++) { + ordinary += i === 0 ? 'Here is the answer. ' : 'answer '.repeat(10); + if (ordinaryGate.shouldAnalyze(ordinary)) ordinaryAnalyses += 1; + } + assert.equal(ordinaryAnalyses, 0); + + const thinkingGate = createThinkingAnalysisGate({ startsWithReasoningPrefix: startsReasoning }); + let thinking = 'Thin'; + assert.equal(thinkingGate.shouldAnalyze(thinking), false); + thinking += 'king: inspect the problem'; + assert.equal(thinkingGate.shouldAnalyze(thinking), true); + for (let i = 0; i < 2000; i++) { + thinking += ' reasoning'.repeat(10); + assert.equal(thinkingGate.shouldAnalyze(thinking, { isThinking: true, nonTagThinking: true }), false); + } + thinking += '\n\nHere is the answer'; + assert.equal(thinkingGate.shouldAnalyze(thinking, { isThinking: true, nonTagThinking: true }), true); + + const whitespaceGate = createThinkingAnalysisGate({ startsWithReasoningPrefix: startsReasoning }); + let whitespaceThinking = ' '.repeat(250); + assert.equal(whitespaceGate.shouldAnalyze(whitespaceThinking), false); + whitespaceThinking += 'Thinking: bounded probe'; + assert.equal(whitespaceGate.shouldAnalyze(whitespaceThinking), true); +}); + +test('split namespaced closes and false-close deadlines request analysis', () => { + let clock = 100; + const gate = createThinkingAnalysisGate({ now: () => clock }); + let text = '<mm:think>x</mm:'; + assert.equal(gate.shouldAnalyze(text, { isThinking: true }), true, 'fresh opening tag is analyzed'); + text += 'think>answer'; + assert.equal(gate.shouldAnalyze(text, { isThinking: true }), true, 'split namespaced close is analyzed'); + + text += ' still waiting'; + assert.equal(gate.shouldAnalyze(text, { isThinking: true, recheckAt: 500 }), false); + clock = 500; + text += ' next delta'; + assert.equal(gate.shouldAnalyze(text, { isThinking: true, recheckAt: 500 }), true); + + const attributedGate = createThinkingAnalysisGate(); + let attributed = `<think data-provider="${'x'.repeat(400)}"`; + assert.equal(attributedGate.shouldAnalyze(attributed), false); + attributed += '>reasoning'; + assert.equal(attributedGate.shouldAnalyze(attributed), true, 'bounded carry preserves split tag attributes'); +}); + +test('display projection is append-only and filters a structured tail once', () => { + let filterCalls = 0; + let filteredCharacters = 0; + const projector = createIncrementalDisplayProjector((text) => { + filterCalls += 1; + filteredCharacters += text.length; + return text.replace(/\[TOOL_CALL\][\s\S]*$/i, ''); + }); + + let text = ''; + for (let i = 0; i < 2000; i++) { + const delta = i === 0 ? 'Here is the answer. ' : 'ordinary text '; + text += delta; + assert.equal(projector.append(delta, text), text); + } + assert.equal(filterCalls, 0, 'ordinary deltas never run the cumulative filter'); + + text += '[TOOL_'; + projector.append('[TOOL_', text); + text += 'CALL]{"name":"read"}'; + const beforeToolPayload = projector.append('CALL]{"name":"read"}', text); + for (let i = 0; i < 2000; i++) { + const delta = 'payload '; + text += delta; + assert.equal(projector.append(delta, text), beforeToolPayload); + } + assert.equal(filterCalls, 1, 'structured payload filtering happens only at its boundary'); + assert.ok(filteredCharacters < text.length, 'filter work is bounded by the first structured boundary'); +}); + +test('literal escaped tags survive and malformed live tags retain trailing text', () => { + assert.equal( + stripLiveThinkingTags('<think>literal</think>'), + '<think>literal</think>', + ); + assert.equal( + stripLiveThinkingTags('<think>first</think> middle <thinking mode="deep">trailing'), + 'first middle trailing', + ); + assert.equal(stripLiveThinkingTags('answer with 2 < 3 and 5 > 4'), 'answer with 2 < 3 and 5 > 4'); +}); + +test('terminal flush prepares and commits the complete trailing cumulative text', () => { + const timers = fakeTimers(); + const commits = []; + const throttle = createLiveThinkingThrottle((value) => commits.push(value), { + ...timers, + prepare: stripLiveThinkingTags, + }); + + throttle.update('<think>reasoning without a closing tag'); + assert.equal(throttle.flush(), true); + assert.deepEqual(commits, ['reasoning without a closing tag']); + assert.deepEqual(timers.pendingIds(), []); +}); + +test('independent throttles cannot commit cancelled text into another session', () => { + const timers = fakeTimers(); + const commits = []; + const first = createLiveThinkingThrottle((value) => commits.push(['first', value]), timers); + const second = createLiveThinkingThrottle((value) => commits.push(['second', value]), timers); + + first.update('stale first-session text'); + second.update('current second-session text'); + first.cancel(); + assert.equal(second.flush(), true); + + assert.deepEqual(timers.pendingIds(), []); + assert.deepEqual(commits, [['second', 'current second-session text']]); +}); + +test('flush synchronously preserves trailing text and cancels the pending callback', () => { + const timers = fakeTimers(); + const commits = []; + const throttle = createLiveThinkingThrottle((value) => commits.push(value), timers); + + throttle.update('trailing text'); + assert.equal(throttle.flush(), true); + assert.deepEqual(commits, ['trailing text']); + assert.deepEqual(timers.pendingIds(), []); + assert.equal(throttle.flush(), false, 'clean flush must not duplicate the commit'); +}); + +test('cancel discards pending work without a late DOM commit', () => { + const timers = fakeTimers(); + const commits = []; + const throttle = createLiveThinkingThrottle((value) => commits.push(value), timers); + + throttle.update('stale session text'); + throttle.cancel(); + assert.deepEqual(timers.pendingIds(), []); + assert.deepEqual(commits, []); +}); + +test('a cancelled throttle accepts new work again', () => { + const timers = fakeTimers(); + const commits = []; + const throttle = createLiveThinkingThrottle((value) => commits.push(value), timers); + + throttle.update('discarded'); + throttle.cancel(); + throttle.update('fresh'); + assert.equal(throttle.flush(), true); + assert.deepEqual(commits, ['fresh']); +}); + +test('coerces nullish updates instead of committing undefined', () => { + const timers = fakeTimers(); + const commits = []; + const throttle = createLiveThinkingThrottle((value) => commits.push(value), timers); + + throttle.update(null); + throttle.flush(); + assert.deepEqual(commits, ['']); +}); diff --git a/tests/test_chat_stream_scope.py b/tests/test_chat_stream_scope.py index a726c776d..efd482d2f 100644 --- a/tests/test_chat_stream_scope.py +++ b/tests/test_chat_stream_scope.py @@ -1,3 +1,4 @@ +import re from pathlib import Path @@ -13,7 +14,7 @@ def test_stream_render_helpers_are_visible_to_catch_block(): assert "let _cancelThinkingTimer = () => {};" in outer_scope assert "let _removeThinkingSpinner = () => {};" in outer_scope - assert "_renderStream = () => {" in try_body + assert re.search(r"(?m)^\s*_renderStream\s*=", try_body) assert "_cancelThinkingTimer = () => {" in try_body assert "_removeThinkingSpinner = () => {" in try_body assert "function _renderStream()" not in try_body diff --git a/tests/test_live_thinking_chat_integration.py b/tests/test_live_thinking_chat_integration.py new file mode 100644 index 000000000..13cc0d230 --- /dev/null +++ b/tests/test_live_thinking_chat_integration.py @@ -0,0 +1,145 @@ +"""Source-level wiring guards for live-thinking stream lifecycle. + +The pure scheduler suite covers timing behavior. These assertions pin the +browser-only integration seams that are impractical to import without the full +application DOM. +""" + +from pathlib import Path + + +_CHAT = (Path(__file__).resolve().parent.parent / "static" / "js" / "chat.js").read_text( + encoding="utf-8" +) + + +def _between(start: str, end: str) -> str: + return _CHAT.split(start, 1)[1].split(end, 1)[0] + + +def test_in_thinking_delta_short_circuits_before_cumulative_normalization(): + delta_handler = _between( + "let _delta = json.delta;", + "} else if (json.type === 'research_progress')", + ) + delta_path = _between( + "// Detect thinking-in-progress:", + "} else if (json.type === 'research_progress')", + ) + guard = "if (!_thinkingAnalysisGate.shouldAnalyze(roundText, {" + normalize = "markdownModule.normalizeThinkingMarkup(roundText)" + assert guard in delta_path + assert delta_path.index(guard) < delta_path.index(normalize) + assert "_queueLiveThinking(roundText);" in delta_path + assert "createThinkingAnalysisGate" in _CHAT + projector_append = "_roundDisplayProjector.append(_delta, roundText);" + assert projector_append in delta_handler + assert delta_handler.index(projector_append) < delta_handler.index(guard) + assert "_renderStream({ knownNormal: true, displayText: _roundDisplayProjector.current() });" in delta_path + assert "_replyDisplayProjector.append(_delta, roundReplyText)" in delta_path + + +def test_short_close_grace_expires_without_another_delta(): + assert "function _scheduleThinkingGrace()" in _CHAT + grace = _between( + "function _scheduleThinkingGrace()", + "function _replyAfterClosedThinking", + ) + assert "setTimeout(() =>" in grace + assert "_finishLiveThinkingTransition();" in grace + cancel = _between("_cancelLiveThinkingWork = () =>", "function _finalizeLiveThinking") + assert "_cancelThinkingGrace();" in cancel + delta_path = _between( + "// Detect thinking-in-progress:", + "} else if (json.type === 'research_progress')", + ) + false_close = _between( + "// Detect false close:", + "if (hasUnclosedThink && !isThinking)", + ) + assert "Do NOT require a prior unclosed delta" in false_close + assert "_afterClose &&" in false_close + assert "&& isThinking" not in false_close.split("let _falseCloseDeadline", 1)[1].split("if (isThinking)", 1)[0] + assert "_thinkingRecheckAt = _falseCloseDeadline || 0;" in delta_path + + +def test_terminal_paths_use_one_authoritative_rich_round_render(): + tool_path = _between( + "} else if (json.type === 'tool_start') {", + "} else if (json.type === 'tool_output') {", + ) + assert "_endLiveThinkingSection({ rich: false });" in tool_path + assert tool_path.count("_finalizeRoundRender();") == 1 + assert "_renderStream();" not in tool_path + + agent_path = _between( + "} else if (json.type === 'agent_step') {", + "} else if (json.type === 'budget_exceeded') {", + ) + assert "_endLiveThinkingSection({ rich: false });" in agent_path + assert agent_path.count("_finalizeRoundRender();") == 1 + assert "if (!roundFinalized)" not in agent_path + + catch_path = _between( + "// foreground session's text.\n const _isBgCatch", + "} finally {", + ) + assert "if (_isBgCatch)" in catch_path + assert "_cancelLiveThinkingWork();" in catch_path + assert "_catchTerminalView = _finalizeInterruptedView();" in catch_path + assert "_finalizeRoundRender();" not in catch_path + assert "_endThinkingOnTerminalPath({ rich: false });" in catch_path + assert "const _catchViewHolder = _catchTerminalView?.holder || holder;" in catch_path + + round_finalizer = _between( + "_finalizeRoundRender = () => {", + "_finalizeInterruptedView = () => {", + ) + assert "if (roundFinalized) return roundFinalization;" in round_finalizer + assert round_finalizer.index("processWithThinking") < round_finalizer.rindex("roundFinalized = true;") + assert "lastContentRoundHolder = terminalHolder;" in round_finalizer + + interrupted_finalizer = _between( + "_finalizeInterruptedView = () => {", + "function _replyAfterClosedThinking", + ) + assert "finalization?.hasContent" in interrupted_finalizer + assert "lastContentRoundHolder || finalization?.holder" in interrupted_finalizer + + stop_path = _between( + "// Render whatever was accumulated so far", + "// Reset button state", + ) + assert "const _stoppedViewHolder = _terminalView?.holder || currentHolder;" in stop_path + assert "_stoppedViewHolder.querySelector('.body').appendChild(stoppedIndicator);" in stop_path + + done_path = _between( + "if (data === '[DONE]') {", + "try {\n const json = JSON.parse(data);", + ) + assert "_finalizeLiveThinking(_closedThinkingText(roundText), false);" in done_path + assert "_renderStream();" not in done_path + + post_loop = _between( + "if (!_streamSawDone) {", + "// --- Final render (skip if stream was ever backgrounded or currently in background) ---", + ) + assert "_cancelLiveThinkingWork();" in post_loop + assert "_renderStream();" not in post_loop + + recovery_path = _between( + "function _tryAutoRecover(holder, accumulated, sessionId)", + "function _removeStallBanner()", + ) + assert "processWithThinking" not in recovery_path + + +def test_detach_synchronously_cancels_delayed_view_work(): + registration = _between("_activeStreams.set(streamSessionId", "_syncForegroundStreamGlobals();") + assert "cancelViewWork: () => _cancelLiveThinkingWork()" in registration + + detach = _between("export function detachCurrentStream", "// _notifyStreamComplete") + cancel = "if (active.cancelViewWork) active.cancelViewWork();" + background = "_backgroundStreams.set(sessionId" + assert cancel in detach + assert detach.index(cancel) < detach.index(background) diff --git a/tests/test_live_thinking_scheduler_js.py b/tests/test_live_thinking_scheduler_js.py new file mode 100644 index 000000000..aa1f5cecb --- /dev/null +++ b/tests/test_live_thinking_scheduler_js.py @@ -0,0 +1,29 @@ +"""Runs the live-thinking throttle's behavioral suite under pytest. + +Behavior lives in tests/live_thinking_scheduler.test.mjs (node:test, no DOM). +This wrapper only exists so the JS suite runs in the normal pytest job. +""" + +import shutil +import subprocess +from pathlib import Path + +import pytest + +_REPO = Path(__file__).resolve().parent.parent +_HAS_NODE = shutil.which("node") is not None + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_live_thinking_scheduler_behavior(): + result = subprocess.run( + ["node", "--test", "tests/live_thinking_scheduler.test.mjs"], + cwd=_REPO, + capture_output=True, + timeout=30, + text=True, + ) + if result.returncode != 0: + raise AssertionError( + f"node --test failed:\nSTDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}" + ) From c00ef8f9c25b4161fadaf46e5f28d2692906d5cd Mon Sep 17 00:00:00 2001 From: "Tal.Yuan" <ydonghao@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:24:55 +0800 Subject: [PATCH 051/125] refactor(routes): move mcp domain into routes/mcp/ subpackage (#5899) Slice 2o of the route-domain reorganization (#4082/#4071). Moves mcp_routes.py (697 lines) into routes/mcp/, leaving a backward-compat sys.modules shim. Pure file reorganization, no behavior change. The shim uses sys.modules replacement so sys.modules.pop + re-import, monkeypatch.setattr(mcp_routes, "MCP_OAUTH_DIR", ...), and __file__ introspection in test_security_regressions.py all reach the canonical module. One source-introspection path string repointed (line 1001). Canonical module imports only from core/, src/, and stdlib (zero internal routes/ coupling). Adds tests/test_mcp_routes_shim.py. Verified: compileall clean; full suite 4804 passed, 3 skipped. --- app.py | 2 +- routes/mcp/__init__.py | 5 + routes/mcp/mcp_routes.py | 697 ++++++++++++++++++++++++++++ routes/mcp_routes.py | 707 +---------------------------- tests/test_mcp_routes_shim.py | 19 + tests/test_security_regressions.py | 2 +- 6 files changed, 737 insertions(+), 695 deletions(-) create mode 100644 routes/mcp/__init__.py create mode 100644 routes/mcp/mcp_routes.py create mode 100644 tests/test_mcp_routes_shim.py diff --git a/app.py b/app.py index 8363ba4e9..bee4dae8f 100644 --- a/app.py +++ b/app.py @@ -805,7 +805,7 @@ app.include_router(setup_font_routes()) # MCP (Model Context Protocol) from src.mcp_manager import McpManager from src.agent_tools import set_mcp_manager -from routes.mcp_routes import setup_mcp_routes +from routes.mcp.mcp_routes import setup_mcp_routes mcp_manager = McpManager() set_mcp_manager(mcp_manager) diff --git a/routes/mcp/__init__.py b/routes/mcp/__init__.py new file mode 100644 index 000000000..bb445ddcc --- /dev/null +++ b/routes/mcp/__init__.py @@ -0,0 +1,5 @@ +"""MCP route domain package (slice 2o, #4082/#4071). + +Contains mcp_routes.py, migrated from the flat routes/ directory. +Backward-compat shim at routes/mcp_routes.py re-exports from here. +""" diff --git a/routes/mcp/mcp_routes.py b/routes/mcp/mcp_routes.py new file mode 100644 index 000000000..a0ade88b6 --- /dev/null +++ b/routes/mcp/mcp_routes.py @@ -0,0 +1,697 @@ +# routes/mcp_routes.py +"""MCP (Model Context Protocol) server management routes.""" +import json +import os +import uuid +import urllib.parse +import html +from pathlib import Path +from fastapi import APIRouter, Form, HTTPException, Request +from fastapi.responses import RedirectResponse, HTMLResponse +import logging +import httpx + +from core.database import McpServer, SessionLocal +from core.middleware import require_admin +from src.constants import DATA_DIR, MCP_OAUTH_DIR +from src.mcp_manager import McpManager + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/api/mcp", tags=["mcp"]) + + +def _mcp_oauth_base_dir() -> Path: + """Directory that may contain OAuth files managed by Odysseus.""" + return Path(MCP_OAUTH_DIR).resolve(strict=False) + + +def _resolve_mcp_oauth_path(raw_path, field_name: str) -> str: + """Resolve an MCP OAuth path and keep it under DATA_DIR/mcp_oauth.""" + raw = str(raw_path or "").strip() + if not raw: + return "" + + base = _mcp_oauth_base_dir() + path = Path(os.path.expanduser(raw)) + if not path.is_absolute(): + path = base / path + resolved = path.resolve(strict=False) + + try: + resolved.relative_to(base) + except ValueError as exc: + raise HTTPException( + 400, + f"Invalid OAuth {field_name}: path must stay under {base}", + ) from exc + return str(resolved) + + +def _sanitize_mcp_oauth_config(oauth_cfg): + """Return an OAuth config copy with file paths confined to mcp_oauth.""" + if not oauth_cfg: + return oauth_cfg + if not isinstance(oauth_cfg, dict): + return {} + sanitized = dict(oauth_cfg) + for field_name in ("keys_file", "token_file"): + if sanitized.get(field_name): + sanitized[field_name] = _resolve_mcp_oauth_path( + sanitized[field_name], + field_name, + ) + return sanitized + + +def _mcp_oauth_token_missing(oauth_cfg, *, strict: bool = True) -> bool: + """Check token existence without letting legacy bad paths break listing.""" + if not isinstance(oauth_cfg, dict): + return False + try: + token_file = _resolve_mcp_oauth_path(oauth_cfg.get("token_file", ""), "token_file") + except HTTPException: + if strict: + raise + logger.warning("Ignoring MCP OAuth config with unsafe token_file") + return True + return bool(token_file and not os.path.exists(token_file)) + + +def _apply_mcp_oauth_env(env: dict, oauth_cfg) -> None: + """Pass sanitized Gmail package paths to MCP servers that honor them.""" + if not oauth_cfg or not isinstance(env, dict): + return + keys_file = oauth_cfg.get("keys_file") + token_file = oauth_cfg.get("token_file") + if keys_file: + env["GMAIL_OAUTH_PATH"] = keys_file + if token_file: + env["GMAIL_CREDENTIALS_PATH"] = token_file + + +def _load_disabled_map(): + """Load per-server disabled tool sets from DB.""" + db = SessionLocal() + try: + disabled_map = {} + for srv in db.query(McpServer).all(): + if srv.disabled_tools: + try: + names = json.loads(srv.disabled_tools) + if names: + disabled_map[srv.id] = set(names) + except (json.JSONDecodeError, TypeError): + pass + return disabled_map + finally: + db.close() + + +def _mcp_oauth_redirect_uri() -> str: + """Shared callback URL for legacy Google and generic MCP OAuth flows.""" + from src.mcp_oauth import REDIRECT_URI + return REDIRECT_URI + + +def setup_mcp_routes(mcp_manager: McpManager): + """Setup MCP routes with the provided manager.""" + + @router.get("/servers") + def list_servers(request: Request): + """List all configured MCP servers with connection status.""" + require_admin(request) + db = SessionLocal() + try: + servers = db.query(McpServer).all() + result = [] + for srv in servers: + status = mcp_manager.get_server_status(srv.id) + oauth_cfg = json.loads(srv.oauth_config) if srv.oauth_config else None + needs_oauth = False + if oauth_cfg: + needs_oauth = _mcp_oauth_token_missing(oauth_cfg, strict=False) + disabled_list = json.loads(srv.disabled_tools) if srv.disabled_tools else [] + total_tools = status.get("tool_count", 0) + result.append({ + "id": srv.id, + "name": srv.name, + "transport": srv.transport, + "command": srv.command, + "args": json.loads(srv.args) if srv.args else [], + "env": json.loads(srv.env) if srv.env else {}, + "url": srv.url, + "is_enabled": srv.is_enabled, + "status": status.get("status", "disconnected"), + "tool_count": total_tools, + "disabled_tool_count": len(disabled_list), + "enabled_tool_count": max(0, total_tools - len(disabled_list)), + "error": status.get("error"), + "auth_url": status.get("auth_url"), + "has_oauth": oauth_cfg is not None, + "needs_oauth": needs_oauth, + }) + return result + finally: + db.close() + + @router.post("/servers") + async def add_server( + request: Request, + name: str = Form(...), + transport: str = Form("stdio"), + command: str = Form(None), + args: str = Form("[]"), + env: str = Form("{}"), + url: str = Form(None), + oauth_file: str = Form(None), + oauth_config: str = Form(None), + ): + """Add a new MCP server config and attempt connection. Admin-only: + registering a stdio server is equivalent to executing arbitrary + binaries on the host.""" + require_admin(request) + server_id = str(uuid.uuid4())[:8] + + # Validate + if transport == "stdio" and not command: + raise HTTPException(400, "command is required for stdio transport") + if transport == "sse" and not url: + raise HTTPException(400, "url is required for SSE transport") + if transport == "http" and not url: + raise HTTPException(400, "url is required for HTTP transport") + + # Parse JSON fields + try: + parsed_args = json.loads(args) if args else [] + except json.JSONDecodeError: + parsed_args = [] + try: + parsed_env = json.loads(env) if env else {} + except json.JSONDecodeError: + parsed_env = {} + if not isinstance(parsed_env, dict): + parsed_env = {} + + # Parse OAuth config + parsed_oauth_config = None + if oauth_config: + try: + parsed_oauth_config = _sanitize_mcp_oauth_config(json.loads(oauth_config)) + except json.JSONDecodeError: + pass + _apply_mcp_oauth_env(parsed_env, parsed_oauth_config) + + # Write OAuth credentials file if provided (for Google MCP servers) + logger.info(f"MCP add_server: oauth_file={oauth_file!r}") + if oauth_file: + try: + oauth_data = json.loads(oauth_file) + oauth_dir = _resolve_mcp_oauth_path(oauth_data.get("dir", ""), "dir") + oauth_filename = oauth_data.get("filename", "") + client_id = oauth_data.get("client_id", "") + client_secret = oauth_data.get("client_secret", "") + if oauth_dir and oauth_filename and client_id and client_secret: + filepath = _resolve_mcp_oauth_path( + Path(oauth_dir) / str(oauth_filename), + "filename", + ) + os.makedirs(os.path.dirname(filepath), exist_ok=True) + creds = { + "installed": { + "client_id": client_id, + "client_secret": client_secret, + "redirect_uris": ["http://localhost"], + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://accounts.google.com/o/oauth2/token", + } + } + with open(filepath, "w", encoding="utf-8") as f: + json.dump(creds, f, indent=2) + logger.info(f"Wrote OAuth credentials to {filepath}") + parsed_env.pop("GOOGLE_CLIENT_ID", None) + parsed_env.pop("GOOGLE_CLIENT_SECRET", None) + except (json.JSONDecodeError, OSError) as e: + logger.warning(f"Failed to write OAuth file: {e}") + + # Save to DB + db = SessionLocal() + try: + srv = McpServer( + id=server_id, + name=name, + transport=transport, + command=command, + args=json.dumps(parsed_args), + env=json.dumps(parsed_env), + url=url, + is_enabled=True, + oauth_config=json.dumps(parsed_oauth_config) if parsed_oauth_config else None, + ) + db.add(srv) + db.commit() + finally: + db.close() + + # Check if OAuth token already exists — skip connection attempt if not + needs_oauth = False + if parsed_oauth_config: + needs_oauth = _mcp_oauth_token_missing(parsed_oauth_config) + + connected = False + if not needs_oauth: + connected = await mcp_manager.connect_server( + server_id=server_id, + name=name, + transport=transport, + command=command, + args=parsed_args, + env=parsed_env, + url=url, + ) + + status = mcp_manager.get_server_status(server_id) + needs_auth = status.get("status") == "needs_auth" + return { + "id": server_id, + "name": name, + "connected": connected, + "status": "needs_oauth" if needs_oauth else status.get("status", "disconnected"), + "tool_count": status.get("tool_count", 0), + "error": "OAuth authorization required" if needs_oauth else status.get("error"), + "needs_oauth": needs_oauth, + "needs_auth": needs_auth, + "auth_url": status.get("auth_url"), + } + + @router.post("/servers/{server_id}/reconnect") + async def reconnect_server(server_id: str, request: Request): + """Reconnect to an MCP server.""" + require_admin(request) + db = SessionLocal() + try: + srv = db.query(McpServer).filter(McpServer.id == server_id).first() + if not srv: + raise HTTPException(404, "Server not found") + + await mcp_manager.disconnect_server(server_id) + + args = json.loads(srv.args) if srv.args else [] + env = json.loads(srv.env) if srv.env else {} + connected = await mcp_manager.connect_server( + server_id=server_id, + name=srv.name, + transport=srv.transport, + command=srv.command, + args=args, + env=env, + url=srv.url, + ) + + status = mcp_manager.get_server_status(server_id) + return { + "connected": connected, + "status": status.get("status", "disconnected"), + "tool_count": status.get("tool_count", 0), + "error": status.get("error"), + "auth_url": status.get("auth_url"), + "needs_auth": status.get("status") == "needs_auth", + } + finally: + db.close() + + @router.patch("/servers/{server_id}") + async def toggle_server(server_id: str, request: Request, is_enabled: str = Form(...)): + """Enable or disable an MCP server.""" + require_admin(request) + db = SessionLocal() + try: + srv = db.query(McpServer).filter(McpServer.id == server_id).first() + if not srv: + raise HTTPException(404, "Server not found") + + enabled = str(is_enabled).lower() == "true" + srv.is_enabled = enabled + db.commit() + + if enabled: + args = json.loads(srv.args) if srv.args else [] + env = json.loads(srv.env) if srv.env else {} + await mcp_manager.connect_server( + server_id=server_id, + name=srv.name, + transport=srv.transport, + command=srv.command, + args=args, + env=env, + url=srv.url, + ) + else: + await mcp_manager.disconnect_server(server_id) + + return {"id": server_id, "is_enabled": enabled} + finally: + db.close() + + @router.delete("/servers/{server_id}") + async def delete_server(server_id: str, request: Request): + """Remove an MCP server.""" + require_admin(request) + db = SessionLocal() + try: + srv = db.query(McpServer).filter(McpServer.id == server_id).first() + if not srv: + raise HTTPException(404, "Server not found") + + await mcp_manager.disconnect_server(server_id) + + db.delete(srv) + db.commit() + return {"status": "deleted"} + finally: + db.close() + + @router.get("/tools") + def list_tools(request: Request): + """List all discovered MCP tools across all connected servers.""" + require_admin(request) + disabled_map = _load_disabled_map() + return mcp_manager.get_all_tools(disabled_map) + + @router.get("/servers/{server_id}/tools") + def list_server_tools(server_id: str, request: Request): + """List all tools for a specific MCP server with enabled/disabled state.""" + require_admin(request) + db = SessionLocal() + try: + srv = db.query(McpServer).filter(McpServer.id == server_id).first() + if not srv: + raise HTTPException(404, "Server not found") + disabled_list = json.loads(srv.disabled_tools) if srv.disabled_tools else [] + disabled_set = set(disabled_list) + finally: + db.close() + + all_tools = mcp_manager.get_all_tools() + server_tools = [t for t in all_tools if t["server_id"] == server_id] + for t in server_tools: + t["is_disabled"] = t["name"] in disabled_set + return server_tools + + @router.patch("/servers/{server_id}/tools") + async def update_disabled_tools(server_id: str, request: Request): + """Bulk update disabled tools list for a server. + + Expects JSON body: {"disabled": ["tool_name_1", "tool_name_2"]} + """ + require_admin(request) + db = SessionLocal() + try: + srv = db.query(McpServer).filter(McpServer.id == server_id).first() + if not srv: + raise HTTPException(404, "Server not found") + + body = await request.json() + disabled = body.get("disabled", []) + if not isinstance(disabled, list): + raise HTTPException(400, "disabled must be a list of tool names") + + srv.disabled_tools = json.dumps(disabled) if disabled else None + db.commit() + + return {"id": server_id, "disabled_count": len(disabled)} + finally: + db.close() + + # ── OAuth flow for Google MCP servers ────────────────────────── + + @router.get("/oauth/authorize/{server_id}") + def oauth_authorize(server_id: str, request: Request): + """Show OAuth authorization page with Google sign-in link.""" + require_admin(request) + db = SessionLocal() + try: + srv = db.query(McpServer).filter(McpServer.id == server_id).first() + if not srv: + raise HTTPException(404, "Server not found") + if not srv.oauth_config: + raise HTTPException(400, "Server has no OAuth config") + + oauth_cfg = _sanitize_mcp_oauth_config(json.loads(srv.oauth_config)) + keys_file = oauth_cfg.get("keys_file", "") + if not keys_file or not os.path.exists(keys_file): + raise HTTPException(400, "OAuth keys file not found") + + with open(keys_file, encoding="utf-8") as f: + keys_data = json.load(f) + keys = keys_data.get("installed") or keys_data.get("web") + if not keys: + raise HTTPException(400, "Invalid OAuth keys file format") + + client_id = keys["client_id"] + scopes = oauth_cfg.get("scopes", []) + + # For Desktop App creds, default to localhost — the user will + # paste the resulting URL back if they're on a different device. + redirect_uri = _mcp_oauth_redirect_uri() + + params = { + "client_id": client_id, + "redirect_uri": redirect_uri, + "response_type": "code", + "scope": " ".join(scopes), + "access_type": "offline", + "prompt": "consent", + "state": server_id, + } + auth_url = "https://accounts.google.com/o/oauth2/v2/auth?" + urllib.parse.urlencode(params) + + # Determine if user is accessing from the same machine + host = request.headers.get("host", "") + is_local = host.startswith("localhost") or host.startswith("127.0.0.1") + + if is_local: + # Same machine — just redirect, callback will work directly + return RedirectResponse(auth_url) + else: + # Remote device — show paste-back page + return HTMLResponse(_oauth_authorize_page(auth_url, server_id, host, redirect_uri)) + finally: + db.close() + + @router.get("/oauth/callback") + async def oauth_callback(code: str, state: str, request: Request): + """Handle OAuth callback. Generic MCP OAuth flows resolve via the + pending-state registry; Google flows fall through to the legacy path.""" + require_admin(request) + from src.mcp_oauth import resolve_pending + if resolve_pending(state, code): + return HTMLResponse(_oauth_result_page( + "Authorization Successful", + "The MCP server is connecting. You can close this window and return to Odysseus.", + success=True, + )) + # Legacy Google path: state is the server_id + return await _exchange_and_connect(state, code, request) + + @router.post("/oauth/exchange/{server_id}") + async def oauth_exchange(server_id: str, request: Request, callback_url: str = Form(...)): + """Manual code exchange — user pastes the callback URL from their browser.""" + require_admin(request) + try: + parsed = urllib.parse.urlparse(callback_url) + params = urllib.parse.parse_qs(parsed.query) + code = params.get("code", [None])[0] + if not code: + return HTMLResponse(_oauth_result_page("Error", "No authorization code found in the URL. Make sure you copied the full URL from your browser."), status_code=400) + except Exception: + return HTMLResponse(_oauth_result_page("Error", "Invalid URL format."), status_code=400) + + # Generic MCP OAuth: if the pasted URL carries a state we are waiting on, + # resolve it directly (the background connect finishes the handshake). + state = params.get("state", [None])[0] + from src.mcp_oauth import resolve_pending + if state and resolve_pending(state, code): + return HTMLResponse(_oauth_result_page( + "Authorization Successful", + "The MCP server is connecting. You can close this window and return to Odysseus.", + success=True, + )) + + return await _exchange_and_connect(server_id, code, request) + + async def _exchange_and_connect(server_id: str, code: str, request: Request): + """Exchange auth code for tokens and connect the MCP server.""" + db = SessionLocal() + try: + srv = db.query(McpServer).filter(McpServer.id == server_id).first() + if not srv: + return HTMLResponse(_oauth_result_page("Error", "Server not found."), status_code=404) + if not srv.oauth_config: + return HTMLResponse(_oauth_result_page("Error", "No OAuth config."), status_code=400) + + oauth_cfg = _sanitize_mcp_oauth_config(json.loads(srv.oauth_config)) + keys_file = oauth_cfg.get("keys_file", "") + token_file = oauth_cfg.get("token_file", "") + if not keys_file or not token_file: + raise HTTPException(400, "OAuth keys/token file not configured") + + with open(keys_file, encoding="utf-8") as f: + keys_data = json.load(f) + keys = keys_data.get("installed") or keys_data.get("web") + client_id = keys["client_id"] + client_secret = keys["client_secret"] + + redirect_uri = _mcp_oauth_redirect_uri() + + async with httpx.AsyncClient() as client: + resp = await client.post( + "https://oauth2.googleapis.com/token", + data={ + "code": code, + "client_id": client_id, + "client_secret": client_secret, + "redirect_uri": redirect_uri, + "grant_type": "authorization_code", + }, + ) + + if resp.status_code != 200: + err = resp.text + logger.error(f"OAuth token exchange failed: {err}") + return HTMLResponse(_oauth_result_page("Authorization Failed", f"Google returned an error: {err}"), status_code=400) + + tokens = resp.json() + logger.info(f"OAuth tokens received for server {server_id}") + + # Save tokens to the file the MCP package expects + os.makedirs(os.path.dirname(token_file), exist_ok=True) + with open(token_file, "w", encoding="utf-8") as f: + json.dump(tokens, f, indent=2) + logger.info(f"Saved OAuth tokens to {token_file}") + + # Attempt to connect the MCP server now + args = json.loads(srv.args) if srv.args else [] + env = json.loads(srv.env) if srv.env else {} + connected = await mcp_manager.connect_server( + server_id=server_id, + name=srv.name, + transport=srv.transport, + command=srv.command, + args=args, + env=env, + url=srv.url, + ) + + if connected: + status = mcp_manager.get_server_status(server_id) + tool_count = status.get("tool_count", 0) + return HTMLResponse(_oauth_result_page( + "Authorization Successful", + f"{srv.name} connected with {tool_count} tools. You can close this window.", + success=True, + )) + else: + status = mcp_manager.get_server_status(server_id) + return HTMLResponse(_oauth_result_page( + "Authorized but Connection Failed", + f"Tokens saved, but the server failed to connect: {status.get('error', 'unknown error')}. Try reconnecting from Settings.", + )) + except HTTPException as e: + logger.warning(f"OAuth callback rejected: {e.detail}") + return HTMLResponse(_oauth_result_page("Error", str(e.detail)), status_code=e.status_code) + except Exception as e: + logger.exception(f"OAuth callback error: {e}") + return HTMLResponse(_oauth_result_page("Error", str(e)), status_code=500) + finally: + db.close() + + return router + + +def _oauth_authorize_page( + auth_url: str, + server_id: str, + host: str, + redirect_uri: str = "http://localhost:7000/api/mcp/oauth/callback", +) -> str: + """Page with Google sign-in link and URL paste-back form for remote access.""" + # Escape values interpolated into the page: `host` comes from the request + # Host header and `server_id` from the OAuth state — neither is trusted. + auth_url = html.escape(auth_url, quote=True) + server_id = html.escape(server_id, quote=True) + host = html.escape(host, quote=True) + redirect_uri = html.escape(redirect_uri, quote=True) + return f"""<!DOCTYPE html> +<html><head> +<meta charset="UTF-8"><title>Authorize — Odysseus + +
+

Authorize Google Account

+
+ 1. Click the button below to sign in with Google
+ 2. After approving, your browser will show an error page — that's normal
+ 3. Copy the full URL from your browser's address bar
+ 4. Paste it below and click Connect +
+ Sign in with Google +
+
+

Paste the URL from your browser after signing in:

+ +
+
+
""" + + +def _oauth_result_page(title: str, message: str, success: bool = False) -> str: + """Generate a simple HTML page for the OAuth result.""" + safe_title = html.escape(title) + safe_message = html.escape(message) + color = "#00661a" if success else "#e06c75" + icon = "✓" if success else "✗" + return f""" + +{safe_title} + +
+
{icon}
+

{safe_title}

+

{safe_message}

+
""" diff --git a/routes/mcp_routes.py b/routes/mcp_routes.py index a0ade88b6..8304dc1d4 100644 --- a/routes/mcp_routes.py +++ b/routes/mcp_routes.py @@ -1,697 +1,18 @@ -# routes/mcp_routes.py -"""MCP (Model Context Protocol) server management routes.""" -import json -import os -import uuid -import urllib.parse -import html -from pathlib import Path -from fastapi import APIRouter, Form, HTTPException, Request -from fastapi.responses import RedirectResponse, HTMLResponse -import logging -import httpx +"""Backward-compat shim — canonical location is routes/mcp/mcp_routes.py. -from core.database import McpServer, SessionLocal -from core.middleware import require_admin -from src.constants import DATA_DIR, MCP_OAUTH_DIR -from src.mcp_manager import McpManager +This module is replaced in ``sys.modules`` by the canonical module object so +that ``import routes.mcp_routes``, ``from routes.mcp_routes import X``, +``importlib.import_module("routes.mcp_routes")``, the +``sys.modules.pop("routes.mcp_routes")`` + re-import pattern in +test_security_regressions.py, and the ``monkeypatch.setattr(mcp_routes, +"MCP_OAUTH_DIR", ...)`` pattern all operate on the *same* object. This also +makes ``mcp_routes.__file__`` resolve to the canonical file (which the +source-introspection at line 839 reads). Keeps existing import paths working +after slice 2o (#4082/#4071). +""" -logger = logging.getLogger(__name__) +import sys as _sys -router = APIRouter(prefix="/api/mcp", tags=["mcp"]) +from routes.mcp import mcp_routes as _canonical # noqa: F401 - -def _mcp_oauth_base_dir() -> Path: - """Directory that may contain OAuth files managed by Odysseus.""" - return Path(MCP_OAUTH_DIR).resolve(strict=False) - - -def _resolve_mcp_oauth_path(raw_path, field_name: str) -> str: - """Resolve an MCP OAuth path and keep it under DATA_DIR/mcp_oauth.""" - raw = str(raw_path or "").strip() - if not raw: - return "" - - base = _mcp_oauth_base_dir() - path = Path(os.path.expanduser(raw)) - if not path.is_absolute(): - path = base / path - resolved = path.resolve(strict=False) - - try: - resolved.relative_to(base) - except ValueError as exc: - raise HTTPException( - 400, - f"Invalid OAuth {field_name}: path must stay under {base}", - ) from exc - return str(resolved) - - -def _sanitize_mcp_oauth_config(oauth_cfg): - """Return an OAuth config copy with file paths confined to mcp_oauth.""" - if not oauth_cfg: - return oauth_cfg - if not isinstance(oauth_cfg, dict): - return {} - sanitized = dict(oauth_cfg) - for field_name in ("keys_file", "token_file"): - if sanitized.get(field_name): - sanitized[field_name] = _resolve_mcp_oauth_path( - sanitized[field_name], - field_name, - ) - return sanitized - - -def _mcp_oauth_token_missing(oauth_cfg, *, strict: bool = True) -> bool: - """Check token existence without letting legacy bad paths break listing.""" - if not isinstance(oauth_cfg, dict): - return False - try: - token_file = _resolve_mcp_oauth_path(oauth_cfg.get("token_file", ""), "token_file") - except HTTPException: - if strict: - raise - logger.warning("Ignoring MCP OAuth config with unsafe token_file") - return True - return bool(token_file and not os.path.exists(token_file)) - - -def _apply_mcp_oauth_env(env: dict, oauth_cfg) -> None: - """Pass sanitized Gmail package paths to MCP servers that honor them.""" - if not oauth_cfg or not isinstance(env, dict): - return - keys_file = oauth_cfg.get("keys_file") - token_file = oauth_cfg.get("token_file") - if keys_file: - env["GMAIL_OAUTH_PATH"] = keys_file - if token_file: - env["GMAIL_CREDENTIALS_PATH"] = token_file - - -def _load_disabled_map(): - """Load per-server disabled tool sets from DB.""" - db = SessionLocal() - try: - disabled_map = {} - for srv in db.query(McpServer).all(): - if srv.disabled_tools: - try: - names = json.loads(srv.disabled_tools) - if names: - disabled_map[srv.id] = set(names) - except (json.JSONDecodeError, TypeError): - pass - return disabled_map - finally: - db.close() - - -def _mcp_oauth_redirect_uri() -> str: - """Shared callback URL for legacy Google and generic MCP OAuth flows.""" - from src.mcp_oauth import REDIRECT_URI - return REDIRECT_URI - - -def setup_mcp_routes(mcp_manager: McpManager): - """Setup MCP routes with the provided manager.""" - - @router.get("/servers") - def list_servers(request: Request): - """List all configured MCP servers with connection status.""" - require_admin(request) - db = SessionLocal() - try: - servers = db.query(McpServer).all() - result = [] - for srv in servers: - status = mcp_manager.get_server_status(srv.id) - oauth_cfg = json.loads(srv.oauth_config) if srv.oauth_config else None - needs_oauth = False - if oauth_cfg: - needs_oauth = _mcp_oauth_token_missing(oauth_cfg, strict=False) - disabled_list = json.loads(srv.disabled_tools) if srv.disabled_tools else [] - total_tools = status.get("tool_count", 0) - result.append({ - "id": srv.id, - "name": srv.name, - "transport": srv.transport, - "command": srv.command, - "args": json.loads(srv.args) if srv.args else [], - "env": json.loads(srv.env) if srv.env else {}, - "url": srv.url, - "is_enabled": srv.is_enabled, - "status": status.get("status", "disconnected"), - "tool_count": total_tools, - "disabled_tool_count": len(disabled_list), - "enabled_tool_count": max(0, total_tools - len(disabled_list)), - "error": status.get("error"), - "auth_url": status.get("auth_url"), - "has_oauth": oauth_cfg is not None, - "needs_oauth": needs_oauth, - }) - return result - finally: - db.close() - - @router.post("/servers") - async def add_server( - request: Request, - name: str = Form(...), - transport: str = Form("stdio"), - command: str = Form(None), - args: str = Form("[]"), - env: str = Form("{}"), - url: str = Form(None), - oauth_file: str = Form(None), - oauth_config: str = Form(None), - ): - """Add a new MCP server config and attempt connection. Admin-only: - registering a stdio server is equivalent to executing arbitrary - binaries on the host.""" - require_admin(request) - server_id = str(uuid.uuid4())[:8] - - # Validate - if transport == "stdio" and not command: - raise HTTPException(400, "command is required for stdio transport") - if transport == "sse" and not url: - raise HTTPException(400, "url is required for SSE transport") - if transport == "http" and not url: - raise HTTPException(400, "url is required for HTTP transport") - - # Parse JSON fields - try: - parsed_args = json.loads(args) if args else [] - except json.JSONDecodeError: - parsed_args = [] - try: - parsed_env = json.loads(env) if env else {} - except json.JSONDecodeError: - parsed_env = {} - if not isinstance(parsed_env, dict): - parsed_env = {} - - # Parse OAuth config - parsed_oauth_config = None - if oauth_config: - try: - parsed_oauth_config = _sanitize_mcp_oauth_config(json.loads(oauth_config)) - except json.JSONDecodeError: - pass - _apply_mcp_oauth_env(parsed_env, parsed_oauth_config) - - # Write OAuth credentials file if provided (for Google MCP servers) - logger.info(f"MCP add_server: oauth_file={oauth_file!r}") - if oauth_file: - try: - oauth_data = json.loads(oauth_file) - oauth_dir = _resolve_mcp_oauth_path(oauth_data.get("dir", ""), "dir") - oauth_filename = oauth_data.get("filename", "") - client_id = oauth_data.get("client_id", "") - client_secret = oauth_data.get("client_secret", "") - if oauth_dir and oauth_filename and client_id and client_secret: - filepath = _resolve_mcp_oauth_path( - Path(oauth_dir) / str(oauth_filename), - "filename", - ) - os.makedirs(os.path.dirname(filepath), exist_ok=True) - creds = { - "installed": { - "client_id": client_id, - "client_secret": client_secret, - "redirect_uris": ["http://localhost"], - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://accounts.google.com/o/oauth2/token", - } - } - with open(filepath, "w", encoding="utf-8") as f: - json.dump(creds, f, indent=2) - logger.info(f"Wrote OAuth credentials to {filepath}") - parsed_env.pop("GOOGLE_CLIENT_ID", None) - parsed_env.pop("GOOGLE_CLIENT_SECRET", None) - except (json.JSONDecodeError, OSError) as e: - logger.warning(f"Failed to write OAuth file: {e}") - - # Save to DB - db = SessionLocal() - try: - srv = McpServer( - id=server_id, - name=name, - transport=transport, - command=command, - args=json.dumps(parsed_args), - env=json.dumps(parsed_env), - url=url, - is_enabled=True, - oauth_config=json.dumps(parsed_oauth_config) if parsed_oauth_config else None, - ) - db.add(srv) - db.commit() - finally: - db.close() - - # Check if OAuth token already exists — skip connection attempt if not - needs_oauth = False - if parsed_oauth_config: - needs_oauth = _mcp_oauth_token_missing(parsed_oauth_config) - - connected = False - if not needs_oauth: - connected = await mcp_manager.connect_server( - server_id=server_id, - name=name, - transport=transport, - command=command, - args=parsed_args, - env=parsed_env, - url=url, - ) - - status = mcp_manager.get_server_status(server_id) - needs_auth = status.get("status") == "needs_auth" - return { - "id": server_id, - "name": name, - "connected": connected, - "status": "needs_oauth" if needs_oauth else status.get("status", "disconnected"), - "tool_count": status.get("tool_count", 0), - "error": "OAuth authorization required" if needs_oauth else status.get("error"), - "needs_oauth": needs_oauth, - "needs_auth": needs_auth, - "auth_url": status.get("auth_url"), - } - - @router.post("/servers/{server_id}/reconnect") - async def reconnect_server(server_id: str, request: Request): - """Reconnect to an MCP server.""" - require_admin(request) - db = SessionLocal() - try: - srv = db.query(McpServer).filter(McpServer.id == server_id).first() - if not srv: - raise HTTPException(404, "Server not found") - - await mcp_manager.disconnect_server(server_id) - - args = json.loads(srv.args) if srv.args else [] - env = json.loads(srv.env) if srv.env else {} - connected = await mcp_manager.connect_server( - server_id=server_id, - name=srv.name, - transport=srv.transport, - command=srv.command, - args=args, - env=env, - url=srv.url, - ) - - status = mcp_manager.get_server_status(server_id) - return { - "connected": connected, - "status": status.get("status", "disconnected"), - "tool_count": status.get("tool_count", 0), - "error": status.get("error"), - "auth_url": status.get("auth_url"), - "needs_auth": status.get("status") == "needs_auth", - } - finally: - db.close() - - @router.patch("/servers/{server_id}") - async def toggle_server(server_id: str, request: Request, is_enabled: str = Form(...)): - """Enable or disable an MCP server.""" - require_admin(request) - db = SessionLocal() - try: - srv = db.query(McpServer).filter(McpServer.id == server_id).first() - if not srv: - raise HTTPException(404, "Server not found") - - enabled = str(is_enabled).lower() == "true" - srv.is_enabled = enabled - db.commit() - - if enabled: - args = json.loads(srv.args) if srv.args else [] - env = json.loads(srv.env) if srv.env else {} - await mcp_manager.connect_server( - server_id=server_id, - name=srv.name, - transport=srv.transport, - command=srv.command, - args=args, - env=env, - url=srv.url, - ) - else: - await mcp_manager.disconnect_server(server_id) - - return {"id": server_id, "is_enabled": enabled} - finally: - db.close() - - @router.delete("/servers/{server_id}") - async def delete_server(server_id: str, request: Request): - """Remove an MCP server.""" - require_admin(request) - db = SessionLocal() - try: - srv = db.query(McpServer).filter(McpServer.id == server_id).first() - if not srv: - raise HTTPException(404, "Server not found") - - await mcp_manager.disconnect_server(server_id) - - db.delete(srv) - db.commit() - return {"status": "deleted"} - finally: - db.close() - - @router.get("/tools") - def list_tools(request: Request): - """List all discovered MCP tools across all connected servers.""" - require_admin(request) - disabled_map = _load_disabled_map() - return mcp_manager.get_all_tools(disabled_map) - - @router.get("/servers/{server_id}/tools") - def list_server_tools(server_id: str, request: Request): - """List all tools for a specific MCP server with enabled/disabled state.""" - require_admin(request) - db = SessionLocal() - try: - srv = db.query(McpServer).filter(McpServer.id == server_id).first() - if not srv: - raise HTTPException(404, "Server not found") - disabled_list = json.loads(srv.disabled_tools) if srv.disabled_tools else [] - disabled_set = set(disabled_list) - finally: - db.close() - - all_tools = mcp_manager.get_all_tools() - server_tools = [t for t in all_tools if t["server_id"] == server_id] - for t in server_tools: - t["is_disabled"] = t["name"] in disabled_set - return server_tools - - @router.patch("/servers/{server_id}/tools") - async def update_disabled_tools(server_id: str, request: Request): - """Bulk update disabled tools list for a server. - - Expects JSON body: {"disabled": ["tool_name_1", "tool_name_2"]} - """ - require_admin(request) - db = SessionLocal() - try: - srv = db.query(McpServer).filter(McpServer.id == server_id).first() - if not srv: - raise HTTPException(404, "Server not found") - - body = await request.json() - disabled = body.get("disabled", []) - if not isinstance(disabled, list): - raise HTTPException(400, "disabled must be a list of tool names") - - srv.disabled_tools = json.dumps(disabled) if disabled else None - db.commit() - - return {"id": server_id, "disabled_count": len(disabled)} - finally: - db.close() - - # ── OAuth flow for Google MCP servers ────────────────────────── - - @router.get("/oauth/authorize/{server_id}") - def oauth_authorize(server_id: str, request: Request): - """Show OAuth authorization page with Google sign-in link.""" - require_admin(request) - db = SessionLocal() - try: - srv = db.query(McpServer).filter(McpServer.id == server_id).first() - if not srv: - raise HTTPException(404, "Server not found") - if not srv.oauth_config: - raise HTTPException(400, "Server has no OAuth config") - - oauth_cfg = _sanitize_mcp_oauth_config(json.loads(srv.oauth_config)) - keys_file = oauth_cfg.get("keys_file", "") - if not keys_file or not os.path.exists(keys_file): - raise HTTPException(400, "OAuth keys file not found") - - with open(keys_file, encoding="utf-8") as f: - keys_data = json.load(f) - keys = keys_data.get("installed") or keys_data.get("web") - if not keys: - raise HTTPException(400, "Invalid OAuth keys file format") - - client_id = keys["client_id"] - scopes = oauth_cfg.get("scopes", []) - - # For Desktop App creds, default to localhost — the user will - # paste the resulting URL back if they're on a different device. - redirect_uri = _mcp_oauth_redirect_uri() - - params = { - "client_id": client_id, - "redirect_uri": redirect_uri, - "response_type": "code", - "scope": " ".join(scopes), - "access_type": "offline", - "prompt": "consent", - "state": server_id, - } - auth_url = "https://accounts.google.com/o/oauth2/v2/auth?" + urllib.parse.urlencode(params) - - # Determine if user is accessing from the same machine - host = request.headers.get("host", "") - is_local = host.startswith("localhost") or host.startswith("127.0.0.1") - - if is_local: - # Same machine — just redirect, callback will work directly - return RedirectResponse(auth_url) - else: - # Remote device — show paste-back page - return HTMLResponse(_oauth_authorize_page(auth_url, server_id, host, redirect_uri)) - finally: - db.close() - - @router.get("/oauth/callback") - async def oauth_callback(code: str, state: str, request: Request): - """Handle OAuth callback. Generic MCP OAuth flows resolve via the - pending-state registry; Google flows fall through to the legacy path.""" - require_admin(request) - from src.mcp_oauth import resolve_pending - if resolve_pending(state, code): - return HTMLResponse(_oauth_result_page( - "Authorization Successful", - "The MCP server is connecting. You can close this window and return to Odysseus.", - success=True, - )) - # Legacy Google path: state is the server_id - return await _exchange_and_connect(state, code, request) - - @router.post("/oauth/exchange/{server_id}") - async def oauth_exchange(server_id: str, request: Request, callback_url: str = Form(...)): - """Manual code exchange — user pastes the callback URL from their browser.""" - require_admin(request) - try: - parsed = urllib.parse.urlparse(callback_url) - params = urllib.parse.parse_qs(parsed.query) - code = params.get("code", [None])[0] - if not code: - return HTMLResponse(_oauth_result_page("Error", "No authorization code found in the URL. Make sure you copied the full URL from your browser."), status_code=400) - except Exception: - return HTMLResponse(_oauth_result_page("Error", "Invalid URL format."), status_code=400) - - # Generic MCP OAuth: if the pasted URL carries a state we are waiting on, - # resolve it directly (the background connect finishes the handshake). - state = params.get("state", [None])[0] - from src.mcp_oauth import resolve_pending - if state and resolve_pending(state, code): - return HTMLResponse(_oauth_result_page( - "Authorization Successful", - "The MCP server is connecting. You can close this window and return to Odysseus.", - success=True, - )) - - return await _exchange_and_connect(server_id, code, request) - - async def _exchange_and_connect(server_id: str, code: str, request: Request): - """Exchange auth code for tokens and connect the MCP server.""" - db = SessionLocal() - try: - srv = db.query(McpServer).filter(McpServer.id == server_id).first() - if not srv: - return HTMLResponse(_oauth_result_page("Error", "Server not found."), status_code=404) - if not srv.oauth_config: - return HTMLResponse(_oauth_result_page("Error", "No OAuth config."), status_code=400) - - oauth_cfg = _sanitize_mcp_oauth_config(json.loads(srv.oauth_config)) - keys_file = oauth_cfg.get("keys_file", "") - token_file = oauth_cfg.get("token_file", "") - if not keys_file or not token_file: - raise HTTPException(400, "OAuth keys/token file not configured") - - with open(keys_file, encoding="utf-8") as f: - keys_data = json.load(f) - keys = keys_data.get("installed") or keys_data.get("web") - client_id = keys["client_id"] - client_secret = keys["client_secret"] - - redirect_uri = _mcp_oauth_redirect_uri() - - async with httpx.AsyncClient() as client: - resp = await client.post( - "https://oauth2.googleapis.com/token", - data={ - "code": code, - "client_id": client_id, - "client_secret": client_secret, - "redirect_uri": redirect_uri, - "grant_type": "authorization_code", - }, - ) - - if resp.status_code != 200: - err = resp.text - logger.error(f"OAuth token exchange failed: {err}") - return HTMLResponse(_oauth_result_page("Authorization Failed", f"Google returned an error: {err}"), status_code=400) - - tokens = resp.json() - logger.info(f"OAuth tokens received for server {server_id}") - - # Save tokens to the file the MCP package expects - os.makedirs(os.path.dirname(token_file), exist_ok=True) - with open(token_file, "w", encoding="utf-8") as f: - json.dump(tokens, f, indent=2) - logger.info(f"Saved OAuth tokens to {token_file}") - - # Attempt to connect the MCP server now - args = json.loads(srv.args) if srv.args else [] - env = json.loads(srv.env) if srv.env else {} - connected = await mcp_manager.connect_server( - server_id=server_id, - name=srv.name, - transport=srv.transport, - command=srv.command, - args=args, - env=env, - url=srv.url, - ) - - if connected: - status = mcp_manager.get_server_status(server_id) - tool_count = status.get("tool_count", 0) - return HTMLResponse(_oauth_result_page( - "Authorization Successful", - f"{srv.name} connected with {tool_count} tools. You can close this window.", - success=True, - )) - else: - status = mcp_manager.get_server_status(server_id) - return HTMLResponse(_oauth_result_page( - "Authorized but Connection Failed", - f"Tokens saved, but the server failed to connect: {status.get('error', 'unknown error')}. Try reconnecting from Settings.", - )) - except HTTPException as e: - logger.warning(f"OAuth callback rejected: {e.detail}") - return HTMLResponse(_oauth_result_page("Error", str(e.detail)), status_code=e.status_code) - except Exception as e: - logger.exception(f"OAuth callback error: {e}") - return HTMLResponse(_oauth_result_page("Error", str(e)), status_code=500) - finally: - db.close() - - return router - - -def _oauth_authorize_page( - auth_url: str, - server_id: str, - host: str, - redirect_uri: str = "http://localhost:7000/api/mcp/oauth/callback", -) -> str: - """Page with Google sign-in link and URL paste-back form for remote access.""" - # Escape values interpolated into the page: `host` comes from the request - # Host header and `server_id` from the OAuth state — neither is trusted. - auth_url = html.escape(auth_url, quote=True) - server_id = html.escape(server_id, quote=True) - host = html.escape(host, quote=True) - redirect_uri = html.escape(redirect_uri, quote=True) - return f""" - -Authorize — Odysseus - -
-

Authorize Google Account

-
- 1. Click the button below to sign in with Google
- 2. After approving, your browser will show an error page — that's normal
- 3. Copy the full URL from your browser's address bar
- 4. Paste it below and click Connect -
- Sign in with Google -
-
-

Paste the URL from your browser after signing in:

- -
-
-
""" - - -def _oauth_result_page(title: str, message: str, success: bool = False) -> str: - """Generate a simple HTML page for the OAuth result.""" - safe_title = html.escape(title) - safe_message = html.escape(message) - color = "#00661a" if success else "#e06c75" - icon = "✓" if success else "✗" - return f""" - -{safe_title} - -
-
{icon}
-

{safe_title}

-

{safe_message}

-
""" +_sys.modules[__name__] = _canonical diff --git a/tests/test_mcp_routes_shim.py b/tests/test_mcp_routes_shim.py new file mode 100644 index 000000000..87d347bd0 --- /dev/null +++ b/tests/test_mcp_routes_shim.py @@ -0,0 +1,19 @@ +"""Regression test for the mcp route shim (slice 2o, #4082/#4071). + +The backward-compat shim at ``routes/mcp_routes.py`` uses ``sys.modules`` +replacement so the legacy import path and the canonical ``routes.mcp.*`` +path resolve to the *same* module object. This is required because +``test_security_regressions.py`` does ``sys.modules.pop("routes.mcp_routes")`` ++ re-import, ``monkeypatch.setattr(mcp_routes, "MCP_OAUTH_DIR", ...)``, and +reads ``mcp_routes.__file__`` for source introspection. +""" + +import importlib + +import routes.mcp_routes as _shim_mcp # noqa: F401 + + +def test_legacy_and_canonical_mcp_module_are_same_object(): + legacy = importlib.import_module("routes.mcp_routes") + canonical = importlib.import_module("routes.mcp.mcp_routes") + assert legacy is canonical diff --git a/tests/test_security_regressions.py b/tests/test_security_regressions.py index f6a05383d..151e5a9e4 100644 --- a/tests/test_security_regressions.py +++ b/tests/test_security_regressions.py @@ -998,7 +998,7 @@ def test_session_html_export_escapes_name(): def test_mcp_oauth_page_escapes_reflected_values(): - src = Path(__file__).resolve().parents[1] / "routes" / "mcp_routes.py" + src = Path(__file__).resolve().parents[1] / "routes" / "mcp" / "mcp_routes.py" text = src.read_text() body = text.split("def _oauth_authorize_page(", 1)[1].split("return f", 1)[0] for var in ("auth_url", "server_id", "host", "redirect_uri"): From 22e0af2a58d378dd67fda54a18233109b26e627b Mon Sep 17 00:00:00 2001 From: Amir Fathi Date: Tue, 11 Aug 2026 06:36:56 -0600 Subject: [PATCH 052/125] fix(core): stop atomic writes from colliding on a constant PID suffix (#5721) atomic_write_json/atomic_write_text build their temp filename as "{path}.tmp.{os.getpid()}". os.getpid() is constant for the life of a process, so it only ever distinguishes concurrent writers that live in different OS processes. Odysseus runs as a single long-lived process per container, so two concurrent writers to the same path (e.g. two request handlers racing a settings save) always compute the identical temp path. Whichever finishes os.replace() first removes the shared tmp file out from under the other, which then raises FileNotFoundError on its own os.replace() instead of landing its write. Fix: derive the temp suffix from uuid4() instead of the PID, so every call gets a distinct temp path regardless of process/thread identity. routes/prefs_routes.py's _save() had an independent, hand-rolled copy of the exact same PID-suffix logic (not the shared core.atomic_io helper other routes already use, e.g. routes/auth_routes.py) with the same bug. Replaced it with a call to atomic_write_json. Fixes #5596 Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- core/atomic_io.py | 12 +++++--- routes/prefs_routes.py | 10 ++----- tests/test_atomic_io.py | 50 ++++++++++++++++++++++++++++---- tests/test_prefs_atomic_write.py | 5 ++-- 4 files changed, 57 insertions(+), 20 deletions(-) diff --git a/core/atomic_io.py b/core/atomic_io.py index 81c640d8a..40a51adbe 100644 --- a/core/atomic_io.py +++ b/core/atomic_io.py @@ -15,17 +15,21 @@ from __future__ import annotations import json import os +import uuid from typing import Any, Optional def atomic_write_json(path: str, data: Any, *, indent: Optional[int] = None) -> None: """Atomically persist `data` as JSON at `path`. - The temp file uses the live PID as a suffix so two processes saving the - same file (e.g. unit tests) don't collide on the rename target. + The temp file uses a random suffix so two concurrent writers saving the + same file don't collide on the rename target. A PID suffix does not do + this: the PID is constant for the life of a process, so two writers on + the same path within one process (or one single-process container, where + the PID never changes at all) still race for the same temp file. """ os.makedirs(os.path.dirname(path) or ".", exist_ok=True) - tmp = f"{path}.tmp.{os.getpid()}" + tmp = f"{path}.tmp.{uuid.uuid4().hex}" with open(tmp, "w", encoding="utf-8") as f: json.dump(data, f, indent=indent) f.flush() @@ -37,7 +41,7 @@ def atomic_write_text(path: str, text: str) -> None: if not isinstance(text, str): raise TypeError("atomic_write_text expects a string") os.makedirs(os.path.dirname(path) or ".", exist_ok=True) - tmp = f"{path}.tmp.{os.getpid()}" + tmp = f"{path}.tmp.{uuid.uuid4().hex}" with open(tmp, "w", encoding="utf-8") as f: f.write(text) f.flush() diff --git a/routes/prefs_routes.py b/routes/prefs_routes.py index f2a778c2d..91a17419e 100644 --- a/routes/prefs_routes.py +++ b/routes/prefs_routes.py @@ -1,8 +1,8 @@ """User preferences API — per-user key/value store backed by a JSON file.""" import json -import os from typing import Optional from fastapi import APIRouter, Request +from core.atomic_io import atomic_write_json from src.auth_helpers import get_current_user from src.constants import USER_PREFS_FILE @@ -20,13 +20,7 @@ def _load(): def _save(prefs): - os.makedirs(os.path.dirname(PREFS_FILE) or ".", exist_ok=True) - tmp = f"{PREFS_FILE}.tmp.{os.getpid()}" - with open(tmp, "w", encoding="utf-8") as f: - json.dump(prefs, f, indent=2) - f.flush() - os.fsync(f.fileno()) - os.replace(tmp, PREFS_FILE) + atomic_write_json(PREFS_FILE, prefs, indent=2) def _load_for_user(user: Optional[str] = None) -> dict: diff --git a/tests/test_atomic_io.py b/tests/test_atomic_io.py index 9fadec20a..e08da9db8 100644 --- a/tests/test_atomic_io.py +++ b/tests/test_atomic_io.py @@ -1,17 +1,19 @@ """Tests for ``core.atomic_io`` durability and crash-safety behavior. ``core.atomic_io`` provides ``atomic_write_json`` and ``atomic_write_text``. -Both write to a sibling ``.tmp.`` file, ``fsync`` it, then ``os.replace`` -into place so a crash mid-write leaves the previous good copy untouched rather -than a truncated/empty file. +Both write to a sibling ``.tmp.`` file, ``fsync`` it, then +``os.replace`` into place so a crash mid-write leaves the previous good copy +untouched rather than a truncated/empty file. These tests cover the happy path (round-trip, indent, parent-dir creation, -full overwrite, no leftover tmp) and the two failure paths the implementation -guarantees: the target file is preserved when serialization fails before the -replace, and when ``os.replace`` itself fails. +full overwrite, no leftover tmp), the two failure paths the implementation +guarantees (the target file is preserved when serialization fails before the +replace, and when ``os.replace`` itself fails), and that two concurrent +writers to the same path don't collide on the same temp file. """ import importlib.util import json +import threading from pathlib import Path import pytest @@ -84,6 +86,42 @@ def test_atomic_write_json_leaves_no_tmp_file(tmp_path): assert _tmp_siblings(tmp_path, "data.json") == [] +def test_atomic_write_json_concurrent_writers_do_not_collide(tmp_path): + # Both writers run in this same process, so a PID-based tmp suffix is + # identical for both: whichever writer finishes first unlinks the tmp + # file (via os.replace) out from under the other, which then raises + # FileNotFoundError on its own os.replace instead of landing its write. + target = tmp_path / "settings.json" + orig_dump = json.dump + barrier = threading.Barrier(2) + errors = [] + + def slow_dump(obj, fp, **kwargs): + orig_dump(obj, fp, **kwargs) + fp.flush() + barrier.wait() + + def write(payload): + try: + atomic_write_json(str(target), payload) + except Exception as exc: # noqa: BLE001 - captured for the assertion below + errors.append(exc) + + json.dump = slow_dump + try: + t1 = threading.Thread(target=write, args=({"writer": "A"},)) + t2 = threading.Thread(target=write, args=({"writer": "B"},)) + t1.start() + t2.start() + t1.join() + t2.join() + finally: + json.dump = orig_dump + + assert errors == [] + assert json.loads(target.read_text(encoding="utf-8"))["writer"] in ("A", "B") + + # --------------------------------------------------------------------------- # atomic_write_json — failure path: target preserved on serialization error. # --------------------------------------------------------------------------- diff --git a/tests/test_prefs_atomic_write.py b/tests/test_prefs_atomic_write.py index d7eac3087..29c24ccde 100644 --- a/tests/test_prefs_atomic_write.py +++ b/tests/test_prefs_atomic_write.py @@ -1,11 +1,12 @@ import json import routes.prefs_routes as prefs_routes +from core import atomic_io def test_save_replaces_prefs_file_atomically(monkeypatch, tmp_path): calls = [] - real_replace = prefs_routes.os.replace + real_replace = atomic_io.os.replace def fake_replace(src, dst): calls.append((src, dst)) @@ -13,7 +14,7 @@ def test_save_replaces_prefs_file_atomically(monkeypatch, tmp_path): prefs_file = tmp_path / "data" / "user_prefs.json" monkeypatch.setattr(prefs_routes, "PREFS_FILE", str(prefs_file)) - monkeypatch.setattr(prefs_routes.os, "replace", fake_replace) + monkeypatch.setattr(atomic_io.os, "replace", fake_replace) prefs_routes._save({"theme": "dark"}) From 3bea7a53eed8fe099064a9d109e576ae69da011c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o?= Date: Wed, 12 Aug 2026 01:04:32 +0200 Subject: [PATCH 053/125] fix(email): derive the Google OAuth redirect URI scheme from the request (#5995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both the authorize and callback routes built the redirect URI with a hardcoded `http://` and the Host header. Behind any TLS terminator that produces `http://host:443/api/email/oauth/google/callback` — the wrong scheme and, on a split-port setup, a dead port. Google then refuses the authorize request or the token exchange, so OAuth email is unusable on every HTTPS deployment unless GOOGLE_OAUTH_REDIRECT_URI is pinned by hand. uvicorn's proxy-headers middleware already rewrites the scheme from X-Forwarded-Proto for trusted proxies (on by default, trusting 127.0.0.1), so request.url.scheme is correct both directly and behind a proxy. Google requires the callback's redirect_uri to match the authorize one exactly, so both sites change together. An explicit GOOGLE_OAUTH_REDIRECT_URI still wins, unchanged. --- routes/email_routes.py | 4 +- tests/test_email_oauth.py | 124 +++++++++++++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 4 deletions(-) diff --git a/routes/email_routes.py b/routes/email_routes.py index 81136e36e..03b73ebb5 100644 --- a/routes/email_routes.py +++ b/routes/email_routes.py @@ -5954,7 +5954,7 @@ def setup_email_routes(): raise HTTPException(400, "GOOGLE_OAUTH_CLIENT_ID not set — add it to .env") redirect_uri = ( os.environ.get("GOOGLE_OAUTH_REDIRECT_URI") - or f"http://{request.headers.get('host', 'localhost:7000')}/api/email/oauth/google/callback" + or f"{request.url.scheme}://{request.headers.get('host', 'localhost:7000')}/api/email/oauth/google/callback" ) state = make_oauth_state(account_id, owner) params = urllib.parse.urlencode({ @@ -5991,7 +5991,7 @@ def setup_email_routes(): client_secret = os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET", "") redirect_uri = ( os.environ.get("GOOGLE_OAUTH_REDIRECT_URI") - or f"http://{request.headers.get('host', 'localhost:7000')}/api/email/oauth/google/callback" + or f"{request.url.scheme}://{request.headers.get('host', 'localhost:7000')}/api/email/oauth/google/callback" ) import httpx as _httpx try: diff --git a/tests/test_email_oauth.py b/tests/test_email_oauth.py index 4fad82739..ee34a9de7 100644 --- a/tests/test_email_oauth.py +++ b/tests/test_email_oauth.py @@ -29,6 +29,7 @@ import base64 import json import time import unittest.mock as mock +from types import SimpleNamespace import pytest @@ -272,8 +273,14 @@ def _callback_endpoint(): class _FakeRequest: - """Minimal stand-in for starlette Request — the callback only reads headers.""" - headers = {"host": "localhost:7000"} + """Minimal stand-in for starlette Request — the callback reads the Host header + and the request scheme. Behind a TLS terminator uvicorn's proxy-headers + middleware rewrites the scheme from `X-Forwarded-Proto`, so the route sees + `https` there and `http` on a plain origin.""" + + def __init__(self, scheme="http", host="localhost:7000"): + self.headers = {"host": host} + self.url = SimpleNamespace(scheme=scheme) def _location(resp): @@ -415,6 +422,119 @@ async def test_callback_valid_owner_writes_encrypted_tokens_to_intended_account( assert other.oauth_access_token is None, "tokens must only touch the intended account" +# ── Redirect URI scheme ─────────────────────────────────────────── +# +# Google rejects the token exchange unless the callback's `redirect_uri` is +# byte-identical to the one the authorize step sent, so both routes have to +# agree — including on the scheme. Deriving it from the request keeps HTTPS +# deployments working without pinning GOOGLE_OAUTH_REDIRECT_URI by hand; +# hardcoding `http://` produced an unusable redirect behind any TLS front. + +def _authorize_endpoint(): + """Return the live google_oauth_authorize endpoint from the email router.""" + from routes.email_routes import setup_email_routes + router = setup_email_routes() + for route in router.routes: + if route.path == "/api/email/oauth/google/authorize" and "GET" in getattr(route, "methods", set()): + return route.endpoint + raise AssertionError("google_oauth_authorize route not found") + + +def _posted_redirect_uri(mock_post): + """Pull `redirect_uri` out of the mocked Google token-exchange POST.""" + return mock_post.call_args.kwargs["data"]["redirect_uri"] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("scheme", ("http", "https")) +async def test_callback_redirect_uri_follows_the_request_scheme(scheme, monkeypatch): + """The token exchange must echo the scheme the request actually arrived on — + `https` behind a TLS terminator, `http` on a plain origin.""" + from routes.email_helpers import make_oauth_state + + monkeypatch.delenv("GOOGLE_OAUTH_REDIRECT_URI", raising=False) + + db, Factory = _make_db() + _make_account(db, account_id="acct-s", owner="alice", imap_user="alice@example.com") + db.close() + + token_resp = mock.MagicMock() + token_resp.raise_for_status = mock.MagicMock() + token_resp.json.return_value = {"access_token": "ya29.t", "refresh_token": "1//r", "expires_in": 3600} + userinfo_resp = mock.MagicMock() + userinfo_resp.is_success = True + userinfo_resp.json.return_value = {"email": "alice@example.com", "name": "Alice"} + + state = make_oauth_state("acct-s", "alice") + + with mock.patch("httpx.post", return_value=token_resp) as mock_post, \ + mock.patch("httpx.get", return_value=userinfo_resp), \ + mock.patch("core.database.SessionLocal", Factory): + callback = _callback_endpoint() + await callback( + code="4/code", state=state, error=None, + request=_FakeRequest(scheme=scheme, host="odysseus.example.ts.net:7443"), + ) + + assert _posted_redirect_uri(mock_post) == ( + f"{scheme}://odysseus.example.ts.net:7443/api/email/oauth/google/callback" + ) + + +@pytest.mark.asyncio +async def test_callback_redirect_uri_env_override_still_wins(monkeypatch): + """An explicit GOOGLE_OAUTH_REDIRECT_URI is used verbatim — deriving the + scheme must not override a value the operator pinned by hand.""" + from routes.email_helpers import make_oauth_state + + pinned = "https://mail.example.com/api/email/oauth/google/callback" + monkeypatch.setenv("GOOGLE_OAUTH_REDIRECT_URI", pinned) + + db, Factory = _make_db() + _make_account(db, account_id="acct-p", owner="alice", imap_user="alice@example.com") + db.close() + + token_resp = mock.MagicMock() + token_resp.raise_for_status = mock.MagicMock() + token_resp.json.return_value = {"access_token": "ya29.t", "refresh_token": "1//r", "expires_in": 3600} + userinfo_resp = mock.MagicMock() + userinfo_resp.is_success = True + userinfo_resp.json.return_value = {"email": "alice@example.com", "name": "Alice"} + + state = make_oauth_state("acct-p", "alice") + + with mock.patch("httpx.post", return_value=token_resp) as mock_post, \ + mock.patch("httpx.get", return_value=userinfo_resp), \ + mock.patch("core.database.SessionLocal", Factory): + callback = _callback_endpoint() + await callback(code="4/code", state=state, error=None, request=_FakeRequest(scheme="http")) + + assert _posted_redirect_uri(mock_post) == pinned + + +@pytest.mark.asyncio +@pytest.mark.parametrize("scheme", ("http", "https")) +async def test_authorize_redirect_uri_follows_the_request_scheme(scheme, monkeypatch): + """The authorize step builds the same redirect_uri the callback will send. + `owner=""` is the unconfigured / single-user case, so no DB is touched.""" + import urllib.parse + + monkeypatch.delenv("GOOGLE_OAUTH_REDIRECT_URI", raising=False) + monkeypatch.setenv("GOOGLE_OAUTH_CLIENT_ID", "client-id.apps.googleusercontent.com") + + authorize = _authorize_endpoint() + resp = await authorize( + account_id="acct-a", + request=_FakeRequest(scheme=scheme, host="odysseus.example.ts.net:7443"), + owner="", + ) + + query = urllib.parse.parse_qs(urllib.parse.urlparse(resp.headers["location"]).query) + assert query["redirect_uri"] == [ + f"{scheme}://odysseus.example.ts.net:7443/api/email/oauth/google/callback" + ] + + @pytest.mark.asyncio async def test_callback_rejects_token_for_a_different_mailbox_identity(): """Reconnecting with another Google identity must not replace the token From 663d6879b7b721d6bfcfc3554ecb9f82d1492aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o?= Date: Wed, 12 Aug 2026 01:25:03 +0200 Subject: [PATCH 054/125] fix(ui): stop the whirlpool spinner animating when it is never attached (#5990) _drawWhirlpool re-armed requestAnimationFrame forever whenever its element had never been connected to the document. The grace period is there so a spinner can keep drawing between start() and the caller appending the element, but it had no deadline: while the element has never been connected _wpWasConnected stays false, so the guard stays true and the else branch is unreachable. Any caller that starts a spinner and then takes an early return, such as an aborted request or a panel that resolved from cache, leaves a loop redrawing an 84-segment spiral into a detached canvas at one frame per displayed frame until the tab closes. Put a 2 second deadline on the grace period. Callers append in the same task as start(), so that is far more slack than any of them need. A spinner that is actually in the document is unaffected. Two supporting changes in the same file: - Both self-terminate paths now call stop() instead of setting isRunning directly, so termination always runs one cancelAnimationFrame and never depends solely on inferring DOM connectivity. Both draw functions bail at the top when they are no longer running, and _requestFrame() clears rafId as the callback enters so it is a truthful "a frame is pending" flag. - start() arms a visibilitychange listener and stop() removes it. A hidden tab cancels the pending frame, a re-shown tab re-arms it. Chrome throttles background rAF but does not reliably stop the canvas work, and owning the listener from start/stop means a dead spinner never leaves one behind. Adds tests/test_spinner_stops_when_never_attached_js.py, which drives the real module under node with a fake clock and a manual frame pump. It covers all four exits and, importantly, the converse: a spinner that is attached keeps running well past the grace window. --- static/js/spinner.js | 99 +++++- ...st_spinner_stops_when_never_attached_js.py | 319 ++++++++++++++++++ 2 files changed, 405 insertions(+), 13 deletions(-) create mode 100644 tests/test_spinner_stops_when_never_attached_js.py diff --git a/static/js/spinner.js b/static/js/spinner.js index d7db7437e..0037aeedf 100644 --- a/static/js/spinner.js +++ b/static/js/spinner.js @@ -4,6 +4,13 @@ * ASCII Spinner Module for AI thinking/processing status */ +// How long a canvas spinner may keep animating before its element has ever +// been inserted into the document. start() runs synchronously, before the +// caller appends the element, so frame 1 is always disconnected. Callers do +// append in the same task, so anything past this window means the element is +// never coming and the frames are drawing for nobody. +const UNATTACHED_GRACE_MS = 2000; + class Spinner { constructor(message = "AI is processing", style = "right", animation = "spinner") { // Different animation frames @@ -21,6 +28,9 @@ class Spinner { this.intervalId = null; this.rafId = null; this.element = null; + this._wpWasConnected = false; + this._wpUnattachedSince = null; + this._visHandler = null; } /** @@ -74,6 +84,7 @@ class Spinner { } _drawSineWave() { + if (!this.isRunning) return; const ctx = this._ctx; const W = this._canvas.width; const H = this._canvas.height; @@ -120,9 +131,7 @@ class Spinner { ctx.fillStyle = 'rgba(156, 222, 242, 0.9)'; ctx.fill(); - if (this.isRunning) { - this.rafId = requestAnimationFrame(() => this._drawSineWave()); - } + if (this.isRunning) this._requestFrame(); } _createWhirlpoolElement() { @@ -158,6 +167,7 @@ class Spinner { } _drawWhirlpool() { + if (!this.isRunning) return; const ctx = this._wpCtx; const W = this._wpCanvas.width; const H = this._wpCanvas.height; @@ -229,18 +239,77 @@ class Spinner { ctx.fill(); ctx.globalAlpha = 1; - if (!this.isRunning) return; - // Leak-safe self-terminate: stop once our element WAS in the DOM and then - // got removed (e.g. a loading row replaced by results). But keep spinning - // before it's first appended — start() runs synchronously, before the - // caller inserts the element, so it isn't connected on frame 1. + // Leak-safe self-terminate. "Nobody can see this spinner" has two shapes + // and we have to catch both: + // 1. the element WAS in the DOM and then got removed (a loading row + // replaced by results); + // 2. the element was NEVER inserted, and the grace window for inserting + // it has expired. The caller started a spinner and then took an early + // return (aborted request, panel that resolved from cache), so no + // frame we draw will ever be observed. + // Case 2 is why this needs a deadline at all: while the element has never + // been connected, `!this._wpWasConnected` stays true forever, so without + // the grace check the loop re-arms until the tab closes. const connected = !!(this.element && this.element.isConnected); - if (connected) this._wpWasConnected = true; - if (connected || !this._wpWasConnected) { - this.rafId = requestAnimationFrame(() => this._drawWhirlpool()); - } else { - this.isRunning = false; + if (connected) { + this._wpWasConnected = true; + this._wpUnattachedSince = null; + } else if (!this._wpWasConnected) { + if (this._wpUnattachedSince === null) this._wpUnattachedSince = performance.now(); + if (performance.now() - this._wpUnattachedSince > UNATTACHED_GRACE_MS) { + this.stop(); + return; + } } + + if (connected || !this._wpWasConnected) { + this._requestFrame(); + } else { + this.stop(); + } + } + + /** + * Arm the next animation frame. Clearing rafId as the callback enters keeps + * it a truthful "a frame is pending" flag, which is what stop() and the + * visibility handler cancel against. + */ + _requestFrame() { + this.rafId = requestAnimationFrame(() => { + this.rafId = null; + if (this.animation === 'sinewave') this._drawSineWave(); + else this._drawWhirlpool(); + }); + } + + /** + * Stop drawing while the tab is hidden. Browsers throttle background rAF but + * do not reliably stop the canvas work, and a spinner nobody is looking at + * should cost nothing. The listener is owned by start()/stop() so it is never + * left behind on a dead spinner. + */ + _armVisibilityPause() { + if (this._visHandler) return; + this._visHandler = () => { + if (document.hidden) { + if (this.rafId) { + cancelAnimationFrame(this.rafId); + this.rafId = null; + } + } else if (this.isRunning && !this.rafId) { + // Reset the wave clock so the hidden interval doesn't arrive as one + // huge dt and skip the animation forward. + this._wavePrev = performance.now(); + this._requestFrame(); + } + }; + document.addEventListener('visibilitychange', this._visHandler); + } + + _disarmVisibilityPause() { + if (!this._visHandler) return; + document.removeEventListener('visibilitychange', this._visHandler); + this._visHandler = null; } /** @@ -272,12 +341,15 @@ class Spinner { if (this.animation === 'sinewave') { this._wavePrev = performance.now(); + this._armVisibilityPause(); this._drawSineWave(); return; } if (this.animation === 'whirlpool') { this._wpStartedAt = performance.now(); + this._wpUnattachedSince = null; + this._armVisibilityPause(); this._drawWhirlpool(); return; } @@ -302,6 +374,7 @@ class Spinner { cancelAnimationFrame(this.rafId); this.rafId = null; } + this._disarmVisibilityPause(); } /** diff --git a/tests/test_spinner_stops_when_never_attached_js.py b/tests/test_spinner_stops_when_never_attached_js.py new file mode 100644 index 000000000..2335f0b4d --- /dev/null +++ b/tests/test_spinner_stops_when_never_attached_js.py @@ -0,0 +1,319 @@ +"""Pin the self-termination contract of the canvas spinners in static/js/spinner.js. + +Background: the whirlpool spinner drives itself with requestAnimationFrame and +decides whether to keep going by looking at `element.isConnected`. It used to +re-arm forever whenever the element had *never* been connected, on the theory +that start() runs before the caller appends the element. Callers that start a +spinner and then take an early return - an aborted request, a panel that +resolved from cache before the loading row was inserted - therefore left a rAF +loop redrawing an 84-segment spiral into a detached canvas until the tab closed. +Measured on an idle app: ~110 whirlpool frames per second with zero canvases in +the document. + +These tests lock in all four exits (never attached, attached-then-removed, +stop(), tab hidden) and, just as importantly, the one case that must NOT stop: +a spinner that is actually on screen. + +Driven through `node --input-type=module` so the real module runs, same idiom as +test_esc_menu_stack_js.py. The module source is inlined rather than imported by +path because the repo has no `"type": "module"` in package.json; spinner.js has +no imports of its own, so inlining is exact. A fake clock and a manual frame +pump replace performance.now()/requestAnimationFrame, so nothing here depends on +wall-clock time or real frame timing. +""" +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +_REPO = Path(__file__).resolve().parent.parent +_MODULE = _REPO / "static" / "js" / "spinner.js" +_HAS_NODE = shutil.which("node") is not None +_SRC = _MODULE.read_text(encoding="utf-8") if _MODULE.exists() else "" + +# Browser stand-ins, installed before the module body runs. `clock` is advanced +# only by pump(), so every timing decision in the module is deterministic. +_STUBS = r""" +let clock = 0; +Object.defineProperty(globalThis, 'performance', { + value: { now: () => clock }, configurable: true, writable: true, +}); + +const pending = new Map(); +let nextFrameId = 1; +let framesRun = 0; +globalThis.requestAnimationFrame = (cb) => { + const id = nextFrameId++; + pending.set(id, cb); + return id; +}; +globalThis.cancelAnimationFrame = (id) => { pending.delete(id); }; + +/** Advance the clock `steps` frames of `msPerFrame` and run whatever is queued. */ +function pump(steps, msPerFrame = 16) { + for (let i = 0; i < steps; i++) { + clock += msPerFrame; + const due = [...pending.values()]; + pending.clear(); + for (const cb of due) { framesRun++; cb(); } + } +} +function framesPending() { return pending.size; } +function framesSince(mark) { return framesRun - mark; } +function frameMark() { return framesRun; } + +function makeCtx() { + const noop = () => {}; + return { + clearRect: noop, beginPath: noop, arc: noop, moveTo: noop, lineTo: noop, + stroke: noop, fill: noop, save: noop, restore: noop, + strokeStyle: '', fillStyle: '', lineWidth: 0, globalAlpha: 1, + lineCap: '', lineJoin: '', + }; +} + +function makeElement(tag) { + const el = { + tagName: tag, className: '', textContent: '', innerHTML: '', + width: 0, height: 0, isConnected: false, parentNode: null, + style: { cssText: '' }, + children: [], + classList: { add: () => {}, remove: () => {}, contains: () => false }, + getContext: () => makeCtx(), + appendChild(child) { + child.parentNode = this; + this.children.push(child); + return child; + }, + removeChild(child) { + this.children = this.children.filter((c) => c !== child); + child.parentNode = null; + return child; + }, + }; + return el; +} + +const docListeners = []; +globalThis.document = { + hidden: false, + documentElement: makeElement('html'), + createElement: makeElement, + createTextNode: (t) => ({ textContent: t }), + addEventListener: (type, fn) => { docListeners.push([type, fn]); }, + removeEventListener: (type, fn) => { + const i = docListeners.findIndex(([t, f]) => t === type && f === fn); + if (i >= 0) docListeners.splice(i, 1); + }, +}; +globalThis.getComputedStyle = () => ({ getPropertyValue: () => '' }); + +function visibilityListeners() { + return docListeners.filter(([t]) => t === 'visibilitychange').length; +} +function fireVisibility(hidden) { + document.hidden = hidden; + for (const [t, fn] of [...docListeners]) if (t === 'visibilitychange') fn(); +} + +/** A started whirlpool spinner whose element is not in the document. */ +function startedWhirlpool() { + const sp = new Spinner('', 'clean', 'whirlpool'); + sp.createElement(); + sp.start(); + return sp; +} +""" + + +def _run(body: str) -> dict: + """Run `body` with the real spinner module and the browser stubs in scope.""" + js = _STUBS + "\n" + _SRC + "\n" + body + proc = subprocess.run( + ["node", "--input-type=module"], + input=js, capture_output=True, text=True, encoding="utf-8", + cwd=str(_REPO), timeout=30, + ) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout.strip()) + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_never_attached_whirlpool_stops_itself(): + # The leak: element created, spinner started, element never inserted. Past + # the grace window it must give up rather than re-arm forever. + body = """ + const sp = startedWhirlpool(); + pump(30); // 480 ms - inside the grace window + const early = { running: sp.isRunning, pending: framesPending() }; + pump(120); // ~2.4 s total - past the grace window + const mark = frameMark(); + pump(60); // nothing should be left to run + console.log(JSON.stringify({ + early, + running: sp.isRunning, + rafId: sp.rafId, + pending: framesPending(), + framesAfterStop: framesSince(mark), + })); + """ + out = _run(body) + assert out["early"] == {"running": True, "pending": 1}, "gave up during the grace window" + assert out["running"] is False + assert out["rafId"] is None + assert out["pending"] == 0 + assert out["framesAfterStop"] == 0, "loop kept drawing after it gave up" + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_attached_whirlpool_keeps_running_past_the_grace_window(): + # The converse guard: the fix must not kill spinners that are on screen. + body = """ + const sp = new Spinner('', 'clean', 'whirlpool'); + sp.createElement(); + sp.element.isConnected = true; + sp.start(); + pump(400); // ~6.4 s, far past the grace window + const mark = frameMark(); + pump(10); + console.log(JSON.stringify({ + running: sp.isRunning, + pending: framesPending(), + framesDrawn: framesSince(mark), + })); + """ + out = _run(body) + assert out["running"] is True + assert out["pending"] == 1 + assert out["framesDrawn"] == 10, "a visible spinner stopped animating" + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_attached_then_removed_whirlpool_stops(): + # The pre-existing exit - a loading row replaced by results - still works. + body = """ + const sp = new Spinner('', 'clean', 'whirlpool'); + sp.createElement(); + sp.element.isConnected = true; + sp.start(); + pump(200); + const whileAttached = sp.isRunning; + sp.element.isConnected = false; // results arrived, row swapped out + pump(3); + const mark = frameMark(); + pump(20); + console.log(JSON.stringify({ + whileAttached, + running: sp.isRunning, + pending: framesPending(), + framesAfterRemoval: framesSince(mark), + })); + """ + out = _run(body) + assert out["whileAttached"] is True + assert out["running"] is False + assert out["pending"] == 0 + assert out["framesAfterRemoval"] == 0 + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_loading_row_helper_stops_when_the_row_is_never_inserted(): + # createLoadingRow() starts the spinner for the caller and hands back a + # detached row, so a caller that early-returns is the real leak shape. + body = """ + const row = createLoadingRow('Loading...', 16); + pump(200); + const mark = frameMark(); + pump(40); + console.log(JSON.stringify({ + pending: framesPending(), + framesAfterStop: framesSince(mark), + rowHasChildren: row.children.length > 0, + })); + """ + out = _run(body) + assert out["rowHasChildren"] is True, "harness built the wrong row" + assert out["pending"] == 0 + assert out["framesAfterStop"] == 0 + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_stop_cancels_the_pending_frame_and_releases_the_listener(): + # stop() must be authoritative: no queued frame survives it, and it leaves + # no visibilitychange listener behind on a dead spinner. + body = """ + const before = visibilityListeners(); + const sp = new Spinner('', 'clean', 'whirlpool'); + sp.createElement(); + sp.element.isConnected = true; + sp.start(); + const armed = visibilityListeners(); + sp.stop(); + const mark = frameMark(); + pump(20); + console.log(JSON.stringify({ + before, armed, after: visibilityListeners(), + running: sp.isRunning, + rafId: sp.rafId, + pending: framesPending(), + framesAfterStop: framesSince(mark), + })); + """ + out = _run(body) + assert (out["before"], out["armed"], out["after"]) == (0, 1, 0) + assert out["running"] is False + assert out["rafId"] is None + assert out["pending"] == 0 + assert out["framesAfterStop"] == 0 + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_hidden_tab_pauses_frames_and_showing_resumes_them(): + body = """ + const sp = new Spinner('', 'clean', 'whirlpool'); + sp.createElement(); + sp.element.isConnected = true; + sp.start(); + pump(5); + fireVisibility(true); + const hiddenMark = frameMark(); + pump(30); + const whileHidden = { drawn: framesSince(hiddenMark), pending: framesPending() }; + fireVisibility(false); + const shownMark = frameMark(); + pump(10); + console.log(JSON.stringify({ + whileHidden, + running: sp.isRunning, + drawnAfterShow: framesSince(shownMark), + })); + """ + out = _run(body) + assert out["whileHidden"] == {"drawn": 0, "pending": 0}, "kept drawing in a hidden tab" + assert out["running"] is True + assert out["drawnAfterShow"] == 10, "did not resume when the tab came back" + + +@pytest.mark.skipif(not _HAS_NODE, reason="node binary not on PATH") +def test_restarted_spinner_gets_a_fresh_grace_window(): + # The grace deadline is per-run. A spinner reused after stop() must not + # inherit the previous run's timestamp and die on its first frame. + body = """ + const sp = startedWhirlpool(); + pump(200); // times out, never attached + const stopped = sp.isRunning; + sp.element.isConnected = true; // now inserted for real + sp.start(); + pump(30); + console.log(JSON.stringify({ + stopped, + running: sp.isRunning, + pending: framesPending(), + })); + """ + out = _run(body) + assert out["stopped"] is False + assert out["running"] is True + assert out["pending"] == 1 From 1183fe0ff12aa914936ec0a74e55a4bde5d2ff81 Mon Sep 17 00:00:00 2001 From: Michael <52305679+michaelxer@users.noreply.github.com> Date: Wed, 12 Aug 2026 06:40:16 +0700 Subject: [PATCH 055/125] fix(llm): normalise Mistral structured content in llm_call_async (#5882) llm_call_async returned raw list content for Mistral thinking models, breaking callers that expect a str (e.g. auto-title). Match the sync and streaming parsers by running list content through _normalize_mistral_content. Fixes #5435 Co-authored-by: michaelxer Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- src/llm_core.py | 12 +++- tests/test_llm_core_async_mistral_content.py | 71 ++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 tests/test_llm_core_async_mistral_content.py diff --git a/src/llm_core.py b/src/llm_core.py index 3e84c1060..86bf9b7e1 100644 --- a/src/llm_core.py +++ b/src/llm_core.py @@ -2109,7 +2109,17 @@ async def llm_call_async( response = _parse_ollama_response(data) else: msg = data["choices"][0]["message"] - response = msg.get("content") or msg.get("reasoning_content") or "" + content = msg.get("content") + if isinstance(content, list): + # Mistral structured content — extract thinking + text + # (same contract as llm_call / stream_llm; see #5435). + text_part, thinking_part = _normalize_mistral_content(content) + if thinking_part: + response = thinking_part + "\n\n" + (text_part or "") + else: + response = text_part or msg.get("reasoning_content") or "" + else: + response = content or msg.get("reasoning_content") or "" _set_cached_response(cache_key, response) return response except Exception: diff --git a/tests/test_llm_core_async_mistral_content.py b/tests/test_llm_core_async_mistral_content.py new file mode 100644 index 000000000..5d9cbcabf --- /dev/null +++ b/tests/test_llm_core_async_mistral_content.py @@ -0,0 +1,71 @@ +"""Integration regression test for #5435. + +llm_call_async must normalise Mistral structured content to a plain string, +matching llm_call (sync) and stream_llm. Before the fix, the async +non-streaming parser returned the raw list when Mistral reasoning was enabled, +violating its -> str contract, leaking a non-string into callers such as +auto-title generation and memory extraction, and poisoning _response_cache +with a non-string value. +""" +import asyncio + +import src.llm_core as llm_core + + +class _FakeResponse: + is_success = True + status_code = 200 + text = "" + + def __init__(self, payload): + self._payload = payload + + def json(self): + return self._payload + + +def _payload(content): + return {"choices": [{"message": {"role": "assistant", "content": content}}]} + + +def _call(monkeypatch, content): + async def fake_post(client, url, headers, **kwargs): + return _FakeResponse(_payload(content)) + + monkeypatch.setattr(llm_core, "httpx_post_kimi_aware_async", fake_post) + llm_core._response_cache.clear() + return asyncio.run(llm_core.llm_call_async( + "http://mistral.test/v1/chat/completions", + "mistral-medium", + [{"role": "user", "content": "q"}], + )) + + +def test_llm_call_async_normalizes_mistral_structured_content(monkeypatch): + out = _call(monkeypatch, [ + {"type": "thinking", + "thinking": [{"type": "text", "text": "Let me work through this..."}], + "closed": True}, + {"type": "text", "text": "The answer is 42."}, + ]) + assert isinstance(out, str), f"expected str, got {type(out).__name__}" + assert "The answer is 42." in out + assert "Let me work through this..." in out + # The cache must hold the normalised string, not the raw list, + # otherwise repeat calls serve the poisoned value. + assert all(isinstance(v, str) for v in llm_core._response_cache.values()) + + +def test_llm_call_async_thinking_only_still_returns_str(monkeypatch): + out = _call(monkeypatch, [ + {"type": "thinking", + "thinking": [{"type": "text", "text": "still thinking"}], + "closed": True}, + ]) + assert isinstance(out, str) + assert "still thinking" in out + + +def test_llm_call_async_plain_string_passthrough(monkeypatch): + out = _call(monkeypatch, "plain answer") + assert out == "plain answer" From c2b9666def78923d9a6de69588080c87eb78a39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A9o?= Date: Wed, 12 Aug 2026 01:50:55 +0200 Subject: [PATCH 056/125] perf(frontend): preload the two first-paint Fira Code faces (#5992) The app font faces are declared in static/style.css, so the browser only discovers FiraCode-Regular.woff2 and FiraCode-SemiBold.woff2 once the stylesheet has parsed. On a cold load they start about 145 ms in, behind the module graph. font-display: swap keeps that from blocking render, so the cost is a visible swap rather than a stall, but the fetch can start immediately instead. Two preload hints move the request into the head. Measured cold on a scratch instance with an empty cache, three runs per arm: request start 142-203 ms becomes 15-19 ms, response end 174-248 ms becomes 46-63 ms. The total request count is unchanged and each face is still fetched exactly once. crossorigin is required even though these are same-origin: fonts are always fetched in CORS mode, and without it the preload is discarded and the font fetched again. Dropping the attribute produces four font entries in the Resource Timing list instead of two. Only Fira Code 400 and 600 are preloaded. They are the only faces first paint uses. Inter, OpenDyslexic and Fira Code 300 stay unloaded on both desktop and mobile, with or without a saved font preference. --- static/index.html | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/static/index.html b/static/index.html index c73d9a890..37aaf6dc7 100644 --- a/static/index.html +++ b/static/index.html @@ -248,6 +248,15 @@ }, { once: true }); })(); + + + From 93653120d690c9cb1ca2f1f0b2d180cc31859ad1 Mon Sep 17 00:00:00 2001 From: Ashvin <76151462+ashvinctrl@users.noreply.github.com> Date: Wed, 12 Aug 2026 05:39:02 +0530 Subject: [PATCH 057/125] fix(skills): stop SKILL.md frontmatter escapes compounding on every save (#5883) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _emit_scalar quotes a frontmatter scalar with json.dumps when it holds punctuation that would change how the line reads back. _parse_scalar undid that with a bare raw[1:-1]: it stripped the quotes but never decoded the escapes. So a description containing ü was written as the escape sequence \u00fc, read back with that escape still sitting literally in the value, and re-escaped on the next save. The backslash run doubles every save, so a non-English skill description degrades into backslash noise after a few edits, and the escapes are shown verbatim in the skills list and the /skills catalog. This is not limited to non-ASCII. Any description containing a quote takes the same path, since the quote is itself what forces the quoted form. Make the two halves symmetric: emit with ensure_ascii=False, since SKILL.md is UTF-8 at both ends (skills.py reads it, atomic_write_text writes it) and the ASCII-escaped form bought nothing; and parse double-quoted scalars with json.loads, falling back to the previous literal reading when the value is not valid JSON. Files already corrupted heal one level per load. ensure_ascii=False on its own would open a smaller hole. json.dumps escapes every C0 control character but passes NEL, LINE SEPARATOR and PARAGRAPH SEPARATOR through literally, and parse_frontmatter reads one scalar per line via str.splitlines(), which breaks on all three. Re-escape those three, and add them plus the remaining splitlines characters to the set that forces a quoted scalar, so none of them can reach the file bare. Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- services/memory/skill_format.py | 43 +++- ...test_skill_frontmatter_escape_roundtrip.py | 214 ++++++++++++++++++ 2 files changed, 255 insertions(+), 2 deletions(-) create mode 100644 tests/test_skill_frontmatter_escape_roundtrip.py diff --git a/services/memory/skill_format.py b/services/memory/skill_format.py index 628474b04..633f4bec5 100644 --- a/services/memory/skill_format.py +++ b/services/memory/skill_format.py @@ -100,6 +100,18 @@ def _parse_scalar(raw: str) -> Any: if raw.lower() in ("null", "none", "~"): return None if (raw[0] == raw[-1]) and raw[0] in ("'", '"'): + if raw[0] == '"': + # _emit_scalar writes double-quoted scalars with json.dumps, so + # decode the escapes instead of only stripping the quotes. Without + # this, `\"` / `\\` / `\uXXXX` stayed verbatim in the value and the + # next save escaped their backslashes again, doubling them on every + # load/save cycle (issue #5210). + try: + return json.loads(raw) + except ValueError: + # Hand-written file using escapes JSON rejects (e.g. a bare + # Windows path). Keep the previous literal reading. + pass return raw[1:-1] # Try number try: @@ -171,6 +183,26 @@ def parse_frontmatter(text: str) -> tuple[Dict[str, Any], str]: return fm, body +# Characters that force a quoted scalar. The punctuation would otherwise change +# how the value reads back; the second row is every character str.splitlines() +# treats as a line break, and parse_frontmatter() reads one scalar per line, so +# emitting one of those bare would split the value across lines. +_FM_MUST_QUOTE = ( + ":", "#", "[", "]", "{", "}", ",", "&", "*", "!", "|", ">", "'", '"', "%", "@", + "\n", "\r", "\v", "\f", "\x1c", "\x1d", "\x1e", "\x85", "\u2028", "\u2029", +) + +# json.dumps escapes every C0 control character, but with ensure_ascii=False it +# passes NEL / LINE SEPARATOR / PARAGRAPH SEPARATOR through literally, and +# str.splitlines() still breaks on all three. Re-escape exactly those, which +# json.loads decodes again on the way in, so the pair stays symmetric. +_FM_POST_DUMPS_ESCAPES = ( + ("\x85", "\\u0085"), + ("\u2028", "\\u2028"), + ("\u2029", "\\u2029"), +) + + def _emit_scalar(v: Any) -> str: if v is None: return "null" @@ -181,8 +213,15 @@ def _emit_scalar(v: Any) -> str: if isinstance(v, list): return "[" + ", ".join(_emit_scalar(x) for x in v) + "]" s = str(v) - if any(c in s for c in (":", "#", "\n", "[", "]", "{", "}", ",", "&", "*", "!", "|", ">", "'", '"', "%", "@")): - return json.dumps(s) + if any(c in s for c in _FM_MUST_QUOTE): + # ensure_ascii=False keeps non-ASCII text as itself. SKILL.md is UTF-8 at + # both ends (skills.py reads it, atomic_write_text writes it), so the + # \uXXXX form bought nothing and leaked into the parsed value (#5210). + out = json.dumps(s, ensure_ascii=False) + for ch, esc in _FM_POST_DUMPS_ESCAPES: + if ch in out: + out = out.replace(ch, esc) + return out return s diff --git a/tests/test_skill_frontmatter_escape_roundtrip.py b/tests/test_skill_frontmatter_escape_roundtrip.py new file mode 100644 index 000000000..25295832b --- /dev/null +++ b/tests/test_skill_frontmatter_escape_roundtrip.py @@ -0,0 +1,214 @@ +"""Regression for issue #5210 — SKILL.md frontmatter scalars must round-trip. + +``_emit_scalar`` quotes a scalar with ``json.dumps`` whenever it contains +punctuation that would change how the line reads back. ``_parse_scalar`` used +to undo that with a bare ``raw[1:-1]``: it stripped the quotes but never +decoded the escapes. So ``"Pr\\u00fcfung"`` was read back as the literal text +``Pr\\u00fcfung``, and the next save escaped *that* backslash again. + +The damage compounds — each save doubles the backslash run — so a German or +Japanese skill description degrades into backslash noise after a handful of +edits, and the same happens to a plain-ASCII description that merely contains +a quote character. The escapes are also shown verbatim in the skills list and +the ``/skills`` catalog. + +The fix is symmetric: emit with ``ensure_ascii=False`` (SKILL.md is UTF-8 at +both ends) and parse double-quoted scalars with ``json.loads``. +""" + +import json + +import pytest + +from services.memory.skill_format import ( + Skill, + _emit_scalar, + _parse_scalar, + parse_frontmatter, +) +from services.memory.skills import SkillsManager + +# Umlauts plus a comma — the comma is what forces the quoted form, which is the +# only path that was corrupted. Reported verbatim in issue #5210. +GERMAN = "Einstiegs- und Pr\u00fcfungslinie f\u00fcr AGB, Datenschutz" +JAPANESE = "\u30b9\u30ad\u30eb: \u30c6\u30b9\u30c8\u7528\u306e\u8aac\u660e" +QUOTED_ASCII = 'Use the "grep" tool, then summarise' + + +def _cycle(skill: Skill, times: int = 1) -> Skill: + """Save to markdown and read it straight back, `times` times over.""" + for _ in range(times): + skill = Skill.from_markdown(skill.to_markdown()) + return skill + + +# --------------------------------------------------------------------------- +# The reported corruption +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "description", + [GERMAN, JAPANESE, QUOTED_ASCII], + ids=["german", "japanese", "quoted-ascii"], +) +def test_description_survives_repeated_saves(description): + """Five load/save cycles must leave the text byte-identical. + + One cycle is enough to corrupt it; five is where the doubling became + obvious in the field. + """ + result = _cycle(Skill(name="demo", description=description), times=5) + assert result.description == description + + +def test_corruption_does_not_compound_across_saves(): + """Pin the *growth*, not just the mismatch. + + The original defect was not a one-off mangling — the escaped form was + re-escaped on every save, so the value grew without bound. A regression + that reintroduced single-level mangling would still be caught by the test + above; this one catches the runaway specifically. + """ + skill = Skill(name="demo", description=GERMAN) + lengths = [] + for _ in range(5): + skill = _cycle(skill) + lengths.append(len(skill.description)) + assert len(set(lengths)) == 1, f"description length drifted across saves: {lengths}" + + +# --------------------------------------------------------------------------- +# What actually lands on disk +# --------------------------------------------------------------------------- + + +def test_non_ascii_is_written_as_utf8_not_ascii_escapes(): + """SKILL.md is opened as UTF-8 at both ends, so \\uXXXX buys nothing and + only makes the file unreadable to a human editing it.""" + markdown = Skill(name="demo", description=GERMAN).to_markdown() + line = next(l for l in markdown.splitlines() if l.startswith("description:")) + assert "Pr\u00fcfungslinie" in line + assert "\\u00fc" not in line + + +def test_quoted_scalar_is_valid_json(): + """The emitted form is what the parser now feeds to json.loads, so the two + halves cannot drift apart without this failing.""" + emitted = _emit_scalar(QUOTED_ASCII) + assert json.loads(emitted) == QUOTED_ASCII + + +# --------------------------------------------------------------------------- +# Existing files +# --------------------------------------------------------------------------- + + +def test_legacy_ascii_escaped_file_is_read_correctly(): + """Files already written by the old emitter hold real JSON escapes, so the + new parser recovers the intended text instead of the escape source.""" + markdown = '---\nname: demo\ndescription: "Pr\\u00fcfung, x"\n---\n\n' + assert Skill.from_markdown(markdown).description == "Pr\u00fcfung, x" + + +def test_already_corrupted_file_heals_one_level_per_load(): + """A file that took one round of damage carries a doubled backslash. That + is still valid JSON, so reading it yields the single-backslash form and the + value stops degrading.""" + markdown = '---\nname: demo\ndescription: "Pr\\\\u00fcfung, x"\n---\n\n' + once = Skill.from_markdown(markdown) + assert once.description == "Pr\\u00fcfung, x" + # And it is now stable rather than growing on every subsequent save. + assert _cycle(once, times=3).description == "Pr\\u00fcfung, x" + + +def test_non_json_escape_falls_back_to_literal_reading(): + """A hand-written frontmatter value can hold escapes JSON rejects (a bare + Windows path is the common one). Those must keep their previous literal + reading rather than raising.""" + assert _parse_scalar('"C:\\Users\\demo"') == "C:\\Users\\demo" + + +# --------------------------------------------------------------------------- +# Unchanged behaviour +# --------------------------------------------------------------------------- + + +def test_plain_scalars_are_still_emitted_bare(): + """Only values needing quotes get them — the common case must not suddenly + start quoting, which would churn every SKILL.md on disk.""" + assert _emit_scalar("open-pr-from-branch") == "open-pr-from-branch" + assert _emit_scalar("1.0.0") == "1.0.0" + assert _emit_scalar(True) == "true" + assert _emit_scalar(None) == "null" + assert _emit_scalar(0.8) == "0.8" + + +def test_single_quoted_scalar_keeps_literal_reading(): + """Only double-quoted scalars are JSON. Single-quoted ones are read the way + they always were.""" + assert _parse_scalar("'plain, text'") == "plain, text" + + +def test_lists_round_trip_with_non_ascii_entries(): + skill = Skill(name="demo", tags=["b\u00fcro", "recht, steuern"]) + assert _cycle(skill, times=3).tags == ["b\u00fcro", "recht, steuern"] + + +# --------------------------------------------------------------------------- +# Line-break characters +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "sep", + ["\u2028", "\u2029", "\x85", "\r", "\v", "\f", "\x1c"], + ids=["ls", "ps", "nel", "cr", "vt", "ff", "fs"], +) +def test_line_break_characters_do_not_split_the_frontmatter(sep): + """parse_frontmatter() reads one scalar per line via str.splitlines(), + which breaks on far more than \\n. Any of these landing unescaped in the + file would silently truncate the value and shift the remainder into a + bogus key. + + json.dumps covers the C0 ones, but with ensure_ascii=False it passes NEL, + LINE SEPARATOR and PARAGRAPH SEPARATOR through as themselves — so those + three are re-escaped explicitly. + """ + description = f"before{sep}after, x" + markdown = Skill(name="demo", description=description).to_markdown() + + frontmatter_text = markdown.split("---")[1] + assert len(frontmatter_text.strip().splitlines()) == len( + [l for l in frontmatter_text.strip().split("\n") if l.strip()] + ), "a scalar leaked a line break into the frontmatter" + + fm, _body = parse_frontmatter(markdown) + assert fm["description"] == description + + +# --------------------------------------------------------------------------- +# End to end, through real files +# --------------------------------------------------------------------------- + + +def test_description_survives_real_save_load_cycles_on_disk(tmp_path): + """The unit tests above go straight through to_markdown/from_markdown. + This drives the same path the app does — SkillsManager writing UTF-8 files + with atomic_write_text and reading them back — because the encoding used at + either end is part of the fix. + """ + manager = SkillsManager(str(tmp_path)) + manager.add_skill(name="agb-pruefung", description=GERMAN, category="general") + + for _ in range(4): + assert manager.update_skill("agb-pruefung", {"status": "published"}) + + stored = [s for s in manager.load_all() if s["name"] == "agb-pruefung"] + assert len(stored) == 1 + assert stored[0]["description"] == GERMAN + + on_disk = (tmp_path / "skills" / "general" / "agb-pruefung" / "SKILL.md").read_text( + encoding="utf-8" + ) + assert GERMAN in on_disk From 5a016e492cdb5e39ba862fbb443355db1045fc39 Mon Sep 17 00:00:00 2001 From: Manuel Cartagena Herrera Date: Tue, 11 Aug 2026 20:23:43 -0400 Subject: [PATCH 058/125] fix(gallery): handle MPS float64 mask inputs (#5903) Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- routes/gallery/gallery_routes.py | 29 ++++++++++++++------ tests/test_gallery_model_input_device.py | 34 ++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 tests/test_gallery_model_input_device.py diff --git a/routes/gallery/gallery_routes.py b/routes/gallery/gallery_routes.py index 457df210d..e6b5e0713 100644 --- a/routes/gallery/gallery_routes.py +++ b/routes/gallery/gallery_routes.py @@ -127,6 +127,25 @@ def _load_grounding_backend(): return cached +def _model_input_to_device(value, device: str, torch): + if not hasattr(value, "to"): + return value + if ( + device == "mps" + and hasattr(torch, "float64") + and getattr(value, "dtype", None) == torch.float64 + ): + return value.to(device=device, dtype=torch.float32) + return value.to(device) + + +def _model_inputs_to_device(inputs, device: str, torch) -> Dict[str, Any]: + return { + key: _model_input_to_device(value, device, torch) + for key, value in inputs.items() + } + + def _ground_text_to_box(image, text: str, *, threshold: float = 0.05): query = (text or "").strip() if not query: @@ -142,10 +161,7 @@ def _ground_text_to_box(image, text: str, *, threshold: float = 0.05): labels.append(f"a photo of {query}") try: inputs = processor(text=[labels], images=image, return_tensors="pt") - model_inputs = { - k: (v.to(device) if hasattr(v, "to") else v) - for k, v in inputs.items() - } + model_inputs = _model_inputs_to_device(inputs, device, torch) with torch.no_grad(): outputs = model(**model_inputs) target_sizes = torch.tensor([[image.height, image.width]]) @@ -1869,10 +1885,7 @@ def setup_gallery_routes() -> APIRouter: try: inputs = processor(image, **kwargs) - model_inputs = { - k: (v.to(device) if hasattr(v, "to") else v) - for k, v in inputs.items() - } + model_inputs = _model_inputs_to_device(inputs, device, torch) with torch.no_grad(): outputs = model(**model_inputs) masks = processor.image_processor.post_process_masks( diff --git a/tests/test_gallery_model_input_device.py b/tests/test_gallery_model_input_device.py new file mode 100644 index 000000000..7e63eb000 --- /dev/null +++ b/tests/test_gallery_model_input_device.py @@ -0,0 +1,34 @@ +import routes.gallery_routes as gallery_routes + + +class _TorchSentinel: + float32 = object() + float64 = object() + + +class _FakeTensor: + def __init__(self, dtype): + self.dtype = dtype + self.to_args = None + + def to(self, *args, **kwargs): + self.to_args = (args, kwargs) + return self + + +def test_model_inputs_to_device_casts_mps_float64_to_float32(): + float_tensor = _FakeTensor(_TorchSentinel.float64) + int_tensor = _FakeTensor("int64") + plain_value = object() + + result = gallery_routes._model_inputs_to_device( + {"points": float_tensor, "labels": int_tensor, "plain": plain_value}, + "mps", + _TorchSentinel, + ) + + assert result["points"] is float_tensor + assert float_tensor.to_args == ((), {"device": "mps", "dtype": _TorchSentinel.float32}) + assert int_tensor.to_args == (("mps",), {}) + assert result["plain"] is plain_value + From bea48c749c59041ae78710afbf95456ee9eaf796 Mon Sep 17 00:00:00 2001 From: Austin Roddy <39446029+austinroddy@users.noreply.github.com> Date: Tue, 11 Aug 2026 18:28:30 -0600 Subject: [PATCH 059/125] fix(sidebar): keep minimized icon rail in sync with per-tab visibility (#5987) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(sidebar): keep minimized icon rail in sync with per-tab visibility Per-tab visibility (Customize UI / Appearance checkboxes, stored in localStorage under `odysseus-ui-visibility`) was only applied to the full sidebar elements — `UI_VIS_MAP` never targeted the collapsed `#icon-rail` launchers. So a user who turned a tab off (e.g. Email) in the full view saw every tab reappear when minimizing the sidebar to the icon rail. Pair each tool/section selector with its `#rail-*` counterpart (mapping mirrors `_railToolMap`), so `applyUIVis()` hides the rail launcher too. Admin feature-flag handling is unaffected: the features-fetch reconcile at app.js already re-applies `applyUIVis()`, so rail launchers now track admin disables exactly like their sidebar buttons. Adds a static regression test asserting every customizable tab pairs its rail button. Co-Authored-By: Claude * refactor(sidebar): extract UI visibility into testable module Move UI_VIS_MAP, UI_VIS_DEFAULT_OFF, and a pure resolveVisibility() into static/js/ui_visibility.js so the icon-rail visibility rules are unit testable without a DOM. app.js applies resolveVisibility() to the document, replacing the ad-hoc tools-section override with an inline parent rule (tools-section off hides every tool rail launcher). Add edge-case tests covering per-tool off, the tools-section parent rule, parent+child combos, email-section, and the tool-library <-> #rail-archive mapping. Refs #5985 Co-Authored-By: Claude --------- Co-authored-by: Claude Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- static/app.js | 51 ++---------- static/js/ui_visibility.js | 73 +++++++++++++++++ tests/test_ui_visibility_js.py | 139 +++++++++++++++++++++++++++++++++ 3 files changed, 218 insertions(+), 45 deletions(-) create mode 100644 static/js/ui_visibility.js create mode 100644 tests/test_ui_visibility_js.py diff --git a/static/app.js b/static/app.js index 514d59d8b..dae6b31a6 100644 --- a/static/app.js +++ b/static/app.js @@ -28,6 +28,7 @@ import memoryModule from './js/memory.js?v=20260722memoryloading1'; import voiceRecorderModule from './js/voiceRecorder.js'; import censorModule from './js/censor.js'; import galleryModule from './js/gallery.js'; +import { UI_VIS_DEFAULT_OFF, resolveVisibility } from './js/ui_visibility.js'; import tasksModule from './js/tasks.js?v=20260723tasksbulkfeedback1'; import calendarModule from './js/calendar.js'; import notesModule from './js/notes.js'; @@ -2725,46 +2726,6 @@ function initializeEventListeners() { // ── UI Visibility (Customize UI modal) ── const UI_VIS_KEY = 'odysseus-ui-visibility'; - // Selector map: key → CSS selector(s) for targets - const UI_VIS_MAP = { - 'sidebar-brand': '.sidebar-brand-title', - 'sidebar-new-chat': '#sidebar-new-chat-btn', - 'sidebar-search': '#sidebar-search-btn', - 'sessions-section': '#sessions-section', - 'email-section': '#email-section', - 'tools-section': '#tools-section', - // Per-tool visibility — fine-grained control over which entries show - // inside the Tools section in the sidebar. - 'tool-calendar': '#tool-calendar-btn', - 'tool-compare': '#tool-compare-btn', - 'tool-cookbook': '#tool-cookbook-btn', - 'tool-research': '#tool-research-btn', - 'tool-gallery': '#tool-gallery-btn', - 'tool-library': '#tool-library-btn', - 'tool-memory': '#tool-memory-btn', - 'tool-notes': '#tool-notes-btn', - 'tool-tasks': '#tool-tasks-btn', - 'tool-theme': '#tool-theme-btn', - 'user-bar': '#user-bar-profile', - 'sidebar-settings-btn':'#user-bar-settings', - 'chat-meta': '.chat-meta-overlay', - 'welcome-text': '.welcome-name, .welcome-sub, #welcome-tip', - 'incognito-btn': '.incognito-btn', - 'web-toggle-btn': '#web-toggle-btn', - 'doc-toggle-btn': '#overflow-doc-btn', - 'rag-toggle-btn': '#overflow-rag-btn', - 'bash-toggle-btn': '#bash-toggle-btn', - 'overflow-plus-btn': '.overflow-wrapper', - 'mode-toggle': '.mode-toggle', - 'preset-mini-btn': '#overflow-preset-btn', - 'attach-btn': '#overflow-attach-btn', - 'research-btn': '#overflow-research-btn', - 'rail-new-chat': '#rail-new-session', - }; - - // Keys hidden by default on first run (no localStorage yet) - const UI_VIS_DEFAULT_OFF = new Set(['rag-toggle-btn', 'text-emojis', 'chat-fullwidth']); - // Keys that need admin to toggle off (reserved for future use) const UI_VIS_ADMIN_ONLY = new Set([]); @@ -2777,14 +2738,14 @@ function initializeEventListeners() { } function applyUIVis(state) { - Object.entries(UI_VIS_MAP).forEach(([key, selector]) => { - // section-drag-reorder uses a body class instead of inline styles - if (key === 'section-drag-reorder') return; - const visible = key in state ? state[key] !== false : !UI_VIS_DEFAULT_OFF.has(key); + // resolveVisibility computes selector→visible (pure; ui_visibility.js), + // including the tools-section parent rule that hides every tool rail + // launcher when Tools is off. Apply the result to the DOM here. + for (const [selector, visible] of Object.entries(resolveVisibility(state))) { document.querySelectorAll(selector).forEach(el => { el.style.display = visible ? '' : 'none'; }); - }); + } // Drag reorder: use body class so dynamically created handles are covered const dragEnabled = state['section-drag-reorder'] === true; document.body.classList.toggle('rearrange-mode', dragEnabled); diff --git a/static/js/ui_visibility.js b/static/js/ui_visibility.js new file mode 100644 index 000000000..f64c23fa9 --- /dev/null +++ b/static/js/ui_visibility.js @@ -0,0 +1,73 @@ +// static/js/ui_visibility.js +// +// Per-item visibility for the sidebar and collapsed icon rail. Drives the +// Settings → Appearance ("Customize UI") checkboxes, persisted in localStorage +// under `odysseus-ui-visibility` (loaded/saved by app.js). +// +// Each key maps to the CSS selector(s) it controls. Tool/section selectors pair +// the full-sidebar element with its #rail-* launcher so a tab hidden in the +// full view also hides when the sidebar is minimized to the icon rail (id +// mapping mirrors _railToolMap in app.js; #tool-library-btn ↔ #rail-archive). + +// Selector map: UI customization key → CSS selector(s) for its target(s). +export const UI_VIS_MAP = { + 'sidebar-brand': '.sidebar-brand-title', + 'sidebar-new-chat': '#sidebar-new-chat-btn', + 'sidebar-search': '#sidebar-search-btn', + 'sessions-section': '#sessions-section', + 'email-section': '#email-section, #rail-email', + 'tools-section': '#tools-section', + // Per-tool entries pair the sidebar button with its rail launcher. + 'tool-calendar': '#tool-calendar-btn, #rail-calendar', + 'tool-compare': '#tool-compare-btn, #rail-compare', + 'tool-cookbook': '#tool-cookbook-btn, #rail-cookbook', + 'tool-research': '#tool-research-btn, #rail-research', + 'tool-gallery': '#tool-gallery-btn, #rail-gallery', + 'tool-library': '#tool-library-btn, #rail-archive', + 'tool-memory': '#tool-memory-btn, #rail-memory', + 'tool-notes': '#tool-notes-btn, #rail-notes', + 'tool-tasks': '#tool-tasks-btn, #rail-tasks', + 'tool-theme': '#tool-theme-btn, #rail-theme', + 'user-bar': '#user-bar-profile', + 'sidebar-settings-btn':'#user-bar-settings', + 'chat-meta': '.chat-meta-overlay', + 'welcome-text': '.welcome-name, .welcome-sub, #welcome-tip', + 'incognito-btn': '.incognito-btn', + 'web-toggle-btn': '#web-toggle-btn', + 'doc-toggle-btn': '#overflow-doc-btn', + 'rag-toggle-btn': '#overflow-rag-btn', + 'bash-toggle-btn': '#bash-toggle-btn', + 'overflow-plus-btn': '.overflow-wrapper', + 'mode-toggle': '.mode-toggle', + 'preset-mini-btn': '#overflow-preset-btn', + 'attach-btn': '#overflow-attach-btn', + 'research-btn': '#overflow-research-btn', + 'rail-new-chat': '#rail-new-session', +}; + +// Keys hidden by default on first run (no localStorage yet). +export const UI_VIS_DEFAULT_OFF = new Set(['rag-toggle-btn', 'text-emojis', 'chat-fullwidth']); + +/** + * Resolve every UI_VIS_MAP selector to visible (true) or hidden (false) for the + * given saved state. Pure: no DOM, no localStorage — app.js applies the result. + * + * A key is visible when its stored value is not `false`, defaulting to on + * unless it is in UI_VIS_DEFAULT_OFF. Per-tool entries also require the Tools + * section to be on: hiding Tools hides every tool, mirroring the full sidebar + * where the #tools-section container already hides them (the rail has no + * container, so this rule keeps it in sync). + * + * @param {Record} state + * @returns {Record} selector → visible + */ +export const resolveVisibility = (state = {}) => { + const toolsOn = state['tools-section'] !== false; + const out = {}; + for (const [key, selector] of Object.entries(UI_VIS_MAP)) { + let visible = key in state ? state[key] !== false : !UI_VIS_DEFAULT_OFF.has(key); + if (!toolsOn && key.startsWith('tool-')) visible = false; + out[selector] = visible; + } + return out; +}; \ No newline at end of file diff --git a/tests/test_ui_visibility_js.py b/tests/test_ui_visibility_js.py new file mode 100644 index 000000000..55e678a86 --- /dev/null +++ b/tests/test_ui_visibility_js.py @@ -0,0 +1,139 @@ +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[1] +pytestmark = pytest.mark.skipif(not shutil.which("node"), reason="node binary not on PATH") + + +def _node_eval(source): + result = subprocess.run( + ["node", "--input-type=module", "-e", source], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ) + return json.loads(result.stdout) + + +def _resolve(state): + """Run resolveVisibility(state) in node; return {selector: visible}.""" + return _node_eval( + f""" + const {{ resolveVisibility }} = await import('./static/js/ui_visibility.js'); + console.log(JSON.stringify(resolveVisibility({json.dumps(state)}))); + """ + ) + + +def _map(): + return _node_eval( + """ + const { UI_VIS_MAP } = await import('./static/js/ui_visibility.js'); + console.log(JSON.stringify(UI_VIS_MAP)); + """ + ) + + +# Selectors (kept in one place so the tests read as plain assertions). +EMAIL = "#email-section, #rail-email" +TOOLS = "#tools-section" +CAL = "#tool-calendar-btn, #rail-calendar" +COMPARE = "#tool-compare-btn, #rail-compare" +LIB = "#tool-library-btn, #rail-archive" +RESEARCH = "#tool-research-btn, #rail-research" +NEWCHAT = "#rail-new-session" +RAG = "#overflow-rag-btn" + +# Full-sidebar tabs that have an icon-rail counterpart must pair it into their +# UI_VIS_MAP selector; otherwise minimizing the sidebar re-shows a tab the user +# turned off in the full view (#tool-library-btn's rail counterpart is #rail-archive). +EXPECTED_RAIL_PAIRS = { + "email-section": "#rail-email", + "tool-calendar": "#rail-calendar", + "tool-compare": "#rail-compare", + "tool-cookbook": "#rail-cookbook", + "tool-research": "#rail-research", + "tool-gallery": "#rail-gallery", + "tool-library": "#rail-archive", + "tool-memory": "#rail-memory", + "tool-notes": "#rail-notes", + "tool-tasks": "#rail-tasks", + "tool-theme": "#rail-theme", +} + + +def test_every_customizable_tab_pairs_its_rail_button(): + ui_vis_map = _map() + missing = { + key: rail + for key, rail in EXPECTED_RAIL_PAIRS.items() + if rail not in ui_vis_map.get(key, "") + } + assert not missing, ( + "these tabs are missing their icon-rail counterpart in UI_VIS_MAP " + f"(minimizing the sidebar would re-show them): {missing}" + ) + + +def test_defaults_everything_visible_except_default_off(): + m = _resolve({}) + assert m[EMAIL] is True + assert m[TOOLS] is True + assert m[CAL] is True + assert m[NEWCHAT] is True + assert m[RAG] is False # rag-toggle-btn is default-off + + +def test_email_off_hides_email_and_its_rail_only(): + m = _resolve({"email-section": False}) + assert m[EMAIL] is False + assert m[CAL] is True + assert m[TOOLS] is True + + +def test_tool_off_hides_its_rail_launcher(): + m = _resolve({"tool-calendar": False}) + assert m[CAL] is False + assert m[COMPARE] is True + + +def test_library_off_hides_archive_rail(): + # tool-library's rail counterpart is #rail-archive (mirrors _railToolMap). + m = _resolve({"tool-library": False}) + assert m[LIB] is False + + +def test_tools_off_hides_every_tool_rail_but_not_email(): + m = _resolve({"tools-section": False}) + assert m[TOOLS] is False + for sel in (CAL, COMPARE, LIB, RESEARCH): + assert m[sel] is False, sel + assert m[EMAIL] is True # email is independent of the Tools section + + +def test_tools_off_overrides_per_tool_on(): + # A tool individually "on" must still hide when its parent Tools is off. + m = _resolve({"tools-section": False, "tool-calendar": True}) + assert m[CAL] is False + + +def test_tools_on_with_tool_off_hides_only_that_tool(): + m = _resolve({"tools-section": True, "tool-research": False}) + assert m[RESEARCH] is False + assert m[CAL] is True + + +def test_rail_new_chat_off_hides_new_session(): + m = _resolve({"rail-new-chat": False}) + assert m[NEWCHAT] is False + + +def test_explicit_false_takes_precedence_over_default_on(): + m = _resolve({"rag-toggle-btn": True}) + assert m[RAG] is True \ No newline at end of file From d87a91372919e3cc8c42d4b245cac20e3de05140 Mon Sep 17 00:00:00 2001 From: Samy <12219635+touzenesmy@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:47:11 -0400 Subject: [PATCH 060/125] fix(ui): stop stripping the word assistant from rendered text (#5974) * fix: stop stripping the word 'assistant' from rendered text The QWEN_BARE_MARKER_RE regex in both the Python backend (tool_parsing.py) and JS frontend (chatRenderer.js) was matching any standalone occurrence of the word 'assistant' separated by any whitespace, then replacing it with a space. This caused normal English uses like 'Home assistant' to render as 'Home '. Fixed by narrowing the word-boundary check from [\t\r\n ] (any whitespace) to [\r\n] (line boundaries only), so only Qwen-format role-token leaks (where 'assistant' appears alone on a line) are stripped. * fix(tests): update bare-marker test expectations for #5971 Move 'x assistant y' from STRIPPED to KEPT (mid-sentence must survive). Add 'Before\nassistant\nAfter' to STRIPPED (bare-marker on own line). * fix(ui): strip whitespace-padded assistant role markers --------- Co-authored-by: samy Co-authored-by: Alexandre Teixeira --- src/tool_parsing.py | 2 +- static/js/chatRenderer.js | 2 +- tests/test_tool_parsing_bare_end_marker.py | 5 ++++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/tool_parsing.py b/src/tool_parsing.py index 98dc1b5f6..5b5d283ec 100644 --- a/src/tool_parsing.py +++ b/src/tool_parsing.py @@ -193,7 +193,7 @@ _QWEN_ROLE_MARKER_RE = re.compile(r"?|< // Keep in sync with _QWEN_BARE_MARKER_RE in src/tool_parsing.py. At least one // pipe is required around `end`: with both optional (`\|?end\|?`) this also ate // a bare `end` on its own line, breaking Ruby/Lua/shell snippets (#5547). -const QWEN_BARE_MARKER_RE = /(?:^|[\t\r\n ])(?:\/?\|end\||\|end|end\|)(?=[\t\r\n ]|$)|(?:^|[\t\r\n ])assistan(?:t)?(?=[\t\r\n ]|$)/gi; +const QWEN_BARE_MARKER_RE = /(?:^|[\t\r\n ])(?:\/?\|end\||\|end|end\|)(?=[\t\r\n ]|$)|(?:^|[\r\n])[ \t]*assistan(?:t)?[ \t]*(?=[\r\n]|$)/gi; // Self-narration about tool results (model echoing stdout/exit_code) const TOOL_NARRATION_RE = /(?:The (?:result|output) shows?:?\s*)?-?\s*(?:stdout|stderr|exit_code):\s*.+/gi; diff --git a/tests/test_tool_parsing_bare_end_marker.py b/tests/test_tool_parsing_bare_end_marker.py index 6167c8dde..703cf616d 100644 --- a/tests/test_tool_parsing_bare_end_marker.py +++ b/tests/test_tool_parsing_bare_end_marker.py @@ -34,6 +34,7 @@ KEPT = [ ("append end", "append end"), ("END", "END"), ("\nEnd\n", "End"), + ("x assistant y", "x assistant y"), # mid-sentence must survive (#5971) ] # Real markers — at least one pipe, plus the role word — with the exact output @@ -44,7 +45,9 @@ STRIPPED = [ ("a /|end| b", "a b"), ("a |end b", "a b"), ("a end| b", "a b"), - ("x assistant y", "x y"), + ("Before\nassistant\nAfter", "Before \nAfter"), # bare-marker on its own line still stripped + ("Before\n assistant\t \nAfter", "Before \nAfter"), # whitespace-padded marker still stripped + ("Before\n\tassistan \nAfter", "Before \nAfter"), # truncated marker variant still stripped ] From adfe3ab37984d1a96ae64c64d884eaa8c961628a Mon Sep 17 00:00:00 2001 From: talentlesshack Date: Wed, 12 Aug 2026 03:01:52 +0200 Subject: [PATCH 061/125] fix(llm): alias tool names that collide with gpt-oss built-ins (#5878) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit gpt-oss (harmony) ships BUILT-IN tools named python/browser, invoked with the raw body as the argument (to=python + bare source), while custom functions use to=functions.NAME + JSON. Exposing our own tools under those names makes the model answer with the built-in convention: it emits raw code, the server parses it as JSON, and the request dies with 'error parsing tool call: raw=import sys, ...'. In streaming mode Ollama does not report it at all — it truncates the stream, so the turn arrives as an empty response and the agent loop reads it as a model stall. bash collides the same way in practice. Measured on gpt-oss:20b via Ollama /v1, fixed agentic prompt, 12 runs per arm: python+bash as-is 2/12, python renamed 10/12, both renamed 12/12. 74 HTTP 500s were logged server-side during investigation with zero surfaced to the client. Rename the colliding tools on the outbound payload and map the names back on responses. Transport-only and gated on gpt-oss: every other model's schemas pass through untouched (asserted in tests). Co-authored-by: Claude Fable 5 Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- src/llm_core.py | 63 +++++++++++++++++++++-- tests/test_harmony_tool_aliasing.py | 78 +++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 4 deletions(-) create mode 100644 tests/test_harmony_tool_aliasing.py diff --git a/src/llm_core.py b/src/llm_core.py index 86bf9b7e1..dd112cedc 100644 --- a/src/llm_core.py +++ b/src/llm_core.py @@ -1,6 +1,7 @@ # src/llm_core.py import httpx import asyncio +import copy import time import json import logging @@ -644,7 +645,7 @@ def _build_ollama_payload( if options: payload["options"] = options if tools: - payload["tools"] = tools + payload["tools"] = _alias_harmony_tools(tools, model) return payload @@ -1055,6 +1056,57 @@ def _model_disallows_reasoning_effort_with_chat_tools(model: str) -> bool: return bool(re.match(r"^(?:openai/)?gpt-5(?:[.\-]\d+)?(?:[-_:].*)?$", m)) +# gpt-oss (harmony) ships BUILT-IN tools named `python` and `browser`, invoked +# with the raw body as the argument (`to=python` + bare source), while custom +# functions use `to=functions.NAME` + JSON. A tool we expose under a built-in's +# name therefore gets called with the built-in convention: the model emits raw +# code, the server tries to parse it as JSON, and the whole request dies +# ("error parsing tool call: raw='import sys, ...'"). In streaming mode Ollama +# does not even report it — it truncates the stream, so the turn looks like an +# empty response. `bash` collides the same way in practice. +# +# Measured on gpt-oss:20b via Ollama /v1 with a fixed agentic prompt: +# tools named python+bash ............ 2/6 succeeded (4 parse failures) +# python renamed ..................... 5/6 +# python and bash renamed ............ 6/6 +# +# So rename the colliding tools on the way out and map the names back on the +# way in. Confined to the transport layer: callers keep using the real names. +_HARMONY_TOOL_ALIASES = { + "python": "run_python_code", + "bash": "run_shell_command", + "browser": "web_browser_tool", +} +_HARMONY_TOOL_ALIASES_REVERSE = {v: k for k, v in _HARMONY_TOOL_ALIASES.items()} + + +def _is_harmony_model(model: str) -> bool: + """True for gpt-oss / harmony-format models, which have built-in tool names.""" + return "gpt-oss" in (model or "").lower() + + +def _alias_harmony_tools(tools: Optional[List[Dict]], model: str) -> Optional[List[Dict]]: + """Rename tools that collide with harmony built-ins. Returns a copy.""" + if not tools or not _is_harmony_model(model): + return tools + out = [] + for t in tools: + fn = t.get("function") or {} + alias = _HARMONY_TOOL_ALIASES.get(fn.get("name")) + if alias: + t = copy.deepcopy(t) + t["function"]["name"] = alias + out.append(t) + return out + + +def _unalias_harmony_tool_name(name: str, model: str) -> str: + """Map an aliased tool name in a model response back to the real name.""" + if not _is_harmony_model(model): + return name + return _HARMONY_TOOL_ALIASES_REVERSE.get(name, name) + + def _scrub_openai_chat_tool_reasoning(payload: Dict, target_url: str, model: str) -> None: if not payload.get("tools"): return @@ -2236,7 +2288,7 @@ async def _stream_llm_inner(url: str, model: str, messages: List[Dict], temperat tok_key = "max_completion_tokens" if _uses_max_completion_tokens(model) else "max_tokens" payload[tok_key] = max_tokens if tools: - payload["tools"] = tools + payload["tools"] = _alias_harmony_tools(tools, model) elif tool_choice_none: payload["tool_choice"] = "none" # Mistral thinking-capable models — send reasoning_effort so Mistral @@ -2370,7 +2422,7 @@ async def _stream_llm_inner(url: str, model: str, messages: List[Dict], temperat if fn.get("name"): _ollama_tool_calls.append({ "id": tc.get("id") or f"call_{len(_ollama_tool_calls)}", - "name": fn.get("name") or "", + "name": _unalias_harmony_tool_name(fn.get("name") or "", model), "arguments": json.dumps(fn.get("arguments") or {}), }) if j.get("done"): @@ -2750,7 +2802,10 @@ async def _stream_llm_inner(url: str, model: str, messages: List[Dict], temperat if tc.get("extra_content"): _tc_acc[idx]["extra_content"] = tc["extra_content"] if func.get("name"): - _tc_acc[idx]["name"] = func["name"] + # Map harmony aliases back to real + # tool names before anything + # downstream sees them. + _tc_acc[idx]["name"] = _unalias_harmony_tool_name(func["name"], model) if "arguments" in func: # Guard against a null arguments delta: `func` can be # {"arguments": None} (JSON null), and a raw `+= None` diff --git a/tests/test_harmony_tool_aliasing.py b/tests/test_harmony_tool_aliasing.py new file mode 100644 index 000000000..dded8d19c --- /dev/null +++ b/tests/test_harmony_tool_aliasing.py @@ -0,0 +1,78 @@ +"""Tools whose names collide with harmony built-ins must be aliased for gpt-oss. + +gpt-oss (harmony format) ships BUILT-IN tools named `python` and `browser`, +invoked with the raw body as the argument (`to=python` + bare source), while +custom functions use `to=functions.NAME` + JSON. Exposing our own tool under a +built-in's name makes the model answer with the built-in convention: it emits +raw code, the server tries to parse it as JSON, and the request dies with +"error parsing tool call: raw='import sys, ...'". Streaming is worse — Ollama +truncates the stream instead of reporting it, so the turn looks like an empty +response and the agent loop reads it as a stall. + +Measured on gpt-oss:20b via Ollama /v1 with a fixed agentic prompt: +python+bash as-is 2/6, python renamed 5/6, both renamed 6/6. + +The aliasing is transport-only and gpt-oss-only: every other model's tool +schemas must pass through untouched, and real tool names must come back out. +""" +from src.llm_core import ( + _alias_harmony_tools, + _unalias_harmony_tool_name, + _is_harmony_model, +) + + +def _tools(*names): + return [ + {"type": "function", "function": {"name": n, "parameters": {}}} + for n in names + ] + + +def _names(tools): + return [t["function"]["name"] for t in tools] + + +def test_gpt_oss_colliding_names_are_aliased(): + out = _alias_harmony_tools(_tools("python", "bash", "web_search"), "gpt-oss:20b") + assert _names(out) == ["run_python_code", "run_shell_command", "web_search"] + + +def test_non_harmony_models_are_untouched(): + tools = _tools("python", "bash", "web_search") + for model in ("qwen3-coder:30b", "gemma4:12b", "claude-opus-5", "gpt-4o", "llama-3.3"): + out = _alias_harmony_tools(tools, model) + assert _names(out) == ["python", "bash", "web_search"], model + assert out is tools, f"{model} should get the same list object, not a copy" + + +def test_aliasing_does_not_mutate_the_caller_list(): + tools = _tools("python") + _alias_harmony_tools(tools, "gpt-oss:20b") + assert _names(tools) == ["python"], "caller's schema list must not be mutated" + + +def test_response_names_map_back_for_gpt_oss(): + assert _unalias_harmony_tool_name("run_python_code", "gpt-oss:20b") == "python" + assert _unalias_harmony_tool_name("run_shell_command", "gpt-oss:20b") == "bash" + # Unrelated names pass through untouched. + assert _unalias_harmony_tool_name("web_search", "gpt-oss:20b") == "web_search" + + +def test_response_names_untouched_for_other_models(): + # A non-harmony model that genuinely has a tool called run_python_code + # must not have it rewritten to `python`. + assert _unalias_harmony_tool_name("run_python_code", "qwen3-coder:30b") == "run_python_code" + + +def test_harmony_detection(): + assert _is_harmony_model("gpt-oss:20b") is True + assert _is_harmony_model("GPT-OSS:120B") is True + assert _is_harmony_model("qwen3-coder:30b") is False + assert _is_harmony_model("") is False + assert _is_harmony_model(None) is False + + +def test_empty_and_none_tools_are_safe(): + assert _alias_harmony_tools(None, "gpt-oss:20b") is None + assert _alias_harmony_tools([], "gpt-oss:20b") == [] From 1976fe1b602fad3ceeb8a1c0219d08a89281db89 Mon Sep 17 00:00:00 2001 From: leepokai <109857817+leepokai@users.noreply.github.com> Date: Wed, 12 Aug 2026 09:41:03 +0800 Subject: [PATCH 062/125] fix(tools): parse Hermes/Qwen JSON bodies inside tool_call wrappers (#5887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parse_tool_blocks fed wrapper bodies only to the XML iterators (_iter_xml_invoke/_iter_xml_direct), so the canonical Qwen/Hermes text-mode form — a bare JSON object like {"name": "bash", "arguments": {"command": "..."}} inside the wrapper — parsed to zero tool blocks and the agent never executed anything. Pattern 4d only matches OpenAI-style blobs with a literal "function" key, which the Hermes format lacks. Wrapper bodies are now classified first: a JSON-looking body ({ or [) is parsed by the new _parse_json_tool_call_body, which requires an object with a string "name" and rejects a non-object "arguments" instead of coercing it, then converts through the same function_call_to_tool_block used by the XML paths so aliases and per-tool argument formatting stay uniform. JSON-looking bodies fail closed — they are never rescanned by the XML iterators (including the unclosed-wrapper and bare-invoke fallbacks), so XML-like text inside JSON argument values stays data instead of selecting a different tool. Non-JSON bodies keep the existing XML path unchanged. Fixes #5187 Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- src/tool_parsing.py | 65 +++++++++++++++++- tests/test_tool_parsing_hermes_json.py | 94 ++++++++++++++++++++++++++ 2 files changed, 157 insertions(+), 2 deletions(-) create mode 100644 tests/test_tool_parsing_hermes_json.py diff --git a/src/tool_parsing.py b/src/tool_parsing.py index 5b5d283ec..b13f3b0a1 100644 --- a/src/tool_parsing.py +++ b/src/tool_parsing.py @@ -929,6 +929,46 @@ def _parse_xml_direct_tool(name, body) -> Optional[ToolBlock]: return function_call_to_tool_block(mapped, json.dumps(params)) +def _looks_like_json_body(body: str) -> bool: + """True when a wrapper body is JSON, not XML markup.""" + return body.lstrip()[:1] in ("{", "[") + + +def _parse_json_tool_call_body(body: str) -> Optional[ToolBlock]: + """Parse a Qwen/Hermes text-mode wrapper body: bare JSON inside . + + + {"name": "bash", "arguments": {"command": "mkdir -p agent-test"}} + + + Strict by design (issue #5187 / tracker #5333): the body must decode to an + object with a string "name", and "arguments" — when present — must itself + be an object. Anything else returns None rather than being coerced, so a + malformed call is dropped instead of dispatching with mangled arguments. + raw_decode tolerates trailing chatter after the JSON object; the trailing + text is never scanned for tool markup. Conversion goes through + function_call_to_tool_block so aliases and per-tool argument formatting + stay identical to the XML invoke path. + """ + stripped = body.strip() + if not stripped.startswith("{"): + return None + try: + parsed, _end = json.JSONDecoder().raw_decode(stripped) + except json.JSONDecodeError: + return None + if not isinstance(parsed, dict): + return None + name = parsed.get("name") + if not isinstance(name, str) or not name.strip(): + return None + if "arguments" in parsed and not isinstance(parsed["arguments"], dict): + return None + args = parsed.get("arguments", {}) + from src.tool_schemas import function_call_to_tool_block + return function_call_to_tool_block(name.strip().lower(), json.dumps(args)) + + def _iter_stepfun_tool_calls(text: str): """Yield StepFun native tool-call token bodies without regex backtracking.""" pos = 0 @@ -1330,10 +1370,21 @@ def parse_tool_blocks(text: str, skip_fenced: bool = False) -> List[ToolBlock]: if blocks: return blocks # Try wrapped: ... + # A wrapper body that is JSON (Qwen/Hermes text mode, issue #5187) is + # parsed as JSON or dropped — never scanned by the XML iterators, so + # XML-like text inside JSON argument values stays data instead of + # selecting a different tool. + json_body_seen = False for _ms, inner_start, inner_end, _me in _iter_delimited( text, _XML_TOOL_CALL_OPEN_RE, _XML_TOOL_CALL_CLOSE_RE ): body = text[inner_start:inner_end] + if _looks_like_json_body(body): + json_body_seen = True + block = _parse_json_tool_call_body(body) + if block: + blocks.append(block) + continue for inv_name, inv_body in _iter_xml_invoke(body): block = _parse_xml_invoke(inv_name, inv_body) if block: @@ -1348,6 +1399,13 @@ def parse_tool_blocks(text: str, skip_fenced: bool = False) -> List[ToolBlock]: if not blocks: for m in _XML_OPEN_TOOL_CALL_RE.finditer(text): body = m.group(1) + if _looks_like_json_body(body): + # Same fail-closed rule as above for an unclosed wrapper. + json_body_seen = True + block = _parse_json_tool_call_body(body) + if block: + blocks.append(block) + break for inv_name, inv_body in _iter_xml_invoke(body): block = _parse_xml_invoke(inv_name, inv_body) if block: @@ -1358,8 +1416,11 @@ def parse_tool_blocks(text: str, skip_fenced: bool = False) -> List[ToolBlock]: block = _parse_xml_direct_tool(d_name, d_body) if block: blocks.append(block) - # Try bare without wrapper - if not blocks: + # Try bare without wrapper. Skipped when a JSON wrapper body + # was seen but produced no block: this rescan covers the full text, + # wrapper bodies included, and markup inside a (possibly + # malformed) JSON payload must stay data rather than dispatch. + if not blocks and not json_body_seen: for inv_name, inv_body in _iter_xml_invoke(text): block = _parse_xml_invoke(inv_name, inv_body) if block: diff --git a/tests/test_tool_parsing_hermes_json.py b/tests/test_tool_parsing_hermes_json.py new file mode 100644 index 000000000..7ba0550d2 --- /dev/null +++ b/tests/test_tool_parsing_hermes_json.py @@ -0,0 +1,94 @@ +"""Qwen/Hermes text-mode tool calls: bare JSON inside wrappers. + +Issue #5187: {"name": "bash", "arguments": {...}} +parsed to zero blocks because wrapper bodies were only fed to the XML +iterators. The JSON body form now parses through the same canonical +function_call_to_tool_block converter as the XML paths, and JSON-looking +bodies fail closed instead of falling through to XML scanning (tracker #5333): +XML-like text inside JSON argument values must stay data, and a non-object +"arguments" value is rejected rather than coerced. +""" +import src.agent_tools # noqa: F401 (break agent_tools<->tool_parsing import cycle) +from src.tool_parsing import parse_tool_blocks, strip_tool_blocks + +# Verbatim payload from issue #5187. +ISSUE_PAYLOAD = '\n{"name": "bash", "arguments": {"command": "mkdir -p agent-test"}}\n' + + +def test_issue_5187_payload_parses(): + blocks = parse_tool_blocks(ISSUE_PAYLOAD) + assert len(blocks) == 1 + assert blocks[0].tool_type == "bash" + assert blocks[0].content == "mkdir -p agent-test" + + +def test_multiple_sequential_wrappers(): + text = ( + '\n{"name": "bash", "arguments": {"command": "ls"}}\n\n' + 'Now the second step:\n' + '\n{"name": "bash", "arguments": {"command": "pwd"}}\n' + ) + blocks = parse_tool_blocks(text) + assert [(b.tool_type, b.content) for b in blocks] == [("bash", "ls"), ("bash", "pwd")] + + +def test_unclosed_wrapper_still_parses(): + text = '\n{"name": "bash", "arguments": {"command": "ls -la"}}' + blocks = parse_tool_blocks(text) + assert len(blocks) == 1 + assert blocks[0].tool_type == "bash" + assert blocks[0].content == "ls -la" + + +def test_xml_inside_json_arguments_stays_data(): + # P1: a valid JSON body whose argument values contain XML-like tool markup + # must parse as the JSON-named tool; the embedded markup is content. + text = ( + '{"name": "write_file", "arguments": ' + '{"path": "notes.txt", "content": "echo unsafe"}}' + ) + blocks = parse_tool_blocks(text) + assert len(blocks) == 1 + assert blocks[0].tool_type == "write_file" + assert "echo unsafe" in blocks[0].content + assert all(b.tool_type != "bash" for b in blocks) + + +def test_malformed_json_body_never_falls_through_to_xml(): + # P1 fail-closed: a JSON-looking body that doesn't decode must not be + # rescanned as XML, even when it contains well-formed tool markup. + text = ( + '{"name": "write_file", "arguments": {broken json ' + 'echo unsafe' + '' + ) + assert parse_tool_blocks(text) == [] + + +def test_non_dict_arguments_rejected(): + # P2: "arguments" must be an object; scalars/arrays are rejected, not coerced. + for args in ('["ls"]', '"ls"', '1', 'null'): + text = '{"name": "bash", "arguments": %s}' % args + assert parse_tool_blocks(text) == [], f"arguments={args} should be rejected" + + +def test_strip_tool_blocks_removes_json_wrapper_spans(): + text = "Before.\n" + ISSUE_PAYLOAD + "\nAfter." + cleaned = strip_tool_blocks(text) + assert "tool_call" not in cleaned + assert "mkdir -p agent-test" not in cleaned + assert "Before." in cleaned + assert "After." in cleaned + + +def test_xml_body_wrapper_regression(): + # The pre-existing XML wrapper form must keep parsing exactly as before. + text = ( + '' + 'echo hi' + '' + ) + blocks = parse_tool_blocks(text) + assert len(blocks) == 1 + assert blocks[0].tool_type == "bash" + assert blocks[0].content == "echo hi" From e0615cda47eb72be6e7a8f9c60e17a2ea0c80546 Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Wed, 12 Aug 2026 03:22:31 +0100 Subject: [PATCH 063/125] fix(upload): recover backups after same-timestamp corruption (#5860) * fix(upload): harden index cache recovery * fix: retry upload index loads across replacement --------- Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- src/upload_handler.py | 143 ++++++++++++++++++------- tests/test_upload_handler_atomicity.py | 85 +++++++++++++-- 2 files changed, 182 insertions(+), 46 deletions(-) diff --git a/src/upload_handler.py b/src/upload_handler.py index ce0b4b129..e2907699d 100644 --- a/src/upload_handler.py +++ b/src/upload_handler.py @@ -35,6 +35,16 @@ import logging logger = logging.getLogger(__name__) +UploadIndexFileSignature = tuple[ + str, + Optional[int], + Optional[int], + Optional[int], + Optional[int], + Optional[int], +] +UploadIndexSignature = tuple[UploadIndexFileSignature, ...] + class UploadCleanupSafetyError(RuntimeError): """Raised when cleanup cannot prove that destructive work is safe.""" @@ -242,7 +252,7 @@ class UploadHandler: # In-memory index cache to avoid O(N) disk I/O on every request self._index_cache: Optional[Dict[str, Any]] = None - self._index_mtime: float = 0.0 + self._index_signature: Optional[UploadIndexSignature] = None def inside_base_dir(self, path: str) -> bool: """Check if path is inside base directory""" @@ -727,62 +737,119 @@ class UploadHandler: # Update cache if this is the main index if path.endswith("uploads.json"): self._index_cache = data + self._index_signature = self._upload_index_signature( + (path, path + ".bak") + ) + + @staticmethod + def _upload_index_signature( + paths: tuple[str, ...], + ) -> Optional[UploadIndexSignature]: + """Return file identities strong enough to validate the index cache. + + Modification time alone is insufficient: a torn write can change a + file without receiving a strictly newer timestamp on some filesystems. + Size, inode, and nanosecond change times make those mutations visible + while preserving the cache fast path for unchanged files. + """ + signature: list[UploadIndexFileSignature] = [] + for candidate in paths: try: - self._index_mtime = os.path.getmtime(path) + stat_result = os.stat(candidate) + except FileNotFoundError: + signature.append((candidate, None, None, None, None, None)) + continue except OSError: - self._index_mtime = time.time() + return None + signature.append( + ( + candidate, + stat_result.st_dev, + stat_result.st_ino, + stat_result.st_size, + stat_result.st_mtime_ns, + stat_result.st_ctime_ns, + ) + ) + return tuple(signature) def _load_upload_index(self, *, fail_on_error: bool = False) -> Dict[str, Any]: - """Load the upload index from disk/cache. Uses mtime-based validation - to avoid redundant parsing on hot paths. When ``fail_on_error`` is - true, a missing, malformed, or unreadable live index raises so - destructive callers cannot mistake corruption for an empty store. + """Load the upload index from disk/cache. Uses file-identity validation + to avoid redundant parsing on hot paths without missing same-timestamp + mutations. When ``fail_on_error`` is true, a missing, malformed, or + unreadable live index raises so destructive callers cannot mistake + corruption for an empty store. """ uploads_db_path = os.path.join(self.upload_dir, "uploads.json") candidates = (uploads_db_path, uploads_db_path + ".bak") - if fail_on_error: - # A backup is intentionally the previous snapshot. It is useful for - # non-destructive reads, but cannot authorize deletion when the live - # index is missing or corrupt. - if not os.path.exists(uploads_db_path): - raise ValueError("live uploads database is missing") - existing_candidates = [uploads_db_path] - else: - existing_candidates = [path for path in candidates if os.path.exists(path)] - if not existing_candidates: - self._index_cache = {} - self._index_mtime = 0.0 - return {} + for _attempt in range(3): + signature = self._upload_index_signature(candidates) + if fail_on_error: + # A backup is intentionally the previous snapshot. It is useful for + # non-destructive reads, but cannot authorize deletion when the live + # index is missing or corrupt. + if not os.path.exists(uploads_db_path): + raise ValueError("live uploads database is missing") + existing_candidates = [uploads_db_path] + else: + existing_candidates = [ + path for path in candidates if os.path.exists(path) + ] + if not existing_candidates: + self._index_cache = {} + self._index_signature = signature + return {} - # Check cache validity - try: - mtime = max(os.path.getmtime(path) for path in existing_candidates) + # Check cache validity if ( not fail_on_error + and signature is not None and self._index_cache is not None - and mtime <= self._index_mtime + and signature == self._index_signature ): return self._index_cache - except OSError: - mtime = 0.0 - # Try the live file first, fall back to the .bak sibling if the - # live file is truncated/corrupted. - for candidate in existing_candidates: - try: - with open(candidate, "r", encoding="utf-8") as f: - data = json.load(f) - if isinstance(data, dict): - self._index_cache = data - self._index_mtime = mtime - return data - except Exception as e: - logger.warning(f"Failed to read uploads database ({candidate}): {e}") + # Try the live file first, fall back to the .bak sibling if the + # live file is truncated/corrupted. A candidate parsed from an old + # inode is accepted only when the whole index signature stays + # stable through the read; otherwise retry so the cache cannot pair + # stale data with a fresh replacement signature. + index_changed_during_read = False + for candidate in existing_candidates: + try: + with open(candidate, "r", encoding="utf-8") as f: + data = json.load(f) + verified_signature = self._upload_index_signature(candidates) + if ( + signature is not None + and verified_signature is not None + and verified_signature != signature + ): + index_changed_during_read = True + break + if isinstance(data, dict): + self._index_cache = data + self._index_signature = verified_signature + return data + except Exception as e: + logger.warning(f"Failed to read uploads database ({candidate}): {e}") + verified_signature = self._upload_index_signature(candidates) + if ( + signature is not None + and verified_signature is not None + and verified_signature != signature + ): + index_changed_during_read = True + break + continue + if index_changed_during_read: continue + break if fail_on_error: raise ValueError("live uploads database is unreadable") self._index_cache = {} + self._index_signature = self._upload_index_signature(candidates) return {} def get_upload_info(self, upload_id: str) -> Optional[Dict[str, Any]]: diff --git a/tests/test_upload_handler_atomicity.py b/tests/test_upload_handler_atomicity.py index 73cf27917..717bf7f0a 100644 --- a/tests/test_upload_handler_atomicity.py +++ b/tests/test_upload_handler_atomicity.py @@ -15,6 +15,7 @@ These tests exercise: * Smoke tests: normal upload, duplicate detection, info lookup after a backup-recovery scenario. """ +import builtins import concurrent.futures import io import json @@ -59,6 +60,16 @@ def _db_path(handler: UploadHandler) -> str: return os.path.join(handler.upload_dir, "uploads.json") +def _truncate_without_newer_mtime(path: str) -> None: + """Model a filesystem where a torn write shares the cached timestamp.""" + before = os.stat(path) + with open(path, "rb") as f: + full = f.read() + with open(path, "wb") as f: + f.write(full[: max(1, len(full) // 2)]) + os.utime(path, ns=(before.st_atime_ns, before.st_mtime_ns)) + + def _seed_entry(owner: str, file_hash: str, file_id: str) -> dict: return { "id": file_id, @@ -246,10 +257,7 @@ def test_partial_write_recovery_via_bak(tmp_path): "Production _atomic_write_json must create a .bak sibling on subsequent writes." ) - full = open(db_path, "rb").read() - truncated_len = max(1, len(full) // 2) - with open(db_path, "wb") as f: - f.write(full[:truncated_len]) + _truncate_without_newer_mtime(db_path) recovered = handler._load_upload_index() missing = [k for k in original if k not in recovered] @@ -259,6 +267,69 @@ def test_partial_write_recovery_via_bak(tmp_path): ) +def test_partial_write_recovery_via_bak_after_restart(tmp_path): + """A fresh handler must recover the previous snapshot from ``.bak``.""" + handler = _make_handler(tmp_path) + db_path = _db_path(handler) + original = { + f"owner:hash_{i}": _seed_entry("owner", f"hash_{i}", f"id_{i}") + for i in range(3) + } + handler._atomic_write_json(db_path, original) + handler._atomic_write_json(db_path, {"latest": True}) + _truncate_without_newer_mtime(db_path) + + restarted_handler = UploadHandler( + base_dir=handler.base_dir, + upload_dir=handler.upload_dir, + ) + + assert restarted_handler._load_upload_index() == original + + +def test_unchanged_upload_index_uses_cache(tmp_path, monkeypatch): + """The stronger file signature must preserve the unchanged-index fast path.""" + handler = _make_handler(tmp_path) + original = {"owner:hash": _seed_entry("owner", "hash", "id")} + handler._atomic_write_json(_db_path(handler), original) + + def fail_if_parsed(_file): + raise AssertionError("unchanged upload index should be served from cache") + + monkeypatch.setattr(json, "load", fail_if_parsed) + + assert handler._load_upload_index() == original + + +def test_upload_index_retries_when_replaced_during_read(tmp_path, monkeypatch): + """Do not cache old JSON under the signature of a newer atomic replace.""" + handler = _make_handler(tmp_path) + db_path = _db_path(handler) + old_index = {"owner:old": _seed_entry("owner", "old", "old_id")} + new_index = {"owner:new": _seed_entry("owner", "new", "new_id")} + handler._atomic_write_json(db_path, old_index) + handler._index_cache = None + handler._index_signature = None + + real_open = builtins.open + replaced = False + + def racing_open(file, mode="r", *args, **kwargs): + nonlocal replaced + handle = real_open(file, mode, *args, **kwargs) + if os.fspath(file) == db_path and "r" in mode and not replaced: + replaced = True + replacement = db_path + ".replacement" + with real_open(replacement, "w", encoding="utf-8") as out: + json.dump(new_index, out) + os.replace(replacement, db_path) + return handle + + monkeypatch.setattr(builtins, "open", racing_open) + + assert handler._load_upload_index() == new_index + + # --------------------------------------------------------------------------- # Atomicity primitive audit on the production module. # --------------------------------------------------------------------------- @@ -390,10 +461,8 @@ def test_smoke_info_lookup_after_bak_recovery(tmp_path): handler._atomic_write_json(db_path, {"sentinel": True}) assert os.path.exists(db_path + ".bak") - # Truncate the live file. - full = open(db_path, "rb").read() - with open(db_path, "wb") as f: - f.write(full[: max(1, len(full) // 2)]) + # Truncate the live file without assuming the filesystem advances mtime. + _truncate_without_newer_mtime(db_path) info = handler.get_upload_info(first["id"]) assert info is not None, "Info lookup must succeed after .bak recovery." From 937c883c416549d8ee592e4c418bb7fcff013498 Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Wed, 12 Aug 2026 03:35:09 +0100 Subject: [PATCH 064/125] ci: make Python validation authoritative (#5940) Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- .github/workflows/ci.yml | 7 ++--- tests/test_ci_authoritative_validation.py | 35 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 tests/test_ci_authoritative_validation.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7d3659e8..e42c1a5d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main] + branches: [main, dev] pull_request: # Least privilege: none of the jobs write to the repo. @@ -103,10 +103,7 @@ jobs: python-tests: name: Python tests (pytest) runs-on: ubuntu-latest - # Informational for now: the suite has known flaky / environment-dependent - # failures (test isolation + embedding-model assertions). Tracked under the - # ROADMAP "fresh install smoke tests" item; make this required once green. - continue-on-error: true + # Make Python test validation authoritative for the configured scope. steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: diff --git a/tests/test_ci_authoritative_validation.py b/tests/test_ci_authoritative_validation.py new file mode 100644 index 000000000..6de26817d --- /dev/null +++ b/tests/test_ci_authoritative_validation.py @@ -0,0 +1,35 @@ +"""Regression coverage for authoritative Python CI validation.""" + +import re +from pathlib import Path + + +_WORKFLOW = ( + Path(__file__).resolve().parent.parent / ".github" / "workflows" / "ci.yml" +) + + +def _indented_block(text: str, heading: str, indent: int) -> str: + pattern = re.compile( + rf"(?ms)^{' ' * indent}{re.escape(heading)}:\n" + rf"(?P(?:(?:{' ' * (indent + 2)}.*|\s*)\n)*)" + ) + match = pattern.search(text) + assert match is not None, f"missing {heading!r} block" + return match.group(0) + + +def test_ci_runs_on_integrated_dev_pushes(): + workflow = _WORKFLOW.read_text() + push = _indented_block(workflow, "push", 2) + + assert re.search(r"(?m)^ branches:\s*\[main,\s*dev\]\s*$", push) + assert "paths-ignore:" not in push + + +def test_python_tests_are_authoritative(): + workflow = _WORKFLOW.read_text() + python_tests = _indented_block(workflow, "python-tests", 2) + + assert "python -m pytest -q" in python_tests + assert "continue-on-error:" not in python_tests From 1939a6ad2d4cd2022148ac55919253a790591809 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Wed, 12 Aug 2026 00:26:13 -0300 Subject: [PATCH 065/125] fix(thinking): add deepseek-v4 to thinking model patterns (#6000) * fix(thinking): add deepseek-v4 to thinking model patterns deepseek-v4-flash emits reasoning_content via the API but was not recognized in _THINKING_MODEL_PATTERNS (only deepseek-r1 and deepseek-reasoner were listed). Add the deepseek-v4 prefix so the model is recognized as thinking-capable. The between-round _thinkOpen leakage was separately fixed by PR #5931 (perf(chat): batch live thinking rendering). Related: #3998, #5931 * test(thinking): cover DeepSeek v4 detection --------- Co-authored-by: Alexandre Teixeira Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- src/llm_core.py | 4 ++-- tests/test_llm_core_thinking_models.py | 27 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 tests/test_llm_core_thinking_models.py diff --git a/src/llm_core.py b/src/llm_core.py index dd112cedc..30aff2e47 100644 --- a/src/llm_core.py +++ b/src/llm_core.py @@ -1319,8 +1319,8 @@ _MISTRAL_REASONING_EFFORT = os.getenv("ODYSSEUS_MISTRAL_REASONING_EFFORT", "high # Models that support structured thinking — may output without opening tag _THINKING_MODEL_PATTERNS = ( - "qwen3", "qwq", "deepseek-r1", "deepseek-reasoner", "minimax", - "m2-reap", "gemma", "stepfun", "step-3", "step3", + "qwen3", "qwq", "deepseek-r1", "deepseek-reasoner", "deepseek-v4", + "minimax", "m2-reap", "gemma", "stepfun", "step-3", "step3", "magistral", "mistral-small", "mistral-medium", ) diff --git a/tests/test_llm_core_thinking_models.py b/tests/test_llm_core_thinking_models.py new file mode 100644 index 000000000..4ed4fc3c8 --- /dev/null +++ b/tests/test_llm_core_thinking_models.py @@ -0,0 +1,27 @@ +"""Regression coverage for structured-thinking model detection.""" + +import os + +os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:") + +import pytest + +from src.llm_core import _supports_thinking + + +@pytest.mark.parametrize( + "model", + [ + "deepseek-v4", + "deepseek-v4-flash", + "DeepSeek-V4-Flash", + "deepseek/deepseek-v4-flash", + ], +) +def test_deepseek_v4_models_support_thinking(model): + assert _supports_thinking(model) is True + + +@pytest.mark.parametrize("model", ["deepseek-v3", "deepseek-chat"]) +def test_other_deepseek_models_are_not_promoted_to_thinking(model): + assert _supports_thinking(model) is False From 858c872832d9bf79472b5ab2034a89e953e042fb Mon Sep 17 00:00:00 2001 From: Anh Nguyen Date: Tue, 11 Aug 2026 22:46:11 -0500 Subject: [PATCH 066/125] docs(setup): document supports_tools opt-in for manual Ollama /v1 endpoints (#5835) * docs(setup): document supports_tools opt-in for manual Ollama /v1 endpoints Manually-added Ollama /v1 endpoints default to the conservative fenced-block tool-calling path, and there's currently no UI control to opt a specific endpoint into native tool calling (#5192). The supports_tools PATCH flag already exists and works, it just wasn't documented anywhere a user could find it without reading source. Adds a short section next to the existing "Ollama with Docker" notes explaining when to use it and the exact API call, framed as an advanced/opt-in setting per the maintainer's stated preference against a casual UI toggle (#3195/#3438). * docs(setup): clarify supports_tools false semantics --------- Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> Co-authored-by: Alexandre Teixeira --- docs/setup.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/setup.md b/docs/setup.md index 53a6fb28c..171a195e7 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -309,6 +309,32 @@ container. Cookbook **Serve** is a separate workflow for serving downloaded models through Odysseus/llama.cpp, so Windows users with an existing Ollama install usually only need to add the endpoint in Settings. +**Tool calls not firing on a manually-added Ollama `/v1` endpoint.** By +design, a local Ollama `/v1` endpoint defaults to the conservative +text-based (fenced-block) tool-calling path rather than native structured +tool calls, since some locally-served models mishandle native schemas (see +#1567). This is correct for most local setups, but if you know your specific +model reliably supports native tool calling (check `ollama show ` for +`tools` under Capabilities), you can opt that endpoint in explicitly. There +is currently no UI control for this on manually-added endpoints (see #5192); +the flag can still be set directly against the existing API, from a browser +console on an authenticated admin session: + +```js +fetch('/api/model-endpoints/', { + method: 'PATCH', + credentials: 'same-origin', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({supports_tools: true}) +}).then(r => r.json()).then(console.log) +``` + +Find `` by inspecting the `/api/model-endpoints` response (or +your browser's network tab while Settings loads the endpoint list). Send +`supports_tools: false` to disable native structured tool calls and force the +conservative fenced/text path, or `supports_tools: null` to return the endpoint +to the Auto heuristic. + **Useful checks.** ```bash From 3f9633c44f768b355c293e4f9fe06e27e07e94f5 Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Wed, 12 Aug 2026 04:51:52 +0100 Subject: [PATCH 067/125] fix(calendar): keep default creation transactional (#5806) * fix(calendar): keep default creation transactional * fix(calendar): serialize default calendar creation * fix(calendar): handle renamed default id collisions --------- Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- routes/calendar_routes.py | 122 ++++- src/tools/calendar.py | 3 + tests/test_calendar_default_transaction.py | 538 +++++++++++++++++++++ 3 files changed, 656 insertions(+), 7 deletions(-) create mode 100644 tests/test_calendar_default_transaction.py diff --git a/routes/calendar_routes.py b/routes/calendar_routes.py index 6e0ee124c..b9c3b0a52 100644 --- a/routes/calendar_routes.py +++ b/routes/calendar_routes.py @@ -10,6 +10,7 @@ from typing import Optional, List from fastapi import APIRouter, HTTPException, Request, UploadFile, File from pydantic import BaseModel from sqlalchemy import or_, and_ +from sqlalchemy.exc import IntegrityError from dateutil.rrule import rrulestr from core.database import SessionLocal, CalendarCal, CalendarDeletedEvent, CalendarEvent @@ -221,22 +222,125 @@ class EventUpdate(BaseModel): # ── Helpers ── +_DEFAULT_CALENDAR_NAMESPACE = uuid.UUID("4840613a-9847-4a3b-bd75-19e6bc5fc3ce") + + +def _default_calendar_id(owner: str, collision_index: int = 0) -> str: + """Return one stable primary-key candidate for an owner's lazy default. + + Slot zero preserves the original owner-derived identifier. Later slots + let a username be reused after its prior calendar was migrated to another + owner during a rename, without making concurrent first use choose random + and therefore divergent identifiers. + """ + if collision_index == 0: + candidate_name = owner + else: + candidate_name = json.dumps( + [owner, collision_index], + ensure_ascii=False, + separators=(",", ":"), + ) + return str(uuid.uuid5(_DEFAULT_CALENDAR_NAMESPACE, candidate_name)) + + +def _begin_sqlite_default_write(db) -> None: + """Serialize an absent-default check with other SQLite writers. + + SQLite's default deferred transactions allow two workers to both read an + empty calendar set before either writes. ``BEGIN IMMEDIATE`` acquires the + writer reservation before the second, authoritative lookup. We issue it + only when the driver has not already opened a write transaction; a caller + with a pending write already owns the required reservation. + """ + connection = db.connection() + dbapi_connection = connection.connection + driver_connection = getattr( + dbapi_connection, + "driver_connection", + dbapi_connection, + ) + if not getattr(driver_connection, "in_transaction", False): + connection.exec_driver_sql("BEGIN IMMEDIATE") + + def _ensure_default_calendar(db, owner: str = None) -> CalendarCal: - """Create default calendar if none exist for this owner.""" + """Return the owner's calendar, staging a default in the caller's transaction. + + A stable owner-derived primary key makes concurrent first-use inserts + converge on one row on every SQL backend. SQLite additionally serializes + the absent-row check because its deferred transactions otherwise permit + both workers to read the gap before either writes. Other backends recover + a lost insert race inside a savepoint so the caller's event transaction + remains usable and atomic. + """ owner = owner or FALLBACK_OWNER cal = db.query(CalendarCal).filter(CalendarCal.owner == owner).first() - if not cal: + if cal: + return cal + + dialect = db.get_bind().dialect.name + if dialect == "sqlite": + _begin_sqlite_default_write(db) + # Another worker may have committed while BEGIN IMMEDIATE waited. + cal = db.query(CalendarCal).filter(CalendarCal.owner == owner).first() + if cal: + return cal + + collision_index = 0 + while True: + default_id = _default_calendar_id(owner, collision_index) + + if dialect == "sqlite": + # BEGIN IMMEDIATE above makes this occupancy check authoritative: + # another SQLite writer cannot rename, delete, or claim this slot + # until the caller commits or rolls back. + occupant = db.query(CalendarCal).filter( + CalendarCal.id == default_id, + ).first() + if occupant is not None: + if occupant.owner == owner: + return occupant + collision_index += 1 + continue + cal = CalendarCal( - id=str(uuid.uuid4()), + id=default_id, owner=owner, name="Personal", color="#5b8abf", source="local", ) - db.add(cal) - db.commit() - db.refresh(cal) - return cal + + if dialect == "sqlite": + db.add(cal) + db.flush() + return cal + + try: + # A uniqueness failure rolls back only this savepoint, not an event + # or reminder already staged by the caller's outer transaction. + with db.begin_nested(): + db.add(cal) + db.flush() + return cal + except IntegrityError: + # Use a locking/current read so repeatable-read backends can observe + # the row that won after our transaction's original empty snapshot. + occupant = db.query(CalendarCal).filter( + CalendarCal.id == default_id, + ).with_for_update().first() + if occupant is None: + # Do not misclassify an unrelated integrity failure as an ID + # collision and loop forever. A concurrently deleted winner is + # safe for the caller to retry as a fresh transaction. + raise + if occupant.owner == owner: + return occupant + # A renamed calendar owns this deterministic slot. Advance to the + # next stable slot; concurrent callers for this owner will still + # converge there. + collision_index += 1 # Per-request user time context. chat_routes sets this from browser timezone @@ -1015,6 +1119,9 @@ def setup_calendar_routes(upload_handler=None) -> APIRouter: db = SessionLocal() try: _ensure_default_calendar(db, owner) + # Listing calendars intentionally lazily creates a durable default. + # Other callers commit it with the event they are creating. + db.commit() cals = db.query(CalendarCal).filter(CalendarCal.owner == owner).all() return {"calendars": [ {"name": c.name, "href": c.id, "color": c.color, "source": c.source} @@ -1023,6 +1130,7 @@ def setup_calendar_routes(upload_handler=None) -> APIRouter: except HTTPException: raise except Exception as e: + db.rollback() logger.error("Failed to list calendars: %s", e) raise HTTPException(500, "Failed to list calendars") finally: diff --git a/src/tools/calendar.py b/src/tools/calendar.py index e6572ba40..6dda5a0e3 100644 --- a/src/tools/calendar.py +++ b/src/tools/calendar.py @@ -196,6 +196,9 @@ async def do_manage_calendar(content: str, owner: Optional[str] = None) -> Dict: try: if action == "list_calendars": _ensure_default_calendar(db, owner) + # This read path intentionally persists the lazily-created default; + # event creation commits it in the event's transaction instead. + db.commit() cals = _calendar_query().all() result = [{"name": c.name, "href": c.id} for c in cals] if result: diff --git a/tests/test_calendar_default_transaction.py b/tests/test_calendar_default_transaction.py new file mode 100644 index 000000000..ffd981e51 --- /dev/null +++ b/tests/test_calendar_default_transaction.py @@ -0,0 +1,538 @@ +"""Default calendar creation belongs to the caller's transaction. + +Before this regression, ``_ensure_default_calendar`` committed independently. +If event persistence then failed, the event rolled back but a new ``Personal`` +calendar remained (``calendar_count=1``, ``event_count=0``). +""" + +import json +import threading +from contextlib import contextmanager +from datetime import datetime, timedelta +from types import SimpleNamespace + +import pytest +from fastapi import HTTPException +from sqlalchemy import create_engine, event +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy.pool import NullPool + +from tests.helpers.import_state import clear_fake_database_modules + +clear_fake_database_modules() + +import core.database as cdb # noqa: E402 +import routes.calendar_routes as calendar_routes # noqa: E402 +from core.database import CalendarCal, CalendarEvent # noqa: E402 +from routes.calendar_routes import EventCreate # noqa: E402 +from routes.calendar_routes import ( # noqa: E402 + _default_calendar_id, + _ensure_default_calendar, +) + + +class _RejectEventCommit(Session): + """Reproduce an event commit failure after default-calendar creation.""" + + def commit(self): + if any(isinstance(row, CalendarEvent) for row in self.new): + raise RuntimeError("commit guard rejected event commit") + return super().commit() + + +@pytest.fixture +def session_factory(tmp_path, monkeypatch): + engine = create_engine( + f"sqlite:///{tmp_path / 'calendar.db'}", + connect_args={"check_same_thread": False}, + poolclass=NullPool, + ) + cdb.Base.metadata.create_all(engine) + factory = sessionmaker( + bind=engine, + autoflush=False, + autocommit=False, + class_=_RejectEventCommit, + ) + monkeypatch.setattr(cdb, "SessionLocal", factory) + monkeypatch.setattr(calendar_routes, "SessionLocal", factory) + try: + yield factory + finally: + engine.dispose() + + +def _request(): + return SimpleNamespace(state=SimpleNamespace(current_user="alice")) + + +def _endpoint(method, suffix): + router = calendar_routes.setup_calendar_routes() + for route in router.routes: + if route.path.endswith(suffix) and method in route.methods: + return route.endpoint + raise RuntimeError(f"{method} *{suffix} not found") + + +def _counts(factory): + db = factory() + try: + return db.query(CalendarCal).count(), db.query(CalendarEvent).count() + finally: + db.close() + + +async def test_route_event_failure_rolls_back_new_default_calendar(session_factory): + create_event = _endpoint("POST", "/events") + + with pytest.raises(HTTPException) as caught: + await create_event( + _request(), + EventCreate(summary="Planning", dtstart="2126-07-20T09:00:00Z"), + ) + + assert caught.value.status_code == 500 + assert _counts(session_factory) == (0, 0) + + +async def test_route_event_validation_failure_rolls_back_new_default_calendar( + session_factory, +): + create_event = _endpoint("POST", "/events") + + with pytest.raises(HTTPException) as caught: + await create_event( + _request(), + EventCreate(summary="Planning", dtstart="not-a-datetime"), + ) + + assert caught.value.status_code == 500 + assert _counts(session_factory) == (0, 0) + + +async def test_tool_event_failure_rolls_back_new_default_calendar(session_factory): + from src.tools.calendar import do_manage_calendar + + result = await do_manage_calendar( + json.dumps({ + "action": "create_event", + "summary": "Planning", + "dtstart": "2126-07-20T09:00:00Z", + }), + owner="alice", + ) + + assert result["exit_code"] == 1 + assert "commit guard rejected event commit" in result["error"] + assert _counts(session_factory) == (0, 0) + + +async def test_tool_event_validation_failure_rolls_back_new_default_calendar( + session_factory, +): + from src.tools.calendar import do_manage_calendar + + result = await do_manage_calendar( + json.dumps({ + "action": "create_event", + "summary": "Planning", + "dtstart": "not-a-datetime", + }), + owner="alice", + ) + + assert result["exit_code"] == 1 + assert "Could not parse dtstart" in result["error"] + assert _counts(session_factory) == (0, 0) + + +async def test_route_list_calendars_persists_lazy_default(session_factory): + list_calendars = _endpoint("GET", "/calendars") + + result = await list_calendars(_request()) + + assert [calendar["name"] for calendar in result["calendars"]] == ["Personal"] + assert _counts(session_factory) == (1, 0) + + +async def test_tool_list_calendars_persists_lazy_default(session_factory): + from src.tools.calendar import do_manage_calendar + + result = await do_manage_calendar( + json.dumps({"action": "list_calendars"}), + owner="alice", + ) + + assert result["exit_code"] == 0 + assert [calendar["name"] for calendar in result["calendars"]] == ["Personal"] + assert _counts(session_factory) == (1, 0) + + +def test_repeated_rename_and_reuse_uses_stable_fallback_ids(tmp_path): + engine = create_engine( + f"sqlite:///{tmp_path / 'renamed-calendar.db'}", + connect_args={"check_same_thread": False}, + poolclass=NullPool, + ) + cdb.Base.metadata.create_all(engine) + factory = sessionmaker(bind=engine, autoflush=False, autocommit=False) + db = factory() + try: + first = _ensure_default_calendar(db, "alice") + assert first.id == _default_calendar_id("alice") + db.commit() + + # The supported user-rename migration changes owner columns while + # deliberately preserving durable row identifiers. + first.owner = "bob" + db.commit() + + second = _ensure_default_calendar(db, "alice") + assert second.id == _default_calendar_id("alice", 1) + db.commit() + + # Repeating the same lifecycle must advance deterministically instead + # of failing or choosing a random identifier. + second.owner = "carol" + db.commit() + + third = _ensure_default_calendar(db, "alice") + assert third.id == _default_calendar_id("alice", 2) + db.commit() + + rows = db.query(CalendarCal).order_by(CalendarCal.owner).all() + assert [(row.owner, row.id) for row in rows] == [ + ("alice", _default_calendar_id("alice", 2)), + ("bob", _default_calendar_id("alice")), + ("carol", _default_calendar_id("alice", 1)), + ] + finally: + db.close() + engine.dispose() + + +def _assert_concurrent_first_use(tmp_path, occupied_owner=None): + engine = create_engine( + f"sqlite:///{tmp_path / 'concurrent-calendar.db'}", + connect_args={"check_same_thread": False, "timeout": 10}, + poolclass=NullPool, + ) + cdb.Base.metadata.create_all(engine) + factory = sessionmaker(bind=engine, autoflush=False, autocommit=False) + expected_collision_index = 0 + if occupied_owner is not None: + seed = factory() + try: + seed.add(CalendarCal( + id=_default_calendar_id("alice"), + owner=occupied_owner, + name="Personal", + source="local", + )) + seed.commit() + expected_collision_index = 1 + finally: + seed.close() + first_staged = threading.Event() + second_selected = threading.Event() + errors = [] + + @event.listens_for(engine, "after_cursor_execute") + def observe_second_gap(conn, cursor, statement, parameters, context, executemany): + if ( + threading.current_thread().name == "calendar-worker-second" + and statement.lstrip().upper().startswith("SELECT") + and "FROM calendars" in statement + ): + second_selected.set() + + def create_default(worker, hold=False): + db = factory() + try: + if not hold: + assert first_staged.wait(5) + cal = _ensure_default_calendar(db, "alice") + start = datetime(2126, 7, 20, 9 if hold else 10) + db.add(CalendarEvent( + uid=worker, + calendar_id=cal.id, + summary=f"Event {worker}", + dtstart=start, + dtend=start + timedelta(hours=1), + )) + if hold: + first_staged.set() + # The second session has observed the uncommitted gap before + # this transaction releases its writer reservation. + assert second_selected.wait(5) + db.commit() + assert cal.id == _default_calendar_id("alice", expected_collision_index) + except BaseException as exc: # pragma: no cover - asserted below + errors.append((worker, exc)) + db.rollback() + finally: + db.close() + + first = threading.Thread( + target=create_default, + args=("first", True), + name="calendar-worker-first", + ) + second = threading.Thread( + target=create_default, + args=("second",), + name="calendar-worker-second", + ) + first.start() + second.start() + first.join(10) + second.join(10) + + try: + assert not first.is_alive() and not second.is_alive() + assert errors == [] + db = factory() + try: + rows = db.query(CalendarCal).filter(CalendarCal.owner == "alice").all() + assert [(row.id, row.name) for row in rows] == [ + (_default_calendar_id("alice", expected_collision_index), "Personal") + ] + assert db.query(CalendarEvent).count() == 2 + if occupied_owner is not None: + occupied = db.query(CalendarCal).filter( + CalendarCal.id == _default_calendar_id("alice"), + ).one() + assert occupied.owner == occupied_owner + finally: + db.close() + finally: + engine.dispose() + + +def test_concurrent_first_use_creates_one_sqlite_default(tmp_path): + _assert_concurrent_first_use(tmp_path) + + +def test_concurrent_first_use_after_rename_creates_one_fallback_default(tmp_path): + _assert_concurrent_first_use(tmp_path, occupied_owner="bob") + + +def test_sqlite_default_stays_in_callers_transaction(session_factory): + db = session_factory() + try: + cal = _ensure_default_calendar(db, "rollback-owner") + assert cal.id == _default_calendar_id("rollback-owner") + db.rollback() + finally: + db.close() + + verify = session_factory() + try: + assert ( + verify.query(CalendarCal) + .filter(CalendarCal.owner == "rollback-owner") + .count() + == 0 + ) + finally: + verify.close() + + +def test_sqlite_fallback_default_stays_in_callers_transaction(session_factory): + seed = session_factory() + try: + seed.add(CalendarCal( + id=_default_calendar_id("alice"), + owner="bob", + name="Personal", + source="local", + )) + seed.commit() + finally: + seed.close() + + db = session_factory() + try: + cal = _ensure_default_calendar(db, "alice") + assert cal.id == _default_calendar_id("alice", 1) + db.rollback() + finally: + db.close() + + verify = session_factory() + try: + assert verify.query(CalendarCal).filter(CalendarCal.owner == "alice").count() == 0 + assert verify.query(CalendarCal).filter(CalendarCal.owner == "bob").count() == 1 + finally: + verify.close() + + +class _FakeDialect: + name = "postgresql" + + +class _FakeBind: + dialect = _FakeDialect() + + +class _FakeQuery: + def __init__(self, session): + self.session = session + + def filter(self, *conditions): + return self + + def with_for_update(self): + self.session.locking_read = True + return self + + def first(self): + self.session.query_count += 1 + if self.session.query_count == 1: + return None + return self.session.winner + + +class _GenericRaceSession: + """Minimal non-SQLite session that loses the deterministic-ID race.""" + + def __init__(self): + self.query_count = 0 + self.nested_entries = 0 + self.locking_read = False + self.candidate = None + self.winner = CalendarCal( + id=_default_calendar_id("alice"), + owner="alice", + name="Personal", + source="local", + ) + + def get_bind(self): + return _FakeBind() + + def query(self, model): + assert model is CalendarCal + return _FakeQuery(self) + + @contextmanager + def begin_nested(self): + self.nested_entries += 1 + yield + + def add(self, row): + self.candidate = row + + def flush(self): + raise IntegrityError("insert", {}, RuntimeError("duplicate primary key")) + + +def test_generic_backend_lost_race_recovers_inside_savepoint(): + db = _GenericRaceSession() + + winner = _ensure_default_calendar(db, "alice") + + assert winner is db.winner + assert db.nested_entries == 1 + assert db.locking_read is True + assert db.candidate.id == db.winner.id + + +def test_generic_backend_unattributed_integrity_error_is_not_retried(): + db = _GenericRaceSession() + db.winner = None + + with pytest.raises(IntegrityError): + _ensure_default_calendar(db, "alice") + + assert db.nested_entries == 1 + + +class _GenericRenamedSlotSession(_GenericRaceSession): + """A different owner occupies slot zero; slot one remains available.""" + + def __init__(self): + super().__init__() + self.candidates = [] + self.winner = CalendarCal( + id=_default_calendar_id("alice"), + owner="bob", + name="Personal", + source="local", + ) + + def add(self, row): + self.candidate = row + self.candidates.append(row) + + def flush(self): + if len(self.candidates) == 1: + raise IntegrityError("insert", {}, RuntimeError("duplicate primary key")) + + +def test_generic_backend_renamed_slot_advances_inside_savepoint(): + db = _GenericRenamedSlotSession() + + fallback = _ensure_default_calendar(db, "alice") + + assert fallback is db.candidates[-1] + assert fallback.id == _default_calendar_id("alice", 1) + assert fallback.owner == "alice" + assert db.nested_entries == 2 + assert db.locking_read is True + assert db.winner.owner == "bob" + + +def test_generic_backend_fallback_keeps_outer_transaction_usable(tmp_path): + engine = create_engine( + f"sqlite:///{tmp_path / 'generic-savepoint-calendar.db'}", + poolclass=NullPool, + ) + cdb.Base.metadata.create_all(engine) + # SQLite supplies a lightweight local SQL executor here; changing only the + # dispatch name exercises the real Session/savepoint branch used by + # PostgreSQL-style backends without pretending to validate their dialect. + engine.dialect.name = "postgresql" + factory = sessionmaker(bind=engine, autoflush=False, autocommit=False) + + seed = factory() + try: + seed.add(CalendarCal( + id=_default_calendar_id("alice"), + owner="bob", + name="Personal", + source="local", + )) + seed.commit() + finally: + seed.close() + + db = factory() + try: + cal = _ensure_default_calendar(db, "alice") + start = datetime(2126, 7, 20, 9) + db.add(CalendarEvent( + uid="after-fallback", + calendar_id=cal.id, + summary="Atomic", + dtstart=start, + dtend=start + timedelta(hours=1), + )) + db.commit() + finally: + db.close() + + verify = factory() + try: + assert [ + (row.owner, row.id) + for row in verify.query(CalendarCal).order_by(CalendarCal.owner).all() + ] == [ + ("alice", _default_calendar_id("alice", 1)), + ("bob", _default_calendar_id("alice")), + ] + assert verify.query(CalendarEvent).count() == 1 + finally: + verify.close() + engine.dispose() From 93eb10d4f0897307cc94de5a9355882079c747ac Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Wed, 12 Aug 2026 04:59:46 +0100 Subject: [PATCH 068/125] fix(email): serialize default-account mutations (#5805) * fix(email): serialize default account mutations * fix(email): enforce default account invariant --------- Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- core/database.py | 278 ++++++++-- routes/auth_routes.py | 54 +- routes/email_routes.py | 120 +++- scripts/demo_email/demo_account.py | 118 +++- ...est_email_account_default_serialization.py | 522 ++++++++++++++++++ tests/test_rename_user_owner_sync.py | 12 + 6 files changed, 1005 insertions(+), 99 deletions(-) create mode 100644 tests/test_email_account_default_serialization.py diff --git a/core/database.py b/core/database.py index a9ad90b8b..6eb529949 100644 --- a/core/database.py +++ b/core/database.py @@ -5,7 +5,7 @@ from datetime import datetime, timezone from pathlib import Path from typing import Optional from urllib.parse import unquote, urlparse -from sqlalchemy import event, create_engine, Column, String, Text, Boolean, DateTime, Integer, ForeignKey, JSON, Index, func, text +from sqlalchemy import DDL, event, create_engine, Column, String, Text, Boolean, DateTime, Integer, ForeignKey, JSON, Index, func, inspect, text from sqlalchemy.engine import Engine, make_url from sqlalchemy.types import TypeDecorator from sqlalchemy.ext.declarative import declarative_base, declared_attr @@ -430,6 +430,93 @@ class EmailAccount(TimestampMixin, Base): ) +class EmailAccountOwnerLock(Base): + """Durable per-owner mutex for email-account default mutations. + + Row-locking databases serialize mutations by locking this row before they + inspect or stage EmailAccount changes. SQLite uses ``BEGIN IMMEDIATE`` + instead, because it ignores ``SELECT ... FOR UPDATE``; keeping the table in + the shared metadata still makes the non-SQLite path available without a + separate migration. The empty key represents the normalized legacy / + unconfigured scope shared by ``owner IS NULL`` and ``owner = ''`` rows. + """ + __tablename__ = "email_account_owner_locks" + + owner_key = Column(String, primary_key=True) + + +_EMAIL_ACCOUNT_DEFAULT_INDEX = "ux_email_accounts_one_default_per_owner" +_EMAIL_ACCOUNT_DEFAULT_INDEX_DDL = { + "sqlite": ( + f"CREATE UNIQUE INDEX IF NOT EXISTS {_EMAIL_ACCOUNT_DEFAULT_INDEX} " + "ON email_accounts (COALESCE(owner, '')) WHERE is_default = 1" + ), + "postgresql": ( + f"CREATE UNIQUE INDEX IF NOT EXISTS {_EMAIL_ACCOUNT_DEFAULT_INDEX} " + "ON email_accounts ((COALESCE(owner, ''))) WHERE is_default IS TRUE" + ), +} + + +# SQLAlchemy cannot express one portable partial, functional index across the +# two supported database families. Register dialect-specific DDL so fresh +# databases get the invariant as part of create_all(); the startup migration +# below installs the same index on existing databases after normalizing legacy +# duplicate rows. +for _dialect_name, _index_ddl in _EMAIL_ACCOUNT_DEFAULT_INDEX_DDL.items(): + event.listen( + EmailAccount.__table__, + "after_create", + DDL(_index_ddl).execute_if(dialect=_dialect_name), + ) + + +def lock_email_account_owner_mutations(db, *owners: str) -> None: + """Lock normalized email-account owner scopes in canonical order. + + ``NULL`` and the empty string are one legacy/single-user owner partition, + matching the unique default-account index. SQLite has only a database + writer reservation, while row-locking databases use durable mutex rows. + Sorting all requested owner keys keeps multi-owner operations such as user + rename from deadlocking with another mutation that requests the same keys + in the opposite order. + """ + from sqlalchemy.exc import IntegrityError + + owner_keys = sorted({owner or "" for owner in owners} or {""}) + if db.get_bind().dialect.name == "sqlite": + db.execute(text("BEGIN IMMEDIATE")) + return + + for owner_key in owner_keys: + lock_row = db.get( + EmailAccountOwnerLock, + owner_key, + with_for_update=True, + ) + if lock_row is not None: + continue + + inserted = False + try: + with db.begin_nested(): + db.add(EmailAccountOwnerLock(owner_key=owner_key)) + db.flush() + inserted = True + except IntegrityError: + # A competing transaction created the mutex row first. Once its + # insert commits, lock that durable row before touching accounts. + pass + + if not inserted: + ( + db.query(EmailAccountOwnerLock) + .filter(EmailAccountOwnerLock.owner_key == owner_key) + .with_for_update() + .one() + ) + + class ModelEndpoint(TimestampMixin, Base): """Admin-configured model endpoints. Models are auto-discovered via /v1/models.""" __tablename__ = "model_endpoints" @@ -1812,72 +1899,142 @@ class Integration(TimestampMixin, Base): -def _migrate_seed_email_account(): - """If email_accounts is empty and settings.json has legacy flat imap_host/smtp_host - keys, create a single default account from them so nothing breaks for users who - upgraded. Safe to run repeatedly — it short-circuits once any row exists.""" +def _migrate_email_account_default_invariant(): + """Normalize legacy duplicates and install durable at-most-one enforcement. + + Older databases only had a non-unique ``(owner, is_default)`` lookup index. + Keep the oldest default deterministically in each normalized owner scope, + then add the same partial functional unique index used for fresh schemas. + """ + dialect_name = engine.dialect.name + index_ddl = _EMAIL_ACCOUNT_DEFAULT_INDEX_DDL.get(dialect_name) + if index_ddl is None: + logger.warning( + "Email-account default uniqueness is not available for database " + "dialect %s; mutations remain serialized but are not protected by " + "a database constraint", + dialect_name, + ) + return + try: - with engine.connect() as conn: - tables = [r[0] for r in conn.execute(text( - "SELECT name FROM sqlite_master WHERE type='table' AND name='email_accounts'" - ))] - if "email_accounts" not in tables: - return - existing = conn.execute(text("SELECT COUNT(*) FROM email_accounts")).scalar() or 0 - if existing > 0: + with engine.begin() as conn: + if not inspect(conn).has_table(EmailAccount.__tablename__): return + default_rows = conn.execute(text(""" + SELECT id, owner + FROM email_accounts + WHERE is_default IS TRUE + ORDER BY + COALESCE(owner, ''), + CASE WHEN created_at IS NULL THEN 1 ELSE 0 END, + created_at, + id + """)).mappings() + seen_owner_keys = set() + duplicate_ids = [] + for row in default_rows: + owner_key = row["owner"] or "" + if owner_key in seen_owner_keys: + duplicate_ids.append(row["id"]) + else: + seen_owner_keys.add(owner_key) - import json as _json - import uuid as _uuid - from pathlib import Path - settings_file = Path(SETTINGS_FILE) - if not settings_file.exists(): - return - try: - s = _json.loads(settings_file.read_text(encoding="utf-8")) - except Exception: - return + for account_id in duplicate_ids: + conn.execute( + text("UPDATE email_accounts SET is_default = :value WHERE id = :id"), + {"value": False, "id": account_id}, + ) + conn.execute(text(index_ddl)) - imap_host = (s.get("imap_host") or "").strip() - smtp_host = (s.get("smtp_host") or "").strip() - if not imap_host and not smtp_host: - return # nothing to migrate + if duplicate_ids: + logger.warning( + "Normalized %d duplicate default email account(s) before " + "installing %s", + len(duplicate_ids), + _EMAIL_ACCOUNT_DEFAULT_INDEX, + ) + except Exception: + # Starting without the constraint would silently retain the race this + # migration is intended to close. Fail startup so an operator sees and + # can repair an incompatible schema instead of accepting unsafe writes. + logger.exception("Failed to enforce the email-account default invariant") + raise + + +def _migrate_seed_email_account(): + """Atomically seed one legacy default account when no account exists. + + Reading settings is intentionally done before taking the owner mutex. The + decisive emptiness check and insert share one locked transaction, so two + application workers starting together cannot both seed a default row. + """ + import json as _json + import uuid as _uuid + + settings_file = Path(SETTINGS_FILE) + if not settings_file.exists(): + return + try: + s = _json.loads(settings_file.read_text(encoding="utf-8")) + except Exception: + return + + imap_host = (s.get("imap_host") or "").strip() + smtp_host = (s.get("smtp_host") or "").strip() + if not imap_host and not smtp_host: + return + + db = None + try: + if not inspect(engine).has_table(EmailAccount.__tablename__): + return + db = SessionLocal() + lock_email_account_owner_mutations(db, "") + existing = db.execute(text("SELECT COUNT(*) FROM email_accounts")).scalar() or 0 + if existing > 0: + return now = utcnow_naive() - with engine.begin() as conn: - conn.execute(text(""" - INSERT INTO email_accounts - (id, owner, name, is_default, enabled, - imap_host, imap_port, imap_user, imap_password, imap_starttls, - smtp_host, smtp_port, smtp_user, smtp_password, - from_address, created_at, updated_at) - VALUES - (:id, :owner, :name, :is_default, :enabled, - :imap_host, :imap_port, :imap_user, :imap_password, :imap_starttls, - :smtp_host, :smtp_port, :smtp_user, :smtp_password, - :from_address, :created_at, :updated_at) - """), { - "id": _uuid.uuid4().hex, - "owner": None, - "name": "Default", - "is_default": True, - "enabled": True, - "imap_host": imap_host, - "imap_port": int(s.get("imap_port") or 993), - "imap_user": s.get("imap_user") or "", - "imap_password": s.get("imap_password") or "", - "imap_starttls": bool(s.get("imap_starttls", True)), - "smtp_host": smtp_host, - "smtp_port": int(s.get("smtp_port") or 465), - "smtp_user": s.get("smtp_user") or "", - "smtp_password": s.get("smtp_password") or "", - "from_address": s.get("email_from") or "", - "created_at": now, - "updated_at": now, - }) - logging.getLogger(__name__).info("Seeded email_accounts 'Default' from settings.json") + db.execute(text(""" + INSERT INTO email_accounts + (id, owner, name, is_default, enabled, + imap_host, imap_port, imap_user, imap_password, imap_starttls, + smtp_host, smtp_port, smtp_user, smtp_password, + from_address, created_at, updated_at) + VALUES + (:id, :owner, :name, :is_default, :enabled, + :imap_host, :imap_port, :imap_user, :imap_password, :imap_starttls, + :smtp_host, :smtp_port, :smtp_user, :smtp_password, + :from_address, :created_at, :updated_at) + """), { + "id": _uuid.uuid4().hex, + "owner": None, + "name": "Default", + "is_default": True, + "enabled": True, + "imap_host": imap_host, + "imap_port": int(s.get("imap_port") or 993), + "imap_user": s.get("imap_user") or "", + "imap_password": s.get("imap_password") or "", + "imap_starttls": bool(s.get("imap_starttls", True)), + "smtp_host": smtp_host, + "smtp_port": int(s.get("smtp_port") or 465), + "smtp_user": s.get("smtp_user") or "", + "smtp_password": s.get("smtp_password") or "", + "from_address": s.get("email_from") or "", + "created_at": now, + "updated_at": now, + }) + db.commit() + logger.info("Seeded email_accounts 'Default' from settings.json") except Exception as e: - logging.getLogger(__name__).warning(f"seed email account migration: {e}") + if db is not None: + db.rollback() + logger.warning("seed email account migration: %s", e) + finally: + if db is not None: + db.close() # WARNING: Foreign-key enforcement is enabled globally for all SQLite connections. @@ -1960,6 +2117,7 @@ def init_db(): _migrate_add_crew_member_id() _migrate_add_assistant_columns() _migrate_add_email_smtp_security() + _migrate_email_account_default_invariant() _migrate_seed_email_account() _migrate_add_calendar_metadata() _migrate_add_calendar_is_utc() diff --git a/routes/auth_routes.py b/routes/auth_routes.py index 5c7a4e04a..c0c370561 100644 --- a/routes/auth_routes.py +++ b/routes/auth_routes.py @@ -345,9 +345,61 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter: # docs, email accounts, tasks, etc. try: from sqlalchemy import func - from core.database import Base, SessionLocal + from core.database import ( + Base, + EmailAccount, + SessionLocal, + lock_email_account_owner_mutations, + ) db = SessionLocal() try: + # Email-account defaults are protected by per-owner mutex rows. + # A rename crosses two owner partitions, so lock both in the + # shared helper's canonical order before inspecting either. + lock_email_account_owner_mutations( + db, old_username, new_username + ) + + source_default_ids = [ + row[0] + for row in ( + db.query(EmailAccount.id) + .filter( + func.lower(EmailAccount.owner) == old_username, + EmailAccount.is_default == True, # noqa: E712 + ) + .order_by(EmailAccount.created_at.asc(), EmailAccount.id.asc()) + .all() + ) + ] + destination_default_ids = [ + row[0] + for row in ( + db.query(EmailAccount.id) + .filter( + func.lower(EmailAccount.owner) == new_username, + EmailAccount.is_default == True, # noqa: E712 + ) + .order_by(EmailAccount.created_at.asc(), EmailAccount.id.asc()) + .all() + ) + ] + if destination_default_ids: + clear_default_ids = ( + destination_default_ids[1:] + source_default_ids + ) + else: + clear_default_ids = source_default_ids[1:] + if clear_default_ids: + ( + db.query(EmailAccount) + .filter(EmailAccount.id.in_(clear_default_ids)) + .update( + {EmailAccount.is_default: False}, + synchronize_session=False, + ) + ) + for mapper in Base.registry.mappers: model = mapper.class_ if not hasattr(model, "owner"): diff --git a/routes/email_routes.py b/routes/email_routes.py index 03b73ebb5..e10eabd59 100644 --- a/routes/email_routes.py +++ b/routes/email_routes.py @@ -45,6 +45,7 @@ from src.upload_limits import read_upload_limited, EMAIL_COMPOSE_UPLOAD_MAX_BYTE from routes.email_helpers import ( _strip_think, _extract_reply, _apply_email_style_mechanics, require_owner, require_user, _assert_owns_account, + _account_visible_to_owner, _q, _attach_compose_uploads, _cleanup_compose_uploads, _load_settings, _save_settings, _get_email_config, _send_smtp_message, _smtp_security_mode, @@ -195,6 +196,64 @@ def _coerce_port(value, default): return None, f"Invalid port {value!r}; must be a whole number" +def _lock_email_account_owner_mutation(db, *owners: str) -> None: + """Delegate account/default serialization to the shared DB primitive.""" + from core.database import lock_email_account_owner_mutations + + lock_email_account_owner_mutations(db, *owners) + + +def _email_account_owner_scope(query, owner: str): + """Restrict a query to one normalized EmailAccount owner partition.""" + from core.database import EmailAccount + from sqlalchemy import or_ + + if owner: + return query.filter(EmailAccount.owner == owner) + return query.filter(or_(EmailAccount.owner == None, EmailAccount.owner == "")) # noqa: E711 + + +def _discover_email_account_mutation_scope(account_id: str, owner: str) -> str: + """Read the initial lock key and fail closed before a mutation session.""" + from core.database import EmailAccount, SessionLocal + + db = SessionLocal() + try: + row = db.get(EmailAccount, account_id) + if row is None or (owner and not _account_visible_to_owner(row, owner)): + raise HTTPException(404, "Account not found") + return row.owner or "" + except HTTPException: + raise + except Exception as exc: + logger.error("Account-owner mutation check failed: %s", exc) + raise HTTPException(503, "Account check failed") + finally: + db.close() + + +def _lock_and_reload_email_account(db, account_id: str, owner: str, scope: str): + """Lock, reload, and revalidate an account, retrying if its owner moved.""" + from core.database import EmailAccount + + owner_scopes = {scope or ""} + while True: + _lock_email_account_owner_mutation(db, *owner_scopes) + row = db.get(EmailAccount, account_id, populate_existing=True) + if row is None or (owner and not _account_visible_to_owner(row, owner)): + raise HTTPException(404, "Account not found") + + current_scope = row.owner or "" + if current_scope in owner_scopes or db.get_bind().dialect.name == "sqlite": + return row + + # The account changed owner after discovery but before lock acquisition. + # Release the partial lock set and reacquire all observed scopes in the + # shared helper's canonical order, then validate from the database again. + db.rollback() + owner_scopes.add(current_scope) + + def _email_tag_owner_aliases(account_id: str | None, owner: str = "") -> list[str]: aliases = [owner or ""] try: @@ -5487,9 +5546,9 @@ def setup_email_routes(): import uuid as _uuid db = SessionLocal() try: + _lock_email_account_owner_mutation(db, owner) q = db.query(EmailAccount).filter(EmailAccount.is_default == True) # noqa: E712 - if owner: - q = q.filter(EmailAccount.owner == owner) + q = _email_account_owner_scope(q, owner) row = q.first() if row is None: row = EmailAccount(id=_uuid.uuid4().hex, owner=owner, name="Default", is_default=True, enabled=True) @@ -5515,8 +5574,7 @@ def setup_email_routes(): if data.get("smtp_password"): row.smtp_password = _enc(data["smtp_password"]) clear_q = db.query(EmailAccount).filter(EmailAccount.id != row.id) - if owner: - clear_q = clear_q.filter(EmailAccount.owner == owner) + clear_q = _email_account_owner_scope(clear_q, owner) clear_q.update({EmailAccount.is_default: False}) db.commit() finally: @@ -5611,6 +5669,7 @@ def setup_email_routes(): return {"ok": False, "error": port_err} db = SessionLocal() try: + _lock_email_account_owner_mutation(db, owner) row = EmailAccount( id=_uuid.uuid4().hex, name=name, @@ -5637,9 +5696,7 @@ def setup_email_routes(): # the one-default invariant — but scope it to THIS user's accounts, # otherwise creating a default would clear every other user's # default flag too. - scope_q = db.query(EmailAccount) - if owner: - scope_q = scope_q.filter(EmailAccount.owner == owner) + scope_q = _email_account_owner_scope(db.query(EmailAccount), owner) existing_count = scope_q.count() if row.is_default or existing_count == 0: scope_q.update({EmailAccount.is_default: False}) @@ -5690,28 +5747,39 @@ def setup_email_routes(): @router.delete("/accounts/{account_id}") async def delete_email_account(account_id: str, owner: str = Depends(require_user)): - _assert_owns_account(account_id, owner) + initial_scope = _discover_email_account_mutation_scope(account_id, owner) from core.database import SessionLocal, EmailAccount db = SessionLocal() try: - row = db.get(EmailAccount, account_id) - if not row: - return {"ok": False, "error": "Account not found"} + row = _lock_and_reload_email_account( + db, account_id, owner, initial_scope + ) + row_scope = row.owner or "" was_default = bool(row.is_default) db.delete(row) - db.commit() + # Flush the removal before staging a replacement default. The + # partial unique index is checked statement-by-statement, and the + # ORM is otherwise free to UPDATE the promoted row before DELETE. + db.flush() # If the deleted row was default, promote the next-oldest enabled # row owned by THIS user. Without the owner filter we'd promote # another user's account and the deleter would silently inherit # it as their default. if was_default: - promote_q = db.query(EmailAccount).filter(EmailAccount.enabled == True) # noqa: E712 - if owner: - promote_q = promote_q.filter(EmailAccount.owner == owner) - promote = promote_q.order_by(EmailAccount.created_at.asc()).first() + promote_q = db.query(EmailAccount).filter( + EmailAccount.id != account_id, + EmailAccount.enabled == True, # noqa: E712 + ) + promote_q = _email_account_owner_scope(promote_q, row_scope) + promote = promote_q.order_by( + EmailAccount.created_at.asc(), EmailAccount.id.asc() + ).first() if promote: promote.is_default = True - db.commit() + # Deletion and any replacement promotion are one durable state + # transition, so another worker can never observe or race the old + # split-commit gap. + db.commit() return {"ok": True} finally: db.close() @@ -5924,18 +5992,18 @@ def setup_email_routes(): @router.post("/accounts/{account_id}/set-default") async def set_default_account(account_id: str, owner: str = Depends(require_user)): - _assert_owns_account(account_id, owner) + initial_scope = _discover_email_account_mutation_scope(account_id, owner) from core.database import SessionLocal, EmailAccount db = SessionLocal() try: - row = db.get(EmailAccount, account_id) - if not row: - return {"ok": False, "error": "Account not found"} - # SECURITY: scope the "clear other defaults" sweep to this user's - # accounts so we don't unset another user's default flag. - clear_q = db.query(EmailAccount) - if owner: - clear_q = clear_q.filter(EmailAccount.owner == owner) + row = _lock_and_reload_email_account( + db, account_id, owner, initial_scope + ) + # Scope the sweep to the target row's normalized owner partition; + # this also handles visible legacy NULL/empty-owner accounts. + clear_q = _email_account_owner_scope( + db.query(EmailAccount), row.owner or "" + ) clear_q.update({EmailAccount.is_default: False}) row.is_default = True db.commit() diff --git a/scripts/demo_email/demo_account.py b/scripts/demo_email/demo_account.py index 9555b6791..8a0f1190a 100755 --- a/scripts/demo_email/demo_account.py +++ b/scripts/demo_email/demo_account.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Create/remove the switchable, non-default 'Demo' EmailAccount in Odysseus. +"""Create/remove the switchable 'Demo' EmailAccount in Odysseus. Mirrors the existing local-Dovecot account (localhost:31143, STARTTLS) but points at the throwaway demo@odysseus.local mailbox. Password is stored Fernet-encrypted @@ -20,7 +20,14 @@ from pathlib import Path ROOT = Path(__file__).resolve().parent.parent.parent sys.path.insert(0, str(ROOT)) -from core.database import SessionLocal, EmailAccount, Base, engine # noqa: E402 +from core.database import ( # noqa: E402 + Base, + EmailAccount, + SessionLocal, + engine, + lock_email_account_owner_mutations, +) +from sqlalchemy import or_ # noqa: E402 from src.secret_storage import encrypt # noqa: E402 NAME = "Demo" @@ -31,18 +38,98 @@ IMAP_PASSWORD = "demodemo" OWNER = "" -def setup() -> int: - Base.metadata.create_all(bind=engine) +def _owner_scope(query, owner: str): + if owner: + return query.filter(EmailAccount.owner == owner) + return query.filter(or_(EmailAccount.owner == None, EmailAccount.owner == "")) # noqa: E711 + + +def _discover_demo_scopes() -> set[str]: db = SessionLocal() try: - acct = db.query(EmailAccount).filter( - EmailAccount.name == NAME, EmailAccount.imap_user == IMAP_USER - ).first() + return { + row.owner or "" + for row in db.query(EmailAccount).filter( + EmailAccount.name == NAME, + EmailAccount.imap_user == IMAP_USER, + ).all() + } + finally: + db.close() + + +def _lock_and_load_demo_rows(db, scopes: set[str]): + """Reload Demo rows under every observed owner lock.""" + scopes = set(scopes) or {OWNER} + while True: + lock_email_account_owner_mutations(db, *scopes) + rows = ( + db.query(EmailAccount) + .filter( + EmailAccount.name == NAME, + EmailAccount.imap_user == IMAP_USER, + ) + .order_by(EmailAccount.created_at.asc(), EmailAccount.id.asc()) + .all() + ) + current_scopes = {row.owner or "" for row in rows} + if current_scopes.issubset(scopes) or db.get_bind().dialect.name == "sqlite": + return rows + db.rollback() + scopes.update(current_scopes) + + +def _promote_oldest_enabled(db, owner: str, excluded_ids: list[str]) -> None: + remaining = _owner_scope( + db.query(EmailAccount).filter( + EmailAccount.enabled == True, # noqa: E712 + ~EmailAccount.id.in_(excluded_ids), + ), + owner, + ) + if remaining.filter(EmailAccount.is_default == True).first() is not None: # noqa: E712 + return + promote = remaining.order_by( + EmailAccount.created_at.asc(), EmailAccount.id.asc() + ).first() + if promote is not None: + promote.is_default = True + + +def setup() -> int: + Base.metadata.create_all(bind=engine) + scopes = _discover_demo_scopes() | {OWNER} + db = SessionLocal() + try: + rows = _lock_and_load_demo_rows(db, scopes) + acct = rows[0] if rows else None if acct is None: acct = EmailAccount(id=uuid.uuid4().hex, name=NAME) db.add(acct) + old_scope = acct.owner or "" + was_default = bool(acct.is_default) + if old_scope != OWNER: + # Move a non-default row first so the unique index cannot see two + # defaults transiently while SQLAlchemy flushes the owner move and + # old-scope promotion in separate UPDATE statements. + acct.is_default = False + acct.owner = OWNER + db.flush() + if was_default: + _promote_oldest_enabled(db, old_scope, [acct.id]) + + target_default = _owner_scope( + db.query(EmailAccount).filter( + EmailAccount.id != acct.id, + EmailAccount.is_default == True, # noqa: E712 + ), + OWNER, + ).first() acct.owner = OWNER - acct.is_default = False # never default — user switches to it + # Keep Demo non-default when a real default exists. If it is the only + # enabled account, it must be default to preserve normal create + # semantics and avoid leaving the owner partition without one. + acct.is_default = target_default is None acct.enabled = True acct.imap_host = "localhost" acct.imap_port = 31143 @@ -57,20 +144,27 @@ def setup() -> int: acct.smtp_password = encrypt(IMAP_PASSWORD) acct.from_address = IMAP_USER db.commit() - print(f"'{NAME}' account ready (id={acct.id}, non-default, switchable).") + state = "default" if acct.is_default else "non-default" + print(f"'{NAME}' account ready (id={acct.id}, {state}, switchable).") return 0 finally: db.close() def teardown() -> int: + scopes = _discover_demo_scopes() db = SessionLocal() try: - rows = db.query(EmailAccount).filter( - EmailAccount.name == NAME, EmailAccount.imap_user == IMAP_USER - ).all() + rows = _lock_and_load_demo_rows(db, scopes) + deleted_ids = [row.id for row in rows] + default_scopes = {row.owner or "" for row in rows if row.is_default} for r in rows: db.delete(r) + # Ensure the old default DELETE reaches the database before a + # replacement UPDATE; the unique index is enforced per statement. + db.flush() + for owner in default_scopes: + _promote_oldest_enabled(db, owner, deleted_ids) db.commit() print(f"removed {len(rows)} '{NAME}' account row(s).") return 0 diff --git a/tests/test_email_account_default_serialization.py b/tests/test_email_account_default_serialization.py new file mode 100644 index 000000000..6d2394378 --- /dev/null +++ b/tests/test_email_account_default_serialization.py @@ -0,0 +1,522 @@ +"""Regressions for process-safe email-account default mutations. + +The file-backed SQLite fixture uses a fresh connection for every Session. +That exercises the same database lock boundary used by separate web workers, +rather than relying on an in-process Python lock. +""" + +import asyncio +import json +import sys +import threading +import types +from pathlib import Path +from types import SimpleNamespace +from unittest import mock + +import pytest +from fastapi import HTTPException +from sqlalchemy import create_engine, create_mock_engine, text +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import NullPool + + +@pytest.fixture +def account_db(tmp_path, monkeypatch): + from core import database as core_db + + engine = create_engine( + f"sqlite:///{tmp_path / 'accounts.db'}", + connect_args={"check_same_thread": False, "timeout": 5}, + poolclass=NullPool, + ) + core_db.Base.metadata.create_all(engine) + factory = sessionmaker( + bind=engine, + autocommit=False, + autoflush=False, + ) + monkeypatch.setattr(core_db, "SessionLocal", factory) + yield factory + engine.dispose() + + +def _endpoint(method, path): + from routes import email_routes + + with mock.patch.object(email_routes, "_start_poller"): + router = email_routes.setup_email_routes() + for route in router.routes: + if route.path == path and method in getattr(route, "methods", set()): + return route.endpoint + raise AssertionError(f"email route not found: {method} {path}") + + +def _named_endpoint(router, name): + for route in router.routes: + if getattr(getattr(route, "endpoint", None), "__name__", "") == name: + return route.endpoint + raise AssertionError(f"route not found: {name}") + + +def _seed_account(factory, account_id, owner, *, is_default=False, enabled=True): + from core.database import EmailAccount + + db = factory() + try: + db.add( + EmailAccount( + id=account_id, + owner=owner, + name=account_id, + is_default=is_default, + enabled=enabled, + ) + ) + db.commit() + finally: + db.close() + + +def _rows(factory): + from core.database import EmailAccount + + db = factory() + try: + return [ + (row.id, row.owner, bool(row.is_default)) + for row in db.query(EmailAccount).order_by(EmailAccount.id).all() + ] + finally: + db.close() + + +def _install_lock_pause(monkeypatch, paused_thread_name): + """Pause one worker after acquisition and observe another waiting.""" + from routes import email_routes + + real_lock = email_routes._lock_email_account_owner_mutation + first_acquired = threading.Event() + release_first = threading.Event() + contender_attempted = threading.Event() + contender_acquired = threading.Event() + + def controlled_lock(db, owner): + is_first = threading.current_thread().name == paused_thread_name + if not is_first: + contender_attempted.set() + real_lock(db, owner) + if is_first: + first_acquired.set() + assert release_first.wait(5), "timed out releasing first mutation" + else: + contender_acquired.set() + + monkeypatch.setattr( + email_routes, + "_lock_email_account_owner_mutation", + controlled_lock, + ) + return first_acquired, release_first, contender_attempted, contender_acquired + + +def test_concurrent_first_account_creates_choose_one_default(account_db, monkeypatch): + create_account = _endpoint("POST", "/api/email/accounts") + first_acquired, release_first, attempted, acquired = _install_lock_pause( + monkeypatch, "first-account" + ) + results = {} + + def create(name): + results[name] = asyncio.run( + create_account({"name": name, "is_default": False}, owner="alice") + ) + + first = threading.Thread(target=create, args=("First",), name="first-account") + second = threading.Thread(target=create, args=("Second",), name="second-account") + first.start() + assert first_acquired.wait(5) + second.start() + assert attempted.wait(5) + assert not acquired.wait(0.1), "second session bypassed the database mutation lock" + + release_first.set() + first.join(5) + second.join(5) + + assert not first.is_alive() + assert not second.is_alive() + assert results["First"]["ok"] is True + assert results["Second"]["ok"] is True + defaults = [row for row in _rows(account_db) if row[2]] + assert [(row[1], row[2]) for row in defaults] == [("alice", True)] + assert len(defaults) == 1 + + +def test_delete_promotion_and_set_default_are_one_serial_transition( + account_db, monkeypatch +): + from sqlalchemy.orm import Session as OrmSession + + _seed_account(account_db, "alice-a", "alice", is_default=True) + _seed_account(account_db, "alice-b", "alice") + _seed_account(account_db, "alice-c", "alice") + _seed_account(account_db, "bob-a", "bob", is_default=True) + + delete_account = _endpoint("DELETE", "/api/email/accounts/{account_id}") + set_default = _endpoint("POST", "/api/email/accounts/{account_id}/set-default") + first_acquired, release_first, attempted, acquired = _install_lock_pause( + monkeypatch, "delete-default" + ) + delete_commit_finished = threading.Event() + release_delete_after_commit = threading.Event() + real_commit = OrmSession.commit + results = {} + + def pause_after_delete_commit(session): + real_commit(session) + if ( + threading.current_thread().name == "delete-default" + and not delete_commit_finished.is_set() + ): + delete_commit_finished.set() + assert release_delete_after_commit.wait(5), ( + "timed out releasing delete after its first commit" + ) + + monkeypatch.setattr(OrmSession, "commit", pause_after_delete_commit) + + def delete_old_default(): + results["delete"] = asyncio.run( + delete_account("alice-a", owner="alice") + ) + + def select_new_default(): + results["set"] = asyncio.run( + set_default("alice-c", owner="alice") + ) + + delete_thread = threading.Thread(target=delete_old_default, name="delete-default") + set_thread = threading.Thread(target=select_new_default, name="set-default") + delete_thread.start() + assert first_acquired.wait(5) + set_thread.start() + assert attempted.wait(5) + assert not acquired.wait(0.1), "set-default bypassed the delete transaction" + + release_first.set() + assert delete_commit_finished.wait(5) + # The deletion transaction has committed. Let the contender complete + # before the deleting handler can continue: if promotion were still a + # second commit, it would now run after set-default and recreate two + # defaults deterministically. + assert acquired.wait(5) + set_thread.join(5) + release_delete_after_commit.set() + delete_thread.join(5) + + assert not delete_thread.is_alive() + assert not set_thread.is_alive() + assert results == {"delete": {"ok": True}, "set": {"ok": True}} + assert _rows(account_db) == [ + ("alice-b", "alice", False), + ("alice-c", "alice", True), + ("bob-a", "bob", True), + ] + + +def test_upgrade_normalizes_legacy_defaults_and_installs_unique_index( + tmp_path, monkeypatch +): + """A pre-index schema upgrades without requiring newer account columns.""" + from core import database as core_db + + engine = create_engine( + f"sqlite:///{tmp_path / 'legacy-accounts.db'}", + connect_args={"check_same_thread": False}, + poolclass=NullPool, + ) + try: + with engine.begin() as conn: + conn.execute(text(""" + CREATE TABLE email_accounts ( + id VARCHAR PRIMARY KEY, + owner VARCHAR, + name VARCHAR NOT NULL, + is_default BOOLEAN NOT NULL, + enabled BOOLEAN NOT NULL, + created_at DATETIME, + updated_at DATETIME + ) + """)) + conn.execute(text(""" + INSERT INTO email_accounts + (id, owner, name, is_default, enabled, created_at, updated_at) + VALUES + ('legacy-old', NULL, 'Old', 1, 1, '2024-01-01', '2024-01-01'), + ('legacy-new', '', 'New', 1, 1, '2025-01-01', '2025-01-01') + """)) + + monkeypatch.setattr(core_db, "engine", engine) + core_db._migrate_email_account_default_invariant() + core_db._migrate_email_account_default_invariant() # idempotent replay + + with engine.connect() as conn: + defaults = conn.execute(text(""" + SELECT id FROM email_accounts + WHERE is_default IS TRUE + ORDER BY id + """)).scalars().all() + index_names = { + row[1] for row in conn.execute(text("PRAGMA index_list(email_accounts)")) + } + assert defaults == ["legacy-old"] + assert core_db._EMAIL_ACCOUNT_DEFAULT_INDEX in index_names + + with pytest.raises(IntegrityError): + with engine.begin() as conn: + conn.execute(text(""" + INSERT INTO email_accounts + (id, owner, name, is_default, enabled, created_at, updated_at) + VALUES + ('legacy-third', NULL, 'Third', 1, 1, '2026-01-01', '2026-01-01') + """)) + finally: + engine.dispose() + + +def test_concurrent_legacy_seed_is_one_locked_transaction( + tmp_path, monkeypatch, caplog +): + from core import database as core_db + + engine = create_engine( + f"sqlite:///{tmp_path / 'seed-accounts.db'}", + connect_args={"check_same_thread": False, "timeout": 5}, + poolclass=NullPool, + ) + core_db.Base.metadata.create_all(engine) + factory = sessionmaker(bind=engine, autocommit=False, autoflush=False) + settings_file = tmp_path / "settings.json" + settings_file.write_text( + json.dumps({"imap_host": "imap.example.test", "imap_user": "alice"}), + encoding="utf-8", + ) + monkeypatch.setattr(core_db, "engine", engine) + monkeypatch.setattr(core_db, "SessionLocal", factory) + monkeypatch.setattr(core_db, "SETTINGS_FILE", str(settings_file)) + + read_barrier = threading.Barrier(2) + real_read_text = Path.read_text + + def synchronized_read(path, *args, **kwargs): + value = real_read_text(path, *args, **kwargs) + if path == settings_file: + read_barrier.wait(5) + return value + + monkeypatch.setattr(Path, "read_text", synchronized_read) + threads = [ + threading.Thread(target=core_db._migrate_seed_email_account) + for _ in range(2) + ] + try: + for thread in threads: + thread.start() + for thread in threads: + thread.join(5) + assert all(not thread.is_alive() for thread in threads) + + with engine.connect() as conn: + rows = conn.execute(text(""" + SELECT owner, is_default FROM email_accounts + ORDER BY id + """)).all() + assert rows == [(None, 1)] + assert "seed email account migration:" not in caplog.text + finally: + engine.dispose() + + +def test_multi_owner_row_locks_are_acquired_in_canonical_order(): + from core.database import lock_email_account_owner_mutations + + class FakeSession: + def __init__(self): + self.locked = [] + + def get_bind(self): + return SimpleNamespace(dialect=SimpleNamespace(name="postgresql")) + + def get(self, _model, owner_key, **kwargs): + assert kwargs == {"with_for_update": True} + self.locked.append(owner_key) + return object() + + db = FakeSession() + lock_email_account_owner_mutations(db, "zeta", "", "alpha", "zeta") + assert db.locked == ["", "alpha", "zeta"] + + +def test_postgresql_fresh_schema_emits_default_unique_index(): + from core import database as core_db + + statements = [] + engine_holder = {} + + def capture(statement, *_args, **_kwargs): + statements.append( + str(statement.compile(dialect=engine_holder["engine"].dialect)) + ) + + mock_engine = create_mock_engine("postgresql://", capture) + engine_holder["engine"] = mock_engine + core_db.EmailAccount.__table__.create(mock_engine) + + assert any( + core_db._EMAIL_ACCOUNT_DEFAULT_INDEX in statement + and "COALESCE(owner, '')" in statement + and "WHERE is_default IS TRUE" in statement + for statement in statements + ) + + +def test_rename_serializes_old_and_new_owner_and_stale_set_default_fails_closed( + account_db, monkeypatch, tmp_path +): + from core import database as core_db + from routes import auth_routes + + _seed_account(account_db, "alice-a", "alice", is_default=True) + _seed_account(account_db, "alice-b", "alice") + _seed_account(account_db, "bob-a", "bob", is_default=True) + + prefs_module = types.ModuleType("routes.prefs_routes") + prefs_module._load = lambda: {} + prefs_module._save = lambda _data: None + monkeypatch.setitem(sys.modules, "routes.prefs_routes", prefs_module) + monkeypatch.setattr( + auth_routes, "DEEP_RESEARCH_DIR", str(tmp_path / "deep_research") + ) + monkeypatch.setattr(auth_routes, "MEMORY_FILE", str(tmp_path / "memory.json")) + monkeypatch.setattr(auth_routes, "SKILLS_DIR", str(tmp_path / "skills")) + + auth_manager = mock.MagicMock() + auth_manager.get_username_for_token.return_value = "admin" + auth_manager.is_admin.return_value = True + auth_manager.users = {"admin": {}, "alice": {}} + auth_manager.rename_user.return_value = True + rename_user = _named_endpoint( + auth_routes.setup_auth_routes(auth_manager), "rename_user" + ) + set_default = _endpoint("POST", "/api/email/accounts/{account_id}/set-default") + + rename_acquired = threading.Event() + release_rename = threading.Event() + set_attempted = threading.Event() + set_acquired = threading.Event() + real_lock = core_db.lock_email_account_owner_mutations + + def controlled_lock(db, *owners): + thread_name = threading.current_thread().name + if thread_name == "rename-owner": + real_lock(db, *owners) + rename_acquired.set() + assert release_rename.wait(5) + return + if thread_name == "stale-set-default": + set_attempted.set() + real_lock(db, *owners) + set_acquired.set() + return + real_lock(db, *owners) + + monkeypatch.setattr(core_db, "lock_email_account_owner_mutations", controlled_lock) + request = SimpleNamespace( + cookies={"odysseus_session": "admin-token"}, + app=SimpleNamespace( + state=SimpleNamespace( + invalidate_token_cache=lambda: None, + session_manager=None, + research_handler=None, + upload_handler=None, + personal_docs_manager=None, + ) + ), + ) + results = {} + + def rename_owner(): + results["rename"] = asyncio.run( + rename_user("alice", SimpleNamespace(username="bob"), request) + ) + + def select_stale_default(): + try: + results["set"] = asyncio.run( + set_default("alice-b", owner="alice") + ) + except Exception as exc: # asserted below with its HTTP status + results["set_error"] = exc + + rename_thread = threading.Thread(target=rename_owner, name="rename-owner") + set_thread = threading.Thread( + target=select_stale_default, name="stale-set-default" + ) + rename_thread.start() + assert rename_acquired.wait(5) + set_thread.start() + assert set_attempted.wait(5) + assert not set_acquired.wait(0.1), "set-default bypassed the rename lock" + + release_rename.set() + rename_thread.join(5) + set_thread.join(5) + + assert not rename_thread.is_alive() + assert not set_thread.is_alive() + assert results["rename"]["ok"] is True + assert isinstance(results["set_error"], HTTPException) + assert results["set_error"].status_code == 404 + assert _rows(account_db) == [ + ("alice-a", "bob", False), + ("alice-b", "bob", False), + ("bob-a", "bob", True), + ] + + +def test_demo_teardown_promotes_replacement_in_same_transaction( + account_db, monkeypatch +): + from core.database import EmailAccount + from scripts.demo_email import demo_account + + db = account_db() + try: + db.add_all([ + EmailAccount( + id="real", + owner="", + name="Real", + is_default=False, + enabled=True, + ), + EmailAccount( + id="demo", + owner="", + name=demo_account.NAME, + imap_user=demo_account.IMAP_USER, + is_default=True, + enabled=True, + ), + ]) + db.commit() + finally: + db.close() + + monkeypatch.setattr(demo_account, "SessionLocal", account_db) + monkeypatch.setattr(demo_account, "engine", account_db.kw["bind"]) + + assert demo_account.teardown() == 0 + assert _rows(account_db) == [("real", "", True)] diff --git a/tests/test_rename_user_owner_sync.py b/tests/test_rename_user_owner_sync.py index 7e9e5d911..b7ac7e200 100644 --- a/tests/test_rename_user_owner_sync.py +++ b/tests/test_rename_user_owner_sync.py @@ -114,6 +114,12 @@ def _force_sql_owner_migration_failure(monkeypatch): def filter(self, *_args, **_kwargs): return self + def order_by(self, *_args, **_kwargs): + return self + + def all(self): + return [] + def update(self, *_args, **_kwargs): raise RuntimeError("forced owner migration failure") @@ -125,6 +131,12 @@ def _force_sql_owner_migration_failure(monkeypatch): def query(self, _model): return FailingQuery() + def get_bind(self): + return SimpleNamespace(dialect=SimpleNamespace(name="postgresql")) + + def get(self, _model, _key, **_kwargs): + return object() + def rollback(self): self.rolled_back = True From e7eddbae139e4534dee1ae46a6541d55ae1b7b69 Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Wed, 12 Aug 2026 05:05:21 +0100 Subject: [PATCH 069/125] fix(email): serialize urgency checkpoint delivery (#5804) * fix(email): serialize urgency checkpoints * fix(email): preserve urgency transaction lifecycle * fix(email): fence stale urgency scans * fix(email): fence stale urgency delivery * fix(email): retire stale urgency accounts * fix(email): fence urgency account retirement * fix(email): retain urgency registration generation --------- Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- src/builtin_actions.py | 787 ++++++++++++-- tests/test_email_urgency_checkpoint.py | 1328 ++++++++++++++++++++++++ 2 files changed, 2010 insertions(+), 105 deletions(-) create mode 100644 tests/test_email_urgency_checkpoint.py diff --git a/src/builtin_actions.py b/src/builtin_actions.py index 68817467f..216bb0360 100644 --- a/src/builtin_actions.py +++ b/src/builtin_actions.py @@ -20,6 +20,395 @@ from src.interactive_gate import wait_for_interactive_quiet logger = logging.getLogger(__name__) +def _read_email_urgency_state(state_path): + """Read one atomic urgency checkpoint, tolerating the legacy shape.""" + from pathlib import Path + + state_path = Path(state_path) + try: + state = ( + json.loads(state_path.read_text(encoding="utf-8")) + if state_path.exists() + else {} + ) + except Exception: + return {} + return state if isinstance(state, dict) else {} + + +def _email_urgency_account_generations(state): + """Return normalized per-account checkpoint/complete generations. + + Checkpoint generations fence every accepted state mutation. Complete + generations advance only for a non-stale complete scan. Missing metadata + is the legacy generation zero. + """ + raw = state.get("account_generations", {}) if isinstance(state, dict) else {} + if not isinstance(raw, dict): + return {} + + generations = {} + for account_id, value in raw.items(): + if isinstance(value, dict): + checkpoint = value.get("checkpoint", 0) + complete = value.get("complete", 0) + else: + # Tolerate an intermediate scalar representation as one completed + # checkpoint generation instead of discarding its fence. + checkpoint = value + complete = value + try: + checkpoint = max(0, int(checkpoint)) + except (TypeError, ValueError): + checkpoint = 0 + try: + complete = max(0, int(complete)) + except (TypeError, ValueError): + complete = 0 + generations[str(account_id)] = { + "checkpoint": checkpoint, + "complete": complete, + } + return generations + + +def _email_urgency_string_set(value): + if not isinstance(value, (list, tuple, set, frozenset)): + return set() + return {str(item) for item in value if isinstance(item, (str, int))} + + +def _acquire_email_urgency_state_lock( + state_path, + lock_db_path, + cancel_event, + timeout_seconds=120, +): + """Acquire the cross-process urgency lock without blocking the app loop.""" + import sqlite3 + import time + from pathlib import Path + + state_path = Path(state_path) + state_path.parent.mkdir(parents=True, exist_ok=True) + deadline = time.monotonic() + timeout_seconds + + while not cancel_event.is_set(): + remaining = deadline - time.monotonic() + if remaining <= 0: + raise sqlite3.OperationalError("timed out waiting for urgency state lock") + conn = sqlite3.connect( + str(lock_db_path), + timeout=min(0.25, max(0.01, remaining)), + check_same_thread=False, + ) + try: + conn.execute("BEGIN IMMEDIATE") + except sqlite3.OperationalError as exc: + conn.close() + if "locked" not in str(exc).lower(): + raise + cancel_event.wait(min(0.05, max(0.0, remaining))) + continue + except BaseException: + conn.close() + raise + + if cancel_event.is_set(): + conn.rollback() + conn.close() + return None, None + return conn, _read_email_urgency_state(state_path) + + return None, None + + +def _close_email_urgency_state_lock(conn): + if conn is None: + return + try: + try: + conn.rollback() + except Exception: + pass + finally: + conn.close() + + +def _commit_email_urgency_state(conn, state_path, next_state): + """Atomically publish JSON before releasing the SQLite write lock.""" + import uuid + from pathlib import Path + + state_path = Path(state_path) + temp_path = state_path.with_name( + f".{state_path.name}.{uuid.uuid4().hex}.tmp" + ) + try: + temp_path.write_text(json.dumps(next_state), encoding="utf-8") + temp_path.replace(state_path) + conn.commit() + except BaseException: + conn.rollback() + raise + finally: + temp_path.unlink(missing_ok=True) + conn.close() + + +async def _run_email_urgency_state_transaction( + state_path, + lock_db_path, + operation, +): + """Serialize one urgency decision while keeping async work on this loop. + + Only lock acquisition waits in a worker thread. ``operation`` is awaited + on the caller's long-lived event loop, where shared async clients, locks, + and the browser-notification queue belong. Cancellation rolls back the + SQLite transaction and never publishes a checkpoint. + """ + import asyncio + import threading + + loop = asyncio.get_running_loop() + cancel_event = threading.Event() + acquire_future = loop.run_in_executor( + None, + _acquire_email_urgency_state_lock, + state_path, + lock_db_path, + cancel_event, + ) + try: + conn, prior = await asyncio.shield(acquire_future) + except asyncio.CancelledError as cancelled: + cancel_event.set() + # The acquisition worker owns any connection until it returns. Wait + # for its short busy-poll to observe cancellation, then close a lock it + # may have won concurrently with the cancellation request. + while True: + try: + conn, _prior = await asyncio.shield(acquire_future) + break + except asyncio.CancelledError: + continue + except Exception: + conn = None + break + _close_email_urgency_state_lock(conn) + raise cancelled + + if conn is None: + raise asyncio.CancelledError + + try: + result, next_state = await operation(prior) + # Keep this small atomic publish synchronous. There is no await between + # the successful operation and commit, so cancellation cannot be + # observed and then followed by a checkpoint. + try: + _commit_email_urgency_state(conn, state_path, next_state) + finally: + conn = None + return result + except BaseException: + _close_email_urgency_state_lock(conn) + raise + + +def _email_urgency_account_key(message_key): + return str(message_key).split(":", 1)[0] + + +def _email_urgency_payload_account_ids(state): + """Return account IDs that still own user-visible urgency payload.""" + if not isinstance(state, dict): + return set() + + per_uid = state.get("per_uid", {}) + per_uid_keys = per_uid if isinstance(per_uid, dict) else {} + return { + _email_urgency_account_key(key) for key in per_uid_keys + } | { + _email_urgency_account_key(key) + for key in _email_urgency_string_set(state.get("notified_uids", [])) + } + + +def _email_urgency_known_account_ids(state): + """Return payload owners plus generation-only active/retired markers.""" + return _email_urgency_payload_account_ids(state) | set( + _email_urgency_account_generations(state) + ) + + +def _email_urgency_stale_accounts( + prior, + base_account_generations, + account_ids, +): + prior_generations = _email_urgency_account_generations(prior) + base_generations = _email_urgency_account_generations( + {"account_generations": base_account_generations} + ) + return { + str(account_id) + for account_id in account_ids + if prior_generations.get(str(account_id), {}).get("checkpoint", 0) + != base_generations.get(str(account_id), {}).get("checkpoint", 0) + } + + +def _merge_email_urgency_state( + prior, + *, + owner, + per_uid_scores, + notified_uids, + all_unread_keys, + fully_scanned_account_ids, + base_account_generations, + timestamp, + retired_account_ids=(), + base_payload_account_ids=(), + known_account_ids=(), +): + """Merge a scan without letting an older snapshot erase newer facts.""" + prior_per_uid = prior.get("per_uid", {}) + if not isinstance(prior_per_uid, dict): + prior_per_uid = {} + complete = {str(account_id) for account_id in fully_scanned_account_ids} + prior_generations = _email_urgency_account_generations(prior) + retire_requested = {str(account_id) for account_id in retired_account_ids} + observed_accounts = { + _email_urgency_account_key(key) for key in per_uid_scores + } | complete | retire_requested + stale_accounts = _email_urgency_stale_accounts( + prior, + base_account_generations, + observed_accounts, + ) + prior_payload_accounts = _email_urgency_payload_account_ids(prior) + base_payload_accounts = { + str(account_id) for account_id in base_payload_account_ids + } + # A selected account can be absent from the base snapshot. If another + # worker creates its first payload before this transaction wins the lock, + # membership itself is a fence even when both snapshots normalize to the + # legacy generation zero. + retired_accounts = { + account_id + for account_id in retire_requested - stale_accounts + if not ( + account_id in prior_payload_accounts + and account_id not in base_payload_accounts + ) + } + fresh_complete = complete - stale_accounts - retired_accounts + changed_accounts = set(fresh_complete) + + merged_per_uid = { + key: value + for key, value in prior_per_uid.items() + if _email_urgency_account_key(key) not in retired_accounts + } + for key in list(merged_per_uid): + account_id = _email_urgency_account_key(key) + if account_id in fresh_complete: + merged_per_uid.pop(key, None) + changed_accounts.add(account_id) + # Partial scans may add or refresh facts, but absence from a partial scan + # is not evidence that another checkpoint or UI row is stale. When another + # worker committed after this scan captured its base generation, discard + # this account's whole stale snapshot. A key absent from the newer state + # may have been removed/read, so even a stale-only key is not safely + # additive without another fresh scan. + for key, value in per_uid_scores.items(): + account_id = _email_urgency_account_key(key) + if account_id in stale_accounts or account_id in retired_accounts: + continue + if merged_per_uid.get(key) != value: + changed_accounts.add(account_id) + merged_per_uid[key] = value + + prior_notified = _email_urgency_string_set(prior.get("notified_uids", [])) + merged_notified = { + key + for key in prior_notified + if _email_urgency_account_key(key) not in retired_accounts + } + for key in _email_urgency_string_set(notified_uids) - prior_notified: + account_id = _email_urgency_account_key(key) + if account_id in stale_accounts or account_id in retired_accounts: + continue + merged_notified.add(key) + changed_accounts.add(account_id) + for key in list(merged_notified): + if ( + _email_urgency_account_key(key) in fresh_complete + and key not in all_unread_keys + ): + merged_notified.discard(key) + changed_accounts.add(_email_urgency_account_key(key)) + + next_generations = { + account_id: dict(value) + for account_id, value in prior_generations.items() + } + for account_id in changed_accounts: + generation = next_generations.setdefault( + account_id, + {"checkpoint": 0, "complete": 0}, + ) + generation["checkpoint"] += 1 + if account_id in fresh_complete: + generation["complete"] += 1 + for account_id in {str(value) for value in known_account_ids}: + next_generations.setdefault( + account_id, + {"checkpoint": 0, "complete": 0}, + ) + for account_id in retired_accounts: + # Every authoritative absence advances its generation, even when the + # prior state is already a payload-empty tombstone. A re-enabled scan + # may have captured that previous tombstone immediately before the + # account was disabled/deleted again; monotonic advancement is what + # makes that in-flight scan stale. + generation = next_generations.setdefault( + account_id, + {"checkpoint": 0, "complete": 0}, + ) + generation["checkpoint"] += 1 + + total_unread = 0 + total_urgent = 0 + max_score = 0 + for value in merged_per_uid.values(): + if not isinstance(value, dict): + continue + try: + score = max(0, min(3, int(value.get("score", 0)))) + except (TypeError, ValueError): + score = 0 + max_score = max(max_score, score) + if value.get("unread"): + total_unread += 1 + if score >= 2: + total_urgent += 1 + + return { + "ts": timestamp, + "owner": owner or "", + "total_unread": total_unread, + "total_urgent": total_urgent, + "max_score": max_score, + "per_uid": merged_per_uid, + "notified_uids": sorted(merged_notified), + "account_generations": next_generations, + } + + class TaskNoop(BaseException): """Raised by an action when it determined there's nothing to do. @@ -1878,6 +2267,7 @@ async def action_check_email_urgency(owner: str, **kwargs) -> Tuple[str, bool]: # filename for single-user installs (matches prior behaviour). _owner_slug = "".join(c if (c.isalnum() or c in "-_.@") else "_" for c in (owner or "default")) STATE_PATH = _P(DATA_DIR) / f"email_urgency_state_{_owner_slug}.json" + STATE_LOCK_DB = STATE_PATH.with_suffix(".lock.sqlite3") CACHE_DIR = _P(EMAIL_URGENCY_CACHE_DIR) CACHE_DIR.mkdir(parents=True, exist_ok=True) STATE_PATH.parent.mkdir(parents=True, exist_ok=True) @@ -1892,35 +2282,144 @@ async def action_check_email_urgency(owner: str, **kwargs) -> Tuple[str, bool]: "shopping", "social", "work", "personal", "legal", "support", "promo", } - # ── 1. Resolve LLM candidates (utility primary + utility fallbacks; fall - # through to default chat as a last resort). + # Resolve with the task owner as before, but defer the availability + # gate until after authoritative account cleanup. State retirement must + # still run when no model is configured. from src.task_endpoint import resolve_task_candidates candidates = resolve_task_candidates(owner=owner) - if not candidates: - return "No LLM endpoint available", False - target_account_id = _email_task_account_id(kwargs) - # ── 2. Enumerate enabled accounts. Match this task's owner AND fall + # ── 1. Enumerate enabled accounts. Match this task's owner AND fall # back to the legacy "unowned account whose imap_user / from_address # == this owner" pattern — same rule `_get_email_config` uses, so a # pre-multi-user account row still gets picked up for the seeded task. - db = _SL() - try: - from sqlalchemy import and_ as _and, or_ as _or - q = db.query(_EA).filter(_EA.enabled == True) # noqa: E712 - if owner: - unowned = _or(_EA.owner == None, _EA.owner == "") # noqa: E711 - same_mailbox = _or(_EA.imap_user == owner, _EA.from_address == owner) - q = q.filter(_or(_EA.owner == owner, _and(unowned, same_mailbox))) - if target_account_id: - q = q.filter(_EA.id == target_account_id) - accounts = q.all() - finally: - db.close() + def _enumerate_enabled_accounts(): + db = _SL() + try: + from sqlalchemy import and_ as _and, or_ as _or + q = db.query(_EA).filter(_EA.enabled == True) # noqa: E712 + if owner: + unowned = _or(_EA.owner == None, _EA.owner == "") # noqa: E711 + same_mailbox = _or( + _EA.imap_user == owner, + _EA.from_address == owner, + ) + q = q.filter( + _or(_EA.owner == owner, _and(unowned, same_mailbox)) + ) + if target_account_id: + q = q.filter(_EA.id == target_account_id) + return q.all() + finally: + db.close() + + initial_accounts = _enumerate_enabled_accounts() + initial_account_ids = { + str(account.id) for account in initial_accounts + } + + # Register every account before IMAP work, including its first-ever + # scan. A concurrent zero-account cleanup can then advance this marker + # and fence delivery even before the scan has produced payload. + registered_state = None + if initial_account_ids: + async def _register_accounts(prior): + next_state = _merge_email_urgency_state( + prior, + owner=owner, + per_uid_scores={}, + notified_uids=prior.get("notified_uids", []), + all_unread_keys=set(), + fully_scanned_account_ids=set(), + base_account_generations=( + _email_urgency_account_generations(prior) + ), + timestamp=_time.time(), + known_account_ids=initial_account_ids, + ) + # Return the exact state committed by registration. This is + # the scan's generation token: adopting a later checkpoint + # after account cleanup would let the stale scan appear fresh. + return next_state, next_state + + registered_state = await _run_email_urgency_state_transaction( + STATE_PATH, + STATE_LOCK_DB, + _register_accounts, + ) + + # Revalidate after registration. If deletion/disable and its cleanup + # completed before the marker was published, this second enumeration + # observes the absence and this action retires its own marker instead + # of starting IMAP. Accounts newly appearing between the two reads are + # left for the next pass rather than scanned without prior registration. + verified_accounts = _enumerate_enabled_accounts() + enabled_account_ids = { + str(account.id) for account in verified_accounts + } + accounts = [ + account + for account in verified_accounts + if str(account.id) in initial_account_ids + ] + + # Capture the checkpoint basis before cleanup or IMAP. A full + # owner-wide enumeration authoritatively retires all known state IDs + # absent from the current enabled/visible set. A scoped task may retire + # only its selected missing/disabled account. Existing accounts remain + # present even if their later network scan fails, so transient IMAP + # failure never erases their last known state. + base_state = ( + registered_state + if registered_state is not None + else _read_email_urgency_state(STATE_PATH) + ) + base_account_generations = _email_urgency_account_generations( + base_state + ) + base_payload_account_ids = _email_urgency_payload_account_ids(base_state) + known_state_account_ids = _email_urgency_known_account_ids(base_state) + if target_account_id: + retired_account_ids = ( + {str(target_account_id)} + if str(target_account_id) not in enabled_account_ids + else set() + ) + else: + retired_account_ids = ( + known_state_account_ids - enabled_account_ids + ) + + if retired_account_ids: + async def _retire_accounts(prior): + next_state = _merge_email_urgency_state( + prior, + owner=owner, + per_uid_scores={}, + notified_uids=prior.get("notified_uids", []), + all_unread_keys=set(), + fully_scanned_account_ids=set(), + base_account_generations=base_account_generations, + timestamp=_time.time(), + retired_account_ids=retired_account_ids, + base_payload_account_ids=base_payload_account_ids, + ) + return None, next_state + + await _run_email_urgency_state_transaction( + STATE_PATH, + STATE_LOCK_DB, + _retire_accounts, + ) if not accounts: raise TaskNoop("no email accounts configured") + # ── 2. Account retirement above is state maintenance and does not + # depend on model availability. Scanning still requires the utility + # primary/fallback candidates resolved for this task owner. + if not candidates: + return "No LLM endpoint available", False + urgency_prompt = settings.get("urgent_email_prompt", "") per_uid_scores = {} # key = ":" → {"score": 0-3, "reason": "..."} all_unread_keys = set() @@ -1929,6 +2428,7 @@ async def action_check_email_urgency(owner: str, **kwargs) -> Tuple[str, bool]: failed_classifications = [] tag_write_details = [] scanned = 0 + fully_scanned_account_ids = set() def _heuristic_email_verdict(item: dict) -> dict: blob = ( @@ -2024,16 +2524,27 @@ async def action_check_email_urgency(owner: str, **kwargs) -> Tuple[str, bool]: def _scan_one(account=acc, cache_uids=cache.get("uids", {})): """Sync IMAP work runs in a thread.""" results = [] + scan_complete = True conn = _imap_connect(account.id) try: - conn.select("INBOX", readonly=True) + select_status, _select_data = conn.select("INBOX", readonly=True) + if select_status != "OK": + return results, False # Tag recent inbox mail, not only unread mail. Urgency # reminders below still only notify for unread messages. since_str = AGE_CUTOFF.strftime("%d-%b-%Y") status, data = conn.uid("SEARCH", None, f'(SINCE {since_str})') - if status != "OK" or not data or not data[0]: - return results - uids = data[0].split()[-30:] + if status != "OK": + return results, False + if not data or not data[0]: + return results, True + matching_uids = data[0].split() + if len(matching_uids) > 30: + # The scale guard deliberately processes only the most + # recent 30. That is a partial account snapshot, so it + # cannot justify pruning older checkpoint facts. + scan_complete = False + uids = matching_uids[-30:] for uid_b in uids: uid = uid_b.decode() if isinstance(uid_b, bytes) else str(uid_b) key = f"{account.id}:{uid}" @@ -2041,12 +2552,41 @@ async def action_check_email_urgency(owner: str, **kwargs) -> Tuple[str, bool]: cached_ok = isinstance(cached, dict) and cached.get("triage_version") == TRIAGE_VERSION results.append({"key": key, "uid": uid, "cached": cached if cached_ok else None}) if cached_ok: - # Already classified — skip the fetch. + # Cached verdicts still need a lightweight FLAGS + # refresh. Without it a cached unread message looks + # read and its successful notification checkpoint + # is pruned on the next pass. + try: + st, flag_data = conn.uid("FETCH", uid_b, "(UID FLAGS)") + if st != "OK" or not flag_data: + scan_complete = False + results.pop() + continue + flag_parts = [] + for part in flag_data: + if isinstance(part, (bytes, bytearray)): + flag_parts.append(bytes(part)) + elif ( + isinstance(part, tuple) + and part + and isinstance(part[0], (bytes, bytearray)) + ): + flag_parts.append(bytes(part[0])) + flags_blob = b" ".join(flag_parts) + results[-1]["unread"] = b"\\Seen" not in flags_blob + except Exception as _fe: + scan_complete = False + results.pop() + logger.debug( + f"urgency: flag fetch for uid {uid} failed: {_fe}" + ) continue # Pull headers + first ~800 chars of plaintext body. try: st, msg_data = conn.uid("FETCH", uid_b, "(UID FLAGS RFC822.HEADER BODY.PEEK[TEXT]<0.800>)") if st != "OK" or not msg_data: + scan_complete = False + results.pop() continue flags_blob = b" ".join( part[0] for part in msg_data @@ -2060,6 +2600,8 @@ async def action_check_email_urgency(owner: str, **kwargs) -> Tuple[str, bool]: if isinstance(part, tuple) and part[1]: raw += part[1] + b"\n\n" if not raw: + scan_complete = False + results.pop() continue msg = _email_mod.message_from_bytes(raw) # Skip Odysseus-generated reminders so the scanner @@ -2115,17 +2657,21 @@ async def action_check_email_urgency(owner: str, **kwargs) -> Tuple[str, bool]: "unread": is_unread, }) except Exception as _fe: + scan_complete = False + results.pop() logger.debug(f"urgency: header fetch for uid {uid} failed: {_fe}") finally: try: conn.logout() except Exception: pass - return results + return results, scan_complete try: - items = await _aio.to_thread(_scan_one) + items, scan_complete = await _aio.to_thread(_scan_one) except Exception as e: logger.warning(f"urgency: IMAP scan failed for account {acc.id}: {e}") continue + if scan_complete: + fully_scanned_account_ids.add(str(acc.id)) for item in items: scanned += 1 @@ -2262,13 +2808,13 @@ async def action_check_email_urgency(owner: str, **kwargs) -> Tuple[str, bool]: logger.debug(f"urgency: LLM classify failed for {key}: {e}") continue - # ── Prune cache entries for UIDs that are no longer in the recent - # scan window. Read messages remain cached because tags are useful - # on read mail too; unread state is refreshed per scan above. - seen_uids = {it["uid"] for it in items} - cache_uids = cache.get("uids", {}) - for stale in [u for u in cache_uids if u not in seen_uids]: - cache_uids.pop(stale, None) + if scan_complete: + # Only a complete account scan proves a cached UID left the + # recent window. Partial/failing scans preserve prior facts. + seen_uids = {it["uid"] for it in items} + cache_uids = cache.get("uids", {}) + for stale in [u for u in cache_uids if u not in seen_uids]: + cache_uids.pop(stale, None) try: cache_file.write_text(_json.dumps(cache), encoding="utf-8") @@ -2372,40 +2918,34 @@ async def action_check_email_urgency(owner: str, **kwargs) -> Tuple[str, bool]: # ── 4. Aggregate state. urgent = score ≥ 2. urgent_keys = [k for k, v in per_uid_scores.items() if v.get("score", 0) >= 2 and v.get("unread")] - max_score = max((v.get("score", 0) for v in per_uid_scores.values()), default=0) - total_urgent = len(urgent_keys) - # Load prior state to know which urgent UIDs we've already notified. - try: - prior = _json.loads(STATE_PATH.read_text(encoding="utf-8")) if STATE_PATH.exists() else {} - except Exception: - prior = {} - notified_uids = set(prior.get("notified_uids", [])) - - # ── 5. Fire reminder ONLY when a previously-unnotified UID scores urgent. - new_urgent = [k for k in urgent_keys if k not in notified_uids] + # ── 5. Fire a reminder only when a previously-unnotified UID scores + # urgent. The read, decision, delivery, and checkpoint are serialized + # below so two scheduler workers cannot both act on the same stale + # state or overwrite each other's successful checkpoint. newly_notified = set() notify_failed = set() - if new_urgent: - title = "Urgent email" if total_urgent == 1 else f"{total_urgent} urgent emails" - # Build a real listing — subject · sender · reason for each urgent - # one — so the reminder email tells you which messages to act on, - # not just "4 needing reply". Optional deep-link when the user has - # `app_public_url` configured in Settings (so the email row links - # straight into the Odysseus Email tab). - # Sort: highest-scored UIDs first; cap at 10 to keep the email tidy. + + def _urgency_reminder_payload(reminder_keys): + total = len(reminder_keys) + title = "Urgent email" if total == 1 else f"{total} urgent emails" sorted_urgent = sorted( - ((k, per_uid_scores[k]) for k in urgent_keys), - key=lambda kv: kv[1].get("score", 0), reverse=True, + ((key, per_uid_scores[key]) for key in reminder_keys), + key=lambda item: item[1].get("score", 0), + reverse=True, )[:10] _pub = (settings.get("app_public_url") or "").strip().rstrip("/") from urllib.parse import quote as _quote - lines = [f"{total_urgent} email" + ("" if total_urgent == 1 else "s") + " need an urgent reply:", ""] - for i, (k, v) in enumerate(sorted_urgent, 1): - subj = (v.get("subject") or "(no subject)")[:160] - frm = v.get("from") or "" - why = v.get("reason") or "" - uid_for_link = str(k).split(":", 1)[-1] + lines = [ + f"{total} email" + ("" if total == 1 else "s") + + " need an urgent reply:", + "", + ] + for i, (key, value) in enumerate(sorted_urgent, 1): + subj = (value.get("subject") or "(no subject)")[:160] + frm = value.get("from") or "" + why = value.get("reason") or "" + uid_for_link = str(key).split(":", 1)[-1] hash_link = f"#email={_quote('INBOX', safe='')}:{uid_for_link}" open_link = f"{_pub}/{hash_link}" if _pub else hash_link line = f"{i}. {subj}" @@ -2415,57 +2955,94 @@ async def action_check_email_urgency(owner: str, **kwargs) -> Tuple[str, bool]: line += f" · {why}" lines.append(line) lines.append(f" Open email: {open_link}") - if total_urgent > len(sorted_urgent): + if total > len(sorted_urgent): lines.append("") - lines.append(f"…and {total_urgent - len(sorted_urgent)} more.") - body = "\n".join(lines) - try: - # Call dispatch_reminder DIRECTLY (no HTTP/auth roundtrip — the - # endpoint version 401's the background scheduler because it - # has no session cookie). - from routes.note_routes import dispatch_reminder - dispatch_result = await dispatch_reminder( - title=title, note_body=body, note_id="urgent-email", - owner=owner or "", - ) - channel = (settings.get("reminder_channel") or "browser").strip().lower() - delivered = bool(dispatch_result.get("browser_sent")) - if channel == "email": - delivered = bool(dispatch_result.get("email_sent")) - elif channel == "ntfy": - delivered = bool(dispatch_result.get("ntfy_sent")) - elif channel == "webhook": - delivered = bool(dispatch_result.get("webhook_sent")) - if delivered: - newly_notified.update(new_urgent) - else: + lines.append(f"…and {total - len(sorted_urgent)} more.") + return title, "\n".join(lines) + + async def _dispatch_urgency_reminder(reminder_keys): + # Call dispatch_reminder directly: a scheduler has no browser + # session cookie with which to call the HTTP endpoint. + from routes.note_routes import dispatch_reminder + title, body = _urgency_reminder_payload(reminder_keys) + return await dispatch_reminder( + title=title, + note_body=body, + note_id="urgent-email", + owner=owner or "", + ) + + async def _dispatch_and_checkpoint(prior): + notified_uids = _email_urgency_string_set( + prior.get("notified_uids", []) + ) + observed_accounts = { + _email_urgency_account_key(key) for key in per_uid_scores + } | fully_scanned_account_ids + stale_accounts = _email_urgency_stale_accounts( + prior, + base_account_generations, + observed_accounts, + ) + # Generation fencing must happen before delivery, not only during + # merge. A stale-only unread UID may have been removed, read, or + # downgraded by the newer completed scan. + deliverable_urgent = [ + key + for key in urgent_keys + if _email_urgency_account_key(key) not in stale_accounts + ] + new_urgent = [ + key + for key in deliverable_urgent + if key not in notified_uids + ] + if new_urgent: + try: + dispatch_result = await _dispatch_urgency_reminder( + deliverable_urgent + ) + channel = (settings.get("reminder_channel") or "browser").strip().lower() + delivered = bool(dispatch_result.get("browser_sent")) + if channel == "email": + delivered = bool(dispatch_result.get("email_sent")) + elif channel == "ntfy": + delivered = bool(dispatch_result.get("ntfy_sent")) + elif channel == "webhook": + delivered = bool(dispatch_result.get("webhook_sent")) + if delivered: + newly_notified.update(new_urgent) + notified_uids.update(new_urgent) + else: + notify_failed.update(new_urgent) + logger.warning( + "urgency: reminder dispatch returned no successful " + f"delivery path: {dispatch_result}" + ) + except Exception as e: + logger.warning(f"urgency: reminder dispatch failed: {e}") notify_failed.update(new_urgent) - logger.warning(f"urgency: reminder dispatch returned no successful delivery path: {dispatch_result}") - except Exception as e: - logger.warning(f"urgency: reminder dispatch failed: {e}") - notify_failed.update(new_urgent) - # Mark only successfully delivered UIDs as notified so a transient - # SMTP/ntfy/browser failure retries instead of lying forever. - notified_uids.update(newly_notified) - # Prune notified_uids that aren't unread anymore (so a future re-urgent - # message with the same UID — rare but possible after archive→unarchive - # — can re-notify). Keep only UIDs still in `all_unread_keys`. - notified_uids = {u for u in notified_uids if u in all_unread_keys} + next_state = _merge_email_urgency_state( + prior, + owner=owner, + per_uid_scores=per_uid_scores, + notified_uids=notified_uids, + all_unread_keys=all_unread_keys, + fully_scanned_account_ids=fully_scanned_account_ids, + base_account_generations=base_account_generations, + timestamp=_time.time(), + ) + return notified_uids, next_state - state = { - "ts": _time.time(), - "owner": owner or "", - "total_unread": len(all_unread_keys), - "total_urgent": total_urgent, - "max_score": max_score, - "per_uid": per_uid_scores, - "notified_uids": sorted(notified_uids), - } try: - STATE_PATH.write_text(_json.dumps(state), encoding="utf-8") + await _run_email_urgency_state_transaction( + STATE_PATH, + STATE_LOCK_DB, + _dispatch_and_checkpoint, + ) except Exception as e: - logger.warning(f"urgency: state write failed: {e}") + logger.warning(f"urgency: state transaction failed: {e}") # ── 6. Activity-log summary — counts line on top, then per-tier # bulleted breakdown so the user can see WHICH emails ranked where diff --git a/tests/test_email_urgency_checkpoint.py b/tests/test_email_urgency_checkpoint.py new file mode 100644 index 000000000..7fcec08a7 --- /dev/null +++ b/tests/test_email_urgency_checkpoint.py @@ -0,0 +1,1328 @@ +import asyncio +import json +import threading +from types import SimpleNamespace + +import pytest + + +class _Column: + def __eq__(self, _other): + return True + + def __ne__(self, _other): + return True + + +class _Query: + def __init__(self, rows): + self._rows = rows + + def filter(self, *_args, **_kwargs): + return self + + def all(self): + return list(self._rows) + + +class _Db: + def __init__(self, rows): + self._rows = rows + + def query(self, _model): + return _Query(self._rows()) + + def close(self): + return None + + +class _EmailAccount: + enabled = _Column() + owner = _Column() + imap_user = _Column() + from_address = _Column() + id = _Column() + + +class _FakeImap: + def __init__(self, account_id, failures, seen_accounts, search_uids): + self.account_id = account_id + self.failures = failures + self.seen_accounts = seen_accounts + self.search_uids = tuple(search_uids.get(account_id, ("1",))) + + def select(self, *_args, **_kwargs): + if self.account_id in self.failures: + raise RuntimeError(f"{self.account_id} unavailable") + return "OK", [] + + def uid(self, command, *_args): + if self.account_id in self.failures: + raise RuntimeError(f"{self.account_id} unavailable") + if command == "SEARCH": + return "OK", [" ".join(self.search_uids).encode()] + uid = _args[0] + uid = uid.decode() if isinstance(uid, bytes) else str(uid) + query = str(_args[-1]) if _args else "" + seen = "\\Seen" if self.account_id in self.seen_accounts else "" + flags = f"{uid} (UID {uid} FLAGS ({seen}))".encode() + if query == "(UID FLAGS)": + return "OK", [flags] + raw = ( + f"From: Sender {self.account_id} \r\n" + f"Subject: Urgent request for {self.account_id} uid {uid}\r\n" + f"Message-ID: <{self.account_id}-{uid}@example.com>\r\n" + "\r\n" + "Please reply immediately." + ).encode() + return "OK", [(flags, raw)] + + def logout(self): + return None + + +def _account(account_id): + return SimpleNamespace( + id=account_id, + enabled=True, + owner="alice", + imap_user="alice", + from_address="alice", + ) + + +def _configure_action(monkeypatch, tmp_path, account_ids): + from core import database + from routes import email_helpers + from src import builtin_actions, llm_core, settings, task_endpoint + + runtime = { + "accounts": list(account_ids), + "failures": set(), + "seen_accounts": set(), + "search_uids": {}, + "settings": { + "reminder_channel": "browser", + "reminder_llm_synthesis": False, + "app_public_url": "", + }, + } + + monkeypatch.setattr(builtin_actions, "DATA_DIR", str(tmp_path)) + monkeypatch.setattr( + builtin_actions, + "EMAIL_URGENCY_CACHE_DIR", + str(tmp_path / "urgency-cache"), + ) + monkeypatch.setattr(database, "EmailAccount", _EmailAccount) + monkeypatch.setattr( + database, + "SessionLocal", + lambda: _Db(lambda: [_account(value) for value in runtime["accounts"]]), + ) + monkeypatch.setattr( + task_endpoint, + "resolve_task_candidates", + lambda *args, **kwargs: [("http://llm", "model", {})], + ) + + async def fake_fallback(*_args, **_kwargs): + return '{"score": 3, "reason": "urgent"}' + + monkeypatch.setattr(llm_core, "llm_call_async_with_fallback", fake_fallback) + monkeypatch.setattr(settings, "load_settings", lambda: dict(runtime["settings"])) + monkeypatch.setattr( + email_helpers, + "SCHEDULED_DB", + tmp_path / "scheduled-emails.db", + ) + monkeypatch.setattr( + email_helpers, + "_imap_connect", + lambda account_id=None, **_kwargs: _FakeImap( + str(account_id), + runtime["failures"], + runtime["seen_accounts"], + runtime["search_uids"], + ), + ) + return builtin_actions, runtime + + +@pytest.mark.asyncio +async def test_urgency_state_transaction_serializes_decision_and_checkpoint(tmp_path): + """A later worker must observe the first worker's delivered UID.""" + from src.builtin_actions import _run_email_urgency_state_transaction + + state_path = tmp_path / "email_urgency_state_alice.json" + lock_db = tmp_path / "urgency.lock.sqlite3" + first_entered = asyncio.Event() + allow_first_to_finish = asyncio.Event() + second_entered = asyncio.Event() + deliveries = [] + + async def operation(name): + async def update(prior): + if name == "first": + first_entered.set() + await allow_first_to_finish.wait() + else: + second_entered.set() + + notified = set(prior.get("notified_uids", [])) + delivered = "acct:42" not in notified + if delivered: + deliveries.append(name) + notified.add("acct:42") + return delivered, { + "owner": "alice", + "notified_uids": sorted(notified), + } + + return await _run_email_urgency_state_transaction( + state_path, + lock_db, + update, + ) + + first = asyncio.create_task(operation("first")) + await asyncio.wait_for(first_entered.wait(), timeout=2) + second = asyncio.create_task(operation("second")) + await asyncio.sleep(0.1) + assert not second_entered.is_set() + allow_first_to_finish.set() + + assert await asyncio.wait_for(first, timeout=2) is True + assert await asyncio.wait_for(second, timeout=2) is False + assert deliveries == ["first"] + assert json.loads(state_path.read_text(encoding="utf-8")) == { + "owner": "alice", + "notified_uids": ["acct:42"], + } + + +def test_stale_complete_scan_preserves_newer_state_and_discards_stale_only_facts(): + from src.builtin_actions import _merge_email_urgency_state + + prior = { + "owner": "alice", + "per_uid": { + "acct:1": {"score": 0, "unread": True, "reason": "newer"}, + "acct:2": {"score": 2, "unread": True, "reason": "new UID"}, + }, + "notified_uids": ["acct:2"], + "account_generations": { + "acct": {"checkpoint": 1, "complete": 1}, + }, + } + + merged = _merge_email_urgency_state( + prior, + owner="alice", + per_uid_scores={ + "acct:1": {"score": 0, "unread": False, "reason": "older"}, + "acct:3": {"score": 3, "unread": True, "reason": "also observed"}, + }, + notified_uids={"acct:1", "acct:2", "acct:3"}, + all_unread_keys={"acct:3"}, + fully_scanned_account_ids={"acct"}, + base_account_generations={ + "acct": {"checkpoint": 0, "complete": 0}, + }, + timestamp=300.0, + ) + + assert merged["per_uid"]["acct:1"]["reason"] == "newer" + assert set(merged["per_uid"]) == {"acct:1", "acct:2"} + assert merged["notified_uids"] == ["acct:2"] + assert merged["account_generations"]["acct"] == { + "checkpoint": 1, + "complete": 1, + } + + +def test_newer_partial_checkpoint_fences_older_complete_snapshot(): + from src.builtin_actions import _merge_email_urgency_state + + legacy = { + "owner": "alice", + "per_uid": { + "acct:1": {"score": 3, "unread": True}, + }, + "notified_uids": ["acct:1"], + } + partial = _merge_email_urgency_state( + legacy, + owner="alice", + per_uid_scores={ + "acct:2": {"score": 3, "unread": True}, + }, + notified_uids={"acct:1", "acct:2"}, + all_unread_keys={"acct:2"}, + fully_scanned_account_ids=set(), + base_account_generations={}, + timestamp=200.0, + ) + assert partial["account_generations"]["acct"] == { + "checkpoint": 1, + "complete": 0, + } + + merged = _merge_email_urgency_state( + partial, + owner="alice", + per_uid_scores={ + "acct:1": {"score": 3, "unread": True}, + }, + notified_uids={"acct:1", "acct:2"}, + all_unread_keys={"acct:1"}, + fully_scanned_account_ids={"acct"}, + base_account_generations={}, + timestamp=300.0, + ) + + assert set(merged["per_uid"]) == {"acct:1", "acct:2"} + assert merged["notified_uids"] == ["acct:1", "acct:2"] + assert merged["account_generations"]["acct"] == { + "checkpoint": 1, + "complete": 0, + } + + +def test_authoritative_retirement_prunes_payload_and_recomputes_api_totals(): + from src.builtin_actions import _merge_email_urgency_state + + prior = { + "owner": "alice", + "per_uid": { + "acct-a:1": {"score": 1, "unread": True}, + "acct-b:1": {"score": 3, "unread": True}, + }, + "notified_uids": ["acct-b:1"], + "account_generations": { + "acct-a": {"checkpoint": 1, "complete": 1}, + "acct-b": {"checkpoint": 1, "complete": 1}, + }, + } + + merged = _merge_email_urgency_state( + prior, + owner="alice", + per_uid_scores={}, + notified_uids=prior["notified_uids"], + all_unread_keys=set(), + fully_scanned_account_ids=set(), + base_account_generations=prior["account_generations"], + timestamp=300.0, + retired_account_ids={"acct-b"}, + base_payload_account_ids={"acct-a", "acct-b"}, + ) + + assert merged["per_uid"] == { + "acct-a:1": {"score": 1, "unread": True}, + } + assert merged["notified_uids"] == [] + assert merged["total_unread"] == 1 + assert merged["total_urgent"] == 0 + assert merged["max_score"] == 1 + assert merged["account_generations"]["acct-b"] == { + "checkpoint": 2, + "complete": 1, + } + + +def test_authoritative_retirement_respects_generation_and_membership_fences(): + from src.builtin_actions import _merge_email_urgency_state + + newer = { + "owner": "alice", + "per_uid": { + "acct-b:2": {"score": 3, "unread": True, "reason": "newer"}, + }, + "notified_uids": ["acct-b:2"], + "account_generations": { + "acct-b": {"checkpoint": 2, "complete": 2}, + }, + } + generation_fenced = _merge_email_urgency_state( + newer, + owner="alice", + per_uid_scores={}, + notified_uids=newer["notified_uids"], + all_unread_keys=set(), + fully_scanned_account_ids=set(), + base_account_generations={ + "acct-b": {"checkpoint": 1, "complete": 1}, + }, + timestamp=300.0, + retired_account_ids={"acct-b"}, + base_payload_account_ids={"acct-b"}, + ) + assert generation_fenced["per_uid"] == newer["per_uid"] + assert generation_fenced["notified_uids"] == ["acct-b:2"] + assert generation_fenced["account_generations"]["acct-b"] == { + "checkpoint": 2, + "complete": 2, + } + + legacy_first_write = { + "owner": "alice", + "per_uid": { + "acct-b:3": {"score": 2, "unread": True, "reason": "concurrent"}, + }, + "notified_uids": [], + } + membership_fenced = _merge_email_urgency_state( + legacy_first_write, + owner="alice", + per_uid_scores={}, + notified_uids=[], + all_unread_keys=set(), + fully_scanned_account_ids=set(), + base_account_generations={}, + timestamp=300.0, + retired_account_ids={"acct-b"}, + base_payload_account_ids=set(), + ) + assert membership_fenced["per_uid"] == legacy_first_write["per_uid"] + + +@pytest.mark.asyncio +async def test_waiting_transaction_cancellation_does_not_leak_lock(tmp_path): + from src.builtin_actions import _run_email_urgency_state_transaction + + state_path = tmp_path / "email_urgency_state_alice.json" + lock_db = tmp_path / "urgency.lock.sqlite3" + first_entered = asyncio.Event() + release_first = asyncio.Event() + + async def first_operation(_prior): + first_entered.set() + await release_first.wait() + return None, {"notified_uids": ["acct:1"]} + + async def later_operation(prior): + return None, prior + + first = asyncio.create_task( + _run_email_urgency_state_transaction( + state_path, lock_db, first_operation + ) + ) + await asyncio.wait_for(first_entered.wait(), timeout=2) + waiting = asyncio.create_task( + _run_email_urgency_state_transaction( + state_path, lock_db, later_operation + ) + ) + await asyncio.sleep(0.05) + waiting.cancel() + with pytest.raises(asyncio.CancelledError): + await asyncio.wait_for(waiting, timeout=2) + + release_first.set() + await asyncio.wait_for(first, timeout=2) + await asyncio.wait_for( + _run_email_urgency_state_transaction( + state_path, lock_db, later_operation + ), + timeout=2, + ) + + +@pytest.mark.asyncio +async def test_action_dispatch_stays_on_app_loop_and_queues_browser_notification( + monkeypatch, + tmp_path, +): + import routes.note_routes as note_routes + from src import endpoint_resolver, llm_core + from src.task_scheduler import TaskScheduler + + builtin_actions, runtime = _configure_action( + monkeypatch, tmp_path, ["acct-a"] + ) + runtime["settings"]["reminder_llm_synthesis"] = True + monkeypatch.setattr(note_routes, "DATA_DIR", str(tmp_path)) + monkeypatch.setattr( + endpoint_resolver, + "resolve_endpoint", + lambda *_args, **_kwargs: ( + "https://api.openai.com/v1", + "utility-model", + {}, + ), + ) + + expected_loop = asyncio.get_running_loop() + expected_thread = threading.get_ident() + synthesis_loops = [] + shared_client = SimpleNamespace(is_closed=False) + monkeypatch.setattr(llm_core, "_http_client", shared_client) + monkeypatch.setattr(llm_core, "_response_cache", {}) + + class _Response: + is_success = True + status_code = 200 + text = "ok" + + @staticmethod + def json(): + return { + "choices": [ + {"message": {"content": "Synthesized urgency reminder."}} + ] + } + + async def fake_http_post(client, *_args, **_kwargs): + synthesis_loops.append(asyncio.get_running_loop()) + assert client is shared_client + return _Response() + + monkeypatch.setattr( + llm_core, + "httpx_post_kimi_aware_async", + fake_http_post, + ) + + scheduler = TaskScheduler(None) + notification_threads = [] + original_add = scheduler.add_notification + + def checked_add(*args, **kwargs): + notification_threads.append(threading.get_ident()) + return original_add(*args, **kwargs) + + monkeypatch.setattr(scheduler, "add_notification", checked_add) + monkeypatch.setattr(note_routes, "_scheduler_ref", scheduler) + + message, ok = await builtin_actions.action_check_email_urgency("alice") + + assert ok is True + assert "notified 1" in message + assert synthesis_loops == [expected_loop] + assert notification_threads == [expected_thread] + notifications = scheduler.pop_notifications(owner="alice") + assert len(notifications) == 1 + assert notifications[0]["body"] == "Synthesized urgency reminder." + + +@pytest.mark.asyncio +async def test_action_cancellation_rolls_back_without_checkpoint( + monkeypatch, + tmp_path, +): + import routes.note_routes as note_routes + + builtin_actions, _runtime = _configure_action( + monkeypatch, tmp_path, ["acct-a"] + ) + state_path = tmp_path / "email_urgency_state_alice.json" + state_path.write_text( + json.dumps({"owner": "alice", "per_uid": {}, "notified_uids": []}), + encoding="utf-8", + ) + entered = threading.Event() + release = threading.Event() + dispatch_cancelled = threading.Event() + + async def blocked_dispatch(**_kwargs): + entered.set() + try: + while not release.is_set(): + await asyncio.sleep(0.01) + except asyncio.CancelledError: + dispatch_cancelled.set() + raise + return { + "browser_sent": True, + "email_sent": False, + "ntfy_sent": False, + "webhook_sent": False, + } + + monkeypatch.setattr(note_routes, "dispatch_reminder", blocked_dispatch) + task = asyncio.create_task( + builtin_actions.action_check_email_urgency("alice") + ) + for _ in range(200): + if entered.is_set(): + break + await asyncio.sleep(0.01) + assert entered.is_set() + + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + release.set() + await asyncio.sleep(0.1) + + assert dispatch_cancelled.is_set() + state = json.loads(state_path.read_text(encoding="utf-8")) + assert state["owner"] == "alice" + assert state["per_uid"] == {} + assert state["notified_uids"] == [] + # The pre-scan active marker is not a delivered/checkpointed UID. It must + # survive cancellation so a concurrent deletion cleanup can fence this + # first-ever account scan. + assert state["account_generations"] == { + "acct-a": {"checkpoint": 0, "complete": 0}, + } + + +@pytest.mark.asyncio +async def test_first_scan_revalidates_account_deleted_before_registration( + monkeypatch, + tmp_path, +): + import routes.note_routes as note_routes + + builtin_actions, runtime = _configure_action( + monkeypatch, tmp_path, ["acct-a"] + ) + deliveries = [] + + async def delivered(**kwargs): + deliveries.append(kwargs["note_body"]) + return { + "browser_sent": True, + "email_sent": False, + "ntfy_sent": False, + "webhook_sent": False, + } + + monkeypatch.setattr(note_routes, "dispatch_reminder", delivered) + original_transaction = builtin_actions._run_email_urgency_state_transaction + + async def delete_before_registration(state_path, lock_db_path, operation): + if operation.__name__ == "_register_accounts": + # The initial DB read saw the account, but deletion commits before + # its first active marker. The post-registration enumeration must + # observe that absence and retire the marker without touching IMAP. + runtime["accounts"] = [] + return await original_transaction(state_path, lock_db_path, operation) + + monkeypatch.setattr( + builtin_actions, + "_run_email_urgency_state_transaction", + delete_before_registration, + ) + + with pytest.raises(builtin_actions.TaskNoop): + await builtin_actions.action_check_email_urgency("alice") + + state = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert deliveries == [] + assert state["per_uid"] == {} + assert state["notified_uids"] == [] + assert state["account_generations"]["acct-a"] == { + "checkpoint": 1, + "complete": 0, + } + + +@pytest.mark.asyncio +async def test_first_scan_deleted_after_revalidation_is_fenced_by_cleanup_pass( + monkeypatch, + tmp_path, +): + import routes.note_routes as note_routes + + builtin_actions, runtime = _configure_action( + monkeypatch, tmp_path, ["acct-a"] + ) + deliveries = [] + + async def delivered(**kwargs): + deliveries.append(kwargs["note_body"]) + return { + "browser_sent": True, + "email_sent": False, + "ntfy_sent": False, + "webhook_sent": False, + } + + monkeypatch.setattr(note_routes, "dispatch_reminder", delivered) + original_transaction = builtin_actions._run_email_urgency_state_transaction + scanned = asyncio.Event() + release_scan = asyncio.Event() + paused = False + + async def pause_first_delivery(state_path, lock_db_path, operation): + nonlocal paused + if operation.__name__ == "_dispatch_and_checkpoint" and not paused: + paused = True + scanned.set() + await release_scan.wait() + return await original_transaction(state_path, lock_db_path, operation) + + monkeypatch.setattr( + builtin_actions, + "_run_email_urgency_state_transaction", + pause_first_delivery, + ) + + stale = asyncio.create_task( + builtin_actions.action_check_email_urgency("alice") + ) + await asyncio.wait_for(scanned.wait(), timeout=2) + registered = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert registered["account_generations"]["acct-a"] == { + "checkpoint": 0, + "complete": 0, + } + + # Public-action boundary: deletion itself does not mutate urgency state. + # The authoritative zero-account pass is what advances the active marker + # before the paused scan reaches any reminder channel. + runtime["accounts"] = [] + with pytest.raises(builtin_actions.TaskNoop): + await builtin_actions.action_check_email_urgency("alice") + release_scan.set() + await asyncio.wait_for(stale, timeout=2) + + state = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert deliveries == [] + assert state["per_uid"] == {} + assert state["notified_uids"] == [] + assert state["total_unread"] == 0 + assert state["total_urgent"] == 0 + assert state["account_generations"]["acct-a"] == { + "checkpoint": 1, + "complete": 0, + } + + +def test_scan_keeps_registration_generation_when_cleanup_precedes_basis( + monkeypatch, + tmp_path, +): + """Cleanup after verification cannot become the stale scan's baseline.""" + from core import database + import routes.note_routes as note_routes + + builtin_actions, runtime = _configure_action( + monkeypatch, tmp_path, ["acct-a"] + ) + deliveries = [] + + async def delivered(**kwargs): + deliveries.append(kwargs["note_body"]) + return { + "browser_sent": True, + "email_sent": False, + "ntfy_sent": False, + "webhook_sent": False, + } + + monkeypatch.setattr(note_routes, "dispatch_reminder", delivered) + verified_account_selected = threading.Event() + release_verified_account = threading.Event() + stale_query_count = 0 + query_count_lock = threading.Lock() + + class _PausingQuery(_Query): + def all(self): + nonlocal stale_query_count + rows = list(self._rows) + if threading.current_thread().name == "stale-urgency-scan": + with query_count_lock: + stale_query_count += 1 + should_pause = stale_query_count == 2 + if should_pause: + # The second enumeration has selected the enabled row, but + # the action has not yet retained/used its checkpoint basis. + verified_account_selected.set() + assert release_verified_account.wait(5) + return rows + + class _PausingDb(_Db): + def query(self, _model): + return _PausingQuery(self._rows()) + + monkeypatch.setattr( + database, + "SessionLocal", + lambda: _PausingDb( + lambda: [_account(value) for value in runtime["accounts"]] + ), + ) + + stale_result = {} + + def run_stale_scan(): + try: + stale_result["value"] = asyncio.run( + builtin_actions.action_check_email_urgency("alice") + ) + except BaseException as exc: + stale_result["error"] = exc + + worker = threading.Thread( + target=run_stale_scan, + name="stale-urgency-scan", + ) + worker.start() + assert verified_account_selected.wait(5) + + runtime["accounts"] = [] + with pytest.raises(builtin_actions.TaskNoop): + asyncio.run(builtin_actions.action_check_email_urgency("alice")) + state_path = tmp_path / "email_urgency_state_alice.json" + retired = json.loads(state_path.read_text(encoding="utf-8")) + assert retired["account_generations"]["acct-a"] == { + "checkpoint": 1, + "complete": 0, + } + + release_verified_account.set() + worker.join(timeout=5) + assert not worker.is_alive() + assert "error" not in stale_result + + state = json.loads(state_path.read_text(encoding="utf-8")) + assert deliveries == [] + assert state["per_uid"] == {} + assert state["notified_uids"] == [] + assert state["account_generations"]["acct-a"] == { + "checkpoint": 1, + "complete": 0, + } + + +@pytest.mark.asyncio +async def test_payload_empty_tombstone_fences_reenable_redelete_and_can_recover( + monkeypatch, + tmp_path, +): + import routes.note_routes as note_routes + + builtin_actions, runtime = _configure_action( + monkeypatch, tmp_path, ["acct-a"] + ) + deliveries = [] + + async def delivered(**kwargs): + deliveries.append(kwargs["note_body"]) + return { + "browser_sent": True, + "email_sent": False, + "ntfy_sent": False, + "webhook_sent": False, + } + + monkeypatch.setattr(note_routes, "dispatch_reminder", delivered) + await builtin_actions.action_check_email_urgency("alice") + assert len(deliveries) == 1 + + runtime["accounts"] = [] + with pytest.raises(builtin_actions.TaskNoop): + await builtin_actions.action_check_email_urgency("alice") + first_tombstone = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert first_tombstone["per_uid"] == {} + assert first_tombstone["account_generations"]["acct-a"] == { + "checkpoint": 2, + "complete": 1, + } + + original_transaction = builtin_actions._run_email_urgency_state_transaction + scanned = asyncio.Event() + release_scan = asyncio.Event() + pause_reenabled = True + + async def pause_reenabled_delivery(state_path, lock_db_path, operation): + nonlocal pause_reenabled + if operation.__name__ == "_dispatch_and_checkpoint" and pause_reenabled: + pause_reenabled = False + scanned.set() + await release_scan.wait() + return await original_transaction(state_path, lock_db_path, operation) + + monkeypatch.setattr( + builtin_actions, + "_run_email_urgency_state_transaction", + pause_reenabled_delivery, + ) + deliveries.clear() + runtime["accounts"] = ["acct-a"] + stale_reenabled = asyncio.create_task( + builtin_actions.action_check_email_urgency("alice") + ) + await asyncio.wait_for(scanned.wait(), timeout=2) + + # Delete/disable again while the re-enabled scan is based on checkpoint 2. + # Discovery must include the payload-empty generation tombstone and advance + # it, otherwise the paused scan would deliver and resurrect acct-a:1. + runtime["accounts"] = [] + with pytest.raises(builtin_actions.TaskNoop): + await builtin_actions.action_check_email_urgency("alice") + release_scan.set() + await asyncio.wait_for(stale_reenabled, timeout=2) + + fenced = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert deliveries == [] + assert fenced["per_uid"] == {} + assert fenced["notified_uids"] == [] + assert fenced["account_generations"]["acct-a"] == { + "checkpoint": 3, + "complete": 1, + } + + # A later authoritative pass advances the empty tombstone again, fencing + # any scan that captured checkpoint 3 before this absence was confirmed. + with pytest.raises(builtin_actions.TaskNoop): + await builtin_actions.action_check_email_urgency("alice") + repeated = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert repeated["account_generations"]["acct-a"] == { + "checkpoint": 4, + "complete": 1, + } + + # Re-enabling after the latest tombstone captures checkpoint 4 and can + # publish fresh payload normally. + runtime["accounts"] = ["acct-a"] + await builtin_actions.action_check_email_urgency("alice") + recovered = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert len(deliveries) == 1 + assert set(recovered["per_uid"]) == {"acct-a:1"} + assert recovered["notified_uids"] == ["acct-a:1"] + assert recovered["account_generations"]["acct-a"] == { + "checkpoint": 5, + "complete": 2, + } + + +@pytest.mark.parametrize( + ("stale_uids", "newer_uids"), + [ + (["1"], ["1", "2"]), + (["1", "2"], ["1"]), + ], + ids=["preserve-newer-addition", "reject-stale-only-delivery"], +) +@pytest.mark.asyncio +async def test_stale_same_account_scan_cannot_override_newer_commit( + monkeypatch, + tmp_path, + stale_uids, + newer_uids, +): + import routes.note_routes as note_routes + + builtin_actions, runtime = _configure_action( + monkeypatch, tmp_path, ["acct-a"] + ) + runtime["search_uids"]["acct-a"] = stale_uids + deliveries = [] + + async def delivered(**kwargs): + deliveries.append(kwargs["note_body"]) + return { + "browser_sent": True, + "email_sent": False, + "ntfy_sent": False, + "webhook_sent": False, + } + + monkeypatch.setattr(note_routes, "dispatch_reminder", delivered) + original_transaction = builtin_actions._run_email_urgency_state_transaction + stale_scan_ready = asyncio.Event() + release_stale_scan = asyncio.Event() + transaction_count = 0 + + async def order_transactions(state_path, lock_db_path, operation): + nonlocal transaction_count + if operation.__name__ == "_dispatch_and_checkpoint": + transaction_count += 1 + if transaction_count == 1: + stale_scan_ready.set() + await release_stale_scan.wait() + return await original_transaction(state_path, lock_db_path, operation) + + monkeypatch.setattr( + builtin_actions, + "_run_email_urgency_state_transaction", + order_transactions, + ) + + stale = asyncio.create_task( + builtin_actions.action_check_email_urgency("alice") + ) + await asyncio.wait_for(stale_scan_ready.wait(), timeout=2) + + runtime["search_uids"]["acct-a"] = newer_uids + newer_result = await asyncio.wait_for( + builtin_actions.action_check_email_urgency("alice"), + timeout=2, + ) + release_stale_scan.set() + stale_result = await asyncio.wait_for(stale, timeout=2) + + state = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert newer_result[1] is True + assert stale_result[1] is True + assert len(deliveries) == 1 + for uid in newer_uids: + assert f"uid {uid}" in deliveries[0] + for uid in set(stale_uids) - set(newer_uids): + assert f"uid {uid}" not in deliveries[0] + expected_keys = {f"acct-a:{uid}" for uid in newer_uids} + assert set(state["per_uid"]) == expected_keys + assert state["notified_uids"] == sorted(expected_keys) + assert state["account_generations"]["acct-a"] == { + "checkpoint": 1, + "complete": 1, + } + + +@pytest.mark.asyncio +async def test_mixed_stale_and_fresh_accounts_exclude_stale_rows_from_delivery( + monkeypatch, + tmp_path, +): + import routes.note_routes as note_routes + + builtin_actions, runtime = _configure_action( + monkeypatch, tmp_path, ["acct-a", "acct-b"] + ) + deliveries = [] + + async def delivered(**kwargs): + deliveries.append(kwargs["note_body"]) + return { + "browser_sent": True, + "email_sent": False, + "ntfy_sent": False, + "webhook_sent": False, + } + + monkeypatch.setattr(note_routes, "dispatch_reminder", delivered) + original_transaction = builtin_actions._run_email_urgency_state_transaction + stale_scan_ready = asyncio.Event() + release_stale_scan = asyncio.Event() + transaction_count = 0 + + async def order_transactions(state_path, lock_db_path, operation): + nonlocal transaction_count + if operation.__name__ == "_dispatch_and_checkpoint": + transaction_count += 1 + if transaction_count == 1: + stale_scan_ready.set() + await release_stale_scan.wait() + return await original_transaction(state_path, lock_db_path, operation) + + monkeypatch.setattr( + builtin_actions, + "_run_email_urgency_state_transaction", + order_transactions, + ) + + stale = asyncio.create_task( + builtin_actions.action_check_email_urgency("alice") + ) + await asyncio.wait_for(stale_scan_ready.wait(), timeout=2) + + runtime["accounts"] = ["acct-a"] + await asyncio.wait_for( + builtin_actions.action_check_email_urgency( + "alice", + prompt='{"account_id":"acct-a"}', + ), + timeout=2, + ) + release_stale_scan.set() + await asyncio.wait_for(stale, timeout=2) + + state = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert len(deliveries) == 2 + assert "acct-a" in deliveries[0] + assert "acct-b" not in deliveries[0] + assert "acct-b" in deliveries[1] + assert "acct-a" not in deliveries[1] + assert set(state["per_uid"]) == {"acct-a:1", "acct-b:1"} + assert state["notified_uids"] == ["acct-a:1", "acct-b:1"] + + +@pytest.mark.asyncio +async def test_account_scoped_actions_merge_disjoint_checkpoints( + monkeypatch, + tmp_path, +): + import routes.note_routes as note_routes + + builtin_actions, runtime = _configure_action( + monkeypatch, tmp_path, ["acct-a"] + ) + deliveries = [] + + async def delivered(**kwargs): + deliveries.append(kwargs["note_body"]) + return { + "browser_sent": True, + "email_sent": False, + "ntfy_sent": False, + "webhook_sent": False, + } + + monkeypatch.setattr(note_routes, "dispatch_reminder", delivered) + + await builtin_actions.action_check_email_urgency( + "alice", prompt='{"account_id":"acct-a"}' + ) + runtime["accounts"] = ["acct-b"] + await builtin_actions.action_check_email_urgency( + "alice", prompt='{"account_id":"acct-b"}' + ) + runtime["accounts"] = ["acct-a"] + await builtin_actions.action_check_email_urgency( + "alice", prompt='{"account_id":"acct-a"}' + ) + + state = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert len(deliveries) == 2 + assert "Urgent request for acct-a" in deliveries[0] + assert "Urgent request for acct-b" in deliveries[1] + assert state["notified_uids"] == ["acct-a:1", "acct-b:1"] + assert set(state["per_uid"]) == {"acct-a:1", "acct-b:1"} + assert state["total_unread"] == 2 + assert state["total_urgent"] == 2 + + +@pytest.mark.asyncio +async def test_full_enumeration_retires_deleted_or_disabled_account( + monkeypatch, + tmp_path, +): + import routes.note_routes as note_routes + + builtin_actions, runtime = _configure_action( + monkeypatch, tmp_path, ["acct-a", "acct-b"] + ) + + async def delivered(**_kwargs): + return { + "browser_sent": True, + "email_sent": False, + "ntfy_sent": False, + "webhook_sent": False, + } + + monkeypatch.setattr(note_routes, "dispatch_reminder", delivered) + await builtin_actions.action_check_email_urgency("alice") + + # The production query returns only enabled, owner-visible accounts. A + # deleted row and a disabled row are therefore the same authoritative + # absence at this boundary. + runtime["accounts"] = ["acct-a"] + await builtin_actions.action_check_email_urgency("alice") + + state = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert set(state["per_uid"]) == {"acct-a:1"} + assert state["notified_uids"] == ["acct-a:1"] + assert state["total_unread"] == 1 + assert state["total_urgent"] == 1 + assert state["max_score"] == 3 + assert state["account_generations"]["acct-b"] == { + "checkpoint": 2, + "complete": 1, + } + + +@pytest.mark.asyncio +async def test_zero_enabled_accounts_cleanup_precedes_model_resolution( + monkeypatch, + tmp_path, +): + import routes.note_routes as note_routes + from src import task_endpoint + + builtin_actions, runtime = _configure_action( + monkeypatch, tmp_path, ["acct-a"] + ) + + async def delivered(**_kwargs): + return { + "browser_sent": True, + "email_sent": False, + "ntfy_sent": False, + "webhook_sent": False, + } + + monkeypatch.setattr(note_routes, "dispatch_reminder", delivered) + await builtin_actions.action_check_email_urgency("alice") + runtime["accounts"] = [] + + model_resolution_owners = [] + + def no_model_available(*_args, **kwargs): + model_resolution_owners.append(kwargs.get("owner")) + return [] + + monkeypatch.setattr( + task_endpoint, + "resolve_task_candidates", + no_model_available, + ) + with pytest.raises(builtin_actions.TaskNoop): + await builtin_actions.action_check_email_urgency("alice") + assert model_resolution_owners == ["alice"] + + state = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert state["per_uid"] == {} + assert state["notified_uids"] == [] + assert state["total_unread"] == 0 + assert state["total_urgent"] == 0 + assert state["max_score"] == 0 + assert state["account_generations"]["acct-a"] == { + "checkpoint": 2, + "complete": 1, + } + + +@pytest.mark.asyncio +async def test_scoped_missing_account_retires_only_selected_payload( + monkeypatch, + tmp_path, +): + import routes.note_routes as note_routes + + builtin_actions, runtime = _configure_action( + monkeypatch, tmp_path, ["acct-a", "acct-b"] + ) + + async def delivered(**_kwargs): + return { + "browser_sent": True, + "email_sent": False, + "ntfy_sent": False, + "webhook_sent": False, + } + + monkeypatch.setattr(note_routes, "dispatch_reminder", delivered) + await builtin_actions.action_check_email_urgency("alice") + + # A production account-id filter returns no row when the selected account + # was deleted or disabled. Other accounts are outside this scoped query and + # must remain untouched. + runtime["accounts"] = [] + with pytest.raises(builtin_actions.TaskNoop): + await builtin_actions.action_check_email_urgency( + "alice", + prompt='{"account_id":"acct-b"}', + ) + + state = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert set(state["per_uid"]) == {"acct-a:1"} + assert state["notified_uids"] == ["acct-a:1"] + assert state["total_unread"] == 1 + assert state["total_urgent"] == 1 + assert state["account_generations"]["acct-b"] == { + "checkpoint": 2, + "complete": 1, + } + + +@pytest.mark.asyncio +async def test_failed_account_scan_preserves_checkpoint_until_recovery( + monkeypatch, + tmp_path, +): + import routes.note_routes as note_routes + + builtin_actions, runtime = _configure_action( + monkeypatch, tmp_path, ["acct-a", "acct-b"] + ) + deliveries = [] + + async def delivered(**kwargs): + deliveries.append(kwargs["note_body"]) + return { + "browser_sent": True, + "email_sent": False, + "ntfy_sent": False, + "webhook_sent": False, + } + + monkeypatch.setattr(note_routes, "dispatch_reminder", delivered) + + await builtin_actions.action_check_email_urgency("alice") + runtime["failures"] = {"acct-b"} + await builtin_actions.action_check_email_urgency("alice") + + failed_state = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert failed_state["notified_uids"] == ["acct-a:1", "acct-b:1"] + assert set(failed_state["per_uid"]) == {"acct-a:1", "acct-b:1"} + assert failed_state["total_unread"] == 2 + assert failed_state["total_urgent"] == 2 + assert failed_state["account_generations"]["acct-b"] == { + "checkpoint": 1, + "complete": 1, + } + + runtime["failures"] = set() + runtime["accounts"] = ["acct-b"] + await builtin_actions.action_check_email_urgency( + "alice", prompt='{"account_id":"acct-b"}' + ) + + assert len(deliveries) == 1 + + +@pytest.mark.asyncio +async def test_cached_flags_refresh_prunes_checkpoint_after_message_is_read( + monkeypatch, + tmp_path, +): + import routes.note_routes as note_routes + + builtin_actions, runtime = _configure_action( + monkeypatch, tmp_path, ["acct-a"] + ) + + async def delivered(**_kwargs): + return { + "browser_sent": True, + "email_sent": False, + "ntfy_sent": False, + "webhook_sent": False, + } + + monkeypatch.setattr(note_routes, "dispatch_reminder", delivered) + await builtin_actions.action_check_email_urgency("alice") + runtime["seen_accounts"] = {"acct-a"} + await builtin_actions.action_check_email_urgency("alice") + + state = json.loads( + (tmp_path / "email_urgency_state_alice.json").read_text(encoding="utf-8") + ) + assert state["notified_uids"] == [] + assert state["per_uid"]["acct-a:1"]["unread"] is False + assert state["total_unread"] == 0 From 17ee856d1c39fcf7dbe22254da1a42dae0b9a98b Mon Sep 17 00:00:00 2001 From: DocFurious Date: Wed, 12 Aug 2026 10:13:16 +0300 Subject: [PATCH 070/125] fix(teacher): import _TEACHER_SYSTEM_PROMPT from its current module (#5756) * fix(teacher): import teacher prompt from current module * test(teacher): make prompt monkeypatch import-order independent --------- Co-authored-by: Alexandre Teixeira --- src/teacher_escalation.py | 3 ++- tests/test_teacher_audit_owner_scope.py | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/teacher_escalation.py b/src/teacher_escalation.py index 49134991c..4b6206a8d 100644 --- a/src/teacher_escalation.py +++ b/src/teacher_escalation.py @@ -233,7 +233,8 @@ async def _call_teacher(teacher_model_spec: str, prompt: str, owner: Optional[str] = None) -> Optional[str]: """Call the configured teacher endpoint with the escalation prompt.""" from src.llm_core import llm_call_async - from src.ai_interaction import _resolve_model, _TEACHER_SYSTEM_PROMPT + from src.ai_interaction import _resolve_model + from src.agent_tools.model_interaction_tools import _TEACHER_SYSTEM_PROMPT try: url, model, headers = await asyncio.to_thread(_resolve_model, teacher_model_spec, owner=owner) except Exception as e: diff --git a/tests/test_teacher_audit_owner_scope.py b/tests/test_teacher_audit_owner_scope.py index 5bd6228d9..8e398d9bb 100644 --- a/tests/test_teacher_audit_owner_scope.py +++ b/tests/test_teacher_audit_owner_scope.py @@ -21,10 +21,17 @@ def test_call_teacher_scopes_model_resolution_to_owner(monkeypatch): return ("http://endpoint.local/v1", "teacher-model", {}) async def fake_llm_call_async(url, model, messages, **kwargs): + seen["messages"] = messages return "teacher reply" + from src.agent_tools import model_interaction_tools + monkeypatch.setattr("src.ai_interaction._resolve_model", fake_resolve_model) - monkeypatch.setattr("src.ai_interaction._TEACHER_SYSTEM_PROMPT", "sys", raising=False) + monkeypatch.setattr( + model_interaction_tools, + "_TEACHER_SYSTEM_PROMPT", + "sys", + ) monkeypatch.setattr("src.llm_core.llm_call_async", fake_llm_call_async) result = asyncio.run( @@ -34,6 +41,7 @@ def test_call_teacher_scopes_model_resolution_to_owner(monkeypatch): assert result == "teacher reply" assert seen["owner"] == "alice" assert seen["spec"] == "teacher-model" + assert seen["messages"][0] == {"role": "system", "content": "sys"} def test_audit_teacher_resolution_scoped_to_owner(monkeypatch): From 53869d194d22b895cf9bee569fd53cec2864895e Mon Sep 17 00:00:00 2001 From: Dividesbyzer0 <54127744+zoomdbz@users.noreply.github.com> Date: Wed, 12 Aug 2026 05:32:24 -0400 Subject: [PATCH 071/125] fix(cookbook): record real Windows pid for local serve so Stop kills the model (#5912) * fix(cookbook): record real Windows pid for local serve so Stop kills the model The Windows-local serve runner recorded Git Bash's `$$`, which is the MSYS/Cygwin pid, not the Windows pid. Win32 tooling (taskkill, Get-CimInstance ParentProcessId, Stop-Process) can't match an MSYS pid, so the frontend Stop-Tree walk found nothing and the llama-server child survived after Stop, leaving the model loaded and the GPU pinned. Record the serving shell's true Win32 pid via `/proc/$$/winpid`, falling back to the outer proc.pid already written from Python when the map is unavailable. The existing pid-tracking test asserted the buggy `$$` literal at the source level, so it passed while the feature was broken; update it to the winpid behavior and add a focused regression test. * fix(cookbook): make Windows serve pid handoff deterministic --------- Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> Co-authored-by: Alexandre Teixeira --- routes/cookbook_routes.py | 42 +++- tests/test_cookbook_helpers.py | 7 +- tests/test_cookbook_local_serve_pid_winpid.py | 180 ++++++++++++++++++ 3 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 tests/test_cookbook_local_serve_pid_winpid.py diff --git a/routes/cookbook_routes.py b/routes/cookbook_routes.py index 1d79ba809..d51fb9a09 100644 --- a/routes/cookbook_routes.py +++ b/routes/cookbook_routes.py @@ -73,6 +73,30 @@ _HF_TOKEN_STATUS_SNIPPET = ( ) +def _windows_local_pid_record_line(pid_path: Path, ready_path: Path) -> str: + """Build the Git Bash prelude that records a Win32-stoppable PID. + + Python publishes the detached outer process's Win32 PID first, then touches + ``ready_path``. The inner Git Bash runner waits for that publication before + replacing the fallback with its own Win32 PID from /proc//winpid. + + Missing, malformed, or late mappings leave the valid outer PID untouched. + """ + pp = shlex.quote(pid_path.as_posix()) + rp = shlex.quote(ready_path.as_posix()) + return ( + "i=0; " + f"while [ ! -e {rp} ] && [ \"$i\" -lt 500 ]; do " + "i=$((i+1)); sleep 0.01; done; " + f"if [ -e {rp} ]; then " + "winpid=\"$(cat /proc/$$/winpid 2>/dev/null || true)\"; " + "case \"$winpid\" in ''|*[!0-9]*) ;; " + f"*) printf '%s\\n' \"$winpid\" > {pp} ;; esac; " + "fi; " + f"rm -f {rp}" + ) + + def _append_mlx_image_server_script(runner_lines: list[str]) -> None: """Write the MLX image API helper next to the tmux runner on remote hosts.""" script_path = Path(__file__).resolve().parents[1] / "scripts" / "mlx_image_server.py" @@ -978,15 +1002,18 @@ def setup_cookbook_routes() -> APIRouter: directly (simple commands only). Returns the launched job record.""" log_path = TMUX_LOG_DIR / f"{session_id}.log" pid_path = TMUX_LOG_DIR / f"{session_id}.pid" + pid_ready_path: Path | None = None bash = find_bash() if bash: # Run the existing bash wrapper verbatim through Git Bash, redirecting # all output to the log the poller reads. Paths handed to bash use # POSIX form + shell-quoting so drive paths / spaces survive. inner = TMUX_LOG_DIR / f"{session_id}_run.sh" - pp = shlex.quote(pid_path.as_posix()) + pid_ready_path = TMUX_LOG_DIR / f"{session_id}.pid.ready" + pid_ready_path.unlink(missing_ok=True) inner.write_text( - f"printf '%s\\n' \"$$\" > {pp}\n" + "\n".join(bash_lines) + "\n", + _windows_local_pid_record_line(pid_path, pid_ready_path) + "\n" + + "\n".join(bash_lines) + "\n", encoding="utf-8", ) lp = shlex.quote(log_path.as_posix()) @@ -1020,7 +1047,18 @@ def setup_cookbook_routes() -> APIRouter: env=env, **detached_popen_kwargs(), ) + # Publish a valid Win32 ancestor first. The Git Bash runner may then + # replace it with its own Win32 pid, but never before this fallback exists. pid_path.write_text(str(proc.pid), encoding="utf-8") + if pid_ready_path is not None: + try: + pid_ready_path.touch() + except OSError as e: + logger.warning( + "Could not publish Windows local PID handoff for %s: %s", + session_id, + e, + ) return {"pid": proc.pid, "log_path": str(log_path)} @router.post("/api/model/download") diff --git a/tests/test_cookbook_helpers.py b/tests/test_cookbook_helpers.py index bf6c47d4b..37620c88a 100644 --- a/tests/test_cookbook_helpers.py +++ b/tests/test_cookbook_helpers.py @@ -723,7 +723,12 @@ def test_local_windows_download_pid_tracks_inner_bash_and_stop_kills_tree(): routes_src = (Path(__file__).resolve().parents[1] / "routes" / "cookbook_routes.py").read_text(encoding="utf-8") running_src = (Path(__file__).resolve().parents[1] / "static" / "js" / "cookbookRunning.js").read_text(encoding="utf-8") - assert 'printf \'%s\\\\n\' \\"$$\\" > {pp}' in routes_src + # The Windows-local runner publishes Python's valid Win32 fallback before + # allowing Git Bash to replace it with /proc/$$/winpid. + assert "_windows_local_pid_record_line(pid_path, pid_ready_path)" in routes_src + assert "/proc/$$/winpid" in routes_src + assert "pid_ready_path.touch()" in routes_src + assert '\\"$$\\" > {pp}' not in routes_src assert "function Stop-Tree([int]$Id)" in running_src assert "('ParentProcessId = ' + $Id)" in running_src assert "Stop-Tree ([int]$p)" in running_src diff --git a/tests/test_cookbook_local_serve_pid_winpid.py b/tests/test_cookbook_local_serve_pid_winpid.py new file mode 100644 index 000000000..7038fccbf --- /dev/null +++ b/tests/test_cookbook_local_serve_pid_winpid.py @@ -0,0 +1,180 @@ +"""Behavioral regression coverage for Windows-local Cookbook PID recording.""" + +import os +import subprocess +import time +from pathlib import Path + +from routes.cookbook_routes import _windows_local_pid_record_line + + +ROOT = Path(__file__).resolve().parents[1] +COOKBOOK_ROUTES = ROOT / "routes" / "cookbook_routes.py" + + +def _fake_cat(tmp_path: Path, body: str) -> Path: + fake_bin = tmp_path / "bin" + fake_bin.mkdir() + cat = fake_bin / "cat" + cat.write_text("#!/bin/sh\n" + body + "\n", encoding="utf-8") + cat.chmod(0o755) + return fake_bin + + +def _env_for(fake_bin: Path, **extra: str) -> dict[str, str]: + env = dict(os.environ) + env["PATH"] = str(fake_bin) + os.pathsep + env.get("PATH", "") + env.update(extra) + return env + + +def _run_pid_line( + pid_path: Path, + ready_path: Path, + fake_bin: Path, + **extra_env: str, +) -> subprocess.CompletedProcess: + return subprocess.run( + ["bash", "-c", _windows_local_pid_record_line(pid_path, ready_path)], + capture_output=True, + text=True, + env=_env_for(fake_bin, **extra_env), + timeout=10, + ) + + +def test_windows_local_pid_line_records_numeric_winpid_after_fallback(tmp_path): + pid_path = tmp_path / "serve.pid" + ready_path = tmp_path / "serve.pid.ready" + + pid_path.write_text("11111", encoding="utf-8") + ready_path.touch() + + cat_arg = tmp_path / "cat-arg.txt" + fake_bin = _fake_cat( + tmp_path, + 'printf "%s\\n" "$1" > "$FAKE_CAT_ARG"\n' + 'printf "%s\\n" "$FAKE_WINPID"', + ) + + result = _run_pid_line( + pid_path, + ready_path, + fake_bin, + FAKE_CAT_ARG=str(cat_arg), + FAKE_WINPID="42324", + ) + + assert result.returncode == 0, result.stderr + assert pid_path.read_text(encoding="utf-8").strip() == "42324" + assert not ready_path.exists() + + proc_path = cat_arg.read_text(encoding="utf-8").strip() + parts = proc_path.strip("/").split("/") + assert len(parts) == 3 + assert parts[0] == "proc" + assert parts[1].isdigit() + assert parts[2] == "winpid" + + +def test_windows_local_pid_line_waits_for_python_fallback_before_replacing(tmp_path): + pid_path = tmp_path / "serve.pid" + ready_path = tmp_path / "serve.pid.ready" + + fake_bin = _fake_cat( + tmp_path, + 'printf "%s\\n" "$FAKE_WINPID"', + ) + + proc = subprocess.Popen( + [ + "bash", + "-c", + _windows_local_pid_record_line(pid_path, ready_path), + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + env=_env_for(fake_bin, FAKE_WINPID="42324"), + ) + + # The inner shell has started, but Python has not published its fallback yet. + time.sleep(0.05) + assert proc.poll() is None + assert not pid_path.exists() + + # Simulate the post-Popen Python publication order. + pid_path.write_text("31100", encoding="utf-8") + ready_path.touch() + + stdout, stderr = proc.communicate(timeout=10) + + assert proc.returncode == 0, stderr or stdout + assert pid_path.read_text(encoding="utf-8").strip() == "42324" + assert not ready_path.exists() + + +def test_windows_local_pid_line_preserves_outer_pid_when_mapping_missing(tmp_path): + pid_path = tmp_path / "serve.pid" + ready_path = tmp_path / "serve.pid.ready" + + pid_path.write_text("31100", encoding="utf-8") + ready_path.touch() + + fake_bin = _fake_cat(tmp_path, "exit 1") + + result = _run_pid_line( + pid_path, + ready_path, + fake_bin, + ) + + assert result.returncode == 0, result.stderr + assert pid_path.read_text(encoding="utf-8").strip() == "31100" + assert not ready_path.exists() + + +def test_windows_local_pid_line_rejects_malformed_mapping(tmp_path): + pid_path = tmp_path / "serve.pid" + ready_path = tmp_path / "serve.pid.ready" + + pid_path.write_text("31100", encoding="utf-8") + ready_path.touch() + + fake_bin = _fake_cat( + tmp_path, + 'printf "not-a-win32-pid\\n"', + ) + + result = _run_pid_line( + pid_path, + ready_path, + fake_bin, + ) + + assert result.returncode == 0, result.stderr + assert pid_path.read_text(encoding="utf-8").strip() == "31100" + assert not ready_path.exists() + + +def test_local_windows_launcher_publishes_fallback_before_releasing_inner_runner(): + source = COOKBOOK_ROUTES.read_text(encoding="utf-8") + start = source.index(" def _launch_local_detached(") + end = source.index( + ' @router.post("/api/model/download")', + start, + ) + launcher = source[start:end] + + assert "_windows_local_pid_record_line(pid_path, pid_ready_path)" in launcher + assert "pid_ready_path.unlink(missing_ok=True)" in launcher + + fallback = launcher.index( + 'pid_path.write_text(str(proc.pid), encoding="utf-8")' + ) + release = launcher.index("pid_ready_path.touch()") + + assert fallback < release + + # Never write Git Bash's bare MSYS $$ to the session pid file. + assert '\\"$$\\" > {pp}' not in launcher From b52296471ba669944aba35b18312725dff1381b8 Mon Sep 17 00:00:00 2001 From: RaresKeY <158580472+RaresKeY@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:10:07 +0100 Subject: [PATCH 072/125] fix(model-routing): keep selected models strict (#5801) Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> --- routes/chat_routes.py | 22 +- routes/email_routes.py | 8 +- routes/model_routes.py | 34 +-- src/endpoint_resolver.py | 20 +- src/foreground_model_routing.py | 31 +++ src/llm_core.py | 9 +- src/settings.py | 13 +- src/task_endpoint.py | 2 +- static/index.html | 2 +- static/js/settings.js | 11 +- tests/test_foreground_model_routing.py | 277 +++++++++++++++++++++++ tests/test_legacy_default_fallback_ui.py | 29 +++ tests/test_model_defaults.py | 49 +++- tests/test_model_routes.py | 13 +- 14 files changed, 417 insertions(+), 103 deletions(-) create mode 100644 src/foreground_model_routing.py create mode 100644 tests/test_foreground_model_routing.py create mode 100644 tests/test_legacy_default_fallback_ui.py diff --git a/routes/chat_routes.py b/routes/chat_routes.py index b081d5f1c..6099e72fd 100644 --- a/routes/chat_routes.py +++ b/routes/chat_routes.py @@ -21,6 +21,7 @@ from src import agent_runs from src.model_context import estimate_tokens from src.chat_helpers import coerce_message_and_session from src.endpoint_resolver import normalize_base as _normalize_base, build_chat_url +from src.foreground_model_routing import build_foreground_model_candidates from src.session_search import search_session_messages from src.prompt_security import untrusted_context_message from core.exceptions import SessionNotFoundError @@ -1399,14 +1400,14 @@ def setup_chat_routes( thinking_response = "" last_metrics = None - # Configured fallback chain for the default chat model. Tried in - # order if the session's primary model fails before producing - # output. Resolved once per request. - try: - from src.endpoint_resolver import resolve_chat_fallback_candidates - _fallback_candidates = resolve_chat_fallback_candidates(owner=_user) - except Exception: - _fallback_candidates = [] + # Foreground Chat and Agent requests use one owner-aware policy + # boundary. Legacy `default_model_fallbacks` data is not eligible. + _foreground_candidates = build_foreground_model_candidates( + sess.endpoint_url, + sess.model, + sess.headers, + owner=_user, + ) # Send model name early so the frontend can show it during streaming _model_suffix = "Research" if effective_do_research else None @@ -1522,9 +1523,8 @@ def setup_chat_routes( _actual_model = None # ── Chat mode: call stream_llm directly, NO tools, NO document access ── try: - _chat_candidates = [(sess.endpoint_url, sess.model, sess.headers)] + _fallback_candidates async for chunk in stream_llm_with_fallback( - _chat_candidates, + _foreground_candidates, messages, temperature=ctx.preset.temperature, # Respect the preset; 0/unset = let the server decide (no @@ -1710,7 +1710,7 @@ def setup_chat_routes( disabled_tools=disabled_tools if disabled_tools else None, tool_policy=tool_policy, owner=_user, - fallbacks=_fallback_candidates, + fallbacks=_foreground_candidates[1:], plan_mode=plan_mode, approved_plan=approved_plan or None, workspace=workspace or None, diff --git a/routes/email_routes.py b/routes/email_routes.py index e10eabd59..a28a68118 100644 --- a/routes/email_routes.py +++ b/routes/email_routes.py @@ -5327,9 +5327,9 @@ def setup_email_routes(): # Build a candidate chain so a stale session-stored API key # (the most common cause of "authentication failed" here) # doesn't kill AI Reply outright — fall through to the - # user's Utility / Default endpoints AND their configured - # fallback chains. Dedupe by url+model so we don't retry - # the same broken endpoint. + # user's Utility / Default endpoints and the active Utility + # fallback chain. The retired default-fallback hook stays empty. + # Dedupe by url+model so we don't retry the same broken endpoint. from src.llm_core import llm_call_async_with_fallback from src.endpoint_resolver import ( resolve_utility_fallback_candidates, @@ -5358,7 +5358,7 @@ def setup_email_routes(): _add(_d_url, _d_model, _d_headers) except Exception: pass - # Configured fallback chains last. + # Active Utility fallbacks, then the retired default hook. for cand in resolve_utility_fallback_candidates(owner=owner) or []: _add(*cand) for cand in resolve_chat_fallback_candidates(owner=owner) or []: diff --git a/routes/model_routes.py b/routes/model_routes.py index 600150a66..de8f884cb 100644 --- a/routes/model_routes.py +++ b/routes/model_routes.py @@ -46,10 +46,11 @@ _ENDPOINT_SETTING_FIELDS = { } _ENDPOINT_FALLBACK_FIELDS = { - "default_model_fallbacks": "Default Model Fallbacks", "utility_model_fallbacks": "Utility Model Fallbacks", "vision_model_fallbacks": "Vision Model Fallbacks", } +# `default_model_fallbacks` is intentionally absent. The legacy data remains +# stored as-is even when an endpoint is removed, but no longer affects routing. def _speech_settings_using_endpoint(settings: dict, ep_id: str) -> list: @@ -2437,7 +2438,6 @@ def setup_model_routes(model_discovery): _user_prefs = _load_for_user(_user) or {} ep_id = (_user_prefs.get("default_endpoint_id") or "").strip() model = (_user_prefs.get("default_model") or "").strip() - _fallbacks = _user_prefs.get("default_model_fallbacks") or [] # If user has no personal default, fall back to global default # But only based on the "share_defaults_with_users" flag # (only if share_defaults_with_users is enabled) @@ -2446,12 +2446,9 @@ def setup_model_routes(model_discovery): ep_id = settings.get("default_endpoint_id", "") if not model: model = settings.get("default_model", "") - if not _fallbacks: - _fallbacks = settings.get("default_model_fallbacks") or [] else: ep_id = settings.get("default_endpoint_id", "") model = settings.get("default_model", "") - _fallbacks = settings.get("default_model_fallbacks") or [] db = SessionLocal() try: ep = None @@ -2466,33 +2463,6 @@ def setup_model_routes(model_discovery): if _user and not _is_admin: ep_q = owner_filter(ep_q, ModelEndpoint, _user) ep = ep_q.first() - # Configured fallback chain — when the chosen default endpoint is - # gone/disabled, honor the user's configured `default_model_fallbacks` - # in order BEFORE arbitrarily grabbing the first enabled endpoint. - # (Previously this jumped straight to "first enabled", which is why - # deleting/changing the main endpoint silently reassigned the default - # chat to some unrelated endpoint instead of the fallback.) - if not ep: - for entry in _fallbacks: - if not isinstance(entry, dict): - continue - fid = (entry.get("endpoint_id") or "").strip() - if not fid: - continue - cand_q = db.query(ModelEndpoint).filter( - ModelEndpoint.id == fid, ModelEndpoint.is_enabled == True - ) - if _user and not _is_admin: - cand_q = owner_filter(cand_q, ModelEndpoint, _user) - cand = cand_q.first() - if cand: - ep = cand - # Use the fallback entry's model. Reset even when empty - # so we don't carry the prior endpoint's stale model onto - # this fallback — the cached-models lookup below then - # fills it from the fallback endpoint. - model = (entry.get("model") or "").strip() - break # Last resort: first enabled endpoint owned by THIS user. Do not # include null-owner/shared endpoints here: a brand-new user with # no explicit default should not auto-open a pending chat using an diff --git a/src/endpoint_resolver.py b/src/endpoint_resolver.py index 71f260fa2..1bb8fc3af 100644 --- a/src/endpoint_resolver.py +++ b/src/endpoint_resolver.py @@ -443,28 +443,14 @@ def resolve_endpoint_by_id( def resolve_chat_fallback_candidates(owner: Optional[str] = None) -> list: - """Build the configured default-chat fallback chain as a list of - (chat_url, model, headers) tuples, skipping any that can't resolve. + """Compatibility shim for the retired default-chat fallback chain.""" - The primary model is NOT included — callers prepend their session's - current (url, model, headers) so per-session model overrides are honored. - """ - return _resolve_fallback_candidates("default_model_fallbacks", owner=owner) + del owner + return [] def resolve_utility_fallback_candidates(owner: Optional[str] = None) -> list: """Configured fallback chain for the Utility model (`utility_model_fallbacks`).""" - try: - from src.settings import get_user_setting, load_settings - settings = load_settings() - utility_ep = (get_user_setting("utility_endpoint_id", owner or "", settings.get("utility_endpoint_id", "")) or "").strip() - if not utility_ep: - utility_chain = get_user_setting("utility_model_fallbacks", owner or "", settings.get("utility_model_fallbacks") or []) or [] - if utility_chain: - return _resolve_fallback_candidates("utility_model_fallbacks", owner=owner) - return _resolve_fallback_candidates("default_model_fallbacks", owner=owner) - except Exception: - pass return _resolve_fallback_candidates("utility_model_fallbacks", owner=owner) diff --git a/src/foreground_model_routing.py b/src/foreground_model_routing.py new file mode 100644 index 000000000..241ccf26b --- /dev/null +++ b/src/foreground_model_routing.py @@ -0,0 +1,31 @@ +"""Foreground Chat and Agent model-routing policy. + +The selected session model is strict by default. Historical +``default_model_fallbacks`` values remain stored for compatibility, but this +policy intentionally does not read or migrate them. +""" + +from typing import Any, Dict, Optional + + +def resolve_foreground_fallback_candidates(owner: Optional[str] = None) -> list: + """Return fallback candidates for a foreground Chat or Agent request. + + Foreground routing is strict, so no alternate endpoint/model is eligible. + ``owner`` is accepted to keep this policy boundary owner-aware. + """ + + del owner + return [] + + +def build_foreground_model_candidates( + endpoint_url: str, + model: str, + headers: Optional[Dict[str, Any]] = None, + owner: Optional[str] = None, +) -> list: + """Build the ordered candidate list for a foreground request.""" + + primary = (endpoint_url, model, headers or {}) + return [primary] + resolve_foreground_fallback_candidates(owner=owner) diff --git a/src/llm_core.py b/src/llm_core.py index 30aff2e47..e69661fb7 100644 --- a/src/llm_core.py +++ b/src/llm_core.py @@ -1949,11 +1949,10 @@ def _dedupe_candidates(candidates): """Filter malformed entries and drop a later repeat of an already-seen ``(url, model)`` route, preserving order (first occurrence wins). - The chain is the primary target followed by the configured fallbacks, so a - fallback that repeats the session's current model — a common misconfiguration, - since callers prepend the live ``(url, model)`` to ``default_model_fallbacks`` - — would otherwise make the chain re-attempt the very route that just failed: - a wasted round-trip plus a spurious ``fallback`` notice for a switch that did + The chain is the primary target followed by any caller-authorized + fallbacks. A fallback that repeats the session's current model would + otherwise make the chain re-attempt the very route that just failed: a + wasted round-trip plus a spurious ``fallback`` notice for a switch that did not happen. Headers are not part of the key; the first tuple (with its headers) is the one kept. """ diff --git a/src/settings.py b/src/settings.py index 5836765f1..da08717d5 100644 --- a/src/settings.py +++ b/src/settings.py @@ -138,14 +138,13 @@ DEFAULT_SETTINGS = { # Email replies use email_writing_style instead because greetings, # signatures, and mailbox identity rules are medium-specific. "document_writing_style": "", - # Ordered fallback chain for the default chat model. Each entry is - # {"endpoint_id": "...", "model": "..."}. If the primary model fails - # before producing output (endpoint offline / errors), the chat - # dispatch retries the next entry in order. + # Legacy ordered fallback chain for the default chat model. Values remain + # stored for compatibility and rollback reference, but model routing no + # longer reads this key. "default_model_fallbacks": [], - # When True, non-admin users inherit global default model/endpoint/fallbacks - # when they have no personal defaults. When False, users only use their - # personal defaults (no global fallback). Default is False. + # When True, non-admin users inherit the global default model/endpoint when + # they have no personal defaults. When False, users only use their personal + # defaults. Default is False. "share_defaults_with_users": False, "utility_endpoint_id": "", "utility_model": "", diff --git a/src/task_endpoint.py b/src/task_endpoint.py index b9c290d65..ae57a81f7 100644 --- a/src/task_endpoint.py +++ b/src/task_endpoint.py @@ -32,7 +32,7 @@ def resolve_task_candidates( 2. Utility endpoint/model 3. Default endpoint/model 4. Utility fallback chain - 5. Default fallback chain + 5. Retired default-fallback compatibility hook (currently empty) """ candidates = [] diff --git a/static/index.html b/static/index.html index 37aaf6dc7..8dc076173 100644 --- a/static/index.html +++ b/static/index.html @@ -1504,7 +1504,7 @@ -
+
diff --git a/static/js/chat.js b/static/js/chat.js index ccf3c83a1..96147459d 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -29,9 +29,16 @@ import { createThinkingAnalysisGate, stripLiveThinkingTags, } from './liveThinkingThrottle.js'; +import { + applyModelMetricsState, + applyModelRouteEventState, + inheritModelRouteState, +} from './chatModelProvenance.js'; +import { createTerminalStreamError, isRecoverableStreamError } from './chatStreamErrors.js'; const RESEARCH_TIMEOUT_MS = 360000; const DEFAULT_TIMEOUT_MS = 120000; + const RUN_ID_ABORT_GRACE_MS = 2000; // timeout waits this long for a run-id header before hard-aborting const RESEARCH_SVG = ''; let API_BASE = ''; @@ -394,13 +401,27 @@ import { const tsSpan = roleEl.querySelector('.role-timestamp'); const req = requestedModel || actualModel || ''; const actual = actualModel || requestedModel || ''; - let label = _modelRouteLabel(req, actual); + let label = _modelRouteLabel( + req, + actual, + opts.requestedEndpointLabel, + opts.actualEndpointLabel, + opts.requestedEndpointId, + opts.actualEndpointId, + ); if (opts.suffix) label += ' (' + opts.suffix + ')'; if (opts.characterName) label = opts.characterName; roleEl.textContent = label + ' '; _applyModelColor(roleEl, actual || req); - if (req && actual && !_sameModelName(req, actual)) { - roleEl.title = req + ' -> ' + actual + (opts.reason ? ': ' + opts.reason : ''); + const endpointChanged = Boolean( + opts.requestedEndpointId + && opts.actualEndpointId + && opts.requestedEndpointId !== opts.actualEndpointId + ); + if (req && actual && (!_sameModelName(req, actual) || endpointChanged)) { + roleEl.title = req + ' -> ' + actual + + (endpointChanged ? ' (' + opts.requestedEndpointLabel + ' -> ' + opts.actualEndpointLabel + ')' : '') + + (opts.reason ? ': ' + opts.reason : ''); } else if (!opts.reason) { roleEl.removeAttribute('title'); } @@ -570,6 +591,11 @@ import { const _backgroundStreams = new Map(); // sessionId -> { status, accumulated, sourcesHtml, abortCtrl, query, metrics } const _activeStreams = new Map(); // sessionId -> { abortCtrl, holder, query, startedAt, cancelViewWork, finalizeView } const _resumingStreams = new Set(); // sessionId -> a resumeStream() reader is live (re-attach lock) + const _terminalSavedStreams = new Set(); // sessionId -> canonical terminal event seen by active reader + const _streamRunIds = new Map(); // sessionId -> opaque identity of the current send's detached run + const _streamGenerations = new Map(); // sessionId -> generation of the current (latest) send + const _sendStates = new Map(); // sessionId -> { generation, abortCtrl } of the current send, installed synchronously at send commit so Stop never has to borrow an older send's controller + const _pendingRunStops = new Map(); // 'sessionId:generation' -> abortCtrl|null; Stop queued for that send while it awaits headers. Keyed per send so concurrent sends' cancellation intents never displace each other. let _streamSessionId = null; // Session ID for the currently active reader loop let _lastReaderActivity = 0; // Timestamp of last reader.read() success — used to detect frozen streams let _webLockRelease = null; // Function to release the Web Lock held during streaming @@ -608,6 +634,60 @@ import { return now; } + /** Stable cost identity for one logical metrics segment within a run. */ + function _metricsCostRecordId(runId, event) { + if (!runId) return ''; + return `${runId}:${event && event.teacher ? 'teacher' : 'primary'}`; + } + + /** POST the exact Stop for one observed run identity. */ + function _postExactStop(sessionId, runId) { + fetch(`/api/chat/stop/${encodeURIComponent(sessionId)}`, { + method: 'POST', + credentials: 'same-origin', + headers: { 'X-Odysseus-Run-Id': runId }, + }).catch(() => {}); + } + + /** Stop only the exact detached run whose identity this browser observed. */ + function _stopExactRun(sessionId, abortCtrl = null) { + if (!sessionId) return false; + const runId = _streamRunIds.get(sessionId); + if (!runId) { + // Queue against the CURRENT send's generation: its POST is the only + // identity channel that can name the run, so the Stop fires from that + // send's own header arrival even if a replacement starts meanwhile. + const generation = _streamGenerations.get(sessionId) || 0; + const pendingKey = sessionId + ':' + generation; + if (abortCtrl || !_pendingRunStops.has(pendingKey)) { + _pendingRunStops.set(pendingKey, abortCtrl); + } + return false; + } + _postExactStop(sessionId, runId); + return true; + } + + function _rememberStreamRunId(sessionId, runId, generation) { + if (!sessionId || !runId) return; + // A superseded send must not record its run id as the session's current + // identity, but it must still flush its own queued Stop: this is the only + // channel that can cancel that run when the replacement dies before its + // own POST reaches the server. + if (_streamGenerations.get(sessionId) === generation) { + _streamRunIds.set(sessionId, runId); + } + const pendingKey = sessionId + ':' + generation; + if (!_pendingRunStops.has(pendingKey)) return; + const pendingAbort = _pendingRunStops.get(pendingKey); + _pendingRunStops.delete(pendingKey); + _postExactStop(sessionId, runId); + if (pendingAbort && !pendingAbort.signal.aborted) { + pendingAbort._reason = 'user-stop'; + pendingAbort.abort(); + } + } + // Sources box builder and toggleSources are now in chatRenderer.js var _buildSourcesBox = chatRenderer.buildSourcesBox; @@ -1342,6 +1422,26 @@ import { if (messageInput) messageInput.disabled = false; updateSubmitButton('streaming', submitBtn); if (submitBtn) submitBtn.classList.remove('send-pending'); + // Per-send generation, reserved SYNCHRONOUSLY before the send gate clears + // and before the first await: from this instant the superseded send may + // not clean session state, register, or POST (each checked at its own + // await boundaries). Session-keyed state (run id, queued Stop, cleanup + // rights) belongs to the latest generation only. A queued Stop from the + // superseded send is deliberately left in place, tagged with ITS + // generation: that send's still-alive POST is the only identity channel + // able to name its run, so the Stop fires from its own header arrival + // (see _rememberStreamRunId) even if this replacement dies before fetch. + const streamSessionId = sessionModule.getCurrentSessionId(); + const streamGeneration = (_streamGenerations.get(streamSessionId) || 0) + 1; + _streamGenerations.set(streamSessionId, streamGeneration); + const _sendState = { generation: streamGeneration, abortCtrl: null }; + _sendStates.set(streamSessionId, _sendState); + // The previous send's run identity dies with its ownership: a Stop after + // this instant must queue for THIS send, not fire against the old run. + // (The old send's own queued Stop still works — its flush carries the run + // id from its header, and its stale generation cannot repopulate this map.) + _streamRunIds.delete(streamSessionId); + _streamSessionId = streamSessionId; _sendInFlight = false; try { @@ -1350,10 +1450,12 @@ import { await pendingSwitch; } } catch (_) {} + // Superseded while awaiting the model switch: the replacement owns the + // session now, and everything below (state resets, registration, POST) + // is its business alone. + if (_streamGenerations.get(streamSessionId) !== streamGeneration) return; - // Capture session ID for background stream detection - const streamSessionId = sessionModule.getCurrentSessionId(); - _streamSessionId = streamSessionId; + _terminalSavedStreams.delete(streamSessionId); const streamQuery = msg; _touchStreamActivity(streamSessionId); @@ -1373,6 +1475,7 @@ import { let _thinkOpen = false; let holder = null; let finalMeta = null; + let _canonicalTerminalSaved = false; let spinner = null; let timedOut = false; let processingProbeTimer = null; @@ -1742,8 +1845,26 @@ import { } + // Superseded during preflight (uploads, document saves): a newer send + // owns the session. Bailing here — before registration and before the + // POST — keeps this stale send from overwriting the replacement's + // stream entry or reaching the server last, where agent_runs.start + // would cancel the newer run in favor of this old one. + if (_streamGenerations.get(streamSessionId) !== streamGeneration) { + // The optimistic user bubble is already in the DOM looking sent, but + // this message never reaches the server. Say so instead of leaving a + // ghost that vanishes on refresh. + if (_userMsgEl && _userMsgEl.parentNode) { + const _notSentNote = document.createElement('div'); + _notSentNote.style.cssText = 'color: var(--color-error); font-style: italic; font-size: 0.85em; padding: 2px 0;'; + _notSentNote.textContent = '[Not sent — superseded by a newer message]'; + _userMsgEl.appendChild(_notSentNote); + } + return; + } abortCtrl = new AbortController(); abortCtrl._reason = ''; + _sendState.abortCtrl = abortCtrl; currentAbort = abortCtrl; const _tState = Storage.loadToggleState(); @@ -1755,15 +1876,28 @@ import { if (!abortCtrl.signal.aborted) { timedOut = true; abortCtrl._reason = 'timeout'; + if (_streamGenerations.get(streamSessionId) !== streamGeneration) { + // Superseded send: the session's run id and Stop queue belong to + // the replacement now. Just kill this hung POST. + abortCtrl.abort(); + return; + } + let abortNow = true; try { - if (streamSessionId) { - fetch(`/api/chat/stop/${encodeURIComponent(streamSessionId)}`, { - method: 'POST', - credentials: 'same-origin', - }).catch(() => {}); - } + abortNow = _streamRunIds.has(streamSessionId) + ? _stopExactRun(streamSessionId) + : _stopExactRun(streamSessionId, abortCtrl); } catch (_) {} - abortCtrl.abort(); + if (abortNow) { + abortCtrl.abort(); + } else { + // The Stop is queued on the run-id header, but a request this + // stalled may never send one. Hard-abort after a short grace so + // the timeout still guarantees cancellation. + setTimeout(() => { + if (!abortCtrl.signal.aborted) abortCtrl.abort(); + }, RUN_ID_ABORT_GRACE_MS); + } } }, timeoutMs); clearResponseTimeout = () => { @@ -1912,6 +2046,8 @@ import { enableResearchBtn(); return; } + const streamRunId = res.headers.get('X-Odysseus-Run-Id') || ''; + if (streamRunId) _rememberStreamRunId(streamSessionId, streamRunId, streamGeneration); // Mark the chat log busy while streaming so screen readers wait for the // settled response instead of announcing every token. Cleared in finally. @@ -1986,9 +2122,17 @@ import { const newRole = document.createElement('div'); newRole.className = 'role'; const metaS = sessionModule.getSessions().find(s => s.id === streamSessionId); - const requested = holder?._requestedModel || metaS?.model || modelName; - const actual = holder?._actualModel || requested; - newRole.textContent = _modelRouteLabel(requested, actual) || ''; + inheritModelRouteState(holder, roundHolder, newWrap, metaS?.model || modelName); + const requested = newWrap._requestedModel; + const actual = newWrap._actualModel; + newRole.textContent = _modelRouteLabel( + requested, + actual, + newWrap._requestedEndpointLabel, + newWrap._actualEndpointLabel, + newWrap._requestedEndpointId, + newWrap._actualEndpointId, + ) || ''; _applyModelColor(newRole, actual); newWrap.appendChild(newRole); const newBody = document.createElement('div'); @@ -2514,6 +2658,7 @@ import { let _nextIsError = false; let _streamSawDone = false; + let _streamTerminalError = null; let _firstVisibleOutputSeen = false; const markFirstVisibleOutput = () => { if (_firstVisibleOutputSeen) return; @@ -2638,10 +2783,9 @@ import { // Handle SSE error events (e.g. HTTP 404 from provider) if (_nextIsError || json.status >= 400) { _nextIsError = false; - const errMsg = json.text || json.error?.message || `Error ${json.status || 'unknown'}`; - console.error('Stream error:', errMsg); + _streamTerminalError = createTerminalStreamError(json); + console.error('Stream error:', _streamTerminalError.message); if (spinner && spinner.element) spinner.destroy(); - typewriterInto(roundHolder.querySelector('.body'), errMsg); break; } if (json.delta || json.type === 'agent_prep' || json.type === 'generated_image' || json.type === 'tool_start' || json.type === 'tool_output' || json.type === 'tool_progress' || json.type === 'agent_step' || json.type === 'loop_breaker_triggered' || json.type === 'intent_nudge_exhausted' || json.type === 'doc_stream_open' || json.type === 'doc_stream_delta' || json.type === 'research_progress') { @@ -3040,18 +3184,6 @@ import { 6000 ); continue; - } else if (json.type === 'model_fallback') { - // Model went offline — switched to fallback - var _fbData = json.data || {}; - uiModule.showToast( - `Model ${_fbData.old_model || '?'} offline — switched to ${_fbData.new_model || '?'}`, - 5000 - ); - // Update the model picker to reflect the new model - if (sessionModule && sessionModule.updateModelPicker) { - sessionModule.updateModelPicker(); - } - continue; } else if (json.type === 'model_info') { // Update role label with model name as soon as we know it if (!_isBg && holder) { @@ -3059,6 +3191,10 @@ import { if (roleEl) { holder._requestedModel = json.requested_model || json.model || holder._requestedModel; holder._actualModel = json.model || holder._actualModel || holder._requestedModel; + holder._requestedEndpointId = json.requested_endpoint_id || json.endpoint_id || holder._requestedEndpointId || null; + holder._requestedEndpointLabel = json.requested_endpoint_label || json.endpoint_label || holder._requestedEndpointLabel || 'Selected route'; + holder._actualEndpointId = json.endpoint_id || holder._actualEndpointId || holder._requestedEndpointId; + holder._actualEndpointLabel = json.endpoint_label || holder._actualEndpointLabel || holder._requestedEndpointLabel; if (json.suffix) holder._roleSuffix = json.suffix; // Prepend character name if sent by server or set locally var _charName = json.character_name || (presetsModule.getCharacterName ? presetsModule.getCharacterName() : ''); @@ -3066,6 +3202,10 @@ import { _setRoleModelLabel(roleEl, holder._requestedModel, holder._actualModel, { suffix: holder._roleSuffix, characterName: holder._characterName, + requestedEndpointId: holder._requestedEndpointId, + requestedEndpointLabel: holder._requestedEndpointLabel, + actualEndpointId: holder._actualEndpointId, + actualEndpointLabel: holder._actualEndpointLabel, }); } } @@ -3076,9 +3216,10 @@ import { if (!_isBg) { var _selM = _shortModel(json.selected_model || ''); var _ansM = _shortModel(json.answered_by || ''); - uiModule.showToast('⚠ ' + _selM + ' failed — answered by ' + _ansM, 6000); - if (holder) { - var _rEl = holder.querySelector('.role'); + uiModule.showToast('Fallback: ' + _selM + ' failed — answered by ' + _ansM, 6000); + var _fallbackHolder = applyModelRouteEventState(json, holder, roundHolder, modelName); + if (_fallbackHolder) { + var _rEl = _fallbackHolder.querySelector('.role'); if (_rEl) { var _tsS = _rEl.querySelector('.role-timestamp'); _rEl.textContent = _ansM + ' (fallback) '; @@ -3086,13 +3227,14 @@ import { (json.reason ? ': ' + json.reason : '') + ' — answered by ' + (json.answered_by || ''); _applyModelColor(_rEl, json.answered_by); if (_tsS) _rEl.appendChild(_tsS); - holder._requestedModel = json.selected_model || holder._requestedModel || modelName; - const _hasResolvedActual = holder._actualModel && !_sameModelName(holder._actualModel, holder._requestedModel); - holder._actualModel = _hasResolvedActual ? holder._actualModel : (json.answered_by || holder._actualModel || holder._requestedModel); - _setRoleModelLabel(_rEl, holder._requestedModel, holder._actualModel, { - suffix: holder._roleSuffix, - characterName: holder._characterName, + _setRoleModelLabel(_rEl, _fallbackHolder._requestedModel, _fallbackHolder._actualModel, { + suffix: _fallbackHolder._roleSuffix, + characterName: _fallbackHolder._characterName, reason: json.reason, + requestedEndpointId: _fallbackHolder._requestedEndpointId, + requestedEndpointLabel: _fallbackHolder._requestedEndpointLabel, + actualEndpointId: _fallbackHolder._actualEndpointId, + actualEndpointLabel: _fallbackHolder._actualEndpointLabel, }); } } @@ -3136,12 +3278,15 @@ import { try { note.scrollIntoView({ block: 'end', behavior: 'smooth' }); } catch (_) { uiModule.scrollHistory && uiModule.scrollHistory(); } } } else if (json.type === 'model_actual') { - if (!_isBg && holder) { - holder._requestedModel = json.requested_model || holder._requestedModel || modelName; - holder._actualModel = json.model || holder._actualModel || holder._requestedModel; - _setRoleModelLabel(holder.querySelector('.role'), holder._requestedModel, holder._actualModel, { - suffix: holder._roleSuffix, - characterName: holder._characterName, + if (!_isBg) { + var _modelHolder = applyModelRouteEventState(json, holder, roundHolder, modelName); + if (_modelHolder) _setRoleModelLabel(_modelHolder.querySelector('.role'), _modelHolder._requestedModel, _modelHolder._actualModel, { + suffix: _modelHolder._roleSuffix, + characterName: _modelHolder._characterName, + requestedEndpointId: _modelHolder._requestedEndpointId, + requestedEndpointLabel: _modelHolder._requestedEndpointLabel, + actualEndpointId: _modelHolder._actualEndpointId, + actualEndpointLabel: _modelHolder._actualEndpointLabel, }); } } else if (json.type === 'attachments') { @@ -3227,15 +3372,60 @@ import { const detail = before && after && before > after ? ` (${after}/${before} messages sent)` : ''; uiModule.showToast(`Context trimmed for this model${detail}`); } + } else if (json.type === 'agent_terminal' || json.type === 'chat_terminal') { + // The backend persisted canonical partial output, sanitized + // failure metadata, and actual-route provenance before this + // event. The terminal catch below reloads that exact record. + _canonicalTerminalSaved = true; + _terminalSavedStreams.add(streamSessionId); + const priorMetrics = metrics; + metrics = json.data || metrics; + if (metrics && streamRunId) { + metrics._costRecordId = _metricsCostRecordId(streamRunId, json); + } + // Direct Chat may have emitted provider usage before its + // terminal event. Carry that already-recorded state onto the + // canonical terminal metadata instead of billing it twice. + if (priorMetrics && priorMetrics._costRecorded && metrics) { + metrics._costRecorded = true; + } + if (_isBg) { + var bgTerminal = _backgroundStreams.get(streamSessionId); + if (bgTerminal) { + if ( + bgTerminal.metrics + && bgTerminal.metrics._costRecorded + && metrics + ) { + metrics._costRecorded = true; + } + bgTerminal.metrics = metrics; + bgTerminal.status = 'completed'; + if (metrics) { + chatRenderer.recordSessionMetricsCost(metrics, streamSessionId); + } + } + continue; + } + if (holder && metrics) { + applyModelMetricsState(metrics, holder, roundHolder, modelName); + const terminalMetricsTarget = _metricsTargetForTurn(); + if (terminalMetricsTarget) displayMetrics(terminalMetricsTarget, metrics); + } } else if (json.type === 'metrics') { metrics = json.data; + if (metrics && streamRunId) { + metrics._costRecordId = _metricsCostRecordId(streamRunId, json); + } if (!_isBg && holder && metrics) { - holder._requestedModel = metrics.requested_model || holder._requestedModel || modelName; - holder._actualModel = metrics.model || holder._actualModel || holder._requestedModel; + applyModelMetricsState(metrics, holder, roundHolder, modelName); } if (_isBg) { var bgM = _backgroundStreams.get(streamSessionId); - if (bgM) bgM.metrics = json.data; + if (bgM) { + bgM.metrics = json.data; + chatRenderer.recordSessionMetricsCost(bgM.metrics, streamSessionId); + } continue; } if (metrics) { @@ -3616,9 +3806,17 @@ import { const newRole = document.createElement('div'); newRole.className = 'role'; const metaS = sessionModule.getSessions().find(s => s.id === streamSessionId); - const _roundRequested = holder?._requestedModel || metaS?.model; - const _roundActual = holder?._actualModel || _roundRequested; - newRole.textContent = _modelRouteLabel(_roundRequested, _roundActual) || ''; + inheritModelRouteState(holder, roundHolder, newWrap, metaS?.model || modelName); + const _roundRequested = newWrap._requestedModel; + const _roundActual = newWrap._actualModel; + newRole.textContent = _modelRouteLabel( + _roundRequested, + _roundActual, + newWrap._requestedEndpointLabel, + newWrap._actualEndpointLabel, + newWrap._requestedEndpointId, + newWrap._actualEndpointId, + ) || ''; _applyModelColor(newRole, _roundActual); newWrap.appendChild(newRole); const newBody = document.createElement('div'); @@ -3725,8 +3923,21 @@ import { } } + if (_streamTerminalError) { + throw _streamTerminalError; + } if (!_streamSawDone) { - throw new Error('Stream closed before completion'); + if (!_canonicalTerminalSaved) { + throw new Error('Stream closed before completion'); + } + // The backend persisted a canonical terminal record (partial output + + // failure metadata) before the connection died. Route through the + // terminal-error path so that record is reloaded; falling through to + // the success renderer would present the partial output as a clean + // completion. + throw createTerminalStreamError({ + text: 'Stream closed after canonical terminal event', + }); } // The final foreground render below is authoritative. Cancel any delayed @@ -3746,15 +3957,25 @@ import { const _isBgFinal = (sessionModule.getCurrentSessionId() !== streamSessionId) || _backgroundStreams.has(streamSessionId); if (!_isBgFinal) { finalMeta = sessionModule.getSessions().find(s => s.id === sessionModule.getCurrentSessionId()); - const _finalActualModel = metrics?.model || holder._actualModel || finalMeta?.model; - const _finalRequestedModel = metrics?.requested_model || holder._requestedModel || finalMeta?.model || _finalActualModel; + const _finalModelHolder = applyModelMetricsState( + metrics, + holder, + roundHolder, + finalMeta?.model || modelName, + ) || holder; + const _finalActualModel = _finalModelHolder._actualModel || finalMeta?.model; + const _finalRequestedModel = _finalModelHolder._requestedModel || finalMeta?.model || _finalActualModel; // Prepend character name if set var _charNameFinal = presetsModule.getCharacterName ? presetsModule.getCharacterName() : ''; - const roleEl = holder.querySelector('.role'); + const roleEl = _finalModelHolder.querySelector('.role'); if (roleEl) { _setRoleModelLabel(roleEl, _finalRequestedModel, _finalActualModel, { - suffix: holder._roleSuffix, - characterName: _charNameFinal || holder._characterName, + suffix: _finalModelHolder._roleSuffix, + characterName: _charNameFinal || _finalModelHolder._characterName, + requestedEndpointId: _finalModelHolder._requestedEndpointId, + requestedEndpointLabel: _finalModelHolder._requestedEndpointLabel, + actualEndpointId: _finalModelHolder._actualEndpointId, + actualEndpointLabel: _finalModelHolder._actualEndpointLabel, }); } holder.dataset.raw = accumulated; @@ -4013,6 +4234,21 @@ import { } // end if (!_isBgFinal) } catch (err) { + // If a Stop or timeout was waiting for an identity header and the POST + // failed before producing one, keep this on the cancellation path. There + // is no safe headerless server cancel to send, but it must not be turned + // into an automatic recovery attempt either. Only this send's own + // queued Stop counts; a replacement's queued Stop is not ours to spend. + const _pendingCatchKey = streamSessionId + ':' + streamGeneration; + if ( + _pendingRunStops.has(_pendingCatchKey) + && abortCtrl + && !abortCtrl.signal.aborted + ) { + _pendingRunStops.delete(_pendingCatchKey); + abortCtrl._reason = 'user-stop'; + abortCtrl.abort(); + } // Check if this stream was running in background — needed before any // stop-state write, so an errored background stream can't clobber the // foreground session's text. @@ -4021,6 +4257,18 @@ import { _closeOpenThinkingMarkup(_isBgCatch); if (_isBgCatch) { _cancelLiveThinkingWork(); + + // A canonical terminal event may have been persisted immediately + // before the stream moved into the background. Preserve that terminal + // state instead of allowing the catch path to turn it back into a + // running/error stream. + const bgTerminal = _backgroundStreams.get(streamSessionId); + if (bgTerminal && _terminalSavedStreams.has(streamSessionId)) { + bgTerminal.status = 'completed'; + if (sessionModule && sessionModule.clearStreaming) { + sessionModule.clearStreaming(streamSessionId); + } + } } else if (accumulated) { _catchTerminalView = _finalizeInterruptedView(); } else { @@ -4039,7 +4287,10 @@ import { // Error happened while backgrounded — update map, don't touch DOM console.error('Background stream error:', err); var bgErr = _backgroundStreams.get(streamSessionId); - if (bgErr && bgErr.status === 'completed') { + if (bgErr && ( + bgErr.status === 'completed' || _terminalSavedStreams.has(streamSessionId) + )) { + bgErr.status = 'completed'; // [DONE] was already processed — this error is benign (e.g. reader.read() after close) // Don't override the completed status; just ensure the completed dot stays if (sessionModule && sessionModule.clearStreaming) { @@ -4191,8 +4442,36 @@ import { // cap. Only auto-recover from connection-class failures; deterministic // errors (unsupported tools, 4xx/5xx, parse failures) surface right away // instead of burning the nudge budget on a guaranteed-to-fail retry. - if (!(_isRecoverableStreamErr(err) && _tryAutoRecover(_catchViewHolder, accumulated, streamSessionId))) { - const errorHolder = _catchViewHolder?.querySelector('.body') || document.querySelector('.msg-ai:last-of-type .body'); + if (!(isRecoverableStreamError(err) && _tryAutoRecover(_catchViewHolder, accumulated, streamSessionId))) { + if (err.terminalStreamError) { + if (_canonicalTerminalSaved || accumulated.trim()) { + // Let this stream's finally block clear foreground state before + // reselecting; otherwise selectSession would detach the already + // terminal reader and leave a stale background-stream marker. + setTimeout(async () => { + if (sessionModule.getCurrentSessionId() === streamSessionId) { + await sessionModule.selectSession(streamSessionId, { showLoading: false }); + } else { + await sessionModule.loadSessions(); + } + }, 0); + } else { + const terminalBody = + _catchViewHolder?.querySelector('.body') + || roundHolder?.querySelector('.body') + || document.querySelector('.msg-ai:last-of-type .body'); + if (terminalBody) { + const terminalNote = document.createElement('div'); + terminalNote.style.cssText = 'color: var(--color-error); font-style: italic; padding: 4px 0;'; + terminalNote.textContent = `[Error: ${err.message}]`; + terminalBody.appendChild(terminalNote); + } + } + return; + } + const errorHolder = + _catchViewHolder?.querySelector('.body') + || document.querySelector('.msg-ai:last-of-type .body'); if (errorHolder) { let errMsg = `Error: ${err.message}`; // Add hint for tool-call errors @@ -4209,23 +4488,52 @@ import { clearResponseTimeout(); clearProcessingProbe(); clearFirstTokenWaitTimers(); - _activeStreams.delete(streamSessionId); - if (_streamSessionId === streamSessionId) _streamSessionId = null; - _syncForegroundStreamGlobals(); + // A replacement send bumps the session's generation the moment it + // starts, before it registers or reaches the server, so cleanup rights + // are decided by generation: a superseded send may remove only what it + // itself owns (its stream registration by controller identity, its own + // generation's queued Stop) and must leave session-level state — the + // reader session id, research marker, UI — to the replacement. + const _ownsStreamState = + _streamGenerations.get(streamSessionId) === streamGeneration; + const _finallyRegistered = _activeStreams.get(streamSessionId); + if (!_finallyRegistered || _finallyRegistered.abortCtrl === abortCtrl) { + _activeStreams.delete(streamSessionId); + } + _pendingRunStops.delete(streamSessionId + ':' + streamGeneration); + if (_ownsStreamState) { + if (_streamSessionId === streamSessionId) _streamSessionId = null; + if (_sendStates.get(streamSessionId) === _sendState) { + _sendStates.delete(streamSessionId); + } + // Superseded sends must not resync: with the replacement not yet + // registered, a stale sync would set isStreaming false and drop + // currentAbort while _sendInFlight is already false, reopening the + // send gate mid-preflight. The replacement syncs when it registers + // or finishes. + _syncForegroundStreamGlobals(); + } // Streaming done — let screen readers announce the settled response. - const _chatLogDone = document.getElementById('chat-history'); - if (_chatLogDone) _chatLogDone.setAttribute('aria-busy', 'false'); - // Always clean up research tracking regardless of background state - _researchingStreamIds.delete(streamSessionId); + if (_ownsStreamState) { + const _chatLogDone = document.getElementById('chat-history'); + if (_chatLogDone) _chatLogDone.setAttribute('aria-busy', 'false'); + } + // Research markers gate /api/research/cancel in the Stop handler, so a + // superseded send must not strip a replacement research run's marker. + if (_ownsStreamState) _researchingStreamIds.delete(streamSessionId); if (_researchingStreamIds.size === 0) { var _rToggleCleanup = document.getElementById('research-toggle-btn'); if (_rToggleCleanup) _rToggleCleanup.classList.remove('research-running'); } - // Only reset UI state if still on the stream's session and was never backgrounded + // Only reset UI state if still on the stream's session, never + // backgrounded, and no replacement stream owns the session now — the + // replacement disabled the composer for its own send, so re-enabling + // it here would hand input back mid-stream. const _isBgFinally = (sessionModule.getCurrentSessionId() !== streamSessionId) || _backgroundStreams.has(streamSessionId); + if (_ownsStreamState) _terminalSavedStreams.delete(streamSessionId); - if (!_isBgFinally) { + if (!_isBgFinally && _ownsStreamState) { // Reset button to idle state updateSubmitButton('idle', submitBtn); @@ -4320,69 +4628,64 @@ import { // the server run — otherwise closing the tab would kill the background task, // defeating the whole point. Only the Stop button cancels the server run. export function abortCurrentRequest(stopServer = false) { + const _sid = (sessionModule && sessionModule.getCurrentSessionId && sessionModule.getCurrentSessionId()) + || _streamSessionId + || (window.sessionModule && window.sessionModule.getCurrentSessionId && window.sessionModule.getCurrentSessionId()); + // The CURRENT send's controller comes from its send state, installed at + // send commit — never borrowed from the stream registry, which during the + // replacement's preflight still holds the superseded send's entry. + // Aborting that older controller here would sever the only identity + // channel able to name the old run. A send committed but pre-POST has a + // null controller: the Stop queues and there is nothing to abort yet. + const _sendStateNow = _sid ? _sendStates.get(_sid) : null; const active = _getForegroundStreamState(); - const abortCtrl = active ? active.abortCtrl : currentAbort; - if (abortCtrl) { - abortCtrl.abort(); - // Don't set to null here - let catch block handle it - } + const abortCtrl = _sendStateNow + ? _sendStateNow.abortCtrl + : (active ? active.abortCtrl : currentAbort); + let abortNow = true; if (stopServer) { try { - const _sid = (sessionModule && sessionModule.getCurrentSessionId && sessionModule.getCurrentSessionId()) - || _streamSessionId - || (window.sessionModule && window.sessionModule.getCurrentSessionId && window.sessionModule.getCurrentSessionId()); if (_sid) { - fetch(`/api/chat/stop/${encodeURIComponent(_sid)}`, { method: 'POST', credentials: 'same-origin' }).catch(() => {}); + // Before response headers arrive there is no safe server-side stop + // identity yet. Keep the POST alive just long enough to receive that + // opaque id, then _rememberStreamRunId sends the exact Stop and aborts + // this reader. Never fall back to a headerless session-wide cancel. + abortNow = _stopExactRun(_sid, abortCtrl); } } catch (_) {} } + if (abortCtrl && abortNow) { + abortCtrl.abort(); + // Don't set to null here - let catch block handle it + } } // ── Stall watchdog ────────────────────────────────────────────── - // Auto-recover a turn whose stream died (connection drop) or went silent: - // preserve the partial, then re-submit a completion handshake by reusing the - // existing continue/resume path. Returns false at the cap so the caller can - // surface the failure instead of nudging forever. + // Auto-recover a turn whose browser stream died by reconnecting to the exact + // detached server run. Returns false at the cap so the caller can surface + // the failure instead of retrying forever. // Only auto-recover from connection-class failures (the genuine "silently // died" case). Deterministic errors — unsupported tools, HTTP 4xx/5xx, JSON // parse failures — will fail identically on retry, so surfacing them // immediately is both more honest and avoids wasting the nudge budget. - function _isRecoverableStreamErr(err) { - if (!err) return false; - if (err.name === 'TypeError') return true; // fetch/reader network failure - const m = (err.message || '').toLowerCase(); - if (/\btool\b|unsupported|json|parse|\b4\d\d\b|\b5\d\d\b/.test(m)) return false; - return /network|fetch|connection|reset|closed|aborted|stream|tim(?:e|ed)\s?out|econn|eof/.test(m); - } - function _tryAutoRecover(holder, accumulated, sessionId) { if (_autoNudges >= _AUTO_NUDGE_CAP) return false; _autoNudges++; if (holder && accumulated) { holder.dataset.raw = accumulated; } - _pendingContinue = holder || null; // merge the continuation into the same bubble - _hideUserBubble = true; // no user bubble for the handshake - _autoContinuePending = true; // don't reset the counter on this submit - const _abandon = () => { // clear the pending flags so they can't - _pendingContinue = null; // leak into whatever chat is now open - _hideUserBubble = false; - _autoContinuePending = false; - }; - // Defer so the stream's finally resets state first — otherwise the send - // button is still in "stop" mode and clicking it would toggle, not send. - setTimeout(() => { + // The server run is detached and keeps its exact pinned model/tool state. + // Reconnect to that run instead of submitting a new user turn, which would + // cancel it, retry the selected model, and risk duplicating side effects. + setTimeout(async () => { // The stream that died may not be the chat the user is now looking at — - // never inject the recovery handshake into the wrong conversation. - if (sessionId && sessionModule.getCurrentSessionId() !== sessionId) { _abandon(); return; } - const msgInput = uiModule.el('message'); - const sb = document.querySelector('.send-btn'); - if (!msgInput || !sb) { _abandon(); return; } - const tail = (accumulated || '').slice(-400); - msgInput.value = tail - ? `The stream dropped before you finished. It ended with:\n\n${tail}\n\nIf the task is fully complete, reply with just: DONE. Otherwise continue exactly where you left off and finish it — do not repeat what you already wrote.` - : `The stream dropped before you produced anything. If the task is already done, reply with just: DONE. Otherwise complete it now.`; - sb.click(); + // never attach the recovery reader to the wrong conversation. + if (sessionId && sessionModule.getCurrentSessionId() !== sessionId) return; + const resumed = await resumeStream(sessionId, holder || null); + if (!resumed && holder && holder.isConnected) { + const body = holder.querySelector('.body'); + if (body) typewriterInto(body, 'Connection lost. The existing run could not be resumed.'); + } }, 200); return true; } @@ -4545,9 +4848,13 @@ import { // view must stop all delayed rendering immediately. The reader loop may not // receive another SSE line for an arbitrary amount of time. if (active.cancelViewWork) active.cancelViewWork(); - // Store background stream state + + const terminalSaved = _terminalSavedStreams.has(sessionId); + // Store background stream state. A canonical terminal event can precede + // its SSE error event; preserve completion if the user switches sessions + // during that gap instead of creating a fresh running/error marker. _backgroundStreams.set(sessionId, { - status: 'running', + status: terminalSaved ? 'completed' : 'running', accumulated: currentAccumulated, sourcesHtml: '', findingsData: null, @@ -4556,8 +4863,10 @@ import { metrics: null, }); // Mark session with pulsing dot in sidebar - if (sessionModule && sessionModule.markStreaming) { + if (!terminalSaved && sessionModule && sessionModule.markStreaming) { sessionModule.markStreaming(sessionId); + } else if (terminalSaved && sessionModule && sessionModule.clearStreaming) { + sessionModule.clearStreaming(sessionId); } // Clear local state WITHOUT aborting the fetch if (currentAbort === active.abortCtrl) currentAbort = null; @@ -4584,7 +4893,7 @@ import { * reloaded from the DB so its full render stays faithful. Returns true if it * attached, false to let the caller fall back to spinner+poll. */ - export async function resumeStream(sessionId) { + export async function resumeStream(sessionId, replaceHolder = null) { if (!sessionId) return false; if (hasActiveStream(sessionId)) return false; @@ -4595,9 +4904,12 @@ import { return false; } if (!res.ok || !res.body) return false; + const resumeRunId = res.headers.get('X-Odysseus-Run-Id') || ''; + if (resumeRunId) _streamRunIds.set(sessionId, resumeRunId); const box = document.getElementById('chat-history'); if (!box) return false; + if (replaceHolder && replaceHolder.parentNode) replaceHolder.remove(); // Block duplicate re-attach attempts while this reader is live. A dedicated // set (not _backgroundStreams) so checkBackgroundStream doesn't mistake this @@ -4612,6 +4924,8 @@ import { holder.innerHTML = '
' + uiModule.esc(roleLabel) + ' ' + roleTs + '
' + '
'; + holder._requestedModel = meta && meta.model; + holder._actualModel = holder._requestedModel; _applyModelColor(holder.querySelector('.role'), meta && meta.model); const contentDiv = holder.querySelector('.stream-content'); box.appendChild(holder); @@ -4629,6 +4943,8 @@ import { let gotDelta = false; let leftSession = false; let metricsData = null; + let replayError = null; + let canonicalTerminalSeen = false; // "Rich" responses (tool calls, sources, doc streaming, multi-round) need the // full canonical render, which is rebuilt from the saved DB record on reload. // Plain text replies can be finalized in place without a reload. @@ -4665,6 +4981,8 @@ import { const parts = buffer.split('\n\n'); buffer = parts.pop(); for (const part of parts) { + const eventIsError = part.split('\n').some(l => l.trim() === 'event: error'); + if (eventIsError) rich = true; const line = part.split('\n').find(l => l.startsWith('data: ')); if (!line) continue; const payload = line.slice(6); @@ -4674,7 +4992,9 @@ import { } let json; try { json = JSON.parse(payload); } catch (_) { continue; } - if (json.delta) { + if (eventIsError) { + replayError = createTerminalStreamError(json); + } else if (json.delta) { roundText += json.delta; if (!docFenceOpened && (roundText.includes('```create_document\n') || roundText.includes('```document\n') || roundText.includes('```documen\n'))) { docFenceOpened = true; @@ -4690,6 +5010,64 @@ import { if (documentModule) documentModule.streamDocDelta(json.content || json.delta || ''); } else if (json.type === 'metrics') { metricsData = json.data || metricsData; + if (metricsData && resumeRunId) { + metricsData._costRecordId = _metricsCostRecordId(resumeRunId, json); + } + if (metricsData) { + chatRenderer.recordSessionMetricsCost(metricsData, sessionId); + } + } else if (json.type === 'fallback') { + // Replay can attach after the selected route has already failed. + // Reflect the fallback immediately, then reload the canonical + // multi-round record when the detached run completes. + rich = true; + const fallbackHolder = applyModelRouteEventState(json, holder, null, meta && meta.model); + if (fallbackHolder) { + _setRoleModelLabel( + fallbackHolder.querySelector('.role'), + fallbackHolder._requestedModel, + fallbackHolder._actualModel, + { + reason: json.reason, + requestedEndpointId: fallbackHolder._requestedEndpointId, + requestedEndpointLabel: fallbackHolder._requestedEndpointLabel, + actualEndpointId: fallbackHolder._actualEndpointId, + actualEndpointLabel: fallbackHolder._actualEndpointLabel, + }, + ); + } + uiModule.showToast( + 'Fallback: ' + _shortModel(json.selected_model || '') + ' failed — answered by ' + + _shortModel(json.answered_by || ''), + 6000, + ); + } else if (json.type === 'model_actual') { + rich = true; + const modelHolder = applyModelRouteEventState(json, holder, null, meta && meta.model); + if (modelHolder) { + _setRoleModelLabel( + modelHolder.querySelector('.role'), + modelHolder._requestedModel, + modelHolder._actualModel, + { + requestedEndpointId: modelHolder._requestedEndpointId, + requestedEndpointLabel: modelHolder._requestedEndpointLabel, + actualEndpointId: modelHolder._actualEndpointId, + actualEndpointLabel: modelHolder._actualEndpointLabel, + }, + ); + } + } else if (json.type === 'agent_terminal' || json.type === 'chat_terminal') { + // The server has already persisted canonical partial content plus + // a sanitized failure note and actual route provenance. Do not + // finalize replayed deltas as a successful local-only answer. + rich = true; + canonicalTerminalSeen = true; + metricsData = json.data || metricsData; + if (metricsData && resumeRunId) { + metricsData._costRecordId = _metricsCostRecordId(resumeRunId, json); + } + if (metricsData) displayMetrics(holder, metricsData); } else if (json.type === 'tool_start' || json.type === 'tool_output' || json.type === 'tool_progress' || json.type === 'agent_step' || json.type === 'web_sources' || json.type === 'rag_sources' || @@ -4700,7 +5078,8 @@ import { } } } catch (e) { - // Network drop or parse failure: fall through to the reload below. + // Network drop or parse failure: fall through to the canonical reload. + rich = true; } cleanup(); @@ -4710,6 +5089,18 @@ import { const onThisSession = sessionModule.getCurrentSessionId && sessionModule.getCurrentSessionId() === sessionId; + // A failure before substantive output has no persisted assistant record to + // recover through a canonical reload. Keep its sanitized provider/request + // error visible in the replay holder instead of deleting the only evidence. + if (onThisSession && replayError && !canonicalTerminalSeen) { + const errorDiv = document.createElement('div'); + errorDiv.style.cssText = 'color: var(--color-error); font-style: italic; padding: 4px 0;'; + errorDiv.textContent = `[Error: ${replayError.message}]`; + contentDiv.appendChild(errorDiv); + uiModule.scrollHistory(); + return true; + } + // Plain text reply: finalize in place. Replace the live bubble with a // canonical single message (markdown + footer actions + metrics) using the // same renderer history does. No history refetch, no end-of-stream flicker. @@ -4726,6 +5117,9 @@ import { // reload from the DB for the full canonical render. if (holder._docWritingThread && holder._docWritingThread.parentNode) holder._docWritingThread.remove(); if (holder.parentNode) holder.remove(); + if (metricsData) { + chatRenderer.recordSessionMetricsCost(metricsData, sessionId); + } if (onThisSession) sessionModule.selectSession(sessionId); else sessionModule.loadSessions(); return true; diff --git a/static/js/chatModelProvenance.js b/static/js/chatModelProvenance.js new file mode 100644 index 000000000..2274537cd --- /dev/null +++ b/static/js/chatModelProvenance.js @@ -0,0 +1,104 @@ +/** Select and update the response holder for a route-provenance event. */ +export function applyModelRouteEventState(event, holder, roundHolder, defaultModel = '') { + const target = event && event.round && roundHolder ? roundHolder : holder; + if (!target) return null; + + target._requestedModel = ( + event.requested_model + || event.selected_model + || target._requestedModel + || defaultModel + ); + target._actualModel = ( + event.model + || event.answered_by + || target._actualModel + || target._requestedModel + ); + const hasEndpointRoute = Boolean( + event.requested_endpoint_id + || event.selected_endpoint_id + || event.endpoint_id + || event.answered_by_endpoint_id + || event.requested_endpoint_label + || event.selected_endpoint_label + || event.endpoint_label + || event.answered_by_endpoint_label + || target._requestedEndpointLabel + ); + if (hasEndpointRoute) { + target._requestedEndpointId = ( + event.requested_endpoint_id + || event.selected_endpoint_id + || target._requestedEndpointId + || null + ); + target._requestedEndpointLabel = ( + event.requested_endpoint_label + || event.selected_endpoint_label + || target._requestedEndpointLabel + || 'Selected route' + ); + target._actualEndpointId = ( + event.endpoint_id + || event.answered_by_endpoint_id + || target._actualEndpointId + || target._requestedEndpointId + || null + ); + target._actualEndpointLabel = ( + event.endpoint_label + || event.answered_by_endpoint_label + || target._actualEndpointLabel + || target._requestedEndpointLabel + ); + } + return target; +} + +/** Copy the active route into the bubble created for the next Agent round. */ +export function inheritModelRouteState(holder, roundHolder, target, defaultModel = '') { + if (!target) return null; + const source = roundHolder || holder; + target._requestedModel = source?._requestedModel || defaultModel; + target._actualModel = source?._actualModel || target._requestedModel; + if (source?._requestedEndpointLabel || source?._actualEndpointLabel) { + target._requestedEndpointId = source?._requestedEndpointId || null; + target._requestedEndpointLabel = source?._requestedEndpointLabel || 'Selected route'; + target._actualEndpointId = source?._actualEndpointId || target._requestedEndpointId; + target._actualEndpointLabel = source?._actualEndpointLabel || target._requestedEndpointLabel; + } + return target; +} + +/** Apply final/metrics provenance to the active round, not the first bubble. */ +export function applyModelMetricsState(metrics, holder, roundHolder, defaultModel = '') { + const target = roundHolder || holder; + if (!target || !metrics) return target || null; + const roundModels = Array.isArray(metrics.round_models) ? metrics.round_models : []; + const roundModel = roundHolder && roundModels.length + ? roundModels[roundModels.length - 1] + : null; + target._requestedModel = metrics.requested_model || target._requestedModel || defaultModel; + target._actualModel = roundModel || metrics.model || target._actualModel || target._requestedModel; + const roundEndpointIds = Array.isArray(metrics.round_endpoint_ids) ? metrics.round_endpoint_ids : []; + const roundEndpointLabels = Array.isArray(metrics.round_endpoint_labels) ? metrics.round_endpoint_labels : []; + if ( + metrics.requested_endpoint_label + || metrics.endpoint_label + || roundEndpointLabels.length + || target._requestedEndpointLabel + ) { + target._requestedEndpointId = metrics.requested_endpoint_id || target._requestedEndpointId || null; + target._requestedEndpointLabel = metrics.requested_endpoint_label || target._requestedEndpointLabel || 'Selected route'; + const hasRoundEndpointId = Boolean(roundHolder && roundEndpointIds.length); + const hasRoundEndpointLabel = Boolean(roundHolder && roundEndpointLabels.length); + target._actualEndpointId = hasRoundEndpointId + ? roundEndpointIds[roundEndpointIds.length - 1] + : (metrics.endpoint_id || target._actualEndpointId || target._requestedEndpointId); + target._actualEndpointLabel = hasRoundEndpointLabel + ? roundEndpointLabels[roundEndpointLabels.length - 1] + : (metrics.endpoint_label || target._actualEndpointLabel || target._requestedEndpointLabel); + } + return target; +} diff --git a/static/js/chatRenderer.js b/static/js/chatRenderer.js index b29909242..71b1fe17f 100644 --- a/static/js/chatRenderer.js +++ b/static/js/chatRenderer.js @@ -615,10 +615,36 @@ export function sameModelName(left, right) { || shortModel(a).toLowerCase() === shortModel(b).toLowerCase(); } -export function modelRouteLabel(requestedModel, actualModel) { +function shortEndpointLabel(label) { + const value = modelValue(label); + if (!value) return ''; + return value.length > 18 ? value.slice(0, 17) + '…' : value; +} + +export function modelRouteLabel( + requestedModel, + actualModel, + requestedEndpointLabel = '', + actualEndpointLabel = '', + requestedEndpointId = '', + actualEndpointId = '', +) { const requested = modelValue(requestedModel); const actual = modelValue(actualModel) || requested; - if (!requested || sameModelName(requested, actual)) return shortModel(actual || requested); + const requestedRoute = modelValue(requestedEndpointId || requestedEndpointLabel); + const actualRoute = modelValue(actualEndpointId || actualEndpointLabel); + const routeChanged = Boolean( + actualRoute + && requestedRoute + && actualRoute !== requestedRoute + ); + if (!requested || sameModelName(requested, actual)) { + const model = shortModel(actual || requested); + if (!routeChanged) return model; + const from = shortEndpointLabel(requestedEndpointLabel || 'Selected route'); + const to = shortEndpointLabel(actualEndpointLabel || actualEndpointId); + return model + ' (' + from + ' -> ' + to + ')'; + } return shortModel(requested) + ' -> ' + shortModel(actual); } @@ -629,10 +655,24 @@ export function replyModelPair(modelName, metadata) { if (actualFromMeta || requestedFromMeta) { const actual = actualFromMeta || requestedFromMeta || modelValue(modelName); const requested = requestedFromMeta || actual; - return { requestedModel: requested, actualModel: actual }; + return { + requestedModel: requested, + actualModel: actual, + requestedEndpointId: meta.requested_endpoint_id || null, + requestedEndpointLabel: meta.requested_endpoint_label || 'Selected route', + actualEndpointId: meta.endpoint_id || null, + actualEndpointLabel: meta.endpoint_label || meta.requested_endpoint_label || 'Selected route', + }; } const fallback = modelValue(modelName); - return { requestedModel: fallback, actualModel: fallback }; + return { + requestedModel: fallback, + actualModel: fallback, + requestedEndpointId: null, + requestedEndpointLabel: 'Selected route', + actualEndpointId: null, + actualEndpointLabel: 'Selected route', + }; } /** @@ -824,12 +864,50 @@ export function isCostTrackedEndpoint(url) { } /** Cost for the current turn, returning null for non-billable endpoints. */ -function _billableCost(model, inputTokens, outputTokens) { - const url = _currentEndpointUrl(); - if (!isCostTrackedEndpoint(url)) return null; +function _billableCost(model, inputTokens, outputTokens, endpointCostTracked, selectedEndpointUrl) { + // Foreground fallback can answer on a different endpoint than the session's + // selected route. Prefer the backend's non-secret actual-route + // classification; retain the selected-endpoint check for older history. + if (endpointCostTracked === false) return null; + const selectedUrl = selectedEndpointUrl === undefined + ? _currentEndpointUrl() + : selectedEndpointUrl; + if (endpointCostTracked !== true && !isCostTrackedEndpoint(selectedUrl)) { + return null; + } return getModelCost(model, inputTokens, outputTokens); } +/** Sum cost using the route/model that produced each Agent round. */ +function _metricsBillableCost(metrics, model, inputTokens, outputTokens, selectedEndpointUrl) { + const buckets = Array.isArray(metrics.usage_buckets) ? metrics.usage_buckets : []; + if (!buckets.length) { + return _billableCost( + model, + inputTokens, + outputTokens, + metrics.endpoint_cost_tracked, + selectedEndpointUrl, + ); + } + let total = 0; + let hasPricedUsage = false; + for (const bucket of buckets) { + if (!bucket || typeof bucket !== 'object') continue; + const bucketCost = _billableCost( + bucket.model || model, + Number(bucket.input_tokens) || 0, + Number(bucket.output_tokens) || 0, + bucket.endpoint_cost_tracked, + selectedEndpointUrl, + ); + if (bucketCost === null) continue; + total += bucketCost; + hasPricedUsage = true; + } + return hasPricedUsage ? total : null; +} + export function getImageCost(model, quality, size) { if (!model) return null; const m = model.toLowerCase(); @@ -844,6 +922,9 @@ export function getImageCost(model, quality, size) { /* ── Session cost helpers ─────────────────────────────────────────── */ const _COST_KEY = 'ody-session-cost'; +const _COST_RUNS_KEY = 'ody-session-cost-runs'; +const _MAX_COST_RUNS_PER_SESSION = 256; +const _COST_LEDGER_LOCK = 'odysseus-session-cost-ledger'; /** Return the accumulated cost for the current (or given) session. */ export function getSessionCost(sessionId) { @@ -851,7 +932,14 @@ export function getSessionCost(sessionId) { if (!sid) return 0; try { const costs = JSON.parse(localStorage.getItem(_COST_KEY) || '{}'); - return costs[sid] || 0; + const runCosts = JSON.parse(localStorage.getItem(_COST_RUNS_KEY) || '{}'); + const recordedRuns = runCosts[sid] && typeof runCosts[sid] === 'object' + ? Object.values(runCosts[sid]) + : []; + return (costs[sid] || 0) + recordedRuns.reduce( + (total, value) => total + (Number(value) || 0), + 0, + ); } catch (_e) { return 0; } } @@ -863,6 +951,9 @@ export function resetSessionCost(sessionId) { const costs = JSON.parse(localStorage.getItem(_COST_KEY) || '{}'); delete costs[sid]; localStorage.setItem(_COST_KEY, JSON.stringify(costs)); + const runCosts = JSON.parse(localStorage.getItem(_COST_RUNS_KEY) || '{}'); + delete runCosts[sid]; + localStorage.setItem(_COST_RUNS_KEY, JSON.stringify(runCosts)); } catch (_e) { /* ignore */ } updateSessionCostUI(); } @@ -871,21 +962,8 @@ export function resetSessionCost(sessionId) { export function updateSessionCostUI() { const el = document.getElementById('session-cost-display'); if (!el) return; - // Non-billable endpoint? Hide the badge and clear stale cost that a previous - // cloud-rate calculation may have left in localStorage for this session. - const _url = _currentEndpointUrl(); - if (!isCostTrackedEndpoint(_url)) { - const sid = window.sessionModule && window.sessionModule.getCurrentSessionId(); - if (sid && getSessionCost(sid) > 0) { - try { - const costs = JSON.parse(localStorage.getItem(_COST_KEY) || '{}'); - delete costs[sid]; - localStorage.setItem(_COST_KEY, JSON.stringify(costs)); - } catch (_e) { /* ignore */ } - } - el.style.display = 'none'; - return; - } + // The ledger records billable work already performed in this session. A + // selected local endpoint does not erase cost from a paid fallback route. const cost = getSessionCost(); if (cost > 0) { el.textContent = '$' + (cost < 0.01 ? cost.toFixed(4) : cost < 1 ? cost.toFixed(3) : cost.toFixed(2)); @@ -895,6 +973,94 @@ export function updateSessionCostUI() { } } +/** Record one metrics payload in a session ledger at most once. */ +export function recordSessionMetricsCost(metrics, sessionId, selectedEndpointUrl) { + if (!metrics || typeof metrics !== 'object') return null; + const cost = _metricsBillableCost( + metrics, + metrics.model || 'Unknown', + metrics.input_tokens || 0, + metrics.output_tokens || 0, + selectedEndpointUrl, + ); + if (metrics._fromHistory) return cost; + const sid = sessionId || ( + window.sessionModule && window.sessionModule.getCurrentSessionId() + ); + if (!sid || cost === null) return cost; + const runId = typeof metrics._costRecordId === 'string' + ? metrics._costRecordId.trim() + : ''; + if ((metrics._costRecorded || metrics._costRecordPending) && !runId) return cost; + // Recorded is only set once the write actually runs; pending covers the + // window while the write waits on the cross-tab lock, so a replay in that + // window cannot double-add and a tab closed mid-queue never claims recorded. + metrics._costRecordPending = true; + const writeCost = () => { + if (runId) { + try { + const runCosts = JSON.parse(localStorage.getItem(_COST_RUNS_KEY) || '{}'); + const sessionRuns = runCosts[sid] && typeof runCosts[sid] === 'object' + ? runCosts[sid] + : {}; + // Assigning by detached-run identity is replay-idempotent even when a + // refresh produces a fresh metrics object. The Web Lock around this + // read/modify/write also keeps distinct runs from two tabs from + // overwriting one another's stale snapshot. + sessionRuns[runId] = cost; + const entries = Object.entries(sessionRuns); + if (entries.length > _MAX_COST_RUNS_PER_SESSION) { + const overflow = entries.slice(0, entries.length - _MAX_COST_RUNS_PER_SESSION); + const costs = JSON.parse(localStorage.getItem(_COST_KEY) || '{}'); + costs[sid] = (costs[sid] || 0) + overflow.reduce( + (total, entry) => total + (Number(entry[1]) || 0), + 0, + ); + overflow.forEach(([oldRunId]) => delete sessionRuns[oldRunId]); + localStorage.setItem(_COST_KEY, JSON.stringify(costs)); + } + runCosts[sid] = sessionRuns; + localStorage.setItem(_COST_RUNS_KEY, JSON.stringify(runCosts)); + } catch (_e) { /* ignore */ } + } else { + try { + const costs = JSON.parse(localStorage.getItem(_COST_KEY) || '{}'); + costs[sid] = (costs[sid] || 0) + cost; + localStorage.setItem(_COST_KEY, JSON.stringify(costs)); + } catch (_e) { /* ignore */ } + } + metrics._costRecorded = true; + metrics._costRecordPending = false; + const currentSid = window.sessionModule && window.sessionModule.getCurrentSessionId(); + if (currentSid === sid) updateSessionCostUI(); + }; + + let writeStarted = false; + const guardedWrite = () => { + writeStarted = true; + writeCost(); + }; + try { + if ( + typeof navigator !== 'undefined' + && navigator.locks + && typeof navigator.locks.request === 'function' + ) { + const pendingWrite = navigator.locks.request(_COST_LEDGER_LOCK, guardedWrite); + if (pendingWrite && typeof pendingWrite.catch === 'function') { + pendingWrite.catch(() => { + if (!writeStarted) guardedWrite(); + }); + } + } else { + guardedWrite(); + } + } catch (_e) { + if (!writeStarted) guardedWrite(); + } + return cost; +} + /** Create a timestamp span for role labels. * Pass an ISO string / Date / epoch-ms to render the message's own time * (used when replaying history). Falls back to "now" when no value is given. */ @@ -1874,23 +2040,19 @@ export function displayMetrics(messageElement, metrics) { const isReal = metrics.usage_source === 'real'; const ctxPct = metrics.context_percent; const model = metrics.model || 'Unknown'; - const cost = _billableCost(model, inputTokens, outputTokens); + const cost = _metricsBillableCost( + metrics, + model, + inputTokens, + outputTokens, + ); // Nothing useful to show — bail out (only if ALL metrics are missing) if (!responseTime && !inputTokens && !outputTokens && tps == null && !ctxPct) return; - // Accumulate session cost (only on fresh metrics, not history reload) - if (!metrics._fromHistory) { - const _sid = window.sessionModule && window.sessionModule.getCurrentSessionId(); - if (_sid && cost !== null) { - try { - const _costs = JSON.parse(localStorage.getItem(_COST_KEY) || '{}'); - _costs[_sid] = (_costs[_sid] || 0) + cost; - localStorage.setItem(_COST_KEY, JSON.stringify(_costs)); - } catch (_e) { /* ignore */ } - updateSessionCostUI(); - } - } + // Rendering can occur when metrics arrive and again after [DONE]. The + // ledger mutation is idempotent for that shared payload. + recordSessionMetricsCost(metrics); // Keep token counts in the Message Stats popup; the footer should stay slim. const costStr0 = cost !== null ? `$${cost < 0.01 ? cost.toFixed(4) : cost.toFixed(3)}` : null; @@ -2307,9 +2469,19 @@ export function addMessage(role, content, modelName, metadata) { const textRaw = Array.isArray(content) ? markdownModule.renderContent(content) : content; // --- Agent multi-bubble reconstruction from saved metadata --- - if (role === 'assistant' && metadata && metadata.tool_events && metadata.tool_events.length > 0) { + if ( + role === 'assistant' + && metadata + && ( + (Array.isArray(metadata.tool_events) && metadata.tool_events.length > 0) + || (Array.isArray(metadata.round_texts) && metadata.round_texts.length > 1) + ) + ) { const roundTexts = metadata.round_texts || []; - const toolEvents = metadata.tool_events; + const roundModels = metadata.round_models || []; + const roundEndpointIds = metadata.round_endpoint_ids || []; + const roundEndpointLabels = metadata.round_endpoint_labels || []; + const toolEvents = metadata.tool_events || []; let pendingAskUser = null; let lastWrap = null; let firstMsgAi = null; @@ -2322,7 +2494,8 @@ export function addMessage(role, content, modelName, metadata) { toolsByRound[r].push(ev); } - const maxRound = Math.max(...Object.keys(toolsByRound).map(Number), roundTexts.length); + const toolRounds = Object.keys(toolsByRound).map(Number); + const maxRound = Math.max(toolRounds.length ? Math.max(...toolRounds) : 0, roundTexts.length); for (let r = 0; r < maxRound; r++) { const roundNum = r + 1; @@ -2334,10 +2507,31 @@ export function addMessage(role, content, modelName, metadata) { const roleEl = document.createElement('div'); roleEl.className = 'role'; const pair = replyModelPair(modelName, metadata); - const contModel = pair.actualModel || pair.requestedModel; - roleEl.textContent = modelRouteLabel(pair.requestedModel, contModel); - if (pair.requestedModel && contModel && !sameModelName(pair.requestedModel, contModel)) { - roleEl.title = pair.requestedModel + ' -> ' + contModel; + const contModel = roundModels[r] || pair.actualModel || pair.requestedModel; + const contEndpointId = r < roundEndpointIds.length + ? roundEndpointIds[r] + : pair.actualEndpointId; + const contEndpointLabel = r < roundEndpointLabels.length + ? roundEndpointLabels[r] + : pair.actualEndpointLabel; + roleEl.textContent = modelRouteLabel( + pair.requestedModel, + contModel, + pair.requestedEndpointLabel, + contEndpointLabel, + pair.requestedEndpointId, + contEndpointId, + ); + if ( + pair.requestedModel + && contModel + && ( + !sameModelName(pair.requestedModel, contModel) + || (pair.requestedEndpointId && contEndpointId && pair.requestedEndpointId !== contEndpointId) + ) + ) { + roleEl.title = pair.requestedModel + ' -> ' + contModel + + ' (' + pair.requestedEndpointLabel + ' -> ' + contEndpointLabel + ')'; } applyModelColor(roleEl, contModel); if (r === 0) roleEl.appendChild(roleTimestamp(metadata?.timestamp)); @@ -2492,7 +2686,14 @@ export function addMessage(role, content, modelName, metadata) { const isCompacted = metadata?.compacted; const replyModels = replyModelPair(modelName, metadata); const resolvedModel = replyModels.actualModel || replyModels.requestedModel; - var _roleText = role === 'user' ? 'You' : (isSlash || isCompacted) ? 'Odysseus' : modelRouteLabel(replyModels.requestedModel, resolvedModel); + var _roleText = role === 'user' ? 'You' : (isSlash || isCompacted) ? 'Odysseus' : modelRouteLabel( + replyModels.requestedModel, + resolvedModel, + replyModels.requestedEndpointLabel, + replyModels.actualEndpointLabel, + replyModels.requestedEndpointId, + replyModels.actualEndpointId, + ); if (role === 'assistant' && (metadata?.research || metadata?.research_clarification)) { _roleText += ' (Research)'; } @@ -2503,8 +2704,14 @@ export function addMessage(role, content, modelName, metadata) { } r.textContent = _roleText; if (role !== 'user') { - if (!isSlash && !isCompacted && replyModels.requestedModel && resolvedModel && !sameModelName(replyModels.requestedModel, resolvedModel)) { - r.title = replyModels.requestedModel + ' -> ' + resolvedModel; + const endpointChanged = Boolean( + replyModels.requestedEndpointId + && replyModels.actualEndpointId + && replyModels.requestedEndpointId !== replyModels.actualEndpointId + ); + if (!isSlash && !isCompacted && replyModels.requestedModel && resolvedModel && (!sameModelName(replyModels.requestedModel, resolvedModel) || endpointChanged)) { + r.title = replyModels.requestedModel + ' -> ' + resolvedModel + + ' (' + replyModels.requestedEndpointLabel + ' -> ' + replyModels.actualEndpointLabel + ')'; } if (!isSlash && !isCompacted) applyModelColor(r, resolvedModel); r.appendChild(roleTimestamp(metadata?.timestamp)); @@ -2788,6 +2995,7 @@ const chatRenderer = { getSessionCost, resetSessionCost, updateSessionCostUI, + recordSessionMetricsCost, roleTimestamp, stripToolBlocks, copyMessageText, diff --git a/static/js/chatStreamErrors.js b/static/js/chatStreamErrors.js new file mode 100644 index 000000000..250cb290d --- /dev/null +++ b/static/js/chatStreamErrors.js @@ -0,0 +1,23 @@ +/** Build a terminal stream error while preserving provider-supplied text. */ +export function createTerminalStreamError(payload = {}) { + const rawError = payload.error; + const message = ( + payload.text + || (typeof rawError === 'string' ? rawError : rawError?.message) + || `Error ${payload.status || 'unknown'}` + ); + const error = new Error(message); + error.name = 'TerminalStreamError'; + error.terminalStreamError = true; + error.status = payload.status; + return error; +} + +/** Only connection-class stream failures are safe to resubmit automatically. */ +export function isRecoverableStreamError(error) { + if (!error || error.terminalStreamError || error.name === 'TerminalStreamError') return false; + if (error.name === 'TypeError') return true; + const message = (error.message || '').toLowerCase(); + if (/\btool\b|unsupported|json|parse|\b4\d\d\b|\b5\d\d\b/.test(message)) return false; + return /network|fetch|connection|reset|closed|aborted|stream|tim(?:e|ed)\s?out|econn|eof/.test(message); +} diff --git a/static/js/settings.js b/static/js/settings.js index 1710460f6..160ddac7c 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -445,14 +445,7 @@ async function initDefaultChat() { var epSel = el('set-defaultEpSelect'); var modelSel = el('set-defaultModelSelect'); var msg = el('set-defaultChatMsg'); - var fbContainer = el('set-defaultFallbacks'); - var addFbBtn = el('set-defaultAddFallback'); var _endpoints = []; - var _fallbacks = []; // Hidden legacy DOM hook; stored values are not loaded or saved. - - function enabledEndpoints() { - return _endpoints.filter(function(e) { return e.is_enabled; }); - } // Fill any '}paragraph({tokens:e}){return`

${this.parser.parseInline(e)}

+`}table(e){let t="",r="";for(let i=0;i${n}`),` + +`+t+` +`+n+`
+`}tablerow({text:e}){return` +${e} +`}tablecell(e){let t=this.parser.parseInline(e.tokens),r=e.header?"th":"td";return(e.align?`<${r} align="${e.align}">`:`<${r}>`)+t+` +`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${ru(e,!0)}`}br(e){return"
"}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:r}){let n=this.parser.parseInline(r),i=cse(e);if(i===null)return n;e=i;let a='",a}image({href:e,title:t,text:r,tokens:n}){n&&(r=this.parser.parseInline(n,this.parser.textRenderer));let i=cse(e);if(i===null)return ru(r);e=i;let a=`${r}{let s=i[a].flat(1/0);r=r.concat(this.walkTokens(s,t))}):i.tokens&&(r=r.concat(this.walkTokens(i.tokens,t)))}}return r}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(r=>{let n={...r};if(n.async=this.defaults.async||n.async||!1,r.extensions&&(r.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let a=t.renderers[i.name];a?t.renderers[i.name]=function(...s){let l=i.renderer.apply(this,s);return l===!1&&(l=a.apply(this,s)),l}:t.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let a=t[i.level];a?a.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),n.extensions=t),r.renderer){let i=this.defaults.renderer||new w4(this.defaults);for(let a in r.renderer){if(!(a in i))throw new Error(`renderer '${a}' does not exist`);if(["options","parser"].includes(a))continue;let s=a,l=r.renderer[s],u=i[s];i[s]=(...h)=>{let d=l.apply(i,h);return d===!1&&(d=u.apply(i,h)),d||""}}n.renderer=i}if(r.tokenizer){let i=this.defaults.tokenizer||new C4(this.defaults);for(let a in r.tokenizer){if(!(a in i))throw new Error(`tokenizer '${a}' does not exist`);if(["options","rules","lexer"].includes(a))continue;let s=a,l=r.tokenizer[s],u=i[s];i[s]=(...h)=>{let d=l.apply(i,h);return d===!1&&(d=u.apply(i,h)),d}}n.tokenizer=i}if(r.hooks){let i=this.defaults.hooks||new D2;for(let a in r.hooks){if(!(a in i))throw new Error(`hook '${a}' does not exist`);if(["options","block"].includes(a))continue;let s=a,l=r.hooks[s],u=i[s];D2.passThroughHooks.has(a)?i[s]=h=>{if(this.defaults.async&&D2.passThroughHooksRespectAsync.has(a))return Promise.resolve(l.call(i,h)).then(f=>u.call(i,f));let d=l.call(i,h);return u.call(i,d)}:i[s]=(...h)=>{let d=l.apply(i,h);return d===!1&&(d=u.apply(i,h)),d}}n.hooks=i}if(r.walkTokens){let i=this.defaults.walkTokens,a=r.walkTokens;n.walkTokens=function(s){let l=[];return l.push(a.call(this,s)),i&&(l=l.concat(i.call(this,s))),l}}this.defaults={...this.defaults,...n}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return Th.lex(e,t??this.defaults)}parser(e,t){return Ch.parse(e,t??this.defaults)}parseMarkdown(e){return(t,r)=>{let n={...r},i={...this.defaults,...n},a=this.onError(!!i.silent,!!i.async);if(this.defaults.async===!0&&n.async===!1)return a(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof t>"u"||t===null)return a(new Error("marked(): input parameter is undefined or null"));if(typeof t!="string")return a(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));i.hooks&&(i.hooks.options=i,i.hooks.block=e);let s=i.hooks?i.hooks.provideLexer():e?Th.lex:Th.lexInline,l=i.hooks?i.hooks.provideParser():e?Ch.parse:Ch.parseInline;if(i.async)return Promise.resolve(i.hooks?i.hooks.preprocess(t):t).then(u=>s(u,i)).then(u=>i.hooks?i.hooks.processAllTokens(u):u).then(u=>i.walkTokens?Promise.all(this.walkTokens(u,i.walkTokens)).then(()=>u):u).then(u=>l(u,i)).then(u=>i.hooks?i.hooks.postprocess(u):u).catch(a);try{i.hooks&&(t=i.hooks.preprocess(t));let u=s(t,i);i.hooks&&(u=i.hooks.processAllTokens(u)),i.walkTokens&&this.walkTokens(u,i.walkTokens);let h=l(u,i);return i.hooks&&(h=i.hooks.postprocess(h)),h}catch(u){return a(u)}}}onError(e,t){return r=>{if(r.message+=` +Please report this to https://github.com/markedjs/marked.`,e){let n="

An error occurred:

"+ru(r.message+"",!0)+"
";return t?Promise.resolve(n):n}if(t)return Promise.reject(r);throw r}}},Tm=new qZe;o(Sn,"d");Sn.options=Sn.setOptions=function(e){return Tm.setOptions(e),Sn.defaults=Tm.defaults,dse(Sn.defaults),Sn};Sn.getDefaults=PP;Sn.defaults=Cm;Sn.use=function(...e){return Tm.use(...e),Sn.defaults=Tm.defaults,dse(Sn.defaults),Sn};Sn.walkTokens=function(e,t){return Tm.walkTokens(e,t)};Sn.parseInline=Tm.parseInline;Sn.Parser=Ch;Sn.parser=Ch.parse;Sn.Renderer=w4;Sn.TextRenderer=WP;Sn.Lexer=Th;Sn.lexer=Th.lex;Sn.Tokenizer=C4;Sn.Hooks=D2;Sn.parse=Sn;xir=Sn.options,bir=Sn.setOptions,Tir=Sn.use,Cir=Sn.walkTokens,wir=Sn.parseInline,kir=Ch.parse,Sir=Th.lex});function HZe(e,{markdownAutoWrap:t}){let n=e.replace(//g,` +`).replace(/\n{2,}/g,` +`);return mS(n)}function kse(e){return e.split(/\\n|\n|/gi).map(t=>t.trim().match(/<[^>]+>|[^\s<>]+/g)?.map(r=>({content:r,type:"normal"}))??[])}function Sse(e,t={}){let r=HZe(e,t),n=Sn.lexer(r),i=[[]],a=0;function s(l,u="normal"){l.type==="text"?l.text.split(` +`).forEach((d,f)=>{f!==0&&(a++,i.push([])),d.split(" ").forEach(p=>{p=p.replace(/'/g,"'"),p&&i[a].push({content:p,type:u})})}):l.type==="strong"||l.type==="em"?l.tokens.forEach(h=>{s(h,l.type)}):l.type==="html"&&i[a].push({content:l.text,type:"normal"})}return o(s,"processNode"),n.forEach(l=>{l.type==="paragraph"?l.tokens?.forEach(u=>{s(u)}):l.type==="html"?i[a].push({content:l.text,type:"normal"}):i[a].push({content:l.raw,type:"normal"})}),i}function Ese(e){return e?`

${e.replace(/\\n|\n/g,"
")}

`:""}function Ase(e,{markdownAutoWrap:t}={}){let r=Sn.lexer(e);function n(i){return i.type==="text"?t===!1?i.text.replace(/\n */g,"
").replace(/ /g," "):i.text.replace(/\n */g,"
"):i.type==="strong"?`${i.tokens?.map(n).join("")}`:i.type==="em"?`${i.tokens?.map(n).join("")}`:i.type==="paragraph"?`

${i.tokens?.map(n).join("")}

`:i.type==="space"?"":i.type==="html"?`${i.text}`:i.type==="escape"?i.text:(Z.warn(`Unsupported markdown: ${i.type}`),i.raw)}return o(n,"output"),r.map(n).join("")}var Rse=F(()=>{"use strict";wse();f8();vt();o(HZe,"preprocessMarkdown");o(kse,"nonMarkdownToLines");o(Sse,"markdownToLines");o(Ese,"nonMarkdownToHTML");o(Ase,"markdownToHTML")});function UZe(e){return Intl.Segmenter?[...new Intl.Segmenter().segment(e)].map(t=>t.segment):[...e]}function YZe(e,t){let r=UZe(t.content);return _se(e,[],r,t.type)}function _se(e,t,r,n){if(r.length===0)return[{content:t.join(""),type:n},{content:"",type:n}];let[i,...a]=r,s=[...t,i];return e([{content:s.join(""),type:n}])?_se(e,s,a,n):(t.length===0&&i&&(t.push(i),r.shift()),[{content:t.join(""),type:n},{content:r.join(""),type:n}])}function Lse(e,t){if(e.some(({content:r})=>r.includes(` +`)))throw new Error("splitLineToFitWidth does not support newlines in the line");return qP(e,t)}function qP(e,t,r=[],n=[]){if(e.length===0)return n.length>0&&r.push(n),r.length>0?r:[];let i="";e[0].content===" "&&(i=" ",e.shift());let a=e.shift()??{content:" ",type:"normal"},s=[...n];if(i!==""&&s.push({content:i,type:"normal"}),s.push(a),t(s))return qP(e,t,r,s);if(n.length>0)r.push(n),e.unshift(a);else if(a.content){let[l,u]=YZe(t,a);r.push([l]),u.content&&e.unshift(u)}return qP(e,t,r)}var Dse=F(()=>{"use strict";o(UZe,"splitTextToChars");o(YZe,"splitWordToFitWidth");o(_se,"splitWordToFitWidthRecursion");o(Lse,"splitLineToFitWidth");o(qP,"splitLineToFitWidthRecursion")});function Ise(e,t){t&&e.attr("style",t)}async function jZe(e,t,r,n,i=!1,a=_t()){let s=e.append("foreignObject");s.attr("width",`${Math.min(10*r,Mse)}px`),s.attr("height",`${Math.min(10*r,Mse)}px`);let l=s.append("xhtml:div"),u=ni(t.label)?await ey(t.label.replace(xt.lineBreakRegex,` +`),a):mr(t.label,a),h=t.isNode?"nodeLabel":"edgeLabel",d=l.append("span");d.html(u),Ise(d,t.labelStyle),d.attr("class",`${h} ${n}`),Ise(l,t.labelStyle),l.style("display","table-cell"),l.style("white-space","nowrap"),l.style("line-height","1.5"),r!==Number.POSITIVE_INFINITY&&(l.style("max-width",r+"px"),l.style("text-align","center")),l.attr("xmlns","http://www.w3.org/1999/xhtml"),i&&l.attr("class","labelBkg");let f=l.node().getBoundingClientRect();return f.width===r&&(l.style("display","table"),l.style("white-space","break-spaces"),l.style("width",r+"px"),f=l.node().getBoundingClientRect()),s.node()}function HP(e,t,r,n=!1){let i=e.append("tspan").attr("class","text-outer-tspan").attr("x",0).attr("y",t*r-.1+"em").attr("dy",r+"em");return n&&i.attr("text-anchor","middle"),i}function XZe(e,t,r){let n=e.append("text"),i=HP(n,1,t);UP(i,r);let a=i.node().getComputedTextLength();return n.remove(),a}function Pse(e,t,r){let n=e.append("text"),i=HP(n,1,t);UP(i,[{content:r,type:"normal"}]);let a=i.node()?.getBoundingClientRect();return a&&n.remove(),a}function KZe(e,t,r,n=!1,i=!1){let s=t.append("g"),l=s.insert("rect").attr("class","background").attr("style","stroke: none"),u=s.append("text").attr("y","-10.1");i&&u.attr("text-anchor","middle");let h=0;for(let d of r){let f=o(m=>XZe(s,1.1,m)<=e,"checkWidth"),p=f(d)?[d]:Lse(d,f);for(let m of p){let g=HP(u,h,1.1,i);UP(g,m),h++}}if(n){let d=u.node().getBBox(),f=2;return l.attr("x",d.x-f).attr("y",d.y-f).attr("width",d.width+2*f).attr("height",d.height+2*f),s.node()}else return u.node()}function Nse(e){let t=/&(amp|lt|gt);/g;return e.replace(t,(r,n)=>{switch(n){case"amp":return"&";case"lt":return"<";case"gt":return">";default:return r}})}function UP(e,t){e.text(""),t.forEach((r,n)=>{let i=e.append("tspan").attr("font-style",r.type==="em"?"italic":"normal").attr("class","text-inner-tspan").attr("font-weight",r.type==="strong"?"bold":"normal");n===0?i.text(Nse(r.content)):i.text(" "+Nse(r.content))})}async function ZZe(e,t={}){let r=[];e.replace(/(fa[bklrs]?):fa-([\w-]+)/g,(i,a,s)=>(r.push((async()=>{let l=`${a}:${s}`;return await aJ(l)?await ts(l,void 0,{class:"label-icon"}):``})()),i));let n=await Promise.all(r);return e.replace(/(fa[bklrs]?):fa-([\w-]+)/g,()=>n.shift()??"")}var Mse,Pn,Ls=F(()=>{"use strict";$r();Vr();vt();Rse();Qt();Vl();Dse();ur();o(Ise,"applyStyle");Mse=16384;o(jZe,"addHtmlSpan");o(HP,"createTspan");o(XZe,"computeWidthOfText");o(Pse,"computeDimensionOfText");o(KZe,"createFormattedText");o(Nse,"decodeHTMLEntities");o(UP,"updateTextContentAndStyles");o(ZZe,"replaceIconSubstring");Pn=o(async(e,t="",{style:r="",isTitle:n=!1,classes:i="",useHtmlLabels:a=!0,markdown:s=!0,isNode:l=!0,width:u=200,addSvgBackground:h=!1}={},d)=>{if(Z.debug("XYZ createText",t,r,n,i,a,l,"addSvgBackground: ",h),a){let f=s?Ase(t,d):Ese(t),p=await ZZe(Rs(f),d),m=t.replace(/\\\\/g,"\\"),g={isNode:l,label:ni(t)?m:p,labelStyle:r.replace("fill:","color:")};return await jZe(e,g,u,i,h,d)}else{let f=Rs(t.replace(//g,"
")),p=s?Sse(f.replace("
","
"),d):kse(f),m=KZe(u,e,p,t?h:!1,!l);if(l){/stroke:/.exec(r)&&(r=r.replace("stroke:","lineColor:"));let g=r.replace(/stroke:[^;]+;?/g,"").replace(/stroke-width:[^;]+;?/g,"").replace(/fill:[^;]+;?/g,"").replace(/color:/g,"fill:");et(m).attr("style",g)}else{let g=r.replace(/stroke:[^;]+;?/g,"").replace(/stroke-width:[^;]+;?/g,"").replace(/fill:[^;]+;?/g,"").replace(/background:/g,"fill:");et(m).select("rect").attr("style",g.replace(/background:/g,"fill:"));let y=r.replace(/stroke:[^;]+;?/g,"").replace(/stroke-width:[^;]+;?/g,"").replace(/fill:[^;]+;?/g,"").replace(/color:/g,"fill:");et(m).select("text").attr("style",y)}return n?et(m).selectAll("tspan.text-outer-tspan").classed("title-row",!0):et(m).selectAll("tspan.text-outer-tspan").classed("row",!0),m}},"createText")});async function E4(e,t){let r=e.getElementsByTagName("img");if(!r||r.length===0)return;let n=t.replace(/]*>/g,"").trim()==="";await Promise.all([...r].map(i=>new Promise(a=>{function s(){if(i.style.display="flex",i.style.flexDirection="column",n){let l=Ae().fontSize?Ae().fontSize:window.getComputedStyle(document.body).fontSize,u=5,[h=cr.fontSize]=As(l),d=h*u+"px";i.style.minWidth=d,i.style.maxWidth=d}else i.style.width="100%";a(i)}o(s,"setupImage"),setTimeout(()=>{i.complete&&s()}),i.addEventListener("error",s),i.addEventListener("load",s)})))}var YP=F(()=>{"use strict";Xt();Wi();Qt();o(E4,"configureLabelImages")});function or(e){let t=e.map((r,n)=>`${n===0?"M":"L"}${r.x},${r.y}`);return t.push("Z"),t.join(" ")}function fl(e,t,r,n,i,a){let s=[],u=r-e,h=n-t,d=u/a,f=2*Math.PI/d,p=t+h/2;for(let m=0;m<=50;m++){let g=m/50,y=e+g*u,v=p+i*Math.sin(f*(y-e));s.push({x:y,y:v})}return s}function wm(e,t,r,n,i,a){let s=[],l=i*Math.PI/180,d=(a*Math.PI/180-l)/(n-1);for(let f=0;fu.tagName==="path"),r=document.createElementNS("http://www.w3.org/2000/svg","path"),n=t.map(u=>u.getAttribute("d")).filter(u=>u!==null).join(" ");r.setAttribute("d",n);let i=t.find(u=>u.getAttribute("fill")!=="none"),a=t.find(u=>u.getAttribute("stroke")!=="none"),s=o((u,h)=>u?.getAttribute(h)??void 0,"getAttr");if(i){let u={fill:s(i,"fill"),"fill-opacity":s(i,"fill-opacity")??"1"};Object.entries(u).forEach(([h,d])=>{d&&r.setAttribute(h,d)})}if(a){let u={stroke:s(a,"stroke"),"stroke-width":s(a,"stroke-width")??"1","stroke-opacity":s(a,"stroke-opacity")??"1"};Object.entries(u).forEach(([h,d])=>{d&&r.setAttribute(h,d)})}let l=document.createElementNS("http://www.w3.org/2000/svg","g");return l.appendChild(r),l}var It,A4,pt,Dt,Kt=F(()=>{"use strict";Ls();Xt();ur();$r();Vr();Qt();YP();It=o(async(e,t,r)=>{let n,i=t.useHtmlLabels||ya(Ae()?.htmlLabels);r?n=r:n="node default";let a=e.insert("g").attr("class",n).attr("id",t.domId||t.id),s=a.insert("g").attr("class","label").attr("style",kn(t.labelStyle)),l;t.label===void 0?l="":l=typeof t.label=="string"?t.label:t.label[0];let u=!!t.icon||!!t.img,h=t.labelType==="markdown",d=await Pn(s,mr(Rs(l),Ae()),{useHtmlLabels:i,width:t.width||Ae().flowchart?.wrappingWidth,classes:h?"markdown-node-label":"",style:t.labelStyle,addSvgBackground:u,markdown:h},Ae()),f=d.getBBox(),p=(t?.padding??0)/2;if(i){let m=d.children[0],g=et(d);await E4(m,l),f=m.getBoundingClientRect(),g.attr("width",f.width),g.attr("height",f.height)}return i?s.attr("transform","translate("+-f.width/2+", "+-f.height/2+")"):s.attr("transform","translate(0, "+-f.height/2+")"),t.centerLabel&&s.attr("transform","translate("+-f.width/2+", "+-f.height/2+")"),s.insert("rect",":first-child"),{shapeSvg:a,bbox:f,halfPadding:p,label:s}},"labelHelper"),A4=o(async(e,t,r)=>{let n=r.useHtmlLabels??Gr(Ae()),i=e.insert("g").attr("class","label").attr("style",r.labelStyle||""),a=await Pn(i,mr(Rs(t),Ae()),{useHtmlLabels:n,width:r.width||Ae()?.flowchart?.wrappingWidth,style:r.labelStyle,addSvgBackground:!!r.icon||!!r.img}),s=a.getBBox(),l=r.padding/2;if(Gr(Ae())){let u=a.children[0],h=et(a);s=u.getBoundingClientRect(),h.attr("width",s.width),h.attr("height",s.height)}return n?i.attr("transform","translate("+-s.width/2+", "+-s.height/2+")"):i.attr("transform","translate(0, "+-s.height/2+")"),r.centerLabel&&i.attr("transform","translate("+-s.width/2+", "+-s.height/2+")"),i.insert("rect",":first-child"),{shapeSvg:e,bbox:s,halfPadding:l,label:i}},"insertLabel"),pt=o((e,t)=>{let r=t.node().getBBox();e.width=r.width,e.height=r.height},"updateNodeBounds"),Dt=o((e,t)=>(e.look==="handDrawn"?"rough-node":"node")+" "+e.cssClasses+" "+(t||""),"getNodeClasses");o(or,"createPathFromPoints");o(fl,"generateFullSineWavePoints");o(wm,"generateCirclePoints");o(jP,"mergePaths")});function QZe(e,t){return e.intersect(t)}var Ose,Bse=F(()=>{"use strict";o(QZe,"intersectNode");Ose=QZe});function JZe(e,t,r,n){var i=e.x,a=e.y,s=i-n.x,l=a-n.y,u=Math.sqrt(t*t*l*l+r*r*s*s),h=Math.abs(t*r*s/u);n.x{"use strict";o(JZe,"intersectEllipse");R4=JZe});function eQe(e,t,r){return R4(e,t,t,r)}var $se,Fse=F(()=>{"use strict";XP();o(eQe,"intersectCircle");$se=eQe});function tQe(e,t,r,n){{let i=t.y-e.y,a=e.x-t.x,s=t.x*e.y-e.x*t.y,l=i*r.x+a*r.y+s,u=i*n.x+a*n.y+s,h=1e-6;if(l!==0&&u!==0&&zse(l,u))return;let d=n.y-r.y,f=r.x-n.x,p=n.x*r.y-r.x*n.y,m=d*e.x+f*e.y+p,g=d*t.x+f*t.y+p;if(Math.abs(m)0}var Gse,Vse=F(()=>{"use strict";o(tQe,"intersectLine");o(zse,"sameSign");Gse=tQe});function rQe(e,t,r){let n=e.x,i=e.y,a=[],s=Number.POSITIVE_INFINITY,l=Number.POSITIVE_INFINITY;typeof t.forEach=="function"?t.forEach(function(d){s=Math.min(s,d.x),l=Math.min(l,d.y)}):(s=Math.min(s,t.x),l=Math.min(l,t.y));let u=n-e.width/2-s,h=i-e.height/2-l;for(let d=0;d1&&a.sort(function(d,f){let p=d.x-r.x,m=d.y-r.y,g=Math.sqrt(p*p+m*m),y=f.x-r.x,v=f.y-r.y,x=Math.sqrt(y*y+v*v);return g{"use strict";Vse();o(rQe,"intersectPolygon");Wse=rQe});var nQe,nu,_4=F(()=>{"use strict";nQe=o((e,t)=>{var r=e.x,n=e.y,i=t.x-r,a=t.y-n,s=e.width/2,l=e.height/2,u,h;return Math.abs(a)*s>Math.abs(i)*l?(a<0&&(l=-l),u=a===0?0:l*i/a,h=l):(i<0&&(s=-s),u=s,h=i===0?0:s*a/i),{x:r+u,y:n+h}},"intersectRect"),nu=nQe});var ht,nr=F(()=>{"use strict";Bse();Fse();XP();qse();_4();ht={node:Ose,circle:$se,ellipse:R4,polygon:Wse,rect:nu}});var Hse,iu,iQe,N2,ct,dt,aQe,Jt=F(()=>{"use strict";Xt();Hse=o(e=>{let{handDrawnSeed:t}=Ae();return{fill:e,hachureAngle:120,hachureGap:4,fillWeight:2,roughness:.7,stroke:e,seed:t}},"solidStateFill"),iu=o(e=>{let t=iQe([...e.cssCompiledStyles||[],...e.cssStyles||[],...e.labelStyle||[]]);return{stylesMap:t,stylesArray:[...t]}},"compileStyles"),iQe=o(e=>{let t=new Map;return e.forEach(r=>{let[n,i]=r.split(":");t.set(n.trim(),i?.trim())}),t},"styles2Map"),N2=o(e=>e==="color"||e==="font-size"||e==="font-family"||e==="font-weight"||e==="font-style"||e==="text-decoration"||e==="text-align"||e==="text-transform"||e==="line-height"||e==="letter-spacing"||e==="word-spacing"||e==="text-shadow"||e==="text-overflow"||e==="white-space"||e==="word-wrap"||e==="word-break"||e==="overflow-wrap"||e==="hyphens","isLabelStyle"),ct=o(e=>{let{stylesArray:t}=iu(e),r=[],n=[],i=[],a=[];return t.forEach(s=>{let l=s[0];N2(l)?r.push(s.join(":")+" !important"):(n.push(s.join(":")+" !important"),l.includes("stroke")&&i.push(s.join(":")+" !important"),l==="fill"&&a.push(s.join(":")+" !important"))}),{labelStyles:r.join(";"),nodeStyles:n.join(";"),stylesArray:t,borderStyles:i,backgroundStyles:a}},"styles2String"),dt=o((e,t)=>{let{themeVariables:r,handDrawnSeed:n}=Ae(),{nodeBorder:i,mainBkg:a}=r,{stylesMap:s}=iu(e);return Object.assign({roughness:.7,fill:s.get("fill")||a,fillStyle:"hachure",fillWeight:4,hachureGap:5.2,stroke:s.get("stroke")||i,seed:n,strokeWidth:s.get("stroke-width")?.replace("px","")||1.3,fillLineDash:[0,0],strokeLineDash:aQe(s.get("stroke-dasharray"))},t)},"userNodeOverrides"),aQe=o(e=>{if(!e)return[0,0];let t=e.trim().split(/\s+/).map(Number);if(t.length===1){let i=isNaN(t[0])?0:t[0];return[i,i]}let r=isNaN(t[0])?0:t[0],n=isNaN(t[1])?0:t[1];return[r,n]},"getStrokeDashArray")});function KP(e,t,r){if(e&&e.length){let[n,i]=t,a=Math.PI/180*r,s=Math.cos(a),l=Math.sin(a);for(let u of e){let[h,d]=u;u[0]=(h-n)*s-(d-i)*l+n,u[1]=(h-n)*l+(d-i)*s+i}}}function sQe(e,t){return e[0]===t[0]&&e[1]===t[1]}function oQe(e,t,r,n=1){let i=r,a=Math.max(t,.1),s=e[0]&&e[0][0]&&typeof e[0][0]=="number"?[e]:e,l=[0,0];if(i)for(let h of s)KP(h,l,i);let u=(function(h,d,f){let p=[];for(let b of h){let T=[...b];sQe(T[0],T[T.length-1])||T.push([T[0][0],T[0][1]]),T.length>2&&p.push(T)}let m=[];d=Math.max(d,.1);let g=[];for(let b of p)for(let T=0;Tb.yminT.ymin?1:b.xT.x?1:b.ymax===T.ymax?0:(b.ymax-T.ymax)/Math.abs(b.ymax-T.ymax))),!g.length)return m;let y=[],v=g[0].ymin,x=0;for(;y.length||g.length;){if(g.length){let b=-1;for(let T=0;Tv);T++)b=T;g.splice(0,b+1).forEach((T=>{y.push({s:v,edge:T})}))}if(y=y.filter((b=>!(b.edge.ymax<=v))),y.sort(((b,T)=>b.edge.x===T.edge.x?0:(b.edge.x-T.edge.x)/Math.abs(b.edge.x-T.edge.x))),(f!==1||x%d==0)&&y.length>1)for(let b=0;b=y.length)break;let k=y[b].edge,C=y[T].edge;m.push([[Math.round(k.x),v],[Math.round(C.x),v]])}v+=f,y.forEach((b=>{b.edge.x=b.edge.x+f*b.edge.islope})),x++}return m})(s,a,n);if(i){for(let h of s)KP(h,l,-i);(function(h,d,f){let p=[];h.forEach((m=>p.push(...m))),KP(p,d,f)})(u,l,-i)}return u}function $2(e,t){var r;let n=t.hachureAngle+90,i=t.hachureGap;i<0&&(i=4*t.strokeWidth),i=Math.round(Math.max(i,.1));let a=1;return t.roughness>=1&&(((r=t.randomizer)===null||r===void 0?void 0:r.next())||Math.random())>.7&&(a=i),oQe(e,i,n,a||1)}function $4(e){let t=e[0],r=e[1];return Math.sqrt(Math.pow(t[0]-r[0],2)+Math.pow(t[1]-r[1],2))}function QP(e,t){return e.type===t}function dO(e){let t=[],r=(function(s){let l=new Array;for(;s!=="";)if(s.match(/^([ \t\r\n,]+)/))s=s.substr(RegExp.$1.length);else if(s.match(/^([aAcChHlLmMqQsStTvVzZ])/))l[l.length]={type:lQe,text:RegExp.$1},s=s.substr(RegExp.$1.length);else{if(!s.match(/^(([-+]?[0-9]+(\.[0-9]*)?|[-+]?\.[0-9]+)([eE][-+]?[0-9]+)?)/))return[];l[l.length]={type:ZP,text:`${parseFloat(RegExp.$1)}`},s=s.substr(RegExp.$1.length)}return l[l.length]={type:Use,text:""},l})(e),n="BOD",i=0,a=r[i];for(;!QP(a,Use);){let s=0,l=[];if(n==="BOD"){if(a.text!=="M"&&a.text!=="m")return dO("M0,0"+e);i++,s=L4[a.text],n=a.text}else QP(a,ZP)?s=L4[n]:(i++,s=L4[a.text],n=a.text);if(!(i+sd%2?h+r:h+t));a.push({key:"C",data:u}),t=u[4],r=u[5];break}case"Q":a.push({key:"Q",data:[...l]}),t=l[2],r=l[3];break;case"q":{let u=l.map(((h,d)=>d%2?h+r:h+t));a.push({key:"Q",data:u}),t=u[2],r=u[3];break}case"A":a.push({key:"A",data:[...l]}),t=l[5],r=l[6];break;case"a":t+=l[5],r+=l[6],a.push({key:"A",data:[l[0],l[1],l[2],l[3],l[4],t,r]});break;case"H":a.push({key:"H",data:[...l]}),t=l[0];break;case"h":t+=l[0],a.push({key:"H",data:[t]});break;case"V":a.push({key:"V",data:[...l]}),r=l[0];break;case"v":r+=l[0],a.push({key:"V",data:[r]});break;case"S":a.push({key:"S",data:[...l]}),t=l[2],r=l[3];break;case"s":{let u=l.map(((h,d)=>d%2?h+r:h+t));a.push({key:"S",data:u}),t=u[2],r=u[3];break}case"T":a.push({key:"T",data:[...l]}),t=l[0],r=l[1];break;case"t":t+=l[0],r+=l[1],a.push({key:"T",data:[t,r]});break;case"Z":case"z":a.push({key:"Z",data:[]}),t=n,r=i}return a}function toe(e){let t=[],r="",n=0,i=0,a=0,s=0,l=0,u=0;for(let{key:h,data:d}of e){switch(h){case"M":t.push({key:"M",data:[...d]}),[n,i]=d,[a,s]=d;break;case"C":t.push({key:"C",data:[...d]}),n=d[4],i=d[5],l=d[2],u=d[3];break;case"L":t.push({key:"L",data:[...d]}),[n,i]=d;break;case"H":n=d[0],t.push({key:"L",data:[n,i]});break;case"V":i=d[0],t.push({key:"L",data:[n,i]});break;case"S":{let f=0,p=0;r==="C"||r==="S"?(f=n+(n-l),p=i+(i-u)):(f=n,p=i),t.push({key:"C",data:[f,p,...d]}),l=d[0],u=d[1],n=d[2],i=d[3];break}case"T":{let[f,p]=d,m=0,g=0;r==="Q"||r==="T"?(m=n+(n-l),g=i+(i-u)):(m=n,g=i);let y=n+2*(m-n)/3,v=i+2*(g-i)/3,x=f+2*(m-f)/3,b=p+2*(g-p)/3;t.push({key:"C",data:[y,v,x,b,f,p]}),l=m,u=g,n=f,i=p;break}case"Q":{let[f,p,m,g]=d,y=n+2*(f-n)/3,v=i+2*(p-i)/3,x=m+2*(f-m)/3,b=g+2*(p-g)/3;t.push({key:"C",data:[y,v,x,b,m,g]}),l=f,u=p,n=m,i=g;break}case"A":{let f=Math.abs(d[0]),p=Math.abs(d[1]),m=d[2],g=d[3],y=d[4],v=d[5],x=d[6];f===0||p===0?(t.push({key:"C",data:[n,i,v,x,v,x]}),n=v,i=x):(n!==v||i!==x)&&(roe(n,i,v,x,f,p,m,g,y).forEach((function(b){t.push({key:"C",data:b})})),n=v,i=x);break}case"Z":t.push({key:"Z",data:[]}),n=a,i=s}r=h}return t}function P2(e,t,r){return[e*Math.cos(r)-t*Math.sin(r),e*Math.sin(r)+t*Math.cos(r)]}function roe(e,t,r,n,i,a,s,l,u,h){let d=(f=s,Math.PI*f/180);var f;let p=[],m=0,g=0,y=0,v=0;if(h)[m,g,y,v]=h;else{[e,t]=P2(e,t,-d),[r,n]=P2(r,n,-d);let A=(e-r)/2,M=(t-n)/2,D=A*A/(i*i)+M*M/(a*a);D>1&&(D=Math.sqrt(D),i*=D,a*=D);let P=i*i,B=a*a,O=P*B-P*M*M-B*A*A,$=P*M*M+B*A*A,V=(l===u?-1:1)*Math.sqrt(Math.abs(O/$));y=V*i*M/a+(e+r)/2,v=V*-a*A/i+(t+n)/2,m=Math.asin(parseFloat(((t-v)/a).toFixed(9))),g=Math.asin(parseFloat(((n-v)/a).toFixed(9))),eg&&(m-=2*Math.PI),!u&&g>m&&(g-=2*Math.PI)}let x=g-m;if(Math.abs(x)>120*Math.PI/180){let A=g,M=r,D=n;g=u&&g>m?m+120*Math.PI/180*1:m+120*Math.PI/180*-1,p=roe(r=y+i*Math.cos(g),n=v+a*Math.sin(g),M,D,i,a,s,0,u,[g,A,y,v])}x=g-m;let b=Math.cos(m),T=Math.sin(m),k=Math.cos(g),C=Math.sin(g),w=Math.tan(x/4),S=4/3*i*w,R=4/3*a*w,L=[e,t],N=[e+S*T,t-R*b],I=[r+S*C,n-R*k],_=[r,n];if(N[0]=2*L[0]-N[0],N[1]=2*L[1]-N[1],h)return[N,I,_].concat(p);{p=[N,I,_].concat(p);let A=[];for(let M=0;M2){let i=[];for(let a=0;a2*Math.PI&&(m=0,g=2*Math.PI);let y=2*Math.PI/u.curveStepCount,v=Math.min(y/2,(g-m)/2),x=Qse(v,h,d,f,p,m,g,1,u);if(!u.disableMultiStroke){let b=Qse(v,h,d,f,p,m,g,1.5,u);x.push(...b)}return s&&(l?x.push(...ef(h,d,h+f*Math.cos(m),d+p*Math.sin(m),u),...ef(h,d,h+f*Math.cos(g),d+p*Math.sin(g),u)):x.push({op:"lineTo",data:[h,d]},{op:"lineTo",data:[h+f*Math.cos(m),d+p*Math.sin(m)]})),{type:"path",ops:x}}function Xse(e,t){let r=toe(eoe(dO(e))),n=[],i=[0,0],a=[0,0];for(let{key:s,data:l}of r)switch(s){case"M":a=[l[0],l[1]],i=[l[0],l[1]];break;case"L":n.push(...ef(a[0],a[1],l[0],l[1],t)),a=[l[0],l[1]];break;case"C":{let[u,h,d,f,p,m]=l;n.push(...hQe(u,h,d,f,p,m,a,t)),a=[p,m];break}case"Z":n.push(...ef(a[0],a[1],i[0],i[1],t)),a=[i[0],i[1]]}return{type:"path",ops:n}}function JP(e,t){let r=[];for(let n of e)if(n.length){let i=t.maxRandomnessOffset||0,a=n.length;if(a>2){r.push({op:"move",data:[n[0][0]+Cr(i,t),n[0][1]+Cr(i,t)]});for(let s=1;s500?.4:-.0016668*u+1.233334;let d=i.maxRandomnessOffset||0;d*d*100>l&&(d=u/10);let f=d/2,p=.2+.2*aoe(i),m=i.bowing*i.maxRandomnessOffset*(n-t)/200,g=i.bowing*i.maxRandomnessOffset*(e-r)/200;m=Cr(m,i,h),g=Cr(g,i,h);let y=[],v=o(()=>Cr(f,i,h),"M"),x=o(()=>Cr(d,i,h),"k"),b=i.preserveVertices;return a&&(s?y.push({op:"move",data:[e+(b?0:v()),t+(b?0:v())]}):y.push({op:"move",data:[e+(b?0:Cr(d,i,h)),t+(b?0:Cr(d,i,h))]})),s?y.push({op:"bcurveTo",data:[m+e+(r-e)*p+v(),g+t+(n-t)*p+v(),m+e+2*(r-e)*p+v(),g+t+2*(n-t)*p+v(),r+(b?0:v()),n+(b?0:v())]}):y.push({op:"bcurveTo",data:[m+e+(r-e)*p+x(),g+t+(n-t)*p+x(),m+e+2*(r-e)*p+x(),g+t+2*(n-t)*p+x(),r+(b?0:x()),n+(b?0:x())]}),y}function D4(e,t,r){if(!e.length)return[];let n=[];n.push([e[0][0]+Cr(t,r),e[0][1]+Cr(t,r)]),n.push([e[0][0]+Cr(t,r),e[0][1]+Cr(t,r)]);for(let i=1;i3){let a=[],s=1-r.curveTightness;i.push({op:"move",data:[e[1][0],e[1][1]]});for(let l=1;l+21&&i.push(l)):i.push(l),i.push(e[t+3])}else{let u=e[t+0],h=e[t+1],d=e[t+2],f=e[t+3],p=km(u,h,.5),m=km(h,d,.5),g=km(d,f,.5),y=km(p,m,.5),v=km(m,g,.5),x=km(y,v,.5);cO([u,p,y,x],0,r,i),cO([x,v,g,f],0,r,i)}var a,s;return i}function fQe(e,t){return B4(e,0,e.length,t)}function B4(e,t,r,n,i){let a=i||[],s=e[t],l=e[r-1],u=0,h=1;for(let d=t+1;du&&(u=f,h=d)}return Math.sqrt(u)>n?(B4(e,t,h+1,n,a),B4(e,h,r,n,a)):(a.length||a.push(s),a.push(l)),a}function eO(e,t=.15,r){let n=[],i=(e.length-1)/3;for(let a=0;a0?B4(n,0,n.length,r):n}var B2,tO,rO,nO,iO,aO,no,sO,lQe,ZP,Use,L4,cQe,zo,zy,uO,I4,hO,ut,tr=F(()=>{"use strict";o(KP,"t");o(sQe,"e");o(oQe,"s");o($2,"n");B2=class{static{o(this,"o")}constructor(t){this.helper=t}fillPolygons(t,r){return this._fillPolygons(t,r)}_fillPolygons(t,r){let n=$2(t,r);return{type:"fillSketch",ops:this.renderLines(n,r)}}renderLines(t,r){let n=[];for(let i of t)n.push(...this.helper.doubleLineOps(i[0][0],i[0][1],i[1][0],i[1][1],r));return n}};o($4,"a");tO=class extends B2{static{o(this,"h")}fillPolygons(t,r){let n=r.hachureGap;n<0&&(n=4*r.strokeWidth),n=Math.max(n,.1);let i=$2(t,Object.assign({},r,{hachureGap:n})),a=Math.PI/180*r.hachureAngle,s=[],l=.5*n*Math.cos(a),u=.5*n*Math.sin(a);for(let[h,d]of i)$4([h,d])&&s.push([[h[0]-l,h[1]+u],[...d]],[[h[0]+l,h[1]-u],[...d]]);return{type:"fillSketch",ops:this.renderLines(s,r)}}},rO=class extends B2{static{o(this,"r")}fillPolygons(t,r){let n=this._fillPolygons(t,r),i=Object.assign({},r,{hachureAngle:r.hachureAngle+90}),a=this._fillPolygons(t,i);return n.ops=n.ops.concat(a.ops),n}},nO=class{static{o(this,"i")}constructor(t){this.helper=t}fillPolygons(t,r){let n=$2(t,r=Object.assign({},r,{hachureAngle:0}));return this.dotsOnLines(n,r)}dotsOnLines(t,r){let n=[],i=r.hachureGap;i<0&&(i=4*r.strokeWidth),i=Math.max(i,.1);let a=r.fillWeight;a<0&&(a=r.strokeWidth/2);let s=i/4;for(let l of t){let u=$4(l),h=u/i,d=Math.ceil(h)-1,f=u-d*i,p=(l[0][0]+l[1][0])/2-i/4,m=Math.min(l[0][1],l[1][1]);for(let g=0;g{let l=$4(s),u=Math.floor(l/(n+i)),h=(l+i-u*(n+i))/2,d=s[0],f=s[1];d[0]>f[0]&&(d=s[1],f=s[0]);let p=Math.atan((f[1]-d[1])/(f[0]-d[0]));for(let m=0;m{let s=$4(a),l=Math.round(s/(2*r)),u=a[0],h=a[1];u[0]>h[0]&&(u=a[1],h=a[0]);let d=Math.atan((h[1]-u[1])/(h[0]-u[0]));for(let f=0;f2*Math.PI&&(S=0,R=2*Math.PI);let L=(R-S)/b.curveStepCount,N=[];for(let I=S;I<=R;I+=L)N.push([T+C*Math.cos(I),k+w*Math.sin(I)]);return N.push([T+C*Math.cos(R),k+w*Math.sin(R)]),N.push([T,k]),Fy([N],b)})(t,r,n,i,a,s,h));return h.stroke!==zo&&d.push(f),this._d("arc",d,h)}curve(t,r){let n=this._o(r),i=[],a=Yse(t,n);if(n.fill&&n.fill!==zo)if(n.fillStyle==="solid"){let s=Yse(t,Object.assign(Object.assign({},n),{disableMultiStroke:!0,roughness:n.roughness?n.roughness+n.fillShapeRoughnessGain:0}));i.push({type:"fillPath",ops:this._mergedShape(s.ops)})}else{let s=[],l=t;if(l.length){let u=typeof l[0][0]=="number"?[l]:l;for(let h of u)h.length<3?s.push(...h):h.length===3?s.push(...eO(Jse([h[0],h[0],h[1],h[2]]),10,(1+n.roughness)/2)):s.push(...eO(Jse(h),10,(1+n.roughness)/2))}s.length&&i.push(Fy([s],n))}return n.stroke!==zo&&i.push(a),this._d("curve",i,n)}polygon(t,r){let n=this._o(r),i=[],a=M4(t,!0,n);return n.fill&&(n.fillStyle==="solid"?i.push(JP([t],n)):i.push(Fy([t],n))),n.stroke!==zo&&i.push(a),this._d("polygon",i,n)}path(t,r){let n=this._o(r),i=[];if(!t)return this._d("path",i,n);t=(t||"").replace(/\n/g," ").replace(/(-\s)/g,"-").replace("/(ss)/g"," ");let a=n.fill&&n.fill!=="transparent"&&n.fill!==zo,s=n.stroke!==zo,l=!!(n.simplification&&n.simplification<1),u=(function(d,f,p){let m=toe(eoe(dO(d))),g=[],y=[],v=[0,0],x=[],b=o(()=>{x.length>=4&&y.push(...eO(x,f)),x=[]},"i"),T=o(()=>{b(),y.length&&(g.push(y),y=[])},"c");for(let{key:C,data:w}of m)switch(C){case"M":T(),v=[w[0],w[1]],y.push(v);break;case"L":b(),y.push([w[0],w[1]]);break;case"C":if(!x.length){let S=y.length?y[y.length-1]:v;x.push([S[0],S[1]])}x.push([w[0],w[1]]),x.push([w[2],w[3]]),x.push([w[4],w[5]]);break;case"Z":b(),y.push([v[0],v[1]])}if(T(),!p)return g;let k=[];for(let C of g){let w=fQe(C,p);w.length&&k.push(w)}return k})(t,1,l?4-4*(n.simplification||1):(1+n.roughness)/2),h=Xse(t,n);if(a)if(n.fillStyle==="solid")if(u.length===1){let d=Xse(t,Object.assign(Object.assign({},n),{disableMultiStroke:!0,roughness:n.roughness?n.roughness+n.fillShapeRoughnessGain:0}));i.push({type:"fillPath",ops:this._mergedShape(d.ops)})}else i.push(JP(u,n));else i.push(Fy(u,n));return s&&(l?u.forEach((d=>{i.push(M4(d,!1,n))})):i.push(h)),this._d("path",i,n)}opsToPath(t,r){let n="";for(let i of t.ops){let a=typeof r=="number"&&r>=0?i.data.map((s=>+s.toFixed(r))):i.data;switch(i.op){case"move":n+=`M${a[0]} ${a[1]} `;break;case"bcurveTo":n+=`C${a[0]} ${a[1]}, ${a[2]} ${a[3]}, ${a[4]} ${a[5]} `;break;case"lineTo":n+=`L${a[0]} ${a[1]} `}}return n.trim()}toPaths(t){let r=t.sets||[],n=t.options||this.defaultOptions,i=[];for(let a of r){let s=null;switch(a.type){case"path":s={d:this.opsToPath(a),stroke:n.stroke,strokeWidth:n.strokeWidth,fill:zo};break;case"fillPath":s={d:this.opsToPath(a),stroke:zo,strokeWidth:0,fill:n.fill||zo};break;case"fillSketch":s=this.fillSketch(a,n)}s&&i.push(s)}return i}fillSketch(t,r){let n=r.fillWeight;return n<0&&(n=r.strokeWidth/2),{d:this.opsToPath(t),stroke:r.fill||zo,strokeWidth:n,fill:zo}}_mergedShape(t){return t.filter(((r,n)=>n===0||r.op!=="move"))}},uO=class{static{o(this,"st")}constructor(t,r){this.canvas=t,this.ctx=this.canvas.getContext("2d"),this.gen=new zy(r)}draw(t){let r=t.sets||[],n=t.options||this.getDefaultOptions(),i=this.ctx,a=t.options.fixedDecimalPlaceDigits;for(let s of r)switch(s.type){case"path":i.save(),i.strokeStyle=n.stroke==="none"?"transparent":n.stroke,i.lineWidth=n.strokeWidth,n.strokeLineDash&&i.setLineDash(n.strokeLineDash),n.strokeLineDashOffset&&(i.lineDashOffset=n.strokeLineDashOffset),this._drawToContext(i,s,a),i.restore();break;case"fillPath":{i.save(),i.fillStyle=n.fill||"";let l=t.shape==="curve"||t.shape==="polygon"||t.shape==="path"?"evenodd":"nonzero";this._drawToContext(i,s,a,l),i.restore();break}case"fillSketch":this.fillSketch(i,s,n)}}fillSketch(t,r,n){let i=n.fillWeight;i<0&&(i=n.strokeWidth/2),t.save(),n.fillLineDash&&t.setLineDash(n.fillLineDash),n.fillLineDashOffset&&(t.lineDashOffset=n.fillLineDashOffset),t.strokeStyle=n.fill||"",t.lineWidth=i,this._drawToContext(t,r,n.fixedDecimalPlaceDigits),t.restore()}_drawToContext(t,r,n,i="nonzero"){t.beginPath();for(let a of r.ops){let s=typeof n=="number"&&n>=0?a.data.map((l=>+l.toFixed(n))):a.data;switch(a.op){case"move":t.moveTo(s[0],s[1]);break;case"bcurveTo":t.bezierCurveTo(s[0],s[1],s[2],s[3],s[4],s[5]);break;case"lineTo":t.lineTo(s[0],s[1])}}r.type==="fillPath"?t.fill(i):t.stroke()}get generator(){return this.gen}getDefaultOptions(){return this.gen.defaultOptions}line(t,r,n,i,a){let s=this.gen.line(t,r,n,i,a);return this.draw(s),s}rectangle(t,r,n,i,a){let s=this.gen.rectangle(t,r,n,i,a);return this.draw(s),s}ellipse(t,r,n,i,a){let s=this.gen.ellipse(t,r,n,i,a);return this.draw(s),s}circle(t,r,n,i){let a=this.gen.circle(t,r,n,i);return this.draw(a),a}linearPath(t,r){let n=this.gen.linearPath(t,r);return this.draw(n),n}polygon(t,r){let n=this.gen.polygon(t,r);return this.draw(n),n}arc(t,r,n,i,a,s,l=!1,u){let h=this.gen.arc(t,r,n,i,a,s,l,u);return this.draw(h),h}curve(t,r){let n=this.gen.curve(t,r);return this.draw(n),n}path(t,r){let n=this.gen.path(t,r);return this.draw(n),n}},I4="http://www.w3.org/2000/svg",hO=class{static{o(this,"ot")}constructor(t,r){this.svg=t,this.gen=new zy(r)}draw(t){let r=t.sets||[],n=t.options||this.getDefaultOptions(),i=this.svg.ownerDocument||window.document,a=i.createElementNS(I4,"g"),s=t.options.fixedDecimalPlaceDigits;for(let l of r){let u=null;switch(l.type){case"path":u=i.createElementNS(I4,"path"),u.setAttribute("d",this.opsToPath(l,s)),u.setAttribute("stroke",n.stroke),u.setAttribute("stroke-width",n.strokeWidth+""),u.setAttribute("fill","none"),n.strokeLineDash&&u.setAttribute("stroke-dasharray",n.strokeLineDash.join(" ").trim()),n.strokeLineDashOffset&&u.setAttribute("stroke-dashoffset",`${n.strokeLineDashOffset}`);break;case"fillPath":u=i.createElementNS(I4,"path"),u.setAttribute("d",this.opsToPath(l,s)),u.setAttribute("stroke","none"),u.setAttribute("stroke-width","0"),u.setAttribute("fill",n.fill||""),t.shape!=="curve"&&t.shape!=="polygon"||u.setAttribute("fill-rule","evenodd");break;case"fillSketch":u=this.fillSketch(i,l,n)}u&&a.appendChild(u)}return a}fillSketch(t,r,n){let i=n.fillWeight;i<0&&(i=n.strokeWidth/2);let a=t.createElementNS(I4,"path");return a.setAttribute("d",this.opsToPath(r,n.fixedDecimalPlaceDigits)),a.setAttribute("stroke",n.fill||""),a.setAttribute("stroke-width",i+""),a.setAttribute("fill","none"),n.fillLineDash&&a.setAttribute("stroke-dasharray",n.fillLineDash.join(" ").trim()),n.fillLineDashOffset&&a.setAttribute("stroke-dashoffset",`${n.fillLineDashOffset}`),a}get generator(){return this.gen}getDefaultOptions(){return this.gen.defaultOptions}opsToPath(t,r){return this.gen.opsToPath(t,r)}line(t,r,n,i,a){let s=this.gen.line(t,r,n,i,a);return this.draw(s)}rectangle(t,r,n,i,a){let s=this.gen.rectangle(t,r,n,i,a);return this.draw(s)}ellipse(t,r,n,i,a){let s=this.gen.ellipse(t,r,n,i,a);return this.draw(s)}circle(t,r,n,i){let a=this.gen.circle(t,r,n,i);return this.draw(a)}linearPath(t,r){let n=this.gen.linearPath(t,r);return this.draw(n)}polygon(t,r){let n=this.gen.polygon(t,r);return this.draw(n)}arc(t,r,n,i,a,s,l=!1,u){let h=this.gen.arc(t,r,n,i,a,s,l,u);return this.draw(h)}curve(t,r){let n=this.gen.curve(t,r);return this.draw(n)}path(t,r){let n=this.gen.path(t,r);return this.draw(n)}},ut={canvas:o((e,t)=>new uO(e,t),"canvas"),svg:o((e,t)=>new hO(e,t),"svg"),generator:o(e=>new zy(e),"generator"),newSeed:o(()=>zy.newSeed(),"newSeed")}});function soe(e,t){let{labelStyles:r}=ct(t);t.labelStyle=r;let n=Dt(t),i=n;n||(i="anchor");let a=e.insert("g").attr("class",i).attr("id",t.domId||t.id),s=1,{cssStyles:l}=t,u=ut.svg(a),h=dt(t,{fill:"black",stroke:"none",fillStyle:"solid"});t.look!=="handDrawn"&&(h.roughness=0);let d=u.circle(0,0,s*2,h),f=a.insert(()=>d,":first-child");return f.attr("class","anchor").attr("style",kn(l)),pt(t,f),t.intersect=function(p){return Z.info("Circle intersect",t,s,p),ht.circle(t,s,p)},a}var ooe=F(()=>{"use strict";vt();Kt();nr();Jt();tr();Qt();o(soe,"anchor")});function loe(e,t,r,n,i,a,s){let u=(e+r)/2,h=(t+n)/2,d=Math.atan2(n-t,r-e),f=(r-e)/2,p=(n-t)/2,m=f/i,g=p/a,y=Math.sqrt(m**2+g**2);if(y>1)throw new Error("The given radii are too small to create an arc between the points.");let v=Math.sqrt(1-y**2),x=u+v*a*Math.sin(d)*(s?-1:1),b=h-v*i*Math.cos(d)*(s?-1:1),T=Math.atan2((t-b)/a,(e-x)/i),C=Math.atan2((n-b)/a,(r-x)/i)-T;s&&C<0&&(C+=2*Math.PI),!s&&C>0&&(C-=2*Math.PI);let w=[];for(let S=0;S<20;S++){let R=S/19,L=T+R*C,N=x+i*Math.cos(L),I=b+a*Math.sin(L);w.push({x:N,y:I})}return w}function pQe(e,t,r){let[n,i]=[t,r].sort((a,s)=>s-a);return i*(1-Math.sqrt(1-(e/n/2)**2))}async function coe(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,s=t.look==="neo"?12:i,l=o(L=>L+s,"calcTotalHeight"),u=o(L=>{let N=L/2;return[N/(2.5+L/50),N]},"calcEllipseRadius"),{shapeSvg:h,bbox:d}=await It(e,t,Dt(t)),f=l(t?.height?t?.height:d.height),[p,m]=u(f),g=pQe(f,p,m),v=(t?.width?t?.width:d.width)+a*2+g-g,x=f,{cssStyles:b}=t,T=[{x:v/2,y:-x/2},{x:-v/2,y:-x/2},...loe(-v/2,-x/2,-v/2,x/2,p,m,!1),{x:v/2,y:x/2},...loe(v/2,x/2,v/2,-x/2,p,m,!0)],k=ut.svg(h),C=dt(t,{});t.look!=="handDrawn"&&(C.roughness=0,C.fillStyle="solid");let w=or(T),S=k.path(w,C),R=h.insert(()=>S,":first-child");return R.attr("class","basic label-container outer-path"),b&&t.look!=="handDrawn"&&R.selectAll("path").attr("style",b),n&&t.look!=="handDrawn"&&R.selectAll("path").attr("style",n),R.attr("transform",`translate(${p/2}, 0)`),pt(t,R),t.intersect=function(L){return ht.polygon(t,T,L)},h}var uoe=F(()=>{"use strict";Kt();nr();Jt();tr();o(loe,"generateArcPoints");o(pQe,"calculateArcSagitta");o(coe,"bowTieRect")});function as(e,t,r,n){return e.insert("polygon",":first-child").attr("points",n.map(function(i){return i.x+","+i.y}).join(" ")).attr("class","label-container").attr("transform","translate("+-t/2+","+r/2+")")}var wh=F(()=>{"use strict";o(as,"insertPolygonShape")});async function hoe(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?28:i,s=t.look==="neo"?24:i,{shapeSvg:l,bbox:u}=await It(e,t,Dt(t)),h=(t?.width??u.width)+(t.look==="neo"?a*2:a+F4),d=(t?.height??u.height)+(t.look==="neo"?s*2:s),f=0,p=h,m=-d,g=0,y=[{x:f+F4,y:m},{x:p,y:m},{x:p,y:g},{x:f,y:g},{x:f,y:m+F4},{x:f+F4,y:m}],v,{cssStyles:x}=t;if(t.look==="handDrawn"){let b=ut.svg(l),T=dt(t,{}),k=or(y),C=b.path(k,T);v=l.insert(()=>C,":first-child").attr("transform",`translate(${-h/2}, ${d/2})`),x&&v.attr("style",x)}else v=as(l,h,d,y);return n&&v.attr("style",n),pt(t,v),t.intersect=function(b){return ht.polygon(t,y,b)},l}var F4,doe=F(()=>{"use strict";Kt();nr();Jt();tr();wh();Kt();F4=12;o(hoe,"card")});function foe(e,t){let{nodeStyles:r}=ct(t);t.label="";let n=e.insert("g").attr("class",Dt(t)).attr("id",t.domId??t.id),{cssStyles:i}=t,a=Math.max(28,t.width??0),s=[{x:0,y:a/2},{x:a/2,y:0},{x:0,y:-a/2},{x:-a/2,y:0}],l=ut.svg(n),u=dt(t,{});t.look!=="handDrawn"&&(u.roughness=0,u.fillStyle="solid");let h=or(s),d=l.path(h,u),f=n.insert(()=>d,":first-child");return i&&t.look!=="handDrawn"&&f.selectAll("path").attr("style",i),r&&t.look!=="handDrawn"&&f.selectAll("path").attr("style",r),t.width=28,t.height=28,t.intersect=function(p){return ht.polygon(t,s,p)},n}var poe=F(()=>{"use strict";nr();tr();Jt();Kt();o(foe,"choice")});async function z4(e,t,r){let{labelStyles:n,nodeStyles:i}=ct(t);t.labelStyle=n;let{shapeSvg:a,bbox:s,halfPadding:l}=await It(e,t,Dt(t)),u=16,h=r?.padding??l,d=t.look==="neo"?s.width/2+u*2:s.width/2+h,f,{cssStyles:p}=t;if(t.look==="handDrawn"){let m=ut.svg(a),g=dt(t,{}),y=m.circle(0,0,d*2,g);f=a.insert(()=>y,":first-child"),f.attr("class","basic label-container").attr("style",kn(p))}else f=a.insert("circle",":first-child").attr("class","basic label-container").attr("style",i).attr("r",d).attr("cx",0).attr("cy",0);return pt(t,f),t.calcIntersect=function(m,g){let y=m.width/2;return ht.circle(m,y,g)},t.intersect=function(m){return Z.info("Circle intersect",t,d,m),ht.circle(t,d,m)},a}var fO=F(()=>{"use strict";tr();vt();Qt();nr();Jt();Kt();o(z4,"circle")});function mQe(e){let t=Math.cos(Math.PI/4),r=Math.sin(Math.PI/4),n=e*2,i={x:n/2*t,y:n/2*r},a={x:-(n/2)*t,y:n/2*r},s={x:-(n/2)*t,y:-(n/2)*r},l={x:n/2*t,y:-(n/2)*r};return`M ${a.x},${a.y} L ${l.x},${l.y} + M ${i.x},${i.y} L ${s.x},${s.y}`}function moe(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r,t.label="";let i=e.insert("g").attr("class",Dt(t)).attr("id",t.domId??t.id),a=Math.max(30,t?.width??0),{cssStyles:s}=t,l=ut.svg(i),u=dt(t,{});t.look!=="handDrawn"&&(u.roughness=0,u.fillStyle="solid");let h=l.circle(0,0,a*2,u),d=mQe(a),f=l.path(d,u),p=i.insert(()=>h,":first-child");return p.insert(()=>f),p.attr("class","outer-path"),s&&t.look!=="handDrawn"&&p.selectAll("path").attr("style",s),n&&t.look!=="handDrawn"&&p.selectAll("path").attr("style",n),pt(t,p),t.intersect=function(m){return Z.info("crossedCircle intersect",t,{radius:a,point:m}),ht.circle(t,a,m)},i}var goe=F(()=>{"use strict";vt();Kt();Jt();tr();nr();o(mQe,"createLine");o(moe,"crossedCircle")});function tf(e,t,r,n=100,i=0,a=180){let s=[],l=i*Math.PI/180,d=(a*Math.PI/180-l)/(n-1);for(let f=0;fC,":first-child").attr("stroke-opacity",0),w.insert(()=>T,":first-child"),w.attr("class","text"),p&&t.look!=="handDrawn"&&w.selectAll("path").attr("style",p),n&&t.look!=="handDrawn"&&w.selectAll("path").attr("style",n),w.attr("transform",`translate(${f}, 0)`),s.attr("transform",`translate(${-h/2+f-(a.x-(a.left??0))},${-d/2+(t.padding??0)/2-(a.y-(a.top??0))})`),pt(t,w),t.intersect=function(S){return ht.polygon(t,g,S)},i}var voe=F(()=>{"use strict";Kt();nr();Jt();tr();o(tf,"generateCirclePoints");o(yoe,"curlyBraceLeft")});function rf(e,t,r,n=100,i=0,a=180){let s=[],l=i*Math.PI/180,d=(a*Math.PI/180-l)/(n-1);for(let f=0;fC,":first-child").attr("stroke-opacity",0),w.insert(()=>T,":first-child"),w.attr("class","text"),p&&t.look!=="handDrawn"&&w.selectAll("path").attr("style",p),n&&t.look!=="handDrawn"&&w.selectAll("path").attr("style",n),w.attr("transform",`translate(${-f}, 0)`),s.attr("transform",`translate(${-h/2+(t.padding??0)/2-(a.x-(a.left??0))},${-d/2+(t.padding??0)/2-(a.y-(a.top??0))})`),pt(t,w),t.intersect=function(S){return ht.polygon(t,g,S)},i}var boe=F(()=>{"use strict";Kt();nr();Jt();tr();o(rf,"generateCirclePoints");o(xoe,"curlyBraceRight")});function ss(e,t,r,n=100,i=0,a=180){let s=[],l=i*Math.PI/180,d=(a*Math.PI/180-l)/(n-1);for(let f=0;fL,":first-child").attr("stroke-opacity",0),N.insert(()=>k,":first-child"),N.insert(()=>S,":first-child"),N.attr("class","text"),p&&t.look!=="handDrawn"&&N.selectAll("path").attr("style",p),n&&t.look!=="handDrawn"&&N.selectAll("path").attr("style",n),N.attr("transform",`translate(${f-f/4}, 0)`),s.attr("transform",`translate(${-h/2+(t.padding??0)/2-(a.x-(a.left??0))},${-d/2+(t.padding??0)/2-(a.y-(a.top??0))})`),pt(t,N),t.intersect=function(I){return ht.polygon(t,y,I)},i}var Coe=F(()=>{"use strict";Kt();nr();Jt();tr();o(ss,"generateCirclePoints");o(Toe,"curlyBraces")});async function woe(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,s=t.look==="neo"?12:i,l=20,u=5,{shapeSvg:h,bbox:d}=await It(e,t,Dt(t)),f=Math.max(l,(d.width+a*2)*1.25,t?.width??0),p=Math.max(u,d.height+s*2,t?.height??0),m=p/2,{cssStyles:g}=t,y=ut.svg(h),v=dt(t,{});t.look!=="handDrawn"&&(v.roughness=0,v.fillStyle="solid");let x=f,b=p,T=x-m,k=b/4,C=[{x:T,y:0},{x:k,y:0},{x:0,y:b/2},{x:k,y:b},{x:T,y:b},...wm(-T,-b/2,m,50,270,90)],w=or(C),S=y.path(w,v),R=h.insert(()=>S,":first-child");return R.attr("class","basic label-container outer-path"),g&&t.look!=="handDrawn"&&R.selectChildren("path").attr("style",g),n&&t.look!=="handDrawn"&&R.selectChildren("path").attr("style",n),R.attr("transform",`translate(${-f/2}, ${-p/2})`),pt(t,R),t.intersect=function(L){return ht.polygon(t,C,L)},h}var koe=F(()=>{"use strict";Kt();nr();Jt();tr();o(woe,"curvedTrapezoid")});async function Aoe(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?24:i,s=t.look==="neo"?24:i;if(t.width||t.height){let v=t.width??0;t.width=(t.width??0)-s,t.widthC,":first-child"),g=l.insert(()=>k,":first-child"),g.attr("class","basic label-container"),y&&g.attr("style",y)}else{let v=gQe(0,0,d,m,f,p);g=l.insert("path",":first-child").attr("d",v).attr("class","basic label-container outer-path").attr("style",kn(y)).attr("style",n)}return g.attr("label-offset-y",p),g.attr("transform",`translate(${-d/2}, ${-(m/2+p)})`),pt(t,g),h.attr("transform",`translate(${-(u.width/2)-(u.x-(u.left??0))}, ${-(u.height/2)+(t.padding??0)/1.5-(u.y-(u.top??0))})`),t.intersect=function(v){let x=ht.rect(t,v),b=x.x-(t.x??0);if(f!=0&&(Math.abs(b)<(t.width??0)/2||Math.abs(b)==(t.width??0)/2&&Math.abs(x.y-(t.y??0))>(t.height??0)/2-p)){let T=p*p*(1-b*b/(f*f));T>0&&(T=Math.sqrt(T)),T=p-T,v.y-(t.y??0)>0&&(T=-T),x.y+=T}return x},l}var gQe,yQe,vQe,Soe,Eoe,Roe=F(()=>{"use strict";Kt();nr();Jt();tr();Qt();gQe=o((e,t,r,n,i,a)=>[`M${e},${t+a}`,`a${i},${a} 0,0,0 ${r},0`,`a${i},${a} 0,0,0 ${-r},0`,`l0,${n}`,`a${i},${a} 0,0,0 ${r},0`,`l0,${-n}`].join(" "),"createCylinderPathD"),yQe=o((e,t,r,n,i,a)=>[`M${e},${t+a}`,`M${e+r},${t+a}`,`a${i},${a} 0,0,0 ${-r},0`,`l0,${n}`,`a${i},${a} 0,0,0 ${r},0`,`l0,${-n}`].join(" "),"createOuterCylinderPathD"),vQe=o((e,t,r,n,i,a)=>[`M${e-r/2},${-n/2}`,`a${i},${a} 0,0,0 ${r},0`].join(" "),"createInnerCylinderPathD"),Soe=8,Eoe=8;o(Aoe,"cylinder")});var io,Sm=F(()=>{"use strict";io=o((e,t,r,n,i)=>["M",e+i,t,"H",e+r-i,"A",i,i,0,0,1,e+r,t+i,"V",t+n-i,"A",i,i,0,0,1,e+r-i,t+n,"H",e+i,"A",i,i,0,0,1,e,t+n-i,"V",t+i,"A",i,i,0,0,1,e+i,t,"Z"].join(" "),"createRoundedRectPathD")});async function ac(e,t,r){let{labelStyles:n,nodeStyles:i}=ct(t);t.labelStyle=n;let{shapeSvg:a,bbox:s}=await It(e,t,Dt(t)),l=Math.max(s.width+r.labelPaddingX*2,t?.width||0),u=Math.max(s.height+r.labelPaddingY*2,t?.height||0),h=-l/2,d=-u/2,f,{rx:p,ry:m}=t,{cssStyles:g}=t;if(r?.rx&&r.ry&&(p=r.rx,m=r.ry),t.look==="handDrawn"){let y=ut.svg(a),v=dt(t,{}),x=p||m?y.path(io(h,d,l,u,p||0),v):y.rectangle(h,d,l,u,v);f=a.insert(()=>x,":first-child"),f.attr("class","basic label-container").attr("style",kn(g))}else f=a.insert("rect",":first-child"),f.attr("class","basic label-container").attr("style",i).attr("rx",kn(p)).attr("ry",kn(m)).attr("x",h).attr("y",d).attr("width",l).attr("height",u);return pt(t,f),t.calcIntersect=function(y,v){return ht.rect(y,v)},t.intersect=function(y){return ht.rect(t,y)},a}var Em=F(()=>{"use strict";Kt();nr();Sm();Jt();tr();Qt();o(ac,"drawRect")});async function _oe(e,t){let{cssClasses:r,labelPaddingX:n,labelPaddingY:i,padding:a,width:s,height:l}=t,u={rx:0,ry:0,classes:r??"",labelPaddingX:n??(a??0)*2,labelPaddingY:i??a??0},h=await ac(e,t,u);if(t.look==="handDrawn"){let m=ut.svg(h),g=dt(t,{}),y=h.select(".basic.label-container > path:nth-child(2)"),v=y.node();if(!v)return h;let x=null;if(v instanceof SVGGraphicsElement)x=v.getBBox();else return h;return h.insert(()=>m.line(x.x,x.y,x.x+x.width,x.y,g),".basic.label-container g.label"),h.insert(()=>m.line(x.x,x.y+x.height,x.x+x.width,x.y+x.height,g),".basic.label-container g.label"),y.remove(),h}let d=h.select(".basic.label-container"),f=(Number(d.attr("width"))||s)??0,p=(Number(d.attr("height"))||l)??0;return f>0&&p>0&&d.attr("stroke-dasharray",`${f} ${p}`),h}var Loe=F(()=>{"use strict";Em();Jt();tr();o(_oe,"datastore")});async function Doe(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.look==="neo"?16:t.padding??0,a=t.look==="neo"?16:t.padding??0,{shapeSvg:s,bbox:l,label:u}=await It(e,t,Dt(t)),h=l.width+i,d=l.height+a,f=d*.2,p=-h/2,m=-d/2-f/2,{cssStyles:g}=t,y=ut.svg(s),v=dt(t,{});t.look!=="handDrawn"&&(v.roughness=0,v.fillStyle="solid");let x=[{x:p,y:m+f},{x:-p,y:m+f},{x:-p,y:-m},{x:p,y:-m},{x:p,y:m},{x:-p,y:m},{x:-p,y:m+f}],b=y.polygon(x.map(k=>[k.x,k.y]),v),T=s.insert(()=>b,":first-child");return T.attr("class","basic label-container outer-path"),g&&t.look!=="handDrawn"&&T.selectAll("path").attr("style",g),n&&t.look!=="handDrawn"&&T.selectAll("path").attr("style",n),u.attr("transform",`translate(${p+(t.padding??0)/2-(l.x-(l.left??0))}, ${m+f+(t.padding??0)/2-(l.y-(l.top??0))})`),pt(t,T),t.intersect=function(k){return ht.rect(t,k)},s}var Ioe=F(()=>{"use strict";Kt();nr();Jt();tr();o(Doe,"dividedRectangle")});async function Moe(e,t){let{labelStyles:r,nodeStyles:n}=ct(t),i=t.look==="neo"?12:5;t.labelStyle=r;let a=t.padding??0,s=t.look==="neo"?16:a,{shapeSvg:l,bbox:u}=await It(e,t,Dt(t)),h=(t?.width?t?.width/2:u.width/2)+(s??0),d=h-i,f,{cssStyles:p}=t;if(t.look==="handDrawn"){let m=ut.svg(l),g=dt(t,{roughness:.2,strokeWidth:2.5}),y=dt(t,{roughness:.2,strokeWidth:1.5}),v=m.circle(0,0,h*2,g),x=m.circle(0,0,d*2,y);f=l.insert("g",":first-child"),f.attr("class",kn(t.cssClasses)).attr("style",kn(p)),f.node()?.appendChild(v),f.node()?.appendChild(x)}else{f=l.insert("g",":first-child");let m=f.insert("circle",":first-child"),g=f.insert("circle");f.attr("class","basic label-container").attr("style",n),m.attr("class","outer-circle").attr("style",n).attr("r",h).attr("cx",0).attr("cy",0),g.attr("class","inner-circle").attr("style",n).attr("r",d).attr("cx",0).attr("cy",0)}return pt(t,f),t.intersect=function(m){return Z.info("DoubleCircle intersect",t,h,m),ht.circle(t,h,m)},l}var Noe=F(()=>{"use strict";vt();Kt();nr();Jt();tr();Qt();o(Moe,"doublecircle")});function Poe(e,t,{config:{themeVariables:r}}){let{labelStyles:n,nodeStyles:i}=ct(t);t.label="",t.labelStyle=n;let a=e.insert("g").attr("class",Dt(t)).attr("id",t.domId??t.id),s=7,{cssStyles:l}=t,u=ut.svg(a),{nodeBorder:h}=r,d=dt(t,{fillStyle:"solid"});t.look!=="handDrawn"&&(d.roughness=0);let f=u.circle(0,0,s*2,d),p=a.insert(()=>f,":first-child");return p.selectAll("path").attr("style",`fill: ${h} !important;`),l&&l.length>0&&t.look!=="handDrawn"&&p.selectAll("path").attr("style",l),i&&t.look!=="handDrawn"&&p.selectAll("path").attr("style",i),pt(t,p),t.intersect=function(m){return Z.info("filledCircle intersect",t,{radius:s,point:m}),ht.circle(t,s,m)},a}var Ooe=F(()=>{"use strict";tr();vt();nr();Jt();Kt();o(Poe,"filledCircle")});async function Foe(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?i*2:i;(t.width||t.height)&&(t.height=t?.height??0,t.heightx,":first-child").attr("transform",`translate(${-d/2}, ${d/2})`).attr("class","outer-path");return m&&t.look!=="handDrawn"&&b.selectChildren("path").attr("style",m),n&&t.look!=="handDrawn"&&b.selectChildren("path").attr("style",n),t.width=h,t.height=d,pt(t,b),u.attr("transform",`translate(${-l.width/2-(l.x-(l.left??0))}, ${-d/2+(t.padding??0)/2+(l.y-(l.top??0))})`),t.intersect=function(T){return Z.info("Triangle intersect",t,p,T),ht.polygon(t,p,T)},s}var Boe,$oe,zoe=F(()=>{"use strict";vt();Kt();nr();Jt();tr();Kt();Boe=10,$oe=10;o(Foe,"flippedTriangle")});function Goe(e,t,{dir:r,config:{state:n,themeVariables:i}}){let{nodeStyles:a}=ct(t);t.label="";let s=e.insert("g").attr("class",Dt(t)).attr("id",t.domId??t.id),{cssStyles:l}=t,u=Math.max(70,t?.width??0),h=Math.max(10,t?.height??0);r==="LR"&&(u=Math.max(10,t?.width??0),h=Math.max(70,t?.height??0));let d=-1*u/2,f=-1*h/2,p=ut.svg(s),m=dt(t,{stroke:i.lineColor,fill:i.lineColor});t.look!=="handDrawn"&&(m.roughness=0,m.fillStyle="solid");let g=p.rectangle(d,f,u,h,m),y=s.insert(()=>g,":first-child");l&&t.look!=="handDrawn"&&y.selectAll("path").attr("style",l),a&&t.look!=="handDrawn"&&y.selectAll("path").attr("style",a),pt(t,y);let v=n?.padding??0;return t.width&&t.height&&(t.width+=v/2||0,t.height+=v/2||0),t.intersect=function(x){return ht.rect(t,x)},s}var Voe=F(()=>{"use strict";tr();nr();Jt();Kt();o(Goe,"forkJoin")});async function Woe(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=15,a=10,s=t.look==="neo"?16:t.padding??0,l=t.look==="neo"?12:t.padding??0;(t.width||t.height)&&(t.height=(t?.height??0)-l*2,t.heightb,":first-child");return T.attr("class","basic label-container outer-path"),m&&t.look!=="handDrawn"&&T.selectChildren("path").attr("style",m),n&&t.look!=="handDrawn"&&T.selectChildren("path").attr("style",n),pt(t,T),t.intersect=function(k){return Z.info("Pill intersect",t,{radius:p,point:k}),ht.polygon(t,v,k)},u}var qoe=F(()=>{"use strict";vt();Kt();nr();Jt();tr();o(Woe,"halfRoundedRectangle")});async function Hoe(e,t){let{labelStyles:r,nodeStyles:n}=ct(t),i=t.look==="neo"?3.5:4;t.labelStyle=r;let a=t.padding??0,s=70,l=32,u=t.look==="neo"?s:a,h=t.look==="neo"?l:a;if(t.width||t.height){let T=(t.height??0)/i;t.width=(t?.width??0)-2*T-h,t.height=(t.height??0)-u}let{shapeSvg:d,bbox:f}=await It(e,t,Dt(t)),p=(t?.height?t?.height:f.height)+u,m=p/i,g=(t?.width?t?.width:f.width)+2*m+h,y=[{x:m,y:0},{x:g-m,y:0},{x:g,y:-p/2},{x:g-m,y:-p},{x:m,y:-p},{x:0,y:-p/2}],v,{cssStyles:x}=t;if(t.look==="handDrawn"){let b=ut.svg(d),T=dt(t,{}),k=xQe(0,0,g,p,m),C=b.path(k,T);v=d.insert(()=>C,":first-child").attr("transform",`translate(${-g/2}, ${p/2})`),x&&v.attr("style",x)}else v=as(d,g,p,y);return n&&v.attr("style",n),t.width=g,t.height=p,pt(t,v),t.intersect=function(b){return ht.polygon(t,y,b)},d}var xQe,Uoe=F(()=>{"use strict";Kt();nr();Jt();tr();wh();xQe=o((e,t,r,n,i)=>[`M${e+i},${t}`,`L${e+r-i},${t}`,`L${e+r},${t-n/2}`,`L${e+r-i},${t-n}`,`L${e+i},${t-n}`,`L${e},${t-n/2}`,"Z"].join(" "),"createHexagonPathD");o(Hoe,"hexagon")});async function Yoe(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.label="",t.labelStyle=r;let{shapeSvg:i}=await It(e,t,Dt(t)),a=Math.max(30,t?.width??0),s=Math.max(30,t?.height??0),{cssStyles:l}=t,u=ut.svg(i),h=dt(t,{});t.look!=="handDrawn"&&(h.roughness=0,h.fillStyle="solid");let d=[{x:0,y:0},{x:a,y:0},{x:0,y:s},{x:a,y:s}],f=or(d),p=u.path(f,h),m=i.insert(()=>p,":first-child");return m.attr("class","basic label-container outer-path"),l&&t.look!=="handDrawn"&&m.selectChildren("path").attr("style",l),n&&t.look!=="handDrawn"&&m.selectChildren("path").attr("style",n),m.attr("transform",`translate(${-a/2}, ${-s/2})`),pt(t,m),t.intersect=function(g){return Z.info("Pill intersect",t,{points:d}),ht.polygon(t,d,g)},i}var joe=F(()=>{"use strict";vt();Kt();nr();Jt();tr();o(Yoe,"hourglass")});async function Xoe(e,t,{config:{themeVariables:r,flowchart:n}}){let{labelStyles:i}=ct(t);t.labelStyle=i;let a=t.assetHeight??48,s=t.assetWidth??48,l=Math.max(a,s),u=n?.wrappingWidth;t.width=Math.max(l,u??0);let{shapeSvg:h,bbox:d,label:f}=await It(e,t,"icon-shape default"),p=t.pos==="t",m=l,g=l,{nodeBorder:y}=r,{stylesMap:v}=iu(t),x=-g/2,b=-m/2,T=t.label?8:0,k=ut.svg(h),C=dt(t,{stroke:"none",fill:"none"});t.look!=="handDrawn"&&(C.roughness=0,C.fillStyle="solid");let w=k.rectangle(x,b,g,m,C),S=Math.max(g,d.width),R=m+d.height+T,L=k.rectangle(-S/2,-R/2,S,R,{...C,fill:"transparent",stroke:"none"}),N=h.insert(()=>w,":first-child"),I=h.insert(()=>L);if(t.icon){let _=h.append("g");_.html(`${await ts(t.icon,{height:l,width:l,fallbackPrefix:""})}`);let A=_.node().getBBox(),M=A.width,D=A.height,P=A.x,B=A.y;_.attr("transform",`translate(${-M/2-P},${p?d.height/2+T/2-D/2-B:-d.height/2-T/2-D/2-B})`),_.attr("style",`color: ${v.get("stroke")??y};`)}return f.attr("transform",`translate(${-d.width/2-(d.x-(d.left??0))},${p?-R/2:R/2-d.height})`),N.attr("transform",`translate(0,${p?d.height/2+T/2:-d.height/2-T/2})`),pt(t,I),t.intersect=function(_){if(Z.info("iconSquare intersect",t,_),!t.label)return ht.rect(t,_);let A=t.x??0,M=t.y??0,D=t.height??0,P=[];return p?P=[{x:A-d.width/2,y:M-D/2},{x:A+d.width/2,y:M-D/2},{x:A+d.width/2,y:M-D/2+d.height+T},{x:A+g/2,y:M-D/2+d.height+T},{x:A+g/2,y:M+D/2},{x:A-g/2,y:M+D/2},{x:A-g/2,y:M-D/2+d.height+T},{x:A-d.width/2,y:M-D/2+d.height+T}]:P=[{x:A-g/2,y:M-D/2},{x:A+g/2,y:M-D/2},{x:A+g/2,y:M-D/2+m},{x:A+d.width/2,y:M-D/2+m},{x:A+d.width/2/2,y:M+D/2},{x:A-d.width/2,y:M+D/2},{x:A-d.width/2,y:M-D/2+m},{x:A-g/2,y:M-D/2+m}],ht.polygon(t,P,_)},h}var Koe=F(()=>{"use strict";tr();vt();Vl();nr();Jt();Kt();o(Xoe,"icon")});async function Zoe(e,t,{config:{themeVariables:r,flowchart:n}}){let{labelStyles:i}=ct(t);t.labelStyle=i;let a=t.assetHeight??48,s=t.assetWidth??48,l=Math.max(a,s),u=n?.wrappingWidth;t.width=Math.max(l,u??0);let{shapeSvg:h,bbox:d,label:f}=await It(e,t,"icon-shape default"),p=20,m=t.label?8:0,g=t.pos==="t",{nodeBorder:y,mainBkg:v}=r,{stylesMap:x}=iu(t),b=ut.svg(h),T=dt(t,{});t.look!=="handDrawn"&&(T.roughness=0,T.fillStyle="solid");let k=x.get("fill");T.stroke=k??v;let C=h.append("g");t.icon&&C.html(`${await ts(t.icon,{height:l,width:l,fallbackPrefix:""})}`);let w=C.node().getBBox(),S=w.width,R=w.height,L=w.x,N=w.y,I=Math.max(S,R)*Math.SQRT2+p*2,_=b.circle(0,0,I,T),A=Math.max(I,d.width),M=I+d.height+m,D=b.rectangle(-A/2,-M/2,A,M,{...T,fill:"transparent",stroke:"none"}),P=h.insert(()=>_,":first-child"),B=h.insert(()=>D);return C.attr("transform",`translate(${-S/2-L},${g?d.height/2+m/2-R/2-N:-d.height/2-m/2-R/2-N})`),C.attr("style",`color: ${x.get("stroke")??y};`),f.attr("transform",`translate(${-d.width/2-(d.x-(d.left??0))},${g?-M/2:M/2-d.height})`),P.attr("transform",`translate(0,${g?d.height/2+m/2:-d.height/2-m/2})`),pt(t,B),t.intersect=function(O){return Z.info("iconSquare intersect",t,O),ht.rect(t,O)},h}var Qoe=F(()=>{"use strict";tr();vt();Vl();nr();Jt();Kt();o(Zoe,"iconCircle")});async function Joe(e,t,{config:{themeVariables:r,flowchart:n}}){let{labelStyles:i}=ct(t);t.labelStyle=i;let a=t.assetHeight??48,s=t.assetWidth??48,l=Math.max(a,s),u=n?.wrappingWidth;t.width=Math.max(l,u??0);let{shapeSvg:h,bbox:d,halfPadding:f,label:p}=await It(e,t,"icon-shape default"),m=t.pos==="t",g=l+f*2,y=l+f*2,{nodeBorder:v,mainBkg:x}=r,{stylesMap:b}=iu(t),T=-y/2,k=-g/2,C=t.label?8:0,w=ut.svg(h),S=dt(t,{});t.look!=="handDrawn"&&(S.roughness=0,S.fillStyle="solid");let R=b.get("fill");S.stroke=R??x;let L=w.path(io(T,k,y,g,5),S),N=Math.max(y,d.width),I=g+d.height+C,_=w.rectangle(-N/2,-I/2,N,I,{...S,fill:"transparent",stroke:"none"}),A=h.insert(()=>L,":first-child").attr("class","icon-shape2"),M=h.insert(()=>_);if(t.icon){let D=h.append("g");D.html(`${await ts(t.icon,{height:l,width:l,fallbackPrefix:""})}`);let P=D.node().getBBox(),B=P.width,O=P.height,$=P.x,V=P.y;D.attr("transform",`translate(${-B/2-$},${m?d.height/2+C/2-O/2-V:-d.height/2-C/2-O/2-V})`),D.attr("style",`color: ${b.get("stroke")??v};`)}return p.attr("transform",`translate(${-d.width/2-(d.x-(d.left??0))},${m?-I/2:I/2-d.height})`),A.attr("transform",`translate(0,${m?d.height/2+C/2:-d.height/2-C/2})`),pt(t,M),t.intersect=function(D){if(Z.info("iconSquare intersect",t,D),!t.label)return ht.rect(t,D);let P=t.x??0,B=t.y??0,O=t.height??0,$=[];return m?$=[{x:P-d.width/2,y:B-O/2},{x:P+d.width/2,y:B-O/2},{x:P+d.width/2,y:B-O/2+d.height+C},{x:P+y/2,y:B-O/2+d.height+C},{x:P+y/2,y:B+O/2},{x:P-y/2,y:B+O/2},{x:P-y/2,y:B-O/2+d.height+C},{x:P-d.width/2,y:B-O/2+d.height+C}]:$=[{x:P-y/2,y:B-O/2},{x:P+y/2,y:B-O/2},{x:P+y/2,y:B-O/2+g},{x:P+d.width/2,y:B-O/2+g},{x:P+d.width/2/2,y:B+O/2},{x:P-d.width/2,y:B+O/2},{x:P-d.width/2,y:B-O/2+g},{x:P-y/2,y:B-O/2+g}],ht.polygon(t,$,D)},h}var ele=F(()=>{"use strict";tr();vt();Vl();nr();Jt();Sm();Kt();o(Joe,"iconRounded")});async function tle(e,t,{config:{themeVariables:r,flowchart:n}}){let{labelStyles:i}=ct(t);t.labelStyle=i;let a=t.assetHeight??48,s=t.assetWidth??48,l=Math.max(a,s),u=n?.wrappingWidth;t.width=Math.max(l,u??0);let{shapeSvg:h,bbox:d,halfPadding:f,label:p}=await It(e,t,"icon-shape default"),m=t.pos==="t",g=l+f*2,y=l+f*2,{nodeBorder:v,mainBkg:x}=r,{stylesMap:b}=iu(t),T=-y/2,k=-g/2,C=t.label?8:0,w=ut.svg(h),S=dt(t,{});t.look!=="handDrawn"&&(S.roughness=0,S.fillStyle="solid");let R=b.get("fill");S.stroke=R??x;let L=w.path(io(T,k,y,g,.1),S),N=Math.max(y,d.width),I=g+d.height+C,_=w.rectangle(-N/2,-I/2,N,I,{...S,fill:"transparent",stroke:"none"}),A=h.insert(()=>L,":first-child"),M=h.insert(()=>_);if(t.icon){let D=h.append("g");D.html(`${await ts(t.icon,{height:l,width:l,fallbackPrefix:""})}`);let P=D.node().getBBox(),B=P.width,O=P.height,$=P.x,V=P.y;D.attr("transform",`translate(${-B/2-$},${m?d.height/2+C/2-O/2-V:-d.height/2-C/2-O/2-V})`),D.attr("style",`color: ${b.get("stroke")??v};`)}return p.attr("transform",`translate(${-d.width/2-(d.x-(d.left??0))},${m?-I/2:I/2-d.height})`),A.attr("transform",`translate(0,${m?d.height/2+C/2:-d.height/2-C/2})`),pt(t,M),t.intersect=function(D){if(Z.info("iconSquare intersect",t,D),!t.label)return ht.rect(t,D);let P=t.x??0,B=t.y??0,O=t.height??0,$=[];return m?$=[{x:P-d.width/2,y:B-O/2},{x:P+d.width/2,y:B-O/2},{x:P+d.width/2,y:B-O/2+d.height+C},{x:P+y/2,y:B-O/2+d.height+C},{x:P+y/2,y:B+O/2},{x:P-y/2,y:B+O/2},{x:P-y/2,y:B-O/2+d.height+C},{x:P-d.width/2,y:B-O/2+d.height+C}]:$=[{x:P-y/2,y:B-O/2},{x:P+y/2,y:B-O/2},{x:P+y/2,y:B-O/2+g},{x:P+d.width/2,y:B-O/2+g},{x:P+d.width/2/2,y:B+O/2},{x:P-d.width/2,y:B+O/2},{x:P-d.width/2,y:B-O/2+g},{x:P-y/2,y:B-O/2+g}],ht.polygon(t,$,D)},h}var rle=F(()=>{"use strict";tr();vt();Vl();nr();Sm();Jt();Kt();o(tle,"iconSquare")});async function nle(e,t,{config:{flowchart:r}}){let n=new Image;n.src=t?.img??"",await n.decode();let i=Number(n.naturalWidth.toString().replace("px","")),a=Number(n.naturalHeight.toString().replace("px",""));t.imageAspectRatio=i/a;let{labelStyles:s}=ct(t);t.labelStyle=s;let l=r?.wrappingWidth;t.defaultWidth=r?.wrappingWidth;let u=Math.max(t.label?l??0:0,t?.assetWidth??i),h=t.constraint==="on"&&t?.assetHeight?t.assetHeight*t.imageAspectRatio:u,d=t.constraint==="on"?h/t.imageAspectRatio:t?.assetHeight??a;t.width=Math.max(h,l??0);let{shapeSvg:f,bbox:p,label:m}=await It(e,t,"image-shape default"),g=t.pos==="t",y=-h/2,v=-d/2,x=t.label?8:0,b=ut.svg(f),T=dt(t,{});t.look!=="handDrawn"&&(T.roughness=0,T.fillStyle="solid");let k=b.rectangle(y,v,h,d,T),C=Math.max(h,p.width),w=d+p.height+x,S=b.rectangle(-C/2,-w/2,C,w,{...T,fill:"none",stroke:"none"}),R=f.insert(()=>k,":first-child"),L=f.insert(()=>S);if(t.img){let N=f.append("image");N.attr("href",t.img),N.attr("width",h),N.attr("height",d),N.attr("preserveAspectRatio","none"),N.attr("transform",`translate(${-h/2},${g?w/2-d:-w/2})`)}return m.attr("transform",`translate(${-p.width/2-(p.x-(p.left??0))},${g?-d/2-p.height/2-x/2:d/2-p.height/2+x/2})`),R.attr("transform",`translate(0,${g?p.height/2+x/2:-p.height/2-x/2})`),pt(t,L),t.intersect=function(N){if(Z.info("iconSquare intersect",t,N),!t.label)return ht.rect(t,N);let I=t.x??0,_=t.y??0,A=t.height??0,M=[];return g?M=[{x:I-p.width/2,y:_-A/2},{x:I+p.width/2,y:_-A/2},{x:I+p.width/2,y:_-A/2+p.height+x},{x:I+h/2,y:_-A/2+p.height+x},{x:I+h/2,y:_+A/2},{x:I-h/2,y:_+A/2},{x:I-h/2,y:_-A/2+p.height+x},{x:I-p.width/2,y:_-A/2+p.height+x}]:M=[{x:I-h/2,y:_-A/2},{x:I+h/2,y:_-A/2},{x:I+h/2,y:_-A/2+d},{x:I+p.width/2,y:_-A/2+d},{x:I+p.width/2/2,y:_+A/2},{x:I-p.width/2,y:_+A/2},{x:I-p.width/2,y:_-A/2+d},{x:I-h/2,y:_-A/2+d}],ht.polygon(t,M,N)},f}var ile=F(()=>{"use strict";tr();vt();nr();Jt();Kt();o(nle,"imageSquare")});async function ale(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=i,s=t.look==="neo"?i*2:i,{shapeSvg:l,bbox:u}=await It(e,t,Dt(t)),h=Math.max(u.width+(s??0)*2,t?.width??0),d=Math.max(u.height+(a??0)*2,t?.height??0),f=[{x:0,y:0},{x:h,y:0},{x:h+3*d/6,y:-d},{x:-3*d/6,y:-d}],p,{cssStyles:m}=t;if(t.look==="handDrawn"){let g=ut.svg(l),y=dt(t,{}),v=or(f),x=g.path(v,y);p=l.insert(()=>x,":first-child").attr("transform",`translate(${-h/2}, ${d/2})`),m&&p.attr("style",m)}else p=as(l,h,d,f);return n&&p.attr("style",n),t.width=h,t.height=d,pt(t,p),t.intersect=function(g){return ht.polygon(t,f,g)},l}var sle=F(()=>{"use strict";Kt();nr();Jt();tr();wh();o(ale,"inv_trapezoid")});async function ole(e,t){let{shapeSvg:r,bbox:n,label:i}=await It(e,t,"label"),a=r.insert("rect",":first-child");return a.attr("width",.1).attr("height",.1),r.attr("class","label edgeLabel"),i.attr("transform",`translate(${-(n.width/2)-(n.x-(n.left??0))}, ${-(n.height/2)-(n.y-(n.top??0))})`),pt(t,a),t.intersect=function(u){return ht.rect(t,u)},r}var lle=F(()=>{"use strict";Em();Kt();nr();o(ole,"labelRect")});async function cle(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=i,s=t.look==="neo"?i*2:i,{shapeSvg:l,bbox:u}=await It(e,t,Dt(t)),h=(t?.height??u.height)+a,d=(t?.width??u.width)+s,f=[{x:0,y:0},{x:d+3*h/6,y:0},{x:d,y:-h},{x:-(3*h)/6,y:-h}],p,{cssStyles:m}=t;if(t.look==="handDrawn"){let g=ut.svg(l),y=dt(t,{}),v=or(f),x=g.path(v,y);p=l.insert(()=>x,":first-child").attr("transform",`translate(${-d/2}, ${h/2})`),m&&p.attr("style",m)}else p=as(l,d,h,f);return n&&p.attr("style",n),t.width=d,t.height=h,pt(t,p),t.intersect=function(g){return ht.polygon(t,f,g)},l}var ule=F(()=>{"use strict";Kt();nr();Jt();tr();wh();o(cle,"lean_left")});async function hle(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=i,s=t.look==="neo"?i*2:i,{shapeSvg:l,bbox:u}=await It(e,t,Dt(t)),h=(t?.height??u.height)+a,d=(t?.width??u.width)+s,f=[{x:-3*h/6,y:0},{x:d,y:0},{x:d+3*h/6,y:-h},{x:0,y:-h}],p,{cssStyles:m}=t;if(t.look==="handDrawn"){let g=ut.svg(l),y=dt(t,{}),v=or(f),x=g.path(v,y);p=l.insert(()=>x,":first-child").attr("transform",`translate(${-d/2}, ${h/2})`),m&&p.attr("style",m)}else p=as(l,d,h,f);return n&&p.attr("style",n),t.width=d,t.height=h,pt(t,p),t.intersect=function(g){return ht.polygon(t,f,g)},l}var dle=F(()=>{"use strict";Kt();nr();Jt();tr();wh();o(hle,"lean_right")});function fle(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.label="",t.labelStyle=r;let i=e.insert("g").attr("class",Dt(t)).attr("id",t.domId??t.id),{cssStyles:a}=t,s=Math.max(35,t?.width??0),l=Math.max(35,t?.height??0),u=7,h=[{x:s,y:0},{x:0,y:l+u/2},{x:s-2*u,y:l+u/2},{x:0,y:2*l},{x:s,y:l-u/2},{x:2*u,y:l-u/2}],d=ut.svg(i),f=dt(t,{});t.look!=="handDrawn"&&(f.roughness=0,f.fillStyle="solid");let p=or(h),m=d.path(p,f),g=i.insert(()=>m,":first-child");return g.attr("class","outer-path"),a&&t.look!=="handDrawn"&&g.selectAll("path").attr("style",a),n&&t.look!=="handDrawn"&&g.selectAll("path").attr("style",n),g.attr("transform",`translate(-${s/2},${-l})`),pt(t,g),t.intersect=function(y){return Z.info("lightningBolt intersect",t,y),ht.polygon(t,h,y)},i}var ple=F(()=>{"use strict";vt();Kt();Jt();tr();nr();Kt();o(fle,"lightningBolt")});async function yle(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,s=t.look==="neo"?24:i;if(t.width||t.height){let x=t.width??0;t.width=(t.width??0)-a,t.widthw,":first-child").attr("class","line"),y=l.insert(()=>C,":first-child"),y.attr("class","basic label-container"),v&&y.attr("style",v)}else{let x=bQe(0,0,d,m,f,p,g);y=l.insert("path",":first-child").attr("d",x).attr("class","basic label-container outer-path").attr("style",kn(v)).attr("style",n)}return y.attr("label-offset-y",p),y.attr("transform",`translate(${-d/2}, ${-(m/2+p)})`),pt(t,y),h.attr("transform",`translate(${-(u.width/2)-(u.x-(u.left??0))}, ${-(u.height/2)+p-(u.y-(u.top??0))})`),t.intersect=function(x){let b=ht.rect(t,x),T=b.x-(t.x??0);if(f!=0&&(Math.abs(T)<(t.width??0)/2||Math.abs(T)==(t.width??0)/2&&Math.abs(b.y-(t.y??0))>(t.height??0)/2-p)){let k=p*p*(1-T*T/(f*f));k>0&&(k=Math.sqrt(k)),k=p-k,x.y-(t.y??0)>0&&(k=-k),b.y+=k}return b},l}var bQe,TQe,CQe,mle,gle,vle=F(()=>{"use strict";Kt();nr();Jt();tr();Qt();bQe=o((e,t,r,n,i,a,s)=>[`M${e},${t+a}`,`a${i},${a} 0,0,0 ${r},0`,`a${i},${a} 0,0,0 ${-r},0`,`l0,${n}`,`a${i},${a} 0,0,0 ${r},0`,`l0,${-n}`,`M${e},${t+a+s}`,`a${i},${a} 0,0,0 ${r},0`].join(" "),"createCylinderPathD"),TQe=o((e,t,r,n,i,a,s)=>[`M${e},${t+a}`,`M${e+r},${t+a}`,`a${i},${a} 0,0,0 ${-r},0`,`l0,${n}`,`a${i},${a} 0,0,0 ${r},0`,`l0,${-n}`,`M${e},${t+a+s}`,`a${i},${a} 0,0,0 ${r},0`].join(" "),"createOuterCylinderPathD"),CQe=o((e,t,r,n,i,a)=>[`M${e-r/2},${-n/2}`,`a${i},${a} 0,0,0 ${r},0`].join(" "),"createInnerCylinderPathD"),mle=10,gle=10;o(yle,"linedCylinder")});async function xle(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,s=t.look==="neo"?12:i;if(t.width||t.height){let k=t.width;t.width=(k??0)*10/11-a*2,t.width<10&&(t.width=10),t.height=(t?.height??0)-s*2,t.height<10&&(t.height=10)}let{shapeSvg:l,bbox:u,label:h}=await It(e,t,Dt(t)),d=(t?.width?t?.width:u.width)+(a??0)*2,f=(t?.height?t?.height:u.height)+(s??0)*2,p=t.look==="neo"?f/4:f/8,m=f+p,{cssStyles:g}=t,y=ut.svg(l),v=dt(t,{});t.look!=="handDrawn"&&(v.roughness=0,v.fillStyle="solid");let x=[{x:-d/2-d/2*.1,y:-m/2},{x:-d/2-d/2*.1,y:m/2},...fl(-d/2-d/2*.1,m/2,d/2+d/2*.1,m/2,p,.8),{x:d/2+d/2*.1,y:-m/2},{x:-d/2-d/2*.1,y:-m/2},{x:-d/2,y:-m/2},{x:-d/2,y:m/2*1.1},{x:-d/2,y:-m/2}],b=y.polygon(x.map(k=>[k.x,k.y]),v),T=l.insert(()=>b,":first-child");return T.attr("class","basic label-container outer-path"),g&&t.look!=="handDrawn"&&T.selectAll("path").attr("style",g),n&&t.look!=="handDrawn"&&T.selectAll("path").attr("style",n),T.attr("transform",`translate(0,${-p/2})`),h.attr("transform",`translate(${-d/2+(t.padding??0)+d/2*.1/2-(u.x-(u.left??0))},${-f/2+(t.padding??0)-p/2-(u.y-(u.top??0))})`),pt(t,T),t.intersect=function(k){return ht.polygon(t,x,k)},l}var ble=F(()=>{"use strict";Kt();nr();tr();Jt();o(xle,"linedWaveEdgedRect")});async function Tle(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,s=t.look==="neo"?12:i,l=t.look==="neo"?10:5;(t.width||t.height)&&(t.width=Math.max((t?.width??0)-a*2-2*l,10),t.height=Math.max((t?.height??0)-s*2-2*l,10));let{shapeSvg:u,bbox:h,label:d}=await It(e,t,Dt(t)),f=(t?.width?t?.width:h.width)+a*2+2*l,p=(t?.height?t?.height:h.height)+s*2+2*l,m=f-2*l,g=p-2*l,y=-m/2,v=-g/2,{cssStyles:x}=t,b=ut.svg(u),T=dt(t,{}),k=[{x:y-l,y:v+l},{x:y-l,y:v+g+l},{x:y+m-l,y:v+g+l},{x:y+m-l,y:v+g},{x:y+m,y:v+g},{x:y+m,y:v+g-l},{x:y+m+l,y:v+g-l},{x:y+m+l,y:v-l},{x:y+l,y:v-l},{x:y+l,y:v},{x:y,y:v},{x:y,y:v+l}],C=[{x:y,y:v+l},{x:y+m-l,y:v+l},{x:y+m-l,y:v+g},{x:y+m,y:v+g},{x:y+m,y:v},{x:y,y:v}];t.look!=="handDrawn"&&(T.roughness=0,T.fillStyle="solid");let w=or(k),S=b.path(w,T),R=or(C),L=b.path(R,T);t.look!=="handDrawn"&&(S=jP(S),L=jP(L));let N=u.insert("g",":first-child");return N.insert(()=>S),N.insert(()=>L),N.attr("class","basic label-container outer-path"),x&&t.look!=="handDrawn"&&N.selectAll("path").attr("style",x),n&&t.look!=="handDrawn"&&N.selectAll("path").attr("style",n),d.attr("transform",`translate(${-(h.width/2)-l-(h.x-(h.left??0))}, ${-(h.height/2)+l-(h.y-(h.top??0))})`),pt(t,N),t.intersect=function(I){return ht.polygon(t,k,I)},u}var Cle=F(()=>{"use strict";Kt();Jt();tr();nr();o(Tle,"multiRect")});async function wle(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let{shapeSvg:i,bbox:a,label:s}=await It(e,t,Dt(t)),l=t.padding??0,u=t.look==="neo"?16:l,h=t.look==="neo"?12:l,d=!0;(t.width||t.height)&&(d=!1,t.width=(t?.width??0)-u*2,t.height=(t?.height??0)-h*3);let f=Math.max(a.width,t?.width??0)+u*2,p=Math.max(a.height,t?.height??0)+h*3,m=t.look==="neo"?p/4:p/8,g=p+(d?m/2:-m/2),y=-f/2,v=-g/2,x=10,{cssStyles:b}=t,T=fl(y-x,v+g+x,y+f-x,v+g+x,m,.8),k=T?.[T.length-1],C=[{x:y-x,y:v+x},{x:y-x,y:v+g+x},...T,{x:y+f-x,y:k.y-x},{x:y+f,y:k.y-x},{x:y+f,y:k.y-2*x},{x:y+f+x,y:k.y-2*x},{x:y+f+x,y:v-x},{x:y+x,y:v-x},{x:y+x,y:v},{x:y,y:v},{x:y,y:v+x}],w=[{x:y,y:v+x},{x:y+f-x,y:v+x},{x:y+f-x,y:k.y-x},{x:y+f,y:k.y-x},{x:y+f,y:v},{x:y,y:v}],S=ut.svg(i),R=dt(t,{});t.look!=="handDrawn"&&(R.roughness=0,R.fillStyle="solid");let L=or(C),N=S.path(L,R),I=or(w),_=S.path(I,R),A=i.insert(()=>N,":first-child");return A.insert(()=>_),A.attr("class","basic label-container outer-path"),b&&t.look!=="handDrawn"&&A.selectAll("path").attr("style",b),n&&t.look!=="handDrawn"&&A.selectAll("path").attr("style",n),A.attr("transform",`translate(0,${-m/2})`),s.attr("transform",`translate(${-(a.width/2)-x-(a.x-(a.left??0))}, ${-(a.height/2)+x-m/2-(a.y-(a.top??0))})`),pt(t,A),t.intersect=function(M){return ht.polygon(t,C,M)},i}var kle=F(()=>{"use strict";Kt();nr();tr();Jt();o(wle,"multiWaveEdgedRectangle")});async function Sle(e,t,{config:{themeVariables:r}}){let{labelStyles:n,nodeStyles:i}=ct(t);t.labelStyle=n,t.useHtmlLabels||Gr(_t())||(t.centerLabel=!0);let{shapeSvg:s,bbox:l,label:u}=await It(e,t,Dt(t)),h=Math.max(l.width+(t.padding??0)*2,t?.width??0),d=Math.max(l.height+(t.padding??0)*2,t?.height??0),f=-h/2,p=-d/2,{cssStyles:m}=t,g=ut.svg(s),y=dt(t,{fill:r.noteBkgColor,stroke:r.noteBorderColor});t.look!=="handDrawn"&&(y.roughness=0,y.fillStyle="solid");let v=g.rectangle(f,p,h,d,y),x=s.insert(()=>v,":first-child");return x.attr("class","basic label-container outer-path"),u.attr("class","label noteLabel"),m&&t.look!=="handDrawn"&&x.selectAll("path").attr("style",m),i&&t.look!=="handDrawn"&&x.selectAll("path").attr("style",i),u.attr("transform",`translate(${-l.width/2-(l.x-(l.left??0))}, ${-(l.height/2)-(l.y-(l.top??0))})`),pt(t,x),t.intersect=function(b){return ht.rect(t,b)},s}var Ele=F(()=>{"use strict";tr();nr();Jt();Kt();ur();ur();o(Sle,"note")});async function Ale(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let{shapeSvg:i,bbox:a}=await It(e,t,Dt(t)),s=a.width+(t.padding??0),l=a.height+(t.padding??0),u=s+l,h=.5,d=[{x:u/2,y:0},{x:u,y:-u/2},{x:u/2,y:-u},{x:0,y:-u/2}],f,{cssStyles:p}=t;if(t.look==="handDrawn"){let m=ut.svg(i),g=dt(t,{}),y=wQe(0,0,u),v=m.path(y,g);f=i.insert(()=>v,":first-child").attr("transform",`translate(${-u/2+h}, ${u/2})`),p&&f.attr("style",p)}else f=as(i,u,u,d),f.attr("transform",`translate(${-u/2+h}, ${u/2})`);return n&&f.attr("style",n),pt(t,f),t.calcIntersect=function(m,g){let y=m.width,v=[{x:y/2,y:0},{x:y,y:-y/2},{x:y/2,y:-y},{x:0,y:-y/2}],x=ht.polygon(m,v,g);return{x:x.x-.5,y:x.y-.5}},t.intersect=function(m){return this.calcIntersect(t,m)},i}var wQe,Rle=F(()=>{"use strict";Kt();nr();Jt();tr();wh();wQe=o((e,t,r)=>[`M${e+r/2},${t}`,`L${e+r},${t-r/2}`,`L${e+r/2},${t-r}`,`L${e},${t-r/2}`,"Z"].join(" "),"createDecisionBoxPathD");o(Ale,"question")});async function _le(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?21:i??0,s=t.look==="neo"?12:i??0,{shapeSvg:l,bbox:u,label:h}=await It(e,t,Dt(t)),d=(t?.width??u.width)+(t.look==="neo"?a*2:a),f=(t?.height??u.height)+(t.look==="neo"?s*2:s),p=-d/2,m=-f/2,g=m/2,y=[{x:p+g,y:m},{x:p,y:0},{x:p+g,y:-m},{x:-p,y:-m},{x:-p,y:m}],{cssStyles:v}=t,x=ut.svg(l),b=dt(t,{});t.look!=="handDrawn"&&(b.roughness=0,b.fillStyle="solid");let T=or(y),k=x.path(T,b),C=l.insert(()=>k,":first-child");return C.attr("class","basic label-container outer-path"),v&&t.look!=="handDrawn"&&C.selectAll("path").attr("style",v),n&&t.look!=="handDrawn"&&C.selectAll("path").attr("style",n),C.attr("transform",`translate(${-g/2},0)`),h.attr("transform",`translate(${-g/2-u.width/2-(u.x-(u.left??0))}, ${-(u.height/2)-(u.y-(u.top??0))})`),pt(t,C),t.intersect=function(w){return ht.polygon(t,y,w)},l}var Lle=F(()=>{"use strict";Kt();nr();Jt();tr();o(_le,"rect_left_inv_arrow")});var kQe,sc,G4=F(()=>{"use strict";ur();Xt();Ls();kQe=o(async(e,t,r,n=!1,i=!1)=>{let a=t||"";typeof a=="object"&&(a=a[0]);let s=Ae(),l=Gr(s);return await Pn(e,a,{style:r,isTitle:n,useHtmlLabels:l,markdown:!1,isNode:i,width:Number.POSITIVE_INFINITY},s)},"createLabel"),sc=kQe});async function Dle(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i;t.cssClasses?i="node "+t.cssClasses:i="node default";let a=e.insert("g").attr("class",i).attr("id",t.domId||t.id),s=a.insert("g"),l=a.insert("g").attr("class","label").attr("style",n),u=t.description,h=t.label,d=await sc(l,h,t.labelStyle,!0,!0),f={width:0,height:0};if(Gr(Ae())){let R=d.children[0],L=et(d);f=R.getBoundingClientRect(),L.attr("width",f.width),L.attr("height",f.height)}Z.info("Text 2",u);let p=u||[],m=d.getBBox(),g=await sc(l,Array.isArray(p)?p.join("
"):p,t.labelStyle,!0,!0),y=g.children[0],v=et(g);f=y.getBoundingClientRect(),v.attr("width",f.width),v.attr("height",f.height);let x=(t.padding||0)/2;et(g).attr("transform","translate( "+(f.width>m.width?0:(m.width-f.width)/2)+", "+(m.height+x+5)+")"),et(d).attr("transform","translate( "+(f.width(Z.debug("Rough node insert CXC",N),I),":first-child"),w=a.insert(()=>(Z.debug("Rough node insert CXC",N),N),":first-child")}else w=s.insert("rect",":first-child"),S=s.insert("line"),w.attr("class","outer title-state").attr("style",n).attr("x",-f.width/2-x).attr("y",-f.height/2-x).attr("width",f.width+(t.padding||0)).attr("height",f.height+(t.padding||0)),S.attr("class","divider").attr("x1",-f.width/2-x).attr("x2",f.width/2+x).attr("y1",-f.height/2-x+m.height+x).attr("y2",-f.height/2-x+m.height+x);return pt(t,w),t.intersect=function(R){return ht.rect(t,R)},a}var Ile=F(()=>{"use strict";$r();Kt();G4();nr();Jt();tr();Xt();Sm();vt();ur();o(Dle,"rectWithTitle")});async function Mle(e,t,{config:{themeVariables:r}}){let n=r?.radius??5,i={rx:n,ry:n,classes:"",labelPaddingX:(t?.padding??0)*1,labelPaddingY:(t?.padding??0)*1};return ac(e,t,i)}var Nle=F(()=>{"use strict";Em();o(Mle,"roundedRect")});async function Ple(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.look==="neo"?16:t.padding??0,a=t.look==="neo"?12:t.padding??0,{shapeSvg:s,bbox:l,label:u}=await It(e,t,Dt(t)),h=(t?.width??l.width)+i*2+(t.look==="neo"?Am:Am*2),d=(t?.height??l.height)+a*2,f=h-Am,p=d,m=Am-h/2,g=-d/2,{cssStyles:y}=t,v=ut.svg(s),x=dt(t,{});t.look!=="handDrawn"&&(x.roughness=0,x.fillStyle="solid");let b=[{x:m,y:g},{x:m+f,y:g},{x:m+f,y:g+p},{x:m-Am,y:g+p},{x:m-Am,y:g},{x:m,y:g},{x:m,y:g+p}],T=v.polygon(b.map(C=>[C.x,C.y]),x),k=s.insert(()=>T,":first-child");return k.attr("class","basic label-container outer-path").attr("style",kn(y)),n&&t.look!=="handDrawn"&&k.selectAll("path").attr("style",n),y&&t.look!=="handDrawn"&&k.selectAll("path").attr("style",n),u.attr("transform",`translate(${Am/2-l.width/2-(l.x-(l.left??0))}, ${-(l.height/2)-(l.y-(l.top??0))})`),pt(t,k),t.intersect=function(C){return ht.rect(t,C)},s}var Am,Ole=F(()=>{"use strict";Kt();nr();Jt();tr();Qt();Am=8;o(Ple,"shadedProcess")});async function Ble(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,s=t.look==="neo"?12:i;(t.width||t.height)&&(t.width=Math.max((t?.width??0)-a*2,10),t.height=Math.max((t?.height??0)/1.5-s*2,10));let{shapeSvg:l,bbox:u,label:h}=await It(e,t,Dt(t)),d=(t?.width?t?.width:u.width)+a*2,f=((t?.height?t?.height:u.height)+s*2)*1.5,p=d,m=f/1.5,g=-p/2,y=-m/2,{cssStyles:v}=t,x=ut.svg(l),b=dt(t,{});t.look!=="handDrawn"&&(b.roughness=0,b.fillStyle="solid");let T=[{x:g,y},{x:g,y:y+m},{x:g+p,y:y+m},{x:g+p,y:y-m/2}],k=or(T),C=x.path(k,b),w=l.insert(()=>C,":first-child");return w.attr("class","basic label-container outer-path"),v&&t.look!=="handDrawn"&&w.selectChildren("path").attr("style",v),n&&t.look!=="handDrawn"&&w.selectChildren("path").attr("style",n),w.attr("transform",`translate(0, ${m/4})`),h.attr("transform",`translate(${-p/2+(t.padding??0)-(u.x-(u.left??0))}, ${-m/4+(t.padding??0)-(u.y-(u.top??0))})`),pt(t,w),t.intersect=function(S){return ht.polygon(t,T,S)},l}var $le=F(()=>{"use strict";Kt();nr();Jt();tr();o(Ble,"slopedRect")});async function Fle(e,t){let r=t.padding??0,n=t.look==="neo"?16:r*2,i=t.look==="neo"?12:r,a={rx:0,ry:0,classes:"",labelPaddingX:t.labelPaddingX??n,labelPaddingY:i};return ac(e,t,a)}var zle=F(()=>{"use strict";Em();o(Fle,"squareRect")});async function Gle(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?20:i,s=t.look==="neo"?12:i,{shapeSvg:l,bbox:u}=await It(e,t,Dt(t)),h=u.height+(t.look==="neo"?s*2:s),d=u.width+h/4+(t.look==="neo"?a*2:a),f=h/2,{cssStyles:p}=t,m=ut.svg(l),g=dt(t,{});t.look!=="handDrawn"&&(g.roughness=0,g.fillStyle="solid");let y=[{x:-d/2+f,y:-h/2},{x:d/2-f,y:-h/2},...wm(-d/2+f,0,f,50,90,270),{x:d/2-f,y:h/2},...wm(d/2-f,0,f,50,270,450)],v=or(y),x=m.path(v,g),b=l.insert(()=>x,":first-child");return b.attr("class","basic label-container outer-path"),p&&t.look!=="handDrawn"&&b.selectChildren("path").attr("style",p),n&&t.look!=="handDrawn"&&b.selectChildren("path").attr("style",n),pt(t,b),t.intersect=function(T){return ht.polygon(t,y,T)},l}var Vle=F(()=>{"use strict";Kt();nr();Jt();tr();o(Gle,"stadium")});async function Wle(e,t){let r={rx:t.look==="neo"?3:5,ry:t.look==="neo"?3:5,classes:"flowchart-node"};return ac(e,t,r)}var qle=F(()=>{"use strict";Em();o(Wle,"state")});function Hle(e,t,{config:{themeVariables:r}}){let{labelStyles:n,nodeStyles:i}=ct(t);t.labelStyle=n;let{cssStyles:a}=t,{lineColor:s,stateBorder:l,nodeBorder:u,nodeShadow:h}=r;(t.width||t.height)&&((t.width??0)<14&&(t.width=14),(t.height??0)<14&&(t.height=14)),t.width||(t.width=14),t.height||(t.height=14);let d=e.insert("g").attr("class","node default").attr("id",t.domId??t.id),f=ut.svg(d),p=dt(t,{});t.look!=="handDrawn"&&(p.roughness=0,p.fillStyle="solid");let m=f.circle(0,0,t.width,{...p,stroke:s,strokeWidth:2}),g=l??u,y=(t.width??0)*5/14,v=f.circle(0,0,y,{...p,fill:g,stroke:g,strokeWidth:2,fillStyle:"solid"}),x=d.insert(()=>m,":first-child");if(x.insert(()=>v),t.look!=="handDrawn"&&x.attr("class","outer-path"),a&&x.selectAll("path").attr("style",a),i&&x.selectAll("path").attr("style",i),t.width<25&&h&&t.look!=="handDrawn"){let b=e.node()?.ownerSVGElement?.id??"",T=b?`${b}-drop-shadow-small`:"drop-shadow-small";x.attr("style",`filter:url(#${T})`)}return pt(t,x),t.intersect=function(b){return ht.circle(t,(t.width??0)/2,b)},d}var Ule=F(()=>{"use strict";tr();nr();Jt();Kt();o(Hle,"stateEnd")});function Yle(e,t,{config:{themeVariables:r}}){let{lineColor:n,nodeShadow:i}=r;(t.width||t.height)&&((t.width??0)<14&&(t.width=14),(t.height??0)<14&&(t.height=14)),t.width||(t.width=14),t.height||(t.height=14);let a=e.insert("g").attr("class","node default").attr("id",t.domId||t.id),s;if(t.look==="handDrawn"){let u=ut.svg(a).circle(0,0,t.width,Hse(n));s=a.insert(()=>u),s.attr("class","state-start").attr("r",(t.width??7)/2).attr("width",t.width??14).attr("height",t.height??14)}else s=a.insert("circle",":first-child"),s.attr("class","state-start").attr("r",(t.width??7)/2).attr("width",t.width??14).attr("height",t.height??14);if(t.width<25&&i&&t.look!=="handDrawn"){let l=e.node()?.ownerSVGElement?.id??"",u=l?`${l}-drop-shadow-small`:"drop-shadow-small";s.attr("style",`filter:url(#${u})`)}return pt(t,s),t.intersect=function(l){return ht.circle(t,(t.width??7)/2,l)},a}var jle=F(()=>{"use strict";tr();nr();Jt();Kt();o(Yle,"stateStart")});async function Xle(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t?.padding??8,a=t.look==="neo"?28:i,s=t.look==="neo"?12:i,{shapeSvg:l,bbox:u}=await It(e,t,Dt(t)),h=(t?.width??u.width)+2*Gy+a,d=(t?.height??u.height)+s,f=h-2*Gy,p=d,m=-h/2,g=-d/2,y=[{x:0,y:0},{x:f,y:0},{x:f,y:-p},{x:0,y:-p},{x:0,y:0},{x:-8,y:0},{x:f+8,y:0},{x:f+8,y:-p},{x:-8,y:-p},{x:-8,y:0}];if(t.look==="handDrawn"){let v=ut.svg(l),x=dt(t,{}),b=v.rectangle(m,g,f+16,p,x),T=v.line(m+Gy,g,m+Gy,g+p,x),k=v.line(m+Gy+f,g,m+Gy+f,g+p,x);l.insert(()=>T,":first-child"),l.insert(()=>k,":first-child");let C=l.insert(()=>b,":first-child"),{cssStyles:w}=t;C.attr("class","basic label-container").attr("style",kn(w)),pt(t,C)}else{let v=as(l,f,p,y);n&&v.attr("style",n),pt(t,v)}return t.intersect=function(v){return ht.polygon(t,y,v)},l}var Gy,Kle=F(()=>{"use strict";Kt();nr();Jt();tr();wh();Qt();Gy=8;o(Xle,"subroutine")});async function Zle(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,s=t.look==="neo"?12:i;(t.width||t.height)&&(t.height=Math.max((t?.height??0)-s*2,10),t.width=Math.max((t?.width??0)-a*2-pO*(t.height+s*2),10));let{shapeSvg:l,bbox:u}=await It(e,t,Dt(t)),h=(t?.height?t?.height:u.height)+s*2,d=pO*h,f=pO*h,m=(t?.width?t?.width:u.width)+a*2+d-d,g=h,y=-m/2,v=-g/2,{cssStyles:x}=t,b=ut.svg(l),T=dt(t,{}),k=[{x:y-d/2,y:v},{x:y+m+d/2,y:v},{x:y+m+d/2,y:v+g},{x:y-d/2,y:v+g}],C=[{x:y+m-d/2,y:v+g},{x:y+m+d/2,y:v+g},{x:y+m+d/2,y:v+g-f}];t.look!=="handDrawn"&&(T.roughness=0,T.fillStyle="solid");let w=or(k),S=b.path(w,T),R=or(C),L=b.path(R,{...T,fillStyle:"solid"}),N=l.insert(()=>L,":first-child");return N.insert(()=>S,":first-child"),N.attr("class","basic label-container outer-path"),x&&t.look!=="handDrawn"&&N.selectAll("path").attr("style",x),n&&t.look!=="handDrawn"&&N.selectAll("path").attr("style",n),pt(t,N),t.intersect=function(I){return ht.polygon(t,k,I)},l}var pO,Qle=F(()=>{"use strict";Kt();Jt();tr();nr();pO=.2;o(Zle,"taggedRect")});async function Jle(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let{shapeSvg:i,bbox:a,label:s}=await It(e,t,Dt(t)),l=Math.max(a.width+(t.padding??0)*2,t?.width??0),u=Math.max(a.height+(t.padding??0)*2,t?.height??0),h=u/8,d=.2*l,f=.2*u,p=u+h,{cssStyles:m}=t,g=ut.svg(i),y=dt(t,{});t.look!=="handDrawn"&&(y.roughness=0,y.fillStyle="solid");let v=[{x:-l/2-l/2*.1,y:p/2},...fl(-l/2-l/2*.1,p/2,l/2+l/2*.1,p/2,h,.8),{x:l/2+l/2*.1,y:-p/2},{x:-l/2-l/2*.1,y:-p/2}],x=-l/2+l/2*.1,b=-p/2-f*.4,T=[{x:x+l-d,y:(b+u)*1.3},{x:x+l,y:b+u-f},{x:x+l,y:(b+u)*.9},...fl(x+l,(b+u)*1.25,x+l-d,(b+u)*1.3,-u*.02,.5)],k=or(v),C=g.path(k,y),w=or(T),S=g.path(w,{...y,fillStyle:"solid"}),R=i.insert(()=>S,":first-child");return R.insert(()=>C,":first-child"),R.attr("class","basic label-container outer-path"),m&&t.look!=="handDrawn"&&R.selectAll("path").attr("style",m),n&&t.look!=="handDrawn"&&R.selectAll("path").attr("style",n),R.attr("transform",`translate(0,${-h/2})`),s.attr("transform",`translate(${-l/2+(t.padding??0)-(a.x-(a.left??0))},${-u/2+(t.padding??0)-h/2-(a.y-(a.top??0))})`),pt(t,R),t.intersect=function(L){return ht.polygon(t,v,L)},i}var ece=F(()=>{"use strict";Kt();nr();tr();Jt();o(Jle,"taggedWaveEdgedRectangle")});async function tce(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let{shapeSvg:i,bbox:a}=await It(e,t,Dt(t)),s=Math.max(a.width+(t.padding??0),t?.width||0),l=Math.max(a.height+(t.padding??0),t?.height||0),u=-s/2,h=-l/2,d=i.insert("rect",":first-child");return d.attr("class","text").attr("style",n).attr("rx",0).attr("ry",0).attr("x",u).attr("y",h).attr("width",s).attr("height",l),pt(t,d),t.intersect=function(f){return ht.rect(t,f)},i}var rce=F(()=>{"use strict";Kt();nr();Jt();o(tce,"text")});async function ace(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?12:i/2;if(t.width||t.height){let y=t.height??0;t.height=(t.height??0)-a,t.heightT,":first-child"),g=s.insert(()=>b,":first-child"),g.attr("class","basic label-container"),m&&g.attr("style",m)}else{let y=SQe(0,0,p,h,f,d);g=s.insert("path",":first-child").attr("d",y).attr("class","basic label-container").attr("style",kn(m)).attr("style",n),g.attr("class","basic label-container outer-path"),m&&g.selectAll("path").attr("style",m),n&&g.selectAll("path").attr("style",n)}return g.attr("label-offset-x",f),g.attr("transform",`translate(${-p/2}, ${h/2} )`),u.attr("transform",`translate(${-(l.width/2)-f-(l.x-(l.left??0))}, ${-(l.height/2)-(l.y-(l.top??0))})`),pt(t,g),t.intersect=function(y){let v=ht.rect(t,y),x=v.y-(t.y??0);if(d!=0&&(Math.abs(x)<(t.height??0)/2||Math.abs(x)==(t.height??0)/2&&Math.abs(v.x-(t.x??0))>(t.width??0)/2-f)){let b=f*f*(1-x*x/(d*d));b!=0&&(b=Math.sqrt(Math.abs(b))),b=f-b,y.x-(t.x??0)>0&&(b=-b),v.x+=b}return v},s}var SQe,EQe,AQe,nce,ice,sce=F(()=>{"use strict";Kt();Jt();tr();nr();Qt();SQe=o((e,t,r,n,i,a)=>`M${e},${t} + a${i},${a} 0,0,1 0,${-n} + l${r},0 + a${i},${a} 0,0,1 0,${n} + M${r},${-n} + a${i},${a} 0,0,0 0,${n} + l${-r},0`,"createCylinderPathD"),EQe=o((e,t,r,n,i,a)=>[`M${e},${t}`,`M${e+r},${t}`,`a${i},${a} 0,0,0 0,${-n}`,`l${-r},0`,`a${i},${a} 0,0,0 0,${n}`,`l${r},0`].join(" "),"createOuterCylinderPathD"),AQe=o((e,t,r,n,i,a)=>[`M${e+r/2},${-n/2}`,`a${i},${a} 0,0,0 0,${n}`].join(" "),"createInnerCylinderPathD"),nce=5,ice=10;o(ace,"tiltedCylinder")});async function oce(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=(t.look==="neo",i),s=t.look==="neo"?i*2:i,{shapeSvg:l,bbox:u}=await It(e,t,Dt(t)),h=(t?.height??u.height)+a,d=(t?.width??u.width)+s,f=[{x:-3*h/6,y:0},{x:d+3*h/6,y:0},{x:d,y:-h},{x:0,y:-h}],p,{cssStyles:m}=t;if(t.look==="handDrawn"){let g=ut.svg(l),y=dt(t,{}),v=or(f),x=g.path(v,y);p=l.insert(()=>x,":first-child").attr("transform",`translate(${-d/2}, ${h/2})`),m&&p.attr("style",m)}else p=as(l,d,h,f);return n&&p.attr("style",n),t.width=d,t.height=h,pt(t,p),t.intersect=function(g){return ht.polygon(t,f,g)},l}var lce=F(()=>{"use strict";Kt();nr();Jt();tr();wh();o(oce,"trapezoid")});async function cce(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,s=t.look==="neo"?12:i,l=15,u=5;(t.width||t.height)&&(t.height=(t.height??0)-s*2,t.heightb,":first-child");return T.attr("class","basic label-container outer-path"),m&&t.look!=="handDrawn"&&T.selectChildren("path").attr("style",m),n&&t.look!=="handDrawn"&&T.selectChildren("path").attr("style",n),pt(t,T),t.intersect=function(k){return ht.polygon(t,v,k)},h}var uce=F(()=>{"use strict";Kt();nr();Jt();tr();o(cce,"trapezoidalPentagon")});async function fce(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?i*2:i;(t.width||t.height)&&(t.width=((t?.width??0)-a)/2,t.widthb,":first-child").attr("transform",`translate(${-f/2}, ${f/2})`).attr("class","outer-path");return g&&t.look!=="handDrawn"&&T.selectChildren("path").attr("style",g),n&&t.look!=="handDrawn"&&T.selectChildren("path").attr("style",n),t.width=d,t.height=f,pt(t,T),u.attr("transform",`translate(${-l.width/2-(l.x-(l.left??0))}, ${f/2-(l.height+(t.padding??0)/(h?2:1)-(l.y-(l.top??0)))})`),t.intersect=function(k){return Z.info("Triangle intersect",t,m,k),ht.polygon(t,m,k)},s}var hce,dce,pce=F(()=>{"use strict";vt();Kt();nr();Jt();tr();Kt();Vr();Xt();hce=10,dce=10;o(fce,"triangle")});async function mce(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,s=t.look==="neo"?12:i,l=!0;(t.width||t.height)&&(l=!1,t.width=(t?.width??0)-a*2,t.width<10&&(t.width=10),t.height=(t?.height??0)-s*2,t.height<10&&(t.height=10));let{shapeSvg:u,bbox:h,label:d}=await It(e,t,Dt(t)),f=(t?.width?t?.width:h.width)+(a??0)*2,p=(t?.height?t?.height:h.height)+(s??0)*2,m=t.look==="neo"?p/4:p/8,g=p+(l?m:-m),{cssStyles:y}=t,x=14-f,b=x>0?x/2:0,T=ut.svg(u),k=dt(t,{});t.look!=="handDrawn"&&(k.roughness=0,k.fillStyle="solid");let C=[{x:-f/2-b,y:g/2},...fl(-f/2-b,g/2,f/2+b,g/2,m,.8),{x:f/2+b,y:-g/2},{x:-f/2-b,y:-g/2}],w=or(C),S=T.path(w,k),R=u.insert(()=>S,":first-child");return R.attr("class","basic label-container outer-path"),y&&t.look!=="handDrawn"&&R.selectAll("path").attr("style",y),n&&t.look!=="handDrawn"&&R.selectAll("path").attr("style",n),R.attr("transform",`translate(0,${-m/2})`),d.attr("transform",`translate(${-f/2+(t.padding??0)-(h.x-(h.left??0))},${-p/2+(t.padding??0)-m-(h.y-(h.top??0))})`),pt(t,R),t.intersect=function(L){return ht.polygon(t,C,L)},u}var gce=F(()=>{"use strict";Kt();nr();tr();Jt();o(mce,"waveEdgedRectangle")});async function yce(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.padding??0,a=t.look==="neo"?16:i,s=t.look==="neo"?20:i;if(t.width||t.height){t.width=t?.width??0,t.width<20&&(t.width=20),t.height=t?.height??0,t.height<10&&(t.height=10);let k=Math.min(t.height*.2,t.height/4);t.height=Math.ceil(t.height-s-k*(20/9)),t.width=t.width-a*2}let{shapeSvg:l,bbox:u}=await It(e,t,Dt(t)),h=(t?.width?t?.width:u.width)+a*2,d=(t?.height?t?.height:u.height)+s,f=d/8,p=d+f*2,{cssStyles:m}=t,g=ut.svg(l),y=dt(t,{});t.look!=="handDrawn"&&(y.roughness=0,y.fillStyle="solid");let v=[{x:-h/2,y:p/2},...fl(-h/2,p/2,h/2,p/2,f,1),{x:h/2,y:-p/2},...fl(h/2,-p/2,-h/2,-p/2,f,-1)],x=or(v),b=g.path(x,y),T=l.insert(()=>b,":first-child");return T.attr("class","basic label-container"),m&&t.look!=="handDrawn"&&T.selectAll("path").attr("style",m),n&&t.look!=="handDrawn"&&T.selectAll("path").attr("style",n),pt(t,T),t.intersect=function(k){return ht.polygon(t,v,k)},l}var vce=F(()=>{"use strict";Kt();nr();Jt();tr();o(yce,"waveRectangle")});async function xce(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t.look==="neo"?16:t.padding??0,a=t.look==="neo"?12:t.padding??0;(t.width||t.height)&&(t.width=Math.max((t?.width??0)-i*2-Ri,10),t.height=Math.max((t?.height??0)-a*2-Ri,10));let{shapeSvg:s,bbox:l,label:u}=await It(e,t,Dt(t)),h=(t?.width?t?.width:l.width)+i*2+Ri,d=(t?.height?t?.height:l.height)+a*2+Ri,f=h-Ri,p=d-Ri,m=-f/2,g=-p/2,{cssStyles:y}=t,v=ut.svg(s),x=dt(t,{}),b=[{x:m-Ri,y:g-Ri},{x:m-Ri,y:g+p},{x:m+f,y:g+p},{x:m+f,y:g-Ri}],T=`M${m-Ri},${g-Ri} L${m+f},${g-Ri} L${m+f},${g+p} L${m-Ri},${g+p} L${m-Ri},${g-Ri} + M${m-Ri},${g} L${m+f},${g} + M${m},${g-Ri} L${m},${g+p}`;t.look!=="handDrawn"&&(x.roughness=0,x.fillStyle="solid");let k=v.path(T,x),C=s.insert(()=>k,":first-child");return C.attr("transform",`translate(${Ri/2}, ${Ri/2})`),C.attr("class","basic label-container outer-path"),y&&t.look!=="handDrawn"&&C.selectAll("path").attr("style",y),n&&t.look!=="handDrawn"&&C.selectAll("path").attr("style",n),u.attr("transform",`translate(${-(l.width/2)+Ri/2-(l.x-(l.left??0))}, ${-(l.height/2)+Ri/2-(l.y-(l.top??0))})`),pt(t,C),t.intersect=function(w){return ht.polygon(t,b,w)},s}var Ri,bce=F(()=>{"use strict";Kt();Jt();tr();nr();Ri=10;o(xce,"windowPane")});async function mO(e,t){let r=t;r.alias&&(t.label=r.alias);let{theme:n,themeVariables:i}=_t(),{rowEven:a,rowOdd:s,nodeBorder:l,borderColorArray:u}=i;if(t.look==="handDrawn"){let{themeVariables:Q}=_t(),{background:U}=Q,oe={...t,id:t.id+"-background",domId:(t.domId||t.id)+"-background",look:"default",cssStyles:["stroke: none",`fill: ${U}`]};await mO(e,oe)}let h=_t();t.useHtmlLabels=h.htmlLabels;let d=h.er?.diagramPadding??10,f=h.er?.entityPadding??6,{cssStyles:p}=t,{labelStyles:m,nodeStyles:g}=ct(t);if(r.attributes.length===0&&t.label){let Q={rx:0,ry:0,labelPaddingX:d,labelPaddingY:d*1.5,classes:""};Ca(t.label,h)+Q.labelPaddingX*20){let Q=x.width+d*2-(C+w+S+R);C+=Q/I,w+=Q/I,S>0&&(S+=Q/I),R>0&&(R+=Q/I)}let A=C+w+S+R,M=ut.svg(v),D=dt(t,{});t.look!=="handDrawn"&&(D.roughness=0,D.fillStyle="solid");let P=0;k.length>0&&(P=k.reduce((Q,U)=>Q+(U?.rowHeight??0),0));let B=Math.max(_.width+d*2,t?.width||0,A),O=Math.max((P??0)+x.height,t?.height||0),$=-B/2,V=-O/2;if(v.selectAll("g:not(:first-child)").each((Q,U,oe)=>{let te=et(oe[U]),le=te.attr("transform"),ie=0,ae=0;if(le){let be=RegExp(/translate\(([^,]+),([^)]+)\)/).exec(le);be&&(ie=parseFloat(be[1]),ae=parseFloat(be[2]),te.attr("class").includes("attribute-name")?ie+=C:te.attr("class").includes("attribute-keys")?ie+=C+w:te.attr("class").includes("attribute-comment")&&(ie+=C+w+S))}te.attr("transform",`translate(${$+d/2+ie}, ${ae+V+x.height+f/2})`)}),v.select(".name").attr("transform","translate("+-x.width/2+", "+(V+f/2)+")"),n!=null&&Tce.has(n)){let Q=r.colorIndex??0;v.attr("data-color-id",`color-${Q%u.length}`)}let G=M.rectangle($,V,B,O,D),z=v.insert(()=>G,":first-child").attr("class","outer-path").attr("style",p.join(""));T.push(0);for(let[Q,U]of k.entries()){let te=(Q+1)%2===0&&U.yOffset!==0,le=M.rectangle($,x.height+V+U?.yOffset,B,U?.rowHeight,{...D,fill:te?a:s,stroke:l});v.insert(()=>le,"g.label").attr("style",p.join("")).attr("class",`row-rect-${te?"even":"odd"}`)}let W=1e-4,H=z2($,x.height+V,B+$,x.height+V,W),j=M.polygon(H.map(Q=>[Q.x,Q.y]),D);if(v.insert(()=>j).attr("class","divider"),H=z2(C+$,x.height+V,C+$,O+V,W),j=M.polygon(H.map(Q=>[Q.x,Q.y]),D),v.insert(()=>j).attr("class","divider"),L){let Q=C+w+$;H=z2(Q,x.height+V,Q,O+V,W),j=M.polygon(H.map(U=>[U.x,U.y]),D),v.insert(()=>j).attr("class","divider")}if(N){let Q=C+w+S+$;H=z2(Q,x.height+V,Q,O+V,W),j=M.polygon(H.map(U=>[U.x,U.y]),D),v.insert(()=>j).attr("class","divider")}for(let Q of T){let U=x.height+V+Q;H=z2($,U,B+$,U,W),j=M.polygon(H.map(oe=>[oe.x,oe.y]),D),v.insert(()=>j).attr("class","divider")}if(pt(t,z),g&&t.look!=="handDrawn")if(n!=null&&RQe.has(n))v.selectAll("path").attr("style",g);else{let U=g.split(";")?.filter(oe=>oe.includes("stroke"))?.map(oe=>`${oe}`).join("; ");v.selectAll("path").attr("style",U??""),v.selectAll(".row-rect-even path").attr("style",g)}return t.intersect=function(Q){return ht.rect(t,Q)},v}async function F2(e,t,r,n=0,i=0,a=[],s=""){let l=e.insert("g").attr("class",`label ${a.join(" ")}`).attr("transform",`translate(${n}, ${i})`).attr("style",s);t!==qc(t)&&(t=qc(t),t=t.replaceAll("<","<").replaceAll(">",">"));let u=l.node().appendChild(await Pn(l,t,{width:Ca(t,r)+100,style:s,useHtmlLabels:r.htmlLabels},r));if(t.includes("<")||t.includes(">")){let d=u.children[0];for(d.textContent=d.textContent.replaceAll("<","<").replaceAll(">",">");d.childNodes[0];)d=d.childNodes[0],d.textContent=d.textContent.replaceAll("<","<").replaceAll(">",">")}let h=u.getBBox();if(ya(r.htmlLabels)){let d=u.children[0];d.style.textAlign="start";let f=et(u);h=d.getBoundingClientRect(),f.attr("width",h.width),f.attr("height",h.height)}return h}function z2(e,t,r,n,i){return e===r?[{x:e-i/2,y:t},{x:e+i/2,y:t},{x:r+i/2,y:n},{x:r-i/2,y:n}]:[{x:e,y:t-i/2},{x:e,y:t+i/2},{x:r,y:n+i/2},{x:r,y:n-i/2}]}var Tce,RQe,Cce=F(()=>{"use strict";Kt();nr();Jt();tr();Em();ur();Ls();Vr();$r();Qt();Tce=new Set(["redux-color","redux-dark-color"]),RQe=new Set(["redux","redux-dark","redux-color","redux-dark-color"]);o(mO,"erBox");o(F2,"addText");o(z2,"lineToPolygon")});async function wce(e,t,r,n,i=r.class.padding??12){let a=n?0:3,s=e.insert("g").attr("class",Dt(t)).attr("id",t.domId||t.id),l=null,u=null,h=null,d=null,f=0,p=0,m=0;if(l=s.insert("g").attr("class","annotation-group text"),t.annotations.length>0){let b=t.annotations[0];await V4(l,{text:`\xAB${b}\xBB`},0),f=l.node().getBBox().height}u=s.insert("g").attr("class","label-group text"),await V4(u,t,0,["font-weight: bolder"]);let g=u.node().getBBox();p=g.height,h=s.insert("g").attr("class","members-group text");let y=0;for(let b of t.members){let T=await V4(h,b,y,[b.parseClassifier()]);y+=T+a}m=h.node().getBBox().height,m<=0&&(m=i/2),d=s.insert("g").attr("class","methods-group text");let v=0;for(let b of t.methods){let T=await V4(d,b,v,[b.parseClassifier()]);v+=T+a}let x=s.node().getBBox();if(l!==null){let b=l.node().getBBox();l.attr("transform",`translate(${-b.width/2})`)}return u.attr("transform",`translate(${-g.width/2}, ${f})`),x=s.node().getBBox(),h.attr("transform",`translate(0, ${f+p+i*2})`),x=s.node().getBBox(),d.attr("transform",`translate(0, ${f+p+(m?m+i*4:i*2)})`),x=s.node().getBBox(),{shapeSvg:s,bbox:x}}async function V4(e,t,r,n=[]){let i=e.insert("g").attr("class","label").attr("style",n.join("; ")),a=_t(),s="useHtmlLabels"in t?t.useHtmlLabels:ya(a.htmlLabels)??!0,l="";"text"in t?l=t.text:l=t.label,!s&&l.startsWith("\\")&&(l=l.substring(1)),ni(l)&&(s=!0);let u=await Pn(i,hb(Rs(l)),{width:Ca(l,a)+50,classes:"markdown-node-label",useHtmlLabels:s},a),h,d=1;if(s){let f=u.children[0],p=et(u);d=f.innerHTML.split("
").length,f.innerHTML.includes("")&&(d+=f.innerHTML.split("").length-1);let m=f.getElementsByTagName("img");if(m){let g=l.replace(/]*>/g,"").trim()==="";await Promise.all([...m].map(y=>new Promise(v=>{function x(){if(y.style.display="flex",y.style.flexDirection="column",g){let b=a.fontSize?.toString()??window.getComputedStyle(document.body).fontSize,k=parseInt(b,10)*5+"px";y.style.minWidth=k,y.style.maxWidth=k}else y.style.width="100%";v(y)}o(x,"setupImage"),setTimeout(()=>{y.complete&&x()}),y.addEventListener("error",x),y.addEventListener("load",x)})))}h=f.getBoundingClientRect(),p.attr("width",h.width),p.attr("height",h.height)}else{n.includes("font-weight: bolder")&&et(u).selectAll("tspan").attr("font-weight",""),d=u.children.length;let f=u.children[0];(u.textContent===""||u.textContent.includes(">"))&&(f.textContent=l[0]+l.substring(1).replaceAll(">",">").replaceAll("<","<").trim(),l[1]===" "&&(f.textContent=f.textContent[0]+" "+f.textContent.substring(1))),f.textContent==="undefined"&&(f.textContent=""),h=u.getBBox()}return i.attr("transform","translate(0,"+(-h.height/(2*d)+r)+")"),h.height}var kce=F(()=>{"use strict";$r();ur();Kt();Qt();Xt();Ls();Vr();o(wce,"textHelper");o(V4,"addText")});async function Sce(e,t){let r=Ae(),{themeVariables:n}=r,{useGradient:i}=n,a=r.class.padding??12,s=a,l=t.useHtmlLabels??ya(r.htmlLabels)??!0,u=t;u.annotations=u.annotations??[],u.members=u.members??[],u.methods=u.methods??[];let{shapeSvg:h,bbox:d}=await wce(e,t,r,l,s),{labelStyles:f,nodeStyles:p}=ct(t);t.labelStyle=f,t.cssStyles=u.styles||"";let m=u.styles?.join(";")||p||"";t.cssStyles||(t.cssStyles=m.replaceAll("!important","").split(";"));let g=u.members.length===0&&u.methods.length===0&&!r.class?.hideEmptyMembersBox,y=ut.svg(h),v=dt(t,{});t.look!=="handDrawn"&&(v.roughness=0,v.fillStyle="solid");let x=Math.max(t.width??0,d.width),b=Math.max(t.height??0,d.height),T=(t.height??0)>d.height;u.members.length===0&&u.methods.length===0?b+=s:u.members.length>0&&u.methods.length===0&&(b+=s*2);let k=-x/2,C=-b/2,w=g?a*2:u.members.length===0&&u.methods.length===0?-a:0;T&&(w=a*2);let S=y.rectangle(k-a,C-a-(g?a:u.members.length===0&&u.methods.length===0?-a/2:0),x+2*a,b+2*a+w,v),R=h.insert(()=>S,":first-child");R.attr("class","basic label-container outer-path");let L=R.node().getBBox(),N=h.select(".annotation-group").node().getBBox().height-(g?a/2:0)||0,I=h.select(".label-group").node().getBBox().height-(g?a/2:0)||0,_=h.select(".members-group").node().getBBox().height-(g?a/2:0)||0,A=(N+I+C+a-(C-a-(g?a:u.members.length===0&&u.methods.length===0?-a/2:0)))/2;if(h.selectAll(".text").each((M,D,P)=>{let B=et(P[D]),O=B.attr("transform"),$=0;if(O){let W=RegExp(/translate\(([^,]+),([^)]+)\)/).exec(O);W&&($=parseFloat(W[2]))}let V=$+C+a-(g?a:u.members.length===0&&u.methods.length===0?-a/2:0);if(B.attr("class").includes("methods-group")){let z=Math.max(_,s/2);T?V=Math.max(A,N+I+z+C+s*2+a)+s*2:V=N+I+z+C+s*4+a}u.members.length===0&&u.methods.length===0&&r.class?.hideEmptyMembersBox&&(u.annotations.length>0?V=$-s:V=$),l||(V-=4);let G=k;(B.attr("class").includes("label-group")||B.attr("class").includes("annotation-group"))&&(G=-B.node()?.getBBox().width/2||0,h.selectAll("text").each(function(z,W,H){window.getComputedStyle(H[W]).textAnchor==="middle"&&(G=0)})),B.attr("transform",`translate(${G}, ${V})`)}),u.members.length>0||u.methods.length>0||g){let M=N+I+C+a,D=y.line(L.x,M,L.x+L.width,M+.001,v);h.insert(()=>D).attr("class",`divider${t.look==="neo"&&!i?" neo-line":""}`).attr("style",m)}if(g||u.members.length>0||u.methods.length>0){let M=N+I+_+C+s*2+a,D=y.line(L.x,T?Math.max(A,M):M,L.x+L.width,(T?Math.max(A,M):M)+.001,v);h.insert(()=>D).attr("class",`divider${t.look==="neo"&&!i?" neo-line":""}`).attr("style",m)}if(u.look!=="handDrawn"&&h.selectAll("path").attr("style",m),R.select(":nth-child(2)").attr("style",m),h.selectAll(".divider").select("path").attr("style",m),t.labelStyle?h.selectAll("span").attr("style",t.labelStyle):h.selectAll("span").attr("style",m),!l){let M=RegExp(/color\s*:\s*([^;]*)/),D=M.exec(m);if(D){let P=D[0].replace("color","fill");h.selectAll("tspan").attr("style",P)}else if(f){let P=M.exec(f);if(P){let B=P[0].replace("color","fill");h.selectAll("tspan").attr("style",B)}}}return pt(t,R),t.intersect=function(M){return ht.rect(t,M)},h}var Ece=F(()=>{"use strict";Kt();Xt();$r();tr();Jt();nr();kce();Vr();o(Sce,"classBox")});async function Ace(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let i=t,a=t,s=20,l=20,u="verifyMethod"in t,h=Dt(t),{themeVariables:d}=Ae(),{borderColorArray:f,requirementEdgeLabelBackground:p}=d,m=e.insert("g").attr("class",h).attr("id",t.domId??t.id),g;u?g=await kh(m,`<<${i.type}>>`,0,t.labelStyle):g=await kh(m,"<<Element>>",0,t.labelStyle);let y=g,v=await kh(m,i.name,y,t.labelStyle+"; font-weight: bold;");if(y+=v+l,u){let L=await kh(m,`${i.requirementId?`ID: ${i.requirementId}`:""}`,y,t.labelStyle);y+=L;let N=await kh(m,`${i.text?`Text: ${i.text}`:""}`,y,t.labelStyle);y+=N;let I=await kh(m,`${i.risk?`Risk: ${i.risk}`:""}`,y,t.labelStyle);y+=I,await kh(m,`${i.verifyMethod?`Verification: ${i.verifyMethod}`:""}`,y,t.labelStyle)}else{let L=await kh(m,`${a.type?`Type: ${a.type}`:""}`,y,t.labelStyle);y+=L,await kh(m,`${a.docRef?`Doc Ref: ${a.docRef}`:""}`,y,t.labelStyle)}let x=(m.node()?.getBBox().width??200)+s,b=(m.node()?.getBBox().height??200)+s,T=-x/2,k=-b/2,C=ut.svg(m),w=dt(t,{});t.look!=="handDrawn"&&(w.roughness=0,w.fillStyle="solid");let S=C.rectangle(T,k,x,b,w),R=m.insert(()=>S,":first-child");if(R.attr("class","basic label-container outer-path").attr("style",n),f?.length){let L=t.colorIndex??0;m.attr("data-color-id",`color-${L%f.length}`)}if(m.selectAll(".label").each((L,N,I)=>{let _=et(I[N]),A=_.attr("transform"),M=0,D=0;if(A){let $=RegExp(/translate\(([^,]+),([^)]+)\)/).exec(A);$&&(M=parseFloat($[1]),D=parseFloat($[2]))}let P=D-b/2,B=T+s/2;(N===0||N===1)&&(B=M),_.attr("transform",`translate(${B}, ${P+s})`)}),y>g+v+l){let L=k+g+v+l,N;if(t.look==="neo"){let A=[[T,L],[T+x,L],[T+x,L+.001],[T,L+.001]];N=C.polygon(A,w)}else N=C.line(T,L,T+x,L,w);m.insert(()=>N).attr("class","divider")}return pt(t,R),t.intersect=function(L){return ht.rect(t,L)},n&&t.look!=="handDrawn"&&(p||f?.length)&&m.selectAll("path").attr("style",n),m}async function kh(e,t,r,n=""){if(t==="")return 0;let i=e.insert("g").attr("class","label").attr("style",n),a=Ae(),s=a.htmlLabels??!0,l=await Pn(i,hb(Rs(t)),{width:Ca(t,a)+50,classes:"markdown-node-label",useHtmlLabels:s,style:n},a),u;if(s){let h=l.children[0],d=et(l);u=h.getBoundingClientRect(),d.attr("width",u.width),d.attr("height",u.height)}else{let h=l.children[0];for(let d of h.children)n&&d.setAttribute("style",n);u=l.getBBox(),u.height+=6}return i.attr("transform",`translate(${-u.width/2},${-u.height/2+r})`),u.height}var Rce=F(()=>{"use strict";Kt();nr();Jt();tr();Qt();Xt();Ls();$r();o(Ace,"requirementBox");o(kh,"addText")});async function _ce(e,t,{config:r}){let{labelStyles:n,nodeStyles:i}=ct(t);t.labelStyle=n||"";let a=10,s=t.width;t.width=(t.width??200)-10;let{shapeSvg:l,bbox:u,label:h}=await It(e,t,Dt(t)),d=t.padding||10,f="",p;"ticket"in t&&t.ticket&&r?.kanban?.ticketBaseUrl&&(f=r?.kanban?.ticketBaseUrl.replace("#TICKET#",t.ticket),p=l.insert("svg:a",":first-child").attr("class","kanban-ticket-link").attr("xlink:href",f).attr("target","_blank"));let m={useHtmlLabels:t.useHtmlLabels,labelStyle:t.labelStyle||"",width:t.width,img:t.img,padding:t.padding||8,centerLabel:!1},g,y;p?{label:g,bbox:y}=await A4(p,"ticket"in t&&t.ticket||"",m):{label:g,bbox:y}=await A4(l,"ticket"in t&&t.ticket||"",m);let{label:v,bbox:x}=await A4(l,"assigned"in t&&t.assigned||"",m);t.width=s;let b=10,T=t?.width||0,k=Math.max(y.height,x.height)/2,C=Math.max(u.height+b*2,t?.height||0)+k,w=-T/2,S=-C/2;h.attr("transform","translate("+(d-T/2)+", "+(-k-u.height/2)+")"),g.attr("transform","translate("+(d-T/2)+", "+(-k+u.height/2)+")"),v.attr("transform","translate("+(d+T/2-x.width-2*a)+", "+(-k+u.height/2)+")");let R,{rx:L,ry:N}=t,{cssStyles:I}=t;if(t.look==="handDrawn"){let _=ut.svg(l),A=dt(t,{}),M=L||N?_.path(io(w,S,T,C,L||0),A):_.rectangle(w,S,T,C,A);R=l.insert(()=>M,":first-child"),R.attr("class","basic label-container").attr("style",I||null)}else{R=l.insert("rect",":first-child"),R.attr("class","basic label-container __APA__").attr("style",i).attr("rx",L??5).attr("ry",N??5).attr("x",w).attr("y",S).attr("width",T).attr("height",C);let _="priority"in t&&t.priority;if(_){let A=l.append("line"),M=w+2,D=S+Math.floor((L??0)/2),P=S+C-Math.floor((L??0)/2);A.attr("x1",M).attr("y1",D).attr("x2",M).attr("y2",P).attr("stroke-width","4").attr("stroke",_Qe(_))}}return pt(t,R),t.height=C,t.intersect=function(_){return ht.rect(t,_)},l}var _Qe,Lce=F(()=>{"use strict";Kt();nr();Sm();Jt();tr();_Qe=o(e=>{switch(e){case"Very High":return"red";case"High":return"orange";case"Medium":return null;case"Low":return"blue";case"Very Low":return"lightblue"}},"colorFromPriority");o(_ce,"kanbanItem")});async function Dce(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let{shapeSvg:i,bbox:a,halfPadding:s,label:l}=await It(e,t,Dt(t)),u=a.width+10*s,h=a.height+8*s,d=.15*u,{cssStyles:f}=t,p=a.width+20,m=a.height+20,g=Math.max(u,p),y=Math.max(h,m);l.attr("transform",`translate(${-a.width/2}, ${-a.height/2})`);let v,x=`M0 0 + a${d},${d} 1 0,0 ${g*.25},${-1*y*.1} + a${d},${d} 1 0,0 ${g*.25},0 + a${d},${d} 1 0,0 ${g*.25},0 + a${d},${d} 1 0,0 ${g*.25},${y*.1} + + a${d},${d} 1 0,0 ${g*.15},${y*.33} + a${d*.8},${d*.8} 1 0,0 0,${y*.34} + a${d},${d} 1 0,0 ${-1*g*.15},${y*.33} + + a${d},${d} 1 0,0 ${-1*g*.25},${y*.15} + a${d},${d} 1 0,0 ${-1*g*.25},0 + a${d},${d} 1 0,0 ${-1*g*.25},0 + a${d},${d} 1 0,0 ${-1*g*.25},${-1*y*.15} + + a${d},${d} 1 0,0 ${-1*g*.1},${-1*y*.33} + a${d*.8},${d*.8} 1 0,0 0,${-1*y*.34} + a${d},${d} 1 0,0 ${g*.1},${-1*y*.33} + H0 V0 Z`;if(t.look==="handDrawn"){let b=ut.svg(i),T=dt(t,{}),k=b.path(x,T);v=i.insert(()=>k,":first-child"),v.attr("class","basic label-container").attr("style",kn(f))}else v=i.insert("path",":first-child").attr("class","basic label-container").attr("style",n).attr("d",x);return v.attr("transform",`translate(${-g/2}, ${-y/2})`),pt(t,v),t.calcIntersect=function(b,T){return ht.rect(b,T)},t.intersect=function(b){return Z.info("Bang intersect",t,b),ht.rect(t,b)},i}var Ice=F(()=>{"use strict";vt();Kt();nr();Jt();tr();Qt();o(Dce,"bang")});async function Mce(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let{shapeSvg:i,bbox:a,halfPadding:s,label:l}=await It(e,t,Dt(t)),u=a.width+2*s,h=a.height+2*s,d=.15*u,f=.25*u,p=.35*u,m=.2*u,{cssStyles:g}=t,y,v=`M0 0 + a${d},${d} 0 0,1 ${u*.25},${-1*u*.1} + a${p},${p} 1 0,1 ${u*.4},${-1*u*.1} + a${f},${f} 1 0,1 ${u*.35},${u*.2} + + a${d},${d} 1 0,1 ${u*.15},${h*.35} + a${m},${m} 1 0,1 ${-1*u*.15},${h*.65} + + a${f},${d} 1 0,1 ${-1*u*.25},${u*.15} + a${p},${p} 1 0,1 ${-1*u*.5},0 + a${d},${d} 1 0,1 ${-1*u*.25},${-1*u*.15} + + a${d},${d} 1 0,1 ${-1*u*.1},${-1*h*.35} + a${m},${m} 1 0,1 ${u*.1},${-1*h*.65} + H0 V0 Z`;if(t.look==="handDrawn"){let x=ut.svg(i),b=dt(t,{}),T=x.path(v,b);y=i.insert(()=>T,":first-child"),y.attr("class","basic label-container").attr("style",kn(g))}else y=i.insert("path",":first-child").attr("class","basic label-container").attr("style",n).attr("d",v);return l.attr("transform",`translate(${-a.width/2}, ${-a.height/2})`),y.attr("transform",`translate(${-u/2}, ${-h/2})`),pt(t,y),t.calcIntersect=function(x,b){return ht.rect(x,b)},t.intersect=function(x){return Z.info("Cloud intersect",t,x),ht.rect(t,x)},i}var Nce=F(()=>{"use strict";tr();vt();Qt();nr();Jt();Kt();o(Mce,"cloud")});async function Pce(e,t){let{labelStyles:r,nodeStyles:n}=ct(t);t.labelStyle=r;let{shapeSvg:i,bbox:a,halfPadding:s,label:l}=await It(e,t,Dt(t)),u=a.width+8*s,h=a.height+2*s,d=5,f=t.look==="neo"?` + M${-u/2} ${h/2-d} + v${-h+2*d} + q0,-${d} ${d},-${d} + h${u-2*d} + q${d},0 ${d},${d} + v${h-d} + H${-u/2} + Z + `:` + M${-u/2} ${h/2-d} + v${-h+2*d} + q0,-${d} ${d},-${d} + h${u-2*d} + q${d},0 ${d},${d} + v${h-2*d} + q0,${d} ${-d},${d} + h${-(u-2*d)} + q${-d},0 ${-d},${-d} + Z + `;if(!t.domId)throw new Error(`defaultMindmapNode: node "${t.id}" is missing a domId \u2014 was render.ts domId prefixing skipped?`);let p=i.append("path").attr("id",t.domId).attr("class","node-bkg node-"+t.type).attr("style",n).attr("d",f);return i.append("line").attr("class","node-line-").attr("x1",-u/2).attr("y1",h/2).attr("x2",u/2).attr("y2",h/2),l.attr("transform",`translate(${-a.width/2}, ${-a.height/2})`),i.append(()=>l.node()),pt(t,p),t.calcIntersect=function(m,g){return ht.rect(m,g)},t.intersect=function(m){return ht.rect(t,m)},i}var Oce=F(()=>{"use strict";nr();Jt();Kt();o(Pce,"defaultMindmapNode")});async function Bce(e,t){let r={padding:t.padding??0};return z4(e,t,r)}var $ce=F(()=>{"use strict";fO();o(Bce,"mindmapCircle")});function Fce(e){return e in gO}var LQe,DQe,gO,yO=F(()=>{"use strict";ooe();uoe();doe();poe();fO();goe();voe();boe();Coe();koe();Roe();Loe();Ioe();Noe();Ooe();zoe();Voe();qoe();Uoe();joe();Koe();Qoe();ele();rle();ile();sle();lle();ule();dle();ple();vle();ble();Cle();kle();Ele();Rle();Lle();Ile();Nle();Ole();$le();zle();Vle();qle();Ule();jle();Kle();Qle();ece();rce();sce();lce();uce();pce();gce();vce();bce();Cce();Ece();Rce();Lce();Ice();Nce();Oce();$ce();LQe=[{semanticName:"Process",name:"Rectangle",shortName:"rect",description:"Standard process shape",aliases:["proc","process","rectangle"],internalAliases:["squareRect"],handler:Fle},{semanticName:"Event",name:"Rounded Rectangle",shortName:"rounded",description:"Represents an event",aliases:["event"],internalAliases:["roundedRect"],handler:Mle},{semanticName:"Terminal Point",name:"Stadium",shortName:"stadium",description:"Terminal point",aliases:["terminal","pill"],handler:Gle},{semanticName:"Subprocess",name:"Framed Rectangle",shortName:"fr-rect",description:"Subprocess",aliases:["subprocess","subproc","framed-rectangle","subroutine"],handler:Xle},{semanticName:"Database",name:"Cylinder",shortName:"cyl",description:"Database storage",aliases:["db","database","cylinder"],handler:Aoe},{semanticName:"Data Store",name:"Data Store",shortName:"datastore",description:"Data flow diagram data store",aliases:["data-store"],handler:_oe},{semanticName:"Start",name:"Circle",shortName:"circle",description:"Starting point",aliases:["circ"],handler:z4},{semanticName:"Bang",name:"Bang",shortName:"bang",description:"Bang",aliases:["bang"],handler:Dce},{semanticName:"Cloud",name:"Cloud",shortName:"cloud",description:"cloud",aliases:["cloud"],handler:Mce},{semanticName:"Decision",name:"Diamond",shortName:"diam",description:"Decision-making step",aliases:["decision","diamond","question"],handler:Ale},{semanticName:"Prepare Conditional",name:"Hexagon",shortName:"hex",description:"Preparation or condition step",aliases:["hexagon","prepare"],handler:Hoe},{semanticName:"Data Input/Output",name:"Lean Right",shortName:"lean-r",description:"Represents input or output",aliases:["lean-right","in-out"],internalAliases:["lean_right"],handler:hle},{semanticName:"Data Input/Output",name:"Lean Left",shortName:"lean-l",description:"Represents output or input",aliases:["lean-left","out-in"],internalAliases:["lean_left"],handler:cle},{semanticName:"Priority Action",name:"Trapezoid Base Bottom",shortName:"trap-b",description:"Priority action",aliases:["priority","trapezoid-bottom","trapezoid"],handler:oce},{semanticName:"Manual Operation",name:"Trapezoid Base Top",shortName:"trap-t",description:"Represents a manual task",aliases:["manual","trapezoid-top","inv-trapezoid"],internalAliases:["inv_trapezoid"],handler:ale},{semanticName:"Stop",name:"Double Circle",shortName:"dbl-circ",description:"Represents a stop point",aliases:["double-circle"],internalAliases:["doublecircle"],handler:Moe},{semanticName:"Text Block",name:"Text Block",shortName:"text",description:"Text block",handler:tce},{semanticName:"Card",name:"Notched Rectangle",shortName:"notch-rect",description:"Represents a card",aliases:["card","notched-rectangle"],handler:hoe},{semanticName:"Lined/Shaded Process",name:"Lined Rectangle",shortName:"lin-rect",description:"Lined process shape",aliases:["lined-rectangle","lined-process","lin-proc","shaded-process"],handler:Ple},{semanticName:"Start",name:"Small Circle",shortName:"sm-circ",description:"Small starting point",aliases:["start","small-circle"],internalAliases:["stateStart"],handler:Yle},{semanticName:"Stop",name:"Framed Circle",shortName:"fr-circ",description:"Stop point",aliases:["stop","framed-circle"],internalAliases:["stateEnd"],handler:Hle},{semanticName:"Fork/Join",name:"Filled Rectangle",shortName:"fork",description:"Fork or join in process flow",aliases:["join"],internalAliases:["forkJoin"],handler:Goe},{semanticName:"Collate",name:"Hourglass",shortName:"hourglass",description:"Represents a collate operation",aliases:["hourglass","collate"],handler:Yoe},{semanticName:"Comment",name:"Curly Brace",shortName:"brace",description:"Adds a comment",aliases:["comment","brace-l"],handler:yoe},{semanticName:"Comment Right",name:"Curly Brace",shortName:"brace-r",description:"Adds a comment",handler:xoe},{semanticName:"Comment with braces on both sides",name:"Curly Braces",shortName:"braces",description:"Adds a comment",handler:Toe},{semanticName:"Com Link",name:"Lightning Bolt",shortName:"bolt",description:"Communication link",aliases:["com-link","lightning-bolt"],handler:fle},{semanticName:"Document",name:"Document",shortName:"doc",description:"Represents a document",aliases:["doc","document"],handler:mce},{semanticName:"Delay",name:"Half-Rounded Rectangle",shortName:"delay",description:"Represents a delay",aliases:["half-rounded-rectangle"],handler:Woe},{semanticName:"Direct Access Storage",name:"Horizontal Cylinder",shortName:"h-cyl",description:"Direct access storage",aliases:["das","horizontal-cylinder"],handler:ace},{semanticName:"Disk Storage",name:"Lined Cylinder",shortName:"lin-cyl",description:"Disk storage",aliases:["disk","lined-cylinder"],handler:yle},{semanticName:"Display",name:"Curved Trapezoid",shortName:"curv-trap",description:"Represents a display",aliases:["curved-trapezoid","display"],handler:woe},{semanticName:"Divided Process",name:"Divided Rectangle",shortName:"div-rect",description:"Divided process shape",aliases:["div-proc","divided-rectangle","divided-process"],handler:Doe},{semanticName:"Extract",name:"Triangle",shortName:"tri",description:"Extraction process",aliases:["extract","triangle"],handler:fce},{semanticName:"Internal Storage",name:"Window Pane",shortName:"win-pane",description:"Internal storage",aliases:["internal-storage","window-pane"],handler:xce},{semanticName:"Junction",name:"Filled Circle",shortName:"f-circ",description:"Junction point",aliases:["junction","filled-circle"],handler:Poe},{semanticName:"Loop Limit",name:"Trapezoidal Pentagon",shortName:"notch-pent",description:"Loop limit step",aliases:["loop-limit","notched-pentagon"],handler:cce},{semanticName:"Manual File",name:"Flipped Triangle",shortName:"flip-tri",description:"Manual file operation",aliases:["manual-file","flipped-triangle"],handler:Foe},{semanticName:"Manual Input",name:"Sloped Rectangle",shortName:"sl-rect",description:"Manual input step",aliases:["manual-input","sloped-rectangle"],handler:Ble},{semanticName:"Multi-Document",name:"Stacked Document",shortName:"docs",description:"Multiple documents",aliases:["documents","st-doc","stacked-document"],handler:wle},{semanticName:"Multi-Process",name:"Stacked Rectangle",shortName:"st-rect",description:"Multiple processes",aliases:["procs","processes","stacked-rectangle"],handler:Tle},{semanticName:"Stored Data",name:"Bow Tie Rectangle",shortName:"bow-rect",description:"Stored data",aliases:["stored-data","bow-tie-rectangle"],handler:coe},{semanticName:"Summary",name:"Crossed Circle",shortName:"cross-circ",description:"Summary",aliases:["summary","crossed-circle"],handler:moe},{semanticName:"Tagged Document",name:"Tagged Document",shortName:"tag-doc",description:"Tagged document",aliases:["tag-doc","tagged-document"],handler:Jle},{semanticName:"Tagged Process",name:"Tagged Rectangle",shortName:"tag-rect",description:"Tagged process",aliases:["tagged-rectangle","tag-proc","tagged-process"],handler:Zle},{semanticName:"Paper Tape",name:"Flag",shortName:"flag",description:"Paper tape",aliases:["paper-tape"],handler:yce},{semanticName:"Odd",name:"Odd",shortName:"odd",description:"Odd shape",internalAliases:["rect_left_inv_arrow"],handler:_le},{semanticName:"Lined Document",name:"Lined Document",shortName:"lin-doc",description:"Lined document",aliases:["lined-document"],handler:xle}],DQe=o(()=>{let t=[...Object.entries({state:Wle,choice:foe,note:Sle,rectWithTitle:Dle,labelRect:ole,iconSquare:tle,iconCircle:Zoe,icon:Xoe,iconRounded:Joe,imageSquare:nle,anchor:soe,kanbanItem:_ce,mindmapCircle:Bce,defaultMindmapNode:Pce,classBox:Sce,erBox:mO,requirementBox:Ace}),...LQe.flatMap(r=>[r.shortName,..."aliases"in r?r.aliases:[],..."internalAliases"in r?r.internalAliases:[]].map(i=>[i,r.handler]))];return Object.fromEntries(t)},"generateShapeMap"),gO=DQe();o(Fce,"isValidShape")});var IQe,W4,zce=F(()=>{"use strict";$r();R2();Xt();vt();yO();Qt();Vr();Nn();Ud();H0();IQe="flowchart-",W4=class{constructor(){this.vertexCounter=0;this.config=Ae();this.diagramId="";this.vertices=new Map;this.edges=[];this.classes=new Map;this.subGraphs=[];this.subGraphLookup=new Map;this.tooltips=new Map;this.subCount=0;this.firstGraphFlag=!0;this.secCount=-1;this.posCrossRef=[];this.funs=[];this.setAccTitle=kr;this.setAccDescription=Rr;this.setDiagramTitle=Or;this.getAccTitle=Ar;this.getAccDescription=_r;this.getDiagramTitle=Lr;this.funs.push(this.setupToolTips.bind(this)),this.addVertex=this.addVertex.bind(this),this.firstGraph=this.firstGraph.bind(this),this.setDirection=this.setDirection.bind(this),this.addSubGraph=this.addSubGraph.bind(this),this.addLink=this.addLink.bind(this),this.setLink=this.setLink.bind(this),this.updateLink=this.updateLink.bind(this),this.addClass=this.addClass.bind(this),this.setClass=this.setClass.bind(this),this.destructLink=this.destructLink.bind(this),this.setClickEvent=this.setClickEvent.bind(this),this.setTooltip=this.setTooltip.bind(this),this.updateLinkInterpolate=this.updateLinkInterpolate.bind(this),this.setClickFun=this.setClickFun.bind(this),this.bindFunctions=this.bindFunctions.bind(this),this.lex={firstGraph:this.firstGraph.bind(this)},this.clear(),this.setGen("gen-2")}static{o(this,"FlowDB")}sanitizeText(t){return xt.sanitizeText(t,this.config)}sanitizeNodeLabelType(t){switch(t){case"markdown":case"string":case"text":return t;default:return"markdown"}}setDiagramId(t){this.diagramId=t}lookUpDomId(t){for(let r of this.vertices.values())if(r.id===t)return this.diagramId?`${this.diagramId}-${r.domId}`:r.domId;return this.diagramId?`${this.diagramId}-${t}`:t}addVertex(t,r,n,i,a,s,l={},u){if(!t||t.trim().length===0)return;let h;if(u!==void 0){let m;u.includes(` +`)?m=u+` +`:m=`{ +`+u+` +}`,h=Jd(m,{schema:Qd})}let d=this.edges.find(m=>m.id===t);if(d){let m=h;m?.animate!==void 0&&(d.animate=m.animate),m?.animation!==void 0&&(d.animation=m.animation),m?.curve!==void 0&&(d.interpolate=m.curve);return}let f,p=this.vertices.get(t);if(p===void 0&&(r===void 0&&n===void 0&&i!==void 0&&i!==null&&Z.warn(`Style applied to unknown node "${t}". This may indicate a typo. The node will be created automatically.`),p={id:t,labelType:"text",domId:IQe+t+"-"+this.vertexCounter,styles:[],classes:[]},this.vertices.set(t,p)),this.vertexCounter++,r!==void 0?(this.config=Ae(),f=this.sanitizeText(r.text.trim()),p.labelType=r.type,f.startsWith('"')&&f.endsWith('"')&&(f=f.substring(1,f.length-1)),p.text=f):p.text===void 0&&(p.text=t),n!==void 0&&(p.type=n),i?.forEach(m=>{p.styles.push(m)}),a?.forEach(m=>{p.classes.push(m)}),s!==void 0&&(p.dir=s),p.props===void 0?p.props=l:l!==void 0&&Object.assign(p.props,l),h!==void 0){if(h.shape){if(h.shape!==h.shape.toLowerCase()||h.shape.includes("_"))throw new Error(`No such shape: ${h.shape}. Shape names should be lowercase.`);if(!Fce(h.shape))throw new Error(`No such shape: ${h.shape}.`);p.type=h?.shape}h?.label&&(p.text=h?.label,p.labelType=this.sanitizeNodeLabelType(h?.labelType)),h?.icon&&(p.icon=h?.icon,!h.label?.trim()&&p.text===t&&(p.text="")),h?.form&&(p.form=h?.form),h?.pos&&(p.pos=h?.pos),h?.img&&(p.img=h?.img,!h.label?.trim()&&p.text===t&&(p.text="")),h?.constraint&&(p.constraint=h.constraint),h.w&&(p.assetWidth=Number(h.w)),h.h&&(p.assetHeight=Number(h.h))}}addSingleLink(t,r,n,i){let l={start:t,end:r,type:void 0,text:"",labelType:"text",classes:[],isUserDefinedId:!1,interpolate:this.edges.defaultInterpolate};Z.info("abc78 Got edge...",l);let u=n.text;if(u!==void 0&&(l.text=this.sanitizeText(u.text.trim()),l.text.startsWith('"')&&l.text.endsWith('"')&&(l.text=l.text.substring(1,l.text.length-1)),l.labelType=this.sanitizeNodeLabelType(u.type)),n!==void 0&&(l.type=n.type,l.stroke=n.stroke,l.length=n.length>10?10:n.length),i&&!this.edges.some(h=>h.id===i))l.id=i,l.isUserDefinedId=!0;else{let h=this.edges.filter(d=>d.start===l.start&&d.end===l.end);h.length===0?l.id=eu(l.start,l.end,{counter:0,prefix:"L"}):l.id=eu(l.start,l.end,{counter:h.length+1,prefix:"L"})}if(this.edges.length<(this.config.maxEdges??500))Z.info("Pushing edge..."),this.edges.push(l);else throw new Error(`Edge limit exceeded. ${this.edges.length} edges found, but the limit is ${this.config.maxEdges}. + +Initialize mermaid with maxEdges set to a higher number to allow more edges. +You cannot set this config via configuration inside the diagram as it is a secure config. +You have to call mermaid.initialize.`)}isLinkData(t){return t!==null&&typeof t=="object"&&"id"in t&&typeof t.id=="string"}addLink(t,r,n){let i=this.isLinkData(n)?n.id.replace("@",""):void 0;Z.info("addLink",t,r,i);for(let a of t)for(let s of r){let l=a===t[t.length-1],u=s===r[0];l&&u?this.addSingleLink(a,s,n,i):this.addSingleLink(a,s,n,void 0)}}updateLinkInterpolate(t,r){t.forEach(n=>{n==="default"?this.edges.defaultInterpolate=r:this.edges[n].interpolate=r})}updateLink(t,r){t.forEach(n=>{if(typeof n=="number"&&n>=this.edges.length)throw new Error(`The index ${n} for linkStyle is out of bounds. Valid indices for linkStyle are between 0 and ${this.edges.length-1}. (Help: Ensure that the index is within the range of existing edges.)`);n==="default"?this.edges.defaultStyle=r:(this.edges[n].style=r,(this.edges[n]?.style?.length??0)>0&&!this.edges[n]?.style?.some(i=>i?.startsWith("fill"))&&this.edges[n]?.style?.push("fill:none"))})}addClass(t,r){let n=r.join().replace(/\\,/g,"\xA7\xA7\xA7").replace(/,/g,";").replace(/§§§/g,",").split(";");t.split(",").forEach(i=>{let a=this.classes.get(i);a===void 0&&(a={id:i,styles:[],textStyles:[]},this.classes.set(i,a)),n?.forEach(s=>{if(/color/.exec(s)){let l=s.replace("fill","bgFill");a.textStyles.push(l)}a.styles.push(s)})})}setDirection(t){this.direction=t.trim(),/.*/.exec(this.direction)&&(this.direction="LR"),/.*v/.exec(this.direction)&&(this.direction="TB"),this.direction==="TD"&&(this.direction="TB")}setClass(t,r){for(let n of t.split(",")){let i=this.vertices.get(n);i&&i.classes.push(r);let a=this.edges.find(l=>l.id===n);a&&a.classes.push(r);let s=this.subGraphLookup.get(n);s&&s.classes.push(r)}}setTooltip(t,r){if(r!==void 0){r=this.sanitizeText(r);for(let n of t.split(","))this.tooltips.set(this.version==="gen-1"?this.lookUpDomId(n):n,r)}}setClickFun(t,r,n){if(Ae().securityLevel!=="loose"||r===void 0)return;let i=[];if(typeof n=="string"){i=n.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);for(let s=0;s{let s=this.lookUpDomId(t),l=document.querySelector(`[id="${s}"]`);l!==null&&l.addEventListener("click",()=>{Zt.runFunc(r,...i)},!1)}))}setLink(t,r,n){t.split(",").forEach(i=>{let a=this.vertices.get(i);a!==void 0&&(a.link=Zt.formatUrl(r,this.config),a.linkTarget=n)}),this.setClass(t,"clickable")}getTooltip(t){return this.tooltips.get(t)}setClickEvent(t,r,n){t.split(",").forEach(i=>{this.setClickFun(i,r,n)}),this.setClass(t,"clickable")}bindFunctions(t){this.funs.forEach(r=>{r(t)})}getDirection(){return this.direction?.trim()}getVertices(){return this.vertices}getEdges(){return this.edges}getClasses(){return this.classes}setupToolTips(t){let r=Dy();et(t).select("svg").selectAll("g.node").on("mouseover",a=>{let s=et(a.currentTarget),l=s.attr("title");if(l===null)return;let u=a.currentTarget?.getBoundingClientRect();r.transition().duration(200).style("opacity",".9"),r.text(s.attr("title")).style("left",window.scrollX+u.left+(u.right-u.left)/2+"px").style("top",window.scrollY+u.bottom+"px"),r.html(Zs.sanitize(l)),s.classed("hover",!0)}).on("mouseout",a=>{r.transition().duration(500).style("opacity",0),et(a.currentTarget).classed("hover",!1)})}clear(t="gen-2"){this.vertices=new Map,this.classes=new Map,this.edges=[],this.funs=[this.setupToolTips.bind(this)],this.diagramId="",this.subGraphs=[],this.subGraphLookup=new Map,this.subCount=0,this.tooltips=new Map,this.firstGraphFlag=!0,this.version=t,this.config=Ae(),yr()}setGen(t){this.version=t||"gen-2"}defaultStyle(){return"fill:#ffa;stroke: #f66; stroke-width: 3px; stroke-dasharray: 5, 5;fill:#ffa;stroke: #666;"}addSubGraph(t,r,n){let i=t.text.trim(),a=n.text;t===n&&/\s/.exec(n.text)&&(i=void 0);let l=o(g=>{let y={boolean:{},number:{},string:{}},v=[],x;return{nodeList:g.filter(function(T){let k=typeof T;return T.stmt&&T.stmt==="dir"?(x=T.value,!1):T.trim()===""?!1:k in y?y[k].hasOwnProperty(T)?!1:y[k][T]=!0:v.includes(T)?!1:v.push(T)}),dir:x}},"uniq")(r.flat()),u=l.nodeList,h=l.dir,d=h!==void 0,f=Ae().flowchart??{},p=h??(f.inheritDir?this.getDirection()??Ae().direction??void 0:void 0);if(this.version==="gen-1")for(let g=0;g2e3)return{result:!1,count:0};if(this.posCrossRef[this.secCount]=r,this.subGraphs[r].id===t)return{result:!0,count:0};let i=0,a=1;for(;i=0){let l=this.indexNodes2(t,s);if(l.result)return{result:!0,count:a+l.count};a=a+l.count}i=i+1}return{result:!1,count:a}}getDepthFirstPos(t){return this.posCrossRef[t]}indexNodes(){this.secCount=-1,this.subGraphs.length>0&&this.indexNodes2("none",this.subGraphs.length-1)}getSubGraphs(){return this.subGraphs}firstGraph(){return this.firstGraphFlag?(this.firstGraphFlag=!1,!0):!1}destructStartLink(t){let r=t.trim(),n="arrow_open";switch(r[0]){case"<":n="arrow_point",r=r.slice(1);break;case"x":n="arrow_cross",r=r.slice(1);break;case"o":n="arrow_circle",r=r.slice(1);break}let i="normal";return r.includes("=")&&(i="thick"),r.includes(".")&&(i="dotted"),{type:n,stroke:i}}countChar(t,r){let n=r.length,i=0;for(let a=0;a":i="arrow_point",r.startsWith("<")&&(i="double_"+i,n=n.slice(1));break;case"o":i="arrow_circle",r.startsWith("o")&&(i="double_"+i,n=n.slice(1));break}let a="normal",s=n.length-1;n.startsWith("=")&&(a="thick"),n.startsWith("~")&&(a="invisible");let l=this.countChar(".",n);return l&&(a="dotted",s=l),{type:i,stroke:a,length:s}}destructLink(t,r){let n=this.destructEndLink(t),i;if(r){if(i=this.destructStartLink(r),i.stroke!==n.stroke)return{type:"INVALID",stroke:"INVALID"};if(i.type==="arrow_open")i.type=n.type;else{if(i.type!==n.type)return{type:"INVALID",stroke:"INVALID"};i.type="double_"+i.type}return i.type==="double_arrow"&&(i.type="double_arrow_point"),i.length=n.length,i}return n}exists(t,r){for(let n of t)if(n.nodes.includes(r))return!0;return!1}makeUniq(t,r){let n=[];return t.nodes.forEach((i,a)=>{this.exists(r,i)||n.push(t.nodes[a])}),{nodes:n}}getTypeFromVertex(t){if(t.img)return"imageSquare";if(t.icon)return t.form==="circle"?"iconCircle":t.form==="square"?"iconSquare":t.form==="rounded"?"iconRounded":"icon";switch(t.type){case"square":case void 0:return"squareRect";case"round":return"roundedRect";case"ellipse":return"ellipse";default:return t.type}}findNode(t,r){return t.find(n=>n.id===r)}destructEdgeType(t){let r="none",n="arrow_point";switch(t){case"arrow_point":case"arrow_circle":case"arrow_cross":n=t;break;case"double_arrow_point":case"double_arrow_circle":case"double_arrow_cross":r=t.replace("double_",""),n=r;break}return{arrowTypeStart:r,arrowTypeEnd:n}}addNodeFromVertex(t,r,n,i,a,s){let l=n.get(t.id),u=i.get(t.id)??!1,h=this.findNode(r,t.id);if(h)h.cssStyles=t.styles,h.cssCompiledStyles=this.getCompiledStyles(t.classes),h.cssClasses=t.classes.join(" ");else{let d={id:t.id,label:t.text,labelType:t.labelType,labelStyle:"",parentId:l,padding:a.flowchart?.padding||8,cssStyles:t.styles,cssCompiledStyles:this.getCompiledStyles(["default","node",...t.classes]),cssClasses:"default "+t.classes.join(" "),dir:t.dir,domId:t.domId,look:s,link:t.link,linkTarget:t.linkTarget,tooltip:this.getTooltip(t.id),icon:t.icon,pos:t.pos,img:t.img,assetWidth:t.assetWidth,assetHeight:t.assetHeight,constraint:t.constraint};u?r.push({...d,isGroup:!0,shape:"rect"}):r.push({...d,isGroup:!1,shape:this.getTypeFromVertex(t)})}}getCompiledStyles(t){let r=[];for(let n of t){let i=this.classes.get(n);i?.styles&&(r=[...r,...i.styles??[]].map(a=>a.trim())),i?.textStyles&&(r=[...r,...i.textStyles??[]].map(a=>a.trim()))}return r}getData(){let t=Ae(),r=[],n=[],i=this.getSubGraphs(),a=new Map,s=new Map;for(let h=i.length-1;h>=0;h--){let d=i[h];d.nodes.length>0&&s.set(d.id,!0);for(let f of d.nodes)a.set(f,d.id)}for(let h=i.length-1;h>=0;h--){let d=i[h];r.push({id:d.id,label:d.title,labelStyle:"",labelType:d.labelType,parentId:a.get(d.id),padding:8,cssCompiledStyles:this.getCompiledStyles(d.classes),cssClasses:d.classes.join(" "),shape:"rect",dir:d.dir==="TD"?"TB":d.dir,explicitDir:d.hasExplicitDir,isGroup:!0,look:t.look})}this.getVertices().forEach(h=>{this.addNodeFromVertex(h,r,a,s,t,t.look||"classic")});let u=this.getEdges();return u.forEach((h,d)=>{let{arrowTypeStart:f,arrowTypeEnd:p}=this.destructEdgeType(h.type),m=[...u.defaultStyle??[]];h.style&&m.push(...h.style);let g={id:eu(h.start,h.end,{counter:d,prefix:"L"},h.id),isUserDefinedId:h.isUserDefinedId,start:h.start,end:h.end,type:h.type??"normal",label:h.text,labelType:h.labelType,labelpos:"c",thickness:h.stroke,minlen:h.length,classes:h?.stroke==="invisible"?"":"edge-thickness-normal edge-pattern-solid flowchart-link",arrowTypeStart:h?.stroke==="invisible"||h?.type==="arrow_open"?"none":f,arrowTypeEnd:h?.stroke==="invisible"||h?.type==="arrow_open"?"none":p,arrowheadStyle:"fill: #333",cssCompiledStyles:this.getCompiledStyles(h.classes),labelStyle:m,style:m,pattern:h.stroke,look:t.look,animate:h.animate,animation:h.animation,curve:h.interpolate||this.edges.defaultInterpolate||t.flowchart?.curve};n.push(g)}),{nodes:r,edges:n,other:{},config:t}}defaultConfig(){return bS.flowchart}}});var pl,Rm=F(()=>{"use strict";$r();pl=o((e,t)=>{let r;return t==="sandbox"&&(r=et("#i"+e)),(t==="sandbox"?et(r.nodes()[0].contentDocument.body):et("body")).select(`[id="${e}"]`)},"getDiagramElement")});var oc,Vy=F(()=>{"use strict";oc=o(({flowchart:e})=>{let t=e?.subGraphTitleMargin?.top??0,r=e?.subGraphTitleMargin?.bottom??0,n=t+r;return{subGraphTitleTopMargin:t,subGraphTitleBottomMargin:r,subGraphTitleTotalMargin:n}},"getSubGraphTitleMargins")});var Gce,Vce=F(()=>{"use strict";Xt();ur();vt();$r();tr();Ls();_4();Jt();Gce=o(async(e,t)=>{let r=Ae(),{themeVariables:n,handDrawnSeed:i}=r,{clusterBkg:a,clusterBorder:s}=n,l=s,{labelStyles:u,nodeStyles:h,borderStyles:d,backgroundStyles:f}=ct(t),p=e.insert("g").attr("class","cluster swimlane "+(t.cssClasses||"")).attr("id",t.id).attr("data-id",t.id).attr("data-et","cluster").attr("data-look",t.look),m=ya(r.flowchart.htmlLabels),g=t.direction==="LR",y=p.insert("g").attr("class","cluster-label swimlane-label"),v=await Pn(y,t.label,{style:t.labelStyle,useHtmlLabels:m,isNode:!0,width:t.width}),x=v.getBBox();if(m){let A=v.children[0],M=et(v);x=A.getBoundingClientRect(),M.attr("width",x.width),M.attr("height",x.height)}let b=t.padding??0,T=t.width<=x.width+b?x.width+b:t.width;t.width<=x.width+b?t.diff=(T-t.width)/2-b:t.diff=-b;let k=t.height,C=t.y-k/2,w=t.y+k/2,S=t.x-T/2,R=t.swimlaneContentTop!==void 0?t.swimlaneContentTop:C+k/3,L=g?4:0,N=x.height+2*L,I,_;if(g){let A=Math.max(N,x.height+2*L),M=S+A,D=Math.max(0,T-A);if(t.look==="handDrawn"){let O=ut.svg(p),$=dt(t,{roughness:.7,fill:a,stroke:l,fillWeight:3,seed:i}),V=dt(t,{roughness:.7,fill:"none",stroke:l,seed:i}),G=O.rectangle(S,C,A,k,$);I=p.insert(()=>G,":first-child");let z=O.rectangle(M,C,D,k,V);_=p.insert(()=>z,":first-child"),I.select("path:nth-child(2)").attr("style",d.join(";")),I.select("path").attr("style",f.join(";").replace("fill","stroke"))}else I=p.insert("rect",":first-child"),_=p.insert("rect",":first-child"),I.attr("class","swimlane-title").attr("style",h).attr("x",S).attr("y",C).attr("width",A).attr("height",k).attr("fill",a).attr("stroke",l),_.attr("class","swimlane-body").attr("style",h).attr("x",M).attr("y",C).attr("width",D).attr("height",k).attr("fill","none").attr("stroke",l);let P=S+A/2,B=t.y;y.attr("transform",`translate(${P}, ${B}) rotate(-90) translate(${-x.width/2}, ${-x.height/2})`)}else{let A=Math.max(0,R-C),M=Math.min(N,A),D=C+M,P=Math.max(0,w-D),B=t.x-T/2;if(t.look==="handDrawn"){let V=ut.svg(p),G=dt(t,{roughness:.7,fill:a,stroke:l,fillWeight:3,seed:i}),z=dt(t,{roughness:.7,fill:"none",stroke:l,seed:i}),W=V.rectangle(B,C,T,M,G);I=p.insert(()=>W,":first-child");let H=V.rectangle(B,D,T,P,z);_=p.insert(()=>H,":first-child"),I.select("path:nth-child(2)").attr("style",d.join(";")),I.select("path").attr("style",f.join(";").replace("fill","stroke"))}else I=p.insert("rect",":first-child"),_=p.insert("rect",":first-child"),I.attr("class","swimlane-title").attr("style",h).attr("x",B).attr("y",C).attr("width",T).attr("height",M).attr("fill",a).attr("stroke",l),_.attr("class","swimlane-body").attr("style",h).attr("x",B).attr("y",D).attr("width",T).attr("height",P).attr("fill","none").attr("stroke",l);let O=t.x-x.width/2,$=C+(M-x.height)/2;y.attr("transform",`translate(${O}, ${$})`)}if(Z.trace("Swimlane data ",t,JSON.stringify(t)),u){let A=y.select("span");A&&A.attr("style",u)}return t.offsetX=0,t.width=T,t.height=k,t.offsetY=x.height-b/2,t.intersect=function(A){return nu(t,A)},{cluster:p,labelBBox:x}},"swimlane")});var Wce,MQe,NQe,PQe,OQe,BQe,$Qe,qce,nf,q4,Wy=F(()=>{"use strict";Xt();ur();vt();Vy();$r();tr();Ls();_4();G4();Sm();Jt();Vce();Wce=o(async(e,t)=>{Z.info("Creating subgraph rect for ",t.id,t);let r=Ae(),{themeVariables:n,handDrawnSeed:i}=r,{clusterBkg:a,clusterBorder:s}=n,{labelStyles:l,nodeStyles:u,borderStyles:h,backgroundStyles:d}=ct(t),f=e.insert("g").attr("class","cluster "+t.cssClasses).attr("id",t.domId).attr("data-look",t.look),p=Gr(r),m=f.insert("g").attr("class","cluster-label "),g;t.labelType==="markdown"?g=await Pn(m,t.label,{style:t.labelStyle,useHtmlLabels:p,isNode:!0,width:t.width}):g=await sc(m,t.label,t.labelStyle||"",!1,!0);let y=g.getBBox();if(Gr(r)){let S=g.children[0],R=et(g);y=S.getBoundingClientRect(),R.attr("width",y.width),R.attr("height",y.height)}let v=t.width<=y.width+t.padding?y.width+t.padding:t.width;t.width<=y.width+t.padding?t.diff=(v-t.width)/2-t.padding:t.diff=-t.padding;let x=t.height,b=t.x-v/2,T=t.y-x/2;Z.trace("Data ",t,JSON.stringify(t));let k;if(t.look==="handDrawn"){let S=ut.svg(f),R=dt(t,{roughness:.7,fill:a,stroke:s,fillWeight:3,seed:i}),L=S.path(io(b,T,v,x,0),R);k=f.insert(()=>(Z.debug("Rough node insert CXC",L),L),":first-child"),k.select("path:nth-child(2)").attr("style",h.join(";")),k.select("path").attr("style",d.join(";").replace("fill","stroke"))}else k=f.insert("rect",":first-child"),k.attr("style",u).attr("rx",t.rx).attr("ry",t.ry).attr("x",b).attr("y",T).attr("width",v).attr("height",x);let{subGraphTitleTopMargin:C}=oc(r);if(m.attr("transform",`translate(${t.x-y.width/2}, ${t.y-t.height/2+C})`),l){let S=m.select("span");S&&S.attr("style",l)}let w=k.node().getBBox();return t.offsetX=0,t.width=w.width,t.height=w.height,t.offsetY=y.height-t.padding/2,t.intersect=function(S){return nu(t,S)},{cluster:f,labelBBox:y}},"rect"),MQe=o((e,t)=>{let r=e.insert("g").attr("class","note-cluster").attr("id",t.domId),n=r.insert("rect",":first-child"),i=0*t.padding,a=i/2;n.attr("rx",t.rx).attr("ry",t.ry).attr("x",t.x-t.width/2-a).attr("y",t.y-t.height/2-a).attr("width",t.width+i).attr("height",t.height+i).attr("fill","none");let s=n.node().getBBox();return t.width=s.width,t.height=s.height,t.intersect=function(l){return nu(t,l)},{cluster:r,labelBBox:{width:0,height:0}}},"noteGroup"),NQe=o(async(e,t)=>{let r=Ae(),{themeVariables:n,handDrawnSeed:i}=r,{altBackground:a,compositeBackground:s,compositeTitleBackground:l,nodeBorder:u}=n,h=e.insert("g").attr("class",t.cssClasses).attr("id",t.domId).attr("data-id",t.id).attr("data-look",t.look),d=h.insert("g",":first-child"),f=h.insert("g").attr("class","cluster-label"),p=h.append("rect"),m=await sc(f,t.label,t.labelStyle,void 0,!0),g=m.getBBox();if(Gr(r)){let L=m.children[0],N=et(m);g=L.getBoundingClientRect(),N.attr("width",g.width),N.attr("height",g.height)}let y=0*t.padding,v=y/2,x=(t.width<=g.width+t.padding?g.width+t.padding:t.width)+y;t.width<=g.width+t.padding?t.diff=(x-t.width)/2-t.padding:t.diff=-t.padding;let b=t.height+y,T=t.height+y-g.height-6,k=t.x-x/2,C=t.y-b/2;t.width=x;let w=t.y-t.height/2-v+g.height+2,S;if(t.look==="handDrawn"){let L=t.cssClasses.includes("statediagram-cluster-alt"),N=ut.svg(h),I=t.rx||t.ry?N.path(io(k,C,x,b,10),{roughness:.7,fill:l,fillStyle:"solid",stroke:u,seed:i}):N.rectangle(k,C,x,b,{seed:i});S=h.insert(()=>I,":first-child");let _=N.rectangle(k,w,x,T,{fill:L?a:s,fillStyle:L?"hachure":"solid",stroke:u,seed:i});S=h.insert(()=>I,":first-child"),p=h.insert(()=>_)}else S=d.insert("rect",":first-child"),S.attr("class","outer").attr("x",k).attr("y",C).attr("width",x).attr("height",b).attr("data-look",t.look),p.attr("class","inner").attr("x",k).attr("y",w).attr("width",x).attr("height",T);f.attr("transform",`translate(${t.x-g.width/2}, ${C+1-(Gr(r)?0:3)})`);let R=S.node().getBBox();return t.height=R.height,t.offsetX=0,t.offsetY=g.height-t.padding/2,t.labelBBox=g,t.intersect=function(L){return nu(t,L)},{cluster:h,labelBBox:g}},"roundedWithTitle"),PQe=o(async(e,t)=>{Z.info("Creating subgraph rect for ",t.id,t);let r=Ae(),{themeVariables:n,handDrawnSeed:i}=r,{clusterBkg:a,clusterBorder:s}=n,{labelStyles:l,nodeStyles:u,borderStyles:h,backgroundStyles:d}=ct(t),f=e.insert("g").attr("class","cluster "+t.cssClasses).attr("id",t.domId).attr("data-look",t.look),p=Gr(r),m=f.insert("g").attr("class","cluster-label "),g=await Pn(m,t.label,{style:t.labelStyle,useHtmlLabels:p,isNode:!0,width:t.width}),y=g.getBBox();if(Gr(r)){let S=g.children[0],R=et(g);y=S.getBoundingClientRect(),R.attr("width",y.width),R.attr("height",y.height)}let v=t.width<=y.width+t.padding?y.width+t.padding:t.width;t.width<=y.width+t.padding?t.diff=(v-t.width)/2-t.padding:t.diff=-t.padding;let x=t.height,b=t.x-v/2,T=t.y-x/2;Z.trace("Data ",t,JSON.stringify(t));let k;if(t.look==="handDrawn"){let S=ut.svg(f),R=dt(t,{roughness:.7,fill:a,stroke:s,fillWeight:4,seed:i}),L=S.path(io(b,T,v,x,t.rx),R);k=f.insert(()=>(Z.debug("Rough node insert CXC",L),L),":first-child"),k.select("path:nth-child(2)").attr("style",h.join(";")),k.select("path").attr("style",d.join(";").replace("fill","stroke"))}else k=f.insert("rect",":first-child"),k.attr("style",u).attr("rx",t.rx).attr("ry",t.ry).attr("x",b).attr("y",T).attr("width",v).attr("height",x);let{subGraphTitleTopMargin:C}=oc(r);if(m.attr("transform",`translate(${t.x-y.width/2}, ${t.y-t.height/2+C})`),l){let S=m.select("span");S&&S.attr("style",l)}let w=k.node().getBBox();return t.offsetX=0,t.width=w.width,t.height=w.height,t.offsetY=y.height-t.padding/2,t.intersect=function(S){return nu(t,S)},{cluster:f,labelBBox:y}},"kanbanSection"),OQe=o((e,t)=>{let r=Ae(),{themeVariables:n,handDrawnSeed:i}=r,{nodeBorder:a}=n,s=e.insert("g").attr("class",t.cssClasses).attr("id",t.domId).attr("data-look",t.look),l=s.insert("g",":first-child"),u=0*t.padding,h=t.width+u;t.diff=-t.padding;let d=t.height+u,f=t.x-h/2,p=t.y-d/2;t.width=h;let m;if(t.look==="handDrawn"){let v=ut.svg(s).rectangle(f,p,h,d,{fill:"lightgrey",roughness:.5,strokeLineDash:[5],stroke:a,seed:i});m=s.insert(()=>v,":first-child")}else{m=l.insert("rect",":first-child");let y="outer";t.look,y="divider",m.attr("class",y).attr("x",f).attr("y",p).attr("width",h).attr("height",d).attr("data-look",t.look)}let g=m.node().getBBox();return t.height=g.height,t.offsetX=0,t.offsetY=0,t.intersect=function(y){return nu(t,y)},{cluster:s,labelBBox:{}}},"divider"),BQe=Wce,$Qe={rect:Wce,squareRect:BQe,roundedWithTitle:NQe,noteGroup:MQe,divider:OQe,kanbanSection:PQe,swimlane:Gce},qce=new Map,nf=o(async(e,t)=>{let r=t.shape||"rect",n=await $Qe[r](e,t);return qce.set(t.id,n),n},"insertCluster"),q4=o(()=>{qce=new Map},"clear")});var ml,vO=F(()=>{"use strict";ml=o((e,t)=>{if(t)return"translate("+-e.width/2+", "+-e.height/2+")";let r=e.x??0,n=e.y??0;return"translate("+-(r+e.width/2)+", "+-(n+e.height/2)+")"},"computeLabelTransform")});function H4(e,t){if(e===void 0||t===void 0)return{angle:0,deltaX:0,deltaY:0};e=di(e),t=di(t);let[r,n]=[e.x,e.y],[i,a]=[t.x,t.y],s=i-r,l=a-n;return{angle:Math.atan(l/s),deltaX:s,deltaY:l}}var Yi,xO,di,U4,Y4=F(()=>{"use strict";Yi={aggregation:17.25,extension:17.25,composition:17.25,dependency:6,lollipop:13.5,arrow_point:4,arrow_barb:0,arrow_barb_neo:5.5},xO={arrow_point:4,arrow_cross:12.5,arrow_circle:12.5};o(H4,"calculateDeltaAndAngle");di=o(e=>Array.isArray(e)?{x:e[0],y:e[1]}:e,"pointTransformer"),U4=o(e=>({x:o(function(t,r,n){let i=0,a=di(n[0]).x=0?1:-1)}else if(r===n.length-1&&Object.hasOwn(Yi,e.arrowTypeEnd)){let{angle:m,deltaX:g}=H4(n[n.length-1],n[n.length-2]);i=Yi[e.arrowTypeEnd]*Math.cos(m)*(g>=0?1:-1)}let s=Math.abs(di(t).x-di(n[n.length-1]).x),l=Math.abs(di(t).y-di(n[n.length-1]).y),u=Math.abs(di(t).x-di(n[0]).x),h=Math.abs(di(t).y-di(n[0]).y),d=Yi[e.arrowTypeStart],f=Yi[e.arrowTypeEnd],p=1;if(s0&&l0&&h=0?1:-1)}else if(r===n.length-1&&Object.hasOwn(Yi,e.arrowTypeEnd)){let{angle:m,deltaY:g}=H4(n[n.length-1],n[n.length-2]);i=Yi[e.arrowTypeEnd]*Math.abs(Math.sin(m))*(g>=0?1:-1)}let s=Math.abs(di(t).y-di(n[n.length-1]).y),l=Math.abs(di(t).x-di(n[n.length-1]).x),u=Math.abs(di(t).y-di(n[0]).y),h=Math.abs(di(t).x-di(n[0]).x),d=Yi[e.arrowTypeStart],f=Yi[e.arrowTypeEnd],p=1;if(s0&&l0&&h{"use strict";vt();Uce=o((e,t,r,n,i,a=!1,s)=>{t.arrowTypeStart&&Hce(e,"start",t.arrowTypeStart,r,n,i,a,s),t.arrowTypeEnd&&Hce(e,"end",t.arrowTypeEnd,r,n,i,a,s)},"addEdgeMarkers"),FQe={arrow_cross:{type:"cross",fill:!1},arrow_point:{type:"point",fill:!0},arrow_barb:{type:"barb",fill:!0},arrow_barb_neo:{type:"barb",fill:!0},arrow_circle:{type:"circle",fill:!1},aggregation:{type:"aggregation",fill:!1},extension:{type:"extension",fill:!1},composition:{type:"composition",fill:!0},dependency:{type:"dependency",fill:!0},lollipop:{type:"lollipop",fill:!1},only_one:{type:"onlyOne",fill:!1},zero_or_one:{type:"zeroOrOne",fill:!1},one_or_more:{type:"oneOrMore",fill:!1},zero_or_more:{type:"zeroOrMore",fill:!1},requirement_arrow:{type:"requirement_arrow",fill:!1},requirement_contains:{type:"requirement_contains",fill:!1}},zQe=["cross","point","circle","lollipop","aggregation","extension","composition","dependency","barb"],Hce=o((e,t,r,n,i,a,s=!1,l)=>{let u=FQe[r],h=u&&zQe.includes(u.type);if(!u){Z.warn(`Unknown arrow type: ${r}`);return}let d=u.type,m=`${i}_${a}-${d}${t==="start"?"Start":"End"}${s&&h?"-margin":""}`;if(l&&l.trim()!==""){let g=l.replace(/[^\dA-Za-z]/g,"_"),y=`${m}_${g}`;if(!document.getElementById(y)){let v=document.getElementById(m);if(v){let x=v.cloneNode(!0);x.id=y,x.querySelectorAll("path, circle, line").forEach(T=>{T.setAttribute("stroke",l),u.fill&&T.setAttribute("fill",l)}),v.parentNode?.appendChild(x)}}e.attr(`marker-${t}`,`url(${n}#${y})`)}else e.attr(`marker-${t}`,`url(${n}#${m})`)},"addEdgeMarker")});function j4(e,t){Gr(Ae())&&e&&(e.style.width=t.length*9+"px",e.style.height="12px")}function HQe(e){let t=[],r=[];for(let n=1;n5&&Math.abs(a.y-i.y)>5||i.y===a.y&&a.x===s.x&&Math.abs(a.x-i.x)>5&&Math.abs(a.y-s.y)>5)&&(t.push(a),r.push(n))}return{cornerPoints:t,cornerPointPositions:r}}function jQe(e,t){if(e.length<2)return"";let r="",n=e.length,i=1e-5;for(let a=0;a({...i}));if(e.length>=2&&Yi[t.arrowTypeStart]){let i=Yi[t.arrowTypeStart],a=e[0],s=e[1],{angle:l}=Kce(a,s),u=i*Math.cos(l),h=i*Math.sin(l);r[0].x=a.x+u,r[0].y=a.y+h}let n=e.length;if(n>=2&&Yi[t.arrowTypeEnd]){let i=Yi[t.arrowTypeEnd],a=e[n-1],s=e[n-2],{angle:l}=Kce(s,a),u=i*Math.cos(l),h=i*Math.sin(l);r[n-1].x=a.x-u,r[n-1].y=a.y-h}return r}var GQe,qy,_i,X4,G2,_m,K4,VQe,WQe,qQe,jce,Xce,UQe,YQe,Hy,V2=F(()=>{"use strict";Xt();ur();vt();Ls();vO();Qt();Y4();Vy();$r();tr();G4();Yce();Jt();GQe=o(e=>typeof e=="string"?e:Ae()?.flowchart?.curve,"resolveEdgeCurveType"),qy=new Map,_i=new Map,X4=o(()=>{qy.clear(),_i.clear()},"clear"),G2=o(e=>e?typeof e=="string"?e:e.reduce((t,r)=>t+";"+r,""):"","getLabelStyles"),_m=o(async(e,t)=>{let r=Ae(),n=Gr(r),{labelStyles:i}=ct(t);t.labelStyle=i;let a=e.insert("g").attr("class","edgeLabel"),s=a.insert("g").attr("class","label").attr("data-id",t.id),l=t.labelType==="markdown",h=await Pn(e,t.label,{style:G2(t.labelStyle),useHtmlLabels:n,addSvgBackground:!0,isNode:!1,markdown:l,width:l?void 0:void 0},r);s.node().appendChild(h),Z.info("abc82",t,t.labelType);let d=h.getBBox(),f=d;if(n){let m=h.children[0],g=et(h);d=m.getBoundingClientRect(),f=d,g.attr("width",d.width),g.attr("height",d.height)}else{let m=et(h).select("text").node();m&&typeof m.getBBox=="function"&&(f=m.getBBox())}s.attr("transform",ml(f,n)),qy.set(t.id,a),t.width=d.width,t.height=d.height;let p;if(t.startLabelLeft){let m=e.insert("g").attr("class","edgeTerminals"),g=m.insert("g").attr("class","inner"),y=await sc(g,t.startLabelLeft,G2(t.labelStyle)||"",!1,!1);p=y;let v=y.getBBox();if(n){let x=y.children[0],b=et(y);v=x.getBoundingClientRect(),b.attr("width",v.width),b.attr("height",v.height)}g.attr("transform",ml(v,n)),_i.get(t.id)||_i.set(t.id,{}),_i.get(t.id).startLeft=m,j4(p,t.startLabelLeft)}if(t.startLabelRight){let m=e.insert("g").attr("class","edgeTerminals"),g=m.insert("g").attr("class","inner"),y=await sc(g,t.startLabelRight,G2(t.labelStyle)||"",!1,!1);p=y;let v=y.getBBox();if(n){let x=y.children[0],b=et(y);v=x.getBoundingClientRect(),b.attr("width",v.width),b.attr("height",v.height)}g.attr("transform",ml(v,n)),_i.get(t.id)||_i.set(t.id,{}),_i.get(t.id).startRight=m,j4(p,t.startLabelRight)}if(t.endLabelLeft){let m=e.insert("g").attr("class","edgeTerminals"),g=m.insert("g").attr("class","inner"),y=await sc(m,t.endLabelLeft,G2(t.labelStyle)||"",!1,!1);p=y;let v=y.getBBox();if(n){let x=y.children[0],b=et(y);v=x.getBoundingClientRect(),b.attr("width",v.width),b.attr("height",v.height)}g.attr("transform",ml(v,n)),_i.get(t.id)||_i.set(t.id,{}),_i.get(t.id).endLeft=m,j4(p,t.endLabelLeft)}if(t.endLabelRight){let m=e.insert("g").attr("class","edgeTerminals"),g=m.insert("g").attr("class","inner"),y=await sc(m,t.endLabelRight,G2(t.labelStyle)||"",!1,!1);p=y;let v=y.getBBox();if(n){let x=y.children[0],b=et(y);v=x.getBoundingClientRect(),b.attr("width",v.width),b.attr("height",v.height)}g.attr("transform",ml(v,n)),_i.get(t.id)||_i.set(t.id,{}),_i.get(t.id).endRight=m,j4(p,t.endLabelRight)}return h},"insertEdgeLabel");o(j4,"setTerminalWidth");K4=o((e,t)=>{Z.debug("Moving label abc88 ",e.id,e.label,qy.get(e.id),t);let r=t.updatedPath?t.updatedPath:t.originalPath,n=Ae(),{subGraphTitleTotalMargin:i}=oc(n);if(e.label){let a=qy.get(e.id),s=e.x,l=e.y;if(r){let u=Zt.calcLabelPosition(r);Z.debug("Moving label "+e.label+" from (",s,",",l,") to (",u.x,",",u.y,") abc88"),t.updatedPath&&(s=u.x,l=u.y)}a.attr("transform",`translate(${s}, ${l+i/2})`)}if(e.startLabelLeft){let a=_i.get(e.id).startLeft,s=e.x,l=e.y;if(r){let u=Zt.calcTerminalLabelPosition(e.arrowTypeStart?10:0,"start_left",r);s=u.x,l=u.y}a.attr("transform",`translate(${s}, ${l})`)}if(e.startLabelRight){let a=_i.get(e.id).startRight,s=e.x,l=e.y;if(r){let u=Zt.calcTerminalLabelPosition(e.arrowTypeStart?10:0,"start_right",r);s=u.x,l=u.y}a.attr("transform",`translate(${s}, ${l})`)}if(e.endLabelLeft){let a=_i.get(e.id).endLeft,s=e.x,l=e.y;if(r){let u=Zt.calcTerminalLabelPosition(e.arrowTypeEnd?10:0,"end_left",r);s=u.x,l=u.y}a.attr("transform",`translate(${s}, ${l})`)}if(e.endLabelRight){let a=_i.get(e.id).endRight,s=e.x,l=e.y;if(r){let u=Zt.calcTerminalLabelPosition(e.arrowTypeEnd?10:0,"end_right",r);s=u.x,l=u.y}a.attr("transform",`translate(${s}, ${l})`)}},"positionEdgeLabel"),VQe=o((e,t)=>{if(!e?.isLabelEdge||!e?.id?.endsWith("-to-label")||!Array.isArray(t)||t.length!==2)return t;let[r,n]=t,i=Math.abs(n.x-r.x),a=Math.abs(n.y-r.y);return i<.001||a<.001?t:a>=i?[r,{x:r.x,y:n.y},n]:[r,{x:n.x,y:r.y},n]},"orthogonalizeToLabelClippedPoints"),WQe=o((e,t)=>{let r=e.x,n=e.y,i=Math.abs(t.x-r),a=Math.abs(t.y-n),s=e.width/2,l=e.height/2;return i>=s||a>=l},"outsideNode"),qQe=o((e,t,r)=>{Z.debug(`intersection calc abc89: + outsidePoint: ${JSON.stringify(t)} + insidePoint : ${JSON.stringify(r)} + node : x:${e.x} y:${e.y} w:${e.width} h:${e.height}`);let n=e.x,i=e.y,a=Math.abs(n-r.x),s=e.width/2,l=r.xMath.abs(n-t.x)*u){let f=r.y{Z.warn("abc88 cutPathAtIntersect",e,t);let r=[],n=e[0],i=!1;return e.forEach(a=>{if(Z.info("abc88 checking point",a,t),!WQe(t,a)&&!i){let s=qQe(t,n,a);Z.debug("abc88 inside",a,n,s),Z.debug("abc88 intersection",s,t);let l=!1;r.forEach(u=>{l=l||u.x===s.x&&u.y===s.y}),r.some(u=>u.x===s.x&&u.y===s.y)?Z.warn("abc88 no intersect",s,r):r.push(s),i=!0}else Z.warn("abc88 outside",a,n),n=a,i||r.push(a)}),Z.debug("returning points",r),r},"cutPathAtIntersect");o(HQe,"extractCornerPoints");Xce=o(function(e,t,r){let n=t.x-e.x,i=t.y-e.y,a=Math.sqrt(n*n+i*i),s=r/a;return{x:t.x-s*n,y:t.y-s*i}},"findAdjacentPoint"),UQe=o(function(e){let{cornerPointPositions:t}=HQe(e),r=[];for(let n=0;n10&&Math.abs(a.y-i.y)>=10){Z.debug("Corner point fixing",Math.abs(a.x-i.x),Math.abs(a.y-i.y));let m=5;s.x===l.x?p={x:h<0?l.x-m+f:l.x+m-f,y:d<0?l.y-f:l.y+f}:p={x:h<0?l.x-f:l.x+f,y:d<0?l.y-m+f:l.y+m-f}}else Z.debug("Corner point skipping fixing",Math.abs(a.x-i.x),Math.abs(a.y-i.y));r.push(p,u)}else r.push(e[n]);return r},"fixCorners"),YQe=o((e,t,r)=>{let n=e-t-r,i=2,a=2,s=i+a,l=Math.floor(n/s),u=Array(l).fill(`${i} ${a}`).join(" ");return`0 ${t} ${u} ${r}`},"generateDashArray"),Hy=o(function(e,t,r,n,i,a,s,l=!1){if(!s)throw new Error(`insertEdge: missing diagramId for edge "${t.id}" \u2014 edge IDs require a diagram prefix for uniqueness`);let{handDrawnSeed:u,layout:h}=Ae(),d=t.points,f=!1,p=i;var m=a;let g=[];for(let O in t.cssCompiledStyles)N2(O)||g.push(t.cssCompiledStyles[O]);if(h==="swimlane"){if(m.intersect&&p.intersect&&Array.isArray(d)&&d.length>=2)if(d.length===2)d=[p.intersect(d[0]),m.intersect(d[1])];else{let O=d.slice(1,-1),$=O[0],V=O[O.length-1],G=.5,z=Math.abs(d[d.length-1].x-V.x)!Number.isNaN(O.y)),x=GQe(t.curve);x!=="rounded"&&(v=UQe(v));let b=Jc;switch(x){case"linear":b=Jc;break;case"basis":b=rc;break;case"cardinal":b=t2;break;case"bumpX":b=Kb;break;case"bumpY":b=Zb;break;case"catmullRom":b=i2;break;case"monotoneX":b=a2;break;case"monotoneY":b=s2;break;case"natural":b=Ry;break;case"step":b=_y;break;case"stepAfter":b=l2;break;case"stepBefore":b=o2;break;case"rounded":b=Jc;break;default:b=rc}let{x:T,y:k}=U4(t),C=tc().x(T).y(k).curve(b),w;switch(t.thickness){case"normal":w="edge-thickness-normal";break;case"thick":w="edge-thickness-thick";break;case"invisible":w="edge-thickness-invisible";break;default:w="edge-thickness-normal"}switch(t.pattern){case"solid":w+=" edge-pattern-solid";break;case"dotted":w+=" edge-pattern-dotted";break;case"dashed":w+=" edge-pattern-dashed";break;default:w+=" edge-pattern-solid"}let S,R=x==="rounded"?jQe(XQe(v,t),5):C(v),L=Array.isArray(t.style)?t.style:[t.style],N=L.find(O=>O?.startsWith("stroke:")),I="";t.animate&&(I="edge-animation-fast"),t.animation&&(I="edge-animation-"+t.animation);let _=!1;if(t.look==="handDrawn"){let O=ut.svg(e);Object.assign([],v);let $=O.path(R,{roughness:.3,seed:u});w+=" transition",S=et($).select("path").attr("id",`${s}-${t.id}`).attr("class"," "+w+(t.classes?" "+t.classes:"")+(I?" "+I:"")).attr("style",L?L.reduce((G,z)=>G+";"+z,""):"");let V=S.attr("d");S.attr("d",V),e.node().appendChild(S.node())}else{let O=g.join(";"),$=L?L.reduce((j,Q)=>j+Q+";",""):"",V=(O?O+";"+$+";":$)+";"+(L?L.reduce((j,Q)=>j+";"+Q,""):"");S=e.append("path").attr("d",R).attr("id",`${s}-${t.id}`).attr("class"," "+w+(t.classes?" "+t.classes:"")+(I?" "+I:"")).attr("style",V),N=V.match(/stroke:([^;]+)/)?.[1],_=t.animate===!0||!!t.animation||O.includes("animation");let G=S.node(),z=typeof G.getTotalLength=="function"?G.getTotalLength():0,W=xO[t.arrowTypeStart]||0,H=xO[t.arrowTypeEnd]||0;if(t.look==="neo"&&!_){let Q=`stroke-dasharray: ${t.pattern==="dotted"||t.pattern==="dashed"?YQe(z,W,H):`0 ${W} ${z-W-H} ${H}`}; stroke-dashoffset: 0;`;S.attr("style",Q+S.attr("style"))}}S.attr("data-edge",!0),S.attr("data-et","edge"),S.attr("data-id",t.id),S.attr("data-points",y),S.attr("data-look",kn(t.look)),t.showPoints&&v.forEach(O=>{e.append("circle").style("stroke","red").style("fill","red").attr("r",1).attr("cx",O.x).attr("cy",O.y)});let A="";(Ae().flowchart.arrowMarkerAbsolute||Ae().state.arrowMarkerAbsolute)&&(A=window.location.protocol+"//"+window.location.host+window.location.pathname+window.location.search,A=A.replace(/\(/g,"\\(").replace(/\)/g,"\\)")),Z.info("arrowTypeStart",t.arrowTypeStart),Z.info("arrowTypeEnd",t.arrowTypeEnd);let M=!_&&t?.look==="neo";Uce(S,t,A,s,n,M,N);let D=Math.floor(d.length/2),P=d[D];Zt.isLabelCoordinateInPath(P,S.attr("d"))||(f=!0);let B={};return f&&(B.updatedPath=d),B.originalPath=t.points,B},"insertEdge");o(jQe,"generateRoundedPath");o(Kce,"calculateDeltaAndAngle");o(XQe,"applyMarkerOffsetsToPoints")});var KQe,ZQe,QQe,JQe,eJe,tJe,rJe,nJe,iJe,aJe,sJe,oJe,lJe,cJe,uJe,hJe,dJe,fJe,pJe,mJe,gJe,yJe,vJe,xJe,Uy,Z4=F(()=>{"use strict";vt();ur();KQe=o((e,t,r,n)=>{t.forEach(i=>{xJe[i](e,r,n)})},"insertMarkers"),ZQe=o((e,t,r)=>{Z.trace("Making markers for ",r),e.append("defs").append("marker").attr("id",r+"_"+t+"-extensionStart").attr("class","marker extension "+t).attr("refX",18).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M 1,7 L18,13 V 1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-extensionEnd").attr("class","marker extension "+t).attr("refX",1).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").append("path").attr("d","M 1,1 V 13 L18,7 Z"),e.append("marker").attr("id",r+"_"+t+"-extensionStart-margin").attr("class","marker extension "+t).attr("refX",18).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").attr("viewBox","0 0 20 14").append("polygon").attr("points","10,7 18,13 18,1").style("stroke-width",2).style("stroke-dasharray","0"),e.append("defs").append("marker").attr("id",r+"_"+t+"-extensionEnd-margin").attr("class","marker extension "+t).attr("refX",9).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").attr("viewBox","0 0 20 14").append("polygon").attr("points","10,1 10,13 18,7").style("stroke-width",2).style("stroke-dasharray","0")},"extension"),QQe=o((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-compositionStart").attr("class","marker composition "+t).attr("refX",18).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").append("path").attr("d","M 18,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-compositionEnd").attr("class","marker composition "+t).attr("refX",1).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").append("path").attr("d","M 18,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-compositionStart-margin").attr("class","marker composition "+t).attr("refX",15).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").style("stroke-width",0).attr("viewBox","0 0 15 15").attr("d","M 18,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-compositionEnd-margin").attr("class","marker composition "+t).attr("refX",3.5).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").style("stroke-width",0).attr("d","M 18,7 L9,13 L1,7 L9,1 Z")},"composition"),JQe=o((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-aggregationStart").attr("class","marker aggregation "+t).attr("refX",18).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").append("path").attr("d","M 18,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-aggregationEnd").attr("class","marker aggregation "+t).attr("refX",1).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").append("path").attr("d","M 18,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-aggregationStart-margin").attr("class","marker aggregation "+t).attr("refX",15).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").style("stroke-width",2).attr("d","M 18,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-aggregationEnd-margin").attr("class","marker aggregation "+t).attr("refX",1).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").style("stroke-width",2).attr("d","M 18,7 L9,13 L1,7 L9,1 Z")},"aggregation"),eJe=o((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-dependencyStart").attr("class","marker dependency "+t).attr("refX",6).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").append("path").attr("d","M 5,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-dependencyEnd").attr("class","marker dependency "+t).attr("refX",13).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").append("path").attr("d","M 18,7 L9,13 L14,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-dependencyStart-margin").attr("class","marker dependency "+t).attr("refX",4).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").style("stroke-width",0).attr("d","M 5,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-dependencyEnd-margin").attr("class","marker dependency "+t).attr("refX",16).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").style("stroke-width",0).attr("d","M 18,7 L9,13 L14,7 L9,1 Z")},"dependency"),tJe=o((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-lollipopStart").attr("class","marker lollipop "+t).attr("refX",13).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").append("circle").attr("fill","transparent").attr("cx",7).attr("cy",7).attr("r",6),e.append("defs").append("marker").attr("id",r+"_"+t+"-lollipopEnd").attr("class","marker lollipop "+t).attr("refX",1).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").append("circle").attr("fill","transparent").attr("cx",7).attr("cy",7).attr("r",6),e.append("defs").append("marker").attr("id",r+"_"+t+"-lollipopStart-margin").attr("class","marker lollipop "+t).attr("refX",13).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("circle").attr("fill","transparent").attr("cx",7).attr("cy",7).attr("r",6).attr("stroke-width",2),e.append("defs").append("marker").attr("id",r+"_"+t+"-lollipopEnd-margin").attr("class","marker lollipop "+t).attr("refX",1).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("circle").attr("fill","transparent").attr("cx",7).attr("cy",7).attr("r",6).attr("stroke-width",2)},"lollipop"),rJe=o((e,t,r)=>{e.append("marker").attr("id",r+"_"+t+"-pointEnd").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refX",5).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",8).attr("markerHeight",8).attr("orient","auto").append("path").attr("d","M 0 0 L 10 5 L 0 10 z").attr("class","arrowMarkerPath").style("stroke-width",1).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-pointStart").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refX",4.5).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",8).attr("markerHeight",8).attr("orient","auto").append("path").attr("d","M 0 5 L 10 10 L 10 0 z").attr("class","arrowMarkerPath").style("stroke-width",1).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-pointEnd-margin").attr("class","marker "+t).attr("viewBox","0 0 11.5 14").attr("refX",11.5).attr("refY",7).attr("markerUnits","userSpaceOnUse").attr("markerWidth",10.5).attr("markerHeight",14).attr("orient","auto").append("path").attr("d","M 0 0 L 11.5 7 L 0 14 z").attr("class","arrowMarkerPath").style("stroke-width",0).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-pointStart-margin").attr("class","marker "+t).attr("viewBox","0 0 11.5 14").attr("refX",1).attr("refY",7).attr("markerUnits","userSpaceOnUse").attr("markerWidth",11.5).attr("markerHeight",14).attr("orient","auto").append("polygon").attr("points","0,7 11.5,14 11.5,0").attr("class","arrowMarkerPath").style("stroke-width",0).style("stroke-dasharray","1,0")},"point"),nJe=o((e,t,r)=>{e.append("marker").attr("id",r+"_"+t+"-circleEnd").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refX",11).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",11).attr("markerHeight",11).attr("orient","auto").append("circle").attr("cx","5").attr("cy","5").attr("r","5").attr("class","arrowMarkerPath").style("stroke-width",1).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-circleStart").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refX",-1).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",11).attr("markerHeight",11).attr("orient","auto").append("circle").attr("cx","5").attr("cy","5").attr("r","5").attr("class","arrowMarkerPath").style("stroke-width",1).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-circleEnd-margin").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refY",5).attr("refX",12.25).attr("markerUnits","userSpaceOnUse").attr("markerWidth",14).attr("markerHeight",14).attr("orient","auto").append("circle").attr("cx","5").attr("cy","5").attr("r","5").attr("class","arrowMarkerPath").style("stroke-width",0).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-circleStart-margin").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refX",-2).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",14).attr("markerHeight",14).attr("orient","auto").append("circle").attr("cx","5").attr("cy","5").attr("r","5").attr("class","arrowMarkerPath").style("stroke-width",0).style("stroke-dasharray","1,0")},"circle"),iJe=o((e,t,r)=>{e.append("marker").attr("id",r+"_"+t+"-crossEnd").attr("class","marker cross "+t).attr("viewBox","0 0 11 11").attr("refX",12).attr("refY",5.2).attr("markerUnits","userSpaceOnUse").attr("markerWidth",11).attr("markerHeight",11).attr("orient","auto").append("path").attr("d","M 1,1 l 9,9 M 10,1 l -9,9").attr("class","arrowMarkerPath").style("stroke-width",2).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-crossStart").attr("class","marker cross "+t).attr("viewBox","0 0 11 11").attr("refX",-1).attr("refY",5.2).attr("markerUnits","userSpaceOnUse").attr("markerWidth",11).attr("markerHeight",11).attr("orient","auto").append("path").attr("d","M 1,1 l 9,9 M 10,1 l -9,9").attr("class","arrowMarkerPath").style("stroke-width",2).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-crossEnd-margin").attr("class","marker cross "+t).attr("viewBox","0 0 15 15").attr("refX",17.7).attr("refY",7.5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",12).attr("markerHeight",12).attr("orient","auto").append("path").attr("d","M 1,1 L 14,14 M 1,14 L 14,1").attr("class","arrowMarkerPath").style("stroke-width",2.5),e.append("marker").attr("id",r+"_"+t+"-crossStart-margin").attr("class","marker cross "+t).attr("viewBox","0 0 15 15").attr("refX",-3.5).attr("refY",7.5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",12).attr("markerHeight",12).attr("orient","auto").append("path").attr("d","M 1,1 L 14,14 M 1,14 L 14,1").attr("class","arrowMarkerPath").style("stroke-width",2.5).style("stroke-dasharray","1,0")},"cross"),aJe=o((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-barbEnd").attr("refX",19).attr("refY",7).attr("markerWidth",20).attr("markerHeight",14).attr("markerUnits","userSpaceOnUse").attr("orient","auto").append("path").attr("d","M 19,7 L9,13 L14,7 L9,1 Z")},"barb"),sJe=o((e,t,r)=>{let n=_t(),{themeVariables:i}=n,{transitionColor:a}=i;e.append("defs").append("marker").attr("id",r+"_"+t+"-barbEnd").attr("refX",19).attr("refY",7).attr("markerWidth",20).attr("markerHeight",14).attr("markerUnits","strokeWidth").attr("orient","auto").append("path").attr("d","M 19,7 L11,14 L13,7 L11,0 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-barbEnd-margin").attr("refX",17).attr("refY",7).attr("markerWidth",20).attr("markerHeight",14).attr("markerUnits","userSpaceOnUse").attr("orient","auto").append("path").attr("d","M 19,7 L11,14 L13,7 L11,0 Z").attr("fill",`${a}`)},"barbNeo"),oJe=o((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-onlyOneStart").attr("class","marker onlyOne "+t).attr("refX",0).attr("refY",9).attr("markerWidth",18).attr("markerHeight",18).attr("orient","auto").append("path").attr("d","M9,0 L9,18 M15,0 L15,18"),e.append("defs").append("marker").attr("id",r+"_"+t+"-onlyOneEnd").attr("class","marker onlyOne "+t).attr("refX",18).attr("refY",9).attr("markerWidth",18).attr("markerHeight",18).attr("orient","auto").append("path").attr("d","M3,0 L3,18 M9,0 L9,18")},"only_one"),lJe=o((e,t,r)=>{let n=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrOneStart").attr("class","marker zeroOrOne "+t).attr("refX",0).attr("refY",9).attr("markerWidth",30).attr("markerHeight",18).attr("orient","auto");n.append("circle").attr("fill","white").attr("cx",21).attr("cy",9).attr("r",6),n.append("path").attr("d","M9,0 L9,18");let i=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrOneEnd").attr("class","marker zeroOrOne "+t).attr("refX",30).attr("refY",9).attr("markerWidth",30).attr("markerHeight",18).attr("orient","auto");i.append("circle").attr("fill","white").attr("cx",9).attr("cy",9).attr("r",6),i.append("path").attr("d","M21,0 L21,18")},"zero_or_one"),cJe=o((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-oneOrMoreStart").attr("class","marker oneOrMore "+t).attr("refX",18).attr("refY",18).attr("markerWidth",45).attr("markerHeight",36).attr("orient","auto").append("path").attr("d","M0,18 Q 18,0 36,18 Q 18,36 0,18 M42,9 L42,27"),e.append("defs").append("marker").attr("id",r+"_"+t+"-oneOrMoreEnd").attr("class","marker oneOrMore "+t).attr("refX",27).attr("refY",18).attr("markerWidth",45).attr("markerHeight",36).attr("orient","auto").append("path").attr("d","M3,9 L3,27 M9,18 Q27,0 45,18 Q27,36 9,18")},"one_or_more"),uJe=o((e,t,r)=>{let n=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrMoreStart").attr("class","marker zeroOrMore "+t).attr("refX",18).attr("refY",18).attr("markerWidth",57).attr("markerHeight",36).attr("orient","auto");n.append("circle").attr("fill","white").attr("cx",48).attr("cy",18).attr("r",6),n.append("path").attr("d","M0,18 Q18,0 36,18 Q18,36 0,18");let i=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrMoreEnd").attr("class","marker zeroOrMore "+t).attr("refX",39).attr("refY",18).attr("markerWidth",57).attr("markerHeight",36).attr("orient","auto");i.append("circle").attr("fill","white").attr("cx",9).attr("cy",18).attr("r",6),i.append("path").attr("d","M21,18 Q39,0 57,18 Q39,36 21,18")},"zero_or_more"),hJe=o((e,t,r)=>{let n=_t(),{themeVariables:i}=n,{strokeWidth:a}=i;e.append("defs").append("marker").attr("id",r+"_"+t+"-onlyOneStart").attr("class","marker onlyOne "+t).attr("refX",0).attr("refY",9).attr("markerWidth",18).attr("markerHeight",18).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M9,0 L9,18 M15,0 L15,18").attr("stroke-width",`${a}`),e.append("defs").append("marker").attr("id",r+"_"+t+"-onlyOneEnd").attr("class","marker onlyOne "+t).attr("refX",18).attr("refY",9).attr("markerWidth",18).attr("markerHeight",18).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M3,0 L3,18 M9,0 L9,18").attr("stroke-width",`${a}`)},"only_one_neo"),dJe=o((e,t,r)=>{let n=_t(),{themeVariables:i}=n,{strokeWidth:a,mainBkg:s}=i,l=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrOneStart").attr("class","marker zeroOrOne "+t).attr("refX",0).attr("refY",9).attr("markerWidth",30).attr("markerHeight",18).attr("orient","auto").attr("markerUnits","userSpaceOnUse");l.append("circle").attr("fill",s??"white").attr("cx",21).attr("cy",9).attr("stroke-width",`${a}`).attr("r",6),l.append("path").attr("d","M9,0 L9,18").attr("stroke-width",`${a}`);let u=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrOneEnd").attr("class","marker zeroOrOne "+t).attr("refX",30).attr("refY",9).attr("markerWidth",30).attr("markerHeight",18).attr("markerUnits","userSpaceOnUse").attr("orient","auto");u.append("circle").attr("fill",s??"white").attr("cx",9).attr("cy",9).attr("stroke-width",`${a}`).attr("r",6),u.append("path").attr("d","M21,0 L21,18").attr("stroke-width",`${a}`)},"zero_or_one_neo"),fJe=o((e,t,r)=>{let n=_t(),{themeVariables:i}=n,{strokeWidth:a}=i;e.append("defs").append("marker").attr("id",r+"_"+t+"-oneOrMoreStart").attr("class","marker oneOrMore "+t).attr("refX",18).attr("refY",18).attr("markerWidth",45).attr("markerHeight",36).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("path").attr("d","M0,18 Q 18,0 36,18 Q 18,36 0,18 M42,9 L42,27").attr("stroke-width",`${a}`),e.append("defs").append("marker").attr("id",r+"_"+t+"-oneOrMoreEnd").attr("class","marker oneOrMore "+t).attr("refX",27).attr("refY",18).attr("markerWidth",45).attr("markerHeight",36).attr("markerUnits","userSpaceOnUse").attr("orient","auto").append("path").attr("d","M3,9 L3,27 M9,18 Q27,0 45,18 Q27,36 9,18").attr("stroke-width",`${a}`)},"one_or_more_neo"),pJe=o((e,t,r)=>{let n=_t(),{themeVariables:i}=n,{strokeWidth:a,mainBkg:s}=i,l=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrMoreStart").attr("class","marker zeroOrMore "+t).attr("refX",18).attr("refY",18).attr("markerWidth",57).attr("markerHeight",36).attr("markerUnits","userSpaceOnUse").attr("orient","auto");l.append("circle").attr("fill",s??"white").attr("cx",45.5).attr("cy",18).attr("r",6).attr("stroke-width",`${a}`),l.append("path").attr("d","M0,18 Q18,0 36,18 Q18,36 0,18").attr("stroke-width",`${a}`);let u=e.append("defs").append("marker").attr("id",r+"_"+t+"-zeroOrMoreEnd").attr("class","marker zeroOrMore "+t).attr("refX",39).attr("refY",18).attr("markerWidth",57).attr("markerHeight",36).attr("orient","auto").attr("markerUnits","userSpaceOnUse");u.append("circle").attr("fill",s??"white").attr("cx",11).attr("cy",18).attr("r",6).attr("stroke-width",`${a}`),u.append("path").attr("d","M21,18 Q39,0 57,18 Q39,36 21,18").attr("stroke-width",`${a}`)},"zero_or_more_neo"),mJe=o((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-requirement_arrowEnd").attr("refX",20).attr("refY",10).attr("markerWidth",20).attr("markerHeight",20).attr("orient","auto").append("path").attr("d",`M0,0 + L20,10 + M20,10 + L0,20`)},"requirement_arrow"),gJe=o((e,t,r)=>{let n=_t(),{themeVariables:i}=n,{strokeWidth:a}=i;e.append("defs").append("marker").attr("id",r+"_"+t+"-requirement_arrowEnd").attr("refX",20).attr("refY",10).attr("markerWidth",20).attr("markerHeight",20).attr("orient","auto").attr("markerUnits","userSpaceOnUse").attr("stroke-width",`${a}`).attr("viewBox","0 0 25 20").append("path").attr("d",`M0,0 + L20,10 + M20,10 + L0,20`).attr("stroke-linejoin","miter")},"requirement_arrow_neo"),yJe=o((e,t,r)=>{let n=e.append("defs").append("marker").attr("id",r+"_"+t+"-requirement_containsStart").attr("refX",0).attr("refY",10).attr("markerWidth",20).attr("markerHeight",20).attr("orient","auto").append("g");n.append("circle").attr("cx",10).attr("cy",10).attr("r",9).attr("fill","none"),n.append("line").attr("x1",1).attr("x2",19).attr("y1",10).attr("y2",10),n.append("line").attr("y1",1).attr("y2",19).attr("x1",10).attr("x2",10)},"requirement_contains"),vJe=o((e,t,r)=>{let n=_t(),{themeVariables:i}=n,{strokeWidth:a}=i,s=e.append("defs").append("marker").attr("id",r+"_"+t+"-requirement_containsStart").attr("refX",0).attr("refY",10).attr("markerWidth",20).attr("markerHeight",20).attr("orient","auto").attr("markerUnits","userSpaceOnUse").append("g");s.append("circle").attr("cx",10).attr("cy",10).attr("r",9).attr("fill","none"),s.append("line").attr("x1",1).attr("x2",19).attr("y1",10).attr("y2",10),s.append("line").attr("y1",1).attr("y2",19).attr("x1",10).attr("x2",10),s.selectAll("*").attr("stroke-width",`${a}`)},"requirement_contains_neo"),xJe={extension:ZQe,composition:QQe,aggregation:JQe,dependency:eJe,lollipop:tJe,point:rJe,circle:nJe,cross:iJe,barb:aJe,barbNeo:sJe,only_one:oJe,zero_or_one:lJe,one_or_more:cJe,zero_or_more:uJe,only_one_neo:hJe,zero_or_one_neo:dJe,one_or_more_neo:fJe,zero_or_more_neo:pJe,requirement_arrow:mJe,requirement_contains:yJe,requirement_arrow_neo:gJe,requirement_contains_neo:vJe},Uy=KQe});async function af(e,t,r){let n,i;t.shape==="rect"&&(t.rx&&t.ry?t.shape="roundedRect":t.shape="squareRect");let a=t.shape?gO[t.shape]:void 0;if(!a)throw new Error(`No such shape: ${t.shape}. Please check your syntax.`);if(t.link){let s;r.config.securityLevel==="sandbox"?s="_top":t.linkTarget&&(s=t.linkTarget||"_blank"),n=e.insert("svg:a").attr("xlink:href",t.link).attr("target",s??null),i=await a(n,t,r)}else i=await a(e,t,r),n=i;return n.attr("data-look",kn(t.look)),t.tooltip&&i.attr("title",t.tooltip),Q4.set(t.id,n),t.haveCallback&&n.attr("class",n.attr("class")+" clickable"),n}var Q4,Zce,J4,Lm,Dm=F(()=>{"use strict";vt();yO();Qt();Q4=new Map;o(af,"insertNode");Zce=o((e,t)=>{Q4.set(t.id,e)},"setNodeElem"),J4=o(()=>{Q4.clear()},"clear"),Lm=o(e=>{let t=Q4.get(e.id);Z.trace("Transforming node",e.diff,e,"translate("+(e.x-e.width/2-5)+", "+e.width/2+")");let r=8,n=e.diff||0;return e.clusterNode?t.attr("transform","translate("+(e.x+n-e.width/2)+", "+(e.y-e.height/2-r)+")"):t.attr("transform","translate("+e.x+", "+e.y+")"),n},"positionNode")});var Qce,Jce=F(()=>{"use strict";ur();Vr();vt();Wy();V2();Z4();Dm();Kt();Qt();Qce={common:xt,getConfig:_t,insertCluster:nf,insertEdge:Hy,insertEdgeLabel:_m,insertMarkers:Uy,insertNode:af,interpolateToCurve:uP,labelHelper:It,log:Z,positionEdgeLabel:K4}});var bJe,e3,bO=F(()=>{"use strict";bJe=typeof global=="object"&&global&&global.Object===Object&&global,e3=bJe});var TJe,CJe,Li,gl=F(()=>{"use strict";bO();TJe=typeof self=="object"&&self&&self.Object===Object&&self,CJe=e3||TJe||Function("return this")(),Li=CJe});var wJe,wa,Im=F(()=>{"use strict";gl();wJe=Li.Symbol,wa=wJe});function EJe(e){var t=kJe.call(e,W2),r=e[W2];try{e[W2]=void 0;var n=!0}catch{}var i=SJe.call(e);return n&&(t?e[W2]=r:delete e[W2]),i}var eue,kJe,SJe,W2,tue,rue=F(()=>{"use strict";Im();eue=Object.prototype,kJe=eue.hasOwnProperty,SJe=eue.toString,W2=wa?wa.toStringTag:void 0;o(EJe,"getRawTag");tue=EJe});function _Je(e){return RJe.call(e)}var AJe,RJe,nue,iue=F(()=>{"use strict";AJe=Object.prototype,RJe=AJe.toString;o(_Je,"objectToString");nue=_Je});function IJe(e){return e==null?e===void 0?DJe:LJe:aue&&aue in Object(e)?tue(e):nue(e)}var LJe,DJe,aue,Ds,sf=F(()=>{"use strict";Im();rue();iue();LJe="[object Null]",DJe="[object Undefined]",aue=wa?wa.toStringTag:void 0;o(IJe,"baseGetTag");Ds=IJe});function MJe(e){return e!=null&&typeof e=="object"}var Bi,lc=F(()=>{"use strict";o(MJe,"isObjectLike");Bi=MJe});function PJe(e){return typeof e=="symbol"||Bi(e)&&Ds(e)==NJe}var NJe,Go,Mm=F(()=>{"use strict";sf();lc();NJe="[object Symbol]";o(PJe,"isSymbol");Go=PJe});function OJe(e,t){for(var r=-1,n=e==null?0:e.length,i=Array(n);++r{"use strict";o(OJe,"arrayMap");au=OJe});var BJe,Yr,ji=F(()=>{"use strict";BJe=Array.isArray,Yr=BJe});function lue(e){if(typeof e=="string")return e;if(Yr(e))return au(e,lue)+"";if(Go(e))return oue?oue.call(e):"";var t=e+"";return t=="0"&&1/e==-$Je?"-0":t}var $Je,sue,oue,cue,uue=F(()=>{"use strict";Im();q2();ji();Mm();$Je=1/0,sue=wa?wa.prototype:void 0,oue=sue?sue.toString:void 0;o(lue,"baseToString");cue=lue});function zJe(e){for(var t=e.length;t--&&FJe.test(e.charAt(t)););return t}var FJe,hue,due=F(()=>{"use strict";FJe=/\s/;o(zJe,"trimmedEndIndex");hue=zJe});function VJe(e){return e&&e.slice(0,hue(e)+1).replace(GJe,"")}var GJe,fue,pue=F(()=>{"use strict";due();GJe=/^\s+/;o(VJe,"baseTrim");fue=VJe});function WJe(e){var t=typeof e;return e!=null&&(t=="object"||t=="function")}var fi,yl=F(()=>{"use strict";o(WJe,"isObject");fi=WJe});function jJe(e){if(typeof e=="number")return e;if(Go(e))return mue;if(fi(e)){var t=typeof e.valueOf=="function"?e.valueOf():e;e=fi(t)?t+"":t}if(typeof e!="string")return e===0?e:+e;e=fue(e);var r=HJe.test(e);return r||UJe.test(e)?YJe(e.slice(2),r?2:8):qJe.test(e)?mue:+e}var mue,qJe,HJe,UJe,YJe,gue,yue=F(()=>{"use strict";pue();yl();Mm();mue=NaN,qJe=/^[-+]0x[0-9a-f]+$/i,HJe=/^0b[01]+$/i,UJe=/^0o[0-7]+$/i,YJe=parseInt;o(jJe,"toNumber");gue=jJe});function KJe(e){if(!e)return e===0?e:0;if(e=gue(e),e===vue||e===-vue){var t=e<0?-1:1;return t*XJe}return e===e?e:0}var vue,XJe,Yy,TO=F(()=>{"use strict";yue();vue=1/0,XJe=17976931348623157e292;o(KJe,"toFinite");Yy=KJe});function ZJe(e){var t=Yy(e),r=t%1;return t===t?r?t-r:t:0}var xue,bue=F(()=>{"use strict";TO();o(ZJe,"toInteger");xue=ZJe});function QJe(e){return e}var ao,of=F(()=>{"use strict";o(QJe,"identity");ao=QJe});function net(e){if(!fi(e))return!1;var t=Ds(e);return t==eet||t==tet||t==JJe||t==ret}var JJe,eet,tet,ret,su,H2=F(()=>{"use strict";sf();yl();JJe="[object AsyncFunction]",eet="[object Function]",tet="[object GeneratorFunction]",ret="[object Proxy]";o(net,"isFunction");su=net});var iet,t3,Tue=F(()=>{"use strict";gl();iet=Li["__core-js_shared__"],t3=iet});function aet(e){return!!Cue&&Cue in e}var Cue,wue,kue=F(()=>{"use strict";Tue();Cue=(function(){var e=/[^.]+$/.exec(t3&&t3.keys&&t3.keys.IE_PROTO||"");return e?"Symbol(src)_1."+e:""})();o(aet,"isMasked");wue=aet});function cet(e){if(e!=null){try{return oet.call(e)}catch{}try{return e+""}catch{}}return""}var set,oet,Sh,CO=F(()=>{"use strict";set=Function.prototype,oet=set.toString;o(cet,"toSource");Sh=cet});function yet(e){if(!fi(e)||wue(e))return!1;var t=su(e)?get:het;return t.test(Sh(e))}var uet,het,det,fet,pet,met,get,Sue,Eue=F(()=>{"use strict";H2();kue();yl();CO();uet=/[\\^$.*+?()[\]{}|]/g,het=/^\[object .+?Constructor\]$/,det=Function.prototype,fet=Object.prototype,pet=det.toString,met=fet.hasOwnProperty,get=RegExp("^"+pet.call(met).replace(uet,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");o(yet,"baseIsNative");Sue=yet});function vet(e,t){return e?.[t]}var Aue,Rue=F(()=>{"use strict";o(vet,"getValue");Aue=vet});function xet(e,t){var r=Aue(e,t);return Sue(r)?r:void 0}var so,lf=F(()=>{"use strict";Eue();Rue();o(xet,"getNative");so=xet});var bet,r3,_ue=F(()=>{"use strict";lf();gl();bet=so(Li,"WeakMap"),r3=bet});var Lue,Tet,Due,Iue=F(()=>{"use strict";yl();Lue=Object.create,Tet=(function(){function e(){}return o(e,"object"),function(t){if(!fi(t))return{};if(Lue)return Lue(t);e.prototype=t;var r=new e;return e.prototype=void 0,r}})(),Due=Tet});function Cet(e,t,r){switch(r.length){case 0:return e.call(t);case 1:return e.call(t,r[0]);case 2:return e.call(t,r[0],r[1]);case 3:return e.call(t,r[0],r[1],r[2])}return e.apply(t,r)}var Mue,Nue=F(()=>{"use strict";o(Cet,"apply");Mue=Cet});function wet(){}var Pue,Oue=F(()=>{"use strict";o(wet,"noop");Pue=wet});function ket(e,t){var r=-1,n=e.length;for(t||(t=Array(n));++r{"use strict";o(ket,"copyArray");n3=ket});function _et(e){var t=0,r=0;return function(){var n=Ret(),i=Aet-(n-r);if(r=n,i>0){if(++t>=Eet)return arguments[0]}else t=0;return e.apply(void 0,arguments)}}var Eet,Aet,Ret,Bue,$ue=F(()=>{"use strict";Eet=800,Aet=16,Ret=Date.now;o(_et,"shortOut");Bue=_et});function Let(e){return function(){return e}}var oo,kO=F(()=>{"use strict";o(Let,"constant");oo=Let});var Det,jy,SO=F(()=>{"use strict";lf();Det=(function(){try{var e=so(Object,"defineProperty");return e({},"",{}),e}catch{}})(),jy=Det});var Iet,Fue,zue=F(()=>{"use strict";kO();SO();of();Iet=jy?function(e,t){return jy(e,"toString",{configurable:!0,enumerable:!1,value:oo(t),writable:!0})}:ao,Fue=Iet});var Met,i3,EO=F(()=>{"use strict";zue();$ue();Met=Bue(Fue),i3=Met});function Net(e,t){for(var r=-1,n=e==null?0:e.length;++r{"use strict";o(Net,"arrayEach");a3=Net});function Pet(e,t,r,n){for(var i=e.length,a=r+(n?1:-1);n?a--:++a{"use strict";o(Pet,"baseFindIndex");s3=Pet});function Oet(e){return e!==e}var Gue,Vue=F(()=>{"use strict";o(Oet,"baseIsNaN");Gue=Oet});function Bet(e,t,r){for(var n=r-1,i=e.length;++n{"use strict";o(Bet,"strictIndexOf");Wue=Bet});function $et(e,t,r){return t===t?Wue(e,t,r):s3(e,Gue,r)}var Hue,Uue=F(()=>{"use strict";RO();Vue();que();o($et,"baseIndexOf");Hue=$et});function Fet(e,t){var r=e==null?0:e.length;return!!r&&Hue(e,t,0)>-1}var Yue,jue=F(()=>{"use strict";Uue();o(Fet,"arrayIncludes");Yue=Fet});function Vet(e,t){var r=typeof e;return t=t??zet,!!t&&(r=="number"||r!="symbol"&&Get.test(e))&&e>-1&&e%1==0&&e{"use strict";zet=9007199254740991,Get=/^(?:0|[1-9]\d*)$/;o(Vet,"isIndex");cf=Vet});function Wet(e,t,r){t=="__proto__"&&jy?jy(e,t,{configurable:!0,enumerable:!0,value:r,writable:!0}):e[t]=r}var uf,Y2=F(()=>{"use strict";SO();o(Wet,"baseAssignValue");uf=Wet});function qet(e,t){return e===t||e!==e&&t!==t}var vl,Nm=F(()=>{"use strict";o(qet,"eq");vl=qet});function Yet(e,t,r){var n=e[t];(!(Uet.call(e,t)&&vl(n,r))||r===void 0&&!(t in e))&&uf(e,t,r)}var Het,Uet,hf,j2=F(()=>{"use strict";Y2();Nm();Het=Object.prototype,Uet=Het.hasOwnProperty;o(Yet,"assignValue");hf=Yet});function jet(e,t,r,n){var i=!r;r||(r={});for(var a=-1,s=t.length;++a{"use strict";j2();Y2();o(jet,"copyObject");ou=jet});function Xet(e,t,r){return t=Xue(t===void 0?e.length-1:t,0),function(){for(var n=arguments,i=-1,a=Xue(n.length-t,0),s=Array(a);++i{"use strict";Nue();Xue=Math.max;o(Xet,"overRest");o3=Xet});function Ket(e,t){return i3(o3(e,t,ao),e+"")}var df,X2=F(()=>{"use strict";of();_O();EO();o(Ket,"baseRest");df=Ket});function Qet(e){return typeof e=="number"&&e>-1&&e%1==0&&e<=Zet}var Zet,Ky,l3=F(()=>{"use strict";Zet=9007199254740991;o(Qet,"isLength");Ky=Qet});function Jet(e){return e!=null&&Ky(e.length)&&!su(e)}var ka,lu=F(()=>{"use strict";H2();l3();o(Jet,"isArrayLike");ka=Jet});function ett(e,t,r){if(!fi(r))return!1;var n=typeof t;return(n=="number"?ka(r)&&cf(t,r.length):n=="string"&&t in r)?vl(r[t],e):!1}var Eh,K2=F(()=>{"use strict";Nm();lu();U2();yl();o(ett,"isIterateeCall");Eh=ett});function ttt(e){return df(function(t,r){var n=-1,i=r.length,a=i>1?r[i-1]:void 0,s=i>2?r[2]:void 0;for(a=e.length>3&&typeof a=="function"?(i--,a):void 0,s&&Eh(r[0],r[1],s)&&(a=i<3?void 0:a,i=1),t=Object(t);++n{"use strict";X2();K2();o(ttt,"createAssigner");Kue=ttt});function ntt(e){var t=e&&e.constructor,r=typeof t=="function"&&t.prototype||rtt;return e===r}var rtt,ff,Z2=F(()=>{"use strict";rtt=Object.prototype;o(ntt,"isPrototype");ff=ntt});function itt(e,t){for(var r=-1,n=Array(e);++r{"use strict";o(itt,"baseTimes");Que=itt});function stt(e){return Bi(e)&&Ds(e)==att}var att,LO,ehe=F(()=>{"use strict";sf();lc();att="[object Arguments]";o(stt,"baseIsArguments");LO=stt});var the,ott,ltt,ctt,cc,Zy=F(()=>{"use strict";ehe();lc();the=Object.prototype,ott=the.hasOwnProperty,ltt=the.propertyIsEnumerable,ctt=LO((function(){return arguments})())?LO:function(e){return Bi(e)&&ott.call(e,"callee")&&!ltt.call(e,"callee")},cc=ctt});function utt(){return!1}var rhe,nhe=F(()=>{"use strict";o(utt,"stubFalse");rhe=utt});var she,ihe,htt,ahe,dtt,ftt,uc,Qy=F(()=>{"use strict";gl();nhe();she=typeof exports=="object"&&exports&&!exports.nodeType&&exports,ihe=she&&typeof module=="object"&&module&&!module.nodeType&&module,htt=ihe&&ihe.exports===she,ahe=htt?Li.Buffer:void 0,dtt=ahe?ahe.isBuffer:void 0,ftt=dtt||rhe,uc=ftt});function $tt(e){return Bi(e)&&Ky(e.length)&&!!ai[Ds(e)]}var ptt,mtt,gtt,ytt,vtt,xtt,btt,Ttt,Ctt,wtt,ktt,Stt,Ett,Att,Rtt,_tt,Ltt,Dtt,Itt,Mtt,Ntt,Ptt,Ott,Btt,ai,ohe,lhe=F(()=>{"use strict";sf();l3();lc();ptt="[object Arguments]",mtt="[object Array]",gtt="[object Boolean]",ytt="[object Date]",vtt="[object Error]",xtt="[object Function]",btt="[object Map]",Ttt="[object Number]",Ctt="[object Object]",wtt="[object RegExp]",ktt="[object Set]",Stt="[object String]",Ett="[object WeakMap]",Att="[object ArrayBuffer]",Rtt="[object DataView]",_tt="[object Float32Array]",Ltt="[object Float64Array]",Dtt="[object Int8Array]",Itt="[object Int16Array]",Mtt="[object Int32Array]",Ntt="[object Uint8Array]",Ptt="[object Uint8ClampedArray]",Ott="[object Uint16Array]",Btt="[object Uint32Array]",ai={};ai[_tt]=ai[Ltt]=ai[Dtt]=ai[Itt]=ai[Mtt]=ai[Ntt]=ai[Ptt]=ai[Ott]=ai[Btt]=!0;ai[ptt]=ai[mtt]=ai[Att]=ai[gtt]=ai[Rtt]=ai[ytt]=ai[vtt]=ai[xtt]=ai[btt]=ai[Ttt]=ai[Ctt]=ai[wtt]=ai[ktt]=ai[Stt]=ai[Ett]=!1;o($tt,"baseIsTypedArray");ohe=$tt});function Ftt(e){return function(t){return e(t)}}var pf,Q2=F(()=>{"use strict";o(Ftt,"baseUnary");pf=Ftt});var che,J2,ztt,DO,Gtt,Ah,c3=F(()=>{"use strict";bO();che=typeof exports=="object"&&exports&&!exports.nodeType&&exports,J2=che&&typeof module=="object"&&module&&!module.nodeType&&module,ztt=J2&&J2.exports===che,DO=ztt&&e3.process,Gtt=(function(){try{var e=J2&&J2.require&&J2.require("util").types;return e||DO&&DO.binding&&DO.binding("util")}catch{}})(),Ah=Gtt});var uhe,Vtt,mf,eT=F(()=>{"use strict";lhe();Q2();c3();uhe=Ah&&Ah.isTypedArray,Vtt=uhe?pf(uhe):ohe,mf=Vtt});function Htt(e,t){var r=Yr(e),n=!r&&cc(e),i=!r&&!n&&uc(e),a=!r&&!n&&!i&&mf(e),s=r||n||i||a,l=s?Que(e.length,String):[],u=l.length;for(var h in e)(t||qtt.call(e,h))&&!(s&&(h=="length"||i&&(h=="offset"||h=="parent")||a&&(h=="buffer"||h=="byteLength"||h=="byteOffset")||cf(h,u)))&&l.push(h);return l}var Wtt,qtt,u3,IO=F(()=>{"use strict";Jue();Zy();ji();Qy();U2();eT();Wtt=Object.prototype,qtt=Wtt.hasOwnProperty;o(Htt,"arrayLikeKeys");u3=Htt});function Utt(e,t){return function(r){return e(t(r))}}var h3,MO=F(()=>{"use strict";o(Utt,"overArg");h3=Utt});var Ytt,hhe,dhe=F(()=>{"use strict";MO();Ytt=h3(Object.keys,Object),hhe=Ytt});function Ktt(e){if(!ff(e))return hhe(e);var t=[];for(var r in Object(e))Xtt.call(e,r)&&r!="constructor"&&t.push(r);return t}var jtt,Xtt,Jy,d3=F(()=>{"use strict";Z2();dhe();jtt=Object.prototype,Xtt=jtt.hasOwnProperty;o(Ktt,"baseKeys");Jy=Ktt});function Ztt(e){return ka(e)?u3(e):Jy(e)}var Di,Rh=F(()=>{"use strict";IO();d3();lu();o(Ztt,"keys");Di=Ztt});function Qtt(e){var t=[];if(e!=null)for(var r in Object(e))t.push(r);return t}var fhe,phe=F(()=>{"use strict";o(Qtt,"nativeKeysIn");fhe=Qtt});function trt(e){if(!fi(e))return fhe(e);var t=ff(e),r=[];for(var n in e)n=="constructor"&&(t||!ert.call(e,n))||r.push(n);return r}var Jtt,ert,mhe,ghe=F(()=>{"use strict";yl();Z2();phe();Jtt=Object.prototype,ert=Jtt.hasOwnProperty;o(trt,"baseKeysIn");mhe=trt});function rrt(e){return ka(e)?u3(e,!0):mhe(e)}var lo,gf=F(()=>{"use strict";IO();ghe();lu();o(rrt,"keysIn");lo=rrt});function art(e,t){if(Yr(e))return!1;var r=typeof e;return r=="number"||r=="symbol"||r=="boolean"||e==null||Go(e)?!0:irt.test(e)||!nrt.test(e)||t!=null&&e in Object(t)}var nrt,irt,e1,f3=F(()=>{"use strict";ji();Mm();nrt=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,irt=/^\w*$/;o(art,"isKey");e1=art});var srt,_h,tT=F(()=>{"use strict";lf();srt=so(Object,"create"),_h=srt});function ort(){this.__data__=_h?_h(null):{},this.size=0}var yhe,vhe=F(()=>{"use strict";tT();o(ort,"hashClear");yhe=ort});function lrt(e){var t=this.has(e)&&delete this.__data__[e];return this.size-=t?1:0,t}var xhe,bhe=F(()=>{"use strict";o(lrt,"hashDelete");xhe=lrt});function drt(e){var t=this.__data__;if(_h){var r=t[e];return r===crt?void 0:r}return hrt.call(t,e)?t[e]:void 0}var crt,urt,hrt,The,Che=F(()=>{"use strict";tT();crt="__lodash_hash_undefined__",urt=Object.prototype,hrt=urt.hasOwnProperty;o(drt,"hashGet");The=drt});function mrt(e){var t=this.__data__;return _h?t[e]!==void 0:prt.call(t,e)}var frt,prt,whe,khe=F(()=>{"use strict";tT();frt=Object.prototype,prt=frt.hasOwnProperty;o(mrt,"hashHas");whe=mrt});function yrt(e,t){var r=this.__data__;return this.size+=this.has(e)?0:1,r[e]=_h&&t===void 0?grt:t,this}var grt,She,Ehe=F(()=>{"use strict";tT();grt="__lodash_hash_undefined__";o(yrt,"hashSet");She=yrt});function t1(e){var t=-1,r=e==null?0:e.length;for(this.clear();++t{"use strict";vhe();bhe();Che();khe();Ehe();o(t1,"Hash");t1.prototype.clear=yhe;t1.prototype.delete=xhe;t1.prototype.get=The;t1.prototype.has=whe;t1.prototype.set=She;NO=t1});function vrt(){this.__data__=[],this.size=0}var Rhe,_he=F(()=>{"use strict";o(vrt,"listCacheClear");Rhe=vrt});function xrt(e,t){for(var r=e.length;r--;)if(vl(e[r][0],t))return r;return-1}var yf,rT=F(()=>{"use strict";Nm();o(xrt,"assocIndexOf");yf=xrt});function Crt(e){var t=this.__data__,r=yf(t,e);if(r<0)return!1;var n=t.length-1;return r==n?t.pop():Trt.call(t,r,1),--this.size,!0}var brt,Trt,Lhe,Dhe=F(()=>{"use strict";rT();brt=Array.prototype,Trt=brt.splice;o(Crt,"listCacheDelete");Lhe=Crt});function wrt(e){var t=this.__data__,r=yf(t,e);return r<0?void 0:t[r][1]}var Ihe,Mhe=F(()=>{"use strict";rT();o(wrt,"listCacheGet");Ihe=wrt});function krt(e){return yf(this.__data__,e)>-1}var Nhe,Phe=F(()=>{"use strict";rT();o(krt,"listCacheHas");Nhe=krt});function Srt(e,t){var r=this.__data__,n=yf(r,e);return n<0?(++this.size,r.push([e,t])):r[n][1]=t,this}var Ohe,Bhe=F(()=>{"use strict";rT();o(Srt,"listCacheSet");Ohe=Srt});function r1(e){var t=-1,r=e==null?0:e.length;for(this.clear();++t{"use strict";_he();Dhe();Mhe();Phe();Bhe();o(r1,"ListCache");r1.prototype.clear=Rhe;r1.prototype.delete=Lhe;r1.prototype.get=Ihe;r1.prototype.has=Nhe;r1.prototype.set=Ohe;vf=r1});var Ert,xf,p3=F(()=>{"use strict";lf();gl();Ert=so(Li,"Map"),xf=Ert});function Art(){this.size=0,this.__data__={hash:new NO,map:new(xf||vf),string:new NO}}var $he,Fhe=F(()=>{"use strict";Ahe();nT();p3();o(Art,"mapCacheClear");$he=Art});function Rrt(e){var t=typeof e;return t=="string"||t=="number"||t=="symbol"||t=="boolean"?e!=="__proto__":e===null}var zhe,Ghe=F(()=>{"use strict";o(Rrt,"isKeyable");zhe=Rrt});function _rt(e,t){var r=e.__data__;return zhe(t)?r[typeof t=="string"?"string":"hash"]:r.map}var bf,iT=F(()=>{"use strict";Ghe();o(_rt,"getMapData");bf=_rt});function Lrt(e){var t=bf(this,e).delete(e);return this.size-=t?1:0,t}var Vhe,Whe=F(()=>{"use strict";iT();o(Lrt,"mapCacheDelete");Vhe=Lrt});function Drt(e){return bf(this,e).get(e)}var qhe,Hhe=F(()=>{"use strict";iT();o(Drt,"mapCacheGet");qhe=Drt});function Irt(e){return bf(this,e).has(e)}var Uhe,Yhe=F(()=>{"use strict";iT();o(Irt,"mapCacheHas");Uhe=Irt});function Mrt(e,t){var r=bf(this,e),n=r.size;return r.set(e,t),this.size+=r.size==n?0:1,this}var jhe,Xhe=F(()=>{"use strict";iT();o(Mrt,"mapCacheSet");jhe=Mrt});function n1(e){var t=-1,r=e==null?0:e.length;for(this.clear();++t{"use strict";Fhe();Whe();Hhe();Yhe();Xhe();o(n1,"MapCache");n1.prototype.clear=$he;n1.prototype.delete=Vhe;n1.prototype.get=qhe;n1.prototype.has=Uhe;n1.prototype.set=jhe;Pm=n1});function PO(e,t){if(typeof e!="function"||t!=null&&typeof t!="function")throw new TypeError(Nrt);var r=o(function(){var n=arguments,i=t?t.apply(this,n):n[0],a=r.cache;if(a.has(i))return a.get(i);var s=e.apply(this,n);return r.cache=a.set(i,s)||a,s},"memoized");return r.cache=new(PO.Cache||Pm),r}var Nrt,Khe,Zhe=F(()=>{"use strict";m3();Nrt="Expected a function";o(PO,"memoize");PO.Cache=Pm;Khe=PO});function Ort(e){var t=Khe(e,function(n){return r.size===Prt&&r.clear(),n}),r=t.cache;return t}var Prt,Qhe,Jhe=F(()=>{"use strict";Zhe();Prt=500;o(Ort,"memoizeCapped");Qhe=Ort});var Brt,$rt,Frt,ede,tde=F(()=>{"use strict";Jhe();Brt=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,$rt=/\\(\\)?/g,Frt=Qhe(function(e){var t=[];return e.charCodeAt(0)===46&&t.push(""),e.replace(Brt,function(r,n,i,a){t.push(i?a.replace($rt,"$1"):n||r)}),t}),ede=Frt});function zrt(e){return e==null?"":cue(e)}var g3,OO=F(()=>{"use strict";uue();o(zrt,"toString");g3=zrt});function Grt(e,t){return Yr(e)?e:e1(e,t)?[e]:ede(g3(e))}var Tf,aT=F(()=>{"use strict";ji();f3();tde();OO();o(Grt,"castPath");Tf=Grt});function Wrt(e){if(typeof e=="string"||Go(e))return e;var t=e+"";return t=="0"&&1/e==-Vrt?"-0":t}var Vrt,cu,i1=F(()=>{"use strict";Mm();Vrt=1/0;o(Wrt,"toKey");cu=Wrt});function qrt(e,t){t=Tf(t,e);for(var r=0,n=t.length;e!=null&&r{"use strict";aT();i1();o(qrt,"baseGet");Cf=qrt});function Hrt(e,t,r){var n=e==null?void 0:Cf(e,t);return n===void 0?r:n}var rde,nde=F(()=>{"use strict";sT();o(Hrt,"get");rde=Hrt});function Urt(e,t){for(var r=-1,n=t.length,i=e.length;++r{"use strict";o(Urt,"arrayPush");a1=Urt});function Yrt(e){return Yr(e)||cc(e)||!!(ide&&e&&e[ide])}var ide,ade,sde=F(()=>{"use strict";Im();Zy();ji();ide=wa?wa.isConcatSpreadable:void 0;o(Yrt,"isFlattenable");ade=Yrt});function ode(e,t,r,n,i){var a=-1,s=e.length;for(r||(r=ade),i||(i=[]);++a0&&r(l)?t>1?ode(l,t-1,r,n,i):a1(i,l):n||(i[i.length]=l)}return i}var s1,v3=F(()=>{"use strict";y3();sde();o(ode,"baseFlatten");s1=ode});function jrt(e){var t=e==null?0:e.length;return t?s1(e,1):[]}var xl,BO=F(()=>{"use strict";v3();o(jrt,"flatten");xl=jrt});function Xrt(e){return i3(o3(e,void 0,xl),e+"")}var lde,cde=F(()=>{"use strict";BO();_O();EO();o(Xrt,"flatRest");lde=Xrt});var Krt,o1,x3=F(()=>{"use strict";MO();Krt=h3(Object.getPrototypeOf,Object),o1=Krt});function rnt(e){if(!Bi(e)||Ds(e)!=Zrt)return!1;var t=o1(e);if(t===null)return!0;var r=ent.call(t,"constructor")&&t.constructor;return typeof r=="function"&&r instanceof r&&ude.call(r)==tnt}var Zrt,Qrt,Jrt,ude,ent,tnt,hde,dde=F(()=>{"use strict";sf();x3();lc();Zrt="[object Object]",Qrt=Function.prototype,Jrt=Object.prototype,ude=Qrt.toString,ent=Jrt.hasOwnProperty,tnt=ude.call(Object);o(rnt,"isPlainObject");hde=rnt});function hnt(e){return unt.test(e)}var nnt,int,ant,snt,ont,lnt,cnt,unt,fde,pde=F(()=>{"use strict";nnt="\\ud800-\\udfff",int="\\u0300-\\u036f",ant="\\ufe20-\\ufe2f",snt="\\u20d0-\\u20ff",ont=int+ant+snt,lnt="\\ufe0e\\ufe0f",cnt="\\u200d",unt=RegExp("["+cnt+nnt+ont+lnt+"]");o(hnt,"hasUnicode");fde=hnt});function dnt(e,t,r,n){var i=-1,a=e==null?0:e.length;for(n&&a&&(r=e[++i]);++i{"use strict";o(dnt,"arrayReduce");mde=dnt});function fnt(){this.__data__=new vf,this.size=0}var yde,vde=F(()=>{"use strict";nT();o(fnt,"stackClear");yde=fnt});function pnt(e){var t=this.__data__,r=t.delete(e);return this.size=t.size,r}var xde,bde=F(()=>{"use strict";o(pnt,"stackDelete");xde=pnt});function mnt(e){return this.__data__.get(e)}var Tde,Cde=F(()=>{"use strict";o(mnt,"stackGet");Tde=mnt});function gnt(e){return this.__data__.has(e)}var wde,kde=F(()=>{"use strict";o(gnt,"stackHas");wde=gnt});function vnt(e,t){var r=this.__data__;if(r instanceof vf){var n=r.__data__;if(!xf||n.length{"use strict";nT();p3();m3();ynt=200;o(vnt,"stackSet");Sde=vnt});function l1(e){var t=this.__data__=new vf(e);this.size=t.size}var uu,oT=F(()=>{"use strict";nT();vde();bde();Cde();kde();Ede();o(l1,"Stack");l1.prototype.clear=yde;l1.prototype.delete=xde;l1.prototype.get=Tde;l1.prototype.has=wde;l1.prototype.set=Sde;uu=l1});function xnt(e,t){return e&&ou(t,Di(t),e)}var Ade,Rde=F(()=>{"use strict";Xy();Rh();o(xnt,"baseAssign");Ade=xnt});function bnt(e,t){return e&&ou(t,lo(t),e)}var _de,Lde=F(()=>{"use strict";Xy();gf();o(bnt,"baseAssignIn");_de=bnt});function Cnt(e,t){if(t)return e.slice();var r=e.length,n=Mde?Mde(r):new e.constructor(r);return e.copy(n),n}var Nde,Dde,Tnt,Ide,Mde,b3,$O=F(()=>{"use strict";gl();Nde=typeof exports=="object"&&exports&&!exports.nodeType&&exports,Dde=Nde&&typeof module=="object"&&module&&!module.nodeType&&module,Tnt=Dde&&Dde.exports===Nde,Ide=Tnt?Li.Buffer:void 0,Mde=Ide?Ide.allocUnsafe:void 0;o(Cnt,"cloneBuffer");b3=Cnt});function wnt(e,t){for(var r=-1,n=e==null?0:e.length,i=0,a=[];++r{"use strict";o(wnt,"arrayFilter");T3=wnt});function knt(){return[]}var C3,zO=F(()=>{"use strict";o(knt,"stubArray");C3=knt});var Snt,Ent,Pde,Ant,c1,w3=F(()=>{"use strict";FO();zO();Snt=Object.prototype,Ent=Snt.propertyIsEnumerable,Pde=Object.getOwnPropertySymbols,Ant=Pde?function(e){return e==null?[]:(e=Object(e),T3(Pde(e),function(t){return Ent.call(e,t)}))}:C3,c1=Ant});function Rnt(e,t){return ou(e,c1(e),t)}var Ode,Bde=F(()=>{"use strict";Xy();w3();o(Rnt,"copySymbols");Ode=Rnt});var _nt,Lnt,k3,GO=F(()=>{"use strict";y3();x3();w3();zO();_nt=Object.getOwnPropertySymbols,Lnt=_nt?function(e){for(var t=[];e;)a1(t,c1(e)),e=o1(e);return t}:C3,k3=Lnt});function Dnt(e,t){return ou(e,k3(e),t)}var $de,Fde=F(()=>{"use strict";Xy();GO();o(Dnt,"copySymbolsIn");$de=Dnt});function Int(e,t,r){var n=t(e);return Yr(e)?n:a1(n,r(e))}var S3,VO=F(()=>{"use strict";y3();ji();o(Int,"baseGetAllKeys");S3=Int});function Mnt(e){return S3(e,Di,c1)}var lT,WO=F(()=>{"use strict";VO();w3();Rh();o(Mnt,"getAllKeys");lT=Mnt});function Nnt(e){return S3(e,lo,k3)}var zde,Gde=F(()=>{"use strict";VO();GO();gf();o(Nnt,"getAllKeysIn");zde=Nnt});var Pnt,E3,Vde=F(()=>{"use strict";lf();gl();Pnt=so(Li,"DataView"),E3=Pnt});var Ont,A3,Wde=F(()=>{"use strict";lf();gl();Ont=so(Li,"Promise"),A3=Ont});var Bnt,wf,qO=F(()=>{"use strict";lf();gl();Bnt=so(Li,"Set"),wf=Bnt});var qde,$nt,Hde,Ude,Yde,jde,Fnt,znt,Gnt,Vnt,Wnt,Om,Vo,Bm=F(()=>{"use strict";Vde();p3();Wde();qO();_ue();sf();CO();qde="[object Map]",$nt="[object Object]",Hde="[object Promise]",Ude="[object Set]",Yde="[object WeakMap]",jde="[object DataView]",Fnt=Sh(E3),znt=Sh(xf),Gnt=Sh(A3),Vnt=Sh(wf),Wnt=Sh(r3),Om=Ds;(E3&&Om(new E3(new ArrayBuffer(1)))!=jde||xf&&Om(new xf)!=qde||A3&&Om(A3.resolve())!=Hde||wf&&Om(new wf)!=Ude||r3&&Om(new r3)!=Yde)&&(Om=o(function(e){var t=Ds(e),r=t==$nt?e.constructor:void 0,n=r?Sh(r):"";if(n)switch(n){case Fnt:return jde;case znt:return qde;case Gnt:return Hde;case Vnt:return Ude;case Wnt:return Yde}return t},"getTag"));Vo=Om});function Unt(e){var t=e.length,r=new e.constructor(t);return t&&typeof e[0]=="string"&&Hnt.call(e,"index")&&(r.index=e.index,r.input=e.input),r}var qnt,Hnt,Xde,Kde=F(()=>{"use strict";qnt=Object.prototype,Hnt=qnt.hasOwnProperty;o(Unt,"initCloneArray");Xde=Unt});var Ynt,u1,HO=F(()=>{"use strict";gl();Ynt=Li.Uint8Array,u1=Ynt});function jnt(e){var t=new e.constructor(e.byteLength);return new u1(t).set(new u1(e)),t}var h1,R3=F(()=>{"use strict";HO();o(jnt,"cloneArrayBuffer");h1=jnt});function Xnt(e,t){var r=t?h1(e.buffer):e.buffer;return new e.constructor(r,e.byteOffset,e.byteLength)}var Zde,Qde=F(()=>{"use strict";R3();o(Xnt,"cloneDataView");Zde=Xnt});function Znt(e){var t=new e.constructor(e.source,Knt.exec(e));return t.lastIndex=e.lastIndex,t}var Knt,Jde,efe=F(()=>{"use strict";Knt=/\w*$/;o(Znt,"cloneRegExp");Jde=Znt});function Qnt(e){return rfe?Object(rfe.call(e)):{}}var tfe,rfe,nfe,ife=F(()=>{"use strict";Im();tfe=wa?wa.prototype:void 0,rfe=tfe?tfe.valueOf:void 0;o(Qnt,"cloneSymbol");nfe=Qnt});function Jnt(e,t){var r=t?h1(e.buffer):e.buffer;return new e.constructor(r,e.byteOffset,e.length)}var _3,UO=F(()=>{"use strict";R3();o(Jnt,"cloneTypedArray");_3=Jnt});function xit(e,t,r){var n=e.constructor;switch(t){case lit:return h1(e);case eit:case tit:return new n(+e);case cit:return Zde(e,r);case uit:case hit:case dit:case fit:case pit:case mit:case git:case yit:case vit:return _3(e,r);case rit:return new n;case nit:case sit:return new n(e);case iit:return Jde(e);case ait:return new n;case oit:return nfe(e)}}var eit,tit,rit,nit,iit,ait,sit,oit,lit,cit,uit,hit,dit,fit,pit,mit,git,yit,vit,afe,sfe=F(()=>{"use strict";R3();Qde();efe();ife();UO();eit="[object Boolean]",tit="[object Date]",rit="[object Map]",nit="[object Number]",iit="[object RegExp]",ait="[object Set]",sit="[object String]",oit="[object Symbol]",lit="[object ArrayBuffer]",cit="[object DataView]",uit="[object Float32Array]",hit="[object Float64Array]",dit="[object Int8Array]",fit="[object Int16Array]",pit="[object Int32Array]",mit="[object Uint8Array]",git="[object Uint8ClampedArray]",yit="[object Uint16Array]",vit="[object Uint32Array]";o(xit,"initCloneByTag");afe=xit});function bit(e){return typeof e.constructor=="function"&&!ff(e)?Due(o1(e)):{}}var L3,YO=F(()=>{"use strict";Iue();x3();Z2();o(bit,"initCloneObject");L3=bit});function Cit(e){return Bi(e)&&Vo(e)==Tit}var Tit,ofe,lfe=F(()=>{"use strict";Bm();lc();Tit="[object Map]";o(Cit,"baseIsMap");ofe=Cit});var cfe,wit,ufe,hfe=F(()=>{"use strict";lfe();Q2();c3();cfe=Ah&&Ah.isMap,wit=cfe?pf(cfe):ofe,ufe=wit});function Sit(e){return Bi(e)&&Vo(e)==kit}var kit,dfe,ffe=F(()=>{"use strict";Bm();lc();kit="[object Set]";o(Sit,"baseIsSet");dfe=Sit});var pfe,Eit,mfe,gfe=F(()=>{"use strict";ffe();Q2();c3();pfe=Ah&&Ah.isSet,Eit=pfe?pf(pfe):dfe,mfe=Eit});function D3(e,t,r,n,i,a){var s,l=t&Ait,u=t&Rit,h=t&_it;if(r&&(s=i?r(e,n,i,a):r(e)),s!==void 0)return s;if(!fi(e))return e;var d=Yr(e);if(d){if(s=Xde(e),!l)return n3(e,s)}else{var f=Vo(e),p=f==vfe||f==Nit;if(uc(e))return b3(e,l);if(f==xfe||f==yfe||p&&!i){if(s=u||p?{}:L3(e),!l)return u?$de(e,_de(s,e)):Ode(e,Ade(s,e))}else{if(!Yn[f])return i?e:{};s=afe(e,f,l)}}a||(a=new uu);var m=a.get(e);if(m)return m;a.set(e,s),mfe(e)?e.forEach(function(v){s.add(D3(v,t,r,v,e,a))}):ufe(e)&&e.forEach(function(v,x){s.set(x,D3(v,t,r,x,e,a))});var g=h?u?zde:lT:u?lo:Di,y=d?void 0:g(e);return a3(y||e,function(v,x){y&&(x=v,v=e[x]),hf(s,x,D3(v,t,r,x,e,a))}),s}var Ait,Rit,_it,yfe,Lit,Dit,Iit,Mit,vfe,Nit,Pit,Oit,xfe,Bit,$it,Fit,zit,Git,Vit,Wit,qit,Hit,Uit,Yit,jit,Xit,Kit,Zit,Qit,Yn,I3,jO=F(()=>{"use strict";oT();AO();j2();Rde();Lde();$O();wO();Bde();Fde();WO();Gde();Bm();Kde();sfe();YO();ji();Qy();hfe();yl();gfe();Rh();gf();Ait=1,Rit=2,_it=4,yfe="[object Arguments]",Lit="[object Array]",Dit="[object Boolean]",Iit="[object Date]",Mit="[object Error]",vfe="[object Function]",Nit="[object GeneratorFunction]",Pit="[object Map]",Oit="[object Number]",xfe="[object Object]",Bit="[object RegExp]",$it="[object Set]",Fit="[object String]",zit="[object Symbol]",Git="[object WeakMap]",Vit="[object ArrayBuffer]",Wit="[object DataView]",qit="[object Float32Array]",Hit="[object Float64Array]",Uit="[object Int8Array]",Yit="[object Int16Array]",jit="[object Int32Array]",Xit="[object Uint8Array]",Kit="[object Uint8ClampedArray]",Zit="[object Uint16Array]",Qit="[object Uint32Array]",Yn={};Yn[yfe]=Yn[Lit]=Yn[Vit]=Yn[Wit]=Yn[Dit]=Yn[Iit]=Yn[qit]=Yn[Hit]=Yn[Uit]=Yn[Yit]=Yn[jit]=Yn[Pit]=Yn[Oit]=Yn[xfe]=Yn[Bit]=Yn[$it]=Yn[Fit]=Yn[zit]=Yn[Xit]=Yn[Kit]=Yn[Zit]=Yn[Qit]=!0;Yn[Mit]=Yn[vfe]=Yn[Git]=!1;o(D3,"baseClone");I3=D3});function eat(e){return I3(e,Jit)}var Jit,XO,bfe=F(()=>{"use strict";jO();Jit=4;o(eat,"clone");XO=eat});function nat(e){return I3(e,tat|rat)}var tat,rat,KO,Tfe=F(()=>{"use strict";jO();tat=1,rat=4;o(nat,"cloneDeep");KO=nat});function aat(e){return this.__data__.set(e,iat),this}var iat,Cfe,wfe=F(()=>{"use strict";iat="__lodash_hash_undefined__";o(aat,"setCacheAdd");Cfe=aat});function sat(e){return this.__data__.has(e)}var kfe,Sfe=F(()=>{"use strict";o(sat,"setCacheHas");kfe=sat});function M3(e){var t=-1,r=e==null?0:e.length;for(this.__data__=new Pm;++t{"use strict";m3();wfe();Sfe();o(M3,"SetCache");M3.prototype.add=M3.prototype.push=Cfe;M3.prototype.has=kfe;N3=M3});function oat(e,t){for(var r=-1,n=e==null?0:e.length;++r{"use strict";o(oat,"arraySome");Efe=oat});function lat(e,t){return e.has(t)}var P3,QO=F(()=>{"use strict";o(lat,"cacheHas");P3=lat});function hat(e,t,r,n,i,a){var s=r&cat,l=e.length,u=t.length;if(l!=u&&!(s&&u>l))return!1;var h=a.get(e),d=a.get(t);if(h&&d)return h==t&&d==e;var f=-1,p=!0,m=r&uat?new N3:void 0;for(a.set(e,t),a.set(t,e);++f{"use strict";ZO();Afe();QO();cat=1,uat=2;o(hat,"equalArrays");O3=hat});function dat(e){var t=-1,r=Array(e.size);return e.forEach(function(n,i){r[++t]=[i,n]}),r}var Rfe,_fe=F(()=>{"use strict";o(dat,"mapToArray");Rfe=dat});function fat(e){var t=-1,r=Array(e.size);return e.forEach(function(n){r[++t]=n}),r}var d1,B3=F(()=>{"use strict";o(fat,"setToArray");d1=fat});function Aat(e,t,r,n,i,a,s){switch(r){case Eat:if(e.byteLength!=t.byteLength||e.byteOffset!=t.byteOffset)return!1;e=e.buffer,t=t.buffer;case Sat:return!(e.byteLength!=t.byteLength||!a(new u1(e),new u1(t)));case gat:case yat:case bat:return vl(+e,+t);case vat:return e.name==t.name&&e.message==t.message;case Tat:case wat:return e==t+"";case xat:var l=Rfe;case Cat:var u=n&pat;if(l||(l=d1),e.size!=t.size&&!u)return!1;var h=s.get(e);if(h)return h==t;n|=mat,s.set(e,t);var d=O3(l(e),l(t),n,i,a,s);return s.delete(e),d;case kat:if(e9)return e9.call(e)==e9.call(t)}return!1}var pat,mat,gat,yat,vat,xat,bat,Tat,Cat,wat,kat,Sat,Eat,Lfe,e9,Dfe,Ife=F(()=>{"use strict";Im();HO();Nm();JO();_fe();B3();pat=1,mat=2,gat="[object Boolean]",yat="[object Date]",vat="[object Error]",xat="[object Map]",bat="[object Number]",Tat="[object RegExp]",Cat="[object Set]",wat="[object String]",kat="[object Symbol]",Sat="[object ArrayBuffer]",Eat="[object DataView]",Lfe=wa?wa.prototype:void 0,e9=Lfe?Lfe.valueOf:void 0;o(Aat,"equalByTag");Dfe=Aat});function Dat(e,t,r,n,i,a){var s=r&Rat,l=lT(e),u=l.length,h=lT(t),d=h.length;if(u!=d&&!s)return!1;for(var f=u;f--;){var p=l[f];if(!(s?p in t:Lat.call(t,p)))return!1}var m=a.get(e),g=a.get(t);if(m&&g)return m==t&&g==e;var y=!0;a.set(e,t),a.set(t,e);for(var v=s;++f{"use strict";WO();Rat=1,_at=Object.prototype,Lat=_at.hasOwnProperty;o(Dat,"equalObjects");Mfe=Dat});function Nat(e,t,r,n,i,a){var s=Yr(e),l=Yr(t),u=s?Ofe:Vo(e),h=l?Ofe:Vo(t);u=u==Pfe?$3:u,h=h==Pfe?$3:h;var d=u==$3,f=h==$3,p=u==h;if(p&&uc(e)){if(!uc(t))return!1;s=!0,d=!1}if(p&&!d)return a||(a=new uu),s||mf(e)?O3(e,t,r,n,i,a):Dfe(e,t,u,r,n,i,a);if(!(r&Iat)){var m=d&&Bfe.call(e,"__wrapped__"),g=f&&Bfe.call(t,"__wrapped__");if(m||g){var y=m?e.value():e,v=g?t.value():t;return a||(a=new uu),i(y,v,r,n,a)}}return p?(a||(a=new uu),Mfe(e,t,r,n,i,a)):!1}var Iat,Pfe,Ofe,$3,Mat,Bfe,$fe,Ffe=F(()=>{"use strict";oT();JO();Ife();Nfe();Bm();ji();Qy();eT();Iat=1,Pfe="[object Arguments]",Ofe="[object Array]",$3="[object Object]",Mat=Object.prototype,Bfe=Mat.hasOwnProperty;o(Nat,"baseIsEqualDeep");$fe=Nat});function zfe(e,t,r,n,i){return e===t?!0:e==null||t==null||!Bi(e)&&!Bi(t)?e!==e&&t!==t:$fe(e,t,r,n,zfe,i)}var F3,t9=F(()=>{"use strict";Ffe();lc();o(zfe,"baseIsEqual");F3=zfe});function Bat(e,t,r,n){var i=r.length,a=i,s=!n;if(e==null)return!a;for(e=Object(e);i--;){var l=r[i];if(s&&l[2]?l[1]!==e[l[0]]:!(l[0]in e))return!1}for(;++i{"use strict";oT();t9();Pat=1,Oat=2;o(Bat,"baseIsMatch");Gfe=Bat});function $at(e){return e===e&&!fi(e)}var z3,r9=F(()=>{"use strict";yl();o($at,"isStrictComparable");z3=$at});function Fat(e){for(var t=Di(e),r=t.length;r--;){var n=t[r],i=e[n];t[r]=[n,i,z3(i)]}return t}var Wfe,qfe=F(()=>{"use strict";r9();Rh();o(Fat,"getMatchData");Wfe=Fat});function zat(e,t){return function(r){return r==null?!1:r[e]===t&&(t!==void 0||e in Object(r))}}var G3,n9=F(()=>{"use strict";o(zat,"matchesStrictComparable");G3=zat});function Gat(e){var t=Wfe(e);return t.length==1&&t[0][2]?G3(t[0][0],t[0][1]):function(r){return r===e||Gfe(r,e,t)}}var Hfe,Ufe=F(()=>{"use strict";Vfe();qfe();n9();o(Gat,"baseMatches");Hfe=Gat});function Vat(e,t){return e!=null&&t in Object(e)}var Yfe,jfe=F(()=>{"use strict";o(Vat,"baseHasIn");Yfe=Vat});function Wat(e,t,r){t=Tf(t,e);for(var n=-1,i=t.length,a=!1;++n{"use strict";aT();Zy();ji();U2();l3();i1();o(Wat,"hasPath");V3=Wat});function qat(e,t){return e!=null&&V3(e,t,Yfe)}var W3,a9=F(()=>{"use strict";jfe();i9();o(qat,"hasIn");W3=qat});function Yat(e,t){return e1(e)&&z3(t)?G3(cu(e),t):function(r){var n=rde(r,e);return n===void 0&&n===t?W3(r,e):F3(t,n,Hat|Uat)}}var Hat,Uat,Xfe,Kfe=F(()=>{"use strict";t9();nde();a9();f3();r9();n9();i1();Hat=1,Uat=2;o(Yat,"baseMatchesProperty");Xfe=Yat});function jat(e){return function(t){return t?.[e]}}var q3,s9=F(()=>{"use strict";o(jat,"baseProperty");q3=jat});function Xat(e){return function(t){return Cf(t,e)}}var Zfe,Qfe=F(()=>{"use strict";sT();o(Xat,"basePropertyDeep");Zfe=Xat});function Kat(e){return e1(e)?q3(cu(e)):Zfe(e)}var Jfe,epe=F(()=>{"use strict";s9();Qfe();f3();i1();o(Kat,"property");Jfe=Kat});function Zat(e){return typeof e=="function"?e:e==null?ao:typeof e=="object"?Yr(e)?Xfe(e[0],e[1]):Hfe(e):Jfe(e)}var os,Lh=F(()=>{"use strict";Ufe();Kfe();of();ji();epe();o(Zat,"baseIteratee");os=Zat});function Qat(e){return function(t,r,n){for(var i=-1,a=Object(t),s=n(t),l=s.length;l--;){var u=s[e?l:++i];if(r(a[u],u,a)===!1)break}return t}}var tpe,rpe=F(()=>{"use strict";o(Qat,"createBaseFor");tpe=Qat});var Jat,f1,H3=F(()=>{"use strict";rpe();Jat=tpe(),f1=Jat});function est(e,t){return e&&f1(e,t,Di)}var p1,U3=F(()=>{"use strict";H3();Rh();o(est,"baseForOwn");p1=est});function tst(e,t){return function(r,n){if(r==null)return r;if(!ka(r))return e(r,n);for(var i=r.length,a=t?i:-1,s=Object(r);(t?a--:++a{"use strict";lu();o(tst,"createBaseEach");npe=tst});var rst,kf,cT=F(()=>{"use strict";U3();ipe();rst=npe(p1),kf=rst});var nst,Y3,ape=F(()=>{"use strict";gl();nst=o(function(){return Li.Date.now()},"now"),Y3=nst});var spe,ist,ast,o9,ope=F(()=>{"use strict";X2();Nm();K2();gf();spe=Object.prototype,ist=spe.hasOwnProperty,ast=df(function(e,t){e=Object(e);var r=-1,n=t.length,i=n>2?t[2]:void 0;for(i&&Eh(t[0],t[1],i)&&(n=1);++r{"use strict";Y2();Nm();o(sst,"assignMergeValue");uT=sst});function ost(e){return Bi(e)&&ka(e)}var j3,c9=F(()=>{"use strict";lu();lc();o(ost,"isArrayLikeObject");j3=ost});function lst(e,t){if(!(t==="constructor"&&typeof e[t]=="function")&&t!="__proto__")return e[t]}var hT,u9=F(()=>{"use strict";o(lst,"safeGet");hT=lst});function cst(e){return ou(e,lo(e))}var lpe,cpe=F(()=>{"use strict";Xy();gf();o(cst,"toPlainObject");lpe=cst});function ust(e,t,r,n,i,a,s){var l=hT(e,r),u=hT(t,r),h=s.get(u);if(h){uT(e,r,h);return}var d=a?a(l,u,r+"",e,t,s):void 0,f=d===void 0;if(f){var p=Yr(u),m=!p&&uc(u),g=!p&&!m&&mf(u);d=u,p||m||g?Yr(l)?d=l:j3(l)?d=n3(l):m?(f=!1,d=b3(u,!0)):g?(f=!1,d=_3(u,!0)):d=[]:hde(u)||cc(u)?(d=l,cc(l)?d=lpe(l):(!fi(l)||su(l))&&(d=L3(u))):f=!1}f&&(s.set(u,d),i(d,u,n,a,s),s.delete(u)),uT(e,r,d)}var upe,hpe=F(()=>{"use strict";l9();$O();UO();wO();YO();Zy();ji();c9();Qy();H2();yl();dde();eT();u9();cpe();o(ust,"baseMergeDeep");upe=ust});function dpe(e,t,r,n,i){e!==t&&f1(t,function(a,s){if(i||(i=new uu),fi(a))upe(e,t,s,r,dpe,n,i);else{var l=n?n(hT(e,s),a,s+"",e,t,i):void 0;l===void 0&&(l=a),uT(e,s,l)}},lo)}var fpe,ppe=F(()=>{"use strict";oT();l9();H3();hpe();yl();gf();u9();o(dpe,"baseMerge");fpe=dpe});function hst(e,t,r){for(var n=-1,i=e==null?0:e.length;++n{"use strict";o(hst,"arrayIncludesWith");mpe=hst});function dst(e){var t=e==null?0:e.length;return t?e[t-1]:void 0}var Sf,ype=F(()=>{"use strict";o(dst,"last");Sf=dst});function fst(e){return typeof e=="function"?e:ao}var m1,X3=F(()=>{"use strict";of();o(fst,"castFunction");m1=fst});function pst(e,t){var r=Yr(e)?a3:kf;return r(e,m1(t))}var st,h9=F(()=>{"use strict";AO();cT();X3();ji();o(pst,"forEach");st=pst});var vpe=F(()=>{"use strict";h9()});function mst(e,t){var r=[];return kf(e,function(n,i,a){t(n,i,a)&&r.push(n)}),r}var xpe,bpe=F(()=>{"use strict";cT();o(mst,"baseFilter");xpe=mst});function gst(e,t){var r=Yr(e)?T3:xpe;return r(e,os(t,3))}var Is,Tpe=F(()=>{"use strict";FO();bpe();Lh();ji();o(gst,"filter");Is=gst});function yst(e){return function(t,r,n){var i=Object(t);if(!ka(t)){var a=os(r,3);t=Di(t),r=o(function(l){return a(i[l],l,i)},"predicate")}var s=e(t,r,n);return s>-1?i[a?t[s]:s]:void 0}}var Cpe,wpe=F(()=>{"use strict";Lh();lu();Rh();o(yst,"createFind");Cpe=yst});function xst(e,t,r){var n=e==null?0:e.length;if(!n)return-1;var i=r==null?0:xue(r);return i<0&&(i=vst(n+i,0)),s3(e,os(t,3),i)}var vst,kpe,Spe=F(()=>{"use strict";RO();Lh();bue();vst=Math.max;o(xst,"findIndex");kpe=xst});var bst,g1,Epe=F(()=>{"use strict";wpe();Spe();bst=Cpe(kpe),g1=bst});function Tst(e,t){var r=-1,n=ka(e)?Array(e.length):[];return kf(e,function(i,a,s){n[++r]=t(i,a,s)}),n}var K3,d9=F(()=>{"use strict";cT();lu();o(Tst,"baseMap");K3=Tst});function Cst(e,t){var r=Yr(e)?au:K3;return r(e,os(t,3))}var mn,Ape=F(()=>{"use strict";q2();Lh();d9();ji();o(Cst,"map");mn=Cst});function wst(e,t){return e==null?e:f1(e,m1(t),lo)}var f9,Rpe=F(()=>{"use strict";H3();X3();gf();o(wst,"forIn");f9=wst});function kst(e,t){return e&&p1(e,m1(t))}var p9,_pe=F(()=>{"use strict";U3();X3();o(kst,"forOwn");p9=kst});function Sst(e,t){return e>t}var Lpe,Dpe=F(()=>{"use strict";o(Sst,"baseGt");Lpe=Sst});function Rst(e,t){return e!=null&&Ast.call(e,t)}var Est,Ast,Ipe,Mpe=F(()=>{"use strict";Est=Object.prototype,Ast=Est.hasOwnProperty;o(Rst,"baseHas");Ipe=Rst});function _st(e,t){return e!=null&&V3(e,t,Ipe)}var dT,Npe=F(()=>{"use strict";Mpe();i9();o(_st,"has");dT=_st});function Dst(e){return typeof e=="string"||!Yr(e)&&Bi(e)&&Ds(e)==Lst}var Lst,Ppe,Ope=F(()=>{"use strict";sf();ji();lc();Lst="[object String]";o(Dst,"isString");Ppe=Dst});function Ist(e,t){return au(t,function(r){return e[r]})}var Bpe,$pe=F(()=>{"use strict";q2();o(Ist,"baseValues");Bpe=Ist});function Mst(e){return e==null?[]:Bpe(e,Di(e))}var Wo,Fpe=F(()=>{"use strict";$pe();Rh();o(Mst,"values");Wo=Mst});function $st(e){if(e==null)return!0;if(ka(e)&&(Yr(e)||typeof e=="string"||typeof e.splice=="function"||uc(e)||mf(e)||cc(e)))return!e.length;var t=Vo(e);if(t==Nst||t==Pst)return!e.size;if(ff(e))return!Jy(e).length;for(var r in e)if(Bst.call(e,r))return!1;return!0}var Nst,Pst,Ost,Bst,Z3,zpe=F(()=>{"use strict";d3();Bm();Zy();ji();lu();Qy();Z2();eT();Nst="[object Map]",Pst="[object Set]",Ost=Object.prototype,Bst=Ost.hasOwnProperty;o($st,"isEmpty");Z3=$st});function Fst(e){return e===void 0}var Fn,Gpe=F(()=>{"use strict";o(Fst,"isUndefined");Fn=Fst});function zst(e,t){return e{"use strict";o(zst,"baseLt");Q3=zst});function Gst(e,t){var r={};return t=os(t,3),p1(e,function(n,i,a){uf(r,i,t(n,i,a))}),r}var $m,Vpe=F(()=>{"use strict";Y2();U3();Lh();o(Gst,"mapValues");$m=Gst});function Vst(e,t,r){for(var n=-1,i=e.length;++n{"use strict";Mm();o(Vst,"baseExtremum");y1=Vst});function Wst(e){return e&&e.length?y1(e,ao,Lpe):void 0}var co,Wpe=F(()=>{"use strict";J3();Dpe();of();o(Wst,"max");co=Wst});var qst,v1,qpe=F(()=>{"use strict";ppe();Zue();qst=Kue(function(e,t,r){fpe(e,t,r)}),v1=qst});function Hst(e){return e&&e.length?y1(e,ao,Q3):void 0}var Dh,Hpe=F(()=>{"use strict";J3();m9();of();o(Hst,"min");Dh=Hst});function Ust(e,t){return e&&e.length?y1(e,os(t,2),Q3):void 0}var Fm,Upe=F(()=>{"use strict";J3();Lh();m9();o(Ust,"minBy");Fm=Ust});function Yst(e,t,r,n){if(!fi(e))return e;t=Tf(t,e);for(var i=-1,a=t.length,s=a-1,l=e;l!=null&&++i{"use strict";j2();aT();U2();yl();i1();o(Yst,"baseSet");Ype=Yst});function jst(e,t,r){for(var n=-1,i=t.length,a={};++n{"use strict";sT();jpe();aT();o(jst,"basePickBy");Xpe=jst});function Xst(e,t){var r=e.length;for(e.sort(t);r--;)e[r]=e[r].value;return e}var Zpe,Qpe=F(()=>{"use strict";o(Xst,"baseSortBy");Zpe=Xst});function Kst(e,t){if(e!==t){var r=e!==void 0,n=e===null,i=e===e,a=Go(e),s=t!==void 0,l=t===null,u=t===t,h=Go(t);if(!l&&!h&&!a&&e>t||a&&s&&u&&!l&&!h||n&&s&&u||!r&&u||!i)return 1;if(!n&&!a&&!h&&e{"use strict";Mm();o(Kst,"compareAscending");Jpe=Kst});function Zst(e,t,r){for(var n=-1,i=e.criteria,a=t.criteria,s=i.length,l=r.length;++n=l)return u;var h=r[n];return u*(h=="desc"?-1:1)}}return e.index-t.index}var tme,rme=F(()=>{"use strict";eme();o(Zst,"compareMultiple");tme=Zst});function Qst(e,t,r){t.length?t=au(t,function(a){return Yr(a)?function(s){return Cf(s,a.length===1?a[0]:a)}:a}):t=[ao];var n=-1;t=au(t,pf(os));var i=K3(e,function(a,s,l){var u=au(t,function(h){return h(a)});return{criteria:u,index:++n,value:a}});return Zpe(i,function(a,s){return tme(a,s,r)})}var nme,ime=F(()=>{"use strict";q2();sT();Lh();d9();Qpe();Q2();rme();of();ji();o(Qst,"baseOrderBy");nme=Qst});var Jst,ame,sme=F(()=>{"use strict";s9();Jst=q3("length"),ame=Jst});function hot(e){for(var t=ome.lastIndex=0;ome.test(e);)++t;return t}var lme,eot,tot,rot,not,iot,aot,g9,y9,sot,cme,ume,hme,oot,dme,fme,lot,cot,uot,ome,pme,mme=F(()=>{"use strict";lme="\\ud800-\\udfff",eot="\\u0300-\\u036f",tot="\\ufe20-\\ufe2f",rot="\\u20d0-\\u20ff",not=eot+tot+rot,iot="\\ufe0e\\ufe0f",aot="["+lme+"]",g9="["+not+"]",y9="\\ud83c[\\udffb-\\udfff]",sot="(?:"+g9+"|"+y9+")",cme="[^"+lme+"]",ume="(?:\\ud83c[\\udde6-\\uddff]){2}",hme="[\\ud800-\\udbff][\\udc00-\\udfff]",oot="\\u200d",dme=sot+"?",fme="["+iot+"]?",lot="(?:"+oot+"(?:"+[cme,ume,hme].join("|")+")"+fme+dme+")*",cot=fme+dme+lot,uot="(?:"+[cme+g9+"?",g9,ume,hme,aot].join("|")+")",ome=RegExp(y9+"(?="+y9+")|"+uot+cot,"g");o(hot,"unicodeSize");pme=hot});function dot(e){return fde(e)?pme(e):ame(e)}var gme,yme=F(()=>{"use strict";sme();pde();mme();o(dot,"stringSize");gme=dot});function fot(e,t){return Xpe(e,t,function(r,n){return W3(e,n)})}var vme,xme=F(()=>{"use strict";Kpe();a9();o(fot,"basePick");vme=fot});var pot,zm,bme=F(()=>{"use strict";xme();cde();pot=lde(function(e,t){return e==null?{}:vme(e,t)}),zm=pot});function yot(e,t,r,n){for(var i=-1,a=got(mot((t-e)/(r||1)),0),s=Array(a);a--;)s[n?a:++i]=e,e+=r;return s}var mot,got,Tme,Cme=F(()=>{"use strict";mot=Math.ceil,got=Math.max;o(yot,"baseRange");Tme=yot});function vot(e){return function(t,r,n){return n&&typeof n!="number"&&Eh(t,r,n)&&(r=n=void 0),t=Yy(t),r===void 0?(r=t,t=0):r=Yy(r),n=n===void 0?t{"use strict";Cme();K2();TO();o(vot,"createRange");wme=vot});var xot,bl,Sme=F(()=>{"use strict";kme();xot=wme(),bl=xot});function bot(e,t,r,n,i){return i(e,function(a,s,l){r=n?(n=!1,a):t(r,a,s,l)}),r}var Eme,Ame=F(()=>{"use strict";o(bot,"baseReduce");Eme=bot});function Tot(e,t,r){var n=Yr(e)?mde:Eme,i=arguments.length<3;return n(e,os(t,4),r,i,kf)}var hu,Rme=F(()=>{"use strict";gde();cT();Lh();Ame();ji();o(Tot,"reduce");hu=Tot});function kot(e){if(e==null)return 0;if(ka(e))return Ppe(e)?gme(e):e.length;var t=Vo(e);return t==Cot||t==wot?e.size:Jy(e).length}var Cot,wot,v9,_me=F(()=>{"use strict";d3();Bm();lu();Ope();yme();Cot="[object Map]",wot="[object Set]";o(kot,"size");v9=kot});var Sot,du,Lme=F(()=>{"use strict";v3();ime();X2();K2();Sot=df(function(e,t){if(e==null)return[];var r=t.length;return r>1&&Eh(e,t[0],t[1])?t=[]:r>2&&Eh(t[0],t[1],t[2])&&(t=[t[0]]),nme(e,s1(t,1),[])}),du=Sot});var Eot,Aot,Dme,Ime=F(()=>{"use strict";qO();Oue();B3();Eot=1/0,Aot=wf&&1/d1(new wf([,-0]))[1]==Eot?function(e){return new wf(e)}:Pue,Dme=Aot});function _ot(e,t,r){var n=-1,i=Yue,a=e.length,s=!0,l=[],u=l;if(r)s=!1,i=mpe;else if(a>=Rot){var h=t?null:Dme(e);if(h)return d1(h);s=!1,i=P3,u=new N3}else u=t?[]:l;e:for(;++n{"use strict";ZO();jue();gpe();QO();Ime();B3();Rot=200;o(_ot,"baseUniq");Mme=_ot});var Lot,x9,Pme=F(()=>{"use strict";v3();X2();Nme();c9();Lot=df(function(e){return Mme(s1(e,1,j3,!0))}),x9=Lot});function Iot(e){var t=++Dot;return g3(e)+t}var Dot,Gm,Ome=F(()=>{"use strict";OO();Dot=0;o(Iot,"uniqueId");Gm=Iot});function Mot(e,t,r){for(var n=-1,i=e.length,a=t.length,s={};++n{"use strict";o(Mot,"baseZipObject");Bme=Mot});function Not(e,t){return Bme(e||[],t||[],hf)}var e5,Fme=F(()=>{"use strict";j2();$me();o(Not,"zipObject");e5=Not});var Ln=F(()=>{"use strict";bfe();Tfe();kO();ope();vpe();Tpe();Epe();BO();h9();Rpe();_pe();Npe();ji();zpe();H2();Gpe();Rh();ype();Ape();Vpe();Wpe();qpe();Hpe();Upe();ape();bme();Sme();Rme();_me();Lme();Pme();Ome();Fpe();Fme();});function Gme(e,t){e[t]?e[t]++:e[t]=1}function Vme(e,t){--e[t]||delete e[t]}function fT(e,t,r,n){var i=""+t,a=""+r;if(!e&&i>a){var s=i;i=a,a=s}return i+zme+a+zme+(Fn(n)?Pot:n)}function Oot(e,t,r,n){var i=""+t,a=""+r;if(!e&&i>a){var s=i;i=a,a=s}var l={v:i,w:a};return n&&(l.name=n),l}function b9(e,t){return fT(e,t.v,t.w,t.name)}var Pot,Vm,zme,on,t5=F(()=>{"use strict";Ln();Pot="\0",Vm="\0",zme="",on=class{static{o(this,"Graph")}constructor(t={}){this._isDirected=Object.prototype.hasOwnProperty.call(t,"directed")?t.directed:!0,this._isMultigraph=Object.prototype.hasOwnProperty.call(t,"multigraph")?t.multigraph:!1,this._isCompound=Object.prototype.hasOwnProperty.call(t,"compound")?t.compound:!1,this._label=void 0,this._defaultNodeLabelFn=oo(void 0),this._defaultEdgeLabelFn=oo(void 0),this._nodes={},this._isCompound&&(this._parent={},this._children={},this._children[Vm]={}),this._in={},this._preds={},this._out={},this._sucs={},this._edgeObjs={},this._edgeLabels={}}isDirected(){return this._isDirected}isMultigraph(){return this._isMultigraph}isCompound(){return this._isCompound}setGraph(t){return this._label=t,this}graph(){return this._label}setDefaultNodeLabel(t){return su(t)||(t=oo(t)),this._defaultNodeLabelFn=t,this}nodeCount(){return this._nodeCount}nodes(){return Di(this._nodes)}sources(){var t=this;return Is(this.nodes(),function(r){return Z3(t._in[r])})}sinks(){var t=this;return Is(this.nodes(),function(r){return Z3(t._out[r])})}setNodes(t,r){var n=arguments,i=this;return st(t,function(a){n.length>1?i.setNode(a,r):i.setNode(a)}),this}setNode(t,r){return Object.prototype.hasOwnProperty.call(this._nodes,t)?(arguments.length>1&&(this._nodes[t]=r),this):(this._nodes[t]=arguments.length>1?r:this._defaultNodeLabelFn(t),this._isCompound&&(this._parent[t]=Vm,this._children[t]={},this._children[Vm][t]=!0),this._in[t]={},this._preds[t]={},this._out[t]={},this._sucs[t]={},++this._nodeCount,this)}node(t){return this._nodes[t]}hasNode(t){return Object.prototype.hasOwnProperty.call(this._nodes,t)}removeNode(t){if(Object.prototype.hasOwnProperty.call(this._nodes,t)){var r=o(n=>this.removeEdge(this._edgeObjs[n]),"removeEdge");delete this._nodes[t],this._isCompound&&(this._removeFromParentsChildList(t),delete this._parent[t],st(this.children(t),n=>{this.setParent(n)}),delete this._children[t]),st(Di(this._in[t]),r),delete this._in[t],delete this._preds[t],st(Di(this._out[t]),r),delete this._out[t],delete this._sucs[t],--this._nodeCount}return this}setParent(t,r){if(!this._isCompound)throw new Error("Cannot set parent in a non-compound graph");if(Fn(r))r=Vm;else{r+="";for(var n=r;!Fn(n);n=this.parent(n))if(n===t)throw new Error("Setting "+r+" as parent of "+t+" would create a cycle");this.setNode(r)}return this.setNode(t),this._removeFromParentsChildList(t),this._parent[t]=r,this._children[r][t]=!0,this}_removeFromParentsChildList(t){delete this._children[this._parent[t]][t]}parent(t){if(this._isCompound){var r=this._parent[t];if(r!==Vm)return r}}children(t){if(Fn(t)&&(t=Vm),this._isCompound){var r=this._children[t];if(r)return Di(r)}else{if(t===Vm)return this.nodes();if(this.hasNode(t))return[]}}predecessors(t){var r=this._preds[t];if(r)return Di(r)}successors(t){var r=this._sucs[t];if(r)return Di(r)}neighbors(t){var r=this.predecessors(t);if(r)return x9(r,this.successors(t))}isLeaf(t){var r;return this.isDirected()?r=this.successors(t):r=this.neighbors(t),r.length===0}filterNodes(t){var r=new this.constructor({directed:this._isDirected,multigraph:this._isMultigraph,compound:this._isCompound});r.setGraph(this.graph());var n=this;st(this._nodes,function(s,l){t(l)&&r.setNode(l,s)}),st(this._edgeObjs,function(s){r.hasNode(s.v)&&r.hasNode(s.w)&&r.setEdge(s,n.edge(s))});var i={};function a(s){var l=n.parent(s);return l===void 0||r.hasNode(l)?(i[s]=l,l):l in i?i[l]:a(l)}return o(a,"findParent"),this._isCompound&&st(r.nodes(),function(s){r.setParent(s,a(s))}),r}setDefaultEdgeLabel(t){return su(t)||(t=oo(t)),this._defaultEdgeLabelFn=t,this}edgeCount(){return this._edgeCount}edges(){return Wo(this._edgeObjs)}setPath(t,r){var n=this,i=arguments;return hu(t,function(a,s){return i.length>1?n.setEdge(a,s,r):n.setEdge(a,s),s}),this}setEdge(){var t,r,n,i,a=!1,s=arguments[0];typeof s=="object"&&s!==null&&"v"in s?(t=s.v,r=s.w,n=s.name,arguments.length===2&&(i=arguments[1],a=!0)):(t=s,r=arguments[1],n=arguments[3],arguments.length>2&&(i=arguments[2],a=!0)),t=""+t,r=""+r,Fn(n)||(n=""+n);var l=fT(this._isDirected,t,r,n);if(Object.prototype.hasOwnProperty.call(this._edgeLabels,l))return a&&(this._edgeLabels[l]=i),this;if(!Fn(n)&&!this._isMultigraph)throw new Error("Cannot set a named edge when isMultigraph = false");this.setNode(t),this.setNode(r),this._edgeLabels[l]=a?i:this._defaultEdgeLabelFn(t,r,n);var u=Oot(this._isDirected,t,r,n);return t=u.v,r=u.w,Object.freeze(u),this._edgeObjs[l]=u,Gme(this._preds[r],t),Gme(this._sucs[t],r),this._in[r][l]=u,this._out[t][l]=u,this._edgeCount++,this}edge(t,r,n){var i=arguments.length===1?b9(this._isDirected,arguments[0]):fT(this._isDirected,t,r,n);return this._edgeLabels[i]}hasEdge(t,r,n){var i=arguments.length===1?b9(this._isDirected,arguments[0]):fT(this._isDirected,t,r,n);return Object.prototype.hasOwnProperty.call(this._edgeLabels,i)}removeEdge(t,r,n){var i=arguments.length===1?b9(this._isDirected,arguments[0]):fT(this._isDirected,t,r,n),a=this._edgeObjs[i];return a&&(t=a.v,r=a.w,delete this._edgeLabels[i],delete this._edgeObjs[i],Vme(this._preds[r],t),Vme(this._sucs[t],r),delete this._in[r][i],delete this._out[t][i],this._edgeCount--),this}inEdges(t,r){var n=this._in[t];if(n){var i=Wo(n);return r?Is(i,function(a){return a.v===r}):i}}outEdges(t,r){var n=this._out[t];if(n){var i=Wo(n);return r?Is(i,function(a){return a.w===r}):i}}nodeEdges(t,r){var n=this.inEdges(t,r);if(n)return n.concat(this.outEdges(t,r))}};on.prototype._nodeCount=0;on.prototype._edgeCount=0;o(Gme,"incrementOrInitEntry");o(Vme,"decrementOrRemoveEntry");o(fT,"edgeArgsToId");o(Oot,"edgeArgsToObj");o(b9,"edgeObjToId")});var qo=F(()=>{"use strict";t5()});function Wme(e){e._prev._next=e._next,e._next._prev=e._prev,delete e._next,delete e._prev}function Bot(e,t){if(e!=="_next"&&e!=="_prev")return t}var r5,qme=F(()=>{"use strict";r5=class{static{o(this,"List")}constructor(){var t={};t._next=t._prev=t,this._sentinel=t}dequeue(){var t=this._sentinel,r=t._prev;if(r!==t)return Wme(r),r}enqueue(t){var r=this._sentinel;t._prev&&t._next&&Wme(t),t._next=r._next,r._next._prev=t,r._next=t,t._prev=r}toString(){for(var t=[],r=this._sentinel,n=r._prev;n!==r;)t.push(JSON.stringify(n,Bot)),n=n._prev;return"["+t.join(", ")+"]"}};o(Wme,"unlink");o(Bot,"filterOutLinks")});function Hme(e,t){if(e.nodeCount()<=1)return[];var r=zot(e,t||$ot),n=Fot(r.graph,r.buckets,r.zeroIdx);return xl(mn(n,function(i){return e.outEdges(i.v,i.w)}))}function Fot(e,t,r){for(var n=[],i=t[t.length-1],a=t[0],s;e.nodeCount();){for(;s=a.dequeue();)T9(e,t,r,s);for(;s=i.dequeue();)T9(e,t,r,s);if(e.nodeCount()){for(var l=t.length-2;l>0;--l)if(s=t[l].dequeue(),s){n=n.concat(T9(e,t,r,s,!0));break}}}return n}function T9(e,t,r,n,i){var a=i?[]:void 0;return st(e.inEdges(n.v),function(s){var l=e.edge(s),u=e.node(s.v);i&&a.push({v:s.v,w:s.w}),u.out-=l,C9(t,r,u)}),st(e.outEdges(n.v),function(s){var l=e.edge(s),u=s.w,h=e.node(u);h.in-=l,C9(t,r,h)}),e.removeNode(n.v),a}function zot(e,t){var r=new on,n=0,i=0;st(e.nodes(),function(l){r.setNode(l,{v:l,in:0,out:0})}),st(e.edges(),function(l){var u=r.edge(l.v,l.w)||0,h=t(l),d=u+h;r.setEdge(l.v,l.w,d),i=Math.max(i,r.node(l.v).out+=h),n=Math.max(n,r.node(l.w).in+=h)});var a=bl(i+n+3).map(function(){return new r5}),s=n+1;return st(r.nodes(),function(l){C9(a,s,r.node(l))}),{graph:r,buckets:a,zeroIdx:s}}function C9(e,t,r){r.out?r.in?e[r.out-r.in+t].enqueue(r):e[e.length-1].enqueue(r):e[0].enqueue(r)}var $ot,Ume=F(()=>{"use strict";Ln();qo();qme();$ot=oo(1);o(Hme,"greedyFAS");o(Fot,"doGreedyFAS");o(T9,"removeNode");o(zot,"buildState");o(C9,"assignBucket")});function Yme(e){var t=e.graph().acyclicer==="greedy"?Hme(e,r(e)):Got(e);st(t,function(n){var i=e.edge(n);e.removeEdge(n),i.forwardName=n.name,i.reversed=!0,e.setEdge(n.w,n.v,i,Gm("rev"))});function r(n){return function(i){return n.edge(i).weight}}o(r,"weightFn")}function Got(e){var t=[],r={},n={};function i(a){Object.prototype.hasOwnProperty.call(n,a)||(n[a]=!0,r[a]=!0,st(e.outEdges(a),function(s){Object.prototype.hasOwnProperty.call(r,s.w)?t.push(s):i(s.w)}),delete r[a])}return o(i,"dfs"),st(e.nodes(),i),t}function jme(e){st(e.edges(),function(t){var r=e.edge(t);if(r.reversed){e.removeEdge(t);var n=r.forwardName;delete r.reversed,delete r.forwardName,e.setEdge(t.w,t.v,r,n)}})}var w9=F(()=>{"use strict";Ln();Ume();o(Yme,"run");o(Got,"dfsFAS");o(jme,"undo")});function fu(e,t,r,n){var i;do i=Gm(n);while(e.hasNode(i));return r.dummy=t,e.setNode(i,r),i}function Kme(e){var t=new on().setGraph(e.graph());return st(e.nodes(),function(r){t.setNode(r,e.node(r))}),st(e.edges(),function(r){var n=t.edge(r.v,r.w)||{weight:0,minlen:1},i=e.edge(r);t.setEdge(r.v,r.w,{weight:n.weight+i.weight,minlen:Math.max(n.minlen,i.minlen)})}),t}function n5(e){var t=new on({multigraph:e.isMultigraph()}).setGraph(e.graph());return st(e.nodes(),function(r){e.children(r).length||t.setNode(r,e.node(r))}),st(e.edges(),function(r){t.setEdge(r,e.edge(r))}),t}function k9(e,t){var r=e.x,n=e.y,i=t.x-r,a=t.y-n,s=e.width/2,l=e.height/2;if(!i&&!a)throw new Error("Not possible to find intersection inside of the rectangle");var u,h;return Math.abs(a)*s>Math.abs(i)*l?(a<0&&(l=-l),u=l*i/a,h=l):(i<0&&(s=-s),u=s,h=s*a/i),{x:r+u,y:n+h}}function Ef(e){var t=mn(bl(E9(e)+1),function(){return[]});return st(e.nodes(),function(r){var n=e.node(r),i=n.rank;Fn(i)||(t[i][n.order]=r)}),t}function Zme(e){var t=Dh(mn(e.nodes(),function(r){return e.node(r).rank}));st(e.nodes(),function(r){var n=e.node(r);dT(n,"rank")&&(n.rank-=t)})}function Qme(e){var t=Dh(mn(e.nodes(),function(a){return e.node(a).rank})),r=[];st(e.nodes(),function(a){var s=e.node(a).rank-t;r[s]||(r[s]=[]),r[s].push(a)});var n=0,i=e.graph().nodeRankFactor;st(r,function(a,s){Fn(a)&&s%i!==0?--n:n&&st(a,function(l){e.node(l).rank+=n})})}function S9(e,t,r,n){var i={width:0,height:0};return arguments.length>=4&&(i.rank=r,i.order=n),fu(e,"border",i,t)}function E9(e){return co(mn(e.nodes(),function(t){var r=e.node(t).rank;if(!Fn(r))return r}))}function Jme(e,t){var r={lhs:[],rhs:[]};return st(e,function(n){t(n)?r.lhs.push(n):r.rhs.push(n)}),r}function ege(e,t){var r=Y3();try{return t()}finally{console.log(e+" time: "+(Y3()-r)+"ms")}}function tge(e,t){return t()}var pu=F(()=>{"use strict";Ln();qo();o(fu,"addDummyNode");o(Kme,"simplify");o(n5,"asNonCompoundGraph");o(k9,"intersectRect");o(Ef,"buildLayerMatrix");o(Zme,"normalizeRanks");o(Qme,"removeEmptyRanks");o(S9,"addBorderNode");o(E9,"maxRank");o(Jme,"partition");o(ege,"time");o(tge,"notime")});function nge(e){function t(r){var n=e.children(r),i=e.node(r);if(n.length&&st(n,t),Object.prototype.hasOwnProperty.call(i,"minRank")){i.borderLeft=[],i.borderRight=[];for(var a=i.minRank,s=i.maxRank+1;a{"use strict";Ln();pu();o(nge,"addBorderSegments");o(rge,"addBorderNode")});function sge(e){var t=e.graph().rankdir.toLowerCase();(t==="lr"||t==="rl")&&lge(e)}function oge(e){var t=e.graph().rankdir.toLowerCase();(t==="bt"||t==="rl")&&Vot(e),(t==="lr"||t==="rl")&&(Wot(e),lge(e))}function lge(e){st(e.nodes(),function(t){age(e.node(t))}),st(e.edges(),function(t){age(e.edge(t))})}function age(e){var t=e.width;e.width=e.height,e.height=t}function Vot(e){st(e.nodes(),function(t){A9(e.node(t))}),st(e.edges(),function(t){var r=e.edge(t);st(r.points,A9),Object.prototype.hasOwnProperty.call(r,"y")&&A9(r)})}function A9(e){e.y=-e.y}function Wot(e){st(e.nodes(),function(t){R9(e.node(t))}),st(e.edges(),function(t){var r=e.edge(t);st(r.points,R9),Object.prototype.hasOwnProperty.call(r,"x")&&R9(r)})}function R9(e){var t=e.x;e.x=e.y,e.y=t}var cge=F(()=>{"use strict";Ln();o(sge,"adjust");o(oge,"undo");o(lge,"swapWidthHeight");o(age,"swapWidthHeightOne");o(Vot,"reverseY");o(A9,"reverseYOne");o(Wot,"swapXY");o(R9,"swapXYOne")});function uge(e){e.graph().dummyChains=[],st(e.edges(),function(t){Hot(e,t)})}function Hot(e,t){var r=t.v,n=e.node(r).rank,i=t.w,a=e.node(i).rank,s=t.name,l=e.edge(t),u=l.labelRank;if(a!==n+1){e.removeEdge(t);var h=void 0,d,f;for(f=0,++n;n{"use strict";Ln();pu();o(uge,"run");o(Hot,"normalizeEdge");o(hge,"undo")});function mT(e){var t={};function r(n){var i=e.node(n);if(Object.prototype.hasOwnProperty.call(t,n))return i.rank;t[n]=!0;var a=Dh(mn(e.outEdges(n),function(s){return r(s.w)-e.edge(s).minlen}));return(a===Number.POSITIVE_INFINITY||a===void 0||a===null)&&(a=0),i.rank=a}o(r,"dfs"),st(e.sources(),r)}function Wm(e,t){return e.node(t.w).rank-e.node(t.v).rank-e.edge(t).minlen}var i5=F(()=>{"use strict";Ln();o(mT,"longestPath");o(Wm,"slack")});function a5(e){var t=new on({directed:!1}),r=e.nodes()[0],n=e.nodeCount();t.setNode(r,{});for(var i,a;Uot(t,e){"use strict";Ln();qo();i5();o(a5,"feasibleTree");o(Uot,"tightTree");o(Yot,"findMinSlackEdge");o(jot,"shiftRanks")});var fge=F(()=>{"use strict"});var D9=F(()=>{"use strict"});var J6r,I9=F(()=>{"use strict";Ln();D9();J6r=oo(1)});var pge=F(()=>{"use strict";I9()});var M9=F(()=>{"use strict"});var mge=F(()=>{"use strict";M9()});var uRr,gge=F(()=>{"use strict";Ln();uRr=oo(1)});function N9(e){var t={},r={},n=[];function i(a){if(Object.prototype.hasOwnProperty.call(r,a))throw new gT;Object.prototype.hasOwnProperty.call(t,a)||(r[a]=!0,t[a]=!0,st(e.predecessors(a),i),delete r[a],n.push(a))}if(o(i,"visit"),st(e.sinks(),i),v9(t)!==e.nodeCount())throw new gT;return n}function gT(){}var P9=F(()=>{"use strict";Ln();N9.CycleException=gT;o(N9,"topsort");o(gT,"CycleException");gT.prototype=new Error});var yge=F(()=>{"use strict";P9()});function s5(e,t,r){Yr(t)||(t=[t]);var n=(e.isDirected()?e.successors:e.neighbors).bind(e),i=[],a={};return st(t,function(s){if(!e.hasNode(s))throw new Error("Graph does not have node: "+s);vge(e,s,r==="post",a,n,i)}),i}function vge(e,t,r,n,i,a){Object.prototype.hasOwnProperty.call(n,t)||(n[t]=!0,r||a.push(t),st(i(t),function(s){vge(e,s,r,n,i,a)}),r&&a.push(t))}var O9=F(()=>{"use strict";Ln();o(s5,"dfs");o(vge,"doDfs")});function B9(e,t){return s5(e,t,"post")}var xge=F(()=>{"use strict";O9();o(B9,"postorder")});function $9(e,t){return s5(e,t,"pre")}var bge=F(()=>{"use strict";O9();o($9,"preorder")});var Tge=F(()=>{"use strict";D9();t5()});var Cge=F(()=>{"use strict";fge();I9();pge();mge();gge();yge();xge();bge();Tge();M9();P9()});function Rf(e){e=Kme(e),mT(e);var t=a5(e);z9(t),F9(t,e);for(var r,n;r=Ege(t);)n=Age(t,e,r),Rge(t,e,r,n)}function F9(e,t){var r=B9(e,e.nodes());r=r.slice(0,r.length-1),st(r,function(n){Jot(e,t,n)})}function Jot(e,t,r){var n=e.node(r),i=n.parent;e.edge(r,i).cutvalue=kge(e,t,r)}function kge(e,t,r){var n=e.node(r),i=n.parent,a=!0,s=t.edge(r,i),l=0;return s||(a=!1,s=t.edge(i,r)),l=s.weight,st(t.nodeEdges(r),function(u){var h=u.v===r,d=h?u.w:u.v;if(d!==i){var f=h===a,p=t.edge(u).weight;if(l+=f?p:-p,tlt(e,r,d)){var m=e.edge(r,d).cutvalue;l+=f?-m:m}}}),l}function z9(e,t){arguments.length<2&&(t=e.nodes()[0]),Sge(e,{},1,t)}function Sge(e,t,r,n,i){var a=r,s=e.node(n);return t[n]=!0,st(e.neighbors(n),function(l){Object.prototype.hasOwnProperty.call(t,l)||(r=Sge(e,t,r,l,n))}),s.low=a,s.lim=r++,i?s.parent=i:delete s.parent,r}function Ege(e){return g1(e.edges(),function(t){return e.edge(t).cutvalue<0})}function Age(e,t,r){var n=r.v,i=r.w;t.hasEdge(n,i)||(n=r.w,i=r.v);var a=e.node(n),s=e.node(i),l=a,u=!1;a.lim>s.lim&&(l=s,u=!0);var h=Is(t.edges(),function(d){return u===wge(e,e.node(d.v),l)&&u!==wge(e,e.node(d.w),l)});return Fm(h,function(d){return Wm(t,d)})}function Rge(e,t,r,n){var i=r.v,a=r.w;e.removeEdge(i,a),e.setEdge(n.v,n.w,{}),z9(e),F9(e,t),elt(e,t)}function elt(e,t){var r=g1(e.nodes(),function(i){return!t.node(i).parent}),n=$9(e,r);n=n.slice(1),st(n,function(i){var a=e.node(i).parent,s=t.edge(i,a),l=!1;s||(s=t.edge(a,i),l=!0),t.node(i).rank=t.node(a).rank+(l?s.minlen:-s.minlen)})}function tlt(e,t,r){return e.hasEdge(t,r)}function wge(e,t,r){return r.low<=t.lim&&t.lim<=r.lim}var _ge=F(()=>{"use strict";Ln();Cge();pu();L9();i5();Rf.initLowLimValues=z9;Rf.initCutValues=F9;Rf.calcCutValue=kge;Rf.leaveEdge=Ege;Rf.enterEdge=Age;Rf.exchangeEdges=Rge;o(Rf,"networkSimplex");o(F9,"initCutValues");o(Jot,"assignCutValue");o(kge,"calcCutValue");o(z9,"initLowLimValues");o(Sge,"dfsAssignLowLim");o(Ege,"leaveEdge");o(Age,"enterEdge");o(Rge,"exchangeEdges");o(elt,"updateRanks");o(tlt,"isTreeEdge");o(wge,"isDescendant")});function G9(e){switch(e.graph().ranker){case"network-simplex":Lge(e);break;case"tight-tree":nlt(e);break;case"longest-path":rlt(e);break;default:Lge(e)}}function nlt(e){mT(e),a5(e)}function Lge(e){Rf(e)}var rlt,V9=F(()=>{"use strict";L9();_ge();i5();o(G9,"rank");rlt=mT;o(nlt,"tightTreeRanker");o(Lge,"networkSimplexRanker")});function Dge(e){var t=fu(e,"root",{},"_root"),r=ilt(e),n=co(Wo(r))-1,i=2*n+1;e.graph().nestingRoot=t,st(e.edges(),function(s){e.edge(s).minlen*=i});var a=alt(e)+1;st(e.children(),function(s){Ige(e,t,i,a,n,r,s)}),e.graph().nodeRankFactor=i}function Ige(e,t,r,n,i,a,s){var l=e.children(s);if(!l.length){s!==t&&e.setEdge(t,s,{weight:0,minlen:r});return}var u=S9(e,"_bt"),h=S9(e,"_bb"),d=e.node(s);e.setParent(u,s),d.borderTop=u,e.setParent(h,s),d.borderBottom=h,st(l,function(f){Ige(e,t,r,n,i,a,f);var p=e.node(f),m=p.borderTop?p.borderTop:f,g=p.borderBottom?p.borderBottom:f,y=p.borderTop?n:2*n,v=m!==g?1:i-a[s]+1;e.setEdge(u,m,{weight:y,minlen:v,nestingEdge:!0}),e.setEdge(g,h,{weight:y,minlen:v,nestingEdge:!0})}),e.parent(s)||e.setEdge(t,u,{weight:0,minlen:i+a[s]})}function ilt(e){var t={};function r(n,i){var a=e.children(n);a&&a.length&&st(a,function(s){r(s,i+1)}),t[n]=i}return o(r,"dfs"),st(e.children(),function(n){r(n,1)}),t}function alt(e){return hu(e.edges(),function(t,r){return t+e.edge(r).weight},0)}function Mge(e){var t=e.graph();e.removeNode(t.nestingRoot),delete t.nestingRoot,st(e.edges(),function(r){var n=e.edge(r);n.nestingEdge&&e.removeEdge(r)})}var Nge=F(()=>{"use strict";Ln();pu();o(Dge,"run");o(Ige,"dfs");o(ilt,"treeDepths");o(alt,"sumWeights");o(Mge,"cleanup")});function Pge(e,t,r){var n={},i;st(r,function(a){for(var s=e.parent(a),l,u;s;){if(l=e.parent(s),l?(u=n[l],n[l]=s):(u=i,i=s),u&&u!==s){t.setEdge(u,s);return}s=l}})}var Oge=F(()=>{"use strict";Ln();o(Pge,"addSubgraphConstraints")});function Bge(e,t,r){var n=olt(e),i=new on({compound:!0}).setGraph({root:n}).setDefaultNodeLabel(function(a){return e.node(a)});return st(e.nodes(),function(a){var s=e.node(a),l=e.parent(a);(s.rank===t||s.minRank<=t&&t<=s.maxRank)&&(i.setNode(a),i.setParent(a,l||n),st(e[r](a),function(u){var h=u.v===a?u.w:u.v,d=i.edge(h,a),f=Fn(d)?0:d.weight;i.setEdge(h,a,{weight:e.edge(u).weight+f})}),Object.prototype.hasOwnProperty.call(s,"minRank")&&i.setNode(a,{borderLeft:s.borderLeft[t],borderRight:s.borderRight[t]}))}),i}function olt(e){for(var t;e.hasNode(t=Gm("_root")););return t}var $ge=F(()=>{"use strict";Ln();qo();o(Bge,"buildLayerGraph");o(olt,"createRootNode")});function Fge(e,t){for(var r=0,n=1;n0;)d%2&&(f+=l[d+1]),d=d-1>>1,l[d]+=h.weight;u+=h.weight*f})),u}var zge=F(()=>{"use strict";Ln();o(Fge,"crossCount");o(llt,"twoLayerCrossCount")});function Gge(e){var t={},r=Is(e.nodes(),function(l){return!e.children(l).length}),n=co(mn(r,function(l){return e.node(l).rank})),i=mn(bl(n+1),function(){return[]});function a(l){if(!dT(t,l)){t[l]=!0;var u=e.node(l);i[u.rank].push(l),st(e.successors(l),a)}}o(a,"dfs");var s=du(r,function(l){return e.node(l).rank});return st(s,a),i}var Vge=F(()=>{"use strict";Ln();o(Gge,"initOrder")});function Wge(e,t){return mn(t,function(r){var n=e.inEdges(r);if(n.length){var i=hu(n,function(a,s){var l=e.edge(s),u=e.node(s.v);return{sum:a.sum+l.weight*u.order,weight:a.weight+l.weight}},{sum:0,weight:0});return{v:r,barycenter:i.sum/i.weight,weight:i.weight}}else return{v:r}})}var qge=F(()=>{"use strict";Ln();o(Wge,"barycenter")});function Hge(e,t){var r={};st(e,function(i,a){var s=r[i.v]={indegree:0,in:[],out:[],vs:[i.v],i:a};Fn(i.barycenter)||(s.barycenter=i.barycenter,s.weight=i.weight)}),st(t.edges(),function(i){var a=r[i.v],s=r[i.w];!Fn(a)&&!Fn(s)&&(s.indegree++,a.out.push(r[i.w]))});var n=Is(r,function(i){return!i.indegree});return clt(n)}function clt(e){var t=[];function r(a){return function(s){s.merged||(Fn(s.barycenter)||Fn(a.barycenter)||s.barycenter>=a.barycenter)&&ult(a,s)}}o(r,"handleIn");function n(a){return function(s){s.in.push(a),--s.indegree===0&&e.push(s)}}for(o(n,"handleOut");e.length;){var i=e.pop();t.push(i),st(i.in.reverse(),r(i)),st(i.out,n(i))}return mn(Is(t,function(a){return!a.merged}),function(a){return zm(a,["vs","i","barycenter","weight"])})}function ult(e,t){var r=0,n=0;e.weight&&(r+=e.barycenter*e.weight,n+=e.weight),t.weight&&(r+=t.barycenter*t.weight,n+=t.weight),e.vs=t.vs.concat(e.vs),e.barycenter=r/n,e.weight=n,e.i=Math.min(t.i,e.i),t.merged=!0}var Uge=F(()=>{"use strict";Ln();o(Hge,"resolveConflicts");o(clt,"doResolveConflicts");o(ult,"mergeEntries")});function jge(e,t){var r=Jme(e,function(d){return Object.prototype.hasOwnProperty.call(d,"barycenter")}),n=r.lhs,i=du(r.rhs,function(d){return-d.i}),a=[],s=0,l=0,u=0;n.sort(hlt(!!t)),u=Yge(a,i,u),st(n,function(d){u+=d.vs.length,a.push(d.vs),s+=d.barycenter*d.weight,l+=d.weight,u=Yge(a,i,u)});var h={vs:xl(a)};return l&&(h.barycenter=s/l,h.weight=l),h}function Yge(e,t,r){for(var n;t.length&&(n=Sf(t)).i<=r;)t.pop(),e.push(n.vs),r++;return r}function hlt(e){return function(t,r){return t.barycenterr.barycenter?1:e?r.i-t.i:t.i-r.i}}var Xge=F(()=>{"use strict";Ln();pu();o(jge,"sort");o(Yge,"consumeUnsortable");o(hlt,"compareWithBias")});function W9(e,t,r,n){var i=e.children(t),a=e.node(t),s=a?a.borderLeft:void 0,l=a?a.borderRight:void 0,u={};s&&(i=Is(i,function(g){return g!==s&&g!==l}));var h=Wge(e,i);st(h,function(g){if(e.children(g.v).length){var y=W9(e,g.v,r,n);u[g.v]=y,Object.prototype.hasOwnProperty.call(y,"barycenter")&&flt(g,y)}});var d=Hge(h,r);dlt(d,u);var f=jge(d,n);if(s&&(f.vs=xl([s,f.vs,l]),e.predecessors(s).length)){var p=e.node(e.predecessors(s)[0]),m=e.node(e.predecessors(l)[0]);Object.prototype.hasOwnProperty.call(f,"barycenter")||(f.barycenter=0,f.weight=0),f.barycenter=(f.barycenter*f.weight+p.order+m.order)/(f.weight+2),f.weight+=2}return f}function dlt(e,t){st(e,function(r){r.vs=xl(r.vs.map(function(n){return t[n]?t[n].vs:n}))})}function flt(e,t){Fn(e.barycenter)?(e.barycenter=t.barycenter,e.weight=t.weight):(e.barycenter=(e.barycenter*e.weight+t.barycenter*t.weight)/(e.weight+t.weight),e.weight+=t.weight)}var Kge=F(()=>{"use strict";Ln();qge();Uge();Xge();o(W9,"sortSubgraph");o(dlt,"expandSubgraphs");o(flt,"mergeBarycenters")});function Jge(e){var t=E9(e),r=Zge(e,bl(1,t+1),"inEdges"),n=Zge(e,bl(t-1,-1,-1),"outEdges"),i=Gge(e);Qge(e,i);for(var a=Number.POSITIVE_INFINITY,s,l=0,u=0;u<4;++l,++u){plt(l%2?r:n,l%4>=2),i=Ef(e);var h=Fge(e,i);h{"use strict";Ln();qo();pu();Oge();$ge();zge();Vge();Kge();o(Jge,"order");o(Zge,"buildLayerGraphs");o(plt,"sweepLayerGraphs");o(Qge,"assignOrder")});function t0e(e){var t=glt(e);st(e.graph().dummyChains,function(r){for(var n=e.node(r),i=n.edgeObj,a=mlt(e,t,i.v,i.w),s=a.path,l=a.lca,u=0,h=s[u],d=!0;r!==i.w;){if(n=e.node(r),d){for(;(h=s[u])!==l&&e.node(h).maxRanks||l>t[u].lim));for(h=u,u=n;(u=e.parent(u))!==h;)a.push(u);return{path:i.concat(a.reverse()),lca:h}}function glt(e){var t={},r=0;function n(i){var a=r;st(e.children(i),n),t[i]={low:a,lim:r++}}return o(n,"dfs"),st(e.children(),n),t}var r0e=F(()=>{"use strict";Ln();o(t0e,"parentDummyChains");o(mlt,"findPath");o(glt,"postorder")});function ylt(e,t){var r={};function n(i,a){var s=0,l=0,u=i.length,h=Sf(a);return st(a,function(d,f){var p=xlt(e,d),m=p?e.node(p).order:u;(p||d===h)&&(st(a.slice(l,f+1),function(g){st(e.predecessors(g),function(y){var v=e.node(y),x=v.order;(xh)&&n0e(r,p,d)})})}o(n,"scan");function i(a,s){var l=-1,u,h=0;return st(s,function(d,f){if(e.node(d).dummy==="border"){var p=e.predecessors(d);p.length&&(u=e.node(p[0]).order,n(s,h,f,l,u),h=f,l=u)}n(s,h,s.length,u,a.length)}),s}return o(i,"visitLayer"),hu(t,i),r}function xlt(e,t){if(e.node(t).dummy)return g1(e.predecessors(t),function(r){return e.node(r).dummy})}function n0e(e,t,r){if(t>r){var n=t;t=r,r=n}Object.prototype.hasOwnProperty.call(e,t)||Object.defineProperty(e,t,{enumerable:!0,configurable:!0,value:{},writable:!0});var i=e[t];Object.defineProperty(i,r,{enumerable:!0,configurable:!0,value:!0,writable:!0})}function blt(e,t,r){if(t>r){var n=t;t=r,r=n}return!!e[t]&&Object.prototype.hasOwnProperty.call(e[t],r)}function Tlt(e,t,r,n){var i={},a={},s={};return st(t,function(l){st(l,function(u,h){i[u]=u,a[u]=u,s[u]=h})}),st(t,function(l){var u=-1;st(l,function(h){var d=n(h);if(d.length){d=du(d,function(y){return s[y]});for(var f=(d.length-1)/2,p=Math.floor(f),m=Math.ceil(f);p<=m;++p){var g=d[p];a[h]===h&&u{"use strict";Ln();qo();pu();o(ylt,"findType1Conflicts");o(vlt,"findType2Conflicts");o(xlt,"findOtherInnerSegmentNode");o(n0e,"addConflict");o(blt,"hasConflict");o(Tlt,"verticalAlignment");o(Clt,"horizontalCompaction");o(wlt,"buildBlockGraph");o(klt,"findSmallestWidthAlignment");o(Slt,"alignCoordinates");o(Elt,"balance");o(i0e,"positionX");o(Alt,"sep");o(Rlt,"width")});function s0e(e){e=n5(e),_lt(e),p9(i0e(e),function(t,r){e.node(r).x=t})}function _lt(e){var t=Ef(e),r=e.graph().ranksep,n=0;st(t,function(i){var a=co(mn(i,function(s){return e.node(s).height}));st(i,function(s){e.node(s).y=n+a/2}),n+=a+r})}var o0e=F(()=>{"use strict";Ln();pu();a0e();o(s0e,"position");o(_lt,"positionY")});function yT(e,t){var r=t&&t.debugTiming?ege:tge;r("layout",()=>{var n=r(" buildLayoutGraph",()=>zlt(e));r(" runLayout",()=>Llt(n,r)),r(" updateInputGraph",()=>Dlt(e,n))})}function Llt(e,t){t(" makeSpaceForEdgeLabels",()=>Glt(e)),t(" removeSelfEdges",()=>Klt(e)),t(" acyclic",()=>Yme(e)),t(" nestingGraph.run",()=>Dge(e)),t(" rank",()=>G9(n5(e))),t(" injectEdgeLabelProxies",()=>Vlt(e)),t(" removeEmptyRanks",()=>Qme(e)),t(" nestingGraph.cleanup",()=>Mge(e)),t(" normalizeRanks",()=>Zme(e)),t(" assignRankMinMax",()=>Wlt(e)),t(" removeEdgeLabelProxies",()=>qlt(e)),t(" normalize.run",()=>uge(e)),t(" parentDummyChains",()=>t0e(e)),t(" addBorderSegments",()=>nge(e)),t(" order",()=>Jge(e)),t(" insertSelfEdges",()=>Zlt(e)),t(" adjustCoordinateSystem",()=>sge(e)),t(" position",()=>s0e(e)),t(" positionSelfEdges",()=>Qlt(e)),t(" removeBorderNodes",()=>Xlt(e)),t(" normalize.undo",()=>hge(e)),t(" fixupEdgeLabelCoords",()=>Ylt(e)),t(" undoCoordinateSystem",()=>oge(e)),t(" translateGraph",()=>Hlt(e)),t(" assignNodeIntersects",()=>Ult(e)),t(" reversePoints",()=>jlt(e)),t(" acyclic.undo",()=>jme(e))}function Dlt(e,t){st(e.nodes(),function(r){var n=e.node(r),i=t.node(r);n&&(n.x=i.x,n.y=i.y,t.children(r).length&&(n.width=i.width,n.height=i.height))}),st(e.edges(),function(r){var n=e.edge(r),i=t.edge(r);n.points=i.points,Object.prototype.hasOwnProperty.call(i,"x")&&(n.x=i.x,n.y=i.y)}),e.graph().width=t.graph().width,e.graph().height=t.graph().height}function zlt(e){var t=new on({multigraph:!0,compound:!0}),r=H9(e.graph());return t.setGraph(v1({},Mlt,q9(r,Ilt),zm(r,Nlt))),st(e.nodes(),function(n){var i=H9(e.node(n));t.setNode(n,o9(q9(i,Plt),Olt)),t.setParent(n,e.parent(n))}),st(e.edges(),function(n){var i=H9(e.edge(n));t.setEdge(n,v1({},$lt,q9(i,Blt),zm(i,Flt)))}),t}function Glt(e){var t=e.graph();t.ranksep/=2,st(e.edges(),function(r){var n=e.edge(r);n.minlen*=2,n.labelpos.toLowerCase()!=="c"&&(t.rankdir==="TB"||t.rankdir==="BT"?n.width+=n.labeloffset:n.height+=n.labeloffset)})}function Vlt(e){st(e.edges(),function(t){var r=e.edge(t);if(r.width&&r.height){var n=e.node(t.v),i=e.node(t.w),a={rank:(i.rank-n.rank)/2+n.rank,e:t};fu(e,"edge-proxy",a,"_ep")}})}function Wlt(e){var t=0;st(e.nodes(),function(r){var n=e.node(r);n.borderTop&&(n.minRank=e.node(n.borderTop).rank,n.maxRank=e.node(n.borderBottom).rank,t=co(t,n.maxRank))}),e.graph().maxRank=t}function qlt(e){st(e.nodes(),function(t){var r=e.node(t);r.dummy==="edge-proxy"&&(e.edge(r.e).labelRank=r.rank,e.removeNode(t))})}function Hlt(e){var t=Number.POSITIVE_INFINITY,r=0,n=Number.POSITIVE_INFINITY,i=0,a=e.graph(),s=a.marginx||0,l=a.marginy||0;function u(h){var d=h.x,f=h.y,p=h.width,m=h.height;t=Math.min(t,d-p/2),r=Math.max(r,d+p/2),n=Math.min(n,f-m/2),i=Math.max(i,f+m/2)}o(u,"getExtremes"),st(e.nodes(),function(h){u(e.node(h))}),st(e.edges(),function(h){var d=e.edge(h);Object.prototype.hasOwnProperty.call(d,"x")&&u(d)}),t-=s,n-=l,st(e.nodes(),function(h){var d=e.node(h);d.x-=t,d.y-=n}),st(e.edges(),function(h){var d=e.edge(h);st(d.points,function(f){f.x-=t,f.y-=n}),Object.prototype.hasOwnProperty.call(d,"x")&&(d.x-=t),Object.prototype.hasOwnProperty.call(d,"y")&&(d.y-=n)}),a.width=r-t+s,a.height=i-n+l}function Ult(e){st(e.edges(),function(t){var r=e.edge(t),n=e.node(t.v),i=e.node(t.w),a,s;r.points?(a=r.points[0],s=r.points[r.points.length-1]):(r.points=[],a=i,s=n),r.points.unshift(k9(n,a)),r.points.push(k9(i,s))})}function Ylt(e){st(e.edges(),function(t){var r=e.edge(t);if(Object.prototype.hasOwnProperty.call(r,"x"))switch((r.labelpos==="l"||r.labelpos==="r")&&(r.width-=r.labeloffset),r.labelpos){case"l":r.x-=r.width/2+r.labeloffset;break;case"r":r.x+=r.width/2+r.labeloffset;break}})}function jlt(e){st(e.edges(),function(t){var r=e.edge(t);r.reversed&&r.points.reverse()})}function Xlt(e){st(e.nodes(),function(t){if(e.children(t).length){var r=e.node(t),n=e.node(r.borderTop),i=e.node(r.borderBottom),a=e.node(Sf(r.borderLeft)),s=e.node(Sf(r.borderRight));r.width=Math.abs(s.x-a.x),r.height=Math.abs(i.y-n.y),r.x=a.x+r.width/2,r.y=n.y+r.height/2}}),st(e.nodes(),function(t){e.node(t).dummy==="border"&&e.removeNode(t)})}function Klt(e){st(e.edges(),function(t){if(t.v===t.w){var r=e.node(t.v);r.selfEdges||(r.selfEdges=[]),r.selfEdges.push({e:t,label:e.edge(t)}),e.removeEdge(t)}})}function Zlt(e){var t=Ef(e);st(t,function(r){var n=0;st(r,function(i,a){var s=e.node(i);s.order=a+n,st(s.selfEdges,function(l){fu(e,"selfedge",{width:l.label.width,height:l.label.height,rank:s.rank,order:a+ ++n,e:l.e,label:l.label},"_se")}),delete s.selfEdges})})}function Qlt(e){st(e.nodes(),function(t){var r=e.node(t);if(r.dummy==="selfedge"){var n=e.node(r.e.v),i=n.x+n.width/2,a=n.y,s=r.x-i,l=n.height/2;e.setEdge(r.e,r.label),e.removeNode(t),r.label.points=[{x:i+2*s/3,y:a-l},{x:i+5*s/6,y:a-l},{x:i+s,y:a},{x:i+5*s/6,y:a+l},{x:i+2*s/3,y:a+l}],r.label.x=r.x,r.label.y=r.y}})}function q9(e,t){return $m(zm(e,t),Number)}function H9(e){var t={};return st(e,function(r,n){t[n.toLowerCase()]=r}),t}var Ilt,Mlt,Nlt,Plt,Olt,Blt,$lt,Flt,l0e=F(()=>{"use strict";Ln();qo();ige();cge();w9();_9();V9();Nge();e0e();r0e();o0e();pu();o(yT,"layout");o(Llt,"runLayout");o(Dlt,"updateInputGraph");Ilt=["nodesep","edgesep","ranksep","marginx","marginy"],Mlt={ranksep:50,edgesep:20,nodesep:50,rankdir:"tb"},Nlt=["acyclicer","ranker","rankdir","align"],Plt=["width","height"],Olt={width:0,height:0},Blt=["minlen","weight","width","height","labeloffset"],$lt={minlen:1,weight:1,width:0,height:0,labeloffset:10,labelpos:"r"},Flt=["labelpos"];o(zlt,"buildLayoutGraph");o(Glt,"makeSpaceForEdgeLabels");o(Vlt,"injectEdgeLabelProxies");o(Wlt,"assignRankMinMax");o(qlt,"removeEdgeLabelProxies");o(Hlt,"translateGraph");o(Ult,"assignNodeIntersects");o(Ylt,"fixupEdgeLabelCoords");o(jlt,"reversePointsForReversedEdges");o(Xlt,"removeBorderNodes");o(Klt,"removeSelfEdges");o(Zlt,"insertSelfEdges");o(Qlt,"positionSelfEdges");o(q9,"selectNumberAttrs");o(H9,"canonicalize")});var U9=F(()=>{"use strict";w9();l0e();_9();V9()});function hc(e){var t={options:{directed:e.isDirected(),multigraph:e.isMultigraph(),compound:e.isCompound()},nodes:Jlt(e),edges:ect(e)};return Fn(e.graph())||(t.value=XO(e.graph())),t}function Jlt(e){return mn(e.nodes(),function(t){var r=e.node(t),n=e.parent(t),i={v:t};return Fn(r)||(i.value=r),Fn(n)||(i.parent=n),i})}function ect(e){return mn(e.edges(),function(t){var r=e.edge(t),n={v:t.v,w:t.w};return Fn(t.name)||(n.name=t.name),Fn(r)||(n.value=r),n})}var Y9=F(()=>{"use strict";Ln();t5();o(hc,"write");o(Jlt,"writeNodes");o(ect,"writeEdges")});var Br,_f,h0e,o5,qm,tct,j9,d0e,rct,Hm,u0e,f0e,p0e,m0e,g0e,y0e,nct,X9=F(()=>{"use strict";vt();qo();Y9();Br=new Map,_f=new Map,h0e=new Map,o5=o(()=>{_f.clear(),h0e.clear(),Br.clear()},"clear"),qm=o((e,t)=>{let r=_f.get(t)||[];return Z.trace("In isDescendant",t," ",e," = ",r.includes(e)),r.includes(e)},"isDescendant"),tct=o((e,t)=>{let r=_f.get(t)||[];return Z.info("Descendants of ",t," is ",r),Z.info("Edge is ",e),e.v===t||e.w===t?!1:r?r.includes(e.v)||qm(e.v,t)||qm(e.w,t)||r.includes(e.w):(Z.debug("Tilt, ",t,",not in descendants"),!1)},"edgeInCluster"),j9=o((e,t,r,n)=>{Z.warn("Copying children of ",e,"root",n,"data",t.node(e),n);let i=t.children(e)||[];e!==n&&i.push(e),Z.warn("Copying (nodes) clusterId",e,"nodes",i),i.forEach(a=>{if(t.children(a).length>0)j9(a,t,r,n);else{let s=t.node(a);Z.info("cp ",a," to ",n," with parent ",e),r.setNode(a,s),n!==t.parent(a)&&(Z.warn("Setting parent",a,t.parent(a)),r.setParent(a,t.parent(a))),e!==n&&a!==e?(Z.debug("Setting parent",a,e),r.setParent(a,e)):(Z.info("In copy ",e,"root",n,"data",t.node(e),n),Z.debug("Not Setting parent for node=",a,"cluster!==rootId",e!==n,"node!==clusterId",a!==e));let l=t.edges(a);Z.debug("Copying Edges",l),l.forEach(u=>{Z.info("Edge",u);let h=t.edge(u.v,u.w,u.name);Z.info("Edge data",h,n);try{if(tct(u,n)){let d=_f.get(n)||[],f=d.includes(u.v)||qm(u.v,n)||u.v===n,p=d.includes(u.w)||qm(u.w,n)||u.w===n;if(f&&p)Z.info("Copying as ",u.v,u.w,h,u.name),r.setEdge(u.v,u.w,h,u.name),Z.info("newGraph edges ",r.edges(),r.edge(r.edges()[0]));else{let m=f?n:u.v,g=p?n:u.w;Z.info("Rebinding cross-boundary edge as ",m,g,h,u.name),t.setEdge(m,g,h,u.name)}}else Z.info("Skipping copy of edge ",u.v,"-->",u.w," rootId: ",n," clusterId:",e)}catch(d){Z.error(d)}})}Z.debug("Removing node",a),t.removeNode(a)})},"copy"),d0e=o((e,t)=>{let r=t.children(e),n=[...r];for(let i of r)h0e.set(i,e),n=[...n,...d0e(i,t)];return n},"extractDescendants"),rct=o((e,t,r)=>{let n=e.edges().filter(u=>u.v===t||u.w===t),i=e.edges().filter(u=>u.v===r||u.w===r),a=n.map(u=>({v:u.v===t?r:u.v,w:u.w===t?t:u.w})),s=i.map(u=>({v:u.v,w:u.w}));return a.filter(u=>s.some(h=>u.v===h.v&&u.w===h.w))},"findCommonEdges"),Hm=o((e,t,r)=>{let n=t.children(e);if(Z.trace("Searching children of id ",e,n),n.length<1)return e;let i;for(let a of n){let s=Hm(a,t,r),l=rct(t,r,s);if(s)if(l.length>0)i=s;else return s}return i},"findNonClusterChild"),u0e=o(e=>!Br.has(e)||!Br.get(e).externalConnections?e:Br.has(e)?Br.get(e).id:e,"getAnchorId"),f0e=o((e,t)=>{if(!e||t>10){Z.debug("Opting out, no graph ");return}else Z.debug("Opting in, graph ");e.nodes().forEach(function(r){e.children(r).length>0&&(Z.warn("Cluster identified",r," Replacement id in edges: ",Hm(r,e,r)),_f.set(r,d0e(r,e)),Br.set(r,{id:Hm(r,e,r),clusterData:e.node(r)}))}),e.nodes().forEach(function(r){let n=e.children(r),i=e.edges();n.length>0?(Z.debug("Cluster identified",r,_f),i.forEach(a=>{let s=qm(a.v,r),l=qm(a.w,r);s^l&&(Z.warn("Edge: ",a," leaves cluster ",r),Z.warn("Descendants of XXX ",r,": ",_f.get(r)),Br.get(r).externalConnections=!0)})):Z.debug("Not a cluster ",r,_f)});for(let r of Br.keys()){let n=Br.get(r).id,i=e.parent(n);i!==r&&Br.has(i)&&!Br.get(i).externalConnections&&(Br.get(r).id=i);let a=e.edges().some(s=>s.v===r);if(n&&Br.get(r)?.externalConnections&&a&&y0e(e,n,r)){let s=nct(e,r,e.parent(n));s&&(Br.get(r).id=s)}}e.edges().forEach(function(r){let n=e.edge(r);Z.warn("Edge "+r.v+" -> "+r.w+": "+JSON.stringify(r)),Z.warn("Edge "+r.v+" -> "+r.w+": "+JSON.stringify(e.edge(r)));let i=r.v,a=r.w;if(Z.warn("Fix XXX",Br,"ids:",r.v,r.w,"Translating: ",Br.get(r.v)," --- ",Br.get(r.w)),Br.get(r.v)||Br.get(r.w)){if(Z.warn("Fixing and trying - removing XXX",r.v,r.w,r.name),i=u0e(r.v),a=u0e(r.w),e.removeEdge(r.v,r.w,r.name),i!==r.v){let s=e.parent(i);Br.get(s).externalConnections=!0,n.fromCluster=r.v}if(a!==r.w){let s=e.parent(a);Br.get(s).externalConnections=!0,n.toCluster=r.w}Z.warn("Fix Replacing with XXX",i,a,r.name),e.setEdge(i,a,n,r.name)}}),Z.warn("Adjusted Graph",hc(e)),p0e(e,0),Z.trace(Br)},"adjustClustersAndEdges"),p0e=o((e,t)=>{if(Z.warn("extractor - ",t,hc(e),e.children("D")),t>10){Z.error("Bailing out");return}let r=e.nodes(),n=!1;for(let i of r){let a=e.children(i);n=n||a.length>0}if(!n){Z.debug("Done, no node has children",e.nodes());return}Z.debug("Nodes = ",r,t);for(let i of r)if(Z.debug("Extracting node",i,Br,Br.has(i)&&!Br.get(i).externalConnections,!e.parent(i),e.node(i),e.children("D")," Depth ",t),!Br.has(i))Z.debug("Not a cluster",i,t);else if(Br.get(i)?.clusterData?.explicitDir&&e.children(i)&&e.children(i).length>0){Z.warn("Cluster with explicit dir, creating subgraph for children",i,t);let a=Br.get(i).clusterData.dir,s=new on({multigraph:!0,compound:!0}).setGraph({rankdir:a,nodesep:50,ranksep:50,marginx:8,marginy:8}).setDefaultEdgeLabel(function(){return{}});j9(i,e,s,i);let l=e.node(i)||{};e.setNode(i,{...l,clusterNode:!0,id:i,clusterData:Br.get(i).clusterData,label:Br.get(i).label,graph:s}),Z.warn("Subgraph for cluster with explicit dir created:",i,hc(s))}else if(!Br.get(i).externalConnections&&e.children(i)&&e.children(i).length>0){Z.warn("Cluster without external connections, without a parent and with children",i,t);let s=e.graph().rankdir==="TB"?"LR":"TB";Br.get(i)?.clusterData?.dir&&(s=Br.get(i).clusterData.dir,Z.warn("Fixing dir",Br.get(i).clusterData.dir,s));let l=new on({multigraph:!0,compound:!0}).setGraph({rankdir:s,nodesep:50,ranksep:50,marginx:8,marginy:8}).setDefaultEdgeLabel(function(){return{}});j9(i,e,l,i);let u=e.node(i)||{};e.setNode(i,{...u,clusterNode:!0,id:i,clusterData:Br.get(i).clusterData,label:Br.get(i).label,graph:l}),Z.debug("Old graph after copy",hc(e))}else Z.warn("Cluster ** ",i," **not meeting the criteria !externalConnections:",!Br.get(i).externalConnections," no parent: ",!e.parent(i)," children ",e.children(i)&&e.children(i).length>0,e.children("D"),t),Z.debug(Br);r=e.nodes(),Z.warn("New list of nodes",r);for(let i of r){let a=e.node(i);Z.warn(" Now next level",i,a),a?.clusterNode&&p0e(a.graph,t+1)}},"extractor"),m0e=o((e,t)=>{if(t.length===0)return[];let r=Object.assign([],t);return t.forEach(n=>{let i=e.children(n),a=m0e(e,i);r=[...r,...a]}),r},"sorter"),g0e=o(e=>m0e(e,e.children()),"sortNodesByHierarchy"),y0e=o((e,t,r)=>{let n=e.parent(t);for(;n&&n!==r;){let i=Br.get(n);if(i&&!i.externalConnections)return!0;n=e.parent(n)}return!1},"isNodeInExtractableCluster"),nct=o((e,t,r)=>{let n=e.children(t)??[];for(let i of n){if(i===r||qm(i,r))continue;let a=Hm(i,e,t);if(a&&!y0e(e,a,t))return a}return null},"findSafeAnchorNode")});var C0e={};ir(C0e,{getEdgesToRender:()=>b0e,render:()=>lct});var v0e,x0e,ict,act,sct,oct,b0e,T0e,lct,w0e=F(()=>{"use strict";U9();Y9();qo();Z4();Kt();X9();Dm();Wy();V2();vt();Vy();Xt();v0e=o((e,t,r)=>Math.max(t,Math.min(r,e)),"clamp"),x0e=o((e="TB")=>{switch(e){case"BT":return"bottom";case"LR":return"right";case"RL":return"left";case"TB":default:return"top"}},"getDefaultSelfLoopSide"),ict=o(e=>e==="flowchart"||e==="flowchart-v2"||e==="stateDiagram","shouldMergeSelfLoopSegments"),act=o((e,t,r,n,i)=>{let a=[],s=new Set;if(r.forEach(({start:d,end:f})=>{d!==n&&s.add(d),f!==n&&s.add(f)}),s.forEach(d=>{let f=e.node(d);typeof f?.x=="number"&&typeof f?.y=="number"&&a.push(f)}),a.length===0&&r.forEach(({edge:d})=>{(d.points??[]).forEach(f=>{typeof f?.x=="number"&&typeof f?.y=="number"&&a.push(f)})}),a.length===0)return x0e(i);let l=a.reduce((d,f)=>({x:d.x+f.x/a.length,y:d.y+f.y/a.length}),{x:0,y:0}),u=l.x-t.x,h=l.y-t.y;return Math.abs(u)>Math.abs(h)?u>0?"right":"left":Math.abs(h)>0?h>0?"bottom":"top":x0e(i)},"getSelfLoopSide"),sct=o((e,t="top",r=0,n=0)=>{let i=e.x,a=e.y-r,s=e.width/2,l=e.height/2,u=Math.max(36,Math.min(100,e.width*.8)),h=v0e(Math.max(n,e.width*.35),36,u),d=v0e(Math.min(e.width,e.height)*.45,24,48);switch(t){case"bottom":{let f=a+l;return[{x:i-h/2,y:f},{x:i-h/2,y:f+d},{x:i+h/2,y:f+d},{x:i+h/2,y:f}]}case"right":{let f=i+s;return[{x:f,y:a-h/2},{x:f+d,y:a-h/2},{x:f+d,y:a+h/2},{x:f,y:a+h/2}]}case"left":{let f=i-s;return[{x:f,y:a-h/2},{x:f-d,y:a-h/2},{x:f-d,y:a+h/2},{x:f,y:a+h/2}]}case"top":default:{let f=a-l;return[{x:i-h/2,y:f},{x:i-h/2,y:f-d},{x:i+h/2,y:f-d},{x:i+h/2,y:f}]}}},"getSelfLoopPoints"),oct=o((e,t,r="top",n=0,i={})=>{let s=e.x,l=e.y-n,u=i.width??0,h=i.height??0;switch(r){case"bottom":return{x:s,y:Math.max(...t.map(d=>d.y))+h/2+4};case"right":return{x:Math.max(...t.map(d=>d.x))+u/2+4,y:l};case"left":return{x:Math.min(...t.map(d=>d.x))-u/2-4,y:l};case"top":default:return{x:s,y:Math.min(...t.map(d=>d.y))-h/2-4}}},"getSelfLoopLabelPosition"),b0e=o((e,t=0,{mergeSelfLoops:r=!0}={})=>{let n=new Map,i=[],a=e.graph()?.rankdir;return e.edges().forEach(s=>{let l=e.edge(s);if(r&&l.selfLoop){let u=l.selfLoop.id;n.has(u)||n.set(u,[]),n.get(u).push({edge:l,start:s.v,end:s.w})}else i.push({edge:l,start:s.v,end:s.w})}),n.forEach(s=>{if(s.length!==3){s.forEach(x=>i.push(x));return}s.sort((x,b)=>x.edge.selfLoop.order-b.edge.selfLoop.order);let[l,u,h]=s,d=l.edge.originalEdge??u.edge.originalEdge??h.edge.originalEdge??u.edge,f=e.node(d.start);if(!f){s.forEach(x=>i.push(x));return}let p={width:u.edge.width,height:u.edge.height},m=act(e,f,s,d.start,a),g=sct(f,m,t,p.width??0),y=oct(f,g,m,t,p),v={...u.edge,...d,id:d.id,points:g,start:d.start,end:d.end,x:y.x,y:y.y,width:p.width,height:p.height,labelStyle:u.edge.labelStyle,fromCluster:l.edge.fromCluster??u.edge.fromCluster??h.edge.fromCluster,toCluster:l.edge.toCluster??u.edge.toCluster??h.edge.toCluster};delete v.selfLoop,delete v.originalEdge,i.push({edge:v,start:v.start,end:v.end})}),i},"getEdgesToRender"),T0e=o(async(e,t,r,n,i,a)=>{Z.warn("Graph in recursive render:XAX",hc(t),i);let s=t.graph().rankdir;Z.trace("Dir in recursive render - dir:",s);let l=e.insert("g").attr("class","root");t.nodes()?Z.info("Recursive render XXX",t.nodes()):Z.info("No nodes found for",t),t.edges().length>0&&Z.info("Recursive edges",t.edge(t.edges()[0]));let u=l.insert("g").attr("class","clusters"),h=l.insert("g").attr("class","edgePaths"),d=l.insert("g").attr("class","edgeLabels"),f=l.insert("g").attr("class","nodes"),p=ict(r);await Promise.all(t.nodes().map(async function(b){let T=t.node(b);if(i!==void 0){let k=JSON.parse(JSON.stringify(i.clusterData));Z.trace(`Setting data for parent cluster XXX + Node.id = `,b,` + data=`,k.height,` +Parent cluster`,i.height),t.setNode(i.id,k),t.parent(b)||(Z.trace("Setting parent",b,i.id),t.setParent(b,i.id,k))}if(Z.info("(Insert) Node XXX"+b+": "+JSON.stringify(t.node(b))),T?.clusterNode){Z.info("Cluster identified XBX",b,T.width,t.node(b));let{ranksep:k,nodesep:C}=t.graph();T.graph.setGraph({...T.graph.graph(),ranksep:k+25,nodesep:C});let w=await T0e(f,T.graph,r,n,t.node(b),a),S=w.elem;pt(T,S),T.diff=w.diff||0,Z.info("New compound node after recursive render XAX",b,"width",T.width,"height",T.height),Zce(S,T)}else t.children(b).length>0?(Z.trace("Cluster - the non recursive path XBX",b,T.id,T,T.width,"Graph:",t),Z.trace(Hm(T.id,t)),Br.set(T.id,{id:Hm(T.id,t),node:T})):(Z.trace("Node - the non recursive path XAX",b,f,t.node(b),s),await af(f,t.node(b),{config:a,dir:s}))})),await o(async()=>{let b=t.edges().map(async function(T){let k=t.edge(T.v,T.w,T.name);if(Z.info("Edge "+T.v+" -> "+T.w+": "+JSON.stringify(T)),Z.info("Edge "+T.v+" -> "+T.w+": ",T," ",JSON.stringify(t.edge(T))),Z.info("Fix",Br,"ids:",T.v,T.w,"Translating: ",Br.get(T.v),Br.get(T.w)),p&&k.selfLoop){if(k.selfLoop.order!==1)return;let C=k.id;k.id=k.selfLoop.id,await _m(d,k),k.id=C;return}await _m(d,k)});await Promise.all(b)},"processEdges")(),Z.info("Graph before layout:",JSON.stringify(hc(t))),Z.info("############################################# XXX"),Z.info("### Layout ### XXX"),Z.info("############################################# XXX"),yT(t),Z.info("Graph after layout:",JSON.stringify(hc(t)));let g=0,{subGraphTitleTotalMargin:y}=oc(a);await Promise.all(g0e(t).map(async function(b){let T=t.node(b);if(Z.info("Position XBX => "+b+": ("+T.x,","+T.y,") width: ",T.width," height: ",T.height),T?.clusterNode)T.y+=y,Z.info("A tainted cluster node XBX1",b,T.id,T.width,T.height,T.x,T.y,t.parent(b)),Br.get(T.id).node=T,Lm(T);else if(t.children(b).length>0){Z.info("A pure cluster node XBX1",b,T.id,T.x,T.y,T.width,T.height,t.parent(b)),T.height+=y,t.node(T.parentId);let k=T?.padding/2||0,C=T?.labelBBox?.height||0,w=C-k||0;Z.debug("OffsetY",w,"labelHeight",C,"halfPadding",k),await nf(u,T),Br.get(T.id).node=T}else{let k=t.node(T.parentId);T.y+=y/2,Z.info("A regular node XBX1 - using the padding",T.id,"parent",T.parentId,T.width,T.height,T.x,T.y,"offsetY",T.offsetY,"parent",k,k?.offsetY,T),Lm(T)}}));let v=y/2;return b0e(t,v,{mergeSelfLoops:p}).forEach(function({edge:b,start:T,end:k}){Z.info("Edge "+T+" -> "+k+": "+JSON.stringify(b),b),b.points.forEach(R=>R.y+=v);let C=t.node(T),w=t.node(k),S=Hy(h,b,Br,r,C,w,n);K4(b,S)}),t.nodes().forEach(function(b){let T=t.node(b);Z.info(b,T.type,T.diff),T.isGroup&&(g=T.diff)}),Z.warn("Returning from recursive render XAX",l,g),{elem:l,diff:g}},"recursiveRender"),lct=o(async(e,t)=>{let r=new on({multigraph:!0,compound:!0}).setGraph({rankdir:e.direction,nodesep:e.config?.nodeSpacing||e.config?.flowchart?.nodeSpacing||e.nodeSpacing,ranksep:e.config?.rankSpacing||e.config?.flowchart?.rankSpacing||e.rankSpacing,marginx:8,marginy:8}).setDefaultEdgeLabel(function(){return{}}),n=t.select("g");Uy(n,e.markers,e.type,e.diagramId),J4(),X4(),q4(),o5(),e.nodes.forEach(a=>{r.setNode(a.id,{...a}),a.parentId&&r.setParent(a.id,a.parentId)}),Z.debug("Edges:",e.edges),e.edges.forEach(a=>{if(a.start===a.end){let s=a.start,l=s+"---"+s+"---1",u=s+"---"+s+"---2",h=r.node(s);r.setNode(l,{domId:l,id:l,parentId:h.parentId,labelStyle:"",label:"",padding:0,shape:"labelRect",style:"",width:10,height:10}),r.setParent(l,h.parentId),r.setNode(u,{domId:u,id:u,parentId:h.parentId,labelStyle:"",padding:0,shape:"labelRect",label:"",style:"",width:10,height:10}),r.setParent(u,h.parentId);let d=structuredClone(a),f=structuredClone(a),p=structuredClone(a),m=structuredClone(a);f.originalEdge=d,f.selfLoop={id:d.id,order:0},p.originalEdge=d,p.selfLoop={id:d.id,order:1},m.originalEdge=d,m.selfLoop={id:d.id,order:2},f.label="",f.arrowTypeEnd="none",f.endLabelLeft="",f.endLabelRight="",f.startLabelLeft="",f.id=s+"-cyclic-special-1",p.startLabelRight="",p.startLabelLeft="",p.endLabelLeft="",p.endLabelRight="",p.arrowTypeStart="none",p.arrowTypeEnd="none",p.id=s+"-cyclic-special-mid",m.label="",m.startLabelRight="",m.startLabelLeft="",m.arrowTypeStart="none",h.isGroup&&(f.fromCluster=s,m.toCluster=s),m.id=s+"-cyclic-special-2",m.arrowTypeStart="none",r.setEdge(s,l,f,s+"-cyclic-special-0"),r.setEdge(l,u,p,s+"-cyclic-special-1"),r.setEdge(u,s,m,s+"-cyclic-special-2")}else r.setEdge(a.start,a.end,{...a},a.id)}),Z.warn("Graph at first:",JSON.stringify(hc(r))),f0e(r),Z.warn("Graph after XAX:",JSON.stringify(hc(r)));let i=Ae();await T0e(n,r,e.type,e.diagramId,void 0,i)},"render")});var k0e=F(()=>{"use strict"});var E0e={};ir(E0e,{captureNodeSizes:()=>fct,shouldCaptureSizes:()=>uct});function S0e(){if(!(typeof globalThis>"u"))return globalThis}function uct(){return!!S0e()?.mermaidCaptureSizes}function hct(){return typeof location>"u"?"browser-dev":`${location.pathname}${location.search}`}function dct(e,t){let r=S0e();if(!r)return;let n=t.node(),a=((n&&"ownerSVGElement"in n?n.ownerSVGElement:null)??n)?.id??"(unknown)";r.mermaidCapturedSizes??=[];let s={svgId:a,sizes:e};r.mermaidCapturedSizes.push(s),r.mermaidLastCapturedSizes=s}function fct(e,t){let r=[];for(let n of t.nodes)n.isGroup||r.push({id:n.id,width:n.width??0,height:n.height??0});r.length!==0&&dct({metadata:{captureVersion:1,capturedAt:new Date().toISOString(),capturedFrom:hct()},nodes:r},e)}var A0e=F(()=>{"use strict";k0e();o(S0e,"getCaptureGlobal");o(uct,"shouldCaptureSizes");o(hct,"capturedFromLocation");o(dct,"emitCapturedSizes");o(fct,"captureNodeSizes")});async function R0e(e,t){let r=new on({multigraph:!0,compound:!0}),n=[...t.edges],i=Ae(),a=e.insert("g").attr("class","root"),s=a.insert("g").attr("class","clusters"),l=a.insert("g").attr("class","edges edgePath"),u=a.insert("g").attr("class","edgeLabels"),h=a.insert("g").attr("class","nodes"),d=new Map,f=e.node()!=null;await Promise.all(t.nodes.map(async p=>{if(p.isGroup)r.setNode(p.id,{...p});else{if(f){let m=await af(h,p,{config:i,dir:p.dir}),g=m.node()?.getBBox()??{width:0,height:0};d.set(p.id,m),p.width=g.width,p.height=g.height}r.setNode(p.id,{...p})}}));for(let p of n)r.setEdge(p.start,p.end,{...p},p.id),t.edges.some(g=>g.id===p.id)||t.edges.push(p);if(globalThis.mermaidCaptureSizes){let{captureNodeSizes:p}=await Promise.resolve().then(()=>(A0e(),E0e));p(e,t)}return{graph:r,groups:{clusters:s,edgePaths:l,edgeLabels:u,nodes:h,rootGroups:a},nodeElements:d}}var _0e=F(()=>{"use strict";qo();Xt();Dm();o(R0e,"createGraphWithElements")});function K9(e){let t=[];for(let r=0;r=1-c5||p<=c5||p>=1-c5?null:{point:{x:e.x+f*i,y:e.y+f*a},tA:f,tB:p}}function D0e(e){return Math.abs(e.b.x-e.a.x)>=Math.abs(e.b.y-e.a.y)}function mct(e){let t=[];for(let r=0;r=Math.abs(r)?t>=0?1:0:r>=0?1:0}function vct(e,t){if(e.length<2)return e.map(a=>({...a}));let r=e.map(a=>({...a})),n=t.arrowTypeStart&&Yi[t.arrowTypeStart];if(n){let a=e[0],s=e[1],l=Math.atan2(s.y-a.y,s.x-a.x);r[0].x=a.x+n*Math.cos(l),r[0].y=a.y+n*Math.sin(l)}let i=t.arrowTypeEnd&&Yi[t.arrowTypeEnd];if(i){let a=e.length,s=e[a-2],l=e[a-1],u=Math.atan2(l.y-s.y,l.x-s.x);r[a-1].x=l.x-i*Math.cos(u),r[a-1].y=l.y-i*Math.sin(u)}return r}function xct(e,t,r,n,i){let a=e.point.x,s=e.point.y,l={x:a-t*e.r,y:s-r*e.r},u={x:a+t*e.r,y:s+r*e.r},h=[`L${vT(l)}`];return i==="arc"?h.push(`A${mu(e.r)},${mu(e.r)} 0 0 ${n} ${vT(u)}`):h.push(`M${vT(u)}`),h}function I0e(e,t,r,n){let i=t.x-e.x,a=t.y-e.y,s=r.x-t.x,l=r.y-t.y,u=Math.hypot(i,a),h=Math.hypot(s,l);if(u0){let T=I0e(i[h-1],i[h],i[h+1]??i[h],L0e);T&&(y=T.cutLen)}let v=f,x=null;a&&hT.t-k.t);for(let T of b)T.r=Math.min(T.r,T.d-y,v-T.d);for(let T=0;Tk){let C=k/2;b[T].r=Math.min(b[T].r,C),b[T+1].r=Math.min(b[T+1].r,C)}}for(let T of b)T.r=2?n:null}catch{return null}}function M0e(e,t,r){if(!r.enabled)return;let n=e.node();if(!n)return;let i=new Map;for(let h of t)i.set(h.id,h);let a=[],s=new Map;for(let h of t){let d=typeof CSS<"u"&&CSS.escape?CSS.escape(h.id):h.id,f=n.querySelector(`path[data-id="${d}"]`);if(!f)continue;s.set(h.id,f);let m=wct(f.getAttribute("data-points"))??h.points;a.push({...h,points:m})}let l=mct(a);if(l.length===0)return;let u=new Map;for(let h of l){let d=u.get(h.jumpEdgeId)??[];d.push(h),u.set(h.jumpEdgeId,d)}for(let h of a){let d=u.get(h.id);if(!d||d.length===0)continue;let p=i.get(h.id)?.curve;if(p!==void 0&&!Cct(p))continue;let m=s.get(h.id);if(!m)continue;if(p===void 0){let T=m.getAttribute("d")??"";if(!Tct(T))continue}let g=m.getAttribute("style")??"",y=/stroke-dasharray\s*:\s*0\s+([\d.]+)\s+[\d.]+\s+([\d.]+)/.exec(g),v=y?Number.parseFloat(y[1]):null,x=y?Number.parseFloat(y[2]):null,b=bct(h,d,r);if(m.setAttribute("d",b),v!==null&&x!==null&&typeof m.getTotalLength=="function"){let T=m.getTotalLength(),k=Math.max(0,T-v-x),C=`0 ${v} ${k} ${x}`,w=g.replace(/stroke-dasharray\s*:[^;]*;?/g,`stroke-dasharray: ${C};`).replace(/;\s*;+/g,";");m.setAttribute("style",w)}}}var L0e,l5,c5,yct,N0e=F(()=>{"use strict";Y4();L0e=5,l5=1e-5,c5=1e-6;o(K9,"buildSegmentList");o(pct,"segmentIntersection");o(D0e,"isHorizontalSeg");o(mct,"findEdgeIntersections");o(mu,"fmt");o(vT,"pointToString");o(gct,"getArcSweepFlag");yct=.001;o(vct,"applyMarkerOffsets");o(xct,"emitJump");o(I0e,"computeRoundedCorner");o(bct,"rewriteEdgePath");o(Tct,"isStraightPath");o(Cct,"curveSupportsLineHops");o(wct,"decodeDataPoints");o(M0e,"applyLineJumpsToSvg")});async function P0e(e,t){for(let i of e.nodes)i.isGroup?await nf(t.clusters,i):Lm(i);let r=new Map;for(let i of e.nodes)i?.id&&r.set(i.id,i);for(let i of e.edges){let a=i.start?r.get(i.start)??{}:{},s=i.end?r.get(i.end)??{}:{},l=Hy(t.edgePaths,{...i},{},e.type,a,s,e.diagramId);i.label&&await _m(t.rootGroups,i),i.label&&kct(i,l)}let n=e.config?.swimlane?.lineHops;if(n!==!1){let i=n==="gap"?"gap":"arc",a=e.edges.filter(s=>Array.isArray(s.points)&&s.points.length>=2).map(s=>({id:s.id,points:s.points,curve:s.curve,arrowTypeStart:s.arrowTypeStart,arrowTypeEnd:s.arrowTypeEnd}));M0e(t.edgePaths,a,{enabled:!0,jumpRadius:6,jumpStyle:i})}}function kct(e,t){let r=t?.updatedPath??t?.originalPath,n=_t(),{subGraphTitleTotalMargin:i}=oc({flowchart:n.flowchart??{}});if(e.label){let a=qy.get(e.id),s=e.x,l=e.y;if(r){let u=Zt.calcLabelPosition(r);Z.debug("Moving label "+e.label+" from (",s,",",l,") to (",u.x,",",u.y,") abc88"),t&&(s=u.x,l=u.y)}a.attr("transform",`translate(${s}, ${l+i/2})`)}if(e?.startLabelLeft){let a=_i.get(e.id).startLeft,s=e?.x,l=e?.y;if(r){let u=Zt.calcTerminalLabelPosition(e.arrowTypeStart?10:0,"start_left",r);s=u.x,l=u.y}a.attr("transform",`translate(${s}, ${l})`)}if(e.startLabelRight){let a=_i.get(e.id).startRight,s=e.x,l=e.y;if(r){let u=Zt.calcTerminalLabelPosition(e.arrowTypeStart?10:0,"start_right",r);s=u.x,l=u.y}a.attr("transform",`translate(${s}, ${l})`)}if(e.endLabelLeft){let a=_i.get(e.id).endLeft,s=e.x,l=e.y;if(r){let u=Zt.calcTerminalLabelPosition(e.arrowTypeEnd?10:0,"end_left",r);s=u.x,l=u.y}a.attr("transform",`translate(${s}, ${l})`)}if(e.endLabelRight){let a=_i.get(e.id).endRight,s=e.x,l=e.y;if(r){let u=Zt.calcTerminalLabelPosition(e.arrowTypeEnd?10:0,"end_right",r);s=u.x,l=u.y}a.attr("transform",`translate(${s}, ${l})`)}}var O0e=F(()=>{"use strict";Dm();Wy();V2();N0e();vt();Vy();ur();Qt();o(P0e,"adjustLayout");o(kct,"positionEdgeLabel")});function B0e(e){return Math.max(e.padding??20,20)}function Sct(e){let{x:t,y:r,width:n,height:i}=e,a=e.swimlaneContentTop;if(typeof t!="number"||typeof r!="number"||typeof n!="number"||typeof i!="number"||typeof a!="number"||!Number.isFinite(t)||!Number.isFinite(r)||!Number.isFinite(n)||!Number.isFinite(i)||!Number.isFinite(a)||n<=0||i<=0){delete e.groupTitleRect;return}let s=r-i/2,l=Math.min(a,r+i/2),u=Math.min(21,Math.max(0,l-s)),h=s+u;if(h<=s){delete e.groupTitleRect;return}e.groupTitleRect={left:t-n/2,right:t+n/2,top:s,bottom:h}}function $0e(e){let t=e.direction,r=e.nodes??=[];for(let a of e.nodes??[])a.isGroup&&!a.parentId&&(a.shape="swimlane",t&&(a.direction=t));let n=r.filter(a=>!a.isGroup&&!a.parentId);if(n.length===0)return;let i=r.find(a=>a.id===Z9);i?i.isGroup&&(i.shape="swimlane",t&&(i.direction=t)):(i={id:Z9,label:"",isGroup:!0,shape:"swimlane",padding:20,...t?{direction:t}:{}},r.push(i));for(let a of n)a.parentId=Z9}function F0e(e){let t=new Map;for(let u of e.nodes??[])t.set(u.id,u);let r=[];for(let u of e.edges??[]){let h=typeof u.start=="string"?u.start:void 0,d=typeof u.end=="string"?u.end:void 0;!h||!d||u.labelNodeId||r.push({id:u.id,src:h,dst:d,ref:u})}let n=e.nodes??[],i=n.filter(u=>u.isGroup),a=n.filter(u=>!u.isGroup);return{nodes:[...[...i].reverse(),...a].map(u=>u.id),edges:r,layout:e,nodeById:t}}function z0e(e,t,r,n){let{layout:i}=e,a=e.nodeById,s=n?.layerGap??100,l=n?.nodeGap??40,u=0;for(let p of t.layers){let m=0;for(let g of p){let y=a.get(g);if(!y){m++;continue}y.layer=u,y.order=m;let v=r.x[g]??m*l,x=r.y[g]??u*s;y.x=v,y.y=x,m++}u++}let h=i.nodes??[],d=new Map,f=[];for(let p of h){if(!p?.isGroup)continue;p.parentId||f.push(p);let m=h.filter(b=>b.parentId===p.id),g=1/0,y=-1/0,v=1/0,x=-1/0;for(let b of m){let T=b.x??r.x[b.id],k=b.y??r.y[b.id],C=b.width??0,w=b.height??0;T!=null&&k!=null&&(g=Math.min(g,T-C/2),y=Math.max(y,T+C/2),v=Math.min(v,k-w/2),x=Math.max(x,k+w/2))}if(g===1/0||v===1/0)p.x=p.x??0,p.y=p.y??0,p.width=p.width??0,p.height=p.height??0;else{let b=p.padding??20,T=p.parentId?b:2*B0e(p),k=b,C=Math.max(0,y-g)+T,w=Math.max(0,x-v)+k,S=(g+y)/2,R=(v+x)/2;p.x=S,p.y=R,p.width=C,p.height=w,d.set(p.id,{minX:g,maxX:y,minY:v,maxY:x})}}if(f.length>0&&d.size>0){let p=1/0,m=-1/0,g=0;for(let y of f){let v=y.padding??20;v>g&&(g=v);let x=d.get(y.id);x&&(p=Math.min(p,x.minY),m=Math.max(m,x.maxY))}if(p!==1/0&&m!==-1/0){let y=Math.max(0,m-p),x=Math.max(g,36),b=y+2*x,T=(p+m)/2;for(let L of f)L.y=T,L.height=b,L.swimlaneContentTop=p;let k=[...f].sort((L,N)=>{let I=L.x??0,_=N.x??0;return I-_}),C=[],w=[],S=[];for(let L of k){let N=d.get(L.id);if(!N)continue;let I=Math.max(0,N.maxX-N.minX)+2*B0e(L),_=(N.minX+N.maxX)/2;C.push(L.id),w.push(_),S.push(I)}let R=C.length;if(R>0){let L=new Map;if(R===1)L.set(C[0],S[0]);else{let N=[];for(let D=0;D{"use strict";Z9="__swimlane_default__";o(B0e,"topLaneHorizontalPadding");o(Sct,"assignTopLaneTitleRect");o($0e,"prepareLayoutForSwimlanes");o(F0e,"toGraphView");o(z0e,"writeBackToLayoutData")});function G0e(e){let t=[],r=[],n=new Map;for(let s of e.nodes)n.set(s.id,s);for(let s of e.edges){if(!s.label||s.label.length===0||s.isLayoutOnly||s.labelNodeId)continue;let l=s.start?n.get(s.start):void 0,u=s.end?n.get(s.end):void 0;if(!l||!u){Z.warn(Ect,`Edge ${s.id} has missing source or target node`);continue}let h=`edge-label-${s.start}-${s.end}-${s.id}`,f=l.parentId!==u.parentId?u.parentId:l.parentId,p={id:h,label:s.label,edgeStart:s.start??"",edgeEnd:s.end??"",shape:"labelRect",width:0,height:0,isEdgeLabel:!0,isDummy:!0,parentId:f,isGroup:!1,labelStyle:Array.isArray(s.labelStyle)?s.labelStyle[0]:s.labelStyle??"",...l.dir?{dir:l.dir}:{}};t.push(p),s.labelNodeId=h,s.label=void 0,s.text=void 0;let m={id:`${s.id}-to-label`,start:s.start,end:h,type:"normal",isLayoutOnly:!0},g={id:`${s.id}-from-label`,start:h,end:s.end,type:"normal",isLayoutOnly:!0};r.push(m,g)}let i=[...e.nodes,...t],a=[...e.edges,...r];return{...e,nodes:i,edges:a}}var Ect,V0e=F(()=>{"use strict";vt();Ect="[EdgeLabelNodes]";o(G0e,"createEdgeLabelNodes")});function q0e(e){let t=e.x??0,r=e.y??0,n=e.width??0,i=e.height??0;return n>0&&i>0?{cx:t,cy:r,rect:x1(t,r,n,i)}:void 0}function H0e(e){if(e.isGroup)return;let t=q0e(e);return t?{id:String(e.id??""),cx:t.cx,cy:t.cy,rect:t.rect}:void 0}function uo(e,t,r=.001){return Math.abs(e.x-t.x)r}function Ti(e,t,r=.001){return Qr(e,t,r)&&Math.abs(e.y-t.y)>r}function ls(e,t,r,n){return Math.max(0,Math.min(Math.max(e,t),Math.max(r,n))-Math.max(Math.min(e,t),Math.min(r,n)))}function dc(e,t,r=.001){return e.horizontal&&t.horizontal&&tn(e.a,t.a,r)?ls(e.a.x,e.b.x,t.a.x,t.b.x):e.vertical&&t.vertical&&Qr(e.a,t.a,r)?ls(e.a.y,e.b.y,t.a.y,t.b.y):0}function Lf(e,t=.001){let r=[];for(let n=0;n0?r[r.length-1]:void 0;(!i||!uo(i,n,t))&&r.push({x:n.x,y:n.y})}return r}function u5(e,t=.001){if(!e||e.length!==4)return;let[r,n,i,a]=e;return bi(r,n,t)&&Ti(n,i,t)&&bi(i,a,t)?{kind:"HVH",p0:r,p1:n,p2:i,p3:a}:Ti(r,n,t)&&bi(n,i,t)&&Ti(i,a,t)?{kind:"VHV",p0:r,p1:n,p2:i,p3:a}:void 0}function xT(e,t,r,n=0){let i=Math.min(e.x,t.x),a=Math.max(e.x,t.x),s=Math.min(e.y,t.y),l=Math.max(e.y,t.y);return a>r.left-n&&ir.top-n&&st.left+r&&e.xt.top+r&&e.y=t.right&&e.top<=t.top&&e.bottom>=t.bottom}function d5(e,t){return e.leftt.left&&e.topt.top}function J9(e,t){return{left:e.left-t,right:e.right+t,top:e.top-t,bottom:e.bottom+t}}function x1(e,t,r,n){return{left:e-r/2,right:e+r/2,top:t-n/2,bottom:t+n/2}}function Sa(e){return q0e(e)?.rect}function Df(e,t){switch(t){case"top":return{x:e.cx,y:e.rect.top};case"bottom":return{x:e.cx,y:e.rect.bottom};case"left":return{x:e.rect.left,y:e.cy};case"right":return{x:e.rect.right,y:e.cy}}}function f5(e,t,r,n,i,a=.001){let s=t==="left"||t==="right",l=n==="left"||n==="right";if(s&&l){if(t==="right"&&n==="left"&&e.xr.x){if(tn(e,r,a))return[e,r];let f=(e.x+r.x)/2;return[e,{x:f,y:e.y},{x:f,y:r.y},r]}if(t===n){if(tn(e,r,a))return;let f=t==="left"?Math.min(e.x,r.x)-i:Math.max(e.x,r.x)+i;return[e,{x:f,y:e.y},{x:f,y:r.y},r]}return}if(!s&&!l){if(t===n){if(Qr(e,r,a))return;let p=t==="top"?Math.min(e.y,r.y)-i:Math.max(e.y,r.y)+i;return[e,{x:e.x,y:p},{x:r.x,y:p},r]}if(!(t==="bottom"&&n==="top"&&e.yr.y))return;if(Qr(e,r,a))return[e,r];let f=(e.y+r.y)/2;return[e,{x:e.x,y:f},{x:r.x,y:f},r]}if(s&&!l){let d=t==="right"&&r.x>e.x||t==="left"&&r.xr.y;return d&&f?[e,{x:r.x,y:e.y},r]:void 0}let u=t==="bottom"&&r.y>e.y||t==="top"&&r.yr.x;return u&&h?[e,{x:e.x,y:r.y},r]:void 0}function p5(e,t,r,n){return t==="left"||t==="right"?[e,{x:n,y:e.y},{x:n,y:r.y},r]:[e,{x:e.x,y:n},{x:r.x,y:n},r]}function b1(e){let t=new Map,r=[];for(let n of e){if(n.isEdgeLabel)continue;let i=H0e(n);i&&(t.set(i.id,i),r.push({id:i.id,rect:i.rect}))}return{nodeInfoById:t,realNodeRects:r}}function gu(e){let t=[],r=[];for(let n of e){let i=H0e(n);if(!i)continue;let a={id:i.id,rect:i.rect};n.isEdgeLabel?r.push(a):t.push(a)}return{realNodeRects:t,labelNodeRects:r}}function Y0e(e,{includeEdgeLabels:t=!0}={}){let r=[];for(let n of e){if(n.isGroup||!t&&n.isEdgeLabel)continue;let i=n.x??0,a=n.y??0,s=n.width??0,l=n.height??0;r.push({nodeId:n.id,...x1(i,a,s,l)})}return r}function m5(e,t,r=.001){let n=e.start,i=e.end;if(!n||!i)return;let a=t.get(n),s=t.get(i);if(!(!a||!s))return{srcId:n,dstId:i,srcInfo:a,dstInfo:s,collinearX:Math.abs(a.cx-s.cx)g||px)return!1;let b=Math.abs(y-d.a.x)i:a&&l&&tn(e,r,i)?ls(e.x,t.x,r.x,n.x)>i:!1}function bT(e,t,r,n,{epsilon:i=.001,skipDegenerateOther:a=!1}={}){for(let s of r){if(s===n||s.isLayoutOnly)continue;let l=s.points;if(!(!l||l.length<2))for(let u=0;up+i&&gy+i&&fn+.001&&e=2?t[t.length-2]:void 0,u=(s?Qr(s,i):!1)?{x:i.x,y:a.y}:{x:a.x,y:i.y};t.push(u)}t.push(a)}let r=[];for(let n of t){let i=r[r.length-1];(!i||!uo(i,n))&&r.push(n)}return r}function Tl(e){if(e.length<3)return e;let t=[...e];for(let r=0;r<32;r++){let n=_ct(t);if(t=n.points,!n.changed)break}return t}var pc=F(()=>{"use strict";o(q0e,"measuredNodeRect");o(H0e,"nodeBoundsInfoFor");o(uo,"samePoint");o(Qr,"sameX");o(tn,"sameY");o(bi,"isHorizontalSegment");o(Ti,"isVerticalSegment");o(ls,"overlapLength");o(dc,"sameAxisSegmentOverlapLength");o(Lf,"orthogonalSegmentsForPoints");o(ho,"countOrthogonalBends");o(Jr,"dedupeConsecutivePoints");o(u5,"classifyThreeSegmentRoute");o(xT,"segmentBoundsOverlapRect");o(h5,"pointInsideRect");o(U0e,"rectContainsRect");o(d5,"rectsOverlap");o(J9,"inflateRect");o(x1,"rectFromCenterSize");o(Sa,"rectOfNodeBounds");o(Df,"portForRectSide");o(f5,"buildOrthogonalPortPath");o(p5,"buildSameSideTrackPath");o(b1,"collectRealNodeBounds");o(gu,"collectNodeRectEntries");o(Y0e,"collectLayoutNodeRects");o(m5,"getNodePairGeometry");o(jn,"segmentHitsAnyRect");o(eB,"orthogonalSegmentsCross");o(Act,"sameAxisSegmentsOverlap");o(bT,"segmentConflictsWithAnyEdge");o(fc,"orthogonalSegmentsStrictlyCross");o(W0e,"strictlyBetween");o(Rct,"isCollinearIntermediate");o(_ct,"simplifyPolylineOnce");o(TT,"orthogonalizePolyline");o(Tl,"simplifyPolyline")});function iye(e,t,r){let n=e;if(n.isLayoutOnly||!n.points||n.points.length=0&&i=e.length)return e;let a=i-n;if(a<0||a>=e.length)return e;let s=Dct(e[i],e[a],t);return r?[s,...e.slice(i)]:[...e.slice(0,i+1),s]}function aye(e,t){for(let r of e){let n=iye(r,t,2);if(!n)continue;let i=[...n.points];n.srcRect&&(i=X0e(i,n.srcRect,!0)),n.dstRect&&(i=X0e(i,n.dstRect,!1)),i=Tl(TT(i)),i=sye(i,n.srcRect,n.dstRect),n.edge.points=Tl(TT(i))}}function K0e(e,t,r,n=!1){if(tn(e,t,Sr)){if(t.yr.bottom+Sr)return t;if(n){if(e.xr.right+Sr)return{x:r.right,y:e.y}}return{x:Math.abs(t.x-r.left)<=Math.abs(t.x-r.right)?r.left:r.right,y:e.y}}if(Qr(e,t,Sr)){if(t.xr.right+Sr)return t;if(n){if(e.yr.bottom+Sr)return{x:e.x,y:r.bottom}}let i=Math.abs(t.y-r.top)<=Math.abs(t.y-r.bottom);return{x:e.x,y:i?r.top:r.bottom}}return t}function tB(e,t,r){let n=e[t];for(let i=t+r;i>=0&&in.lo)),r=Math.min(...e.map(n=>n.hi));if(!(t>r))return{lo:t,hi:r}}function Q0e(e,t){return t==="left"||t==="right"?rB(e.top,e.bottom):rB(e.left,e.right)}function nB(e,t,r){let n=e.y>=r.top-Sr&&e.y<=r.bottom+Sr,i=e.x>=r.left-Sr&&e.x<=r.right+Sr;if(tn(e,t,Sr)&&n){if(Math.abs(e.x-r.left)0?Ict(a):void 0}function J0e(e,t,r,n,i){let a=Mct(e,t,r,n,i);if(!a)return;let s=i?e.y:e.x,l=Math.min(a.hi,Math.max(a.lo,s));if(!(Math.abs(l-s)({...l}));for(let l=t;l>=0&&l=r.left-Sr&&Math.max(e.x,t.x)<=r.right+Sr,i=Math.min(e.y,t.y)>=r.top-Sr&&Math.max(e.y,t.y)<=r.bottom+Sr;if(Math.abs(e.y-r.top)n.bottom+Sr;case"left":return tn(t,r,Sr)&&r.xn.right+Sr}}function nye(e,t,r){if(e.length<3)return e;if(r){let a=tye(e[0],e[1],t);return a&&rye(a,e[1],e[2],t)?e.slice(1):e}let n=e.length-1,i=tye(e[n-1],e[n],t);return i&&rye(i,e[n-1],e[n-2],t)?e.slice(0,n):e}function Oct(e,t,r){let n=e;if(t){let a=tB(n,0,1);if(a){let s=K0e(a,n[0],t);s!==n[0]&&(n=[s,...n.slice(1)])}n=nye(n,t,!0)}if(r){let a=n.length-1,s=tB(n,a,-1);if(s){let l=K0e(s,n[a],r,!0);l!==n[a]&&(n=[...n.slice(0,a),l])}n=nye(n,r,!1)}let i=sye(n,t,r);return i!==n||n.length===2?i:(t&&(n=eye(n,t,!0)),r&&(n=eye(n,r,!1)),n)}function iB(e,t){for(let r of e){let n=iye(r,t,2);if(!n)continue;let i=Jr(n.points,Sr),a=Oct(i,n.srcRect,n.dstRect);if(a.length<3){n.edge.points=a;continue}let s=[a[0],{...a[0]},...a.slice(1,-1),a[a.length-1],{...a[a.length-1]}];n.edge.points=s}}var Sr,Lct,j0e,oye=F(()=>{"use strict";pc();Sr=.001,Lct=.5,j0e=4;o(iye,"endpointContextFor");o(Dct,"segmentEnterPoint");o(X0e,"clipEndpoint");o(aye,"clipEdgeEndpointsToNodeBoundaries");o(K0e,"snapEndpointToBoundary");o(tB,"firstDistinctAdjacent");o(rB,"cornerClearanceRange");o(Z0e,"clampToCornerClearance");o(Ict,"intersectRanges");o(Q0e,"clearanceRangeForSide");o(nB,"terminalSideForSegment");o(g5,"isHorizontalSide");o(Mct,"straightClearanceRange");o(J0e,"clearStraightEndpointCornerAxis");o(sye,"clearStraightEndpointCornerConnections");o(Nct,"cornerClearedEndpoint");o(Pct,"moveCollinearEndpointRun");o(eye,"clearEndpointCornerConnection");o(tye,"borderSideForSegment");o(rye,"leavesOutward");o(nye,"collapseOwnBorderStub");o(Oct,"snapAndCollapseEndpoints");o(iB,"prepareEdgeEndpointsForRenderer")});function cye(e){return new Map(e.map(t=>[t.id,t]))}function Bct(e,t){let r=e.parentId,n=null;for(;r;){let i=t.get(r);if(!i?.isGroup)break;n=i.id,r=i.parentId}return n}function lye(e,t){let r=0,n=e.parentId;for(;n;){let i=t.get(n);if(!i?.isGroup)break;r++,n=i.parentId}return r}function uye(e){let t=1/0,r=-1/0,n=1/0,i=-1/0;for(let a of e){let s=a.x,l=a.y;if(typeof s!="number"||typeof l!="number")continue;let u=a.width??0,h=a.height??0;t=Math.min(t,s-u/2),r=Math.max(r,s+u/2),n=Math.min(n,l-h/2),i=Math.max(i,l+h/2)}return t===1/0||n===1/0?null:{minX:t,maxX:r,minY:n,maxY:i}}function $ct(e,t){let r=e.padding??20;e.x=(t.minX+t.maxX)/2,e.y=(t.minY+t.maxY)/2,e.width=Math.max(0,t.maxX-t.minX)+r,e.height=Math.max(0,t.maxY-t.minY)+r}function Fct(e){let t=cye(e),r=e.filter(n=>n.isGroup&&n.parentId).sort((n,i)=>lye(i,t)-lye(n,t));for(let n of r){let i=e.filter(s=>s.parentId===n.id),a=uye(i);a&&$ct(n,a)}}function aB(e,t){let r=e.nodes??[],n=e.edges??[],i=r.filter(u=>!u.isGroup),a=1/0,s=-1/0;for(let u of i){let h=u[t];typeof h=="number"&&(a=Math.min(a,h),s=Math.max(s,h))}if(!Number.isFinite(a)||!Number.isFinite(s))return!1;let l=o(u=>a+s-u,"mirror");for(let u of r){let h=u[t];typeof h=="number"&&(u[t]=l(h));let d=u.groupTitleRect;d&&(u.groupTitleRect=t==="x"?{...d,left:l(d.right),right:l(d.left)}:{...d,top:l(d.bottom),bottom:l(d.top)})}for(let u of n)for(let h of u.points??[])h[t]=l(h[t]);return!0}function hye(e){return(e.nodes??[]).some(r=>!r.isGroup)?aB(e,"y"):!0}function dye(e,t="LR"){let r=e.nodes??[],n=e.edges??[],i=r.filter(A=>!A.isGroup),a=1/0,s=1/0;for(let A of i){let M=A.x??0,D=A.y??0;M0?Math.max(1,d/f):1;for(let A of i){let M=A.x??0,P=((A.y??0)-s)*p+l,B=M-a;A.x=P,A.y=B}for(let A of n)if(A.points)for(let M of A.points){let D=M.x,B=(M.y-s)*p+l,O=D-a;M.x=B,M.y=O}Fct(r);let m=r.filter(A=>A.isGroup&&!A.parentId);if(m.length===0)return t==="RL"&&aB(e,"x"),!0;let g=cye(r),y=new Map;for(let A of r){if(A.isGroup)continue;let M=Bct(A,g);if(!M)continue;let D=y.get(M)??[];D.push(A),y.set(M,D)}let v=0;for(let A of m){let M=A.padding??0;M>v&&(v=M)}let x=[],b=1/0,T=-1/0;for(let A of m){let M=y.get(A.id)??[],D=uye(M);D&&(b=Math.min(b,D.minX),T=Math.max(T,D.maxX),x.push({lane:A,contentTop:D.minY,contentBottom:D.maxY,centerY:(D.minY+D.maxY)/2}))}if(b===1/0||T===-1/0)return!0;let k=Math.max(0,T-b),C=Math.max(v,10),w=k+2*C,S=l+w,N=(b+T)/2-w/2-l,I=N+S/2,_=Math.max(v,l);x.sort((A,M)=>A.centerY-M.centerY);for(let A=0;A{"use strict";o(cye,"buildNodeMap");o(Bct,"resolveTopLevelGroupId");o(lye,"groupDepth");o(uye,"boundsForChildren");o($ct,"applyGroupBounds");o(Fct,"recomputeNestedGroupBounds");o(aB,"mirrorAxis");o(hye,"applyBtDirectionTransform");o(dye,"applyLrDirectionTransform")});function pye(e,t){let{nodeInfoById:r,realNodeRects:n}=b1(t);for(let i of e){if(i.isLayoutOnly)continue;let a=i.points;if(!a||a.length<4)continue;let s=u5(Jr(a,mc),mc);if(!s)continue;let{p3:l}=s,u=s.kind==="HVH",h=m5(i,r,mc);if(!h)continue;let{srcId:d,dstId:f,srcInfo:p,dstInfo:m,collinearX:g,collinearY:y}=h;if(g||y)continue;let v,x=p.rect;for(let b of Gct){let T,k,C;if(u){let I=m.cy>p.cy?x.bottom:x.top,_=p.cx+b;if(_<=x.left+mc||_>=x.right-mc)continue;T={x:_,y:I},k={x:_,y:l.y},C={x:l.x,y:l.y}}else{let I=m.cx>p.cx?x.right:x.left,_=p.cy+b;if(_<=x.top+mc||_>=x.bottom-mc)continue;T={x:I,y:_},k={x:l.x,y:_},C={x:l.x,y:l.y}}let w=uo(T,k,mc),S=uo(k,C,mc);if(w&&S||!w&&jn(T,k,n,[d],1)||!S&&jn(k,C,n,[f],1))continue;let R=!w&&bT(T,k,e,i,{epsilon:mc,skipDegenerateOther:!0}),L=!S&&bT(k,C,e,i,{epsilon:mc,skipDegenerateOther:!0});if(!(R||L)){w?v=[k,C]:S?v=[T,k]:v=[T,k,C];break}}v&&(i.points=v)}}var mc,zct,y5,Gct,mye=F(()=>{"use strict";pc();mc=1e-6,zct=8,y5=zct,Gct=[0,y5,-y5,2*y5,-2*y5];o(pye,"portSwapToLShape")});function gye(e,t){let{realNodeRects:a,labelNodeRects:s}=gu(t.values());for(let l of e){if(l.isLayoutOnly)continue;let u=l.points;if(!u||u.length<4)continue;let h=Jr(u,.001);if(h.length<4)continue;let d=h.length-1,f=h[d],p=h[d-1],m=h[d-2],g=f.x-p.x,y=f.y-p.y,v=Math.hypot(g,y);if(v>=10||v<.001)continue;let x=p.x-m.x,b=p.y-m.y;if(Math.hypot(x,b)<.001)continue;let k=bi(p,f,.001),C=Ti(p,f,.001),w=bi(m,p,.001),S=Ti(m,p,.001);if(!(k&&S||C&&w))continue;let R=l.end,L=l.start,N=R?t.get(R):void 0;if(!N)continue;let I=N.x??0,_=N.y??0,A=Sa(N);if(!A)continue;let M,D;if(S){let z=b<0;M={x:I,y:m.y},D={x:I,y:z?A.bottom:A.top}}else{let z=x>0;M={x:m.x,y:_},D={x:z?A.right:A.left,y:_}}if(jn(M,D,a,R?[R]:[],-2)||jn(M,D,s,[],-2))continue;if(L){let z=t.get(L),W=z?Sa(z):void 0;if(W&&h5(M,W,2))continue}let P=o((z,W)=>`${z.x.toFixed(3)},${z.y.toFixed(3)}|${W.x.toFixed(3)},${W.y.toFixed(3)}`,"ownSegmentKey"),B=new Set;for(let z=0;z{for(let H of e){if(H===l||H.isLayoutOnly)continue;let j=H.points;if(!(!j||j.length<2))for(let Q=0;Q=0){let z=h[d-3],W=[L,R].filter(H=>!!H);if(jn(z,M,a,W,-2)||O(z,M))continue}let V=[...h.slice(0,d-2),M,D];l.points=V;let G=l.labelNodeId;if(G){let z=t.get(G);if(z){let W=z.width??0,H=z.height??0;if(W>0&&H>0){let j,Q,U=-1;for(let oe=0;oe=W+2||Re&&ie>=H+2)&&ie>U&&(U=ie,j=(te.x+le.x)/2,Q=(te.y+le.y)/2)}j!==void 0&&Q!==void 0&&(z.x=j,z.y=Q)}}}}}var yye=F(()=>{"use strict";pc();o(gye,"collapseShortTerminalStub")});function vye(e,t){let i=o((m,g)=>{let y=m.x??0,v=m.y??0,x=g.x-y,b=g.y-v,T=(m.width??0)/2,k=(m.height??0)/2;return Math.abs(b)*T>Math.abs(x)*k?(b<0&&(k=-k),{x:y+(b===0?0:k*x/b),y:v+k}):(x<0&&(T=-T),{x:y+T,y:v+(x===0?0:T*b/x)})},"rectIntersect"),a=o((m,g)=>{let y=Jr(m.points??[]);if(y.length<2)return;let v=g?m.start:m.end,x=v?t.get(v):void 0,b=x?Sa(x):void 0;if(!x||!v||!b)return;let T=g?y[0]:y[y.length-1],k=g?y[1]:y[y.length-2],C=i(x,T),w=T;if(sB(k,C)&&(w=k),Qr(C,w,rr))return{edge:m,edgeId:String(m.id??""),nodeId:v,atStart:g,orientation:"V",coord:C.x,min:Math.min(C.y,w.y),max:Math.max(C.y,w.y),boundary:C,railEnd:w,rect:b};if(tn(C,w,rr))return{edge:m,edgeId:String(m.id??""),nodeId:v,atStart:g,orientation:"H",coord:C.y,min:Math.min(C.x,w.x),max:Math.max(C.x,w.x),boundary:C,railEnd:w,rect:b}},"terminalLaneFor"),s=o((m,g)=>Math.max(0,Math.min(m.max,g.max)-Math.max(m.min,g.min)),"projectedOverlapLength"),l=o((m,g)=>m.nodeId!==g.nodeId||m.orientation!==g.orientation?!1:m.orientation==="H"?(Math.abs(m.boundary.x-m.rect.left)<1||Math.abs(m.boundary.x-m.rect.right)<1)&&Qr(m.boundary,g.boundary,1):(Math.abs(m.boundary.y-m.rect.top)<1||Math.abs(m.boundary.y-m.rect.bottom)<1)&&tn(m.boundary,g.boundary,1),"sameTerminalFace"),u=o((m,g)=>m.nodeId!==g.nodeId||m.orientation!==g.orientation?!1:s(m,g)>=Ea&&Math.abs(m.coord-g.coord)<.5,"exactTerminalLaneConflict"),h=o((m,g)=>{if(m.nodeId!==g.nodeId||m.orientation!==g.orientation||m.orientation!=="H"||m.atStart===g.atStart)return!1;let y=s(m,g);if(y2*v?!1:l(m,g)&&Math.abs(m.coord-g.coord)<16},"nearTerminalLaneConflict"),d=o((m,g)=>{let y=Jr(m.edge.points??[]);if(y.length<2)return;let v=m.orientation==="V"?{x:m.boundary.x+g,y:m.boundary.y}:{x:m.boundary.x,y:m.boundary.y+g},x=m.orientation==="V"?{x:m.railEnd.x+g,y:m.railEnd.y}:{x:m.railEnd.x,y:m.railEnd.y+g};if(!o(()=>Math.abs(m.boundary.y-m.rect.top)<1||Math.abs(m.boundary.y-m.rect.bottom)<1?tn(v,m.boundary,rr)&&v.x>=m.rect.left+1&&v.x<=m.rect.right-1:Math.abs(m.boundary.x-m.rect.left)<1||Math.abs(m.boundary.x-m.rect.right)<1?Qr(v,m.boundary,rr)&&v.y>=m.rect.top+1&&v.y<=m.rect.bottom-1:!1,"boundaryStaysOnSameFace")())return;if(m.atStart){let w=y.length>1&&uo(y[1],m.railEnd,rr),S=y.slice(w?2:1),R=S[0];return R&&!sB(R,x)?void 0:[v,x,...S]}let T=y.length>1&&uo(y[y.length-2],m.railEnd,rr),k=y.slice(0,T?-2:-1),C=k[k.length-1];if(!(C&&!sB(C,x)))return[...k,x,v]},"shiftedCandidate"),f=o(m=>{let g=m.edge,y=Jr(g.points??[]);if(y.length!==2)return!1;let v=g.start,x=g.end,b=v?t.get(v):void 0,T=x?t.get(x):void 0;if(!b||!T)return!1;let k=b.x??0,C=b.y??0,w=T.x??0,S=T.y??0,[R,L]=y;return tn(R,L,rr)&&Math.abs(C-S)<1&&Math.abs(k-w)>1||Qr(R,L,rr)&&Math.abs(k-w)<1&&Math.abs(C-S)>1},"laneIsStraightCollinearConnector"),p=[-7,7,-14,14,-21,21];for(let m=0;m<8;m++){let g=e.filter(v=>!v.isLayoutOnly).flatMap(v=>[a(v,!0),a(v,!1)]).filter(v=>!!v),y=!1;for(let v=0;v{let R=f(w),L=f(S);return R!==L?Number(R)-Number(L):+!S.atStart-+!w.atStart});for(let w of C){for(let S of p){let R=d(w,S);if(!R)continue;let L=a({...w.edge,points:R},w.atStart);if(!(!L||g.some(N=>N.edge!==w.edge&&(u(L,N)||k&&h(L,N))))){w.edge.points=R,y=!0;break}}if(y)break}}if(!y)return}}function xye(e,t){let{realNodeRects:i,labelNodeRects:a}=gu(t.values()),s=o((u,h)=>{let d=u.start,f=u.end,p=jr(h);if(p.length!==h.length-1)return!1;let m=[d,f].filter(g=>!!g);for(let g of p)if(jn(g.a,g.b,i,m,-2)||jn(g.a,g.b,a,[],-2))return!1;for(let g of e){if(g===u||g.isLayoutOnly)continue;let y=g.points;if(!(!y||y.length<2)){for(let v of p)for(let x of jr(Jr(y)))if(dc(v,x,.5)>=Ea||fc(v.a,v.b,x.a,x.b,rr))return!1}}return!0},"candidateIsSafe"),l=o((u,h)=>{if(h+4>=u.length)return;let d=u[h],f=u[h+1],p=u[h+2],m=u[h+3],g=u[h+4],y=bi(d,f)&&Ti(f,p)&&bi(p,m)&&Ti(m,g)&&Qr(d,m,rr)&&Qr(d,g,rr)&&Qr(f,p,rr)&&(f.x-d.x)*(m.x-p.x)<0,v=Ti(d,f)&&bi(f,p)&&Ti(p,m)&&bi(m,g)&&tn(d,m,rr)&&tn(d,g,rr)&&tn(f,p,rr)&&(f.y-d.y)*(m.y-p.y)<0;if(y||v)return Jr([...u.slice(0,h+1),g,...u.slice(h+5)]);if(h+5>=u.length)return;let x=u[h+5],b=Ti(d,f)&&bi(f,p)&&Ti(p,m)&&bi(m,g)&&Ti(g,x)&&Qr(d,g,rr)&&Qr(d,x,rr)&&Qr(p,m,rr)&&(p.x-f.x)*(g.x-m.x)<0,T=bi(d,f)&&Ti(f,p)&&bi(p,m)&&Ti(m,g)&&bi(g,x)&&tn(d,g,rr)&&tn(d,x,rr)&&tn(p,m,rr)&&(p.y-f.y)*(g.y-m.y)<0;if(!(!b&&!T))return Jr([...u.slice(0,h+1),x,...u.slice(h+6)])},"withoutDogleg");for(let u=0;u<8;u++){let h=!1;for(let d of e){if(d.isLayoutOnly)continue;let f=Jr(d.points??[]);for(let p=0;p<=f.length-5;p++){let m=l(f,p);if(!(!m||!s(d,m))){d.points=m,h=!0;break}}if(h)break}if(!h)return}}function oB(e,t){let{realNodeRects:a,labelNodeRects:s}=gu(t.values()),l=e.filter(g=>!g.isLayoutOnly),u=o((g,y,v)=>Jr(g===y?v??[]:g.points??[]),"pointsFor"),h=o((g,y)=>{let v=0;for(let x=0;x{let y=jr(g);if(y.length!==3)return;let v=y[1];if(!(y[0].horizontal===v.horizontal||y[2].horizontal===v.horizontal))return{index:v.index,horizontal:v.horizontal,vertical:v.vertical,segment:v}},"middleRail"),f=o((g,y)=>{let v=[g.start,g.end].filter(x=>!!x);return a.filter(x=>{if(v.includes(x.id))return!1;let b=x.rect;return y.horizontal?ls(y.a.x,y.b.x,b.left,b.right)>=Ea&&y.a.y>=b.top-2&&y.a.y<=b.bottom+2:ls(y.a.y,y.b.y,b.top,b.bottom)>=Ea&&y.a.x>=b.left-2&&y.a.x<=b.right+2})},"blockingRectsFor"),p=o((g,y,v)=>{let x=g.map(T=>({...T}));if(y.horizontal)x[y.index].y=v,x[y.index+1].y=v;else if(y.vertical)x[y.index].x=v,x[y.index+1].x=v;else return;let b=Tl(Jr(x));return jr(b).length===b.length-1?b:void 0},"candidateByMovingRail"),m=o((g,y,v)=>{let x=[g.start,g.end].filter(T=>!!T),b=jr(y);if(b.length!==y.length-1)return!1;for(let T of b)if(jn(T.a,T.b,a,x,-2)||jn(T.a,T.b,s,[],-2))return!1;for(let T of l)if(T!==g){for(let k of b)for(let C of jr(u(T)))if(dc(k,C,.5)>=Ea)return!1}return h(g,y)<=v},"candidateIsSafe");for(let g=0;g<8;g++){let y=h(),v=!1;for(let x of l){let b=u(x),T=d(b);if(!T)continue;let k=f(x,T.segment);if(k.length===0)continue;let C=T.horizontal?[Math.min(...k.map(w=>w.rect.top))-20,Math.max(...k.map(w=>w.rect.bottom))+20]:[Math.min(...k.map(w=>w.rect.left))-20,Math.max(...k.map(w=>w.rect.right))+20];for(let w of C){let S=p(b,T.segment,w);if(!(!S||!m(x,S,y))){x.points=S,v=!0;break}}if(v)break}if(!v)return}}function lB(e,t){let n=o(u=>{let h=u.groupTitleRect;if(!(!h||typeof h.left!="number"||typeof h.right!="number"||typeof h.top!="number"||typeof h.bottom!="number"||!Number.isFinite(h.left)||!Number.isFinite(h.right)||!Number.isFinite(h.top)||!Number.isFinite(h.bottom)||h.right<=h.left||h.bottom<=h.top))return{left:h.left,right:h.right,top:h.top,bottom:h.bottom}},"validTitleRect"),i=o(u=>{if(!u.isGroup||u.parentId)return;let h=u.direction,d=typeof h=="string"?h.toUpperCase():"";if(d==="LR"||d==="RL"||d==="BT")return;let f=n(u),p=u.y,m=u.height;if(!f||typeof p!="number"||typeof m!="number"||!Number.isFinite(p)||!Number.isFinite(m)||m<=0)return;let g=f.right-f.left,y=f.bottom-f.top;if(!(y<=0||g{if(!u.horizontal)return!1;let d=u.a.y;return d<=h.top+rr||d>=h.bottom-rr?!1:ls(u.a.x,u.b.x,h.left,h.right)>=Ea},"horizontalSegmentIntersectsTitle"),s=[...t.values()].map(i).filter(u=>!!u);if(s.length===0)return;let l=0;for(let u of e){if(u.isLayoutOnly)continue;let h=Jr(u.points??[]);for(let d of jr(h))for(let f of s)a(d,f.rect)&&(l=Math.max(l,f.rect.bottom-d.a.y+4))}if(!(l<=rr))for(let u of s){let h=u.node.y,d=u.node.height;typeof h!="number"||typeof d!="number"||!Number.isFinite(h)||!Number.isFinite(d)||d<=0||(u.node.y=h-l/2,u.node.height=d+l,u.node.groupTitleRect={...u.rect,top:u.rect.top-l,bottom:u.rect.bottom-l})}}function cB(e,t){let n=o(h=>{let d=h.groupTitleRect;if(!(!d||typeof d.left!="number"||typeof d.right!="number"||typeof d.top!="number"||typeof d.bottom!="number"||!Number.isFinite(d.left)||!Number.isFinite(d.right)||!Number.isFinite(d.top)||!Number.isFinite(d.bottom)||d.right<=d.left||d.bottom<=d.top))return{left:d.left,right:d.right,top:d.top,bottom:d.bottom}},"validTitleRect"),i=o(h=>{if(!h.isGroup||h.parentId||h.direction!=="LR")return;let f=n(h),p=h.x,m=h.width;if(!f||typeof p!="number"||typeof m!="number"||!Number.isFinite(p)||!Number.isFinite(m)||m<=0)return;let g=f.right-f.left,y=f.bottom-f.top;if(!(g<=0||y{if(!h.vertical)return!1;let f=h.a.x;return f<=d.left+rr||f>=d.right-rr?!1:ls(h.a.y,h.b.y,d.top,d.bottom)>=Ea},"verticalSegmentIntersectsTitle"),s=o((h,d)=>{if(!h.horizontal)return!1;let f=h.a.y;return f<=d.top+rr||f>=d.bottom-rr?!1:ls(h.a.x,h.b.x,d.left,d.right)>=Ea},"horizontalSegmentIntersectsTitle"),l=[...t.values()].map(i).filter(h=>!!h);if(l.length===0)return;let u=0;for(let h of e){if(h.isLayoutOnly)continue;let d=Jr(h.points??[]);for(let f of jr(d))for(let p of l)if(a(f,p.rect))u=Math.max(u,p.rect.right-f.a.x+4);else if(s(f,p.rect)){let m=Math.min(f.a.x,f.b.x);u=Math.max(u,p.rect.right-m+4)}}if(!(u<=rr))for(let h of l){let d=h.node.x,f=h.node.width;typeof d!="number"||typeof f!="number"||!Number.isFinite(d)||!Number.isFinite(f)||f<=0||(h.node.x=d-u/2,h.node.width=f+u,h.node.groupTitleRect={...h.rect,left:h.rect.left-u,right:h.rect.right-u})}}function bye(e,t){let{realNodeRects:i}=gu(t.values()),a=e.filter(y=>!y.isLayoutOnly),s=o((y,v=new Map)=>Jr(v.get(y)??y.points??[]),"replacementPointsFor"),l=o((y=new Map)=>{let v=0;for(let x=0;xa.reduce((v,x)=>v+ho(s(x,y)),0),"totalBends"),h=o(y=>{let v=s(y);if(v.length<4)return;let x=v[v.length-2],b=v[v.length-1];if(!(!bi(x,b,rr)&&!Ti(x,b,rr)))return{tailStart:x,terminal:b}},"terminalTailFor"),d=o((y,v)=>{let x=s(y);if(x.length<3)return;let b=x[0],T=x[1],k;if(bi(b,T,rr))k={x:T.x,y:v.tailStart.y};else if(Ti(b,T,rr))k={x:v.tailStart.x,y:T.y};else return;let C=Tl(Jr([b,T,k,v.tailStart,v.terminal]));return jr(C).length===C.length-1?C:void 0},"candidateWithDestinationTail"),f=o((y,v)=>{let x=[y.start,y.end].filter(b=>!!b);for(let b of jr(v))if(jn(b.a,b.b,i,x,-2))return!0;return!1},"pathHasNodeHit"),p=o((y,v,x)=>{for(let b of a)if(b!==y){for(let T of jr(v))for(let k of jr(s(b,x)))if(dc(T,k,.5)>=Ea)return!0}return!1},"pathHasSharedTrack"),m=o((y,v,x)=>!f(y,v)&&!p(y,v,x),"candidateIsSafe"),g=o(()=>{let y=new Map;for(let v of a){let x=v.end;if(!x||!t.has(x)||s(v).length<4)continue;let T=y.get(x)??[];T.push(v),y.set(x,T)}return y},"edgesByDestination");for(let y=0;y<4;y++){let v=l();if(v===0)return;let x=u(),b,T=v,k=x;for(let C of g().values())for(let w=0;w=v||D>T||D===T&&P>=k||(b=M,T=D,k=P)}if(!b)return;for(let[C,w]of b)C.points=w}}function Tye(e,t){let{realNodeRects:s,labelNodeRects:l}=gu(t.values()),u=e.filter(C=>!C.isLayoutOnly),h=o((C,w=new Map)=>Jr(w.get(C)??C.points??[]),"replacementPointsFor"),d=o((C=new Map)=>{let w=0;for(let S=0;Su.reduce((w,S)=>w+ho(h(S,C)),0),"totalBends"),p=o(C=>{let w=C.start,S=C.end,R=w?t.get(w):void 0,L=S?t.get(S):void 0,N=R?Sa(R):void 0,I=L?Sa(L):void 0;return N&&I?{src:N,dst:I}:void 0},"endpointRectsFor"),m=o((C,w,S)=>{if(S.index<=0||S.index+1>=w.length-1)return;let R=p(C);if(R){if(S.vertical){let L=S.a.x,N=Math.min(R.src.left,R.dst.left),I=Math.max(R.src.right,R.dst.right),_=LI+rr?"right":void 0;return _?{edge:C,points:w,segmentIndex:S.index,axis:"vertical",side:_,coord:L,min:Math.min(S.a.y,S.b.y),max:Math.max(S.a.y,S.b.y)}:void 0}if(S.horizontal){let L=S.a.y,N=Math.min(R.src.top,R.dst.top),I=Math.max(R.src.bottom,R.dst.bottom),_=LI+rr?"bottom":void 0;return _?{edge:C,points:w,segmentIndex:S.index,axis:"horizontal",side:_,coord:L,min:Math.min(S.a.x,S.b.x),max:Math.max(S.a.x,S.b.x)}:void 0}}},"externalRailForSegment"),g=o(()=>{let C=[];for(let w of u){let S=h(w);for(let R of jr(S)){let L=m(w,S,R);L&&C.push(L)}}return C},"collectExternalRails"),y=o((C,w)=>C.edge!==w.edge&&C.axis===w.axis&&C.side===w.side&&ls(C.min,C.max,w.min,w.max)>=Ea,"railsInteract"),v=o(C=>{let w=[],S=new Set;for(let R of C){if(S.has(R))continue;let L=[R],N=[];for(S.add(R);L.length>0;){let I=L.pop();N.push(I);for(let _ of C)!S.has(_)&&y(I,_)&&(S.add(_),L.push(_))}N.length>1&&w.push(N)}return w},"connectedComponents"),x=o(C=>{let w=[];for(let S of C)w.some(R=>Math.abs(R-S.coord){let w=C.map(L=>L.coord),S=x(C),R=[];if(C.length<=6){let L=new Array(S.length).fill(!1),N=[],I=o(()=>{if(N.length===C.length){N.some((_,A)=>Math.abs(_-w[A])>=rr)&&R.push([...N]);return}for(let[_,A]of S.entries())L[_]||(L[_]=!0,N.push(A),I(),N.pop(),L[_]=!1)},"visit");return I(),R}for(let L=0;L{let S=new Map;for(let[L,N]of C.entries()){let I=w[L],_=S.get(N.edge)??N.points.map(A=>({x:A.x,y:A.y}));N.axis==="vertical"?(_[N.segmentIndex].x=I,_[N.segmentIndex+1].x=I):(_[N.segmentIndex].y=I,_[N.segmentIndex+1].y=I),S.set(N.edge,_)}let R=new Map;for(let[L,N]of S){let I=Tl(Jr(N));if(jr(I).length!==I.length-1)return;R.set(L,I)}return R},"replacementsForAssignment"),k=o(C=>{for(let[w,S]of C){let R=[w.start,w.end].filter(L=>!!L);for(let L of jr(S))if(jn(L.a,L.b,s,R,-2)||jn(L.a,L.b,l,[],-2))return!1}for(let w=0;w=Ea)return!1}}return!0},"candidateIsSafe");for(let C=0;C<4;C++){let w=d();if(w===0)return;let S,R=w,L=f(),N=Number.POSITIVE_INFINITY;for(let I of v(g()))for(let _ of b(I)){let A=T(I,_);if(!A||!k(A))continue;let M=d(A);if(M>=w)continue;let D=f(A),P=I.reduce((B,O,$)=>B+Math.abs(_[$]-O.coord),0);M>R||M===R&&(D>L||D===L&&P>=N)||(S=A,R=M,L=D,N=P)}if(!S)return;for(let[I,_]of S)I.points=_}}function Cye(e,t){let{realNodeRects:i,labelNodeRects:a}=gu(t.values()),s=e.filter(g=>!g.isLayoutOnly),l=o((g,y,v)=>Jr(g===y?v??[]:g.points??[]),"pointsFor"),u=o(g=>jr(g).reduce((y,v)=>{let x=v.a.x-v.b.x,b=v.a.y-v.b.y;return y+Math.hypot(x,b)},0),"pathLength"),h=o((g,y)=>{let v=0;for(let x=0;x{if(g.horizontal){let v=g.a.y;return(Math.abs(v-y.top)<1||Math.abs(v-y.bottom)<1)&&ls(g.a.x,g.b.x,y.left,y.right)>=Ea}if(g.vertical){let v=g.a.x;return(Math.abs(v-y.left)<1||Math.abs(v-y.right)<1)&&ls(g.a.y,g.b.y,y.top,y.bottom)>=Ea}return!1},"segmentRunsAlongRectBorder"),f=o(g=>{let y=[g.start,g.end].filter(x=>!!x),v=[];for(let x of y){let b=t.get(x),T=b?Sa(b):void 0;T&&v.push(T)}return v},"endpointRectsFor"),p=o((g,y)=>{if(y+3>=g.length)return[];let v=g[y],x=g[y+1],b=g[y+2],T=g[y+3],k=bi(v,x,rr)&&Ti(x,b,rr)&&bi(b,T,rr),C=Ti(v,x,rr)&&bi(x,b,rr)&&Ti(b,T,rr);if(!k&&!C)return[];if(!(k?Math.sign(x.x-v.x)!==Math.sign(T.x-b.x):Math.sign(x.y-v.y)!==Math.sign(T.y-b.y)))return[];let S=Qr(v,T,rr)||tn(v,T,rr)?[]:[{x:v.x,y:T.y},{x:T.x,y:v.y}],R=S.length===0?[[...g.slice(0,y+1),...g.slice(y+3)]]:S.map(N=>[...g.slice(0,y+1),N,...g.slice(y+3)]),L=new Set;return R.map(N=>Tl(Jr(N))).filter(N=>{if(jr(N).length!==N.length-1||!N.some(_=>uo(_,T,rr)))return!1;let I=N.map(_=>`${_.x.toFixed(3)},${_.y.toFixed(3)}`).join("|");return L.has(I)?!1:(L.add(I),!0)})},"shortcutCandidatesAt"),m=o((g,y,v)=>{let x=[g.start,g.end].filter(T=>!!T),b=f(g);for(let T of jr(y))if(jn(T.a,T.b,i,x,-2)||jn(T.a,T.b,a,[],-2)||b.some(k=>d(T,k)))return!1;for(let T of s)if(T!==g){for(let k of jr(y))for(let C of jr(l(T)))if(dc(k,C,.5)>=Ea)return!1}return h(g,y)<=v},"candidateIsSafe");for(let g=0;g<8;g++){let y=h(),v,x,b=y,T=Number.POSITIVE_INFINITY,k=Number.POSITIVE_INFINITY;for(let C of s){let w=l(C),S=ho(w,rr),R=u(w);for(let L=0;L<=w.length-4;L++)for(let N of p(w,L)){let I=ho(N,rr),_=u(N);if(!(Ib||M===b&&(I>T||I===T&&_>=k)||(v=C,x=N,b=M,T=I,k=_)}}if(!v||!x)return;v.points=x}}function wye(e,t){let s=[];for(let ue of t.values()){if(ue.isGroup||ue.isEdgeLabel)continue;let ye=ue.x??0,ke=ue.y??0,ce=Sa(ue);ce&&s.push({id:String(ue.id??""),cx:ye,cy:ke,rect:ce})}if(s.length===0)return;let l=new Map(s.map(ue=>[ue.id,ue])),u=s.map(ue=>({id:ue.id,rect:ue.rect})),h=["top","bottom","left","right"],d={top:Math.min(...s.map(ue=>ue.rect.top))-20,bottom:Math.max(...s.map(ue=>ue.rect.bottom))+20,left:Math.min(...s.map(ue=>ue.rect.left))-20,right:Math.max(...s.map(ue=>ue.rect.right))+20},f=e.filter(ue=>!ue.isLayoutOnly),p=new Map(f.map((ue,ye)=>[ue,ye])),m=o(ue=>{let ye=ue==="left"||ue==="top"?-1:1,ke=[];for(let ce=0;ce<=2;ce++)ke.push(d[ue]+ye*20*ce);return ke},"outwardTracksForSide"),g=o((ue,ye=new Map)=>Jr(ye.get(ue)??ue.points??[]),"replacementPointsFor"),y=o((ue,ye)=>{let ke=0;for(let ce of ue)for(let re of ye)fc(ce.a,ce.b,re.a,re.b,rr)&&ke++;return ke},"crossingCountBetweenSegments"),v=o((ue,ye)=>y(jr(ue),jr(ye)),"crossingCountBetweenPaths"),x=o((ue=new Map)=>{let ye=0,ke=[],ce=new Set,re=[],J=o(se=>{ce.has(se)||(ce.add(se),re.push(se))},"addEdge");for(let se=0;se0&&(ye+=ve,ke.push({first:ge,second:Me,count:ve}),J(ge),J(Me))}}return re.sort((se,ge)=>(p.get(se)??0)-(p.get(ge)??0)),{count:ye,pairs:ke,edgeSet:ce,edges:re}},"crossingSnapshot"),b=o((ue,ye)=>{let ke=new Set(ye.keys());if(ke.size===0)return ue.count;let ce=0;for(let J of ue.pairs)(ke.has(J.first)||ke.has(J.second))&&(ce+=J.count);let re=0;for(let J=0;J{let ye=new Map;for(let re of ue.pairs){let J=ye.get(re.first)??new Set;J.add(re.second),ye.set(re.first,J);let se=ye.get(re.second)??new Set;se.add(re.first),ye.set(re.second,se)}let ke=[],ce=new Set;for(let re of ue.edges){if(ce.has(re))continue;let J=[re],se=[];for(ce.add(re);J.length>0;){let ge=J.pop();se.push(ge);for(let Te of ye.get(ge)??[])ce.has(Te)||(ce.add(Te),J.push(Te))}se.sort((ge,Te)=>(p.get(ge)??0)-(p.get(Te)??0)),se.length>1&&ke.push(se)}return ke},"crossingComponents"),k=o(ue=>[ue.start,ue.end].filter(ye=>!!ye),"endpointIdsFor"),C=o(ue=>{let ye=[];for(let ke of T(ue)){let ce=new Set(ke),re=new Set(ke.flatMap(se=>k(se))),J=[...ke];for(let se of f)ce.has(se)||k(se).some(ge=>re.has(ge))&&J.push(se);J.sort((se,ge)=>(p.get(se)??0)-(p.get(ge)??0)),ye.push(J)}return ye},"pairSearchGroups"),w=o((ue,ye,ke)=>b(ue,new Map([[ye,ke]])),"crossingCountWithSingleReplacement"),S=o(ue=>{let ye=new Map;for(let ke of ue.pairs)ye.set(ke.first,(ye.get(ke.first)??0)+ke.count),ye.set(ke.second,(ye.get(ke.second)??0)+ke.count);return ye},"currentCrossingsByEdge"),R=o(ue=>ue.slice(1).reduce((ye,ke,ce)=>{let re=ue[ce];return ye+Math.abs(ke.x-re.x)+Math.abs(ke.y-re.y)},0),"pathLength"),L=o((ue=new Map)=>f.reduce((ye,ke)=>ye+ho(g(ke,ue)),0),"totalBends"),N=o((ue=new Map)=>f.reduce((ye,ke)=>ye+R(g(ke,ue)),0),"totalLength"),I=o((ue,ye,ke=new Map)=>{let ce=jr(ye);for(let re of f)if(re!==ue){for(let J of ce)for(let se of jr(g(re,ke)))if(dc(J,se,.5)>=Ea)return!0}return!1},"pathHasSegmentConflict"),_=o((ue,ye)=>{let ke=[ue.start,ue.end].filter(ce=>!!ce);for(let ce of jr(ye))if(jn(ce.a,ce.b,u,ke,-2))return!0;return!1},"pathHitsNode"),A=o((ue,ye)=>{let ke=Tl(Jr(ye));jr(ke).length===ke.length-1&&ue.push(ke)},"pushOrthogonalCandidate"),M=o(ue=>ue==="left"||ue==="right","sideIsHorizontal"),D=o((ue,ye,ke)=>{switch(ye){case"left":return Math.min(ue.x,ke.x)-20;case"right":return Math.max(ue.x,ke.x)+20;case"top":return Math.min(ue.y,ke.y)-20;case"bottom":return Math.max(ue.y,ke.y)+20}},"localTrackForSameSide"),P=o((ue,ye,ke,ce)=>{let re=ke==="left"||ke==="top"?-1:1,J=[D(ye,ke,ce),d[ke]];for(let se of J)for(let ge=0;ge<=2;ge++)A(ue,p5(ye,ke,ce,se+re*20*ge))},"addSameSideCandidates"),B=o((ue,ye,ke,ce,re)=>{for(let J of m(ke))for(let se of m(re))A(ue,[ye,{x:J,y:ye.y},{x:J,y:se},{x:ce.x,y:se},ce])},"addHorizontalToVerticalCandidates"),O=o((ue,ye,ke,ce,re)=>{for(let J of m(ke))for(let se of m(re))A(ue,[ye,{x:ye.x,y:J},{x:se,y:J},{x:se,y:ce.y},ce])},"addVerticalToHorizontalCandidates"),$=o((ue,ye,ke,ce,re)=>{let J=[...m("top"),...m("bottom")];for(let se of m(ke))for(let ge of m(re))for(let Te of J)A(ue,[ye,{x:se,y:ye.y},{x:se,y:Te},{x:ge,y:Te},{x:ge,y:ce.y},ce])},"addHorizontalPairCandidates"),V=o((ue,ye,ke,ce,re)=>{let J=[...m("left"),...m("right")];for(let se of m(ke))for(let ge of m(re))for(let Te of J)A(ue,[ye,{x:ye.x,y:se},{x:Te,y:se},{x:Te,y:ge},{x:ce.x,y:ge},ce])},"addVerticalPairCandidates"),G=o(ue=>{let ye=new Set;return ue.map(ke=>Jr(ke)).filter(ke=>{let ce=ke.map(re=>`${re.x.toFixed(3)},${re.y.toFixed(3)}`).join("|");return ye.has(ce)||ke.length<2?!1:(ye.add(ce),!0)})},"dedupeCandidatePaths"),z=o((ue,ye,ke,ce)=>{let re=[],J=f5(ue,ye,ke,ce,20,rr);J&&A(re,J),ye===ce&&P(re,ue,ye,ke);let se=M(ye),ge=M(ce);return se&&!ge?B(re,ue,ye,ke,ce):!se&&ge?O(re,ue,ye,ke,ce):se?$(re,ue,ye,ke,ce):V(re,ue,ye,ke,ce),G(re)},"buildCandidatesForSides"),W=o((ue,ye,ke,ce)=>{let re=[...m("left"),...m("right")],J=[...m("top"),...m("bottom")];for(let se of h){let ge=Df(ce,se),Te=se==="top"||se==="bottom"?m(se):J;for(let we of re){A(ue,[ye,ke,{x:we,y:ke.y},{x:we,y:ge.y},ge]);for(let Me of Te)A(ue,[ye,ke,{x:we,y:ke.y},{x:we,y:Me},{x:ge.x,y:Me},ge])}}},"addVerticalDepartureOuterTrackCandidates"),H=o((ue,ye,ke,ce)=>{let re=[...m("left"),...m("right")],J=[...m("top"),...m("bottom")];for(let se of h){let ge=Df(ce,se),Te=se==="left"||se==="right"?m(se):re;for(let we of J){A(ue,[ye,ke,{x:ke.x,y:we},{x:ge.x,y:we},ge]);for(let Me of Te)A(ue,[ye,ke,{x:ke.x,y:we},{x:Me,y:we},{x:Me,y:ge.y},ge])}}},"addHorizontalDepartureOuterTrackCandidates"),j=o(ue=>{let ye=ue.start,ke=ue.end,ce=ke?l.get(ke):void 0;if(!ye||!ce)return[];let re=Jr(ue.points??[]);if(re.length<4)return[];let J=re[0],se=re[1],ge=[];return Ti(J,se,rr)?W(ge,J,se,ce):bi(J,se,rr)&&H(ge,J,se,ce),ge},"terminalPreservingOuterTrackCandidates"),Q=o(ue=>{let ye=ue.start,ke=ue.end,ce=ye?l.get(ye):void 0,re=ke?l.get(ke):void 0;if(!ce||!re)return[];let J=[];for(let se of h){let ge=Df(ce,se);for(let Te of h)J.push(...z(ge,se,Df(re,Te),Te))}return J.push(...j(ue)),J},"candidatePathsFor"),U=o(()=>new Map(f.map(ue=>[ue,jr(g(ue))])),"currentSegmentsByEdge"),oe=o((ue,ye,ke)=>{let ce=new Set;for(let re of f){if(re===ue)continue;let J=ke.get(re)??jr(g(re));ye.some(se=>J.some(ge=>dc(se,ge,.5)>=Ea))&&ce.add(re)}return ce},"sharedTrackConflictsFor"),te=o((ue,ye,ke,ce)=>{let re=new Set;return Q(ue).map(se=>Tl(Jr(se))).filter(se=>{if(_(ue,se))return!1;let ge=se.map(Te=>`${Te.x.toFixed(3)},${Te.y.toFixed(3)}`).join("|");return re.has(ge)||se.length<2?!1:(re.add(ge),!0)}).map(se=>{let ge=jr(se),Te=0;for(let we of f)we!==ue&&(Te+=y(ge,ke.get(we)??jr(g(we))));return{candidate:se,candidateSegments:ge,crossings:ye.count-(ce.get(ue)??0)+Te,bends:ho(se,rr),totalBends:ho(se),length:R(se)}}).filter(({crossings:se})=>se<=ye.count).sort((se,ge)=>se.crossings-ge.crossings||se.bends-ge.bends||se.length-ge.length).slice(0,48).map(se=>({path:se.candidate,segments:se.candidateSegments,sharedTrackConflicts:oe(ue,se.candidateSegments,ke),totalBends:se.totalBends,length:se.length}))},"pairCandidatesFor"),le=o((ue,ye,ke,ce,re,J)=>{let se=0;for(let Te of ue.pairs)(Te.first===ye||Te.second===ye||Te.first===ce||Te.second===ce)&&(se+=Te.count);let ge=y(ke.segments,re.segments);for(let Te of f){if(Te===ye||Te===ce)continue;let we=J.get(Te)??jr(g(Te));ge+=y(ke.segments,we)+y(re.segments,we)}return ue.count-se+ge},"pairCrossingCount"),ie=o((ue,ye)=>{for(let ke of ue.sharedTrackConflicts)if(ke!==ye)return!1;return!0},"conflictsOnlyWith"),ae=o((ue,ye)=>ue.segments.some(ke=>ye.segments.some(ce=>dc(ke,ce,.5)>=Ea)),"candidatesShareTrack"),Re=o((ue,ye,ke,ce)=>ie(ye,ke.edge)&&ie(ce,ue.edge)&&!ae(ye,ce),"pairCandidatesAreCompatible"),be=o((ue,ye,ke,ce,re)=>{let J=le(ue.current,ye.edge,ke,ce.edge,re,ue.baseSegments);if(!(J>=ue.current.count))return{replacements:new Map([[ye.edge,ke.path],[ce.edge,re.path]]),crossings:J,bends:ue.currentBends-(ue.baseBendsByEdge.get(ye.edge)??0)-(ue.baseBendsByEdge.get(ce.edge)??0)+ke.totalBends+re.totalBends,length:ue.currentLength-(ue.baseLengthByEdge.get(ye.edge)??0)-(ue.baseLengthByEdge.get(ce.edge)??0)+ke.length+re.length}},"scorePairReplacement"),Pe=o((ue,ye)=>ue.crossings{let re=ce;for(let J of ye.candidates)for(let se of ke.candidates){if(!Re(ye,J,ke,se))continue;let ge=be(ue,ye,J,ke,se);ge&&Pe(ge,re)&&(re=ge)}return re},"bestScoreForOptionPair"),Oe=o(ue=>{let ye=L(),ke=N(),ce=U(),re=S(ue),J=new Map(f.map(ve=>[ve,ho(g(ve))])),se=new Map(f.map(ve=>[ve,R(g(ve))])),ge=new Map,Te=C(ue);for(let ve of Te)for(let ne of ve){if(ge.has(ne))continue;let q=te(ne,ue,ce,re);q.length>0&&ge.set(ne,{edge:ne,candidates:q})}let we={replacements:new Map,crossings:ue.count,bends:ye,length:ke},Me={current:ue,currentBends:ye,currentLength:ke,baseBendsByEdge:J,baseLengthByEdge:se,baseSegments:ce};for(let ve of Te){let ne=new Set(ve.filter(he=>ue.edgeSet.has(he))),q=ve.map(he=>ge.get(he)).filter(he=>!!he);for(let he=0;he0?we.replacements:void 0},"bestPairedReplacement");for(let ue=0;ue<4;ue++){let ye=x(),ke=ye.count;if(ke===0)return;let ce,re,J=ke,se=Number.POSITIVE_INFINITY;for(let Te of ye.edges){let we=ho(g(Te),rr);for(let Me of Q(Te)){let ve=_(Te,Me),ne=!ve&&I(Te,Me),q=w(ye,Te,Me),he=ho(Me,rr);ve||ne||!(qJ||q===J&&he>=se||(ce=Te,re=Me,J=q,se=he)}}if(ce&&re){ce.points=re;continue}let ge=Oe(ye);if(!ge)return;for(let[Te,we]of ge)Te.points=we}}var rr,Ea,jr,sB,kye=F(()=>{"use strict";pc();rr=.001,Ea=8,jr=Lf,sB=o((e,t)=>Qr(e,t,rr)||tn(e,t,rr),"orthogonallyAligned");o(vye,"separateSharedRenderedTerminalLanes");o(xye,"collapseRedundantRectangularDoglegs");o(oB,"liftObstacleHuggingSameSideRails");o(lB,"liftTopLaneTitleBandsAboveRails");o(cB,"shiftLeftLaneTitleBandsLeftOfRails");o(bye,"swapDestinationTerminalTailsToReduceCrossings");o(Tye,"reassignCrossingExternalRailChannels");o(Cye,"shortcutRedundantOrthogonalJogs");o(wye,"resolveRenderedOrthogonalCrossings")});function Sye(e,t){let{nodeInfoById:r,realNodeRects:n}=b1(t),i=["top","bottom","left","right"],a=20,s={top:Math.min(...n.map(y=>y.rect.top))-a,bottom:Math.max(...n.map(y=>y.rect.bottom))+a,left:Math.min(...n.map(y=>y.rect.left))-a,right:Math.max(...n.map(y=>y.rect.right))+a},l=o((y,v,x,b)=>{let T=[],k=f5(y,v,x,b,a,If);return k&&T.push(k),v===b&&T.push(p5(y,v,x,s[v])),T},"buildOrthogonalPathCandidates"),u=o((y,v)=>{for(let x=0;x{let b=0,T=Lf(y,If),k=v.start,C=v.end;for(let w of e){if(w===v||w.isLayoutOnly)continue;let S=w.start,R=w.end;if(!x&&k&&C&&(S===k||S===C||R===k||R===C))continue;let L=w.points;if(!(!L||L.length<2))for(let N of T)for(let I of Lf(L,If)){if(eB(N.a,N.b,I.a,I.b,If,If)){b++;continue}dc(N,I,If)>=Vct&&b++}}return b},"pathConflictCount"),d=4,f=o((y,v)=>{let x=Math.abs(y.y-v.rect.top),b=Math.abs(y.y-v.rect.bottom),T=Math.abs(y.x-v.rect.left),k=Math.abs(y.x-v.rect.right),C="top",w=x;return b{let b=p.get(y)??[];b.push({side:v,edgeId:x}),p.set(y,b)},"addFaceClaim");for(let y of e){if(y.isLayoutOnly)continue;let v=y.points??[];if(v.length<1)continue;let x=y.id??"",b=y.start,T=y.end;if(b){let k=r.get(b);k&&m(b,f(v[0],k),x)}if(T){let k=r.get(T);k&&m(T,f(v[v.length-1],k),x)}}let g=o((y,v,x)=>p.get(y)?.some(b=>b.edgeId!==x&&b.side===v)??!1,"faceIsClaimed");for(let y of e){if(y.isLayoutOnly)continue;let v=y.points;if(!v||v.length<2)continue;let x=ho(v,If);if(x0){let O=h(P,y,!0);if(O>N||O===N&&B>=I)continue;N=O,I=B,L=P;continue}h(P,y)>R||BM.edgeId!==w));let A=p.get(T);A&&p.set(T,A.filter(M=>M.edgeId!==w)),m(b,f(L[0],k),w),m(T,f(L[L.length-1],C),w)}}}var If,Vct,Eye=F(()=>{"use strict";pc();If=.001,Vct=8;o(Sye,"simplifyDetouredEdges")});function Rye(e,t){let r=t?0:e.length-1,n=t?1:-1,i=e[r],a=e[r+n];if(!i||!a)return;let s=a.x-i.x,l=a.y-i.y;if(!(Math.abs(s)+Math.abs(l)a&&d5(e,Wct(a)))}function x5(e,t){let r=[];for(let g of e){if(g.isLayoutOnly)continue;let y=g.points;if(!(!y||y.length<2))for(let v=0;v{let v=J9(y,a);for(let{nodeId:x,rect:b}of n)if(x!==g&&d5(v,b))return!0;return!1},"labelOverlapsForeignNode"),h=o((g,y)=>{let v=J9(y,a);for(let x of r)if(x.edgeId!==g&&xT(x.p1,x.p2,v))return!0;return!1},"labelOverlapsForeignEdge"),d=o((g,y,v)=>u(g,v)||h(y,v),"labelOverlapsAnything"),f=[],p=o(g=>{for(let{id:y,rect:v}of i)if(U0e(v,g))return y},"findContainingLane"),m=o((g,y)=>f.some(v=>v.labelId!==g&&d5(y,v.rect)),"overlapsPlacedLabel");for(let g of e){if(g.isLayoutOnly)continue;let y=g.labelNodeId;if(!y)continue;let v=t.get(y);if(!v)continue;let x=g.points;if(!x||x.length<2)continue;let b=v.width??0,T=v.height??0;if(b<=0||T<=0)continue;let k=[];for(let G=0;G=fo&&j>=fo||k.push({idx:G,length:H+j,orientation:H>=fo?"horizontal":"vertical",midX:(z.x+W.x)/2,midY:(z.y+W.y)/2})}if(k.length===0)continue;let C=k.length>=3?k.filter(G=>G.idx>0&&G.idx0?C:k,S=b>=T?"horizontal":"vertical",R=o(G=>[...G].sort((z,W)=>{let H=z.orientation===S,j=W.orientation===S;if(H!==j)return H?-1:1;let Q=z.length>=(z.orientation==="horizontal"?b:T)+2,U=W.length>=(W.orientation==="horizontal"?b:T)+2;return Q!==U?Q?-1:1:W.length-z.length}),"rankSegments"),L=k[0],N=k[k.length-1],I=[.5,.25,.75,.05,.95,.15,.85,.1,.9],_=o((G,z)=>{let W=x[G.idx],H=x[G.idx+1];return{midX:W.x+(H.x-W.x)*z,midY:W.y+(H.y-W.y)*z}},"anchorAtT"),A=o((G,z,W)=>Math.min(W,Math.max(z,G)),"clamp"),M=o((G,z)=>G.midX>=z.left-fo&&G.midX<=z.right+fo&&G.midY>=z.top-fo&&G.midY<=z.bottom+fo,"pointInsideRectInclusive"),D=o(G=>{let z=x1(G.midX,G.midY,b,T),W=p(z);if(W)return{laneId:W,anchor:G,rect:z};let H=i.find(({rect:ie})=>M(G,ie));if(!H)return;let j=H.rect.left+b/2+s,Q=H.rect.right-b/2-s,U=H.rect.top+T/2+s,oe=H.rect.bottom-T/2-s;if(j>Q||U>oe)return;let te={midX:A(G.midX,j,Q),midY:A(G.midY,U,oe)},le=x1(te.midX,te.midY,b,T);return M(G,le)?{laneId:H.id,anchor:te,rect:le}:void 0},"placementForAnchor"),P=o((G,z,W)=>G.orientation==="horizontal"?Math.abs(z.midX-W.x):Math.abs(z.midY-W.y),"distanceAlongSegment"),B=o((G,z)=>{let H=(G.orientation==="horizontal"?b/2:T/2)+l;if(G===L){let j=x[G.idx];if(P(G,z,j)+fo{let z=R(G);for(let W of z)for(let H of I){let j=_(W,H);if(!B(W,j))continue;let Q=D(j);if(Q&&!_ye(Q.rect,x)&&!m(y,Q.rect)&&!d(y,g.id,Q.rect))return{laneId:Q.laneId,anchor:Q.anchor}}},"tryPool"),$=o((G,z,W=!1)=>{let H=R(G);for(let j of H){let Q={midX:j.midX,midY:j.midY};if(z&&!B(j,Q))continue;let U=D(Q);if(U&&!_ye(U.rect,x)&&!m(y,U.rect)&&!u(y,U.rect)&&(W||!h(g.id,U.rect)))return{laneId:U.laneId,anchor:U.anchor}}},"findLaneContainingFallback"),V=O(w)??(w.lengthW.labelId===y);z>=0?f[z]={labelId:y,rect:G}:f.push({labelId:y,rect:G})}}}var fo,Aye,v5,Lye=F(()=>{"use strict";pc();fo=.001,Aye=10,v5=7;o(Rye,"markerClearanceRectFor");o(Wct,"normalizeRect");o(_ye,"labelOverlapsOwnMarker");o(x5,"anchorLabelsToPolyline")});function Iye(e,t){return e{let d=Iye(l,u),f=0,p=o(m=>{if(!m)return;let g=i.get(m);if(!g)return;let y=h==="x"?g.w/2:g.h/2;y>f&&(f=y)},"consider");p(s.labelNodeId);for(let m of e){if(m===s||m.isLayoutOnly)continue;let g=m.start,y=m.end;!g||!y||Iye(g,y)===d&&p(m.labelNodeId)}return f>0?f+Hct:0},"labelClearanceFor");for(let s of e){if(s.isLayoutOnly)continue;let l=s.points;if(!u5(l,uB))continue;let u=m5(s,r,uB);if(!u)continue;let{srcId:h,dstId:d,srcInfo:f,dstInfo:p,collinearX:m,collinearY:g}=u;if(m===g)continue;let y,v;if(m){let C=p.cy>f.cy;y={x:f.cx,y:C?f.rect.bottom:f.rect.top},v={x:p.cx,y:C?p.rect.top:p.rect.bottom}}else{let C=p.cx>f.cx;y={x:C?f.rect.right:f.rect.left,y:f.cy},v={x:C?p.rect.left:p.rect.right,y:p.cy}}if(jn(y,v,n,[h,d],1))continue;let b=a(s,h,d,m?"x":"y"),T=b>Dye?b:Dye,k=[0,T,-T];for(let C of k){let w={...y},S={...v};if(m){if(w.x+=C,S.x+=C,w.x<=f.rect.left||w.x>=f.rect.right||S.x<=p.rect.left||S.x>=p.rect.right)continue}else if(w.y+=C,S.y+=C,w.y<=f.rect.top||w.y>=f.rect.bottom||S.y<=p.rect.top||S.y>=p.rect.bottom)continue;if(!jn(w,S,n,[h,d],1)&&!bT(w,S,e,s,{epsilon:uB})){s.points=[w,S];break}}}}var uB,qct,Dye,Hct,Nye=F(()=>{"use strict";pc();uB=1e-6,qct=8,Dye=qct/2,Hct=3;o(Iye,"pairKey");o(Mye,"straightenCollinearSiblingDetours")});function hB(e,t){let{realNodeRects:h,labelNodeRects:d}=gu(t.values()),f=o((w,S)=>Lf(S,.001).map(R=>({...R,edge:w,interior:R.index>=1&&R.index<=S.length-3})),"segmentsFor"),p=o(()=>{let w=[];for(let S of e){if(S.isLayoutOnly)continue;let R=S.points;!R||R.length<2||w.push(...f(S,Jr(R)))}return w},"allSegments"),m=o((w,S)=>w.horizontal&&S.horizontal?ls(w.a.x,w.b.x,S.a.x,S.b.x)>=8&&Math.abs(w.a.y-S.a.y)<7:w.vertical&&S.vertical?ls(w.a.y,w.b.y,S.a.y,S.b.y)>=8&&Math.abs(w.a.x-S.a.x)<7:!1,"hasCrowdedParallelTrack"),g=o((w,S)=>{let R=w.start,L=w.end,N=f(w,S);if(N.length!==S.length-1)return!1;let I=[R,L].filter(A=>!!A),_=w.labelNodeId?[w.labelNodeId]:[];for(let A of N)if(jn(A.a,A.b,h,I,-2)||jn(A.a,A.b,d,_,-2))return!1;for(let A of e){if(A===w||A.isLayoutOnly)continue;let M=A.points;if(!(!M||M.length<2)){for(let D of N)for(let P of f(A,Jr(M)))if(m(D,P)||fc(D.a,D.b,P.a,P.b,.001))return!1}}return!0},"candidateIsSafe"),y=o((w,S)=>{let R=Jr(w.edge.points??[]);if(R.length<4||w.index>=R.length-1)return;let L=R.map(N=>({...N}));if(w.horizontal)L[w.index].y+=S,L[w.index+1].y+=S;else if(w.vertical)L[w.index].x+=S,L[w.index+1].x+=S;else return;return f(w.edge,L).length===L.length-1?L:void 0},"shiftedCandidate"),v=o((w,S)=>({x:w.x??(S.left+S.right)/2,y:w.y??(S.top+S.bottom)/2}),"nodeCenter"),x=o(w=>{let S=w.edge,R=Jr(S.points??[]);if(R.length!==4||w.index!==1)return;let L=S.start?t.get(S.start):void 0,N=S.end?t.get(S.end):void 0,I=L?Sa(L):void 0,_=N?Sa(N):void 0,A=R.slice(w.index+2);if(!(!L||!N||!I||!_||A.length===0))return{sourceCenter:v(L,I),targetCenter:v(N,_),sourceRect:I,tail:A}},"sourceDetourContextFor"),b=o((w,S,R,L,N,I)=>{let _=L.y>=R.y,A=_?N.bottom:N.top,M=A+(_?20:-20);if(_&&w.b.y<=M+.001||!_&&w.b.y>=M-.001)return;let D=w.a.x+S;return Jr([{x:R.x,y:A},{x:R.x,y:M},{x:D,y:M},{x:D,y:w.b.y},...I],.001)},"verticalSourceDetour"),T=o((w,S,R,L,N,I)=>{let _=L.x>=R.x,A=_?N.right:N.left,M=A+(_?20:-20);if(_&&w.b.x<=M+.001||!_&&w.b.x>=M-.001)return;let D=w.a.y+S;return Jr([{x:A,y:R.y},{x:M,y:R.y},{x:M,y:D},{x:w.b.x,y:D},...I],.001)},"horizontalSourceDetour"),k=o((w,S)=>{let R=x(w);if(R){if(w.vertical)return b(w,S,R.sourceCenter,R.targetCenter,R.sourceRect,R.tail);if(w.horizontal)return T(w,S,R.sourceCenter,R.targetCenter,R.sourceRect,R.tail)}},"sourceDetourCandidate"),C=[-7,7,-14,14,-21,21];for(let w=0;w<12;w++){let S=p(),R=!1;for(let L=0;LM.interior);for(let M of A){for(let D of C){let P=y(M,D);if(P&&g(M.edge,P)){M.edge.points=P,R=!0;break}let B=k(M,D);if(B&&g(M.edge,B)){M.edge.points=B,R=!0;break}}if(R)break}}if(!R)return}}var Pye=F(()=>{"use strict";pc();o(hB,"nudgeSharedInteriorSubpaths")});function Uct(e,t,r,n){let i=t.x-e.x,a=t.y-e.y,s=n.x-r.x,l=n.y-r.y,u=i*l-a*s;if(Math.abs(u)<1e-10)return!1;let h=r.x-e.x,d=r.y-e.y,f=(h*l-d*s)/u,p=(h*a-d*i)/u,m=.01;return f>m&&f<1-m&&p>m&&p<1-m}function dB(e){let t=e.nodes??[],r=e.edges??[],n=[];if(!r.length||!t.length)return n;let i=Y0e(t),a=1,s=[];for(let u of r){if(u.isLayoutOnly)continue;let h=u.points;if(!h||h.length<2)continue;let d=u.start,f=u.end,p=u.labelNodeId,m=u.id??`${d}->${f}`;for(let g of i)if(!(g.nodeId===d||g.nodeId===f)&&!(p&&g.nodeId===p)){for(let y=0;y0){let u=n.filter(d=>d.type==="edge-node-overlap").length,h=n.filter(d=>d.type==="edge-edge-crossing").length;Z.warn(`[SWIMLANE_VALIDATE] ${n.length} issue(s) detected: ${u} edge-node overlap(s), ${h} edge crossing(s)`);for(let d of n)Z.warn(`[SWIMLANE_VALIDATE] ${d.type}: ${d.detail}`)}return n}var Oye=F(()=>{"use strict";vt();pc();o(Uct,"segmentsIntersect");o(dB,"validateSwimlanesLayout")});function Bye(e,t){let r=e.nodes??[],n=e.edges??[],i=r.filter(l=>!l.isGroup);if((t==="LR"||t==="RL")&&i.length>0&&!dye(e,t)||t==="BT"&&i.length>0&&!hye(e))return;for(let l of n){if(l.isLayoutOnly)continue;let u=l.points;!u||u.length<2||(l.points=Tl(TT(u)))}Sye(n,r),Mye(n,r),pye(n,r);let a=new Map;for(let l of r)a.set(String(l.id),l);x5(n,a),aye(n,a),gye(n,a),hB(n,a),vye(n,a),xye(n,a),oB(n,a),bye(n,a);let s=o(()=>{wye(n,a),Tye(n,a),Cye(n,a),x5(n,a),iB(n,a),oB(n,a),x5(n,a),iB(n,a)},"finalizeRenderedEdges");s(),hB(n,a),s(),lB(n,a),cB(n,a),lB(n,a),cB(n,a)}var $ye=F(()=>{"use strict";oye();pc();fye();mye();yye();kye();Eye();Lye();Nye();Pye();Oye();o(Bye,"postProcessSwimlaneLayout")});function po(e){let t=new Map(e.nodeById),r=new Set,n=[];for(let a of e.edges){if(!t.has(a.src)||!t.has(a.dst))continue;let s=`${a.id}:${a.src}->${a.dst}`;r.has(s)||(r.add(s),n.push(a))}return{nodes:[...t.keys()],edges:n,layout:e.layout,nodeById:t}}function b5(e,t){return e.edges.filter(r=>r.dst===t)}function Yct(e){let t=new Map;for(let r of e.nodes)t.set(r,[]);for(let r of e.edges)t.get(r.src).push(r.dst);return t}function fB(e){let t=Yct(e);for(let r of t.values())r.sort((n,i)=>n.localeCompare(i));return t}function pB(e){let t=new Map;for(let r of e.nodes)t.set(r,0);for(let r of e.edges)t.set(r.dst,(t.get(r.dst)??0)+1);return t}function mB(e){return[...e.entries()].filter(([,t])=>t===0).map(([t])=>t).sort((t,r)=>t.localeCompare(r))}function T1(e,t=()=>!0){let r=new Map,n=new Map;for(let i of e.nodes)r.set(i,[]),n.set(i,[]);for(let i of e.edges)t(i)&&(n.get(i.src).push(i.dst),r.get(i.dst).push(i.src));return{preds:r,succs:n}}function T5(e,t,r,n){let i=0;for(let s of e.nodes)n?.skipGroups&&e.nodeById.get(s)?.isGroup||(i=Math.max(i,r[s]??0));let a=Array.from({length:i+1},()=>[]);for(let s of t)n?.skipGroups&&e.nodeById.get(s)?.isGroup||a[Math.max(0,r[s]??0)].push(s);return a}function Mf(e){let t=pB(e),r=mB(t),n=[],i=fB(e);for(;r.length;){let a=r.shift();n.push(a);for(let s of i.get(a)??[])if(t.set(s,(t.get(s)??0)-1),(t.get(s)??0)===0){let l=0;for(;l{if(i-n<=1)return 0;let a=n+i>>1,s=r(n,a)+r(a,i),l=n,u=a,h=n;for(;l=i||l{"use strict";o(po,"normalizeGraph");o(b5,"incoming");o(Yct,"buildSuccessorMap");o(fB,"buildSortedSuccessorMap");o(pB,"buildInDegreeMap");o(mB,"sortedZeroInDegreeNodes");o(T1,"buildPredecessorSuccessorMaps");o(T5,"buildLayersFromRanks");o(Mf,"topoSortIfAcyclic");o(Um,"buildLayerIndex");o(C5,"countInversions")});function Fye(e){let t=po(e),r=new Map;for(let d of t.nodes)r.set(d,[]);for(let d of t.edges)r.get(d.src).push(d);for(let d of r.values())d.sort((f,p)=>f.dst===p.dst?f.id.localeCompare(p.id):f.dst.localeCompare(p.dst));let n=Object.create(null);for(let d of t.nodes)n[d]=0;let i=[],a=o(d=>{n[d]=1;for(let f of r.get(d)??[]){let p=f.dst;n[p]===0?a(p):n[p]===1&&i.push(f)}n[d]=2},"dfs"),s=[...t.nodes].sort((d,f)=>d.localeCompare(f));for(let d of s)n[d]===0&&a(d);let l=new Set(i.map(d=>`${d.id}:${d.src}->${d.dst}`)),u=t.edges.map(d=>l.has(`${d.id}:${d.src}->${d.dst}`)?{id:d.id,src:d.dst,dst:d.src,weight:d.weight,ref:d.ref}:d);return{acyclic:{nodes:[...t.nodes],edges:u,layout:t.layout,nodeById:new Map(t.nodeById)},reversed:i}}var zye=F(()=>{"use strict";yu();o(Fye,"removeCycles_DFS")});function jct(e){let t=new Map,r=o(n=>{if(t.has(n))return t.get(n);let i=e.nodeById.get(n);if(!i)return t.set(n,null),null;let a=i.parentId;if(!a)return t.set(n,null),null;let l=r(a)??a;return t.set(n,l),l},"resolve");for(let n of e.nodes)r(n);return t}function cs(e){let t=jct(e);return r=>t.get(r)??null}function w5(e){let t=[];for(let r of e.layout.nodes??[])r.isGroup&&!r.parentId&&t.push(r.id);return[...new Set(t)].reverse()}function k5(e,t){let r=w5(e);if(!t||t.length===0)return r;let n=new Set(r),i=new Set,a=[];for(let s of t)!n.has(s)||i.has(s)||(i.add(s),a.push(s));for(let s of r)i.has(s)||a.push(s);return a}var Ih=F(()=>{"use strict";o(jct,"buildTopLaneMap");o(cs,"createTopLaneResolver");o(w5,"buildTopLaneOrder");o(k5,"resolveTopLaneOrder")});var Gye,Ym,gB,C1=F(()=>{"use strict";Gye={EPSILON:1e-6},Ym={GRAVITY_ITERATIONS:8,MAX_CROSSING_OPTIMIZATION_PASSES:4,DEFAULT_COMPACT_SINGLE_INPUT:!0},gB={DEFAULT_LAYER_GAP:100,DEFAULT_NODE_GAP:40}});function Vye(e,t){let r=po(e),n=t?.laneOf??(()=>null),i=t?.rankHint,{preds:a}=T1(r);for(let C of a.values())C.sort((w,S)=>w.localeCompare(S));let s=Mf(r)??[...r.nodes].sort((C,w)=>C.localeCompare(w)),l=new Map;for(let[C,w]of s.entries())l.set(w,C);let u=new Map,h=new Map;for(let C of r.nodes)h.set(C,[]);for(let C of s){let w=(a.get(C)??[]).filter(S=>u.has(S));if(w.length>0){let S=Xct(C,w,{laneOf:n,rankHint:i,topoIndex:l});u.set(C,S),h.get(S).push(C)}else u.has(C)||u.set(C,null)}for(let C of r.nodes)u.has(C)||u.set(C,null);let d=new Set;for(let C of r.nodes)(u.get(C)??null)===null&&d.add(C);let f=[...d].sort((C,w)=>{let S=l.get(C)??0,R=l.get(w)??0;return S===R?C.localeCompare(w):S-R}),p=Kct(r),m=new Map;for(let[C,w]of p.entries())m.set(C,[...w].sort((S,R)=>S.localeCompare(R)));let g=Zct(m),y=Qct(m),v=new Map;for(let C of r.nodes)v.set(C,[]);for(let C of y)for(let w of C.nodes){let S=v.get(w);S?S.push(C.id):v.set(w,[C.id])}let x=[],b=[],T=new Set,k=o(C=>{if(!T.has(C)){T.add(C),x.push(C);for(let w of h.get(C)??[])k(w);b.push(C)}},"walk");for(let C of f)k(C);for(let C of s)k(C);return{parent:u,children:h,roots:f,componentOf:g,blocks:y,nodeBlocks:v,adjacency:m,preorder:x,postorder:b,topologicalOrder:s}}function Xct(e,t,r){let n=r.laneOf(e);return[...t].sort((a,s)=>{let l=r.laneOf(a),u=r.laneOf(s),h=l!=null&&l===n,d=u!=null&&u===n;if(h!==d)return h?-1:1;let f=r.rankHint?.[a],p=r.rankHint?.[s];if(f!=null&&p!=null&&f!==p)return p-f;let m=r.topoIndex.get(a)??0,g=r.topoIndex.get(s)??0;return m!==g?m-g:a.localeCompare(s)})[0]}function Kct(e){let t=new Map;for(let r of e.nodes)t.set(r,new Set);for(let r of e.edges)t.get(r.src).add(r.dst),t.get(r.dst).add(r.src);return t}function Zct(e){let t=new Map,r=0;for(let n of e.keys()){if(t.has(n))continue;let i=[n];for(;i.length>0;){let a=i.pop();if(!t.has(a)){t.set(a,r);for(let s of e.get(a)??[])t.has(s)||i.push(s)}}r++}return t}function Qct(e){let t=new Map,r=new Map,n=[],i=[],a=0,s=o((l,u)=>{t.set(l,++a),r.set(l,a);for(let h of e.get(l)??[])h!==u&&(t.has(h)?(t.get(h)??0)<(t.get(l)??0)&&(n.push([l,h]),r.set(l,Math.min(r.get(l)??a,t.get(h)??a))):(n.push([l,h]),s(h,l),r.set(l,Math.min(r.get(l)??a,r.get(h)??a)),(r.get(h)??0)>=(t.get(l)??0)&&i.push(Jct(l,h,n,i.length))))},"visit");for(let l of e.keys())t.has(l)||s(l,null);return i}function Jct(e,t,r,n){let i=[],a=new Set;for(;r.length>0;){let s=r.pop();if(i.push(s),a.add(s[0]),a.add(s[1]),s[0]===e&&s[1]===t||s[0]===t&&s[1]===e)break}return{id:n,edges:i,nodes:[...a]}}var Wye=F(()=>{"use strict";yu();o(Vye,"buildDrivingTree");o(Xct,"chooseParent");o(Kct,"buildAdjacency");o(Zct,"assignComponents");o(Qct,"computeBlocks");o(Jct,"popBlock")});function qye(e,t,r){let n=[...e.nodes],i=new Map;for(let[b,T]of n.entries())i.set(T,b);let a=n.length,s=new Array(a).fill(-1),l=new Array(a).fill(0),u=[],h=new Set;for(let b of n){let T=r.parent.get(b)??null,k=i.get(b);k!=null&&T==null&&(s[k]=-1,l[k]=0,h.has(b)||(h.add(b),u.push(b)))}for(;u.length>0;){let b=u.shift(),T=i.get(b);if(T==null)continue;let k=r.children.get(b)??[];for(let C of k){if(h.has(C))continue;let w=i.get(C);w!=null&&(s[w]=T,l[w]=l[T]+1,h.add(C),u.push(C))}}for(let b of n){if(h.has(b))continue;let T=i.get(b);T!=null&&(s[T]=-1,l[T]=0,h.add(b))}let d=Math.max(1,Math.ceil(Math.log2(Math.max(1,a)))+1),f=Array.from({length:d},()=>new Array(a).fill(-1));for(let b=0;b{if(b===-1||T===-1)return-1;l[b]>C&1&&(b=f[C][b],b===-1))return-1;if(b===T)return b;for(let C=d-1;C>=0;C--){let w=f[C][b],S=f[C][T];w===-1||S===-1||w!==S&&(b=w,T=S)}return f[0][b]},"lcaIndex"),m=Array.from({length:a},()=>new Map);for(let b of e.edges){let T=b.src,k=b.dst,C=t[T],w=t[k];if(C==null||w==null||(C>w&&([T,k]=[k,T],[C,w]=[w,C]),C==null||w==null||C===w))continue;let S=i.get(T),R=i.get(k);if(S==null||R==null)continue;let L=p(S,R);if(L===-1)continue;let N=m[L];for(let I=C;I{if(T.size!==0)for(let[k,C]of T)b.set(k,(b.get(k)??0)+C)},"mergeInto"),v=new Set,x=o(b=>{let T=i.get(b);v.add(b);let k=T==null?void 0:m[T],C=k?new Map(k):new Map,w=r.children.get(b)??[];for(let S of w){let R=x(S),L=t[b];if(L!=null){let N=g.get(b);N||(N=new Map,g.set(b,N));let I=R.get(L)??0,_=t[S];_!=null&&_>L&&(I+=1),N.set(S,I)}y(C,R)}return C},"dfs");for(let b of r.roots)v.has(b)||x(b);for(let b of n)v.has(b)||x(b);return g}var Hye=F(()=>{"use strict";o(qye,"computeSubtreeCrossCounts")});function Uye(e,t,r){let n=new Map,i=o(a=>{let s=r[a]??0,l=[...t.get(a)??[]];l.sort(yB(r));for(let u of l){i(u);let h=n.get(u);h!=null&&(s=Math.min(s,h))}n.set(a,s)},"annotate");for(let a of e)i(a);return n}function yB(e){return(t,r)=>{let n=e[t]??0,i=e[r]??0;return n===i?t.localeCompare(r):n-i}}function Yye(e,t,r,n){let i=0;for(let u of t){let h=r[u]??0;h>i&&(i=h)}let a=Array.from({length:i+1},()=>[]),s=new Set,l=o(u=>{if(s.has(u))return;s.add(u);let h=r[u]??0;a[h]||(a[h]=[]),a[h].push(u);for(let d of n(u))l(d)},"emit");for(let u of e)l(u);for(let u of t)if(!s.has(u)){let h=r[u]??0;a[h]||(a[h]=[]),a[h].push(u),s.add(u)}return a}function jye(e){let t=[];for(let r of e){let n=new Set,i=[];for(let a of r)n.has(a)||(n.add(a),i.push(a));t.push(i)}return t}var Xye=F(()=>{"use strict";o(Uye,"annotateMinimumLayers");o(yB,"compareByRankThenId");o(Yye,"emitNodesInTreeOrder");o(jye,"deduplicateLayers")});function eut(e,t,r,n){return i=>{let a=e.get(i)??[];if(a.length===0)return[];let s=t[i]??0,l=[],u=[],h=r.get(i);for(let d of a){let f=n.get(d)??s;f>s?l.push({child:d,min:f}):u.push(d)}return l.sort((d,f)=>d.min===f.min?d.child.localeCompare(f.child):d.min-f.min),u.sort((d,f)=>{let p=h?.get(d)??0,m=h?.get(f)??0;if(p!==m)return p-m;let g=n.get(d)??s,y=n.get(f)??s;return g!==y?g-y:d.localeCompare(f)}),[...l.map(d=>d.child),...u]}}function CT(e,t,r){let n=Vye(e,{rankHint:t,laneOf:r}),{children:i,roots:a}=n;for(let f of e.nodes)i.has(f)||i.set(f,[]);let s=qye(e,t,n),l=[...a].sort(yB(t)),u=Uye(l,i,t),h=eut(i,t,s,u),d=Yye(l,e.nodes,t,h);return d=jye(d),d}var vB=F(()=>{"use strict";Wye();Hye();Xye();o(eut,"createChildOrderer");o(CT,"buildMultitreeLayerOrder")});function tut(e,t,r){let n=new Set(e),i=new Set(t),a=Um(t),s=[];for(let l of r)n.has(l.src)&&i.has(l.dst)&&s.push(a.get(l.dst));return C5(s)}function Kye(e,t,r){let n=[];for(let a of t){let s=r[a.src],l=r[a.dst];if(s==null||l==null||s===l)continue;let u=a.src,h=a.dst,d=s,f=l;s>l&&(u=a.dst,h=a.src,d=l,f=s);for(let p=d;p(r[p]??0)-(r[f]??0));for(let f of d){let p=r[f]??0;if(p===0)continue;let m=0;for(let x of n.get(f)??[])m=Math.max(m,(r[x]??0)+1);if(m>=p)continue;let g=p;r[f]=m;let y=CT(e,r,i),v=Kye(y,e.edges,r);v{"use strict";yu();C1();Ih();vB();o(tut,"countCrossingsBetweenAdjacent");o(Kye,"totalCrossings");o(Zye,"optimizeRanksByCrossings")});function Jye(e,t){let r=cs(e),n=[...e.nodes].sort((i,a)=>(t[i]??0)-(t[a]??0)||i.localeCompare(a));for(let i of n){let a=r(i);if(!a)continue;let s=e.edges.filter(y=>y.src===i);if(s.length===0)continue;let l=!1,u=0;for(let y of s){let v=r(y.dst);v==null||v===a?l=!0:u++}if(u===0||l)continue;let h=0,d=!1;for(let y of e.edges){if(y.dst!==i)continue;let v=r(y.src);v&&(v===a?d=!0:h++)}if(h>0||!d)continue;let f=t[i]??0,p=f+u,m=0;for(let y of e.edges)y.dst===i&&(m=Math.max(m,(t[y.src]??0)+1));let g=Math.max(f,m,p);g!==f&&(t[i]=g)}}var e1e=F(()=>{"use strict";Ih();o(Jye,"adjustCrossLaneSources")});function t1e(e,t){let r=po(e),n=Mf(r)??[...r.nodes].sort(),i=t?.compactSingleInput??!1,a=cs(r),s=Object.create(null);for(let u of n){let h=b5(r,u),d=t?.ignoreCrossLaneEdges?h.filter(f=>{let p=a(f.src),m=a(u);return!p||!m?!0:p===m}):h;if(d.length===0)s[u]=0;else if(i&&d.length===1){let f=d[0].src,p=a(f),m=a(u);p!==m?s[u]=s[f]??0:s[u]=(s[f]??0)+1}else{let f=-1/0;for(let p of d)f=Math.max(f,(s[p.src]??0)+1);s[u]=f===-1/0?0:f}}return(t?.optimizeRanksByCrossings??!1)&&(s=Zye(r,s)),t?.ignoreCrossLaneEdges&&Jye(r,s),{layers:CT(r,s,a),rankOf:s,dummy:new Set}}var r1e=F(()=>{"use strict";yu();Ih();Qye();e1e();vB();o(t1e,"assignLayers_LongestPath")});function n1e(e,t){let r=po(e),i={...t1e(r,{compactSingleInput:t?.compactSingleInput,ignoreCrossLaneEdges:t?.ignoreCrossLaneEdges,optimizeRanksByCrossings:t?.optimizeRanksByCrossings}).rankOf},a=cs(r),{preds:s,succs:l}=T1(r,g=>{if(t?.ignoreCrossLaneEdges){let y=a(g.src),v=a(g.dst);if(y&&v&&y!==v)return!1}return!0}),u=Mf(r)??[...r.nodes],h=[...u].reverse(),d=o((g,y)=>{let v=0;for(let T of s.get(g)??[])v=Math.max(v,(i[T]??0)+1);let x=Number.POSITIVE_INFINITY,b=l.get(g)??[];return b.length>0&&(x=Math.min(...b.map(T=>(i[T]??0)-1))),Number.isFinite(x)||(x=Math.max(v,y)),Math.min(Math.max(y,v),x)},"clampFeasible"),f=Ym.GRAVITY_ITERATIONS,p=o(g=>{let y=!1;for(let v of g){let x=s.get(v)??[],b=l.get(v)??[];if(x.length===0&&b.length===0)continue;let T=x.length>0?x.reduce((S,R)=>S+(i[R]??0)+1,0)/x.length:i[v]??0,k=b.length>0?b.reduce((S,R)=>S+(i[R]??0)-1,0)/b.length:i[v]??0,C=Math.round((T+k)/2),w=d(v,C);w!==i[v]&&(i[v]=w,y=!0)}return y},"relaxOrder");for(let g=0;g0){let v=Math.min(...y.map(x=>(i[x]??0)-1));(i[g]??0)>v&&(i[g]=v)}}return{layers:T5(r,u,i),rankOf:i,dummy:new Set}}var i1e=F(()=>{"use strict";yu();Ih();C1();r1e();o(n1e,"assignLayers_Gravity")});function rut(e){let t=pB(e),r=fB(e),n=mB(t),i=[];for(;n.length>0;){let a=[];for(let s of n){i.push(s);for(let l of r.get(s)??[])t.set(l,(t.get(l)??0)-1),(t.get(l)??0)===0&&a.push(l)}n=a.sort((s,l)=>s.localeCompare(l))}return i.length===e.nodes.length?i:null}function a1e(e,t){let r=po(e),n=t?.direction==="LR"?rut(r)??[...r.nodes].sort():Mf(r)??[...r.nodes].sort(),i=cs(r),a=o(d=>i(d)??d,"laneOf"),s=Object.create(null),l=new Map,u=o((d,f)=>t?.ignoreCrossLaneEdges??!0?a(d)===a(f)?1:0:1,"edgeWeight");for(let d of n){if(r.nodeById.get(d)?.isGroup)continue;let p=b5(r,d),m=0;if(p.length>0)for(let x of p){let b=x.src,T=s[b]??0;m=Math.max(m,T+u(b,d))}let g=a(d),y=l.get(g)??0,v=Math.max(m,y);s[d]=v,l.set(g,v+1)}return{layers:T5(r,n,s,{skipGroups:!0}),rankOf:s,dummy:new Set}}var s1e=F(()=>{"use strict";yu();Ih();o(rut,"topoSortByGenerationIfAcyclic");o(a1e,"assignLayers_LaneAwareCompact")});function o1e(e,t){let r=po(t),{rankOf:n}=e,i=e.layers.map(m=>[...m]),a=new Set(e.dummy?[...e.dummy]:[]),s=0,l=new Map(r.nodeById),u=o(m=>{let g=`placeholder-${s++}`,y={id:g,isGroup:!1,isDummy:!0,width:0,height:0};for(l.set(g,y),a.add(g);i.length<=m;)i.push([]);return i[m].push(g),n[g]=m,g},"addDummyAt"),h=[...r.edges].sort((m,g)=>m.id===g.id?m.src===g.src?m.dst.localeCompare(g.dst):m.src.localeCompare(g.src):m.id.localeCompare(g.id)),d=[];for(let m of h){let g=n[m.src]??0,y=n[m.dst]??0;if(y-g<=1){d.push(m);continue}let v=m.src;for(let b=g+1,T=0;b!r.nodes.includes(m))],edges:d,layout:r.layout,nodeById:l};return{layering:{layers:i,rankOf:n,dummy:a},graphWithDummies:p}}var l1e=F(()=>{"use strict";yu();o(o1e,"makeProperLayering")});function c1e(e){let t=e.length;if(t===0)return Number.POSITIVE_INFINITY;let r=[...e].sort((n,i)=>n-i);return t%2===1?r[(t-1)/2]:.5*(r[t/2-1]+r[t/2])}function u1e(e){return e.length===0?Number.POSITIVE_INFINITY:e.reduce((r,n)=>r+n,0)/e.length}function nut(e,t,r,n){let i=new Map;for(let a of e)i.set(a,[]);for(let a of r)n==="down"?t.has(a.src)&&i.has(a.dst)&&i.get(a.dst).push(t.get(a.src)):t.has(a.dst)&&i.has(a.src)&&i.get(a.src).push(t.get(a.dst));return i}function iut(e,t,r){let n=r.get(e)??0,i=r.get(t)??0;return n!==i?n-i:e.localeCompare(t)}function h1e(e,t,r){let n=new Set(e),i=new Set(t),a=Um(e),s=Um(t),l=[];for(let h of r)n.has(h.src)&&i.has(h.dst)&&l.push({u:a.get(h.src),v:s.get(h.dst)});l.sort((h,d)=>h.u===d.u?h.v-d.v:h.u-d.u);let u=l.map(h=>h.v);return C5(u)}function xB(e,t,r){return[...e].sort((n,i)=>{let a=c1e(t.get(n)??[]),s=c1e(t.get(i)??[]);return a===s?iut(n,i,r):isFinite(a)?isFinite(s)?a-s:-1:1})}function d1e(e,t,r,n,i,a){let s=Um(e),l=Um(t),u=nut(t,s,r,n);if(!i||!a||a.length===0)return xB(t,u,l);let h=new Map;for(let p of t){let m=i(p),g=h.get(m)??[];g.push(p),h.set(m,g)}let d=[];for(let p of a){let m=h.get(p);if(!m||m.length===0)continue;let g=xB(m,u,l);d.push(...g)}let f=h.get(null);if(f&&f.length>0){let p=xB(f,u,l);for(let m of p){let g=u1e(u.get(m)??[]),y=d.length;if(isFinite(g))for(let[v,x]of d.entries()){let b=u1e(u.get(x)??[]);if(gs.has(y.src)&&l.has(y.dst)),d=u?r.filter(y=>l.has(y.src)&&u.has(y.dst)):void 0,f=o(y=>{let v=h1e(e,y,h);return d&&n&&(v+=h1e(y,n,d)),v},"crossingScore"),p=i?new Map:null;if(i&&p)for(let y of t)p.set(y,i(y));let m=!0,g=f(a);for(;m;){m=!1;for(let y=0;y+1[...l]),i=t.edges,a=cs(t),s=k5(t,r?.laneOrder);for(let l=0;l<3;l++){for(let u=1;u=0;u--)n[u]=d1e(n[u+1],n[u],i,"up",a,s),n[u]=f1e(n[u+1],n[u],i,n[u-1],a)}return{layers:n}}var m1e=F(()=>{"use strict";yu();Ih();o(c1e,"median");o(u1e,"barycenter");o(nut,"neighborPositionsFor");o(iut,"currentOrderTieBreak");o(h1e,"countCrossingsBetweenAdjacent");o(xB,"sortByHeuristic");o(d1e,"reorderLayer");o(f1e,"transposeImprove");o(p1e,"orderLayers")});function g1e(e,t,r){let n=r?.layerGap??gB.DEFAULT_LAYER_GAP,i=r?.nodeGap??gB.DEFAULT_NODE_GAP,a=r?.laneGap??i*2,s=r?.direction??"TB",l=s==="LR"||s==="RL",u=e.layers,h=Object.create(null),d=Object.create(null),f=o(N=>t.nodeById.get(N),"getNode"),p=o(N=>f(N)?.width??0,"getWidth"),m=o(N=>f(N)?.height??0,"getHeight"),g=cs(t),y=k5(t,r?.laneOrder),v=u.map(N=>N.reduce((I,_)=>Math.max(I,m(_)),0)),x=[];if(l)for(let N=0;N+1Math.max(O,p($)),0),_=u[N+1].reduce((O,$)=>Math.max(O,p($)),0),A=v[N],M=v[N+1],D=A/2+M/2,P=(I+_)/2,B=Math.max(0,P-D-n);x.push(B)}let b=new Set;for(let N of u)for(let I of N)b.add(g(I));let T=b.has(null),k=y.filter(N=>b.has(N)),C=[...T?[null]:[],...k],w=Object.create(null);for(let N of k)w[N]=0;T&&(w.null=0);for(let N of u){let I=Object.create(null),_=[];for(let A of N){let M=g(A);M===null?_.push(A):(I[M]||=[]).push(A)}for(let[A,M]of Object.entries(I)){let D=M.reduce((P,B)=>P+p(B),0)+i*Math.max(0,M.length-1);w[A]=Math.max(w[A]??0,D)}if(T&&_.length){let A=_.reduce((M,D)=>M+p(D),0)+i*Math.max(0,_.length-1);w.null=Math.max(w.null??0,A)}}let S=new Map;{let N=C.map(A=>(A===null?w.null:w[A])??0),_=-(N.reduce((A,M)=>A+M,0)+a*Math.max(0,C.length-1))/2;for(let A=0;Ap(G)),$=O.reduce((G,z)=>G+z,0)+i*(P.length-1),V=B-$/2;for(let[G,z]of P.entries()){let W=O[G];h[z]=V+W/2,d[z]=R+_/2,V+=W+i}}}let M=x[N]??0;R+=_+n+M}let L=new Map;for(let N of t.edges){let I=N.ref.id;L.has(I)||L.set(I,[]),L.get(I).push(N)}for(let[,N]of L){if(N.length===0)continue;let I=N[0].ref,_=I.start,A=I.end;if(_==null||A==null)continue;let M=Math.round(((h[_]??0)+(h[A]??0))/2),D=new Set;for(let P of N)D.add(P.src),D.add(P.dst);for(let P of D){if(P===_||P===A)continue;t.nodeById.get(P)?.isDummy&&(h[P]=M)}}return{x:h,y:d}}var y1e=F(()=>{"use strict";C1();Ih();o(g1e,"assignCoordinates")});function aut(e){let t=2166136261;for(let r=0;r>>0}function sut(e){let t=e>>>0;return()=>{t+=1831565813;let r=t;return r=Math.imul(r^r>>>15,r|1),r^=r+Math.imul(r^r>>>7,r|61),((r^r>>>14)>>>0)/4294967296}}function out(e,t){let r=[...e],n=sut(t);for(let i=r.length-1;i>0;i--){let a=Math.floor(n()*(i+1));[r[i],r[a]]=[r[a],r[i]]}return r}function lut(e,t){let r=0;for(let[n,i]of e.entries())r+=Math.abs(n-(t.get(i)??n));return r}function v1e(e,t){let r=new Map;for(let[i,a]of e.entries())r.set(a,i);let n=0;for(let{a:i,b:a,weight:s}of t){let l=r.get(i),u=r.get(a);l==null||u==null||(n+=s*Math.abs(l-u))}return n}function cut(e){let t=w5(e);if(t.length<2)return[];let r=new Map(t.map((a,s)=>[a,s])),n=cs(e),i=new Map;for(let a of e.layout.edges??[]){if(a.isLayoutOnly)continue;let s=typeof a.start=="string"?a.start:void 0,l=typeof a.end=="string"?a.end:void 0;if(!s||!l||!e.nodeById.has(s)||!e.nodeById.has(l))continue;let u=n(s),h=n(l);if(!u||!h||u===h)continue;let d=r.get(u),f=r.get(h);if(d==null||f==null)continue;let[p,m]=d<=f?[u,h]:[h,u],g=`${p}\0${m}`,y=i.get(g);y?y.weight++:i.set(g,{a:p,b:m,weight:1})}return[...i.values()]}function x1e(e,t,r){let n=[...e],i=v1e(n,t),a=!0,s=0,l=Math.max(1,n.length);for(;a&&si.a===a.a?i.b.localeCompare(a.b):i.a.localeCompare(a.a)).map(({a:i,b:a,weight:s})=>`${i}:${a}:${s}`).join("|");return aut(`${e.join("|")}#${n}#${r}`)}function b1e(e,t={}){let r=w5(e);if(r.length<2)return r;let n=cut(e);if(n.length===0)return r;let i=new Map(r.map((l,u)=>[l,u])),a=x1e(r,n,i),s=Math.max(0,t.restarts??bB);for(let l=0;l{"use strict";Ih();bB=8;o(aut,"hashString");o(sut,"mulberry32");o(out,"deterministicShuffle");o(lut,"sourceDistance");o(v1e,"laneArrangementCost");o(cut,"buildWeightedLaneEdges");o(x1e,"greedySwitch");o(uut,"isBetterCandidate");o(hut,"seedForRestart");o(b1e,"optimizeTopLaneOrder")});function C1e(e,t){let r=t?.ignoreCrossLaneEdges??!0,n=t?.optimizeRanksByCrossings??!0,i=po(e),a=t?.automaticLaneOrdering?b1e(i,{restarts:bB}):void 0,s=Fye(i),l=s.acyclic,u=r?a1e(l,{compactSingleInput:t?.compactSingleInput??Ym.DEFAULT_COMPACT_SINGLE_INPUT,ignoreCrossLaneEdges:!0,direction:t?.direction}):n1e(l,{compactSingleInput:t?.compactSingleInput??Ym.DEFAULT_COMPACT_SINGLE_INPUT,ignoreCrossLaneEdges:!1,optimizeRanksByCrossings:n}),{layering:h,graphWithDummies:d}=o1e(u,l),f=p1e(h,d,{laneOrder:a}),p=g1e(f,d,{layerGap:t?.layerGap,nodeGap:t?.nodeGap,direction:t?.direction,laneOrder:a});return{acyclic:l,reversed:s.reversed,layering:h,ordered:f,coordinates:p}}var w1e=F(()=>{"use strict";yu();zye();i1e();s1e();l1e();m1e();y1e();C1();T1e();o(C1e,"sugiyamaLayout")});function k1e(e,t,r){let n=e.x??0,i=e.y??0,a=t.x-n,s=t.y-i,l=Math.abs(a),u=Math.abs(s);return lXr&&u*3>=l?s>0?"bottom":"top":l>Xr?a>0?"right":"left":r}function S1e(e,t){return Math.abs(e.to-t.from)re.isGroup&&!re.parentId);for(let re of h){let J={id:re.id},se=o(ge=>{s.set(ge.id,J),r.filter(Te=>Te.parentId===ge.id).forEach(se)},"assignLane");se(re)}let d=r.filter(re=>!re.isGroup&&!re.isEdgeLabel).map(re=>{let J=re.width??10,se=re.height??10,ge=re.x??0,Te=re.y??0,we=dut;return{nodeId:re.id,minX:ge-J/2-we,maxX:ge+J/2+we,minY:Te-se/2-we,maxY:Te+se/2+we,visualXHalfExtent:u?se/2+we:J/2+we}}),f=o((re,J,se,ge)=>{let Te=l.find(we=>we.orientation===re&&Math.abs(we.coord-J)<1);return Te||(Te={id:`pipe-${re}-${J.toFixed(0)}`,orientation:re,coord:J,spanMin:se,spanMax:ge,tracks:[]},l.push(Te)),Te.spanMin=Math.min(Te.spanMin,se),Te.spanMax=Math.max(Te.spanMax,ge),Te},"getOrAddPipe"),p=o((re,J)=>{let se=re.width??10,ge=re.height??10,Te=re.x??0,we=re.y??0;switch(J){case"top":return{x:Te,y:we-ge/2};case"bottom":return{x:Te,y:we+ge/2};case"left":return{x:Te-se/2,y:we};case"right":return{x:Te+se/2,y:we}}},"portForSide"),m=o((re,J,se)=>p(re,k1e(re,J,se?"bottom":"top")),"getOrthogonalPort"),g=[],y=[],v=new Set,x=1e3,b=o((re,J,se)=>{if(g.length===0)return 0;let ge=Math.abs(J.y-se.y)ne||q.from-Xr<=Me&&q.to+Xr>=Me&&(we+=x)}else if(Te){let Me=J.x,ve=Math.min(J.y,se.y)-Xr,ne=Math.max(J.y,se.y)+Xr;if(ne<=ve)return 0;for(let q of g)q.edgeIndex===re||q.orientation!=="horizontal"||q.pipe.coordne||q.from-Xr<=Me&&q.to+Xr>=Me&&(we+=x)}return we},"crossingPenalty"),T=i.map((re,J)=>{if(!re.start||!re.end)return{idx:J,crossLane:0,dx:0,dy:0};let se=a.get(re.start),ge=a.get(re.end),Te=s.get(re.start),we=s.get(re.end),Me=Te&&we&&Te.id!==we.id?1:0,ve=se&&ge?Math.abs((ge.x??0)-(se.x??0)):0,ne=se&&ge?Math.abs((ge.y??0)-(se.y??0)):0;return{idx:J,crossLane:Me,dx:ve,dy:ne}}).sort((re,J)=>{if(re.crossLane!==J.crossLane)return J.crossLane-re.crossLane;let se=re.dx+re.dy,ge=J.dx+J.dy;return Math.abs(se-ge)>1?se-ge:re.idx-J.idx}).map(re=>re.idx),k=o((re,J,se,ge)=>{let Te=Math.min(re.x,J.x),we=Math.max(re.x,J.x),Me=Math.min(re.y,J.y),ve=Math.max(re.y,J.y);return!!d.find(q=>se&&q.nodeId===se||ge&&q.nodeId===ge?!1:Math.abs(re.x-J.x)>Xr?q.minYre.y&&q.maxX>Te&&q.minXre.x&&q.maxY>Me&&q.minYk1e(re,J,"bottom"),"determineSide"),R=new Map;for(let[re,J]of i.entries()){if(!J.start||!J.end||J.start===J.end||J.points&&J.points.length>0)continue;let se=a.get(J.start),ge=a.get(J.end);if(!se||!ge)continue;let Te=(ge.x??0)-(se.x??0),we=(ge.y??0)-(se.y??0);R.set(re,{edgeIdx:re,srcId:J.start,dstId:J.end,srcSide:S(se,{x:ge.x??0,y:ge.y??0}),dstSide:S(ge,{x:se.x??0,y:se.y??0}),absDx:Math.abs(Te),absDy:Math.abs(we),dxSign:Math.sign(Te),dySign:Math.sign(we)})}let L=o(re=>re.srcSide==="top"||re.srcSide==="bottom"?re.absDx===0?1/0:re.absDy/re.absDx:re.absDy===0?1/0:re.absDx/re.absDy,"preferenceStrength"),N=o(re=>re.srcSide==="top"||re.srcSide==="bottom"?re.dxSign>=0?"right":"left":re.dySign>=0?"bottom":"top","secondarySide"),I=new Map;for(let re of R.values()){let J=`${re.srcId}:${re.srcSide}`;I.has(J)||I.set(J,[]),I.get(J).push(re)}let _=new Map,A=o((re,J)=>`${re}:${J}`,"loadKey");for(let re of R.values())_.set(A(re.srcId,re.srcSide),(_.get(A(re.srcId,re.srcSide))??0)+1),_.set(A(re.dstId,re.dstSide),(_.get(A(re.dstId,re.dstSide))??0)+1);for(let re of I.values())if(!(re.length<2)){re.sort((J,se)=>{let ge=L(J),Te=L(se);return Math.abs(ge-Te)>1e-9?Te-ge:J.edgeIdx-se.edgeIdx});for(let J=1;J=Te||(_.set(A(se.srcId,se.srcSide),Te-1),_.set(A(se.srcId,ge),we+1),se.srcSide=ge)}}let M=o(re=>{let J=re?.shape;return J==="question"||J==="diamond"},"isDiamondNode"),D=new Map;for(let re of R.values())D.has(re.dstId)||D.set(re.dstId,new Set),D.get(re.dstId).add(re.dstSide);for(let re of R.values()){if(!M(a.get(re.srcId)))continue;let J=D.get(re.srcId);if(!J?.has(re.srcSide))continue;let se=N(re);if(J.has(se)||(_.get(A(re.srcId,se))??0)>0)continue;let ge=_.get(A(re.srcId,re.srcSide))??0;_.set(A(re.srcId,re.srcSide),Math.max(0,ge-1)),_.set(A(re.srcId,se),1),re.srcSide=se}for(let re of R.values()){let{edgeIdx:J,srcId:se,dstId:ge,srcSide:Te,dstSide:we}=re,Me=a.get(se),ve=a.get(ge),ne=`${se}:${Te}:src`,q=Te==="top"||Te==="bottom"?ve.x??0:ve.y??0;C.has(ne)||C.set(ne,[]),C.get(ne).push({edgeIdx:J,oppositeCoord:q});let he=`${ge}:${we}:dst`,X=we==="top"||we==="bottom"?Me.x??0:Me.y??0;C.has(he)||C.set(he,[]),C.get(he).push({edgeIdx:J,oppositeCoord:X})}let P=new Map,B=8;for(let[re,J]of C){if(J.length<2)continue;J.sort((Be,Ne)=>Be.oppositeCoord-Ne.oppositeCoord);let se=re.split(":"),ge=se.slice(0,-2).join(":"),Te=se[se.length-2],we=se[se.length-1],Me=a.get(ge);if(!Me)continue;let ne=Te==="left"||Te==="right"?Me.height??10:Me.width??10,q=Me.shape,X=q==="question"||q==="diamond"?ne*.3:ne,K=Math.min(20,Math.max(B,X/(J.length+1))),_e=-(K*(J.length-1))/2;for(let[Be,Ne]of J.entries()){let He=_e+Be*K,$e=`${Ne.edgeIdx}:${we}`;P.set($e,He)}}let O=o(re=>!!i[re]?.labelNodeId,"edgeHasLabelNode"),$=o((re,J)=>re?(C.get(`${re}:${J}:src`)??[]).some(({edgeIdx:se})=>O(se))||(C.get(`${re}:${J}:dst`)??[]).some(({edgeIdx:se})=>O(se)):!1,"faceHasLabelNode"),V=o((re,J,se)=>J==="top"||J==="bottom"?{x:re.x+se,y:re.y}:{x:re.x,y:re.y+se},"applyPortOffset"),G=o((re,J,se)=>{let ge=R.get(re),Te={x:se.x??0,y:se.y??0},we={x:J.x??0,y:J.y??0},Me=ge?.srcSide??S(J,Te),ve=ge?.dstSide??S(se,we),ne=ge?p(J,ge.srcSide):m(J,Te,!0),q=ge?p(se,ge.dstSide):m(se,we,!1),he=P.get(`${re}:src`),X=P.get(`${re}:dst`);return he!==void 0&&(ne=V(ne,Me,he)),X!==void 0&&(q=V(q,ve,X)),{pSrcPort:ne,pDstPort:q,srcSide:Me,dstSide:ve}},"portsForEdge");for(let re of T){let J=i[re];if(y[re]=[],!J.start||!J.end||J.points&&J.points.length>0||J.start===J.end)continue;let se=a.get(J.start),ge=a.get(J.end);if(!se||!ge)continue;let{pSrcPort:Te,pDstPort:we,srcSide:Me,dstSide:ve}=G(re,se,ge),ne={...Te},q={...we},he=Me==="top"||Me==="bottom",X=ve==="top"||ve==="bottom";if(he){let We=Te.y>(se.y??0);ne.y=We?Te.y+Cl:Te.y-Cl}else{let We=Te.x>(se.x??0);ne.x=We?Te.x+Cl:Te.x-Cl}if(X){let We=we.y>(ge.y??0);q.y=We?we.y+Cl:we.y-Cl}else{let We=we.x>(ge.x??0);q.x=We?we.x+Cl:we.x-Cl}let fe=o((We,rt)=>{for(let yt of d)if(!rt.includes(yt.nodeId)&&We.x>yt.minX&&We.xyt.minY&&We.y{if(Ht){let Er=We.y>(rt.y??0);return{x:(yt.x??0)>=We.x?Yt.maxX+jm:Yt.minX-jm,y:Er?Yt.maxY+w1:Yt.minY-w1,leavesPositiveSide:Er}}let pr=We.x>(rt.x??0),Hr=(yt.y??0)>=We.y;return{x:pr?Yt.maxX+jm:Yt.minX-jm,y:Hr?Yt.maxY+w1:Yt.minY-w1,leavesPositiveSide:pr}},"obstacleDetour"),qe=[],_e=[J.start,J.end],Be=fe(ne,_e);if(Be.inside&&Be.obstacle){let We=Be.obstacle;if(he){let rt=K(Te,se,ge,We,!0);ne.x=rt.x,ne.y=rt.y;let yt=rt.leavesPositiveSide?Math.min(We.minY-2,Te.y+Cl):Math.max(We.maxY+2,Te.y-Cl);qe=[{x:Te.x,y:yt},{x:rt.x,y:yt},{x:rt.x,y:rt.y}]}else{let rt=K(Te,se,ge,We,!1),yt=rt.leavesPositiveSide?Math.min(We.minX-2,Te.x+Cl):Math.max(We.maxX+2,Te.x-Cl);ne.x=rt.x,ne.y=rt.y,qe=[{x:yt,y:Te.y},{x:yt,y:rt.y},{x:rt.x,y:rt.y}]}}let Ne=[],He=fe(q,_e);if(He.inside&&He.obstacle){let We=He.obstacle;if(X){let rt=K(we,ge,se,We,!0);q.x=rt.x,q.y=rt.y,Ne=[{x:rt.x,y:rt.y},{x:we.x,y:rt.y}]}else{let rt=K(we,ge,se,We,!1);q.x=rt.x,q.y=rt.y,Ne=[{x:rt.x,y:rt.y},{x:rt.x,y:we.y}]}}if(qe.length===0&&Ne.length===0){let We=jm,rt=Math.abs(ne.x-q.x)1||pr>1,Er=w.get(J.start??"")??0,kt=w.get(J.end??"")??0,Ct=Ht>1&&$(J.start,Me)||pr>1&&$(J.end,ve),Ot=Ht<=1||Er<=2,Ft=pr<=1||kt<=2;if((rt||yt)&&!Yt&&(!Hr||Hr&&!Ct&&Ot&&Ft)&&!k(Te,we,J.start,J.end)){J.points=[{...Te},{...ne},{...q},{...we}],v.add(re);let Se=yt?"horizontal":"vertical",ti=yt?Te.y:Te.x,Ie=yt?Math.min(Te.x,we.x):Math.min(Te.y,we.y),Nr=yt?Math.max(Te.x,we.x):Math.max(Te.y,we.y),Pa={id:`fast-path-${Se}-${ti.toFixed(0)}-${re}`,orientation:Se,coord:ti,spanMin:Ie,spanMax:Nr,tracks:[]};g.push({edgeIndex:re,segmentIndex:0,orientation:Se,pipe:Pa,trackIndex:0,from:Ie,to:Nr});continue}}let $e=f("vertical",ne.x,ne.y,ne.y);ne.x=$e.coord;let Xe=f("vertical",q.x,q.y,q.y);q.x=Xe.coord;let Fe=Math.min(ne.x,q.x)-50,Ke=Math.max(ne.x,q.x)+50,xe=Math.min(ne.y,q.y)-50,mt=Math.max(ne.y,q.y)+50;for(let We of d){let rt=Math.min(ne.x,q.x),yt=Math.max(ne.x,q.x),Yt=Math.min(ne.y,q.y),Ht=Math.max(ne.y,q.y);We.minXrt&&We.minYYt&&(Fe=Math.min(Fe,We.minX-S5),Ke=Math.max(Ke,We.maxX+S5),xe=Math.min(xe,We.minY-S5),mt=Math.max(mt,We.maxY+S5))}for(let We of d){if(We.maxXKe||We.maxYmt)continue;let rt=jm;f("horizontal",We.minY-rt,Fe,Ke),f("horizontal",We.maxY+rt,Fe,Ke);let yt=w1;f("vertical",We.minX-yt,xe,mt),f("vertical",We.maxX+yt,xe,mt)}f("horizontal",ne.y,Fe,Ke),f("horizontal",q.y,Fe,Ke);let Le=l.filter(We=>We.orientation==="horizontal"&&We.coord>=xe&&We.coord<=mt),ft=l.filter(We=>We.orientation==="vertical"&&We.coord>=Fe&&We.coord<=Ke),wt=o((We,rt)=>`${We.toFixed(1)},${rt.toFixed(1)}`,"getKey"),zt=wt(ne.x,ne.y),St=wt(q.x,q.y),At=new Map,bt=new Map,me=new Map,lt=new Set,gt=[];At.set(zt,0),me.set(zt,"n"),gt.push({key:zt,f:Math.hypot(q.x-ne.x,q.y-ne.y),pt:ne}),lt.add(zt);let Ze=[],Ee=o((We,rt)=>k(We,rt,J.start,J.end),"checkSegmentBlocked"),tt={x:q.x,y:ne.y},at=Ee(ne,tt),ot=Ee(tt,q),Wt=at||ot,Bt={x:ne.x,y:q.y},qt=Ee(ne,Bt),vr=Ee(Bt,q);if(Wt?qt||vr||(Math.abs(ne.x-q.x)0;){gt.sort((kt,Ct)=>kt.f-Ct.f);let We=gt.shift();if(lt.delete(We.key),We.key===St){let kt=St,Ct=q;for(Ze=[Ct];bt.has(kt);){let Ot=bt.get(kt);Ze.unshift(Ot),Ct=Ot,kt=wt(Ot.x,Ot.y)}break}let rt=We.pt.x,yt=We.pt.y,Yt=ft.sort((kt,Ct)=>kt.coord-Ct.coord),Ht=Yt.findIndex(kt=>Math.abs(kt.coord-rt)<1),pr=Le.sort((kt,Ct)=>kt.coord-Ct.coord),Hr=pr.findIndex(kt=>Math.abs(kt.coord-yt)<1),Er=[];Ht>0&&Er.push({x:Yt[Ht-1].coord,y:yt}),Ht>=0&&Ht0&&Er.push({x:rt,y:pr[Hr-1].coord}),Hr>=0&&Hrjs.nodeId===J.start||js.nodeId===J.end?!1:Ct!==Ot?js.minYyt&&js.maxX>Ct&&js.minXrt&&js.maxY>Ft&&js.minY10&&$0<-5||Ku<-10&&$0>5)&&(Nr=Math.abs($0)*100),(Pa>10&&B0<-5||Pa<-10&&B0>5)&&(Nr+=Math.abs(B0)*50);let fk=0,Pi=me.get(We.key)??"n",Mc=Math.abs(B0)>Xr?"h":"v";Pi!=="n"&&Pi!==Mc&&(fk=50);let Px=ti+Ie+Nr+fk,Td=(At.get(We.key)??1/0)+Px,pk=Math.abs(q.x-kt.x)+Math.abs(q.y-kt.y);if(Td<(At.get(Se)??1/0))if(bt.set(Se,We.pt),At.set(Se,Td),me.set(Se,Mc),!lt.has(Se))gt.push({key:Se,f:Td+pk,pt:kt}),lt.add(Se);else{let js=gt.findIndex(LD=>LD.key===Se);js!==-1&&(gt[js].f=Td+pk)}}}if(Ze.length===0&&(Ze=[ne,{x:ne.x,y:q.y},q]),Ze.length>4){let We=Ze[0],rt=Ze[Ze.length-1],yt=Math.min(We.x,rt.x),Yt=Math.max(We.x,rt.x),Ht=Math.min(We.y,rt.y),pr=Math.max(We.y,rt.y);for(let Ft of Ze)yt=Math.min(yt,Ft.x),Yt=Math.max(Yt,Ft.x),Ht=Math.min(Ht,Ft.y),pr=Math.max(pr,Ft.y);let Hr=Yt>Math.max(We.x,rt.x),Er=ytIe.minXRt&&Ie.minYgr);if(ti.length>0){let Ie=Math.max(We.x,rt.x);for(let Nr of ti){let Pa=(Nr.minX+Nr.maxX)/2;if(Nr.visualXHalfExtent===void 0||isNaN(Nr.visualXHalfExtent))continue;let Ku=Pa+Nr.visualXHalfExtent+Ft;Ie=Math.max(Ie,Ku)}isNaN(Ie)||(Yt=Ie)}}if(Er){let Rt=d.filter(gr=>gr.minXMath.min(We.y,rt.y));if(Rt.length>0){let gr=Math.min(We.x,rt.x);for(let Se of Rt){let Ie=(Se.minX+Se.maxX)/2-Se.visualXHalfExtent-Ft;gr=Math.min(gr,Ie)}yt=gr}}}let kt=o(Ft=>{let Rt=rt.y>We.y,gr=d.filter(Ie=>{let Nr=Math.min(We.x,rt.x)Ie.minX,Pa=Math.min(We.y,rt.y)Ie.minY;return Nr&&Pa}),Se=gr;if(u&&gr.length>0){let Ie=gr.filter(Nr=>Nr.minXFt);Ie.length>0&&(Se=Ie)}if(Se.length===0)return rt.y;let ti=jm;if(Rt){let Nr=Math.max(...Se.map(Pa=>Pa.maxY))+ti;if(NrPa.minY))-ti;if(Nr>rt.y+Xr)return Nr}return rt.y},"findBestReturnY"),Ct=o(Ft=>{let Rt=kt(Ft),gr={x:Ft,y:We.y},Se={x:Ft,y:Rt},ti={x:rt.x,y:Rt},Ie=Ee(We,gr),Nr=Ee(gr,Se),Pa=Ee(Se,ti),Ku=Rt!==rt.y?Ee(ti,rt):!1;return!Ie&&!Nr&&!Pa&&!Ku?Math.abs(Rt-rt.y)=3){let We=De[De.length-1],rt=De[De.length-2],yt=De[De.length-3],Yt=Math.abs(yt.y-rt.y)Math.abs(We.x-yt.x)&&De.splice(-2,1)}else if(Ht){let pr=Math.sign(rt.y-yt.y),Hr=Math.sign(We.y-yt.y);pr!==0&&pr===Hr&&Math.abs(rt.y-yt.y)>Math.abs(We.y-yt.y)&&De.splice(-2,1)}}let it=[De[0]];for(let We=1;Wert.x,pr=Yt.x>yt.x;if(Ht!==pr){it.push(yt);continue}continue}if(Math.abs(rt.x-yt.x)rt.y,pr=Yt.y>yt.y;if(Ht!==pr){it.push(yt);continue}continue}it.push(yt)}it.push(De[De.length-1]);for(let We=0;Were.from{let Te=!ge.segments.some(Me=>(Me.edgeIndex!==J.edgeIndex||Me.segmentIndex!==J.segmentIndex)&&z(Me,re)),we=!se.segments.some(Me=>(Me.edgeIndex!==re.edgeIndex||Me.segmentIndex!==re.segmentIndex)&&z(Me,J));return Te&&we?(re.trackIndex=ge.index,J.trackIndex=se.index,se.segments=[...se.segments.filter(Me=>Me.edgeIndex!==re.edgeIndex||Me.segmentIndex!==re.segmentIndex),{edgeIndex:J.edgeIndex,segmentIndex:J.segmentIndex,from:J.from,to:J.to}],ge.segments=[...ge.segments.filter(Me=>Me.edgeIndex!==J.edgeIndex||Me.segmentIndex!==J.segmentIndex),{edgeIndex:re.edgeIndex,segmentIndex:re.segmentIndex,from:re.from,to:re.to}],!0):!1},"trySwapSegmentsAcrossTracks"),H=o(re=>{let J=re.tracks.length;return re.tracks[J]={index:J,coord:re.coord,segments:[]},J},"createNewTrack"),j=o((re,J)=>{let se=re.pipe.tracks[re.trackIndex];se.segments=se.segments.filter(Te=>Te.edgeIndex!==re.edgeIndex||Te.segmentIndex!==re.segmentIndex),re.trackIndex=J,re.pipe.tracks[J].segments.push({edgeIndex:re.edgeIndex,segmentIndex:re.segmentIndex,from:re.from,to:re.to})},"moveSegmentToTrack"),Q=o((re,J)=>{let se=y[re.edgeIndex];for(let ge of se){let Te=g[ge];Te.pipe===re.pipe&&j(Te,J)}},"moveSegmentChainToTrack"),U=o(re=>{let J=y[re.edgeIndex],se=J.indexOf(g.indexOf(re)),ge=[];return se>0&&ge.push(g[J[se-1]]),se{if(re.orientation===J.orientation)return!1;let se=re.orientation==="horizontal"?re:J,ge=re.orientation==="horizontal"?J:re;return ge.pipe.coord>se.from&&ge.pipe.coordge.from&&se.pipe.coord{for(let se of re.tracks)if(!se.segments.some(Te=>(Te.edgeIndex!==J.edgeIndex||Te.segmentIndex!==J.segmentIndex)&&z(Te,J)))return se.index;return-1},"findAvailableTrack"),le=o((re,J)=>{if(re.trackIndex===J.trackIndex)return z(re,J);let se=U(re),ge=U(J);return se.some(Te=>ge.some(we=>oe(Te,we)))},"segmentsConflict"),ie=o((re,J,se)=>{if(W(re,J,re.pipe.tracks[re.trackIndex],J.pipe.tracks[J.trackIndex]))return;let ge=te(re.pipe,J);se(J,ge!==-1?ge:H(re.pipe))},"resolveTrackConflict"),ae=o(re=>{let J=0;for(let se=0;se{if(Re.has(re))return Re.get(re);let J=y[re];if(J.length===0){let ve={dest:0,deviation:0,base:0,delta:0};return Re.set(re,ve),ve}let ge=g[J[0]].pipe.coord,Te=ge;for(let ve=1;veMath.abs(he-ge)?q:he;break}}let we=Math.abs(Te-ge),Me={dest:Te,deviation:we,base:ge,delta:Te-ge};return Re.set(re,Me),Me},"getDestInfo"),Pe=o(()=>{let re=0,J=new Map;for(let[ge,Te]of i.entries())y[ge].length!==0&&Te.start&&(J.has(Te.start)||J.set(Te.start,[]),J.get(Te.start).push(ge));let se=o(ge=>{let Te=i[ge];if(!Te.start||!Te.end)return 0;let we=a.get(Te.start),Me=a.get(Te.end);if(!we||!Me)return 0;let ve=(Me.x??0)-(we.x??0),ne=(Me.y??0)-(we.y??0);return Math.abs(ve)+Math.abs(ne)},"getEdgeDistance");for(let ge of J.values()){ge.sort((we,Me)=>{let ve=be(we),ne=be(Me);if(Math.abs(ve.deviation-ne.deviation)>1)return ve.deviation-ne.deviation;if(Math.abs(ve.dest-ne.dest)>1)return ve.dest-ne.dest;let q=se(we),he=se(Me);if(Math.abs(q-he)>1)return he-q;let X=y[we].length,fe=y[Me].length;if(X!==fe)return X-fe;if(X===1){let K=y[we][0],qe=y[Me][0];if(g[K]&&g[qe]){let _e=g[K],Be=g[qe],Ne=Math.abs(_e.to-_e.from),He=Math.abs(Be.to-Be.from);if(Math.abs(Ne-He)>1)return Ne-He}}return 0});let Te=ge.map(we=>g[y[we][0]]);re+=ae(Te)}return re},"fixSourceHandleCrossings"),Ge=o(()=>{let re=0,J=new Map;for(let[se,ge]of i.entries())y[se].length!==0&&ge.end&&(J.has(ge.end)||J.set(ge.end,[]),J.get(ge.end).push(se));for(let se of J.values()){se.sort((Te,we)=>{let Me=o(q=>{let he=y[q];if(he.length<2)return 0;let X=g[he[he.length-2]];return Math.abs(X.to-X.from)},"getDist"),ve=Me(Te),ne=Me(we);return Math.abs(ve-ne)>.1?ve-ne:Te-we});let ge=se.map(Te=>g[y[Te][y[Te].length-1]]);re+=ae(ge)}return re},"fixTargetHandleCrossings"),Oe=o(()=>{let re=0;for(let J of l){let se=[];for(let ge of J.tracks)for(let Te of ge.segments){let we=y[Te.edgeIndex].find(Me=>g[Me].segmentIndex===Te.segmentIndex);we!==void 0&&se.push(g[we])}se.sort((ge,Te)=>ge.edgeIndex-Te.edgeIndex||ge.segmentIndex-Te.segmentIndex);for(let ge=0;ge{ge.segments.forEach(Te=>{J.push({edgeIndex:Te.edgeIndex,segmentIndex:Te.segmentIndex,trackIndex:ge.index,from:Te.from,to:Te.to})})}),J.sort((ge,Te)=>ge.from-Te.from);let se=[];if(J.length>0){let ge=[J[0]],Te=J[0].to;for(let we=1;weTe.add(K.trackIndex));let we=new Map;ge.forEach(K=>{let qe=be(K.edgeIndex);we.set(K.trackIndex,(we.get(K.trackIndex)??0)+qe.delta)});let Me=[...Te].filter(K=>(we.get(K)??0)<-1),ve=[...Te].filter(K=>(we.get(K)??0)>1),ne=[...Te].filter(K=>Math.abs(we.get(K)??0)<=1);Me.sort((K,qe)=>(we.get(qe)??0)-(we.get(K)??0)),ve.sort((K,qe)=>(we.get(K)??0)-(we.get(qe)??0));let q=o((K,qe)=>{ge.filter(_e=>_e.trackIndex===K).forEach(_e=>{let Be=v.has(_e.edgeIndex)?re.coord:qe;ke.set(`${_e.edgeIndex}-${_e.segmentIndex}`,Be)})},"assignCoord"),he=0;for(let K of Me)he++,q(K,re.coord-he*TB);if(ne.length===0&&Te.size>0){let K=[...Te].sort((Be,Ne)=>Math.abs(we.get(Be)??0)-Math.abs(we.get(Ne)??0))[0],qe=Me.indexOf(K);qe!==-1&&Me.splice(qe,1);let _e=ve.indexOf(K);_e!==-1&&ve.splice(_e,1),ne.push(K)}let X=0;for(let K of ne){if(X===0)q(K,re.coord);else{let qe=X%2===1?1:-1,_e=Math.ceil(X/2);q(K,re.coord+qe*_e*TB*.5)}X++}let fe=0;for(let K of ve)fe++,q(K,re.coord+fe*TB)}}for(let[re,J]of i.entries()){let se=y[re]??[];if(se.length===0)continue;let ge=[],Te=a.get(J.start),we=a.get(J.end),{pSrcPort:Me,pDstPort:ve}=G(re,Te,we),ne=se.map(X=>{let fe=g[X],K=ke.get(`${fe.edgeIndex}-${fe.segmentIndex}`)??fe.pipe.coord;return{orient:fe.orientation,coord:K,from:fe.from,to:fe.to}});ge.push(Me);for(let X=0;XXr&&ge.push(k1(fe,qe)),Ne&&Be.orient===fe.orient)if(Math.abs(fe.coord-Be.coord)>Xr){let He=fe.orient==="vertical"?(qe+Be.from)/2:S1e(fe,Be);ge.push(k1(fe,He),k1(Be,He))}else(X===0||X===ne.length-2)&&ge.push(k1(fe,S1e(fe,Be)));else if(Ne)ge.push(k1(fe,Be.coord));else{let He=Math.abs(fe.from-qe)Xr||Math.abs(q.y-ve.y)>Xr)&&ge.push(ve);let he=[];ge.length>0&&he.push(ge[0]);for(let X=1;XXr||Math.abs(fe.y-K.y)>Xr)&&he.push(fe)}J.points=he}for(let re of i){let J=re.__originalEdge;J&&re.points&&(J.points=re.points)}e.edges=(e.edges??[]).filter(re=>!re.isLayoutOnly);let ce=o((re,J)=>{let se=J.x??0,ge=J.y??0,Te=J.width??0,we=J.height??0;if(Te<=0||we<=0)return re;let Me=se-Te/2,ve=se+Te/2,ne=ge-we/2,q=ge+we/2;if(re.xve||re.yq)return re;let he=re.x-Me,X=ve-re.x,fe=re.y-ne,K=q-re.y,qe=Math.min(he,X,fe,K);return qe===he?{x:Me,y:re.y}:qe===X?{x:ve,y:re.y}:qe===fe?{x:re.x,y:ne}:{x:re.x,y:q}},"nodeBoundaryClamp");for(let re of e.edges){let J=re.points;if(!J||J.length<2)continue;let se=re.start,ge=re.end,Te=se?a.get(se):void 0,we=ge?a.get(ge):void 0;Te&&(J[0]=ce(J[0],Te)),we&&(J[J.length-1]=ce(J[J.length-1],we))}return e}var Xr,dut,jm,w1,S5,Cl,TB,A1e=F(()=>{"use strict";C1();Xr=Gye.EPSILON,dut=8,jm=15,w1=15,S5=25,Cl=20,TB=10;o(k1e,"chooseOrthogonalSide");o(S1e,"sharedLineEndpointCoord");o(k1,"pointOnLine");o(E1e,"routeEdgesOrthogonal")});function fut(e){return e.direction??"TB"}function R1e(e){let t=F0e(e),r=e.config.flowchart?.nodeSpacing??40,n=e.config.flowchart?.rankSpacing??100,i=e.config.swimlane?.ignoreCrossLaneEdges??!0,a=e.config.swimlane?.optimizeRanksByCrossings??!0,s=e.config.swimlane?.automaticLaneOrdering??!1,l=fut(e),{ordered:u,coordinates:h}=C1e(t,{nodeGap:r,layerGap:n,ignoreCrossLaneEdges:i,optimizeRanksByCrossings:a,automaticLaneOrdering:s,direction:l});z0e(t,u,h,{nodeGap:r,layerGap:n});for(let d of e.edges??[])delete d.points;E1e(e,l);for(let d of e.edges??[])(!d.curve||d.curve==="basis")&&(d.curve="rounded");return Bye(e,l),dB(e),l}var _1e=F(()=>{"use strict";$ye();Q9();w1e();A1e();o(fut,"getSwimlaneDirection");o(R1e,"runSwimlaneLayoutCore")});var L1e={};ir(L1e,{render:()=>put});async function put(e,t){let r=t.select("g");Uy(r,e.markers,e.type,e.diagramId),J4(),X4(),q4(),o5(),$0e(e);let n=G0e(e);e.nodes=n.nodes,e.edges=n.edges;let{groups:i}=await R0e(r,e);R1e(e),await P0e(e,i)}var D1e=F(()=>{"use strict";_0e();Z4();X9();Dm();Wy();V2();O0e();Q9();V0e();_1e();o(put,"render")});function Q$(e,t){(t==null||t>e.length)&&(t=e.length);for(var r=0,n=Array(t);r=e.length?{done:!0}:{done:!1,value:e[n++]}},"n"),e:o(function(u){throw u},"e"),f:i}}throw new TypeError(`Invalid attempt to iterate non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}var a,s=!0,l=!1;return{s:o(function(){r=r.call(e)},"s"),n:o(function(){var u=r.next();return s=u.done,u},"n"),e:o(function(u){l=!0,a=u},"e"),f:o(function(){try{s||r.return==null||r.return()}finally{if(l)throw a}},"f")}}function sbe(e,t,r){return(t=obe(t))in e?Object.defineProperty(e,t,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[t]=r,e}function vut(e){if(typeof Symbol<"u"&&e[Symbol.iterator]!=null||e["@@iterator"]!=null)return Array.from(e)}function xut(e,t){var r=e==null?null:typeof Symbol<"u"&&e[Symbol.iterator]||e["@@iterator"];if(r!=null){var n,i,a,s,l=[],u=!0,h=!1;try{if(a=(r=r.call(e)).next,t===0){if(Object(r)!==r)return;u=!1}else for(;!(u=(n=a.call(r)).done)&&(l.push(n.value),l.length!==t);u=!0);}catch(d){h=!0,i=d}finally{try{if(!u&&r.return!=null&&(s=r.return(),Object(s)!==s))return}finally{if(h)throw i}}return l}}function but(){throw new TypeError(`Invalid attempt to destructure non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Tut(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function Ki(e,t){return mut(e)||xut(e,t)||vF(e,t)||but()}function K5(e){return gut(e)||vut(e)||vF(e)||Tut()}function Cut(e,t){if(typeof e!="object"||!e)return e;var r=e[Symbol.toPrimitive];if(r!==void 0){var n=r.call(e,t);if(typeof n!="object")return n;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(e)}function obe(e){var t=Cut(e,"string");return typeof t=="symbol"?t:t+""}function ca(e){"@babel/helpers - typeof";return ca=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},ca(e)}function vF(e,t){if(e){if(typeof e=="string")return Q$(e,t);var r={}.toString.call(e).slice(8,-1);return r==="Object"&&e.constructor&&(r=e.constructor.name),r==="Map"||r==="Set"?Array.from(e):r==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(r)?Q$(e,t):void 0}}function jT(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}function XT(){if(N1e)return CB;N1e=1;function e(t){var r=typeof t;return t!=null&&(r=="object"||r=="function")}return o(e,"isObject"),CB=e,CB}function Hut(){if(P1e)return wB;P1e=1;var e=typeof E5=="object"&&E5&&E5.Object===Object&&E5;return wB=e,wB}function hA(){if(O1e)return kB;O1e=1;var e=Hut(),t=typeof self=="object"&&self&&self.Object===Object&&self,r=e||t||Function("return this")();return kB=r,kB}function Uut(){if(B1e)return SB;B1e=1;var e=hA(),t=o(function(){return e.Date.now()},"now");return SB=t,SB}function Yut(){if($1e)return EB;$1e=1;var e=/\s/;function t(r){for(var n=r.length;n--&&e.test(r.charAt(n)););return n}return o(t,"trimmedEndIndex"),EB=t,EB}function jut(){if(F1e)return AB;F1e=1;var e=Yut(),t=/^\s+/;function r(n){return n&&n.slice(0,e(n)+1).replace(t,"")}return o(r,"baseTrim"),AB=r,AB}function TF(){if(z1e)return RB;z1e=1;var e=hA(),t=e.Symbol;return RB=t,RB}function Xut(){if(G1e)return _B;G1e=1;var e=TF(),t=Object.prototype,r=t.hasOwnProperty,n=t.toString,i=e?e.toStringTag:void 0;function a(s){var l=r.call(s,i),u=s[i];try{s[i]=void 0;var h=!0}catch{}var d=n.call(s);return h&&(l?s[i]=u:delete s[i]),d}return o(a,"getRawTag"),_B=a,_B}function Kut(){if(V1e)return LB;V1e=1;var e=Object.prototype,t=e.toString;function r(n){return t.call(n)}return o(r,"objectToString"),LB=r,LB}function gbe(){if(W1e)return DB;W1e=1;var e=TF(),t=Xut(),r=Kut(),n="[object Null]",i="[object Undefined]",a=e?e.toStringTag:void 0;function s(l){return l==null?l===void 0?i:n:a&&a in Object(l)?t(l):r(l)}return o(s,"baseGetTag"),DB=s,DB}function Zut(){if(q1e)return IB;q1e=1;function e(t){return t!=null&&typeof t=="object"}return o(e,"isObjectLike"),IB=e,IB}function KT(){if(H1e)return MB;H1e=1;var e=gbe(),t=Zut(),r="[object Symbol]";function n(i){return typeof i=="symbol"||t(i)&&e(i)==r}return o(n,"isSymbol"),MB=n,MB}function Qut(){if(U1e)return NB;U1e=1;var e=jut(),t=XT(),r=KT(),n=NaN,i=/^[-+]0x[0-9a-f]+$/i,a=/^0b[01]+$/i,s=/^0o[0-7]+$/i,l=parseInt;function u(h){if(typeof h=="number")return h;if(r(h))return n;if(t(h)){var d=typeof h.valueOf=="function"?h.valueOf():h;h=t(d)?d+"":d}if(typeof h!="string")return h===0?h:+h;h=e(h);var f=a.test(h);return f||s.test(h)?l(h.slice(2),f?2:8):i.test(h)?n:+h}return o(u,"toNumber"),NB=u,NB}function Jut(){if(Y1e)return PB;Y1e=1;var e=XT(),t=Uut(),r=Qut(),n="Expected a function",i=Math.max,a=Math.min;function s(l,u,h){var d,f,p,m,g,y,v=0,x=!1,b=!1,T=!0;if(typeof l!="function")throw new TypeError(n);u=r(u)||0,e(h)&&(x=!!h.leading,b="maxWait"in h,p=b?i(r(h.maxWait)||0,u):p,T="trailing"in h?!!h.trailing:T);function k(A){var M=d,D=f;return d=f=void 0,v=A,m=l.apply(D,M),m}o(k,"invokeFunc");function C(A){return v=A,g=setTimeout(R,u),x?k(A):m}o(C,"leadingEdge");function w(A){var M=A-y,D=A-v,P=u-M;return b?a(P,p-D):P}o(w,"remainingWait");function S(A){var M=A-y,D=A-v;return y===void 0||M>=u||M<0||b&&D>=p}o(S,"shouldInvoke");function R(){var A=t();if(S(A))return L(A);g=setTimeout(R,w(A))}o(R,"timerExpired");function L(A){return g=void 0,T&&d?k(A):(d=f=void 0,m)}o(L,"trailingEdge");function N(){g!==void 0&&clearTimeout(g),v=0,d=y=f=g=void 0}o(N,"cancel");function I(){return g===void 0?m:L(t())}o(I,"flush");function _(){var A=t(),M=S(A);if(d=arguments,f=this,y=A,M){if(g===void 0)return C(y);if(b)return clearTimeout(g),g=setTimeout(R,u),k(y)}return g===void 0&&(g=setTimeout(R,u)),m}return o(_,"debounced"),_.cancel=N,_.flush=I,_}return o(s,"debounce"),PB=s,PB}function iht(e,t,r,n,i){var a=i*Math.PI/180,s=Math.cos(a)*(e-r)-Math.sin(a)*(t-n)+r,l=Math.sin(a)*(e-r)+Math.cos(a)*(t-n)+n;return{x:s,y:l}}function sht(e,t,r){if(r===0)return e;var n=(t.x1+t.x2)/2,i=(t.y1+t.y2)/2,a=t.w/t.h,s=1/a,l=iht(e.x,e.y,n,i,r),u=aht(l.x,l.y,n,i,a,s);return{x:u.x,y:u.y}}function yht(){return Q1e||(Q1e=1,(function(e,t){(function(){var r,n,i,a,s,l,u,h,d,f,p,m,g,y,v;i=Math.floor,f=Math.min,n=o(function(x,b){return xb?1:0},"defaultCmp"),d=o(function(x,b,T,k,C){var w;if(T==null&&(T=0),C==null&&(C=n),T<0)throw new Error("lo must be non-negative");for(k==null&&(k=x.length);TN;0<=N?L++:L--)R.push(L);return R}).apply(this).reverse(),S=[],k=0,C=w.length;kI;0<=I?++R:--R)_.push(s(x,T));return _},"nsmallest"),y=o(function(x,b,T,k){var C,w,S;for(k==null&&(k=n),C=x[T];T>b;){if(S=T-1>>1,w=x[S],k(C,w)<0){x[T]=w,T=S;continue}break}return x[T]=C},"_siftdown"),v=o(function(x,b,T){var k,C,w,S,R;for(T==null&&(T=n),C=x.length,R=b,w=x[b],k=2*b+1;k-1}return o(t,"listCacheHas"),u$=t,u$}function uft(){if(Fve)return h$;Fve=1;var e=yA();function t(r,n){var i=this.__data__,a=e(i,r);return a<0?(++this.size,i.push([r,n])):i[a][1]=n,this}return o(t,"listCacheSet"),h$=t,h$}function hft(){if(zve)return d$;zve=1;var e=sft(),t=oft(),r=lft(),n=cft(),i=uft();function a(s){var l=-1,u=s==null?0:s.length;for(this.clear();++l-1&&n%1==0&&n0;){var d=i.shift();t(d),a.add(d.id()),l&&n(i,a,d)}return e}function Ybe(e,t,r){if(r.isParent())for(var n=r._private.children,i=0;i0&&arguments[0]!==void 0?arguments[0]:Tpt,t=arguments.length>1?arguments[1]:void 0,r=0;r0?_=M:I=M;while(Math.abs(A)>s&&++D=a?b(N,D):P===0?D:k(N,I,I+h)}o(C,"getTForX");var w=!1;function S(){w=!0,(e!==t||r!==n)&&T()}o(S,"precompute");var R=o(function(I){return w||S(),e===t&&r===n?I:I===0?0:I===1?1:v(C(I),t,n)},"f");R.getControlPoints=function(){return[{x:e,y:t},{x:r,y:n}]};var L="generateBezier("+[e,t,r,n]+")";return R.toString=function(){return L},R}function _xe(e,t,r,n,i){if(n===1||t===r)return r;var a=i(t,r,n);return e==null||((e.roundValue||e.color)&&(a=Math.round(a)),e.min!==void 0&&(a=Math.max(a,e.min)),e.max!==void 0&&(a=Math.min(a,e.max))),a}function Lxe(e,t){return e.pfValue!=null||e.value!=null?e.pfValue!=null&&(t==null||t.type.units!=="%")?e.pfValue:e.value:e}function A1(e,t,r,n,i){var a=i!=null?i.type:null;r<0?r=0:r>1&&(r=1);var s=Lxe(e,i),l=Lxe(t,i);if(Gt(s)&&Gt(l))return _xe(a,s,l,r,n);if(Hn(s)&&Hn(l)){for(var u=[],h=0;h0?(m==="spring"&&g.push(s.duration),s.easingImpl=q5[m].apply(null,g)):s.easingImpl=q5[m]}var y=s.easingImpl,v;if(s.duration===0?v=1:v=(r-u)/s.duration,s.applying&&(v=s.progress),v<0?v=0:v>1&&(v=1),s.delay==null){var x=s.startPosition,b=s.position;if(b&&i&&!e.locked()){var T={};ET(x.x,b.x)&&(T.x=A1(x.x,b.x,v,y)),ET(x.y,b.y)&&(T.y=A1(x.y,b.y,v,y)),e.position(T)}var k=s.startPan,C=s.pan,w=a.pan,S=C!=null&&n;S&&(ET(k.x,C.x)&&(w.x=A1(k.x,C.x,v,y)),ET(k.y,C.y)&&(w.y=A1(k.y,C.y,v,y)),e.emit("pan"));var R=s.startZoom,L=s.zoom,N=L!=null&&n;N&&(ET(R,L)&&(a.zoom=FT(a.minZoom,A1(R,L,v,y),a.maxZoom)),e.emit("zoom")),(S||N)&&e.emit("viewport");var I=s.style;if(I&&I.length>0&&i){for(var _=0;_=0;S--){var R=w[S];R()}w.splice(0,w.length)},"callbacks"),b=m.length-1;b>=0;b--){var T=m[b],k=T._private;if(k.stopped){m.splice(b,1),k.hooked=!1,k.playing=!1,k.started=!1,x(k.frames);continue}!k.playing&&!k.applying||(k.playing&&k.applying&&(k.applying=!1),k.started||Ppt(d,T,e),Npt(d,T,e,f),k.applying&&(k.applying=!1),x(k.frames),k.step!=null&&k.step(e),T.completed()&&(m.splice(b,1),k.hooked=!1,k.playing=!1,k.started=!1,x(k.completes)),y=!0)}return!f&&m.length===0&&g.length===0&&n.push(d),y}o(i,"stepOne");for(var a=!1,s=0;s0?t.notify("draw",r):t.notify("draw")),r.unmerge(n),t.emit("step")}function h2e(e){this.options=br({},Wpt,qpt,e)}function d2e(e){this.options=br({},Hpt,e)}function f2e(e){this.options=br({},Upt,e)}function SA(e){this.options=br({},Ypt,e),this.options.layout=this;var t=this.options.eles.nodes(),r=this.options.eles.edges(),n=r.filter(function(i){var a=i.source().data("id"),s=i.target().data("id"),l=t.some(function(h){return h.data("id")===a}),u=t.some(function(h){return h.data("id")===s});return!l||!u});this.options.eles=this.options.eles.not(n)}function y2e(e){this.options=br({},lmt,e)}function BF(e){this.options=br({},cmt,e)}function v2e(e){this.options=br({},umt,e)}function x2e(e){this.options=br({},hmt,e)}function b2e(e){this.options=e,this.notifications=0}function w2e(e,t){t.radius===0?e.lineTo(t.cx,t.cy):e.arc(t.cx,t.cy,t.radius,t.startAngle,t.endAngle,t.counterClockwise)}function FF(e,t,r,n){var i=arguments.length>4&&arguments[4]!==void 0?arguments[4]:!0;return n===0||t.radius===0?{cx:t.x,cy:t.y,radius:0,startX:t.x,startY:t.y,stopX:t.x,stopY:t.y,startAngle:void 0,endAngle:void 0,counterClockwise:void 0}:(pmt(e,t,r,n,i),{cx:cF,cy:uF,radius:Jm,startX:T2e,startY:C2e,stopX:hF,stopY:dF,startAngle:bu.ang+Math.PI/2*tg,endAngle:wl.ang-Math.PI/2*tg,counterClockwise:Y5})}function k2e(e){var t=[];if(e!=null){for(var r=0;r5&&arguments[5]!==void 0?arguments[5]:5,s=Math.min(a,n/2,i/2);e.beginPath(),e.moveTo(t+s,r),e.lineTo(t+n-s,r),e.quadraticCurveTo(t+n,r,t+n,r+s),e.lineTo(t+n,r+i-s),e.quadraticCurveTo(t+n,r+i,t+n-s,r+i),e.lineTo(t+s,r+i),e.quadraticCurveTo(t,r+i,t,r+i-s),e.lineTo(t,r+s),e.quadraticCurveTo(t,r,t+s,r),e.closePath()}function Zxe(e,t,r){var n=e.createShader(t);if(e.shaderSource(n,r),e.compileShader(n),!e.getShaderParameter(n,e.COMPILE_STATUS))throw new Error(e.getShaderInfoLog(n));return n}function agt(e,t,r){var n=Zxe(e,e.VERTEX_SHADER,t),i=Zxe(e,e.FRAGMENT_SHADER,r),a=e.createProgram();if(e.attachShader(a,n),e.attachShader(a,i),e.linkProgram(a),!e.getProgramParameter(a,e.LINK_STATUS))throw new Error("Could not initialize shaders");return a}function sgt(e,t,r){r===void 0&&(r=t);var n=e.makeOffscreenCanvas(t,r),i=n.context=n.getContext("2d");return n.clear=function(){return i.clearRect(0,0,n.width,n.height)},n.clear(),n}function VF(e){var t=e.pixelRatio,r=e.cy.zoom(),n=e.cy.pan();return{zoom:r*t,pan:{x:n.x*t,y:n.y*t}}}function ogt(e){var t=e.pixelRatio,r=e.cy.zoom();return r*t}function lgt(e,t,r,n,i){var a=n*r+t.x,s=i*r+t.y;return s=Math.round(e.canvasHeight-s),[a,s]}function cgt(e,t){return t.picking?!0:e.pstyle("background-fill").value!=="solid"||e.pstyle("background-image").strValue!=="none"?!1:e.pstyle("border-width").value===0||e.pstyle("border-opacity").value===0?!0:e.pstyle("border-style").value==="solid"}function ugt(e,t){if(e.length!==t.length)return!1;for(var r=0;r>0&255)/255,r[1]=(e>>8&255)/255,r[2]=(e>>16&255)/255,r[3]=(e>>24&255)/255,r}function hgt(e){return e[0]+(e[1]<<8)+(e[2]<<16)+(e[3]<<24)}function dgt(e,t){var r=e.createTexture();return r.buffer=function(n){e.bindTexture(e.TEXTURE_2D,r),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.LINEAR),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.LINEAR_MIPMAP_NEAREST),e.pixelStorei(e.UNPACK_PREMULTIPLY_ALPHA_WEBGL,!0),e.texImage2D(e.TEXTURE_2D,0,e.RGBA,e.RGBA,e.UNSIGNED_BYTE,n),e.generateMipmap(e.TEXTURE_2D),e.bindTexture(e.TEXTURE_2D,null)},r.deleteTexture=function(){e.deleteTexture(r)},r}function $2e(e,t){switch(t){case"float":return[1,e.FLOAT,4];case"vec2":return[2,e.FLOAT,4];case"vec3":return[3,e.FLOAT,4];case"vec4":return[4,e.FLOAT,4];case"int":return[1,e.INT,4];case"ivec2":return[2,e.INT,4]}}function F2e(e,t,r){switch(t){case e.FLOAT:return new Float32Array(r);case e.INT:return new Int32Array(r)}}function fgt(e,t,r,n,i,a){switch(t){case e.FLOAT:return new Float32Array(r.buffer,a*n,i);case e.INT:return new Int32Array(r.buffer,a*n,i)}}function pgt(e,t,r,n){var i=$2e(e,t),a=Ki(i,2),s=a[0],l=a[1],u=F2e(e,l,n),h=e.createBuffer();return e.bindBuffer(e.ARRAY_BUFFER,h),e.bufferData(e.ARRAY_BUFFER,u,e.STATIC_DRAW),l===e.FLOAT?e.vertexAttribPointer(r,s,l,!1,0,0):l===e.INT&&e.vertexAttribIPointer(r,s,l,0,0),e.enableVertexAttribArray(r),e.bindBuffer(e.ARRAY_BUFFER,null),h}function xu(e,t,r,n){var i=$2e(e,r),a=Ki(i,3),s=a[0],l=a[1],u=a[2],h=F2e(e,l,t*s),d=s*u,f=e.createBuffer();e.bindBuffer(e.ARRAY_BUFFER,f),e.bufferData(e.ARRAY_BUFFER,t*d,e.DYNAMIC_DRAW),e.enableVertexAttribArray(n),l===e.FLOAT?e.vertexAttribPointer(n,s,l,!1,d,0):l===e.INT&&e.vertexAttribIPointer(n,s,l,d,0),e.vertexAttribDivisor(n,1),e.bindBuffer(e.ARRAY_BUFFER,null);for(var p=new Array(t),m=0;mM2e?(Dgt(e),t.call(e,a)):(Igt(e),W2e(e,a,PT.SCREEN)))}}{var r=e.matchCanvasSize;e.matchCanvasSize=function(a){r.call(e,a),e.pickingFrameBuffer.setFramebufferAttachmentSizes(e.canvasWidth,e.canvasHeight),e.pickingFrameBuffer.needsDraw=!0}}e.findNearestElements=function(a,s,l,u){return $gt(e,a,s)};{var n=e.invalidateCachedZSortedEles;e.invalidateCachedZSortedEles=function(){n.call(e),e.pickingFrameBuffer.needsDraw=!0}}{var i=e.notify;e.notify=function(a,s){i.call(e,a,s),a==="viewport"||a==="bounds"?e.pickingFrameBuffer.needsDraw=!0:a==="background"&&e.drawing.invalidate(s,{type:"node-body"})}}}function Dgt(e){var t=e.data.contexts[e.WEBGL];t.clear(t.COLOR_BUFFER_BIT|t.DEPTH_BUFFER_BIT)}function Igt(e){var t=o(function(n){n.save(),n.setTransform(1,0,0,1,0,0),n.clearRect(0,0,e.canvasWidth,e.canvasHeight),n.restore()},"clear");t(e.data.contexts[e.NODE]),t(e.data.contexts[e.DRAG])}function Mgt(e){var t=e.canvasWidth,r=e.canvasHeight,n=VF(e),i=n.pan,a=n.zoom,s=U$();X5(s,s,[i.x,i.y]),pF(s,s,[a,a]);var l=U$();vgt(l,t,r);var u=U$();return ygt(u,l,s),u}function V2e(e,t){var r=e.canvasWidth,n=e.canvasHeight,i=VF(e),a=i.pan,s=i.zoom;t.setTransform(1,0,0,1,0,0),t.clearRect(0,0,r,n),t.translate(a.x,a.y),t.scale(s,s)}function Ngt(e,t){e.drawSelectionRectangle(t,function(r){return V2e(e,r)})}function Pgt(e){var t=e.data.contexts[e.NODE];t.save(),V2e(e,t),t.strokeStyle="rgba(0, 0, 0, 0.3)",t.beginPath(),t.moveTo(-1e3,0),t.lineTo(1e3,0),t.stroke(),t.beginPath(),t.moveTo(0,-1e3),t.lineTo(0,1e3),t.stroke(),t.restore()}function Ogt(e){var t=o(function(i,a,s){for(var l=i.atlasManager.getAtlasCollection(a),u=e.data.contexts[e.NODE],h=l.atlases,d=0;d=0&&k.add(S)}return k}function $gt(e,t,r){var n=Bgt(e,t,r),i=e.getCachedZSortedEles(),a,s,l=yo(n),u;try{for(l.s();!(u=l.n()).done;){var h=u.value,d=i[h];if(!a&&d.isNode()&&(a=d),!s&&d.isEdge()&&(s=d),a&&s)break}}catch(f){l.e(f)}finally{l.f()}return[a,s].filter(Boolean)}function Z$(e,t,r){var n=e.drawing;t+=1,r.isNode()?(n.drawNode(r,t,"node-underlay"),n.drawNode(r,t,"node-body"),n.drawTexture(r,t,"label"),n.drawNode(r,t,"node-overlay")):(n.drawEdgeLine(r,t),n.drawEdgeArrow(r,t,"source"),n.drawEdgeArrow(r,t,"target"),n.drawTexture(r,t,"label"),n.drawTexture(r,t,"edge-source-label"),n.drawTexture(r,t,"edge-target-label"))}function W2e(e,t,r){var n;e.webglDebug&&(n=performance.now());var i=e.drawing,a=0;if(r.screen&&e.data.canvasNeedsRedraw[e.SELECT_BOX]&&Ngt(e,t),e.data.canvasNeedsRedraw[e.NODE]||r.picking){var s=e.data.contexts[e.WEBGL];r.screen?(s.clearColor(0,0,0,0),s.enable(s.BLEND),s.blendFunc(s.ONE,s.ONE_MINUS_SRC_ALPHA)):s.disable(s.BLEND),s.clear(s.COLOR_BUFFER_BIT|s.DEPTH_BUFFER_BIT),s.viewport(0,0,s.canvas.width,s.canvas.height);var l=Mgt(e),u=e.getCachedZSortedEles();if(a=u.length,i.startFrame(l,r),r.screen){for(var h=0;h{"use strict";o(Q$,"_arrayLikeToArray");o(mut,"_arrayWithHoles");o(gut,"_arrayWithoutHoles");o(jf,"_classCallCheck");o(yut,"_defineProperties");o(Xf,"_createClass");o(yo,"_createForOfIteratorHelper");o(sbe,"_defineProperty$1");o(vut,"_iterableToArray");o(xut,"_iterableToArrayLimit");o(but,"_nonIterableRest");o(Tut,"_nonIterableSpread");o(Ki,"_slicedToArray");o(K5,"_toConsumableArray");o(Cut,"_toPrimitive");o(obe,"_toPropertyKey");o(ca,"_typeof");o(vF,"_unsupportedIterableToArray");oa=typeof window>"u"?null:window,I1e=oa?oa.navigator:null;oa&&oa.document;wut=ca(""),lbe=ca({}),kut=ca(function(){}),Sut=typeof HTMLElement>"u"?"undefined":ca(HTMLElement),UT=o(function(t){return t&&t.instanceString&&Ci(t.instanceString)?t.instanceString():null},"instanceStr"),fr=o(function(t){return t!=null&&ca(t)==wut},"string"),Ci=o(function(t){return t!=null&&ca(t)===kut},"fn"),Hn=o(function(t){return!Ho(t)&&(Array.isArray?Array.isArray(t):t!=null&&t instanceof Array)},"array"),cn=o(function(t){return t!=null&&ca(t)===lbe&&!Hn(t)&&t.constructor===Object},"plainObject"),Eut=o(function(t){return t!=null&&ca(t)===lbe},"object"),Gt=o(function(t){return t!=null&&ca(t)===ca(1)&&!isNaN(t)},"number"),Aut=o(function(t){return Gt(t)&&Math.floor(t)===t},"integer"),Z5=o(function(t){if(Sut!=="undefined")return t!=null&&t instanceof HTMLElement},"htmlElement"),Ho=o(function(t){return YT(t)||cbe(t)},"elementOrCollection"),YT=o(function(t){return UT(t)==="collection"&&t._private.single},"element"),cbe=o(function(t){return UT(t)==="collection"&&!t._private.single},"collection"),xF=o(function(t){return UT(t)==="core"},"core"),ube=o(function(t){return UT(t)==="stylesheet"},"stylesheet"),Rut=o(function(t){return UT(t)==="event"},"event"),Vf=o(function(t){return t==null?!0:!!(t===""||t.match(/^\s+$/))},"emptyString"),_ut=o(function(t){return typeof HTMLElement>"u"?!1:t instanceof HTMLElement},"domElement"),Lut=o(function(t){return cn(t)&&Gt(t.x1)&&Gt(t.x2)&&Gt(t.y1)&&Gt(t.y2)},"boundingBox"),Dut=o(function(t){return Eut(t)&&Ci(t.then)},"promise"),Iut=o(function(){return I1e&&I1e.userAgent.match(/msie|trident|edge/i)},"ms"),z1=o(function(t,r){r||(r=o(function(){if(arguments.length===1)return arguments[0];if(arguments.length===0)return"undefined";for(var a=[],s=0;sr?1:0},"ascending"),Fut=o(function(t,r){return-1*dbe(t,r)},"descending"),br=Object.assign!=null?Object.assign.bind(Object):function(e){for(var t=arguments,r=1;r1&&(v-=1),v<1/6?g+(y-g)*6*v:v<1/2?y:v<2/3?g+(y-g)*(2/3-v)*6:g}o(d,"hue2rgb");var f=new RegExp("^"+Put+"$").exec(t);if(f){if(n=parseInt(f[1]),n<0?n=(360- -1*n%360)%360:n>360&&(n=n%360),n/=360,i=parseFloat(f[2]),i<0||i>100||(i=i/100,a=parseFloat(f[3]),a<0||a>100)||(a=a/100,s=f[4],s!==void 0&&(s=parseFloat(s),s<0||s>1)))return;if(i===0)l=u=h=Math.round(a*255);else{var p=a<.5?a*(1+i):a+i-a*i,m=2*a-p;l=Math.round(255*d(m,p,n+1/3)),u=Math.round(255*d(m,p,n)),h=Math.round(255*d(m,p,n-1/3))}r=[l,u,h,s]}return r},"hsl2tuple"),Vut=o(function(t){var r,n=new RegExp("^"+Mut+"$").exec(t);if(n){r=[];for(var i=[],a=1;a<=3;a++){var s=n[a];if(s[s.length-1]==="%"&&(i[a]=!0),s=parseFloat(s),i[a]&&(s=s/100*255),s<0||s>255)return;r.push(Math.floor(s))}var l=i[1]||i[2]||i[3],u=i[1]&&i[2]&&i[3];if(l&&!u)return;var h=n[4];if(h!==void 0){if(h=parseFloat(h),h<0||h>1)return;r.push(h)}}return r},"rgb2tuple"),Wut=o(function(t){return qut[t.toLowerCase()]},"colorname2tuple"),fbe=o(function(t){return(Hn(t)?t:null)||Wut(t)||zut(t)||Vut(t)||Gut(t)},"color2tuple"),qut={transparent:[0,0,0,0],aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],grey:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]},pbe=o(function(t){for(var r=t.map,n=t.keys,i=n.length,a=0;a1&&arguments[1]!==void 0?arguments[1]:eg,n=r,i;i=t.next(),!i.done;)n=n*vbe+i.value|0;return n},"hashIterableInts"),OT=o(function(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:eg;return r*vbe+t|0},"hashInt"),BT=o(function(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:I1;return(r<<5)+r+t|0},"hashIntAlt"),rht=o(function(t,r){return t*2097152+r},"combineHashes"),Nf=o(function(t){return t[0]*2097152+t[1]},"combineHashesArray"),A5=o(function(t,r){return[OT(t[0],r[0]),BT(t[1],r[1])]},"hashArrays"),j1e=o(function(t,r){var n={value:0,done:!1},i=0,a=t.length,s={next:o(function(){return i=0;i--)t[i]===r&&t.splice(i,1)},"removeFromArray"),kF=o(function(t){t.splice(0,t.length)},"clearArray"),dht=o(function(t,r){for(var n=0;n"u"?"undefined":ca(Set))!==pht?Set:mht,dA=o(function(t,r){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!0;if(t===void 0||r===void 0||!xF(t)){pi("An element must have a core reference and parameters set");return}var i=r.group;if(i==null&&(r.data&&r.data.source!=null&&r.data.target!=null?i="edges":i="nodes"),i!=="nodes"&&i!=="edges"){pi("An element must be of type `nodes` or `edges`; you specified `"+i+"`");return}this.length=1,this[0]=this;var a=this._private={cy:t,single:!0,data:r.data||{},position:r.position||{x:0,y:0},autoWidth:void 0,autoHeight:void 0,autoPadding:void 0,compoundBoundsClean:!1,listeners:[],group:i,style:{},rstyle:{},styleCxts:[],styleKeys:{},removed:!0,selected:!!r.selected,selectable:r.selectable===void 0?!0:!!r.selectable,locked:!!r.locked,grabbed:!1,grabbable:r.grabbable===void 0?!0:!!r.grabbable,pannable:r.pannable===void 0?i==="edges":!!r.pannable,active:!1,classes:new W1,animation:{current:[],queue:[]},rscratch:{},scratch:r.scratch||{},edges:[],children:[],parent:r.parent&&r.parent.isNode()?r.parent:null,traversalCache:{},backgrounding:!1,bbCache:null,bbCacheShift:{x:0,y:0},bodyBounds:null,overlayBounds:null,labelBounds:{all:null,source:null,target:null,main:null},arrowBounds:{source:null,target:null,"mid-source":null,"mid-target":null}};if(a.position.x==null&&(a.position.x=0),a.position.y==null&&(a.position.y=0),r.renderedPosition){var s=r.renderedPosition,l=t.pan(),u=t.zoom();a.position={x:(s.x-l.x)/u,y:(s.y-l.y)/u}}var h=[];Hn(r.classes)?h=r.classes:fr(r.classes)&&(h=r.classes.split(/\s+/));for(var d=0,f=h.length;d0;){var w=b.pop(),S=v(w),R=w.id();if(p[R]=S,S!==1/0)for(var L=w.neighborhood().intersect(g),N=0;N0)for(O.unshift(B);f[V];){var G=f[V];O.unshift(G.edge),O.unshift(G.node),$=G.node,V=$.id()}return l.spawn(O)},"pathTo")}},"dijkstra")},Cht={kruskal:o(function(t){t=t||function(T){return 1};for(var r=this.byGroup(),n=r.nodes,i=r.edges,a=n.length,s=new Array(a),l=n,u=o(function(k){for(var C=0;C0;){if(C(),S++,k===d){for(var R=[],L=a,N=d,I=x[N];R.unshift(L),I!=null&&R.unshift(I),L=v[N],L!=null;)N=L.id(),I=x[N];return{found:!0,distance:f[k],path:this.spawn(R),steps:S}}m[k]=!0;for(var _=T._private.edges,A=0;A<_.length;A++){var M=_[A];if(this.hasElementWithId(M.id())&&!(l&&M.data("source")!==k)){var D=M.source(),P=M.target(),B=D.id()!==k?D:P,O=B.id();if(this.hasElementWithId(O)&&!m[O]){var $=f[k]+u(M);if(!w(O)){f[O]=$,p[O]=$+s(B),b(B,O),v[O]=T,x[O]=M;continue}$I&&(g[N]=I,b[N]=L,T[N]=C),!a){var _=L*d+R;!a&&g[_]>I&&(g[_]=I,b[_]=R,T[_]=C)}}}for(var A=0;A1&&arguments[1]!==void 0?arguments[1]:s,ke=T(ue),ce=[],re=ke;;){if(re==null)return r.spawn();var J=b(re),se=J.edge,ge=J.pred;if(ce.unshift(re[0]),re.same(ye)&&ce.length>0)break;se!=null&&ce.unshift(se),re=ge}return u.spawn(ce)},"pathTo"),w=0;w=0;d--){var f=h[d],p=f[1],m=f[2];(r[p]===l&&r[m]===u||r[p]===u&&r[m]===l)&&h.splice(d,1)}for(var g=0;gi;){var a=Math.floor(Math.random()*r.length);r=Lht(a,t,r),n--}return r},"contractUntil"),Dht={kargerStein:o(function(){var t=this,r=this.byGroup(),n=r.nodes,i=r.edges;i.unmergeBy(function(O){return O.isLoop()});var a=n.length,s=i.length,l=Math.ceil(Math.pow(Math.log(a)/Math.LN2,2)),u=Math.floor(a/_ht);if(a<2){pi("At least 2 nodes are required for Karger-Stein algorithm");return}for(var h=[],d=0;d1&&arguments[1]!==void 0?arguments[1]:0,n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:t.length,i=1/0,a=r;a1&&arguments[1]!==void 0?arguments[1]:0,n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:t.length,i=-1/0,a=r;a1&&arguments[1]!==void 0?arguments[1]:0,n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:t.length,i=0,a=0,s=r;s1&&arguments[1]!==void 0?arguments[1]:0,n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:t.length,i=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!0,a=arguments.length>4&&arguments[4]!==void 0?arguments[4]:!0,s=arguments.length>5&&arguments[5]!==void 0?arguments[5]:!0;i?t=t.slice(r,n):(n0&&t.splice(0,r));for(var l=0,u=t.length-1;u>=0;u--){var h=t[u];s?isFinite(h)||(t[u]=-1/0,l++):t.splice(u,1)}a&&t.sort(function(p,m){return p-m});var d=t.length,f=Math.floor(d/2);return d%2!==0?t[f+1+l]:(t[f-1+l]+t[f+l])/2},"median"),Bht=o(function(t){return Math.PI*t/180},"deg2rad"),R5=o(function(t,r){return Math.atan2(r,t)-Math.PI/2},"getAngleFromDisp"),SF=Math.log2||function(e){return Math.log(e)/Math.log(2)},EF=o(function(t){return t>0?1:t<0?-1:0},"signum"),ig=o(function(t,r){return Math.sqrt(Qm(t,r))},"dist"),Qm=o(function(t,r){var n=r.x-t.x,i=r.y-t.y;return n*n+i*i},"sqdist"),$ht=o(function(t){for(var r=t.length,n=0,i=0;i=t.x1&&t.y2>=t.y1)return{x1:t.x1,y1:t.y1,x2:t.x2,y2:t.y2,w:t.x2-t.x1,h:t.y2-t.y1};if(t.w!=null&&t.h!=null&&t.w>=0&&t.h>=0)return{x1:t.x1,y1:t.y1,x2:t.x1+t.w,y2:t.y1+t.h,w:t.w,h:t.h}}},"makeBoundingBox"),zht=o(function(t){return{x1:t.x1,x2:t.x2,w:t.w,y1:t.y1,y2:t.y2,h:t.h}},"copyBoundingBox"),Ght=o(function(t){t.x1=1/0,t.y1=1/0,t.x2=-1/0,t.y2=-1/0,t.w=0,t.h=0},"clearBoundingBox"),Vht=o(function(t,r){t.x1=Math.min(t.x1,r.x1),t.x2=Math.max(t.x2,r.x2),t.w=t.x2-t.x1,t.y1=Math.min(t.y1,r.y1),t.y2=Math.max(t.y2,r.y2),t.h=t.y2-t.y1},"updateBoundingBox"),Ebe=o(function(t,r,n){t.x1=Math.min(t.x1,r),t.x2=Math.max(t.x2,r),t.w=t.x2-t.x1,t.y1=Math.min(t.y1,n),t.y2=Math.max(t.y2,n),t.h=t.y2-t.y1},"expandBoundingBoxByPoint"),z5=o(function(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:0;return t.x1-=r,t.x2+=r,t.y1-=r,t.y2+=r,t.w=t.x2-t.x1,t.h=t.y2-t.y1,t},"expandBoundingBox"),G5=o(function(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:[0],n,i,a,s;if(r.length===1)n=i=a=s=r[0];else if(r.length===2)n=a=r[0],s=i=r[1];else if(r.length===4){var l=Ki(r,4);n=l[0],i=l[1],a=l[2],s=l[3]}return t.x1-=s,t.x2+=i,t.y1-=n,t.y2+=a,t.w=t.x2-t.x1,t.h=t.y2-t.y1,t},"expandBoundingBoxSides"),eve=o(function(t,r){t.x1=r.x1,t.y1=r.y1,t.x2=r.x2,t.y2=r.y2,t.w=t.x2-t.x1,t.h=t.y2-t.y1},"assignBoundingBox"),AF=o(function(t,r){return!(t.x1>r.x2||r.x1>t.x2||t.x2r.y2||r.y1>t.y2)},"boundingBoxesIntersect"),$f=o(function(t,r,n){return t.x1<=r&&r<=t.x2&&t.y1<=n&&n<=t.y2},"inBoundingBox"),tve=o(function(t,r){return $f(t,r.x,r.y)},"pointInBoundingBox"),Abe=o(function(t,r){return $f(t,r.x1,r.y1)&&$f(t,r.x2,r.y2)},"boundingBoxInBoundingBox"),Wht=(FB=Math.hypot)!==null&&FB!==void 0?FB:function(e,t){return Math.sqrt(e*e+t*t)};o(qht,"inflatePolygon");o(Hht,"miterBox");Rbe=o(function(t,r,n,i,a,s,l){var u=arguments.length>7&&arguments[7]!==void 0?arguments[7]:"auto",h=u==="auto"?qf(a,s):u,d=a/2,f=s/2;h=Math.min(h,d,f);var p=h!==d,m=h!==f,g;if(p){var y=n-d+h-l,v=i-f-l,x=n+d-h+l,b=v;if(g=Ff(t,r,n,i,y,v,x,b,!1),g.length>0)return g}if(m){var T=n+d+l,k=i-f+h-l,C=T,w=i+f-h+l;if(g=Ff(t,r,n,i,T,k,C,w,!1),g.length>0)return g}if(p){var S=n-d+h-l,R=i+f+l,L=n+d-h+l,N=R;if(g=Ff(t,r,n,i,S,R,L,N,!1),g.length>0)return g}if(m){var I=n-d-l,_=i-f+h-l,A=I,M=i+f-h+l;if(g=Ff(t,r,n,i,I,_,A,M,!1),g.length>0)return g}var D;{var P=n-d+h,B=i-f+h;if(D=LT(t,r,n,i,P,B,h+l),D.length>0&&D[0]<=P&&D[1]<=B)return[D[0],D[1]]}{var O=n+d-h,$=i-f+h;if(D=LT(t,r,n,i,O,$,h+l),D.length>0&&D[0]>=O&&D[1]<=$)return[D[0],D[1]]}{var V=n+d-h,G=i+f-h;if(D=LT(t,r,n,i,V,G,h+l),D.length>0&&D[0]>=V&&D[1]>=G)return[D[0],D[1]]}{var z=n-d+h,W=i+f-h;if(D=LT(t,r,n,i,z,W,h+l),D.length>0&&D[0]<=z&&D[1]>=W)return[D[0],D[1]]}return[]},"roundRectangleIntersectLine"),Uht=o(function(t,r,n,i,a,s,l){var u=l,h=Math.min(n,a),d=Math.max(n,a),f=Math.min(i,s),p=Math.max(i,s);return h-u<=t&&t<=d+u&&f-u<=r&&r<=p+u},"inLineVicinity"),Yht=o(function(t,r,n,i,a,s,l,u,h){var d={x1:Math.min(n,l,a)-h,x2:Math.max(n,l,a)+h,y1:Math.min(i,u,s)-h,y2:Math.max(i,u,s)+h};return!(td.x2||rd.y2)},"inBezierVicinity"),jht=o(function(t,r,n,i){n-=i;var a=r*r-4*t*n;if(a<0)return[];var s=Math.sqrt(a),l=2*t,u=(-r+s)/l,h=(-r-s)/l;return[u,h]},"solveQuadratic"),Xht=o(function(t,r,n,i,a){var s=1e-5;t===0&&(t=s),r/=t,n/=t,i/=t;var l,u,h,d,f,p,m,g;if(u=(3*n-r*r)/9,h=-(27*i)+r*(9*n-2*(r*r)),h/=54,l=u*u*u+h*h,a[1]=0,m=r/3,l>0){f=h+Math.sqrt(l),f=f<0?-Math.pow(-f,1/3):Math.pow(f,1/3),p=h-Math.sqrt(l),p=p<0?-Math.pow(-p,1/3):Math.pow(p,1/3),a[0]=-m+f+p,m+=(f+p)/2,a[4]=a[2]=-m,m=Math.sqrt(3)*(-p+f)/2,a[3]=m,a[5]=-m;return}if(a[5]=a[3]=0,l===0){g=h<0?-Math.pow(-h,1/3):Math.pow(h,1/3),a[0]=-m+2*g,a[4]=a[2]=-(g+m);return}u=-u,d=u*u*u,d=Math.acos(h/Math.sqrt(d)),g=2*Math.sqrt(u),a[0]=-m+g*Math.cos(d/3),a[2]=-m+g*Math.cos((d+2*Math.PI)/3),a[4]=-m+g*Math.cos((d+4*Math.PI)/3)},"solveCubic"),Kht=o(function(t,r,n,i,a,s,l,u){var h=1*n*n-4*n*a+2*n*l+4*a*a-4*a*l+l*l+i*i-4*i*s+2*i*u+4*s*s-4*s*u+u*u,d=9*n*a-3*n*n-3*n*l-6*a*a+3*a*l+9*i*s-3*i*i-3*i*u-6*s*s+3*s*u,f=3*n*n-6*n*a+n*l-n*t+2*a*a+2*a*t-l*t+3*i*i-6*i*s+i*u-i*r+2*s*s+2*s*r-u*r,p=1*n*a-n*n+n*t-a*t+i*s-i*i+i*r-s*r,m=[];Xht(h,d,f,p,m);for(var g=1e-7,y=[],v=0;v<6;v+=2)Math.abs(m[v+1])=0&&m[v]<=1&&y.push(m[v]);y.push(1),y.push(0);for(var x=-1,b,T,k,C=0;C=0?kh?(t-a)*(t-a)+(r-s)*(r-s):d-p},"sqdistToFiniteLine"),go=o(function(t,r,n){for(var i,a,s,l,u,h=0,d=0;d=t&&t>=s||i<=t&&t<=s)u=(t-i)/(s-i)*(l-a)+a,u>r&&h++;else continue;return h%2!==0},"pointInsidePolygonPoints"),Bh=o(function(t,r,n,i,a,s,l,u,h){var d=new Array(n.length),f;u[0]!=null?(f=Math.atan(u[1]/u[0]),u[0]<0?f=f+Math.PI/2:f=-f-Math.PI/2):f=u;for(var p=Math.cos(-f),m=Math.sin(-f),g=0;g0){var v=tA(d,-h);y=eA(v)}else y=d;return go(t,r,y)},"pointInsidePolygon"),Qht=o(function(t,r,n,i,a,s,l,u){for(var h=new Array(n.length*2),d=0;d=0&&v<=1&&b.push(v),x>=0&&x<=1&&b.push(x),b.length===0)return[];var T=b[0]*u[0]+t,k=b[0]*u[1]+r;if(b.length>1){if(b[0]==b[1])return[T,k];var C=b[1]*u[0]+t,w=b[1]*u[1]+r;return[T,k,C,w]}else return[T,k]},"intersectLineCircle"),zB=o(function(t,r,n){return r<=t&&t<=n||n<=t&&t<=r?t:t<=r&&r<=n||n<=r&&r<=t?r:n},"midOfThree"),Ff=o(function(t,r,n,i,a,s,l,u,h){var d=t-a,f=n-t,p=l-a,m=r-s,g=i-r,y=u-s,v=p*m-y*d,x=f*m-g*d,b=y*f-p*g;if(b!==0){var T=v/b,k=x/b,C=.001,w=0-C,S=1+C;return w<=T&&T<=S&&w<=k&&k<=S?[t+T*f,r+T*g]:h?[t+T*f,r+T*g]:[]}else return v===0||x===0?zB(t,n,l)===l?[l,u]:zB(t,n,a)===a?[a,s]:zB(a,l,n)===n?[n,i]:[]:[]},"finiteLinesIntersect"),edt=o(function(t,r,n,i,a){var s=[],l=i/2,u=a/2,h=r,d=n;s.push({x:h+l*t[0],y:d+u*t[1]});for(var f=1;f0){var y=tA(f,-u);m=eA(y)}else m=f}else m=n;for(var v,x,b,T,k=0;k2){for(var g=[d[0],d[1]],y=Math.pow(g[0]-t,2)+Math.pow(g[1]-r,2),v=1;vd&&(d=k)},"set"),get:o(function(T){return h[T]},"get")},p=0;p0?D=M.edgesTo(A)[0]:D=A.edgesTo(M)[0];var P=i(D);A=A.id(),S[A]>S[I]+P&&(S[A]=S[I]+P,R.nodes.indexOf(A)<0?R.push(A):R.updateItem(A),w[A]=0,C[A]=[]),S[A]==S[I]+P&&(w[A]=w[A]+w[I],C[A].push(I))}else for(var B=0;B0;){for(var G=k.pop(),z=0;z0&&l.push(n[u]);l.length!==0&&a.push(i.collection(l))}return a},"assign"),mdt=o(function(t,r){for(var n=0;n5&&arguments[5]!==void 0?arguments[5]:vdt,l=i,u,h,d=0;d=2?wT(t,r,n,0,sve,xdt):wT(t,r,n,0,ave)},"euclidean"),squaredEuclidean:o(function(t,r,n){return wT(t,r,n,0,sve)},"squaredEuclidean"),manhattan:o(function(t,r,n){return wT(t,r,n,0,ave)},"manhattan"),max:o(function(t,r,n){return wT(t,r,n,-1/0,bdt)},"max")};G1["squared-euclidean"]=G1.squaredEuclidean;G1.squaredeuclidean=G1.squaredEuclidean;o(pA,"clusteringDistance");Tdt=Va({k:2,m:2,sensitivityThreshold:1e-4,distance:"euclidean",maxIterations:10,attributes:[],testMode:!1,testCentroids:null}),_F=o(function(t){return Tdt(t)},"setOptions"),rA=o(function(t,r,n,i,a){var s=a!=="kMedoids",l=s?function(f){return n[f]}:function(f){return i[f](n)},u=o(function(p){return i[p](r)},"getQ"),h=n,d=r;return pA(t,i.length,l,u,h,d)},"getDist"),VB=o(function(t,r,n){for(var i=n.length,a=new Array(i),s=new Array(i),l=new Array(r),u=null,h=0;hn)return!1}return!0},"haveMatricesConverged"),kdt=o(function(t,r,n){for(var i=0;il&&(l=r[h][d],u=d);a[u].push(t[h])}for(var f=0;f=a.threshold||a.mode==="dendrogram"&&t.length===1)return!1;var g=r[s],y=r[i[s]],v;a.mode==="dendrogram"?v={left:g,right:y,key:g.key}:v={value:g.value.concat(y.value),key:g.key},t[g.index]=v,t.splice(y.index,1),r[g.key]=v;for(var x=0;xn[y.key][b.key]&&(u=n[y.key][b.key])):a.linkage==="max"?(u=n[g.key][b.key],n[g.key][b.key]0&&i.push(a);return i},"findExemplars"),dve=o(function(t,r,n){for(var i=[],a=0;al&&(s=h,l=r[a*t+h])}s>0&&i.push(s)}for(var d=0;dh&&(u=d,h=f)}n[a]=s[u]}return i=dve(t,r,n),i},"assign"),fve=o(function(t){for(var r=this.cy(),n=this.nodes(),i=Odt(t),a={},s=0;s=I?(_=I,I=M,A=D):M>_&&(_=M);for(var P=0;P0?1:0;S[L%i.minIterations*l+z]=W,G+=W}if(G>0&&(L>=i.minIterations-1||L==i.maxIterations-1)){for(var H=0,j=0;j1||w>1)&&(l=!0),f[T]=[],b.outgoers().forEach(function(R){R.isEdge()&&f[T].push(R.id())})}else p[T]=[void 0,b.target().id()]}):s.forEach(function(b){var T=b.id();if(b.isNode()){var k=b.degree(!0);k%2&&(u?h?l=!0:h=T:u=T),f[T]=[],b.connectedEdges().forEach(function(C){return f[T].push(C.id())})}else p[T]=[b.source().id(),b.target().id()]});var m={found:!1,trail:void 0};if(l)return m;if(h&&u)if(a){if(d&&h!=d)return m;d=h}else{if(d&&h!=d&&u!=d)return m;d||(d=h)}else d||(d=s[0].id());var g=o(function(T){for(var k=T,C=[T],w,S,R;f[k].length;)w=f[k].shift(),S=p[w][0],R=p[w][1],k!=R?(f[R]=f[R].filter(function(L){return L!=w}),k=R):!a&&k!=S&&(f[S]=f[S].filter(function(L){return L!=w}),k=S),C.unshift(w),C.unshift(k);return C},"walk"),y=[],v=[];for(v=g(d);v.length!=1;)f[v[0]].length==0?(y.unshift(s.getElementById(v.shift())),y.unshift(s.getElementById(v.shift()))):v=g(v.shift()).concat(v);y.unshift(s.getElementById(v.shift()));for(var x in f)if(f[x].length)return m;return m.found=!0,m.trail=this.spawn(y,!0),m},"hierholzer")},L5=o(function(){var t=this,r={},n=0,i=0,a=[],s=[],l={},u=o(function(p,m){for(var g=s.length-1,y=[],v=t.spawn();s[g].x!=p||s[g].y!=m;)y.push(s.pop().edge),g--;y.push(s.pop().edge),y.forEach(function(x){var b=x.connectedNodes().intersection(t);v.merge(x),b.forEach(function(T){var k=T.id(),C=T.connectedEdges().intersection(t);v.merge(T),r[k].cutVertex?v.merge(C.filter(function(w){return w.isLoop()})):v.merge(C)})}),a.push(v)},"buildComponent"),h=o(function(p,m,g){p===g&&(i+=1),r[m]={id:n,low:n++,cutVertex:!1};var y=t.getElementById(m).connectedEdges().intersection(t);if(y.size()===0)a.push(t.spawn(t.getElementById(m)));else{var v,x,b,T;y.forEach(function(k){v=k.source().id(),x=k.target().id(),b=v===m?x:v,b!==g&&(T=k.id(),l[T]||(l[T]=!0,s.push({x:m,y:b,edge:k})),b in r?r[m].low=Math.min(r[m].low,r[b].id):(h(p,b,m),r[m].low=Math.min(r[m].low,r[b].low),r[m].id<=r[b].low&&(r[m].cutVertex=!0,u(m,b))))})}},"biconnectedSearch");t.forEach(function(f){if(f.isNode()){var p=f.id();p in r||(i=0,h(p,p),r[p].cutVertex=i>1)}});var d=Object.keys(r).filter(function(f){return r[f].cutVertex}).map(function(f){return t.getElementById(f)});return{cut:t.spawn(d),components:a}},"hopcroftTarjanBiconnected"),qdt={hopcroftTarjanBiconnected:L5,htbc:L5,htb:L5,hopcroftTarjanBiconnectedComponents:L5},D5=o(function(){var t=this,r={},n=0,i=[],a=[],s=t.spawn(t),l=o(function(h){a.push(h),r[h]={index:n,low:n++,explored:!1};var d=t.getElementById(h).connectedEdges().intersection(t);if(d.forEach(function(y){var v=y.target().id();v!==h&&(v in r||l(v),r[v].explored||(r[h].low=Math.min(r[h].low,r[v].low)))}),r[h].index===r[h].low){for(var f=t.spawn();;){var p=a.pop();if(f.merge(t.getElementById(p)),r[p].low=r[h].index,r[p].explored=!0,p===h)break}var m=f.edgesWith(f),g=f.merge(m);i.push(g),s=s.difference(g)}},"stronglyConnectedSearch");return t.forEach(function(u){if(u.isNode()){var h=u.id();h in r||l(h)}}),{cut:s,components:i}},"tarjanStronglyConnected"),Hdt={tarjanStronglyConnected:D5,tsc:D5,tscc:D5,tarjanStronglyConnectedComponents:D5},Pbe={};[$T,Tht,Cht,kht,Eht,Rht,Dht,idt,B1,$1,tF,ydt,Ldt,Ndt,Gdt,Wdt,qdt,Hdt].forEach(function(e){br(Pbe,e)});Obe=0,Bbe=1,$be=2,vc=o(function(t){if(!(this instanceof vc))return new vc(t);this.id="Thenable/1.0.7",this.state=Obe,this.fulfillValue=void 0,this.rejectReason=void 0,this.onFulfilled=[],this.onRejected=[],this.proxy={then:this.then.bind(this)},typeof t=="function"&&t.call(this,this.fulfill.bind(this),this.reject.bind(this))},"api");vc.prototype={fulfill:o(function(t){return pve(this,Bbe,"fulfillValue",t)},"fulfill"),reject:o(function(t){return pve(this,$be,"rejectReason",t)},"reject"),then:o(function(t,r){var n=this,i=new vc;return n.onFulfilled.push(gve(t,i,"fulfill")),n.onRejected.push(gve(r,i,"reject")),Fbe(n),i.proxy},"then")};pve=o(function(t,r,n,i){return t.state===Obe&&(t.state=r,t[n]=i,Fbe(t)),t},"deliver"),Fbe=o(function(t){t.state===Bbe?mve(t,"onFulfilled",t.fulfillValue):t.state===$be&&mve(t,"onRejected",t.rejectReason)},"execute"),mve=o(function(t,r,n){if(t[r].length!==0){var i=t[r];t[r]=[];var a=o(function(){for(var l=0;l0},"animatedImpl")},"animated"),clearQueue:o(function(){return o(function(){var r=this,n=r.length!==void 0,i=n?r:[r],a=this._private.cy||this;if(!a.styleEnabled())return this;for(var s=0;s0&&this.spawn(i).updateStyle().emit("class"),r},"classes"),addClass:o(function(t){return this.toggleClass(t,!0)},"addClass"),hasClass:o(function(t){var r=this[0];return r!=null&&r._private.classes.has(t)},"hasClass"),toggleClass:o(function(t,r){Hn(t)||(t=t.match(/\S+/g)||[]);for(var n=this,i=r===void 0,a=[],s=0,l=n.length;s0&&this.spawn(a).updateStyle().emit("class"),n},"toggleClass"),removeClass:o(function(t){return this.toggleClass(t,!1)},"removeClass"),flashClass:o(function(t,r){var n=this;if(r==null)r=250;else if(r===0)return n;return n.addClass(t),setTimeout(function(){n.removeClass(t)},r),n},"flashClass")};V5.className=V5.classNames=V5.classes;ln={metaChar:"[\\!\\\"\\#\\$\\%\\&\\'\\(\\)\\*\\+\\,\\.\\/\\:\\;\\<\\=\\>\\?\\@\\[\\]\\^\\`\\{\\|\\}\\~]",comparatorOp:"=|\\!=|>|>=|<|<=|\\$=|\\^=|\\*=",boolOp:"\\?|\\!|\\^",string:`"(?:\\\\"|[^"])*"|'(?:\\\\'|[^'])*'`,number:la,meta:"degree|indegree|outdegree",separator:"\\s*,\\s*",descendant:"\\s+",child:"\\s+>\\s+",subject:"\\$",group:"node|edge|\\*",directedEdge:"\\s+->\\s+",undirectedEdge:"\\s+<->\\s+"};ln.variable="(?:[\\w-.]|(?:\\\\"+ln.metaChar+"))+";ln.className="(?:[\\w-]|(?:\\\\"+ln.metaChar+"))+";ln.value=ln.string+"|"+ln.number;ln.id=ln.variable;(function(){var e,t,r;for(e=ln.comparatorOp.split("|"),r=0;r=0)&&t!=="="&&(ln.comparatorOp+="|\\!"+t)})();zn=o(function(){return{checks:[]}},"newQuery"),er={GROUP:0,COLLECTION:1,FILTER:2,DATA_COMPARE:3,DATA_EXIST:4,DATA_BOOL:5,META_COMPARE:6,STATE:7,ID:8,CLASS:9,UNDIRECTED_EDGE:10,DIRECTED_EDGE:11,NODE_SOURCE:12,NODE_TARGET:13,NODE_NEIGHBOR:14,CHILD:15,DESCENDANT:16,PARENT:17,ANCESTOR:18,COMPOUND_SPLIT:19,TRUE:20},aF=[{selector:":selected",matches:o(function(t){return t.selected()},"matches")},{selector:":unselected",matches:o(function(t){return!t.selected()},"matches")},{selector:":selectable",matches:o(function(t){return t.selectable()},"matches")},{selector:":unselectable",matches:o(function(t){return!t.selectable()},"matches")},{selector:":locked",matches:o(function(t){return t.locked()},"matches")},{selector:":unlocked",matches:o(function(t){return!t.locked()},"matches")},{selector:":visible",matches:o(function(t){return t.visible()},"matches")},{selector:":hidden",matches:o(function(t){return!t.visible()},"matches")},{selector:":transparent",matches:o(function(t){return t.transparent()},"matches")},{selector:":grabbed",matches:o(function(t){return t.grabbed()},"matches")},{selector:":free",matches:o(function(t){return!t.grabbed()},"matches")},{selector:":removed",matches:o(function(t){return t.removed()},"matches")},{selector:":inside",matches:o(function(t){return!t.removed()},"matches")},{selector:":grabbable",matches:o(function(t){return t.grabbable()},"matches")},{selector:":ungrabbable",matches:o(function(t){return!t.grabbable()},"matches")},{selector:":animated",matches:o(function(t){return t.animated()},"matches")},{selector:":unanimated",matches:o(function(t){return!t.animated()},"matches")},{selector:":parent",matches:o(function(t){return t.isParent()},"matches")},{selector:":childless",matches:o(function(t){return t.isChildless()},"matches")},{selector:":child",matches:o(function(t){return t.isChild()},"matches")},{selector:":orphan",matches:o(function(t){return t.isOrphan()},"matches")},{selector:":nonorphan",matches:o(function(t){return t.isChild()},"matches")},{selector:":compound",matches:o(function(t){return t.isNode()?t.isParent():t.source().isParent()||t.target().isParent()},"matches")},{selector:":loop",matches:o(function(t){return t.isLoop()},"matches")},{selector:":simple",matches:o(function(t){return t.isSimple()},"matches")},{selector:":active",matches:o(function(t){return t.active()},"matches")},{selector:":inactive",matches:o(function(t){return!t.active()},"matches")},{selector:":backgrounding",matches:o(function(t){return t.backgrounding()},"matches")},{selector:":nonbackgrounding",matches:o(function(t){return!t.backgrounding()},"matches")}].sort(function(e,t){return Fut(e.selector,t.selector)}),Vft=(function(){for(var e={},t,r=0;r0&&d.edgeCount>0)return In("The selector `"+t+"` is invalid because it uses both a compound selector and an edge selector"),!1;if(d.edgeCount>1)return In("The selector `"+t+"` is invalid because it uses multiple edge selectors"),!1;d.edgeCount===1&&In("The selector `"+t+"` is deprecated. Edge selectors do not take effect on changes to source and target nodes after an edge is added, for performance reasons. Use a class or data selector on edges instead, updating the class or data of an edge when your app detects a change in source or target nodes.")}return!0},"parse"),jft=o(function(){if(this.toStringCache!=null)return this.toStringCache;for(var t=o(function(d){return d??""},"clean"),r=o(function(d){return fr(d)?'"'+d+'"':t(d)},"cleanVal"),n=o(function(d){return" "+d+" "},"space"),i=o(function(d,f){var p=d.type,m=d.value;switch(p){case er.GROUP:{var g=t(m);return g.substring(0,g.length-1)}case er.DATA_COMPARE:{var y=d.field,v=d.operator;return"["+y+n(t(v))+r(m)+"]"}case er.DATA_BOOL:{var x=d.operator,b=d.field;return"["+t(x)+b+"]"}case er.DATA_EXIST:{var T=d.field;return"["+T+"]"}case er.META_COMPARE:{var k=d.operator,C=d.field;return"[["+C+n(t(k))+r(m)+"]]"}case er.STATE:return m;case er.ID:return"#"+m;case er.CLASS:return"."+m;case er.PARENT:case er.CHILD:return a(d.parent,f)+n(">")+a(d.child,f);case er.ANCESTOR:case er.DESCENDANT:return a(d.ancestor,f)+" "+a(d.descendant,f);case er.COMPOUND_SPLIT:{var w=a(d.left,f),S=a(d.subject,f),R=a(d.right,f);return w+(w.length>0?" ":"")+S+R}case er.TRUE:return""}},"checkToString"),a=o(function(d,f){return d.checks.reduce(function(p,m,g){return p+(f===d&&g===0?"$":"")+i(m,f)},"")},"queryToString"),s="",l=0;l1&&l=0&&(r=r.replace("!",""),f=!0),r.indexOf("@")>=0&&(r=r.replace("@",""),d=!0),(a||l||d)&&(u=!a&&!s?"":""+t,h=""+n),d&&(t=u=u.toLowerCase(),n=h=h.toLowerCase()),r){case"*=":i=u.indexOf(h)>=0;break;case"$=":i=u.indexOf(h,u.length-h.length)>=0;break;case"^=":i=u.indexOf(h)===0;break;case"=":i=t===n;break;case">":p=!0,i=t>n;break;case">=":p=!0,i=t>=n;break;case"<":p=!0,i=t1&&arguments[1]!==void 0?arguments[1]:!0;return MF(this,e,t,Ybe)};o(jbe,"addParent");V1.forEachUp=function(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0;return MF(this,e,t,jbe)};o(rpt,"addParentAndChildren");V1.forEachUpAndDown=function(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0;return MF(this,e,t,rpt)};V1.ancestors=V1.parents;GT=Xbe={data:Dn.data({field:"data",bindingEvent:"data",allowBinding:!0,allowSetting:!0,settingEvent:"data",settingTriggersEvent:!0,triggerFnName:"trigger",allowGetting:!0,immutableKeys:{id:!0,source:!0,target:!0,parent:!0},updateStyle:!0}),removeData:Dn.removeData({field:"data",event:"data",triggerFnName:"trigger",triggerEvent:!0,immutableKeys:{id:!0,source:!0,target:!0,parent:!0},updateStyle:!0}),scratch:Dn.data({field:"scratch",bindingEvent:"scratch",allowBinding:!0,allowSetting:!0,settingEvent:"scratch",settingTriggersEvent:!0,triggerFnName:"trigger",allowGetting:!0,updateStyle:!0}),removeScratch:Dn.removeData({field:"scratch",event:"scratch",triggerFnName:"trigger",triggerEvent:!0,updateStyle:!0}),rscratch:Dn.data({field:"rscratch",allowBinding:!1,allowSetting:!0,settingTriggersEvent:!1,allowGetting:!0}),removeRscratch:Dn.removeData({field:"rscratch",triggerEvent:!1}),id:o(function(){var t=this[0];if(t)return t._private.data.id},"id")};GT.attr=GT.data;GT.removeAttr=GT.removeData;npt=Xbe,xA={};o(z$,"defineDegreeFunction");br(xA,{degree:z$(function(e,t){return t.source().same(t.target())?2:1}),indegree:z$(function(e,t){return t.target().same(e)?1:0}),outdegree:z$(function(e,t){return t.source().same(e)?1:0})});o(E1,"defineDegreeBoundsFunction");br(xA,{minDegree:E1("degree",function(e,t){return et}),minIndegree:E1("indegree",function(e,t){return et}),minOutdegree:E1("outdegree",function(e,t){return et})});br(xA,{totalDegree:o(function(t){for(var r=0,n=this.nodes(),i=0;i0,p=f;f&&(d=d[0]);var m=p?d.position():{x:0,y:0};r!==void 0?h.position(t,r+m[t]):a!==void 0&&h.position({x:a.x+m.x,y:a.y+m.y})}else{var g=n.position(),y=l?n.parent():null,v=y&&y.length>0,x=v;v&&(y=y[0]);var b=x?y.position():{x:0,y:0};return a={x:g.x-b.x,y:g.y-b.y},t===void 0?a:a[t]}else if(!s)return;return this},"relativePosition")};yc.modelPosition=yc.point=yc.position;yc.modelPositions=yc.points=yc.positions;yc.renderedPoint=yc.renderedPosition;yc.relativePoint=yc.relativePosition;ipt=Kbe;F1=Kf={};Kf.renderedBoundingBox=function(e){var t=this.boundingBox(e),r=this.cy(),n=r.zoom(),i=r.pan(),a=t.x1*n+i.x,s=t.x2*n+i.x,l=t.y1*n+i.y,u=t.y2*n+i.y;return{x1:a,x2:s,y1:l,y2:u,w:s-a,h:u-l}};Kf.dirtyCompoundBoundsCache=function(){var e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:!1,t=this.cy();return!t.styleEnabled()||!t.hasCompoundNodes()?this:(this.forEachUp(function(r){if(r.isParent()){var n=r._private;n.compoundBoundsClean=!1,n.bbCache=null,e||r.emitAndNotify("bounds")}}),this)};Kf.updateCompoundBounds=function(){var e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:!1,t=this.cy();if(!t.styleEnabled()||!t.hasCompoundNodes())return this;if(!e&&t.batching())return this;function r(s){if(!s.isParent())return;var l=s._private,u=s.children(),h=s.pstyle("compound-sizing-wrt-labels").value==="include",d={width:{val:s.pstyle("min-width").pfValue,left:s.pstyle("min-width-bias-left"),right:s.pstyle("min-width-bias-right")},height:{val:s.pstyle("min-height").pfValue,top:s.pstyle("min-height-bias-top"),bottom:s.pstyle("min-height-bias-bottom")}},f=u.boundingBox({includeLabels:h,includeOverlays:!1,useCache:!1}),p=l.position;(f.w===0||f.h===0)&&(f={w:s.pstyle("width").pfValue,h:s.pstyle("height").pfValue},f.x1=p.x-f.w/2,f.x2=p.x+f.w/2,f.y1=p.y-f.h/2,f.y2=p.y+f.h/2);function m(L,N,I){var _=0,A=0,M=N+I;return L>0&&M>0&&(_=N/M*L,A=I/M*L),{biasDiff:_,biasComplementDiff:A}}o(m,"computeBiasValues");function g(L,N,I,_){if(I.units==="%")switch(_){case"width":return L>0?I.pfValue*L:0;case"height":return N>0?I.pfValue*N:0;case"average":return L>0&&N>0?I.pfValue*(L+N)/2:0;case"min":return L>0&&N>0?L>N?I.pfValue*N:I.pfValue*L:0;case"max":return L>0&&N>0?L>N?I.pfValue*L:I.pfValue*N:0;default:return 0}else return I.units==="px"?I.pfValue:0}o(g,"computePaddingValues");var y=d.width.left.value;d.width.left.units==="px"&&d.width.val>0&&(y=y*100/d.width.val);var v=d.width.right.value;d.width.right.units==="px"&&d.width.val>0&&(v=v*100/d.width.val);var x=d.height.top.value;d.height.top.units==="px"&&d.height.val>0&&(x=x*100/d.height.val);var b=d.height.bottom.value;d.height.bottom.units==="px"&&d.height.val>0&&(b=b*100/d.height.val);var T=m(d.width.val-f.w,y,v),k=T.biasDiff,C=T.biasComplementDiff,w=m(d.height.val-f.h,x,b),S=w.biasDiff,R=w.biasComplementDiff;l.autoPadding=g(f.w,f.h,s.pstyle("padding"),s.pstyle("padding-relative-to").value),l.autoWidth=Math.max(f.w,d.width.val),p.x=(-k+f.x1+f.x2+C)/2,l.autoHeight=Math.max(f.h,d.height.val),p.y=(-S+f.y1+f.y2+R)/2}o(r,"update");for(var n=0;nt.x2?i:t.x2,t.y1=nt.y2?a:t.y2,t.w=t.x2-t.x1,t.h=t.y2-t.y1)},"updateBounds"),Of=o(function(t,r){return r==null?t:gc(t,r.x1,r.y1,r.x2,r.y2)},"updateBoundsFromBox"),kT=o(function(t,r,n){return mo(t,r,n)},"prefixedProperty"),I5=o(function(t,r,n){if(!r.cy().headless()){var i=r._private,a=i.rstyle,s=a.arrowWidth/2,l=r.pstyle(n+"-arrow-shape").value,u,h;if(l!=="none"){n==="source"?(u=a.srcX,h=a.srcY):n==="target"?(u=a.tgtX,h=a.tgtY):(u=a.midX,h=a.midY);var d=i.arrowBounds=i.arrowBounds||{},f=d[n]=d[n]||{};f.x1=u-s,f.y1=h-s,f.x2=u+s,f.y2=h+s,f.w=f.x2-f.x1,f.h=f.y2-f.y1,z5(f,1),gc(t,f.x1,f.y1,f.x2,f.y2)}}},"updateBoundsFromArrow"),G$=o(function(t,r,n){if(!r.cy().headless()){var i;n?i=n+"-":i="";var a=r._private,s=a.rstyle,l=r.pstyle(i+"label").strValue;if(l){var u=r.pstyle("text-halign"),h=r.pstyle("text-valign"),d=kT(s,"labelWidth",n),f=kT(s,"labelHeight",n),p=kT(s,"labelX",n),m=kT(s,"labelY",n),g=r.pstyle(i+"text-margin-x").pfValue,y=r.pstyle(i+"text-margin-y").pfValue,v=r.isEdge(),x=r.pstyle(i+"text-rotation"),b=r.pstyle("text-outline-width").pfValue,T=r.pstyle("text-border-width").pfValue,k=T/2,C=r.pstyle("text-background-padding").pfValue,w=2,S=f,R=d,L=R/2,N=S/2,I,_,A,M;if(v)I=p-L,_=p+L,A=m-N,M=m+N;else{switch(u.value){case"left":I=p-R,_=p;break;case"center":I=p-L,_=p+L;break;case"right":I=p,_=p+R;break}switch(h.value){case"top":A=m-S,M=m;break;case"center":A=m-N,M=m+N;break;case"bottom":A=m,M=m+S;break}}var D=g-Math.max(b,k)-C-w,P=g+Math.max(b,k)+C+w,B=y-Math.max(b,k)-C-w,O=y+Math.max(b,k)+C+w;I+=D,_+=P,A+=B,M+=O;var $=n||"main",V=a.labelBounds,G=V[$]=V[$]||{};G.x1=I,G.y1=A,G.x2=_,G.y2=M,G.w=_-I,G.h=M-A,G.leftPad=D,G.rightPad=P,G.topPad=B,G.botPad=O;var z=v&&x.strValue==="autorotate",W=x.pfValue!=null&&x.pfValue!==0;if(z||W){var H=z?kT(a.rstyle,"labelAngle",n):x.pfValue,j=Math.cos(H),Q=Math.sin(H),U=(I+_)/2,oe=(A+M)/2;if(!v){switch(u.value){case"left":U=_;break;case"right":U=I;break}switch(h.value){case"top":oe=M;break;case"bottom":oe=A;break}}var te=o(function(Oe,ue){return Oe=Oe-U,ue=ue-oe,{x:Oe*j-ue*Q+U,y:Oe*Q+ue*j+oe}},"rotate"),le=te(I,A),ie=te(I,M),ae=te(_,A),Re=te(_,M);I=Math.min(le.x,ie.x,ae.x,Re.x),_=Math.max(le.x,ie.x,ae.x,Re.x),A=Math.min(le.y,ie.y,ae.y,Re.y),M=Math.max(le.y,ie.y,ae.y,Re.y)}var be=$+"Rot",Pe=V[be]=V[be]||{};Pe.x1=I,Pe.y1=A,Pe.x2=_,Pe.y2=M,Pe.w=_-I,Pe.h=M-A,gc(t,I,A,_,M),gc(a.labelBounds.all,I,A,_,M)}return t}},"updateBoundsFromLabel"),mxe=o(function(t,r){if(!r.cy().headless()){var n=r.pstyle("outline-opacity").value,i=r.pstyle("outline-width").value,a=r.pstyle("outline-offset").value,s=i+a;Qbe(t,r,n,s,"outside",s/2)}},"updateBoundsFromOutline"),Qbe=o(function(t,r,n,i,a,s){if(!(n===0||i<=0||a==="inside")){var l=r.cy(),u=l.renderer(),h=u.nodeShapes[u.getNodeShape(r)];if(h){var d=r.position(),f=d.x,p=d.y,m=r.width(),g=r.height();if(h.hasMiterBounds){a==="center"&&(i/=2);var y=h.miterBounds(f,p,m,g,i);Of(t,y)}else s!=null&&s>0&&G5(t,[s,s,s,s])}}},"updateBoundsFromMiter"),apt=o(function(t,r){if(!r.cy().headless()){var n=r.pstyle("border-opacity").value,i=r.pstyle("border-width").pfValue,a=r.pstyle("border-position").value;Qbe(t,r,n,i,a)}},"updateBoundsFromMiterBorder"),spt=o(function(t,r){var n=t._private.cy,i=n.styleEnabled(),a=n.headless(),s=Ns(),l=t._private,u=t.isNode(),h=t.isEdge(),d,f,p,m,g,y,v=l.rstyle,x=u&&i?t.pstyle("bounds-expansion").pfValue:[0],b=o(function(Ge){return Ge.pstyle("display").value!=="none"},"isDisplayed"),T=!i||b(t)&&(!h||b(t.source())&&b(t.target()));if(T){var k=0,C=0;i&&r.includeOverlays&&(k=t.pstyle("overlay-opacity").value,k!==0&&(C=t.pstyle("overlay-padding").value));var w=0,S=0;i&&r.includeUnderlays&&(w=t.pstyle("underlay-opacity").value,w!==0&&(S=t.pstyle("underlay-padding").value));var R=Math.max(C,S),L=0,N=0;if(i&&(L=t.pstyle("width").pfValue,N=L/2),u&&r.includeNodes){var I=t.position();g=I.x,y=I.y;var _=t.outerWidth(),A=_/2,M=t.outerHeight(),D=M/2;d=g-A,f=g+A,p=y-D,m=y+D,gc(s,d,p,f,m),i&&mxe(s,t),i&&r.includeOutlines&&!a&&mxe(s,t),i&&apt(s,t)}else if(h&&r.includeEdges)if(i&&!a){var P=t.pstyle("curve-style").strValue;if(d=Math.min(v.srcX,v.midX,v.tgtX),f=Math.max(v.srcX,v.midX,v.tgtX),p=Math.min(v.srcY,v.midY,v.tgtY),m=Math.max(v.srcY,v.midY,v.tgtY),d-=N,f+=N,p-=N,m+=N,gc(s,d,p,f,m),P==="haystack"){var B=v.haystackPts;if(B&&B.length===2){if(d=B[0].x,p=B[0].y,f=B[1].x,m=B[1].y,d>f){var O=d;d=f,f=O}if(p>m){var $=p;p=m,m=$}gc(s,d-N,p-N,f+N,m+N)}}else if(P==="bezier"||P==="unbundled-bezier"||Bf(P,"segments")||Bf(P,"taxi")){var V;switch(P){case"bezier":case"unbundled-bezier":V=v.bezierPts;break;case"segments":case"taxi":case"round-segments":case"round-taxi":V=v.linePts;break}if(V!=null)for(var G=0;Gf){var U=d;d=f,f=U}if(p>m){var oe=p;p=m,m=oe}d-=N,f+=N,p-=N,m+=N,gc(s,d,p,f,m)}if(i&&r.includeEdges&&h&&(I5(s,t,"mid-source"),I5(s,t,"mid-target"),I5(s,t,"source"),I5(s,t,"target")),i){var te=t.pstyle("ghost").value==="yes";if(te){var le=t.pstyle("ghost-offset-x").pfValue,ie=t.pstyle("ghost-offset-y").pfValue;gc(s,s.x1+le,s.y1+ie,s.x2+le,s.y2+ie)}}var ae=l.bodyBounds=l.bodyBounds||{};eve(ae,s),G5(ae,x),z5(ae,1),i&&(d=s.x1,f=s.x2,p=s.y1,m=s.y2,gc(s,d-R,p-R,f+R,m+R));var Re=l.overlayBounds=l.overlayBounds||{};eve(Re,s),G5(Re,x),z5(Re,1);var be=l.labelBounds=l.labelBounds||{};be.all!=null?Ght(be.all):be.all=Ns(),i&&r.includeLabels&&(r.includeMainLabels&&G$(s,t,null),h&&(r.includeSourceLabels&&G$(s,t,"source"),r.includeTargetLabels&&G$(s,t,"target")))}return s.x1=kl(s.x1),s.y1=kl(s.y1),s.x2=kl(s.x2),s.y2=kl(s.y2),s.w=kl(s.x2-s.x1),s.h=kl(s.y2-s.y1),s.w>0&&s.h>0&&T&&(G5(s,x),z5(s,1)),s},"boundingBoxImpl"),Jbe=o(function(t){var r=0,n=o(function(s){return(s?1:0)<=0;l--)s(l);return this};Yf.removeAllListeners=function(){return this.removeListener("*")};Yf.emit=Yf.trigger=function(e,t,r){var n=this.listeners,i=n.length;return this.emitting++,Hn(t)||(t=[t]),Cpt(this,function(a,s){r!=null&&(n=[{event:s.event,type:s.type,namespace:s.namespace,callback:r}],i=n.length);for(var l=o(function(){var d=n[u];if(d.type===s.type&&(!d.namespace||d.namespace===s.namespace||d.namespace===bpt)&&a.eventMatches(a.context,d,s)){var f=[s];t!=null&&dht(f,t),a.beforeEmit(a.context,d,s),d.conf&&d.conf.one&&(a.listeners=a.listeners.filter(function(g){return g!==d}));var p=a.callbackContext(a.context,d,s),m=d.callback.apply(p,f);a.afterEmit(a.context,d,s),m===!1&&(s.stopPropagation(),s.preventDefault())}},"_loop2"),u=0;u1&&!s){var l=this.length-1,u=this[l],h=u._private.data.id;this[l]=void 0,this[t]=u,a.set(h,{ele:u,index:t})}return this.length--,this},"unmergeAt"),unmergeOne:o(function(t){t=t[0];var r=this._private,n=t._private.data.id,i=r.map,a=i.get(n);if(!a)return this;var s=a.index;return this.unmergeAt(s),this},"unmergeOne"),unmerge:o(function(t){var r=this._private.cy;if(!t)return this;if(t&&fr(t)){var n=t;t=r.mutableElements().filter(n)}for(var i=0;i=0;r--){var n=this[r];t(n)&&this.unmergeAt(r)}return this},"unmergeBy"),map:o(function(t,r){for(var n=[],i=this,a=0;an&&(n=u,i=l)}return{value:n,ele:i}},"max"),min:o(function(t,r){for(var n=1/0,i,a=this,s=0;s=0&&a"u"?"undefined":ca(Symbol))!=t&&ca(Symbol.iterator)!=t;r&&(nA[Symbol.iterator]=function(){var n=this,i={value:void 0,done:!1},a=0,s=this.length;return sbe({next:o(function(){return a1&&arguments[1]!==void 0?arguments[1]:!0,n=this[0],i=n.cy();if(i.styleEnabled()&&n){n._private.styleDirty&&(n._private.styleDirty=!1,i.style().apply(n));var a=n._private.style[t];return a??(r?i.style().getDefaultProperty(t):null)}},"parsedStyle"),numericStyle:o(function(t){var r=this[0];if(r.cy().styleEnabled()&&r){var n=r.pstyle(t);return n.pfValue!==void 0?n.pfValue:n.value}},"numericStyle"),numericStyleUnits:o(function(t){var r=this[0];if(r.cy().styleEnabled()&&r)return r.pstyle(t).units},"numericStyleUnits"),renderedStyle:o(function(t){var r=this.cy();if(!r.styleEnabled())return this;var n=this[0];if(n)return r.style().getRenderedStyle(n,t)},"renderedStyle"),style:o(function(t,r){var n=this.cy();if(!n.styleEnabled())return this;var i=!1,a=n.style();if(cn(t)){var s=t;a.applyBypass(this,s,i),this.emitAndNotify("style")}else if(fr(t))if(r===void 0){var l=this[0];return l?a.getStylePropertyValue(l,t):void 0}else a.applyBypass(this,t,r,i),this.emitAndNotify("style");else if(t===void 0){var u=this[0];return u?a.getRawStyle(u):void 0}return this},"style"),removeStyle:o(function(t){var r=this.cy();if(!r.styleEnabled())return this;var n=!1,i=r.style(),a=this;if(t===void 0)for(var s=0;s0&&t.push(d[0]),t.push(l[0])}return this.spawn(t,!0).filter(e)},"neighborhood"),closedNeighborhood:o(function(t){return this.neighborhood().add(this).filter(t)},"closedNeighborhood"),openNeighborhood:o(function(t){return this.neighborhood(t)},"openNeighborhood")});us.neighbourhood=us.neighborhood;us.closedNeighbourhood=us.closedNeighborhood;us.openNeighbourhood=us.openNeighborhood;br(us,{source:Sl(o(function(t){var r=this[0],n;return r&&(n=r._private.source||r.cy().collection()),n&&t?n.filter(t):n},"sourceImpl"),"source"),target:Sl(o(function(t){var r=this[0],n;return r&&(n=r._private.target||r.cy().collection()),n&&t?n.filter(t):n},"targetImpl"),"target"),sources:Exe({attr:"source"}),targets:Exe({attr:"target"})});o(Exe,"defineSourceFunction");br(us,{edgesWith:Sl(Axe(),"edgesWith"),edgesTo:Sl(Axe({thisIsSrc:!0}),"edgesTo")});o(Axe,"defineEdgesWithFunction");br(us,{connectedEdges:Sl(function(e){for(var t=[],r=this,n=0;n0);return s},"components"),component:o(function(){var t=this[0];return t.cy().mutableElements().components(t)[0]},"component")});us.componentsOf=us.components;Ga=o(function(t,r){var n=arguments.length>2&&arguments[2]!==void 0?arguments[2]:!1,i=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!1;if(t===void 0){pi("A collection must have a reference to the core");return}var a=new Ph,s=!1;if(!r)r=[];else if(r.length>0&&cn(r[0])&&!YT(r[0])){s=!0;for(var l=[],u=new W1,h=0,d=r.length;h0&&arguments[0]!==void 0?arguments[0]:!0,t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0,r=this,n=r.cy(),i=n._private,a=[],s=[],l,u=0,h=r.length;u0){for(var $=l.length===r.length?r:new Ga(n,l),V=0;V<$.length;V++){var G=$[V];G.isNode()||(G.parallelEdges().clearTraversalCache(),G.source().clearTraversalCache(),G.target().clearTraversalCache())}var z;i.hasCompoundNodes?z=n.collection().merge($).merge($.connectedNodes()).merge($.parent()):z=$,z.dirtyCompoundBoundsCache().dirtyBoundingBoxCache().updateStyle(e),e?$.emitAndNotify("add"):t&&$.emit("add")}return r};Kn.removed=function(){var e=this[0];return e&&e._private.removed};Kn.inside=function(){var e=this[0];return e&&!e._private.removed};Kn.remove=function(){var e=arguments.length>0&&arguments[0]!==void 0?arguments[0]:!0,t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0,r=this,n=[],i={},a=r._private.cy;function s(M){for(var D=M._private.edges,P=0;P0&&(e?I.emitAndNotify("remove"):t&&I.emit("remove"));for(var _=0;_d&&Math.abs(g.v)>d;);return p?function(y){return u[y*(u.length-1)|0]}:h},"springRK4Factory")})(),Xn=o(function(t,r,n,i){var a=Ipt(t,r,n,i);return function(s,l,u){return s+(l-s)*a(u)}},"cubicBezier"),q5={linear:o(function(t,r,n){return t+(r-t)*n},"linear"),ease:Xn(.25,.1,.25,1),"ease-in":Xn(.42,0,1,1),"ease-out":Xn(0,0,.58,1),"ease-in-out":Xn(.42,0,.58,1),"ease-in-sine":Xn(.47,0,.745,.715),"ease-out-sine":Xn(.39,.575,.565,1),"ease-in-out-sine":Xn(.445,.05,.55,.95),"ease-in-quad":Xn(.55,.085,.68,.53),"ease-out-quad":Xn(.25,.46,.45,.94),"ease-in-out-quad":Xn(.455,.03,.515,.955),"ease-in-cubic":Xn(.55,.055,.675,.19),"ease-out-cubic":Xn(.215,.61,.355,1),"ease-in-out-cubic":Xn(.645,.045,.355,1),"ease-in-quart":Xn(.895,.03,.685,.22),"ease-out-quart":Xn(.165,.84,.44,1),"ease-in-out-quart":Xn(.77,0,.175,1),"ease-in-quint":Xn(.755,.05,.855,.06),"ease-out-quint":Xn(.23,1,.32,1),"ease-in-out-quint":Xn(.86,0,.07,1),"ease-in-expo":Xn(.95,.05,.795,.035),"ease-out-expo":Xn(.19,1,.22,1),"ease-in-out-expo":Xn(1,0,0,1),"ease-in-circ":Xn(.6,.04,.98,.335),"ease-out-circ":Xn(.075,.82,.165,1),"ease-in-out-circ":Xn(.785,.135,.15,.86),spring:o(function(t,r,n){if(n===0)return q5.linear;var i=Mpt(t,r,n);return function(a,s,l){return a+(s-a)*i(l)}},"spring"),"cubic-bezier":Xn};o(_xe,"getEasedValue");o(Lxe,"getValue");o(A1,"ease");o(Npt,"step$1");o(ET,"valid");o(Ppt,"startAnimation");o(Dxe,"stepAll");Opt={animate:Dn.animate(),animation:Dn.animation(),animated:Dn.animated(),clearQueue:Dn.clearQueue(),delay:Dn.delay(),delayAnimation:Dn.delayAnimation(),stop:Dn.stop(),addToAnimationPool:o(function(t){var r=this;r.styleEnabled()&&r._private.aniEles.merge(t)},"addToAnimationPool"),stopAnimationLoop:o(function(){this._private.animationsRunning=!1},"stopAnimationLoop"),startAnimationLoop:o(function(){var t=this;if(t._private.animationsRunning=!0,!t.styleEnabled())return;function r(){t._private.animationsRunning&&Q5(o(function(a){Dxe(a,t),r()},"animationStep"))}o(r,"headlessStep");var n=t.renderer();n&&n.beforeRender?n.beforeRender(o(function(a,s){Dxe(s,t)},"rendererAnimationStep"),n.beforeRenderPriorities.animations):r()},"startAnimationLoop")},Bpt={qualifierCompare:o(function(t,r){return t==null||r==null?t==null&&r==null:t.sameText(r)},"qualifierCompare"),eventMatches:o(function(t,r,n){var i=r.qualifier;return i!=null?t!==n.target&&YT(n.target)&&i.matches(n.target):!0},"eventMatches"),addEventFields:o(function(t,r){r.cy=t,r.target=t},"addEventFields"),callbackContext:o(function(t,r,n){return r.qualifier!=null?n.target:t},"callbackContext")},P5=o(function(t){return fr(t)?new Hf(t):t},"argSelector"),u2e={createEmitter:o(function(){var t=this._private;return t.emitter||(t.emitter=new bA(Bpt,this)),this},"createEmitter"),emitter:o(function(){return this._private.emitter},"emitter"),on:o(function(t,r,n){return this.emitter().on(t,P5(r),n),this},"on"),removeListener:o(function(t,r,n){return this.emitter().removeListener(t,P5(r),n),this},"removeListener"),removeAllListeners:o(function(){return this.emitter().removeAllListeners(),this},"removeAllListeners"),one:o(function(t,r,n){return this.emitter().one(t,P5(r),n),this},"one"),once:o(function(t,r,n){return this.emitter().one(t,P5(r),n),this},"once"),emit:o(function(t,r){return this.emitter().emit(t,r),this},"emit"),emitAndNotify:o(function(t,r){return this.emit(t),this.notify(t,r),this},"emitAndNotify")};Dn.eventAliasesOn(u2e);oF={png:o(function(t){var r=this._private.renderer;return t=t||{},r.png(t)},"png"),jpg:o(function(t){var r=this._private.renderer;return t=t||{},t.bg=t.bg||"#fff",r.jpg(t)},"jpg")};oF.jpeg=oF.jpg;H5={layout:o(function(t){var r=this;if(t==null){pi("Layout options must be specified to make a layout");return}if(t.name==null){pi("A `name` must be specified to make a layout");return}var n=t.name,i=r.extension("layout",n);if(i==null){pi("No such layout `"+n+"` found. Did you forget to import it and `cytoscape.use()` it?");return}var a;fr(t.eles)?a=r.$(t.eles):a=t.eles!=null?t.eles:r.$();var s=new i(br({},t,{cy:r,eles:a}));return s},"layout")};H5.createLayout=H5.makeLayout=H5.layout;$pt={notify:o(function(t,r){var n=this._private;if(this.batching()){n.batchNotifications=n.batchNotifications||{};var i=n.batchNotifications[t]=n.batchNotifications[t]||this.collection();r!=null&&i.merge(r);return}if(n.notificationsEnabled){var a=this.renderer();this.destroyed()||!a||a.notify(t,r)}},"notify"),notifications:o(function(t){var r=this._private;return t===void 0?r.notificationsEnabled:(r.notificationsEnabled=!!t,this)},"notifications"),noNotifications:o(function(t){this.notifications(!1),t(),this.notifications(!0)},"noNotifications"),batching:o(function(){return this._private.batchCount>0},"batching"),startBatch:o(function(){var t=this._private;return t.batchCount==null&&(t.batchCount=0),t.batchCount===0&&(t.batchStyleEles=this.collection(),t.batchNotifications={}),t.batchCount++,this},"startBatch"),endBatch:o(function(){var t=this._private;if(t.batchCount===0)return this;if(t.batchCount--,t.batchCount===0){t.batchStyleEles.updateStyle();var r=this.renderer();Object.keys(t.batchNotifications).forEach(function(n){var i=t.batchNotifications[n];i.empty()?r.notify(n):r.notify(n,i)})}return this},"endBatch"),batch:o(function(t){return this.startBatch(),t(),this.endBatch(),this},"batch"),batchData:o(function(t){var r=this;return this.batch(function(){for(var n=Object.keys(t),i=0;i0;)r.removeChild(r.childNodes[0]);t._private.renderer=null,t.mutableElements().forEach(function(n){var i=n._private;i.rscratch={},i.rstyle={},i.animation.current=[],i.animation.queue=[]})},"destroyRenderer"),onRender:o(function(t){return this.on("render",t)},"onRender"),offRender:o(function(t){return this.off("render",t)},"offRender")};lF.invalidateDimensions=lF.resize;U5={collection:o(function(t,r){return fr(t)?this.$(t):Ho(t)?t.collection():Hn(t)?(r||(r={}),new Ga(this,t,r.unique,r.removed)):new Ga(this)},"collection"),nodes:o(function(t){var r=this.$(function(n){return n.isNode()});return t?r.filter(t):r},"nodes"),edges:o(function(t){var r=this.$(function(n){return n.isEdge()});return t?r.filter(t):r},"edges"),$:o(function(t){var r=this._private.elements;return t?r.filter(t):r.spawnSelf()},"$"),mutableElements:o(function(){return this._private.elements},"mutableElements")};U5.elements=U5.filter=U5.$;_a={},MT="t",zpt="f";_a.apply=function(e){for(var t=this,r=t._private,n=r.cy,i=n.collection(),a=0;a0;if(p||f&&m){var g=void 0;p&&m||p?g=h.properties:m&&(g=h.mappedProperties);for(var y=0;y1&&(k=1),l.color){var w=n.valueMin[0],S=n.valueMax[0],R=n.valueMin[1],L=n.valueMax[1],N=n.valueMin[2],I=n.valueMax[2],_=n.valueMin[3]==null?1:n.valueMin[3],A=n.valueMax[3]==null?1:n.valueMax[3],M=[Math.round(w+(S-w)*k),Math.round(R+(L-R)*k),Math.round(N+(I-N)*k),Math.round(_+(A-_)*k)];a={bypass:n.bypass,name:n.name,value:M,strValue:"rgb("+M[0]+", "+M[1]+", "+M[2]+")"}}else if(l.number){var D=n.valueMin+(n.valueMax-n.valueMin)*k;a=this.parse(n.name,D,n.bypass,p)}else return!1;if(!a)return y(),!1;a.mapping=n,n=a;break}case s.data:{for(var P=n.field.split("."),B=f.data,O=0;O0&&a>0){for(var l={},u=!1,h=0;h0?e.delayAnimation(s).play().promise().then(T):T()}).then(function(){return e.animation({style:l,duration:a,easing:e.pstyle("transition-timing-function").value,queue:!1}).play().promise()}).then(function(){r.removeBypasses(e,i),e.emitAndNotify("style"),n.transitioning=!1})}else n.transitioning&&(this.removeBypasses(e,i),e.emitAndNotify("style"),n.transitioning=!1)};_a.checkTrigger=function(e,t,r,n,i,a){var s=this.properties[t],l=i(s);e.removed()||l!=null&&l(r,n,e)&&a(s)};_a.checkZOrderTrigger=function(e,t,r,n){var i=this;this.checkTrigger(e,t,r,n,function(a){return a.triggersZOrder},function(){i._private.cy.notify("zorder",e)})};_a.checkBoundsTrigger=function(e,t,r,n){this.checkTrigger(e,t,r,n,function(i){return i.triggersBounds},function(i){e.dirtyCompoundBoundsCache(),e.dirtyBoundingBoxCache()})};_a.checkConnectedEdgesBoundsTrigger=function(e,t,r,n){this.checkTrigger(e,t,r,n,function(i){return i.triggersBoundsOfConnectedEdges},function(i){e.connectedEdges().forEach(function(a){a.dirtyBoundingBoxCache()})})};_a.checkParallelEdgesBoundsTrigger=function(e,t,r,n){this.checkTrigger(e,t,r,n,function(i){return i.triggersBoundsOfParallelEdges},function(i){e.parallelEdges().forEach(function(a){a.dirtyBoundingBoxCache()})})};_a.checkTriggers=function(e,t,r,n){e.dirtyStyleCache(),this.checkZOrderTrigger(e,t,r,n),this.checkBoundsTrigger(e,t,r,n),this.checkConnectedEdgesBoundsTrigger(e,t,r,n),this.checkParallelEdgesBoundsTrigger(e,t,r,n)};eC={};eC.applyBypass=function(e,t,r,n){var i=this,a=[],s=!0;if(t==="*"||t==="**"){if(r!==void 0)for(var l=0;li.length?n=n.substr(i.length):n=""}o(l,"removeSelAndBlockFromRemaining");function u(){a.length>s.length?a=a.substr(s.length):a=""}for(o(u,"removePropAndValFromRem");;){var h=n.match(/^\s*$/);if(h)break;var d=n.match(/^\s*((?:.|\s)+?)\s*\{((?:.|\s)+?)\}/);if(!d){In("Halting stylesheet parsing: String stylesheet contains more to parse but no selector and block found in: "+n);break}i=d[0];var f=d[1];if(f!=="core"){var p=new Hf(f);if(p.invalid){In("Skipping parsing of block: Invalid selector found in string stylesheet: "+f),l();continue}}var m=d[2],g=!1;a=m;for(var y=[];;){var v=a.match(/^\s*$/);if(v)break;var x=a.match(/^\s*(.+?)\s*:\s*(.+?)(?:\s*;|\s*$)/);if(!x){In("Skipping parsing of block: Invalid formatting of style property and value definitions found in:"+m),g=!0;break}s=x[0];var b=x[1],T=x[2],k=t.properties[b];if(!k){In("Skipping property: Invalid property name in: "+s),u();continue}var C=r.parse(b,T);if(!C){In("Skipping property: Invalid property definition in: "+s),u();continue}y.push({name:b,val:T}),u()}if(g){l();break}r.selector(f);for(var w=0;w=7&&t[0]==="d"&&(d=new RegExp(l.data.regex).exec(t))){if(r)return!1;var p=l.data;return{name:e,value:d,strValue:""+t,mapped:p,field:d[1],bypass:r}}else if(t.length>=10&&t[0]==="m"&&(f=new RegExp(l.mapData.regex).exec(t))){if(r||h.multiple)return!1;var m=l.mapData;if(!(h.color||h.number))return!1;var g=this.parse(e,f[4]);if(!g||g.mapped)return!1;var y=this.parse(e,f[5]);if(!y||y.mapped)return!1;if(g.pfValue===y.pfValue||g.strValue===y.strValue)return In("`"+e+": "+t+"` is not a valid mapper because the output range is zero; converting to `"+e+": "+g.strValue+"`"),this.parse(e,g.strValue);if(h.color){var v=g.value,x=y.value,b=v[0]===x[0]&&v[1]===x[1]&&v[2]===x[2]&&(v[3]===x[3]||(v[3]==null||v[3]===1)&&(x[3]==null||x[3]===1));if(b)return!1}return{name:e,value:f,strValue:""+t,mapped:m,field:f[1],fieldMin:parseFloat(f[2]),fieldMax:parseFloat(f[3]),valueMin:g.value,valueMax:y.value,bypass:r}}}if(h.multiple&&n!=="multiple"){var T;if(u?T=t.split(/\s+/):Hn(t)?T=t:T=[t],h.evenMultiple&&T.length%2!==0)return null;for(var k=[],C=[],w=[],S="",R=!1,L=0;L0?" ":"")+N.strValue}return h.validate&&!h.validate(k,C)?null:h.singleEnum&&R?k.length===1&&fr(k[0])?{name:e,value:k[0],strValue:k[0],bypass:r}:null:{name:e,value:k,pfValue:w,strValue:S,bypass:r,units:C}}var I=o(function(){for(var te=0;teh.max||h.strictMax&&t===h.max))return null;var P={name:e,value:t,strValue:""+t+(_||""),units:_,bypass:r};return h.unitless||_!=="px"&&_!=="em"?P.pfValue=t:P.pfValue=_==="px"||!_?t:this.getEmSizeInPixels()*t,(_==="ms"||_==="s")&&(P.pfValue=_==="ms"?t:1e3*t),(_==="deg"||_==="rad")&&(P.pfValue=_==="rad"?t:Bht(t)),_==="%"&&(P.pfValue=t/100),P}else if(h.propList){var B=[],O=""+t;if(O!=="none"){for(var $=O.split(/\s*,\s*|\s+/),V=0;V<$.length;V++){var G=$[V].trim();i.properties[G]?B.push(G):In("`"+G+"` is not a valid property name")}if(B.length===0)return null}return{name:e,value:B,strValue:B.length===0?"none":B.join(" "),bypass:r}}else if(h.color){var z=fbe(t);return z?{name:e,value:z,pfValue:z,strValue:"rgb("+z[0]+","+z[1]+","+z[2]+")",bypass:r}:null}else if(h.regex||h.regexes){if(h.enums){var W=I();if(W)return W}for(var H=h.regexes?h.regexes:[h.regex],j=0;j0&&l>0&&!isNaN(n.w)&&!isNaN(n.h)&&n.w>0&&n.h>0){u=Math.min((s-2*r)/n.w,(l-2*r)/n.h),u=u>this._private.maxZoom?this._private.maxZoom:u,u=u=n.minZoom&&(n.maxZoom=r),this},"zoomRange"),minZoom:o(function(t){return t===void 0?this._private.minZoom:this.zoomRange({min:t})},"minZoom"),maxZoom:o(function(t){return t===void 0?this._private.maxZoom:this.zoomRange({max:t})},"maxZoom"),getZoomedViewport:o(function(t){var r=this._private,n=r.pan,i=r.zoom,a,s,l=!1;if(r.zoomingEnabled||(l=!0),Gt(t)?s=t:cn(t)&&(s=t.level,t.position!=null?a=fA(t.position,i,n):t.renderedPosition!=null&&(a=t.renderedPosition),a!=null&&!r.panningEnabled&&(l=!0)),s=s>r.maxZoom?r.maxZoom:s,s=sr.maxZoom||!r.zoomingEnabled?s=!0:(r.zoom=u,a.push("zoom"))}if(i&&(!s||!t.cancelOnFailedZoom)&&r.panningEnabled){var h=t.pan;Gt(h.x)&&(r.pan.x=h.x,l=!1),Gt(h.y)&&(r.pan.y=h.y,l=!1),l||a.push("pan")}return a.length>0&&(a.push("viewport"),this.emit(a.join(" ")),this.notify("viewport")),this},"viewport"),center:o(function(t){var r=this.getCenterPan(t);return r&&(this._private.pan=r,this.emit("pan viewport"),this.notify("viewport")),this},"center"),getCenterPan:o(function(t,r){if(this._private.panningEnabled){if(fr(t)){var n=t;t=this.mutableElements().filter(n)}else Ho(t)||(t=this.mutableElements());if(t.length!==0){var i=t.boundingBox(),a=this.width(),s=this.height();r=r===void 0?this._private.zoom:r;var l={x:(a-r*(i.x1+i.x2))/2,y:(s-r*(i.y1+i.y2))/2};return l}}},"getCenterPan"),reset:o(function(){return!this._private.panningEnabled||!this._private.zoomingEnabled?this:(this.viewport({pan:{x:0,y:0},zoom:1}),this)},"reset"),invalidateSize:o(function(){this._private.sizeCache=null},"invalidateSize"),size:o(function(){var t=this._private,r=t.container,n=this;return t.sizeCache=t.sizeCache||(r?(function(){var i=n.window().getComputedStyle(r),a=o(function(l){return parseFloat(i.getPropertyValue(l))},"val");return{width:r.clientWidth-a("padding-left")-a("padding-right"),height:r.clientHeight-a("padding-top")-a("padding-bottom")}})():{width:1,height:1})},"size"),width:o(function(){return this.size().width},"width"),height:o(function(){return this.size().height},"height"),extent:o(function(){var t=this._private.pan,r=this._private.zoom,n=this.renderedExtent(),i={x1:(n.x1-t.x)/r,x2:(n.x2-t.x)/r,y1:(n.y1-t.y)/r,y2:(n.y2-t.y)/r};return i.w=i.x2-i.x1,i.h=i.y2-i.y1,i},"extent"),renderedExtent:o(function(){var t=this.width(),r=this.height();return{x1:0,y1:0,x2:t,y2:r,w:t,h:r}},"renderedExtent"),multiClickDebounceTime:o(function(t){if(t)this._private.multiClickDebounceTime=t;else return this._private.multiClickDebounceTime;return this},"multiClickDebounceTime")};sg.centre=sg.center;sg.autolockNodes=sg.autolock;sg.autoungrabifyNodes=sg.autoungrabify;WT={data:Dn.data({field:"data",bindingEvent:"data",allowBinding:!0,allowSetting:!0,settingEvent:"data",settingTriggersEvent:!0,triggerFnName:"trigger",allowGetting:!0,updateStyle:!0}),removeData:Dn.removeData({field:"data",event:"data",triggerFnName:"trigger",triggerEvent:!0,updateStyle:!0}),scratch:Dn.data({field:"scratch",bindingEvent:"scratch",allowBinding:!0,allowSetting:!0,settingEvent:"scratch",settingTriggersEvent:!0,triggerFnName:"trigger",allowGetting:!0,updateStyle:!0}),removeScratch:Dn.removeData({field:"scratch",event:"scratch",triggerFnName:"trigger",triggerEvent:!0,updateStyle:!0})};WT.attr=WT.data;WT.removeAttr=WT.removeData;qT=o(function(t){var r=this;t=br({},t);var n=t.container;n&&!Z5(n)&&Z5(n[0])&&(n=n[0]);var i=n?n._cyreg:null;i=i||{},i&&i.cy&&(i.cy.destroy(),i={});var a=i.readies=i.readies||[];n&&(n._cyreg=i),i.cy=r;var s=oa!==void 0&&n!==void 0&&!t.headless,l=t;l.layout=br({name:s?"grid":"null"},l.layout),l.renderer=br({name:s?"canvas":"null"},l.renderer);var u=o(function(g,y,v){return y!==void 0?y:v!==void 0?v:g},"defVal"),h=this._private={container:n,ready:!1,options:l,elements:new Ga(this),listeners:[],aniEles:new Ga(this),data:l.data||{},scratch:{},layout:null,renderer:null,destroyed:!1,notificationsEnabled:!0,minZoom:1e-50,maxZoom:1e50,zoomingEnabled:u(!0,l.zoomingEnabled),userZoomingEnabled:u(!0,l.userZoomingEnabled),panningEnabled:u(!0,l.panningEnabled),userPanningEnabled:u(!0,l.userPanningEnabled),boxSelectionEnabled:u(!0,l.boxSelectionEnabled),autolock:u(!1,l.autolock,l.autolockNodes),autoungrabify:u(!1,l.autoungrabify,l.autoungrabifyNodes),autounselectify:u(!1,l.autounselectify),styleEnabled:l.styleEnabled===void 0?s:l.styleEnabled,zoom:Gt(l.zoom)?l.zoom:1,pan:{x:cn(l.pan)&&Gt(l.pan.x)?l.pan.x:0,y:cn(l.pan)&&Gt(l.pan.y)?l.pan.y:0},animation:{current:[],queue:[]},hasCompoundNodes:!1,multiClickDebounceTime:u(250,l.multiClickDebounceTime)};this.createEmitter(),this.selectionType(l.selectionType),this.zoomRange({min:l.minZoom,max:l.maxZoom});var d=o(function(g,y){var v=g.some(Dut);if(v)return q1.all(g).then(y);y(g)},"loadExtData");h.styleEnabled&&r.setStyle([]);var f=br({},l,l.renderer);r.initRenderer(f);var p=o(function(g,y,v){r.notifications(!1);var x=r.mutableElements();x.length>0&&x.remove(),g!=null&&(cn(g)||Hn(g))&&r.add(g),r.one("layoutready",function(T){r.notifications(!0),r.emit(T),r.one("load",y),r.emitAndNotify("load")}).one("layoutstop",function(){r.one("done",v),r.emit("done")});var b=br({},r._private.options.layout);b.eles=r.elements(),r.layout(b).run()},"setElesAndLayout");d([l.style,l.elements],function(m){var g=m[0],y=m[1];h.styleEnabled&&r.style().append(g),p(y,function(){r.startAnimationLoop(),h.ready=!0,Ci(l.ready)&&r.on("ready",l.ready);for(var v=0;v0,l=!!e.boundingBox,u=Ns(l?e.boundingBox:structuredClone(t.extent())),h;if(Ho(e.roots))h=e.roots;else if(Hn(e.roots)){for(var d=[],f=0;f0;){var M=A(),D=L(M,I);if(D)M.outgoers().filter(function(ye){return ye.isNode()&&r.has(ye)}).forEach(_);else if(D===null){In("Detected double maximal shift for node `"+M.id()+"`. Bailing maximal adjustment due to cycle. Use `options.maximal: true` only on DAGs.");break}}}var P=0;if(e.avoidOverlap)for(var B=0;B0&&x[0].length<=3?se/2:0),Te=2*Math.PI/x[re].length*J;return re===0&&x[0].length===1&&(ge=1),{x:ae.x+ge*Math.cos(Te),y:ae.y+ge*Math.sin(Te)}}else{var we=x[re].length,Me=Math.max(we===1?0:l?(u.w-e.padding*2-Re.w)/((e.grid?Pe:we)-1):(u.w-e.padding*2-Re.w)/((e.grid?Pe:we)+1),P),ve={x:ae.x+(J+1-(we+1)/2)*Me,y:ae.y+(re+1-(j+1)/2)*be};return ve}},"getPositionTopBottom"),Oe={downward:0,leftward:90,upward:180,rightward:-90};Object.keys(Oe).indexOf(e.direction)===-1&&pi("Invalid direction '".concat(e.direction,"' specified for breadthfirst layout. Valid values are: ").concat(Object.keys(Oe).join(", ")));var ue=o(function(ke){return sht(Ge(ke),u,Oe[e.direction])},"getPosition");return r.nodes().layoutPositions(this,e,ue),this};Hpt={fit:!0,padding:30,boundingBox:void 0,avoidOverlap:!0,nodeDimensionsIncludeLabels:!1,spacingFactor:void 0,radius:void 0,startAngle:3/2*Math.PI,sweep:void 0,clockwise:!0,sort:void 0,animate:!1,animationDuration:500,animationEasing:void 0,animateFilter:o(function(t,r){return!0},"animateFilter"),ready:void 0,stop:void 0,transform:o(function(t,r){return r},"transform")};o(d2e,"CircleLayout");d2e.prototype.run=function(){var e=this.options,t=e,r=e.cy,n=t.eles,i=t.counterclockwise!==void 0?!t.counterclockwise:t.clockwise,a=n.nodes().not(":parent");t.sort&&(a=a.sort(t.sort));for(var s=Ns(t.boundingBox?t.boundingBox:{x1:0,y1:0,w:r.width(),h:r.height()}),l={x:s.x1+s.w/2,y:s.y1+s.h/2},u=t.sweep===void 0?2*Math.PI-2*Math.PI/a.length:t.sweep,h=u/Math.max(1,a.length-1),d,f=0,p=0;p1&&t.avoidOverlap){f*=1.75;var x=Math.cos(h)-Math.cos(0),b=Math.sin(h)-Math.sin(0),T=Math.sqrt(f*f/(x*x+b*b));d=Math.max(T,d)}var k=o(function(w,S){var R=t.startAngle+S*h*(i?1:-1),L=d*Math.cos(R),N=d*Math.sin(R),I={x:l.x+L,y:l.y+N};return I},"getPos");return n.nodes().layoutPositions(this,t,k),this};Upt={fit:!0,padding:30,startAngle:3/2*Math.PI,sweep:void 0,clockwise:!0,equidistant:!1,minNodeSpacing:10,boundingBox:void 0,avoidOverlap:!0,nodeDimensionsIncludeLabels:!1,height:void 0,width:void 0,spacingFactor:void 0,concentric:o(function(t){return t.degree()},"concentric"),levelWidth:o(function(t){return t.maxDegree()/4},"levelWidth"),animate:!1,animationDuration:500,animationEasing:void 0,animateFilter:o(function(t,r){return!0},"animateFilter"),ready:void 0,stop:void 0,transform:o(function(t,r){return r},"transform")};o(f2e,"ConcentricLayout");f2e.prototype.run=function(){for(var e=this.options,t=e,r=t.counterclockwise!==void 0?!t.counterclockwise:t.clockwise,n=e.cy,i=t.eles,a=i.nodes().not(":parent"),s=Ns(t.boundingBox?t.boundingBox:{x1:0,y1:0,w:n.width(),h:n.height()}),l={x:s.x1+s.w/2,y:s.y1+s.h/2},u=[],h=0,d=0;d0){var C=Math.abs(b[0].value-k.value);C>=v&&(b=[],x.push(b))}b.push(k)}var w=h+t.minNodeSpacing;if(!t.avoidOverlap){var S=x.length>0&&x[0].length>1,R=Math.min(s.w,s.h)/2-w,L=R/(x.length+S?1:0);w=Math.min(w,L)}for(var N=0,I=0;I1&&t.avoidOverlap){var D=Math.cos(M)-Math.cos(0),P=Math.sin(M)-Math.sin(0),B=Math.sqrt(w*w/(D*D+P*P));N=Math.max(B,N)}_.r=N,N+=w}if(t.equidistant){for(var O=0,$=0,V=0;V=e.numIter||(Jpt(n,e),n.temperature=n.temperature*e.coolingFactor,n.temperature=e.animationThreshold&&a(),Q5(d)}},"frame");d()}else{for(;h;)h=s(u),u++;Nxe(n,e),l()}return this};SA.prototype.stop=function(){return this.stopped=!0,this.thread&&this.thread.stop(),this.emit("layoutstop"),this};SA.prototype.destroy=function(){return this.thread&&this.thread.stop(),this};jpt=o(function(t,r,n){for(var i=n.eles.edges(),a=n.eles.nodes(),s=Ns(n.boundingBox?n.boundingBox:{x1:0,y1:0,w:t.width(),h:t.height()}),l={isCompound:t.hasCompoundNodes(),layoutNodes:[],idToIndex:{},nodeSize:a.size(),graphSet:[],indexToGraph:[],layoutEdges:[],edgeSize:i.size(),temperature:n.initialTemp,clientWidth:s.w,clientHeight:s.h,boundingBox:s},u=n.eles.components(),h={},d=0;d0){l.graphSet.push(R);for(var d=0;di.count?0:i.graph},"findLCA"),p2e=o(function(t,r,n,i){var a=i.graphSet[n];if(-10)var f=i.nodeOverlap*d,p=Math.sqrt(l*l+u*u),m=f*l/p,g=f*u/p;else var y=aA(t,l,u),v=aA(r,-1*l,-1*u),x=v.x-y.x,b=v.y-y.y,T=x*x+b*b,p=Math.sqrt(T),f=(t.nodeRepulsion+r.nodeRepulsion)/T,m=f*x/p,g=f*b/p;t.isLocked||(t.offsetX-=m,t.offsetY-=g),r.isLocked||(r.offsetX+=m,r.offsetY+=g)}},"nodeRepulsion"),rmt=o(function(t,r,n,i){if(n>0)var a=t.maxX-r.minX;else var a=r.maxX-t.minX;if(i>0)var s=t.maxY-r.minY;else var s=r.maxY-t.minY;return a>=0&&s>=0?Math.sqrt(a*a+s*s):0},"nodesOverlap"),aA=o(function(t,r,n){var i=t.positionX,a=t.positionY,s=t.height||1,l=t.width||1,u=n/r,h=s/l,d={};return r===0&&0n?(d.x=i,d.y=a+s/2,d):0r&&-1*h<=u&&u<=h?(d.x=i-l/2,d.y=a-l*n/2/r,d):0=h)?(d.x=i+s*r/2/n,d.y=a+s/2,d):(0>n&&(u<=-1*h||u>=h)&&(d.x=i-s*r/2/n,d.y=a-s/2),d)},"findClippingPoint"),nmt=o(function(t,r){for(var n=0;nn){var v=r.gravity*m/y,x=r.gravity*g/y;p.offsetX+=v,p.offsetY+=x}}}}},"calculateGravityForces"),amt=o(function(t,r){var n=[],i=0,a=-1;for(n.push.apply(n,t.graphSet[0]),a+=t.graphSet[0].length;i<=a;){var s=n[i++],l=t.idToIndex[s],u=t.layoutNodes[l],h=u.children;if(0n)var a={x:n*t/i,y:n*r/i};else var a={x:t,y:r};return a},"limitForce"),g2e=o(function(t,r){var n=t.parentId;if(n!=null){var i=r.layoutNodes[r.idToIndex[n]],a=!1;if((i.maxX==null||t.maxX+i.padRight>i.maxX)&&(i.maxX=t.maxX+i.padRight,a=!0),(i.minX==null||t.minX-i.padLefti.maxY)&&(i.maxY=t.maxY+i.padBottom,a=!0),(i.minY==null||t.minY-i.padTopx&&(g+=v+r.componentSpacing,m=0,y=0,v=0)}}},"separateComponents"),lmt={fit:!0,padding:30,boundingBox:void 0,avoidOverlap:!0,avoidOverlapPadding:10,nodeDimensionsIncludeLabels:!1,spacingFactor:void 0,condense:!1,rows:void 0,cols:void 0,position:o(function(t){},"position"),sort:void 0,animate:!1,animationDuration:500,animationEasing:void 0,animateFilter:o(function(t,r){return!0},"animateFilter"),ready:void 0,stop:void 0,transform:o(function(t,r){return r},"transform")};o(y2e,"GridLayout");y2e.prototype.run=function(){var e=this.options,t=e,r=e.cy,n=t.eles,i=n.nodes().not(":parent");t.sort&&(i=i.sort(t.sort));var a=Ns(t.boundingBox?t.boundingBox:{x1:0,y1:0,w:r.width(),h:r.height()});if(a.h===0||a.w===0)n.nodes().layoutPositions(this,t,function(W){return{x:a.x1,y:a.y1}});else{var s=i.size(),l=Math.sqrt(s*a.h/a.w),u=Math.round(l),h=Math.round(a.w/a.h*l),d=o(function(H){if(H==null)return Math.min(u,h);var j=Math.min(u,h);j==u?u=H:h=H},"small"),f=o(function(H){if(H==null)return Math.max(u,h);var j=Math.max(u,h);j==u?u=H:h=H},"large"),p=t.rows,m=t.cols!=null?t.cols:t.columns;if(p!=null&&m!=null)u=p,h=m;else if(p!=null&&m==null)u=p,h=Math.ceil(s/u);else if(p==null&&m!=null)h=m,u=Math.ceil(s/h);else if(h*u>s){var g=d(),y=f();(g-1)*y>=s?d(g-1):(y-1)*g>=s&&f(y-1)}else for(;h*u=s?f(x+1):d(v+1)}var b=a.w/h,T=a.h/u;if(t.condense&&(b=0,T=0),t.avoidOverlap)for(var k=0;k=h&&(D=0,M++)},"moveToNextCell"),B={},O=0;O(D=Zht(e,t,P[B],P[B+1],P[B+2],P[B+3])))return v(S,D),!0}else if(L.edgeType==="bezier"||L.edgeType==="multibezier"||L.edgeType==="self"||L.edgeType==="compound"){for(var P=L.allpts,B=0;B+5(D=Kht(e,t,P[B],P[B+1],P[B+2],P[B+3],P[B+4],P[B+5])))return v(S,D),!0}for(var O=O||R.source,$=$||R.target,V=i.getArrowWidth(N,I),G=[{name:"source",x:L.arrowStartX,y:L.arrowStartY,angle:L.srcArrowAngle},{name:"target",x:L.arrowEndX,y:L.arrowEndY,angle:L.tgtArrowAngle},{name:"mid-source",x:L.midX,y:L.midY,angle:L.midsrcArrowAngle},{name:"mid-target",x:L.midX,y:L.midY,angle:L.midtgtArrowAngle}],B=0;B0&&(x(O),x($))}o(b,"checkEdge");function T(S,R,L){return mo(S,R,L)}o(T,"preprop");function k(S,R){var L=S._private,N=p,I;R?I=R+"-":I="",S.boundingBox();var _=L.labelBounds[R||"main"],A=S.pstyle(I+"label").value,M=S.pstyle("text-events").strValue==="yes";if(!(!M||!A)){var D=T(L.rscratch,"labelX",R),P=T(L.rscratch,"labelY",R),B=T(L.rscratch,"labelAngle",R),O=S.pstyle(I+"text-margin-x").pfValue,$=S.pstyle(I+"text-margin-y").pfValue,V=_.x1-N-O,G=_.x2+N-O,z=_.y1-N-$,W=_.y2+N-$;if(B){var H=Math.cos(B),j=Math.sin(B),Q=o(function(Re,be){return Re=Re-D,be=be-P,{x:Re*H-be*j+D,y:Re*j+be*H+P}},"rotate"),U=Q(V,z),oe=Q(V,W),te=Q(G,z),le=Q(G,W),ie=[U.x+O,U.y+$,te.x+O,te.y+$,le.x+O,le.y+$,oe.x+O,oe.y+$];if(go(e,t,ie))return v(S),!0}else if($f(_,e,t))return v(S),!0}}o(k,"checkLabel");for(var C=s.length-1;C>=0;C--){var w=s[C];w.isNode()?x(w)||k(w):b(w)||k(w)||k(w,"source")||k(w,"target")}return l};lg.getAllInBox=function(e,t,r,n){var i=this.getCachedZSortedEles().interactive,a=this.cy.zoom(),s=2/a,l=[],u=Math.min(e,r),h=Math.max(e,r),d=Math.min(t,n),f=Math.max(t,n);e=u,r=h,t=d,n=f;var p=Ns({x1:e,y1:t,x2:r,y2:n}),m=[{x:p.x1,y:p.y1},{x:p.x2,y:p.y1},{x:p.x2,y:p.y2},{x:p.x1,y:p.y2}],g=[[m[0],m[1]],[m[1],m[2]],[m[2],m[3]],[m[3],m[0]]];function y(Re,be,Pe){return mo(Re,be,Pe)}o(y,"preprop");function v(Re,be){var Pe=Re._private,Ge=s,Oe="";Re.boundingBox();var ue=Pe.labelBounds.main;if(!ue)return null;var ye=y(Pe.rscratch,"labelX",be),ke=y(Pe.rscratch,"labelY",be),ce=y(Pe.rscratch,"labelAngle",be),re=Re.pstyle(Oe+"text-margin-x").pfValue,J=Re.pstyle(Oe+"text-margin-y").pfValue,se=ue.x1-Ge-re,ge=ue.x2+Ge-re,Te=ue.y1-Ge-J,we=ue.y2+Ge-J;if(ce){var Me=Math.cos(ce),ve=Math.sin(ce),ne=o(function(he,X){return he=he-ye,X=X-ke,{x:he*Me-X*ve+ye,y:he*ve+X*Me+ke}},"rotate");return[ne(se,Te),ne(ge,Te),ne(ge,we),ne(se,we)]}else return[{x:se,y:Te},{x:ge,y:Te},{x:ge,y:we},{x:se,y:we}]}o(v,"getRotatedLabelBox");function x(Re,be,Pe,Ge){function Oe(ue,ye,ke){return(ke.y-ue.y)*(ye.x-ue.x)>(ye.y-ue.y)*(ke.x-ue.x)}return o(Oe,"ccw"),Oe(Re,Pe,Ge)!==Oe(be,Pe,Ge)&&Oe(Re,be,Pe)!==Oe(Re,be,Ge)}o(x,"doLinesIntersect");for(var b=0;b0?-(Math.PI-t.ang):Math.PI+t.ang},"invertVec"),pmt=o(function(t,r,n,i,a){if(t!==Fxe?zxe(r,t,bu):fmt(wl,bu),zxe(r,n,wl),Bxe=bu.nx*wl.ny-bu.ny*wl.nx,$xe=bu.nx*wl.nx-bu.ny*-wl.ny,Mh=Math.asin(Math.max(-1,Math.min(1,Bxe))),Math.abs(Mh)<1e-6){cF=r.x,uF=r.y,Jm=_1=0;return}tg=1,Y5=!1,$xe<0?Mh<0?Mh=Math.PI+Mh:(Mh=Math.PI-Mh,tg=-1,Y5=!0):Mh>0&&(tg=-1,Y5=!0),r.radius!==void 0?_1=r.radius:_1=i,Xm=Mh/2,O5=Math.min(bu.len/2,wl.len/2),a?(vu=Math.abs(Math.cos(Xm)*_1/Math.sin(Xm)),vu>O5?(vu=O5,Jm=Math.abs(vu*Math.sin(Xm)/Math.cos(Xm))):Jm=_1):(vu=Math.min(O5,_1),Jm=Math.abs(vu*Math.sin(Xm)/Math.cos(Xm))),hF=r.x+wl.nx*vu,dF=r.y+wl.ny*vu,cF=hF-wl.ny*Jm*tg,uF=dF+wl.nx*Jm*tg,T2e=r.x+bu.nx*vu,C2e=r.y+bu.ny*vu,Fxe=r},"calcCornerArc");o(w2e,"drawPreparedRoundCorner");o(FF,"getRoundCorner");HT=.01,mmt=Math.sqrt(2*HT),ds={};ds.findMidptPtsEtc=function(e,t){var r=t.posPts,n=t.intersectionPts,i=t.vectorNormInverse,a,s=e.pstyle("source-endpoint"),l=e.pstyle("target-endpoint"),u=s.units!=null&&l.units!=null,h=o(function(C,w,S,R){var L=R-w,N=S-C,I=Math.sqrt(N*N+L*L);return{x:-L/I,y:N/I}},"recalcVectorNormInverse"),d=e.pstyle("edge-distances").value;switch(d){case"node-position":a=r;break;case"intersection":a=n;break;case"endpoints":{if(u){var f=this.manualEndptToPx(e.source()[0],s),p=Ki(f,2),m=p[0],g=p[1],y=this.manualEndptToPx(e.target()[0],l),v=Ki(y,2),x=v[0],b=v[1],T={x1:m,y1:g,x2:x,y2:b};i=h(m,g,x,b),a=T}else In("Edge ".concat(e.id()," has edge-distances:endpoints specified without manual endpoints specified via source-endpoint and target-endpoint. Falling back on edge-distances:intersection (default).")),a=n;break}}return{midptPts:a,vectorNormInverse:i}};ds.findHaystackPoints=function(e){for(var t=0;t0?Math.max(X-fe,0):Math.min(X+fe,0)},"subDWH"),A=_(N,R),M=_(I,L),D=!1;b===h?x=Math.abs(A)>Math.abs(M)?i:n:b===u||b===l?(x=n,D=!0):(b===a||b===s)&&(x=i,D=!0);var P=x===n,B=P?M:A,O=P?I:N,$=EF(O),V=!1;!(D&&(k||w))&&(b===l&&O<0||b===u&&O>0||b===a&&O>0||b===s&&O<0)&&($*=-1,B=$*Math.abs(B),V=!0);var G;if(k){var z=C<0?1+C:C;G=z*B}else{var W=C<0?B:0;G=W+C*$}var H=o(function(X){return Math.abs(X)=Math.abs(B)},"getIsTooClose"),j=H(G),Q=H(Math.abs(B)-Math.abs(G)),U=j||Q;if(U&&!V)if(P){var oe=Math.abs(O)<=p/2,te=Math.abs(N)<=m/2;if(oe){var le=(d.x1+d.x2)/2,ie=d.y1,ae=d.y2;r.segpts=[le,ie,le,ae]}else if(te){var Re=(d.y1+d.y2)/2,be=d.x1,Pe=d.x2;r.segpts=[be,Re,Pe,Re]}else r.segpts=[d.x1,d.y2]}else{var Ge=Math.abs(O)<=f/2,Oe=Math.abs(I)<=g/2;if(Ge){var ue=(d.y1+d.y2)/2,ye=d.x1,ke=d.x2;r.segpts=[ye,ue,ke,ue]}else if(Oe){var ce=(d.x1+d.x2)/2,re=d.y1,J=d.y2;r.segpts=[ce,re,ce,J]}else r.segpts=[d.x2,d.y1]}else if(P){var se=d.y1+G+(v?p/2*$:0),ge=d.x1,Te=d.x2;r.segpts=[ge,se,Te,se]}else{var we=d.x1+G+(v?f/2*$:0),Me=d.y1,ve=d.y2;r.segpts=[we,Me,we,ve]}if(r.isRound){var ne=e.pstyle("taxi-radius").value,q=e.pstyle("radius-type").value[0]==="arc-radius";r.radii=new Array(r.segpts.length/2).fill(ne),r.isArcRadius=new Array(r.segpts.length/2).fill(q)}};ds.tryToCorrectInvalidPoints=function(e,t){var r=e._private.rscratch;if(r.edgeType==="bezier"){var n=t.srcPos,i=t.tgtPos,a=t.srcW,s=t.srcH,l=t.tgtW,u=t.tgtH,h=t.srcShape,d=t.tgtShape,f=t.srcCornerRadius,p=t.tgtCornerRadius,m=t.srcRs,g=t.tgtRs,y=!Gt(r.startX)||!Gt(r.startY),v=!Gt(r.arrowStartX)||!Gt(r.arrowStartY),x=!Gt(r.endX)||!Gt(r.endY),b=!Gt(r.arrowEndX)||!Gt(r.arrowEndY),T=3,k=this.getArrowWidth(e.pstyle("width").pfValue,e.pstyle("arrow-scale").value)*this.arrowShapeWidth,C=T*k,w=ig({x:r.ctrlpts[0],y:r.ctrlpts[1]},{x:r.startX,y:r.startY}),S=wO.poolIndex()){var $=B;B=O,O=$}var V=A.srcPos=B.position(),G=A.tgtPos=O.position(),z=A.srcW=B.outerWidth(),W=A.srcH=B.outerHeight(),H=A.tgtW=O.outerWidth(),j=A.tgtH=O.outerHeight(),Q=A.srcShape=r.nodeShapes[t.getNodeShape(B)],U=A.tgtShape=r.nodeShapes[t.getNodeShape(O)],oe=A.srcCornerRadius=B.pstyle("corner-radius").value==="auto"?"auto":B.pstyle("corner-radius").pfValue,te=A.tgtCornerRadius=O.pstyle("corner-radius").value==="auto"?"auto":O.pstyle("corner-radius").pfValue,le=A.tgtRs=O._private.rscratch,ie=A.srcRs=B._private.rscratch;A.dirCounts={north:0,west:0,south:0,east:0,northwest:0,southwest:0,northeast:0,southeast:0};for(var ae=0;ae=mmt||(Te=Math.sqrt(Math.max(ge*ge,HT)+Math.max(se*se,HT)));var we=A.vector={x:ge,y:se},Me=A.vectorNorm={x:we.x/Te,y:we.y/Te},ve={x:-Me.y,y:Me.x};A.nodesOverlap=!Gt(Te)||U.checkPoint(ue[0],ue[1],0,H,j,G.x,G.y,te,le)||Q.checkPoint(ke[0],ke[1],0,z,W,V.x,V.y,oe,ie),A.vectorNormInverse=ve,M={nodesOverlap:A.nodesOverlap,dirCounts:A.dirCounts,calculatedIntersection:!0,hasBezier:A.hasBezier,hasUnbundled:A.hasUnbundled,eles:A.eles,srcPos:G,srcRs:le,tgtPos:V,tgtRs:ie,srcW:H,srcH:j,tgtW:z,tgtH:W,srcIntn:ce,tgtIntn:ye,srcShape:U,tgtShape:Q,posPts:{x1:J.x2,y1:J.y2,x2:J.x1,y2:J.y1},intersectionPts:{x1:re.x2,y1:re.y2,x2:re.x1,y2:re.y1},vector:{x:-we.x,y:-we.y},vectorNorm:{x:-Me.x,y:-Me.y},vectorNormInverse:{x:-ve.x,y:-ve.y}}}var ne=Oe?M:A;be.nodesOverlap=ne.nodesOverlap,be.srcIntn=ne.srcIntn,be.tgtIntn=ne.tgtIntn,be.isRound=Pe.startsWith("round"),i&&(B.isParent()||B.isChild()||O.isParent()||O.isChild())&&(B.parents().anySame(O)||O.parents().anySame(B)||B.same(O)&&B.isParent())?t.findCompoundLoopPoints(Re,ne,ae,Ge):B===O?t.findLoopPoints(Re,ne,ae,Ge):Pe.endsWith("segments")?t.findSegmentsPoints(Re,ne):Pe.endsWith("taxi")?t.findTaxiPoints(Re,ne):Pe==="straight"||!Ge&&A.eles.length%2===1&&ae===Math.floor(A.eles.length/2)?t.findStraightEdgePoints(Re):t.findBezierPoints(Re,ne,ae,Ge,Oe),t.findEndpoints(Re),t.tryToCorrectInvalidPoints(Re,ne),t.checkForInvalidEdgeWarning(Re),t.storeAllpts(Re),t.storeEdgeProjections(Re),t.calculateArrowAngles(Re),t.recalculateEdgeLabelProjections(Re),t.calculateLabelAngles(Re)}},"_loop"),S=0;S0){var ue=h,ye=Qm(ue,M1(s)),ke=Qm(ue,M1(Oe)),ce=ye;if(ke2){var re=Qm(ue,{x:Oe[2],y:Oe[3]});re0){var K=d,qe=Qm(K,M1(s)),_e=Qm(K,M1(fe)),Be=qe;if(_e2){var Ne=Qm(K,{x:fe[2],y:fe[3]});Ne=g||S){v={cp:k,segment:w};break}}if(v)break}var R=v.cp,L=v.segment,N=(g-x)/L.length,I=L.t1-L.t0,_=m?L.t0+I*N:L.t1-I*N;_=FT(0,_,1),t=O1(R.p0,R.p1,R.p2,_),p=ymt(R.p0,R.p1,R.p2,_);break}case"straight":case"segments":case"haystack":{for(var A=0,M,D,P,B,O=n.allpts.length,$=0;$+3=g));$+=2);var V=g-D,G=V/M;G=FT(0,G,1),t=Fht(P,B,G),p=E2e(P,B);break}}s("labelX",f,t.x),s("labelY",f,t.y),s("labelAutoAngle",f,p)}},"calculateEndProjection");h("source"),h("target"),this.applyLabelDimensions(e)}};wu.applyLabelDimensions=function(e){this.applyPrefixedLabelDimensions(e),e.isEdge()&&(this.applyPrefixedLabelDimensions(e,"source"),this.applyPrefixedLabelDimensions(e,"target"))};wu.applyPrefixedLabelDimensions=function(e,t){var r=e._private,n=this.getLabelText(e,t),i=ng(n,e._private.labelDimsKey);if(mo(r.rscratch,"prefixedLabelDimsKey",t)!==i){Nh(r.rscratch,"prefixedLabelDimsKey",t,i);var a=this.calculateLabelDimensions(e,n),s=e.pstyle("line-height").pfValue,l=e.pstyle("text-wrap").strValue,u=mo(r.rscratch,"labelWrapCachedLines",t)||[],h=l!=="wrap"?1:Math.max(u.length,1),d=a.height/h,f=d*s,p=a.width,m=a.height+(h-1)*(s-1)*d;Nh(r.rstyle,"labelWidth",t,p),Nh(r.rscratch,"labelWidth",t,p),Nh(r.rstyle,"labelHeight",t,m),Nh(r.rscratch,"labelHeight",t,m),Nh(r.rscratch,"labelLineHeight",t,f)}};wu.getLabelText=function(e,t){var r=e._private,n=t?t+"-":"",i=e.pstyle(n+"label").strValue,a=e.pstyle("text-transform").value,s=o(function(W,H){return H?(Nh(r.rscratch,W,t,H),H):mo(r.rscratch,W,t)},"rscratch");if(!i)return"";a=="none"||(a=="uppercase"?i=i.toUpperCase():a=="lowercase"&&(i=i.toLowerCase()));var l=e.pstyle("text-wrap").value;if(l==="wrap"){var u=s("labelKey");if(u!=null&&s("labelWrapKey")===u)return s("labelWrapCachedText");for(var h="\u200B",d=i.split(` +`),f=e.pstyle("text-max-width").pfValue,p=e.pstyle("text-overflow-wrap").value,m=p==="anywhere",g=[],y=/[\s\u200b]+|$/g,v=0;vf){var C=x.matchAll(y),w="",S=0,R=yo(C),L;try{for(R.s();!(L=R.n()).done;){var N=L.value,I=N[0],_=x.substring(S,N.index);S=N.index+I.length;var A=w.length===0?_:w+_+I,M=this.calculateLabelDimensions(e,A),D=M.width;D<=f?w+=_+I:(w&&g.push(w),w=_+I)}}catch(z){R.e(z)}finally{R.f()}w.match(/^[\s\u200b]+$/)||g.push(w)}else g.push(x)}s("labelWrapCachedLines",g),i=s("labelWrapCachedText",g.join(` +`)),s("labelWrapKey",u)}else if(l==="ellipsis"){var P=e.pstyle("text-max-width").pfValue,B="",O="\u2026",$=!1;if(this.calculateLabelDimensions(e,i).widthP)break;B+=i[V],V===i.length-1&&($=!0)}return $||(B+=O),B}return i};wu.getLabelJustification=function(e){var t=e.pstyle("text-justification").strValue,r=e.pstyle("text-halign").strValue;if(t==="auto")if(e.isNode())switch(r){case"left":return"right";case"right":return"left";default:return"center"}else return"center";else return t};wu.calculateLabelDimensions=function(e,t){var r=this,n=r.cy.window(),i=n.document,a=0,s=e.pstyle("font-style").strValue,l=e.pstyle("font-size").pfValue,u=e.pstyle("font-family").strValue,h=e.pstyle("font-weight").strValue,d=this.labelCalcCanvas,f=this.labelCalcCanvasContext;if(!d){d=this.labelCalcCanvas=i.createElement("canvas"),f=this.labelCalcCanvasContext=d.getContext("2d");var p=d.style;p.position="absolute",p.left="-9999px",p.top="-9999px",p.zIndex="-1",p.visibility="hidden",p.pointerEvents="none"}f.font="".concat(s," ").concat(h," ").concat(l,"px ").concat(u);for(var m=0,g=0,y=t.split(` +`),v=0;v1&&arguments[1]!==void 0?arguments[1]:!0;if(t.merge(s),l)for(var u=0;u=e.desktopTapThreshold2}var At=a(q);mt&&(e.hoverData.tapholdCancelled=!0);var bt=o(function(){var qt=e.hoverData.dragDelta=e.hoverData.dragDelta||[];qt.length===0?(qt.push(Ke[0]),qt.push(Ke[1])):(qt[0]+=Ke[0],qt[1]+=Ke[1])},"updateDragDelta");X=!0,i($e,["mousemove","vmousemove","tapdrag"],q,{x:_e[0],y:_e[1]});var me=o(function(qt){return{originalEvent:q,type:qt,position:{x:_e[0],y:_e[1]}}},"makeEvent"),lt=o(function(){e.data.bgActivePosistion=void 0,e.hoverData.selecting||fe.emit(me("boxstart")),He[4]=1,e.hoverData.selecting=!0,e.redrawHint("select",!0),e.redraw()},"goIntoBoxMode");if(e.hoverData.which===3){if(mt){var gt=me("cxtdrag");Fe?Fe.emit(gt):fe.emit(gt),e.hoverData.cxtDragged=!0,(!e.hoverData.cxtOver||$e!==e.hoverData.cxtOver)&&(e.hoverData.cxtOver&&e.hoverData.cxtOver.emit(me("cxtdragout")),e.hoverData.cxtOver=$e,$e&&$e.emit(me("cxtdragover")))}}else if(e.hoverData.dragging){if(X=!0,fe.panningEnabled()&&fe.userPanningEnabled()){var Ze;if(e.hoverData.justStartedPan){var Ee=e.hoverData.mdownPos;Ze={x:(_e[0]-Ee[0])*K,y:(_e[1]-Ee[1])*K},e.hoverData.justStartedPan=!1}else Ze={x:Ke[0]*K,y:Ke[1]*K};fe.panBy(Ze),fe.emit(me("dragpan")),e.hoverData.dragged=!0}_e=e.projectIntoViewport(q.clientX,q.clientY)}else if(He[4]==1&&(Fe==null||Fe.pannable())){if(mt){if(!e.hoverData.dragging&&fe.boxSelectionEnabled()&&(At||!fe.panningEnabled()||!fe.userPanningEnabled()))lt();else if(!e.hoverData.selecting&&fe.panningEnabled()&&fe.userPanningEnabled()){var tt=s(Fe,e.hoverData.downs);tt&&(e.hoverData.dragging=!0,e.hoverData.justStartedPan=!0,He[4]=0,e.data.bgActivePosistion=M1(Be),e.redrawHint("select",!0),e.redraw())}Fe&&Fe.pannable()&&Fe.active()&&Fe.unactivate()}}else{if(Fe&&Fe.pannable()&&Fe.active()&&Fe.unactivate(),(!Fe||!Fe.grabbed())&&$e!=Xe&&(Xe&&i(Xe,["mouseout","tapdragout"],q,{x:_e[0],y:_e[1]}),$e&&i($e,["mouseover","tapdragover"],q,{x:_e[0],y:_e[1]}),e.hoverData.last=$e),Fe)if(mt){if(fe.boxSelectionEnabled()&&At)Fe&&Fe.grabbed()&&(y(xe),Fe.emit(me("freeon")),xe.emit(me("free")),e.dragData.didDrag&&(Fe.emit(me("dragfreeon")),xe.emit(me("dragfree")))),lt();else if(Fe&&Fe.grabbed()&&e.nodeIsDraggable(Fe)){var at=!e.dragData.didDrag;at&&e.redrawHint("eles",!0),e.dragData.didDrag=!0,e.hoverData.draggingEles||m(xe,{inDragLayer:!0});var ot={x:0,y:0};if(Gt(Ke[0])&&Gt(Ke[1])&&(ot.x+=Ke[0],ot.y+=Ke[1],at)){var Wt=e.hoverData.dragDelta;Wt&&Gt(Wt[0])&&Gt(Wt[1])&&(ot.x+=Wt[0],ot.y+=Wt[1])}e.hoverData.draggingEles=!0,xe.silentShift(ot).emit(me("position")).emit(me("drag")),e.redrawHint("drag",!0),e.redraw()}}else bt();X=!0}if(He[2]=_e[0],He[3]=_e[1],X)return q.stopPropagation&&q.stopPropagation(),q.preventDefault&&q.preventDefault(),!1}},"mousemoveHandler"),!1);var N,I,_;e.registerBinding(t,"mouseup",o(function(q){if(!(e.hoverData.which===1&&q.which!==1&&e.hoverData.capture)){var he=e.hoverData.capture;if(he){e.hoverData.capture=!1;var X=e.cy,fe=e.projectIntoViewport(q.clientX,q.clientY),K=e.selection,qe=e.findNearestElement(fe[0],fe[1],!0,!1),_e=e.dragData.possibleDragElements,Be=e.hoverData.down,Ne=a(q);e.data.bgActivePosistion&&(e.redrawHint("select",!0),e.redraw()),e.hoverData.tapholdCancelled=!0,e.data.bgActivePosistion=void 0,Be&&Be.unactivate();var He=o(function(Le){return{originalEvent:q,type:Le,position:{x:fe[0],y:fe[1]}}},"makeEvent");if(e.hoverData.which===3){var $e=He("cxttapend");if(Be?Be.emit($e):X.emit($e),!e.hoverData.cxtDragged){var Xe=He("cxttap");Be?Be.emit(Xe):X.emit(Xe)}e.hoverData.cxtDragged=!1,e.hoverData.which=null}else if(e.hoverData.which===1){if(i(qe,["mouseup","tapend","vmouseup"],q,{x:fe[0],y:fe[1]}),!e.dragData.didDrag&&!e.hoverData.dragged&&!e.hoverData.selecting&&!e.hoverData.isOverThresholdDrag&&(i(Be,["click","tap","vclick"],q,{x:fe[0],y:fe[1]}),I=!1,q.timeStamp-_<=X.multiClickDebounceTime()?(N&&clearTimeout(N),I=!0,_=null,i(Be,["dblclick","dbltap","vdblclick"],q,{x:fe[0],y:fe[1]})):(N=setTimeout(function(){I||i(Be,["oneclick","onetap","voneclick"],q,{x:fe[0],y:fe[1]})},X.multiClickDebounceTime()),_=q.timeStamp)),Be==null&&!e.dragData.didDrag&&!e.hoverData.selecting&&!e.hoverData.dragged&&!a(q)&&(X.$(r).unselect(["tapunselect"]),_e.length>0&&e.redrawHint("eles",!0),e.dragData.possibleDragElements=_e=X.collection()),qe==Be&&!e.dragData.didDrag&&!e.hoverData.selecting&&qe!=null&&qe._private.selectable&&(e.hoverData.dragging||(X.selectionType()==="additive"||Ne?qe.selected()?qe.unselect(["tapunselect"]):qe.select(["tapselect"]):Ne||(X.$(r).unmerge(qe).unselect(["tapunselect"]),qe.select(["tapselect"]))),e.redrawHint("eles",!0)),e.hoverData.selecting){var Fe=X.collection(e.getAllInBox(K[0],K[1],K[2],K[3]));e.redrawHint("select",!0),Fe.length>0&&e.redrawHint("eles",!0),X.emit(He("boxend"));var Ke=o(function(Le){return Le.selectable()&&!Le.selected()},"eleWouldBeSelected");X.selectionType()==="additive"||Ne||X.$(r).unmerge(Fe).unselect(),Fe.emit(He("box")).stdFilter(Ke).select().emit(He("boxselect")),e.redraw()}if(e.hoverData.dragging&&(e.hoverData.dragging=!1,e.redrawHint("select",!0),e.redrawHint("eles",!0),e.redraw()),!K[4]){e.redrawHint("drag",!0),e.redrawHint("eles",!0);var xe=Be&&Be.grabbed();y(_e),xe&&(Be.emit(He("freeon")),_e.emit(He("free")),e.dragData.didDrag&&(Be.emit(He("dragfreeon")),_e.emit(He("dragfree"))))}}K[4]=0,e.hoverData.down=null,e.hoverData.cxtStarted=!1,e.hoverData.draggingEles=!1,e.hoverData.selecting=!1,e.hoverData.isOverThresholdDrag=!1,e.dragData.didDrag=!1,e.hoverData.dragged=!1,e.hoverData.dragDelta=[],e.hoverData.mdownPos=null,e.hoverData.mdownGPos=null,e.hoverData.which=null}}},"mouseupHandler"),!1);var A=[],M=4,D,P=1e5,B=o(function(q,he){for(var X=0;X=M){var fe=A;if(D=B(fe,5),!D){var K=Math.abs(fe[0]);D=O(fe)&&K>5}if(D)for(var qe=0;qe5&&(X=EF(X)*5),Xe=X/-250,D&&(Xe/=P,Xe*=3),Xe=Xe*e.wheelSensitivity;var Fe=q.deltaMode===1;Fe&&(Xe*=33);var Ke=_e.zoom()*Math.pow(10,Xe);q.type==="gesturechange"&&(Ke=e.gestureStartZoom*q.scale),_e.zoom({level:Ke,renderedPosition:{x:$e[0],y:$e[1]}}),_e.emit({type:q.type==="gesturechange"?"pinchzoom":"scrollzoom",originalEvent:q,position:{x:He[0],y:He[1]}})}}}},"wheelHandler");e.registerBinding(e.container,"wheel",$,!0),e.registerBinding(t,"scroll",o(function(q){e.scrollingPage=!0,clearTimeout(e.scrollingPageTimeout),e.scrollingPageTimeout=setTimeout(function(){e.scrollingPage=!1},250)},"scrollHandler"),!0),e.registerBinding(e.container,"gesturestart",o(function(q){e.gestureStartZoom=e.cy.zoom(),e.hasTouchStarted||q.preventDefault()},"gestureStartHandler"),!0),e.registerBinding(e.container,"gesturechange",function(ne){e.hasTouchStarted||$(ne)},!0),e.registerBinding(e.container,"mouseout",o(function(q){var he=e.projectIntoViewport(q.clientX,q.clientY);e.cy.emit({originalEvent:q,type:"mouseout",position:{x:he[0],y:he[1]}})},"mouseOutHandler"),!1),e.registerBinding(e.container,"mouseover",o(function(q){var he=e.projectIntoViewport(q.clientX,q.clientY);e.cy.emit({originalEvent:q,type:"mouseover",position:{x:he[0],y:he[1]}})},"mouseOverHandler"),!1);var V,G,z,W,H,j,Q,U,oe,te,le,ie,ae,Re=o(function(q,he,X,fe){return Math.sqrt((X-q)*(X-q)+(fe-he)*(fe-he))},"distance"),be=o(function(q,he,X,fe){return(X-q)*(X-q)+(fe-he)*(fe-he)},"distanceSq"),Pe;e.registerBinding(e.container,"touchstart",Pe=o(function(q){if(e.hasTouchStarted=!0,!!R(q)){x(),e.touchData.capture=!0,e.data.bgActivePosistion=void 0;var he=e.cy,X=e.touchData.now,fe=e.touchData.earlier;if(q.touches[0]){var K=e.projectIntoViewport(q.touches[0].clientX,q.touches[0].clientY);X[0]=K[0],X[1]=K[1]}if(q.touches[1]){var K=e.projectIntoViewport(q.touches[1].clientX,q.touches[1].clientY);X[2]=K[0],X[3]=K[1]}if(q.touches[2]){var K=e.projectIntoViewport(q.touches[2].clientX,q.touches[2].clientY);X[4]=K[0],X[5]=K[1]}var qe=o(function(At){return{originalEvent:q,type:At,position:{x:X[0],y:X[1]}}},"makeEvent");if(q.touches[1]){e.touchData.singleTouchMoved=!0,y(e.dragData.touchDragEles);var _e=e.findContainerClientCoords();oe=_e[0],te=_e[1],le=_e[2],ie=_e[3],V=q.touches[0].clientX-oe,G=q.touches[0].clientY-te,z=q.touches[1].clientX-oe,W=q.touches[1].clientY-te,ae=0<=V&&V<=le&&0<=z&&z<=le&&0<=G&&G<=ie&&0<=W&&W<=ie;var Be=he.pan(),Ne=he.zoom();H=Re(V,G,z,W),j=be(V,G,z,W),Q=[(V+z)/2,(G+W)/2],U=[(Q[0]-Be.x)/Ne,(Q[1]-Be.y)/Ne];var He=200,$e=He*He;if(j<$e&&!q.touches[2]){var Xe=e.findNearestElement(X[0],X[1],!0,!0),Fe=e.findNearestElement(X[2],X[3],!0,!0);Xe&&Xe.isNode()?(Xe.activate().emit(qe("cxttapstart")),e.touchData.start=Xe):Fe&&Fe.isNode()?(Fe.activate().emit(qe("cxttapstart")),e.touchData.start=Fe):he.emit(qe("cxttapstart")),e.touchData.start&&(e.touchData.start._private.grabbed=!1),e.touchData.cxt=!0,e.touchData.cxtDragged=!1,e.data.bgActivePosistion=void 0,e.redraw();return}}if(q.touches[2])he.boxSelectionEnabled()&&q.preventDefault();else if(!q.touches[1]){if(q.touches[0]){var Ke=e.findNearestElements(X[0],X[1],!0,!0),xe=Ke[0];if(xe!=null&&(xe.activate(),e.touchData.start=xe,e.touchData.starts=Ke,e.nodeIsGrabbable(xe))){var mt=e.dragData.touchDragEles=he.collection(),Le=null;e.redrawHint("eles",!0),e.redrawHint("drag",!0),xe.selected()?(Le=he.$(function(St){return St.selected()&&e.nodeIsGrabbable(St)}),m(Le,{addToList:mt})):g(xe,{addToList:mt}),h(xe),xe.emit(qe("grabon")),Le?Le.forEach(function(St){St.emit(qe("grab"))}):xe.emit(qe("grab"))}i(xe,["touchstart","tapstart","vmousedown"],q,{x:X[0],y:X[1]}),xe==null&&(e.data.bgActivePosistion={x:K[0],y:K[1]},e.redrawHint("select",!0),e.redraw()),e.touchData.singleTouchMoved=!1,e.touchData.singleTouchStartTime=+new Date,clearTimeout(e.touchData.tapholdTimeout),e.touchData.tapholdTimeout=setTimeout(function(){e.touchData.singleTouchMoved===!1&&!e.pinching&&!e.touchData.selecting&&i(e.touchData.start,["taphold"],q,{x:X[0],y:X[1]})},e.tapholdDuration)}}if(q.touches.length>=1){for(var ft=e.touchData.startPosition=[null,null,null,null,null,null],wt=0;wt=e.touchTapThreshold2}if(he&&e.touchData.cxt){q.preventDefault();var wt=q.touches[0].clientX-oe,zt=q.touches[0].clientY-te,St=q.touches[1].clientX-oe,At=q.touches[1].clientY-te,bt=be(wt,zt,St,At),me=bt/j,lt=150,gt=lt*lt,Ze=1.5,Ee=Ze*Ze;if(me>=Ee||bt>=gt){e.touchData.cxt=!1,e.data.bgActivePosistion=void 0,e.redrawHint("select",!0);var tt=Ne("cxttapend");e.touchData.start?(e.touchData.start.unactivate().emit(tt),e.touchData.start=null):fe.emit(tt)}}if(he&&e.touchData.cxt){var tt=Ne("cxtdrag");e.data.bgActivePosistion=void 0,e.redrawHint("select",!0),e.touchData.start?e.touchData.start.emit(tt):fe.emit(tt),e.touchData.start&&(e.touchData.start._private.grabbed=!1),e.touchData.cxtDragged=!0;var at=e.findNearestElement(K[0],K[1],!0,!0);(!e.touchData.cxtOver||at!==e.touchData.cxtOver)&&(e.touchData.cxtOver&&e.touchData.cxtOver.emit(Ne("cxtdragout")),e.touchData.cxtOver=at,at&&at.emit(Ne("cxtdragover")))}else if(he&&q.touches[2]&&fe.boxSelectionEnabled())q.preventDefault(),e.data.bgActivePosistion=void 0,this.lastThreeTouch=+new Date,e.touchData.selecting||fe.emit(Ne("boxstart")),e.touchData.selecting=!0,e.touchData.didSelect=!0,X[4]=1,!X||X.length===0||X[0]===void 0?(X[0]=(K[0]+K[2]+K[4])/3,X[1]=(K[1]+K[3]+K[5])/3,X[2]=(K[0]+K[2]+K[4])/3+1,X[3]=(K[1]+K[3]+K[5])/3+1):(X[2]=(K[0]+K[2]+K[4])/3,X[3]=(K[1]+K[3]+K[5])/3),e.redrawHint("select",!0),e.redraw();else if(he&&q.touches[1]&&!e.touchData.didSelect&&fe.zoomingEnabled()&&fe.panningEnabled()&&fe.userZoomingEnabled()&&fe.userPanningEnabled()){q.preventDefault(),e.data.bgActivePosistion=void 0,e.redrawHint("select",!0);var ot=e.dragData.touchDragEles;if(ot){e.redrawHint("drag",!0);for(var Wt=0;Wt0&&!e.hoverData.draggingEles&&!e.swipePanning&&e.data.bgActivePosistion!=null&&(e.data.bgActivePosistion=void 0,e.redrawHint("select",!0),e.redraw())}},"touchmoveHandler"),!1);var Oe;e.registerBinding(t,"touchcancel",Oe=o(function(q){var he=e.touchData.start;e.touchData.capture=!1,he&&he.unactivate()},"touchcancelHandler"));var ue,ye,ke,ce;if(e.registerBinding(t,"touchend",ue=o(function(q){var he=e.touchData.start,X=e.touchData.capture;if(X)q.touches.length===0&&(e.touchData.capture=!1),q.preventDefault();else return;var fe=e.selection;e.swipePanning=!1,e.hoverData.draggingEles=!1;var K=e.cy,qe=K.zoom(),_e=e.touchData.now,Be=e.touchData.earlier;if(q.touches[0]){var Ne=e.projectIntoViewport(q.touches[0].clientX,q.touches[0].clientY);_e[0]=Ne[0],_e[1]=Ne[1]}if(q.touches[1]){var Ne=e.projectIntoViewport(q.touches[1].clientX,q.touches[1].clientY);_e[2]=Ne[0],_e[3]=Ne[1]}if(q.touches[2]){var Ne=e.projectIntoViewport(q.touches[2].clientX,q.touches[2].clientY);_e[4]=Ne[0],_e[5]=Ne[1]}var He=o(function(gt){return{originalEvent:q,type:gt,position:{x:_e[0],y:_e[1]}}},"makeEvent");he&&he.unactivate();var $e;if(e.touchData.cxt){if($e=He("cxttapend"),he?he.emit($e):K.emit($e),!e.touchData.cxtDragged){var Xe=He("cxttap");he?he.emit(Xe):K.emit(Xe)}e.touchData.start&&(e.touchData.start._private.grabbed=!1),e.touchData.cxt=!1,e.touchData.start=null,e.redraw();return}if(!q.touches[2]&&K.boxSelectionEnabled()&&e.touchData.selecting){e.touchData.selecting=!1;var Fe=K.collection(e.getAllInBox(fe[0],fe[1],fe[2],fe[3]));fe[0]=void 0,fe[1]=void 0,fe[2]=void 0,fe[3]=void 0,fe[4]=0,e.redrawHint("select",!0),K.emit(He("boxend"));var Ke=o(function(gt){return gt.selectable()&&!gt.selected()},"eleWouldBeSelected");Fe.emit(He("box")).stdFilter(Ke).select().emit(He("boxselect")),Fe.nonempty()&&e.redrawHint("eles",!0),e.redraw()}if(he?.unactivate(),q.touches[2])e.data.bgActivePosistion=void 0,e.redrawHint("select",!0);else if(!q.touches[1]){if(!q.touches[0]){if(!q.touches[0]){e.data.bgActivePosistion=void 0,e.redrawHint("select",!0);var xe=e.dragData.touchDragEles;if(he!=null){var mt=he._private.grabbed;y(xe),e.redrawHint("drag",!0),e.redrawHint("eles",!0),mt&&(he.emit(He("freeon")),xe.emit(He("free")),e.dragData.didDrag&&(he.emit(He("dragfreeon")),xe.emit(He("dragfree")))),i(he,["touchend","tapend","vmouseup","tapdragout"],q,{x:_e[0],y:_e[1]}),he.unactivate(),e.touchData.start=null}else{var Le=e.findNearestElement(_e[0],_e[1],!0,!0);i(Le,["touchend","tapend","vmouseup","tapdragout"],q,{x:_e[0],y:_e[1]})}var ft=e.touchData.startPosition[0]-_e[0],wt=ft*ft,zt=e.touchData.startPosition[1]-_e[1],St=zt*zt,At=wt+St,bt=At*qe*qe;e.touchData.singleTouchMoved||(he||K.$(":selected").unselect(["tapunselect"]),i(he,["tap","vclick"],q,{x:_e[0],y:_e[1]}),ye=!1,q.timeStamp-ce<=K.multiClickDebounceTime()?(ke&&clearTimeout(ke),ye=!0,ce=null,i(he,["dbltap","vdblclick"],q,{x:_e[0],y:_e[1]})):(ke=setTimeout(function(){ye||i(he,["onetap","voneclick"],q,{x:_e[0],y:_e[1]})},K.multiClickDebounceTime()),ce=q.timeStamp)),he!=null&&!e.dragData.didDrag&&he._private.selectable&&bt"u"){var re=[],J=o(function(q){return{clientX:q.clientX,clientY:q.clientY,force:1,identifier:q.pointerId,pageX:q.pageX,pageY:q.pageY,radiusX:q.width/2,radiusY:q.height/2,screenX:q.screenX,screenY:q.screenY,target:q.target}},"makeTouch"),se=o(function(q){return{event:q,touch:J(q)}},"makePointer"),ge=o(function(q){re.push(se(q))},"addPointer"),Te=o(function(q){for(var he=0;he0)return z[0]}return null},"getCurveT"),g=Object.keys(p),y=0;y0?m:Rbe(a,s,t,r,n,i,l,u)},"intersectLine"),checkPoint:o(function(t,r,n,i,a,s,l,u){u=u==="auto"?qf(i,a):u;var h=2*u;if(Bh(t,r,this.points,s,l,i,a-h,[0,-1],n)||Bh(t,r,this.points,s,l,i-h,a,[0,-1],n))return!0;var d=i/2+2*n,f=a/2+2*n,p=[s-d,l-f,s-d,l,s+d,l,s+d,l-f];return!!(go(t,r,p)||rg(t,r,h,h,s+i/2-u,l+a/2-u,n)||rg(t,r,h,h,s-i/2+u,l+a/2-u,n))},"checkPoint")}};$h.registerNodeShapes=function(){var e=this.nodeShapes={},t=this;this.generateEllipse(),this.generatePolygon("triangle",Ms(3,0)),this.generateRoundPolygon("round-triangle",Ms(3,0)),this.generatePolygon("rectangle",Ms(4,0)),e.square=e.rectangle,this.generateRoundRectangle(),this.generateCutRectangle(),this.generateBarrel(),this.generateBottomRoundrectangle();{var r=[0,1,1,0,0,-1,-1,0];this.generatePolygon("diamond",r),this.generateRoundPolygon("round-diamond",r)}this.generatePolygon("pentagon",Ms(5,0)),this.generateRoundPolygon("round-pentagon",Ms(5,0)),this.generatePolygon("hexagon",Ms(6,0)),this.generateRoundPolygon("round-hexagon",Ms(6,0)),this.generatePolygon("heptagon",Ms(7,0)),this.generateRoundPolygon("round-heptagon",Ms(7,0)),this.generatePolygon("octagon",Ms(8,0)),this.generateRoundPolygon("round-octagon",Ms(8,0));var n=new Array(20);{var i=J$(5,0),a=J$(5,Math.PI/5),s=.5*(3-Math.sqrt(5));s*=1.57;for(var l=0;l=t.deqFastCost*k)break}else if(h){if(b>=t.deqCost*m||b>=t.deqAvgCost*p)break}else if(T>=t.deqNoDrawCost*q$)break;var C=t.deq(n,v,y);if(C.length>0)for(var w=0;w0&&(t.onDeqd(n,g),!h&&t.shouldRedraw(n,g,v,y)&&a())},"dequeue"),l=t.priority||wF;i.beforeRender(s,l(n))}},"setupDequeueingImpl")},"setupDequeueing")},Tmt=(function(){function e(t){var r=arguments.length>1&&arguments[1]!==void 0?arguments[1]:J5;jf(this,e),this.idsByKey=new Ph,this.keyForId=new Ph,this.cachesByLvl=new Ph,this.lvls=[],this.getKey=t,this.doesEleInvalidateKey=r}return o(e,"ElementTextureCacheLookup"),Xf(e,[{key:"getIdsFor",value:o(function(r){r==null&&pi("Can not get id list for null key");var n=this.idsByKey,i=this.idsByKey.get(r);return i||(i=new W1,n.set(r,i)),i},"getIdsFor")},{key:"addIdForKey",value:o(function(r,n){r!=null&&this.getIdsFor(r).add(n)},"addIdForKey")},{key:"deleteIdForKey",value:o(function(r,n){r!=null&&this.getIdsFor(r).delete(n)},"deleteIdForKey")},{key:"getNumberOfIdsForKey",value:o(function(r){return r==null?0:this.getIdsFor(r).size},"getNumberOfIdsForKey")},{key:"updateKeyMappingFor",value:o(function(r){var n=r.id(),i=this.keyForId.get(n),a=this.getKey(r);this.deleteIdForKey(i,n),this.addIdForKey(a,n),this.keyForId.set(n,a)},"updateKeyMappingFor")},{key:"deleteKeyMappingFor",value:o(function(r){var n=r.id(),i=this.keyForId.get(n);this.deleteIdForKey(i,n),this.keyForId.delete(n)},"deleteKeyMappingFor")},{key:"keyHasChangedFor",value:o(function(r){var n=r.id(),i=this.keyForId.get(n),a=this.getKey(r);return i!==a},"keyHasChangedFor")},{key:"isInvalid",value:o(function(r){return this.keyHasChangedFor(r)||this.doesEleInvalidateKey(r)},"isInvalid")},{key:"getCachesAt",value:o(function(r){var n=this.cachesByLvl,i=this.lvls,a=n.get(r);return a||(a=new Ph,n.set(r,a),i.push(r)),a},"getCachesAt")},{key:"getCache",value:o(function(r,n){return this.getCachesAt(n).get(r)},"getCache")},{key:"get",value:o(function(r,n){var i=this.getKey(r),a=this.getCache(i,n);return a!=null&&this.updateKeyMappingFor(r),a},"get")},{key:"getForCachedKey",value:o(function(r,n){var i=this.keyForId.get(r.id()),a=this.getCache(i,n);return a},"getForCachedKey")},{key:"hasCache",value:o(function(r,n){return this.getCachesAt(n).has(r)},"hasCache")},{key:"has",value:o(function(r,n){var i=this.getKey(r);return this.hasCache(i,n)},"has")},{key:"setCache",value:o(function(r,n,i){i.key=r,this.getCachesAt(n).set(r,i)},"setCache")},{key:"set",value:o(function(r,n,i){var a=this.getKey(r);this.setCache(a,n,i),this.updateKeyMappingFor(r)},"set")},{key:"deleteCache",value:o(function(r,n){this.getCachesAt(n).delete(r)},"deleteCache")},{key:"delete",value:o(function(r,n){var i=this.getKey(r);this.deleteCache(i,n)},"_delete")},{key:"invalidateKey",value:o(function(r){var n=this;this.lvls.forEach(function(i){return n.deleteCache(r,i)})},"invalidateKey")},{key:"invalidate",value:o(function(r){var n=r.id(),i=this.keyForId.get(n);this.deleteKeyMappingFor(r);var a=this.doesEleInvalidateKey(r);return a&&this.invalidateKey(i),a||this.getNumberOfIdsForKey(i)===0},"invalidate")}])})(),qxe=25,B5=50,j5=-4,fF=3,M2e=7.99,Cmt=8,wmt=1024,kmt=1024,Smt=1024,Emt=.2,Amt=.8,Rmt=10,_mt=.15,Lmt=.1,Dmt=.9,Imt=.9,Mmt=100,Nmt=1,P1={dequeue:"dequeue",downscale:"downscale",highQuality:"highQuality"},Pmt=Va({getKey:null,doesEleInvalidateKey:J5,drawElement:null,getBoundingBox:null,getRotationPoint:null,getRotationOffset:null,isVisible:Tbe,allowEdgeTxrCaching:!0,allowParentTxrCaching:!0}),IT=o(function(t,r){var n=this;n.renderer=t,n.onDequeues=[];var i=Pmt(r);br(n,i),n.lookup=new Tmt(i.getKey,i.doesEleInvalidateKey),n.setupDequeueing()},"ElementTextureCache"),ua=IT.prototype;ua.reasons=P1;ua.getTextureQueue=function(e){var t=this;return t.eleImgCaches=t.eleImgCaches||{},t.eleImgCaches[e]=t.eleImgCaches[e]||[]};ua.getRetiredTextureQueue=function(e){var t=this,r=t.eleImgCaches.retired=t.eleImgCaches.retired||{},n=r[e]=r[e]||[];return n};ua.getElementQueue=function(){var e=this,t=e.eleCacheQueue=e.eleCacheQueue||new QT(function(r,n){return n.reqs-r.reqs});return t};ua.getElementKeyToQueue=function(){var e=this,t=e.eleKeyToCacheQueue=e.eleKeyToCacheQueue||{};return t};ua.getElement=function(e,t,r,n,i){var a=this,s=this.renderer,l=s.cy.zoom(),u=this.lookup;if(!t||t.w===0||t.h===0||isNaN(t.w)||isNaN(t.h)||!e.visible()||e.removed()||!a.allowEdgeTxrCaching&&e.isEdge()||!a.allowParentTxrCaching&&e.isParent())return null;if(n==null&&(n=Math.ceil(SF(l*r))),n=M2e||n>fF)return null;var h=Math.pow(2,n),d=t.h*h,f=t.w*h,p=s.eleTextBiggerThanMin(e,h);if(!this.isVisible(e,p))return null;var m=u.get(e,n);if(m&&m.invalidated&&(m.invalidated=!1,m.texture.invalidatedWidth-=m.width),m)return m;var g;if(d<=qxe?g=qxe:d<=B5?g=B5:g=Math.ceil(d/B5)*B5,d>Smt||f>kmt)return null;var y=a.getTextureQueue(g),v=y[y.length-2],x=o(function(){return a.recycleTexture(g,f)||a.addTexture(g,f)},"addNewTxr");v||(v=y[y.length-1]),v||(v=x()),v.width-v.usedWidthn;I--)L=a.getElement(e,t,r,I,P1.downscale);N()}else return a.queueElement(e,w.level-1),w;else{var _;if(!T&&!k&&!C)for(var A=n-1;A>=j5;A--){var M=u.get(e,A);if(M){_=M;break}}if(b(_))return a.queueElement(e,n),_;v.context.translate(v.usedWidth,0),v.context.scale(h,h),this.drawElement(v.context,e,t,p,!1),v.context.scale(1/h,1/h),v.context.translate(-v.usedWidth,0)}return m={x:v.usedWidth,texture:v,level:n,scale:h,width:f,height:d,scaledLabelShown:p},v.usedWidth+=Math.ceil(f+Cmt),v.eleCaches.push(m),u.set(e,n,m),a.checkTextureFullness(v),m};ua.invalidateElements=function(e){for(var t=0;t=Emt*e.width&&this.retireTexture(e)};ua.checkTextureFullness=function(e){var t=this,r=t.getTextureQueue(e.height);e.usedWidth/e.width>Amt&&e.fullnessChecks>=Rmt?Wf(r,e):e.fullnessChecks++};ua.retireTexture=function(e){var t=this,r=e.height,n=t.getTextureQueue(r),i=this.lookup;Wf(n,e),e.retired=!0;for(var a=e.eleCaches,s=0;s=t)return s.retired=!1,s.usedWidth=0,s.invalidatedWidth=0,s.fullnessChecks=0,kF(s.eleCaches),s.context.setTransform(1,0,0,1,0,0),s.context.clearRect(0,0,s.width,s.height),Wf(i,s),n.push(s),s}};ua.queueElement=function(e,t){var r=this,n=r.getElementQueue(),i=r.getElementKeyToQueue(),a=this.getKey(e),s=i[a];if(s)s.level=Math.max(s.level,t),s.eles.merge(e),s.reqs++,n.updateItem(s);else{var l={eles:e.spawn().merge(e),level:t,reqs:1,key:a};n.push(l),i[a]=l}};ua.dequeue=function(e){for(var t=this,r=t.getElementQueue(),n=t.getElementKeyToQueue(),i=[],a=t.lookup,s=0;s0;s++){var l=r.pop(),u=l.key,h=l.eles[0],d=a.hasCache(h,l.level);if(n[u]=null,d)continue;i.push(l);var f=t.getBoundingBox(h);t.getElement(h,f,e,l.level,P1.dequeue)}return i};ua.removeFromQueue=function(e){var t=this,r=t.getElementQueue(),n=t.getElementKeyToQueue(),i=this.getKey(e),a=n[i];a!=null&&(a.eles.length===1?(a.reqs=CF,r.updateItem(a),r.pop(),n[i]=null):a.eles.unmerge(e))};ua.onDequeue=function(e){this.onDequeues.push(e)};ua.offDequeue=function(e){Wf(this.onDequeues,e)};ua.setupDequeueing=I2e.setupDequeueing({deqRedrawThreshold:Mmt,deqCost:_mt,deqAvgCost:Lmt,deqNoDrawCost:Dmt,deqFastCost:Imt,deq:o(function(t,r,n){return t.dequeue(r,n)},"deq"),onDeqd:o(function(t,r){for(var n=0;n=Bmt||r>oA)return null}n.validateLayersElesOrdering(r,e);var u=n.layersByLevel,h=Math.pow(2,r),d=u[r]=u[r]||[],f,p=n.levelIsComplete(r,e),m,g=o(function(){var N=o(function(D){if(n.validateLayersElesOrdering(D,e),n.levelIsComplete(D,e))return m=u[D],!0},"canUseAsTmpLvl"),I=o(function(D){if(!m)for(var P=r+D;NT<=P&&P<=oA&&!N(P);P+=D);},"checkLvls");I(1),I(-1);for(var _=d.length-1;_>=0;_--){var A=d[_];A.invalid&&Wf(d,A)}},"checkTempLevels");if(!p)g();else return d;var y=o(function(){if(!f){f=Ns();for(var N=0;NUxe||A>Uxe)return null;var M=_*A;if(M>Hmt)return null;var D=n.makeLayer(f,r);if(I!=null){var P=d.indexOf(I)+1;d.splice(P,0,D)}else(N.insert===void 0||N.insert)&&d.unshift(D);return D},"makeLayer");if(n.skipping&&!l)return null;for(var x=null,b=e.length/Omt,T=!l,k=0;k=b||!Abe(x.bb,C.boundingBox()))&&(x=v({insert:!0,after:x}),!x))return null;m||T?n.queueLayer(x,C):n.drawEleInLayer(x,C,r,t),x.eles.push(C),S[r]=x}return m||(T?null:d)};Wa.getEleLevelForLayerLevel=function(e,t){return e};Wa.drawEleInLayer=function(e,t,r,n){var i=this,a=this.renderer,s=e.context,l=t.boundingBox();l.w===0||l.h===0||!t.visible()||(r=i.getEleLevelForLayerLevel(r,n),a.setImgSmoothing(s,!1),a.drawCachedElement(s,t,null,null,r,Umt),a.setImgSmoothing(s,!0))};Wa.levelIsComplete=function(e,t){var r=this,n=r.layersByLevel[e];if(!n||n.length===0)return!1;for(var i=0,a=0;a0||s.invalid)return!1;i+=s.eles.length}return i===t.length};Wa.validateLayersElesOrdering=function(e,t){var r=this.layersByLevel[e];if(r)for(var n=0;n0){t=!0;break}}return t};Wa.invalidateElements=function(e){var t=this;e.length!==0&&(t.lastInvalidationTime=Oh(),!(e.length===0||!t.haveLayers())&&t.updateElementsInLayers(e,o(function(n,i,a){t.invalidateLayer(n)},"invalAssocLayers")))};Wa.invalidateLayer=function(e){if(this.lastInvalidationTime=Oh(),!e.invalid){var t=e.level,r=e.eles,n=this.layersByLevel[t];Wf(n,e),e.elesQueue=[],e.invalid=!0,e.replacement&&(e.replacement.invalid=!0);for(var i=0;i3&&arguments[3]!==void 0?arguments[3]:!0,i=arguments.length>4&&arguments[4]!==void 0?arguments[4]:!0,a=arguments.length>5&&arguments[5]!==void 0?arguments[5]:!0,s=this,l=t._private.rscratch;if(!(a&&!t.visible())&&!(l.badLine||l.allpts==null||isNaN(l.allpts[0]))){var u;r&&(u=r,e.translate(-u.x1,-u.y1));var h=a?t.pstyle("opacity").value:1,d=a?t.pstyle("line-opacity").value:1,f=t.pstyle("curve-style").value,p=t.pstyle("line-style").value,m=t.pstyle("width").pfValue,g=t.pstyle("line-cap").value,y=t.pstyle("line-outline-width").value,v=t.pstyle("line-outline-color").value,x=h*d,b=h*d,T=o(function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:x;f==="straight-triangle"?(s.eleStrokeStyle(e,t,D),s.drawEdgeTrianglePath(t,e,l.allpts)):(e.lineWidth=m,e.lineCap=g,s.eleStrokeStyle(e,t,D),s.drawEdgePath(t,e,l.allpts,p),e.lineCap="butt")},"drawLine"),k=o(function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:x;if(e.lineWidth=m+y,e.lineCap=g,y>0)s.colorStrokeStyle(e,v[0],v[1],v[2],D);else{e.lineCap="butt";return}f==="straight-triangle"?s.drawEdgeTrianglePath(t,e,l.allpts):(s.drawEdgePath(t,e,l.allpts,p),e.lineCap="butt")},"drawLineOutline"),C=o(function(){i&&s.drawEdgeOverlay(e,t)},"drawOverlay"),w=o(function(){i&&s.drawEdgeUnderlay(e,t)},"drawUnderlay"),S=o(function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:b;s.drawArrowheads(e,t,D)},"drawArrows"),R=o(function(){s.drawElementText(e,t,null,n)},"drawText");e.lineJoin="round";var L=t.pstyle("ghost").value==="yes";if(L){var N=t.pstyle("ghost-offset-x").pfValue,I=t.pstyle("ghost-offset-y").pfValue,_=t.pstyle("ghost-opacity").value,A=x*_;e.translate(N,I),T(A),S(A),e.translate(-N,-I)}else k();w(),T(),S(),C(),R(),r&&e.translate(u.x1,u.y1)}};O2e=o(function(t){if(!["overlay","underlay"].includes(t))throw new Error("Invalid state");return function(r,n){if(n.visible()){var i=n.pstyle("".concat(t,"-opacity")).value;if(i!==0){var a=this,s=a.usePaths(),l=n._private.rscratch,u=n.pstyle("".concat(t,"-padding")).pfValue,h=2*u,d=n.pstyle("".concat(t,"-color")).value;r.lineWidth=h,l.edgeType==="self"&&!s?r.lineCap="butt":r.lineCap="round",a.colorStrokeStyle(r,d[0],d[1],d[2],i),a.drawEdgePath(n,r,l.allpts,"solid")}}}},"drawEdgeOverlayUnderlay");Fh.drawEdgeOverlay=O2e("overlay");Fh.drawEdgeUnderlay=O2e("underlay");Fh.drawEdgePath=function(e,t,r,n){var i=e._private.rscratch,a=t,s,l=!1,u=this.usePaths(),h=e.pstyle("line-dash-pattern").pfValue,d=e.pstyle("line-dash-offset").pfValue;if(u){var f=r.join("$"),p=i.pathCacheKey&&i.pathCacheKey===f;p?(s=t=i.pathCache,l=!0):(s=t=new Path2D,i.pathCacheKey=f,i.pathCache=s)}if(a.setLineDash)switch(n){case"dotted":a.setLineDash([1,1]);break;case"dashed":a.setLineDash(h),a.lineDashOffset=d;break;case"solid":a.setLineDash([]);break}if(!l&&!i.badLine)switch(t.beginPath&&t.beginPath(),t.moveTo(r[0],r[1]),i.edgeType){case"bezier":case"self":case"compound":case"multibezier":for(var m=2;m+35&&arguments[5]!==void 0?arguments[5]:!0,s=this;if(n==null){if(a&&!s.eleTextBiggerThanMin(t))return}else if(n===!1)return;if(t.isNode()){var l=t.pstyle("label");if(!l||!l.value)return;var u=s.getLabelJustification(t);e.textAlign=u,e.textBaseline="bottom"}else{var h=t.element()._private.rscratch.badLine,d=t.pstyle("label"),f=t.pstyle("source-label"),p=t.pstyle("target-label");if(h||(!d||!d.value)&&(!f||!f.value)&&(!p||!p.value))return;e.textAlign="center",e.textBaseline="bottom"}var m=!r,g;r&&(g=r,e.translate(-g.x1,-g.y1)),i==null?(s.drawText(e,t,null,m,a),t.isEdge()&&(s.drawText(e,t,"source",m,a),s.drawText(e,t,"target",m,a))):s.drawText(e,t,i,m,a),r&&e.translate(g.x1,g.y1)};cg.getFontCache=function(e){var t;this.fontCaches=this.fontCaches||[];for(var r=0;r2&&arguments[2]!==void 0?arguments[2]:!0,n=t.pstyle("font-style").strValue,i=t.pstyle("font-size").pfValue+"px",a=t.pstyle("font-family").strValue,s=t.pstyle("font-weight").strValue,l=r?t.effectiveOpacity()*t.pstyle("text-opacity").value:1,u=t.pstyle("text-outline-opacity").value*l,h=t.pstyle("color").value,d=t.pstyle("text-outline-color").value;e.font=n+" "+s+" "+i+" "+a,e.lineJoin="round",this.colorFillStyle(e,h[0],h[1],h[2],l),this.colorStrokeStyle(e,d[0],d[1],d[2],u)};o(ngt,"circle");o(Kxe,"roundRect");cg.getTextAngle=function(e,t){var r,n=e._private,i=n.rscratch,a=t?t+"-":"",s=e.pstyle(a+"text-rotation");if(s.strValue==="autorotate"){var l=mo(i,"labelAngle",t);r=e.isEdge()?l:0}else s.strValue==="none"?r=0:r=s.pfValue;return r};cg.drawText=function(e,t,r){var n=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!0,i=arguments.length>4&&arguments[4]!==void 0?arguments[4]:!0,a=t._private,s=a.rscratch,l=i?t.effectiveOpacity():1;if(!(i&&(l===0||t.pstyle("text-opacity").value===0))){r==="main"&&(r=null);var u=mo(s,"labelX",r),h=mo(s,"labelY",r),d,f,p=this.getLabelText(t,r);if(p!=null&&p!==""&&!isNaN(u)&&!isNaN(h)){this.setupTextStyle(e,t,i);var m=r?r+"-":"",g=mo(s,"labelWidth",r),y=mo(s,"labelHeight",r),v=t.pstyle(m+"text-margin-x").pfValue,x=t.pstyle(m+"text-margin-y").pfValue,b=t.isEdge(),T=t.pstyle("text-halign").value,k=t.pstyle("text-valign").value;b&&(T="center",k="center"),u+=v,h+=x;var C;switch(n?C=this.getTextAngle(t,r):C=0,C!==0&&(d=u,f=h,e.translate(d,f),e.rotate(C),u=0,h=0),k){case"top":break;case"center":h+=y/2;break;case"bottom":h+=y;break}var w=t.pstyle("text-background-opacity").value,S=t.pstyle("text-border-opacity").value,R=t.pstyle("text-border-width").pfValue,L=t.pstyle("text-background-padding").pfValue,N=t.pstyle("text-background-shape").strValue,I=N==="round-rectangle"||N==="roundrectangle",_=N==="circle",A=2;if(w>0||R>0&&S>0){var M=e.fillStyle,D=e.strokeStyle,P=e.lineWidth,B=t.pstyle("text-background-color").value,O=t.pstyle("text-border-color").value,$=t.pstyle("text-border-style").value,V=w>0,G=R>0&&S>0,z=u-L;switch(T){case"left":z-=g;break;case"center":z-=g/2;break}var W=h-y-L,H=g+2*L,j=y+2*L;if(V&&(e.fillStyle="rgba(".concat(B[0],",").concat(B[1],",").concat(B[2],",").concat(w*l,")")),G&&(e.strokeStyle="rgba(".concat(O[0],",").concat(O[1],",").concat(O[2],",").concat(S*l,")"),e.lineWidth=R,e.setLineDash))switch($){case"dotted":e.setLineDash([1,1]);break;case"dashed":e.setLineDash([4,2]);break;case"double":e.lineWidth=R/4,e.setLineDash([]);break;case"solid":default:e.setLineDash([]);break}if(I?(e.beginPath(),Kxe(e,z,W,H,j,A)):_?(e.beginPath(),ngt(e,z,W,H,j)):(e.beginPath(),e.rect(z,W,H,j)),V&&e.fill(),G&&e.stroke(),G&&$==="double"){var Q=R/2;e.beginPath(),I?Kxe(e,z+Q,W+Q,H-2*Q,j-2*Q,A):e.rect(z+Q,W+Q,H-2*Q,j-2*Q),e.stroke()}e.fillStyle=M,e.strokeStyle=D,e.lineWidth=P,e.setLineDash&&e.setLineDash([])}var U=2*t.pstyle("text-outline-width").pfValue;if(U>0&&(e.lineWidth=U),t.pstyle("text-wrap").value==="wrap"){var oe=mo(s,"labelWrapCachedLines",r),te=mo(s,"labelLineHeight",r),le=g/2,ie=this.getLabelJustification(t);switch(ie==="auto"||(T==="left"?ie==="left"?u+=-g:ie==="center"&&(u+=-le):T==="center"?ie==="left"?u+=-le:ie==="right"&&(u+=le):T==="right"&&(ie==="center"?u+=le:ie==="right"&&(u+=g))),k){case"top":h-=(oe.length-1)*te;break;case"center":case"bottom":h-=(oe.length-1)*te;break}for(var ae=0;ae0&&e.strokeText(oe[ae],u,h),e.fillText(oe[ae],u,h),h+=te}else U>0&&e.strokeText(p,u,h),e.fillText(p,u,h);C!==0&&(e.rotate(-C),e.translate(-d,-f))}}};Zf={};Zf.drawNode=function(e,t,r){var n=arguments.length>3&&arguments[3]!==void 0?arguments[3]:!0,i=arguments.length>4&&arguments[4]!==void 0?arguments[4]:!0,a=arguments.length>5&&arguments[5]!==void 0?arguments[5]:!0,s=this,l,u,h=t._private,d=h.rscratch,f=t.position();if(!(!Gt(f.x)||!Gt(f.y))&&!(a&&!t.visible())){var p=a?t.effectiveOpacity():1,m=s.usePaths(),g,y=!1,v=t.padding();l=t.width()+2*v,u=t.height()+2*v;var x;r&&(x=r,e.translate(-x.x1,-x.y1));for(var b=t.pstyle("background-image"),T=b.value,k=new Array(T.length),C=new Array(T.length),w=0,S=0;S0&&arguments[0]!==void 0?arguments[0]:A;s.eleFillStyle(e,t,q)},"setupShapeColor"),te=o(function(){var q=arguments.length>0&&arguments[0]!==void 0?arguments[0]:G;s.colorStrokeStyle(e,M[0],M[1],M[2],q)},"setupBorderColor"),le=o(function(){var q=arguments.length>0&&arguments[0]!==void 0?arguments[0]:j;s.colorStrokeStyle(e,W[0],W[1],W[2],q)},"setupOutlineColor"),ie=o(function(q,he,X,fe){var K=s.nodePathCache=s.nodePathCache||[],qe=bbe(X==="polygon"?X+","+fe.join(","):X,""+he,""+q,""+U),_e=K[qe],Be,Ne=!1;return _e!=null?(Be=_e,Ne=!0,d.pathCache=Be):(Be=new Path2D,K[qe]=d.pathCache=Be),{path:Be,cacheHit:Ne}},"getPath"),ae=t.pstyle("shape").strValue,Re=t.pstyle("shape-polygon-points").pfValue;if(m){e.translate(f.x,f.y);var be=ie(l,u,ae,Re);g=be.path,y=be.cacheHit}var Pe=o(function(){if(!y){var q=f;m&&(q={x:0,y:0}),s.nodeShapes[s.getNodeShape(t)].draw(g||e,q.x,q.y,l,u,U,d)}m?e.fill(g):e.fill()},"drawShape"),Ge=o(function(){for(var q=arguments.length>0&&arguments[0]!==void 0?arguments[0]:p,he=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!0,X=h.backgrounding,fe=0,K=0;K0&&arguments[0]!==void 0?arguments[0]:!1,he=arguments.length>1&&arguments[1]!==void 0?arguments[1]:p;s.hasPie(t)&&(s.drawPie(e,t,he),q&&(m||s.nodeShapes[s.getNodeShape(t)].draw(e,f.x,f.y,l,u,U,d)))},"drawPie"),ue=o(function(){var q=arguments.length>0&&arguments[0]!==void 0?arguments[0]:!1,he=arguments.length>1&&arguments[1]!==void 0?arguments[1]:p;s.hasStripe(t)&&(e.save(),m?e.clip(d.pathCache):(s.nodeShapes[s.getNodeShape(t)].draw(e,f.x,f.y,l,u,U,d),e.clip()),s.drawStripe(e,t,he),e.restore(),q&&(m||s.nodeShapes[s.getNodeShape(t)].draw(e,f.x,f.y,l,u,U,d)))},"drawStripe"),ye=o(function(){var q=arguments.length>0&&arguments[0]!==void 0?arguments[0]:p,he=(I>0?I:-I)*q,X=I>0?0:255;I!==0&&(s.colorFillStyle(e,X,X,X,he),m?e.fill(g):e.fill())},"darken"),ke=o(function(){if(_>0){if(e.lineWidth=_,e.lineCap=B,e.lineJoin=P,e.setLineDash)switch(D){case"dotted":e.setLineDash([1,1]);break;case"dashed":e.setLineDash($),e.lineDashOffset=V;break;case"solid":case"double":e.setLineDash([]);break}if(O!=="center"){if(e.save(),e.lineWidth*=2,O==="inside")m?e.clip(g):e.clip();else{var q=new Path2D;q.rect(-l/2-_,-u/2-_,l+2*_,u+2*_),q.addPath(g),e.clip(q,"evenodd")}m?e.stroke(g):e.stroke(),e.restore()}else m?e.stroke(g):e.stroke();if(D==="double"){e.lineWidth=_/3;var he=e.globalCompositeOperation;e.globalCompositeOperation="destination-out",m?e.stroke(g):e.stroke(),e.globalCompositeOperation=he}e.setLineDash&&e.setLineDash([])}},"drawBorder"),ce=o(function(){if(z>0){if(e.lineWidth=z,e.lineCap="butt",e.setLineDash)switch(H){case"dotted":e.setLineDash([1,1]);break;case"dashed":e.setLineDash([4,2]);break;case"solid":case"double":e.setLineDash([]);break}var q=f;m&&(q={x:0,y:0});var he=s.getNodeShape(t),X=_;O==="inside"&&(X=0),O==="outside"&&(X*=2);var fe=(l+X+(z+Q))/l,K=(u+X+(z+Q))/u,qe=l*fe,_e=u*K,Be=s.nodeShapes[he].points,Ne;if(m){var He=ie(qe,_e,he,Be);Ne=He.path}if(he==="ellipse")s.drawEllipsePath(Ne||e,q.x,q.y,qe,_e);else if(["round-diamond","round-heptagon","round-hexagon","round-octagon","round-pentagon","round-polygon","round-triangle","round-tag"].includes(he)){var $e=0,Xe=0,Fe=0;he==="round-diamond"?$e=(X+Q+z)*1.4:he==="round-heptagon"?($e=(X+Q+z)*1.075,Fe=-(X/2+Q+z)/35):he==="round-hexagon"?$e=(X+Q+z)*1.12:he==="round-pentagon"?($e=(X+Q+z)*1.13,Fe=-(X/2+Q+z)/15):he==="round-tag"?($e=(X+Q+z)*1.12,Xe=(X/2+z+Q)*.07):he==="round-triangle"&&($e=(X+Q+z)*(Math.PI/2),Fe=-(X+Q/2+z)/Math.PI),$e!==0&&(fe=(l+$e)/l,qe=l*fe,["round-hexagon","round-tag"].includes(he)||(K=(u+$e)/u,_e=u*K)),U=U==="auto"?Lbe(qe,_e):U;for(var Ke=qe/2,xe=_e/2,mt=U+(X+z+Q)/2,Le=new Array(Be.length/2),ft=new Array(Be.length/2),wt=0;wt0){if(i=i||n.position(),a==null||s==null){var m=n.padding();a=n.width()+2*m,s=n.height()+2*m}l.colorFillStyle(r,d[0],d[1],d[2],h),l.nodeShapes[f].draw(r,i.x,i.y,a+u*2,s+u*2,p),r.fill()}}}},"drawNodeOverlayUnderlay");Zf.drawNodeOverlay=B2e("overlay");Zf.drawNodeUnderlay=B2e("underlay");Zf.hasPie=function(e){return e=e[0],e._private.hasPie};Zf.hasStripe=function(e){return e=e[0],e._private.hasStripe};Zf.drawPie=function(e,t,r,n){t=t[0],n=n||t.position();var i=t.cy().style(),a=t.pstyle("pie-size"),s=t.pstyle("pie-hole"),l=t.pstyle("pie-start-angle").pfValue,u=n.x,h=n.y,d=t.width(),f=t.height(),p=Math.min(d,f)/2,m,g=0,y=this.usePaths();if(y&&(u=0,h=0),a.units==="%"?p=p*a.pfValue:a.pfValue!==void 0&&(p=a.pfValue/2),s.units==="%"?m=p*s.pfValue:s.pfValue!==void 0&&(m=s.pfValue/2),!(m>=p))for(var v=1;v<=i.pieBackgroundN;v++){var x=t.pstyle("pie-"+v+"-background-size").value,b=t.pstyle("pie-"+v+"-background-color").value,T=t.pstyle("pie-"+v+"-background-opacity").value*r,k=x/100;k+g>1&&(k=1-g);var C=1.5*Math.PI+2*Math.PI*g;C+=l;var w=2*Math.PI*k,S=C+w;x===0||g>=1||g+k>1||(m===0?(e.beginPath(),e.moveTo(u,h),e.arc(u,h,p,C,S),e.closePath()):(e.beginPath(),e.arc(u,h,p,C,S),e.arc(u,h,m,S,C,!0),e.closePath()),this.colorFillStyle(e,b[0],b[1],b[2],T),e.fill(),g+=k)}};Zf.drawStripe=function(e,t,r,n){t=t[0],n=n||t.position();var i=t.cy().style(),a=n.x,s=n.y,l=t.width(),u=t.height(),h=0,d=this.usePaths();e.save();var f=t.pstyle("stripe-direction").value,p=t.pstyle("stripe-size");switch(f){case"vertical":break;case"righward":e.rotate(-Math.PI/2);break}var m=l,g=u;p.units==="%"?(m=m*p.pfValue,g=g*p.pfValue):p.pfValue!==void 0&&(m=p.pfValue,g=p.pfValue),d&&(a=0,s=0),s-=m/2,a-=g/2;for(var y=1;y<=i.stripeBackgroundN;y++){var v=t.pstyle("stripe-"+y+"-background-size").value,x=t.pstyle("stripe-"+y+"-background-color").value,b=t.pstyle("stripe-"+y+"-background-opacity").value*r,T=v/100;T+h>1&&(T=1-h),!(v===0||h>=1||h+T>1)&&(e.beginPath(),e.rect(a,s+g*h,m,g*T),e.closePath(),this.colorFillStyle(e,x[0],x[1],x[2],b),e.fill(),h+=T)}e.restore()};Ps={},igt=100;Ps.getPixelRatio=function(){var e=this.data.contexts[0];if(this.forcedPixelRatio!=null)return this.forcedPixelRatio;var t=this.cy.window(),r=e.backingStorePixelRatio||e.webkitBackingStorePixelRatio||e.mozBackingStorePixelRatio||e.msBackingStorePixelRatio||e.oBackingStorePixelRatio||e.backingStorePixelRatio||1;return(t.devicePixelRatio||1)/r};Ps.paintCache=function(e){for(var t=this.paintCaches=this.paintCaches||[],r=!0,n,i=0;it.minMbLowQualFrames&&(t.motionBlurPxRatio=t.mbPxRBlurry)),t.clearingMotionBlur&&(t.motionBlurPxRatio=1),t.textureDrawLastFrame&&!f&&(d[t.NODE]=!0,d[t.SELECT_BOX]=!0);var b=r.style(),T=r.zoom(),k=s!==void 0?s:T,C=r.pan(),w={x:C.x,y:C.y},S={zoom:T,pan:{x:C.x,y:C.y}},R=t.prevViewport,L=R===void 0||S.zoom!==R.zoom||S.pan.x!==R.pan.x||S.pan.y!==R.pan.y;!L&&!(y&&!g)&&(t.motionBlurPxRatio=1),l&&(w=l),k*=u,w.x*=u,w.y*=u;var N=t.getCachedZSortedEles();function I(te,le,ie,ae,Re){var be=te.globalCompositeOperation;te.globalCompositeOperation="destination-out",t.colorFillStyle(te,255,255,255,t.motionBlurTransparency),te.fillRect(le,ie,ae,Re),te.globalCompositeOperation=be}o(I,"mbclear");function _(te,le){var ie,ae,Re,be;!t.clearingMotionBlur&&(te===h.bufferContexts[t.MOTIONBLUR_BUFFER_NODE]||te===h.bufferContexts[t.MOTIONBLUR_BUFFER_DRAG])?(ie={x:C.x*m,y:C.y*m},ae=T*m,Re=t.canvasWidth*m,be=t.canvasHeight*m):(ie=w,ae=k,Re=t.canvasWidth,be=t.canvasHeight),te.setTransform(1,0,0,1,0,0),le==="motionBlur"?I(te,0,0,Re,be):!n&&(le===void 0||le)&&te.clearRect(0,0,Re,be),i||(te.translate(ie.x,ie.y),te.scale(ae,ae)),l&&te.translate(l.x,l.y),s&&te.scale(s,s)}if(o(_,"setContextTransform"),f||(t.textureDrawLastFrame=!1),f){if(t.textureDrawLastFrame=!0,!t.textureCache){t.textureCache={},t.textureCache.bb=r.mutableElements().boundingBox(),t.textureCache.texture=t.data.bufferCanvases[t.TEXTURE_BUFFER];var A=t.data.bufferContexts[t.TEXTURE_BUFFER];A.setTransform(1,0,0,1,0,0),A.clearRect(0,0,t.canvasWidth*t.textureMult,t.canvasHeight*t.textureMult),t.render({forcedContext:A,drawOnlyNodeLayer:!0,forcedPxRatio:u*t.textureMult});var S=t.textureCache.viewport={zoom:r.zoom(),pan:r.pan(),width:t.canvasWidth,height:t.canvasHeight};S.mpan={x:(0-S.pan.x)/S.zoom,y:(0-S.pan.y)/S.zoom}}d[t.DRAG]=!1,d[t.NODE]=!1;var M=h.contexts[t.NODE],D=t.textureCache.texture,S=t.textureCache.viewport;M.setTransform(1,0,0,1,0,0),p?I(M,0,0,S.width,S.height):M.clearRect(0,0,S.width,S.height);var P=b.core("outside-texture-bg-color").value,B=b.core("outside-texture-bg-opacity").value;t.colorFillStyle(M,P[0],P[1],P[2],B),M.fillRect(0,0,S.width,S.height);var T=r.zoom();_(M,!1),M.clearRect(S.mpan.x,S.mpan.y,S.width/S.zoom/u,S.height/S.zoom/u),M.drawImage(D,S.mpan.x,S.mpan.y,S.width/S.zoom/u,S.height/S.zoom/u)}else t.textureOnViewport&&!n&&(t.textureCache=null);var O=r.extent(),$=t.pinching||t.hoverData.dragging||t.swipePanning||t.data.wheelZooming||t.hoverData.draggingEles||t.cy.animated(),V=t.hideEdgesOnViewport&&$,G=[];if(G[t.NODE]=!d[t.NODE]&&p&&!t.clearedForMotionBlur[t.NODE]||t.clearingMotionBlur,G[t.NODE]&&(t.clearedForMotionBlur[t.NODE]=!0),G[t.DRAG]=!d[t.DRAG]&&p&&!t.clearedForMotionBlur[t.DRAG]||t.clearingMotionBlur,G[t.DRAG]&&(t.clearedForMotionBlur[t.DRAG]=!0),d[t.NODE]||i||a||G[t.NODE]){var z=p&&!G[t.NODE]&&m!==1,M=n||(z?t.data.bufferContexts[t.MOTIONBLUR_BUFFER_NODE]:h.contexts[t.NODE]),W=p&&!z?"motionBlur":void 0;_(M,W),V?t.drawCachedNodes(M,N.nondrag,u,O):t.drawLayeredElements(M,N.nondrag,u,O),t.debug&&t.drawDebugPoints(M,N.nondrag),!i&&!p&&(d[t.NODE]=!1)}if(!a&&(d[t.DRAG]||i||G[t.DRAG])){var z=p&&!G[t.DRAG]&&m!==1,M=n||(z?t.data.bufferContexts[t.MOTIONBLUR_BUFFER_DRAG]:h.contexts[t.DRAG]);_(M,p&&!z?"motionBlur":void 0),V?t.drawCachedNodes(M,N.drag,u,O):t.drawCachedElements(M,N.drag,u,O),t.debug&&t.drawDebugPoints(M,N.drag),!i&&!p&&(d[t.DRAG]=!1)}if(this.drawSelectionRectangle(e,_),p&&m!==1){var H=h.contexts[t.NODE],j=t.data.bufferCanvases[t.MOTIONBLUR_BUFFER_NODE],Q=h.contexts[t.DRAG],U=t.data.bufferCanvases[t.MOTIONBLUR_BUFFER_DRAG],oe=o(function(le,ie,ae){le.setTransform(1,0,0,1,0,0),ae||!x?le.clearRect(0,0,t.canvasWidth,t.canvasHeight):I(le,0,0,t.canvasWidth,t.canvasHeight);var Re=m;le.drawImage(ie,0,0,t.canvasWidth*Re,t.canvasHeight*Re,0,0,t.canvasWidth,t.canvasHeight)},"drawMotionBlur");(d[t.NODE]||G[t.NODE])&&(oe(H,j,G[t.NODE]),d[t.NODE]=!1),(d[t.DRAG]||G[t.DRAG])&&(oe(Q,U,G[t.DRAG]),d[t.DRAG]=!1)}t.prevViewport=S,t.clearingMotionBlur&&(t.clearingMotionBlur=!1,t.motionBlurCleared=!0,t.motionBlur=!0),p&&(t.motionBlurTimeout=setTimeout(function(){t.motionBlurTimeout=null,t.clearedForMotionBlur[t.NODE]=!1,t.clearedForMotionBlur[t.DRAG]=!1,t.motionBlur=!1,t.clearingMotionBlur=!f,t.mbFrames=0,d[t.NODE]=!0,d[t.DRAG]=!0,t.redraw()},igt)),n||r.emit("render")};Ps.drawSelectionRectangle=function(e,t){var r=this,n=r.cy,i=r.data,a=n.style(),s=e.drawOnlyNodeLayer,l=e.drawAllLayers,u=i.canvasNeedsRedraw,h=e.forcedContext;if(r.showFps||!s&&u[r.SELECT_BOX]&&!l){var d=h||i.contexts[r.SELECT_BOX];if(t(d),r.selection[4]==1&&(r.hoverData.selecting||r.touchData.selecting)){var f=r.cy.zoom(),p=a.core("selection-box-border-width").value/f;d.lineWidth=p,d.fillStyle="rgba("+a.core("selection-box-color").value[0]+","+a.core("selection-box-color").value[1]+","+a.core("selection-box-color").value[2]+","+a.core("selection-box-opacity").value+")",d.fillRect(r.selection[0],r.selection[1],r.selection[2]-r.selection[0],r.selection[3]-r.selection[1]),p>0&&(d.strokeStyle="rgba("+a.core("selection-box-border-color").value[0]+","+a.core("selection-box-border-color").value[1]+","+a.core("selection-box-border-color").value[2]+","+a.core("selection-box-opacity").value+")",d.strokeRect(r.selection[0],r.selection[1],r.selection[2]-r.selection[0],r.selection[3]-r.selection[1]))}if(i.bgActivePosistion&&!r.hoverData.selecting){var f=r.cy.zoom(),m=i.bgActivePosistion;d.fillStyle="rgba("+a.core("active-bg-color").value[0]+","+a.core("active-bg-color").value[1]+","+a.core("active-bg-color").value[2]+","+a.core("active-bg-opacity").value+")",d.beginPath(),d.arc(m.x,m.y,a.core("active-bg-size").pfValue/f,0,2*Math.PI),d.fill()}var g=r.lastRedrawTime;if(r.showFps&&g){g=Math.round(g);var y=Math.round(1e3/g),v="1 frame = "+g+" ms = "+y+" fps";if(d.setTransform(1,0,0,1,0,0),d.fillStyle="rgba(255, 0, 0, 0.75)",d.strokeStyle="rgba(255, 0, 0, 0.75)",d.font="30px Arial",!AT){var x=d.measureText(v);AT=x.actualBoundingBoxAscent}d.fillText(v,0,AT);var b=60;d.strokeRect(0,AT+10,250,20),d.fillRect(0,AT+10,250*Math.min(y/b,1),20)}l||(u[r.SELECT_BOX]=!1)}};o(Zxe,"compileShader");o(agt,"createProgram");o(sgt,"createTextureCanvas");o(VF,"getEffectivePanZoom");o(ogt,"getEffectiveZoom");o(lgt,"modelToRenderedPosition");o(cgt,"isSimpleShape");o(ugt,"arrayEqual");o(Km,"toWebGLColor");o(L1,"indexToVec4");o(hgt,"vec4ToIndex");o(dgt,"createTexture");o($2e,"getTypeInfo");o(F2e,"createTypedArray");o(fgt,"createTypedArrayView");o(pgt,"createBufferStaticDraw");o(xu,"createBufferDynamicDraw");o(mgt,"create3x3MatrixBufferDynamicDraw");o(ggt,"createPickingFrameBuffer");Qxe=typeof Float32Array<"u"?Float32Array:Array;Math.hypot||(Math.hypot=function(){for(var e=0,t=arguments.length;t--;)e+=arguments[t]*arguments[t];return Math.sqrt(e)});o(U$,"create");o(Jxe,"identity");o(ygt,"multiply");o(X5,"translate");o(ebe,"rotate");o(pF,"scale");o(vgt,"projection");xgt=(function(){function e(t,r,n,i){jf(this,e),this.debugID=Math.floor(Math.random()*1e4),this.r=t,this.texSize=r,this.texRows=n,this.texHeight=Math.floor(r/n),this.enableWrapping=!0,this.locked=!1,this.texture=null,this.needsBuffer=!0,this.freePointer={x:0,row:0},this.keyToLocation=new Map,this.canvas=i(t,r,r),this.scratch=i(t,r,this.texHeight,"scratch")}return o(e,"Atlas"),Xf(e,[{key:"lock",value:o(function(){this.locked=!0},"lock")},{key:"getKeys",value:o(function(){return new Set(this.keyToLocation.keys())},"getKeys")},{key:"getScale",value:o(function(r){var n=r.w,i=r.h,a=this.texHeight,s=this.texSize,l=a/i,u=n*l,h=i*l;return u>s&&(l=s/n,u=n*l,h=i*l),{scale:l,texW:u,texH:h}},"getScale")},{key:"draw",value:o(function(r,n,i){var a=this;if(this.locked)throw new Error("can't draw, atlas is locked");var s=this.texSize,l=this.texRows,u=this.texHeight,h=this.getScale(n),d=h.scale,f=h.texW,p=h.texH,m=o(function(T,k){if(i&&k){var C=k.context,w=T.x,S=T.row,R=w,L=u*S;C.save(),C.translate(R,L),C.scale(d,d),i(C,n),C.restore()}},"drawAt"),g=[null,null],y=o(function(){m(a.freePointer,a.canvas),g[0]={x:a.freePointer.x,y:a.freePointer.row*u,w:f,h:p},g[1]={x:a.freePointer.x+f,y:a.freePointer.row*u,w:0,h:p},a.freePointer.x+=f,a.freePointer.x==s&&(a.freePointer.x=0,a.freePointer.row++)},"drawNormal"),v=o(function(){var T=a.scratch,k=a.canvas;T.clear(),m({x:0,row:0},T);var C=s-a.freePointer.x,w=f-C,S=u;{var R=a.freePointer.x,L=a.freePointer.row*u,N=C;k.context.drawImage(T,0,0,N,S,R,L,N,S),g[0]={x:R,y:L,w:N,h:p}}{var I=C,_=(a.freePointer.row+1)*u,A=w;k&&k.context.drawImage(T,I,0,A,S,0,_,A,S),g[1]={x:0,y:_,w:A,h:p}}a.freePointer.x=w,a.freePointer.row++},"drawWrapped"),x=o(function(){a.freePointer.x=0,a.freePointer.row++},"moveToStartOfNextRow");if(this.freePointer.x+f<=s)y();else{if(this.freePointer.row>=l-1)return!1;this.freePointer.x===s?(x(),y()):this.enableWrapping?v():(x(),y())}return this.keyToLocation.set(r,g),this.needsBuffer=!0,g},"draw")},{key:"getOffsets",value:o(function(r){return this.keyToLocation.get(r)},"getOffsets")},{key:"isEmpty",value:o(function(){return this.freePointer.x===0&&this.freePointer.row===0},"isEmpty")},{key:"canFit",value:o(function(r){if(this.locked)return!1;var n=this.texSize,i=this.texRows,a=this.getScale(r),s=a.texW;return this.freePointer.x+s>n?this.freePointer.row1&&arguments[1]!==void 0?arguments[1]:{},a=i.forceRedraw,s=a===void 0?!1:a,l=i.filterEle,u=l===void 0?function(){return!0}:l,h=i.filterType,d=h===void 0?function(){return!0}:h,f=!1,p=!1,m=yo(r),g;try{for(m.s();!(g=m.n()).done;){var y=g.value;if(u(y)){var v=yo(this.renderTypes.values()),x;try{var b=o(function(){var k=x.value,C=k.type;if(d(C)){var w=n.collections.get(k.collection),S=k.getKey(y),R=Array.isArray(S)?S:[S];if(s)R.forEach(function(_){return w.markKeyForGC(_)}),p=!0;else{var L=k.getID?k.getID(y):y.id(),N=n._key(C,L),I=n.typeAndIdToKey.get(N);I!==void 0&&!ugt(R,I)&&(f=!0,n.typeAndIdToKey.delete(N),I.forEach(function(_){return w.markKeyForGC(_)}))}}},"_loop2");for(v.s();!(x=v.n()).done;)b()}catch(T){v.e(T)}finally{v.f()}}}}catch(T){m.e(T)}finally{m.f()}return p&&(this.gc(),f=!1),f},"invalidate")},{key:"gc",value:o(function(){var r=yo(this.collections.values()),n;try{for(r.s();!(n=r.n()).done;){var i=n.value;i.gc()}}catch(a){r.e(a)}finally{r.f()}},"gc")},{key:"getOrCreateAtlas",value:o(function(r,n,i,a){var s=this.renderTypes.get(n),l=this.collections.get(s.collection),u=!1,h=l.draw(a,i,function(p){s.drawClipped?(p.save(),p.beginPath(),p.rect(0,0,i.w,i.h),p.clip(),s.drawElement(p,r,i,!0,!0),p.restore()):s.drawElement(p,r,i,!0,!0),u=!0});if(u){var d=s.getID?s.getID(r):r.id(),f=this._key(n,d);this.typeAndIdToKey.has(f)?this.typeAndIdToKey.get(f).push(a):this.typeAndIdToKey.set(f,[a])}return h},"getOrCreateAtlas")},{key:"getAtlasInfo",value:o(function(r,n){var i=this,a=this.renderTypes.get(n),s=a.getKey(r),l=Array.isArray(s)?s:[s];return l.map(function(u){var h=a.getBoundingBox(r,u),d=i.getOrCreateAtlas(r,n,h,u),f=d.getOffsets(u),p=Ki(f,2),m=p[0],g=p[1];return{atlas:d,tex:m,tex1:m,tex2:g,bb:h}})},"getAtlasInfo")},{key:"getDebugInfo",value:o(function(){var r=[],n=yo(this.collections),i;try{for(n.s();!(i=n.n()).done;){var a=Ki(i.value,2),s=a[0],l=a[1],u=l.getCounts(),h=u.keyCount,d=u.atlasCount;r.push({type:s,keyCount:h,atlasCount:d})}}catch(f){n.e(f)}finally{n.f()}return r},"getDebugInfo")}])})(),wgt=(function(){function e(t){jf(this,e),this.globalOptions=t,this.atlasSize=t.webglTexSize,this.maxAtlasesPerBatch=t.webglTexPerBatch,this.batchAtlases=[]}return o(e,"AtlasBatchManager"),Xf(e,[{key:"getMaxAtlasesPerBatch",value:o(function(){return this.maxAtlasesPerBatch},"getMaxAtlasesPerBatch")},{key:"getAtlasSize",value:o(function(){return this.atlasSize},"getAtlasSize")},{key:"getIndexArray",value:o(function(){return Array.from({length:this.maxAtlasesPerBatch},function(r,n){return n})},"getIndexArray")},{key:"startBatch",value:o(function(){this.batchAtlases=[]},"startBatch")},{key:"getAtlasCount",value:o(function(){return this.batchAtlases.length},"getAtlasCount")},{key:"getAtlases",value:o(function(){return this.batchAtlases},"getAtlases")},{key:"canAddToCurrentBatch",value:o(function(r){return this.batchAtlases.length===this.maxAtlasesPerBatch?this.batchAtlases.includes(r):!0},"canAddToCurrentBatch")},{key:"getAtlasIndexForBatch",value:o(function(r){var n=this.batchAtlases.indexOf(r);if(n<0){if(this.batchAtlases.length===this.maxAtlasesPerBatch)throw new Error("cannot add more atlases to batch");this.batchAtlases.push(r),n=this.batchAtlases.length-1}return n},"getAtlasIndexForBatch")}])})(),kgt=` + float circleSD(vec2 p, float r) { + return distance(vec2(0), p) - r; // signed distance + } +`,Sgt=` + float rectangleSD(vec2 p, vec2 b) { + vec2 d = abs(p)-b; + return distance(vec2(0),max(d,0.0)) + min(max(d.x,d.y),0.0); + } +`,Egt=` + float roundRectangleSD(vec2 p, vec2 b, vec4 cr) { + cr.xy = (p.x > 0.0) ? cr.xy : cr.zw; + cr.x = (p.y > 0.0) ? cr.x : cr.y; + vec2 q = abs(p) - b + cr.x; + return min(max(q.x, q.y), 0.0) + distance(vec2(0), max(q, 0.0)) - cr.x; + } +`,Agt=` + float ellipseSD(vec2 p, vec2 ab) { + p = abs( p ); // symmetry + + // find root with Newton solver + vec2 q = ab*(p-ab); + float w = (q.x1.0) ? d : -d; + } +`,PT={SCREEN:{name:"screen",screen:!0},PICKING:{name:"picking",picking:!0}},lA={IGNORE:1,USE_BB:2},Y$=0,tbe=1,rbe=2,j$=3,D1=4,$5=5,RT=6,_T=7,Rgt=(function(){function e(t,r,n){jf(this,e),this.r=t,this.gl=r,this.maxInstances=n.webglBatchSize,this.atlasSize=n.webglTexSize,this.bgColor=n.bgColor,this.debug=n.webglDebug,this.batchDebugInfo=[],n.enableWrapping=!0,n.createTextureCanvas=sgt,this.atlasManager=new Cgt(t,n),this.batchManager=new wgt(n),this.simpleShapeOptions=new Map,this.program=this._createShaderProgram(PT.SCREEN),this.pickingProgram=this._createShaderProgram(PT.PICKING),this.vao=this._createVAO()}return o(e,"ElementDrawingWebGL"),Xf(e,[{key:"addAtlasCollection",value:o(function(r,n){this.atlasManager.addAtlasCollection(r,n)},"addAtlasCollection")},{key:"addTextureAtlasRenderType",value:o(function(r,n){this.atlasManager.addRenderType(r,n)},"addTextureAtlasRenderType")},{key:"addSimpleShapeRenderType",value:o(function(r,n){this.simpleShapeOptions.set(r,n)},"addSimpleShapeRenderType")},{key:"invalidate",value:o(function(r){var n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{},i=n.type,a=this.atlasManager;return i?a.invalidate(r,{filterType:o(function(l){return l===i},"filterType"),forceRedraw:!0}):a.invalidate(r)},"invalidate")},{key:"gc",value:o(function(){this.atlasManager.gc()},"gc")},{key:"_createShaderProgram",value:o(function(r){var n=this.gl,i=`#version 300 es + precision highp float; + + uniform mat3 uPanZoomMatrix; + uniform int uAtlasSize; + + // instanced + in vec2 aPosition; // a vertex from the unit square + + in mat3 aTransform; // used to transform verticies, eg into a bounding box + in int aVertType; // the type of thing we are rendering + + // the z-index that is output when using picking mode + in vec4 aIndex; + + // For textures + in int aAtlasId; // which shader unit/atlas to use + in vec4 aTex; // x/y/w/h of texture in atlas + + // for edges + in vec4 aPointAPointB; + in vec4 aPointCPointD; + in vec2 aLineWidth; // also used for node border width + + // simple shapes + in vec4 aCornerRadius; // for round-rectangle [top-right, bottom-right, top-left, bottom-left] + in vec4 aColor; // also used for edges + in vec4 aBorderColor; // aLineWidth is used for border width + + // output values passed to the fragment shader + out vec2 vTexCoord; + out vec4 vColor; + out vec2 vPosition; + // flat values are not interpolated + flat out int vAtlasId; + flat out int vVertType; + flat out vec2 vTopRight; + flat out vec2 vBotLeft; + flat out vec4 vCornerRadius; + flat out vec4 vBorderColor; + flat out vec2 vBorderWidth; + flat out vec4 vIndex; + + void main(void) { + int vid = gl_VertexID; + vec2 position = aPosition; // TODO make this a vec3, simplifies some code below + + if(aVertType == `.concat(Y$,`) { + float texX = aTex.x; // texture coordinates + float texY = aTex.y; + float texW = aTex.z; + float texH = aTex.w; + + if(vid == 1 || vid == 2 || vid == 4) { + texX += texW; + } + if(vid == 2 || vid == 4 || vid == 5) { + texY += texH; + } + + float d = float(uAtlasSize); + vTexCoord = vec2(texX / d, texY / d); // tex coords must be between 0 and 1 + + gl_Position = vec4(uPanZoomMatrix * aTransform * vec3(position, 1.0), 1.0); + } + else if(aVertType == `).concat(D1," || aVertType == ").concat(_T,` + || aVertType == `).concat($5," || aVertType == ").concat(RT,`) { // simple shapes + + // the bounding box is needed by the fragment shader + vBotLeft = (aTransform * vec3(0, 0, 1)).xy; // flat + vTopRight = (aTransform * vec3(1, 1, 1)).xy; // flat + vPosition = (aTransform * vec3(position, 1)).xy; // will be interpolated + + // calculations are done in the fragment shader, just pass these along + vColor = aColor; + vCornerRadius = aCornerRadius; + vBorderColor = aBorderColor; + vBorderWidth = aLineWidth; + + gl_Position = vec4(uPanZoomMatrix * aTransform * vec3(position, 1.0), 1.0); + } + else if(aVertType == `).concat(tbe,`) { + vec2 source = aPointAPointB.xy; + vec2 target = aPointAPointB.zw; + + // adjust the geometry so that the line is centered on the edge + position.y = position.y - 0.5; + + // stretch the unit square into a long skinny rectangle + vec2 xBasis = target - source; + vec2 yBasis = normalize(vec2(-xBasis.y, xBasis.x)); + vec2 point = source + xBasis * position.x + yBasis * aLineWidth[0] * position.y; + + gl_Position = vec4(uPanZoomMatrix * vec3(point, 1.0), 1.0); + vColor = aColor; + } + else if(aVertType == `).concat(rbe,`) { + vec2 pointA = aPointAPointB.xy; + vec2 pointB = aPointAPointB.zw; + vec2 pointC = aPointCPointD.xy; + vec2 pointD = aPointCPointD.zw; + + // adjust the geometry so that the line is centered on the edge + position.y = position.y - 0.5; + + vec2 p0, p1, p2, pos; + if(position.x == 0.0) { // The left side of the unit square + p0 = pointA; + p1 = pointB; + p2 = pointC; + pos = position; + } else { // The right side of the unit square, use same approach but flip the geometry upside down + p0 = pointD; + p1 = pointC; + p2 = pointB; + pos = vec2(0.0, -position.y); + } + + vec2 p01 = p1 - p0; + vec2 p12 = p2 - p1; + vec2 p21 = p1 - p2; + + // Find the normal vector. + vec2 tangent = normalize(normalize(p12) + normalize(p01)); + vec2 normal = vec2(-tangent.y, tangent.x); + + // Find the vector perpendicular to p0 -> p1. + vec2 p01Norm = normalize(vec2(-p01.y, p01.x)); + + // Determine the bend direction. + float sigma = sign(dot(p01 + p21, normal)); + float width = aLineWidth[0]; + + if(sign(pos.y) == -sigma) { + // This is an intersecting vertex. Adjust the position so that there's no overlap. + vec2 point = 0.5 * width * normal * -sigma / dot(normal, p01Norm); + gl_Position = vec4(uPanZoomMatrix * vec3(p1 + point, 1.0), 1.0); + } else { + // This is a non-intersecting vertex. Treat it like a mitre join. + vec2 point = 0.5 * width * normal * sigma * dot(normal, p01Norm); + gl_Position = vec4(uPanZoomMatrix * vec3(p1 + point, 1.0), 1.0); + } + + vColor = aColor; + } + else if(aVertType == `).concat(j$,` && vid < 3) { + // massage the first triangle into an edge arrow + if(vid == 0) + position = vec2(-0.15, -0.3); + if(vid == 1) + position = vec2( 0.0, 0.0); + if(vid == 2) + position = vec2( 0.15, -0.3); + + gl_Position = vec4(uPanZoomMatrix * aTransform * vec3(position, 1.0), 1.0); + vColor = aColor; + } + else { + gl_Position = vec4(2.0, 0.0, 0.0, 1.0); // discard vertex by putting it outside webgl clip space + } + + vAtlasId = aAtlasId; + vVertType = aVertType; + vIndex = aIndex; + } + `),a=this.batchManager.getIndexArray(),s=`#version 300 es + precision highp float; + + // declare texture unit for each texture atlas in the batch + `.concat(a.map(function(h){return"uniform sampler2D uTexture".concat(h,";")}).join(` + `),` + + uniform vec4 uBGColor; + uniform float uZoom; + + in vec2 vTexCoord; + in vec4 vColor; + in vec2 vPosition; // model coordinates + + flat in int vAtlasId; + flat in vec4 vIndex; + flat in int vVertType; + flat in vec2 vTopRight; + flat in vec2 vBotLeft; + flat in vec4 vCornerRadius; + flat in vec4 vBorderColor; + flat in vec2 vBorderWidth; + + out vec4 outColor; + + `).concat(kgt,` + `).concat(Sgt,` + `).concat(Egt,` + `).concat(Agt,` + + vec4 blend(vec4 top, vec4 bot) { // blend colors with premultiplied alpha + return vec4( + top.rgb + (bot.rgb * (1.0 - top.a)), + top.a + (bot.a * (1.0 - top.a)) + ); + } + + vec4 distInterp(vec4 cA, vec4 cB, float d) { // interpolate color using Signed Distance + // scale to the zoom level so that borders don't look blurry when zoomed in + // note 1.5 is an aribitrary value chosen because it looks good + return mix(cA, cB, 1.0 - smoothstep(0.0, 1.5 / uZoom, abs(d))); + } + + void main(void) { + if(vVertType == `).concat(Y$,`) { + // look up the texel from the texture unit + `).concat(a.map(function(h){return"if(vAtlasId == ".concat(h,") outColor = texture(uTexture").concat(h,", vTexCoord);")}).join(` + else `),` + } + else if(vVertType == `).concat(j$,`) { + // mimics how canvas renderer uses context.globalCompositeOperation = 'destination-out'; + outColor = blend(vColor, uBGColor); + outColor.a = 1.0; // make opaque, masks out line under arrow + } + else if(vVertType == `).concat(D1,` && vBorderWidth == vec2(0.0)) { // simple rectangle with no border + outColor = vColor; // unit square is already transformed to the rectangle, nothing else needs to be done + } + else if(vVertType == `).concat(D1," || vVertType == ").concat(_T,` + || vVertType == `).concat($5," || vVertType == ").concat(RT,`) { // use SDF + + float outerBorder = vBorderWidth[0]; + float innerBorder = vBorderWidth[1]; + float borderPadding = outerBorder * 2.0; + float w = vTopRight.x - vBotLeft.x - borderPadding; + float h = vTopRight.y - vBotLeft.y - borderPadding; + vec2 b = vec2(w/2.0, h/2.0); // half width, half height + vec2 p = vPosition - vec2(vTopRight.x - b[0] - outerBorder, vTopRight.y - b[1] - outerBorder); // translate to center + + float d; // signed distance + if(vVertType == `).concat(D1,`) { + d = rectangleSD(p, b); + } else if(vVertType == `).concat(_T,` && w == h) { + d = circleSD(p, b.x); // faster than ellipse + } else if(vVertType == `).concat(_T,`) { + d = ellipseSD(p, b); + } else { + d = roundRectangleSD(p, b, vCornerRadius.wzyx); + } + + // use the distance to interpolate a color to smooth the edges of the shape, doesn't need multisampling + // we must smooth colors inwards, because we can't change pixels outside the shape's bounding box + if(d > 0.0) { + if(d > outerBorder) { + discard; + } else { + outColor = distInterp(vBorderColor, vec4(0), d - outerBorder); + } + } else { + if(d > innerBorder) { + vec4 outerColor = outerBorder == 0.0 ? vec4(0) : vBorderColor; + vec4 innerBorderColor = blend(vBorderColor, vColor); + outColor = distInterp(innerBorderColor, outerColor, d); + } + else { + vec4 outerColor; + if(innerBorder == 0.0 && outerBorder == 0.0) { + outerColor = vec4(0); + } else if(innerBorder == 0.0) { + outerColor = vBorderColor; + } else { + outerColor = blend(vBorderColor, vColor); + } + outColor = distInterp(vColor, outerColor, d - innerBorder); + } + } + } + else { + outColor = vColor; + } + + `).concat(r.picking?`if(outColor.a == 0.0) discard; + else outColor = vIndex;`:"",` + } + `),l=agt(n,i,s);l.aPosition=n.getAttribLocation(l,"aPosition"),l.aIndex=n.getAttribLocation(l,"aIndex"),l.aVertType=n.getAttribLocation(l,"aVertType"),l.aTransform=n.getAttribLocation(l,"aTransform"),l.aAtlasId=n.getAttribLocation(l,"aAtlasId"),l.aTex=n.getAttribLocation(l,"aTex"),l.aPointAPointB=n.getAttribLocation(l,"aPointAPointB"),l.aPointCPointD=n.getAttribLocation(l,"aPointCPointD"),l.aLineWidth=n.getAttribLocation(l,"aLineWidth"),l.aColor=n.getAttribLocation(l,"aColor"),l.aCornerRadius=n.getAttribLocation(l,"aCornerRadius"),l.aBorderColor=n.getAttribLocation(l,"aBorderColor"),l.uPanZoomMatrix=n.getUniformLocation(l,"uPanZoomMatrix"),l.uAtlasSize=n.getUniformLocation(l,"uAtlasSize"),l.uBGColor=n.getUniformLocation(l,"uBGColor"),l.uZoom=n.getUniformLocation(l,"uZoom"),l.uTextures=[];for(var u=0;u1&&arguments[1]!==void 0?arguments[1]:PT.SCREEN;this.panZoomMatrix=r,this.renderTarget=n,this.batchDebugInfo=[],this.wrappedCount=0,this.simpleCount=0,this.startBatch()},"startFrame")},{key:"startBatch",value:o(function(){this.instanceCount=0,this.batchManager.startBatch()},"startBatch")},{key:"endFrame",value:o(function(){this.endBatch()},"endFrame")},{key:"_isVisible",value:o(function(r,n){return r.visible()?n&&n.isVisible?n.isVisible(r):!0:!1},"_isVisible")},{key:"drawTexture",value:o(function(r,n,i){var a=this.atlasManager,s=this.batchManager,l=a.getRenderTypeOpts(i);if(this._isVisible(r,l)&&!(r.isEdge()&&!this._isValidEdge(r))){if(this.renderTarget.picking&&l.getTexPickingMode){var u=l.getTexPickingMode(r);if(u===lA.IGNORE)return;if(u==lA.USE_BB){this.drawPickingRectangle(r,n,i);return}}var h=a.getAtlasInfo(r,i),d=yo(h),f;try{for(d.s();!(f=d.n()).done;){var p=f.value,m=p.atlas,g=p.tex1,y=p.tex2;s.canAddToCurrentBatch(m)||this.endBatch();for(var v=s.getAtlasIndexForBatch(m),x=0,b=[[g,!0],[y,!1]];x=this.maxInstances&&this.endBatch()}}}}catch(I){d.e(I)}finally{d.f()}}},"drawTexture")},{key:"setTransformMatrix",value:o(function(r,n,i,a){var s=arguments.length>4&&arguments[4]!==void 0?arguments[4]:!0,l=0;if(i.shapeProps&&i.shapeProps.padding&&(l=r.pstyle(i.shapeProps.padding).pfValue),a){var u=a.bb,h=a.tex1,d=a.tex2,f=h.w/(h.w+d.w);s||(f=1-f);var p=this._getAdjustedBB(u,l,s,f);this._applyTransformMatrix(n,p,i,r)}else{var m=i.getBoundingBox(r),g=this._getAdjustedBB(m,l,!0,1);this._applyTransformMatrix(n,g,i,r)}},"setTransformMatrix")},{key:"_applyTransformMatrix",value:o(function(r,n,i,a){var s,l;Jxe(r);var u=i.getRotation?i.getRotation(a):0;if(u!==0){var h=i.getRotationPoint(a),d=h.x,f=h.y;X5(r,r,[d,f]),ebe(r,r,u);var p=i.getRotationOffset(a);s=p.x+(n.xOffset||0),l=p.y+(n.yOffset||0)}else s=n.x1,l=n.y1;X5(r,r,[s,l]),pF(r,r,[n.w,n.h])},"_applyTransformMatrix")},{key:"_getAdjustedBB",value:o(function(r,n,i,a){var s=r.x1,l=r.y1,u=r.w,h=r.h,d=r.yOffset;n&&(s-=n,l-=n,u+=2*n,h+=2*n);var f=0,p=u*a;return i&&a<1?u=p:!i&&a<1&&(f=u-p,s+=f,u=p),{x1:s,y1:l,w:u,h,xOffset:f,yOffset:d}},"_getAdjustedBB")},{key:"drawPickingRectangle",value:o(function(r,n,i){var a=this.atlasManager.getRenderTypeOpts(i),s=this.instanceCount;this.vertTypeBuffer.getView(s)[0]=D1;var l=this.indexBuffer.getView(s);L1(n,l);var u=this.colorBuffer.getView(s);Km([0,0,0],1,u);var h=this.transformBuffer.getMatrixView(s);this.setTransformMatrix(r,h,a),this.simpleCount++,this.instanceCount++,this.instanceCount>=this.maxInstances&&this.endBatch()},"drawPickingRectangle")},{key:"drawNode",value:o(function(r,n,i){var a=this.simpleShapeOptions.get(i);if(this._isVisible(r,a)){var s=a.shapeProps,l=this._getVertTypeForShape(r,s.shape);if(l===void 0||a.isSimple&&!a.isSimple(r,this.renderTarget)){this.drawTexture(r,n,i);return}var u=this.instanceCount;if(this.vertTypeBuffer.getView(u)[0]=l,l===$5||l===RT){var h=a.getBoundingBox(r),d=this._getCornerRadius(r,s.radius,h),f=this.cornerRadiusBuffer.getView(u);f[0]=d,f[1]=d,f[2]=d,f[3]=d,l===RT&&(f[0]=0,f[2]=0)}var p=this.indexBuffer.getView(u);L1(n,p);var m=this.renderTarget.picking?1:i==="node-body"?r.effectiveOpacity():1,g=this.renderTarget.picking?1:r.pstyle(s.opacity).value*m,y=r.pstyle(s.color).value,v=this.colorBuffer.getView(u);Km(y,g,v);var x=this.lineWidthBuffer.getView(u);if(x[0]=0,x[1]=0,s.border){var b=r.pstyle("border-width").value;if(b>0){var T=r.pstyle("border-color").value,k=m*r.pstyle("border-opacity").value,C=this.borderColorBuffer.getView(u);Km(T,k,C);var w=r.pstyle("border-position").value;if(w==="inside")x[0]=0,x[1]=-b;else if(w==="outside")x[0]=b,x[1]=0;else{var S=b/2;x[0]=S,x[1]=-S}}}var R=this.transformBuffer.getMatrixView(u);this.setTransformMatrix(r,R,a),this.simpleCount++,this.instanceCount++,this.instanceCount>=this.maxInstances&&this.endBatch()}},"drawNode")},{key:"_getVertTypeForShape",value:o(function(r,n){var i=r.pstyle(n).value;switch(i){case"rectangle":return D1;case"ellipse":return _T;case"roundrectangle":case"round-rectangle":return $5;case"bottom-round-rectangle":return RT;default:return}},"_getVertTypeForShape")},{key:"_getCornerRadius",value:o(function(r,n,i){var a=i.w,s=i.h;if(r.pstyle(n).value==="auto")return qf(a,s);var l=r.pstyle(n).pfValue,u=a/2,h=s/2;return Math.min(l,h,u)},"_getCornerRadius")},{key:"drawEdgeArrow",value:o(function(r,n,i){if(r.visible()){var a=r._private.rscratch,s,l,u;if(i==="source"?(s=a.arrowStartX,l=a.arrowStartY,u=a.srcArrowAngle):(s=a.arrowEndX,l=a.arrowEndY,u=a.tgtArrowAngle),!(isNaN(s)||s==null||isNaN(l)||l==null||isNaN(u)||u==null)){var h=r.pstyle(i+"-arrow-shape").value;if(h!=="none"){var d=r.pstyle(i+"-arrow-color").value,f=r.pstyle("opacity").value,p=r.pstyle("line-opacity").value,m=f*p,g=r.pstyle("width").pfValue,y=r.pstyle("arrow-scale").value,v=this.r.getArrowWidth(g,y),x=this.instanceCount,b=this.transformBuffer.getMatrixView(x);Jxe(b),X5(b,b,[s,l]),pF(b,b,[v,v]),ebe(b,b,u),this.vertTypeBuffer.getView(x)[0]=j$;var T=this.indexBuffer.getView(x);L1(n,T);var k=this.colorBuffer.getView(x);Km(d,m,k),this.instanceCount++,this.instanceCount>=this.maxInstances&&this.endBatch()}}}},"drawEdgeArrow")},{key:"drawEdgeLine",value:o(function(r,n){if(r.visible()){var i=this._getEdgePoints(r);if(i){var a=r.pstyle("opacity").value,s=r.pstyle("line-opacity").value,l=r.pstyle("width").pfValue,u=r.pstyle("line-color").value,h=a*s;if(i.length/2+this.instanceCount>this.maxInstances&&this.endBatch(),i.length==4){var d=this.instanceCount;this.vertTypeBuffer.getView(d)[0]=tbe;var f=this.indexBuffer.getView(d);L1(n,f);var p=this.colorBuffer.getView(d);Km(u,h,p);var m=this.lineWidthBuffer.getView(d);m[0]=l;var g=this.pointAPointBBuffer.getView(d);g[0]=i[0],g[1]=i[1],g[2]=i[2],g[3]=i[3],this.instanceCount++,this.instanceCount>=this.maxInstances&&this.endBatch()}else for(var y=0;y=this.maxInstances&&this.endBatch()}}}},"drawEdgeLine")},{key:"_isValidEdge",value:o(function(r){var n=r._private.rscratch;return!(n.badLine||n.allpts==null||isNaN(n.allpts[0]))},"_isValidEdge")},{key:"_getEdgePoints",value:o(function(r){var n=r._private.rscratch;if(this._isValidEdge(r)){var i=n.allpts;if(i.length==4)return i;var a=this._getNumSegments(r);return this._getCurveSegmentPoints(i,a)}},"_getEdgePoints")},{key:"_getNumSegments",value:o(function(r){var n=15;return Math.min(Math.max(n,5),this.maxInstances)},"_getNumSegments")},{key:"_getCurveSegmentPoints",value:o(function(r,n){if(r.length==4)return r;for(var i=Array((n+1)*2),a=0;a<=n;a++)if(a==0)i[0]=r[0],i[1]=r[1];else if(a==n)i[a*2]=r[r.length-2],i[a*2+1]=r[r.length-1];else{var s=a/n;this._setCurvePoint(r,s,i,a*2)}return i},"_getCurveSegmentPoints")},{key:"_setCurvePoint",value:o(function(r,n,i,a){if(r.length<=2)i[a]=r[0],i[a+1]=r[1];else{for(var s=Array(r.length-2),l=0;l0}},"isLayerVisible"),l=o(function(f){var p=f.pstyle("text-events").strValue==="yes";return p?lA.USE_BB:lA.IGNORE},"getTexPickingMode"),u=o(function(f){var p=f.position(),m=p.x,g=p.y,y=f.outerWidth(),v=f.outerHeight();return{w:y,h:v,x1:m-y/2,y1:g-v/2}},"getBBForSimpleShape");r.drawing.addAtlasCollection("node",{texRows:e.webglTexRowsNodes}),r.drawing.addAtlasCollection("label",{texRows:e.webglTexRows}),r.drawing.addTextureAtlasRenderType("node-body",{collection:"node",getKey:t.getStyleKey,getBoundingBox:t.getElementBox,drawElement:t.drawElement}),r.drawing.addSimpleShapeRenderType("node-body",{getBoundingBox:u,isSimple:cgt,shapeProps:{shape:"shape",color:"background-color",opacity:"background-opacity",radius:"corner-radius",border:!0}}),r.drawing.addSimpleShapeRenderType("node-overlay",{getBoundingBox:u,isVisible:s("overlay"),shapeProps:{shape:"overlay-shape",color:"overlay-color",opacity:"overlay-opacity",padding:"overlay-padding",radius:"overlay-corner-radius"}}),r.drawing.addSimpleShapeRenderType("node-underlay",{getBoundingBox:u,isVisible:s("underlay"),shapeProps:{shape:"underlay-shape",color:"underlay-color",opacity:"underlay-opacity",padding:"underlay-padding",radius:"underlay-corner-radius"}}),r.drawing.addTextureAtlasRenderType("label",{collection:"label",getTexPickingMode:l,getKey:X$(t.getLabelKey,null),getBoundingBox:K$(t.getLabelBox,null),drawClipped:!0,drawElement:t.drawLabel,getRotation:i(null),getRotationPoint:t.getLabelRotationPoint,getRotationOffset:t.getLabelRotationOffset,isVisible:a("label")}),r.drawing.addTextureAtlasRenderType("edge-source-label",{collection:"label",getTexPickingMode:l,getKey:X$(t.getSourceLabelKey,"source"),getBoundingBox:K$(t.getSourceLabelBox,"source"),drawClipped:!0,drawElement:t.drawSourceLabel,getRotation:i("source"),getRotationPoint:t.getSourceLabelRotationPoint,getRotationOffset:t.getSourceLabelRotationOffset,isVisible:a("source-label")}),r.drawing.addTextureAtlasRenderType("edge-target-label",{collection:"label",getTexPickingMode:l,getKey:X$(t.getTargetLabelKey,"target"),getBoundingBox:K$(t.getTargetLabelBox,"target"),drawClipped:!0,drawElement:t.drawTargetLabel,getRotation:i("target"),getRotationPoint:t.getTargetLabelRotationPoint,getRotationOffset:t.getTargetLabelRotationOffset,isVisible:a("target-label")});var h=ZT(function(){console.log("garbage collect flag set"),r.data.gc=!0},1e4);r.onUpdateEleCalcs(function(d,f){var p=!1;f&&f.length>0&&(p|=r.drawing.invalidate(f)),p&&h()}),Lgt(r)};o(_gt,"getBGColor");o(G2e,"getLabelLines");X$=o(function(t,r){return function(n){var i=t(n),a=G2e(n,r);return a.length>1?a.map(function(s,l){return"".concat(i,"_").concat(l)}):i}},"getStyleKeysForLabel"),K$=o(function(t,r){return function(n,i){var a=t(n);if(typeof i=="string"){var s=i.indexOf("_");if(s>0){var l=Number(i.substring(s+1)),u=G2e(n,r),h=a.h/u.length,d=h*l,f=a.y1+d;return{x1:a.x1,w:a.w,y1:f,h,yOffset:d}}}return a}},"getBoundingBoxForLabel");o(Lgt,"overrideCanvasRendererFunctions");o(Dgt,"clearWebgl");o(Igt,"clearCanvas");o(Mgt,"createPanZoomMatrix");o(V2e,"setContextTransform");o(Ngt,"drawSelectionRectangle");o(Pgt,"drawAxes");o(Ogt,"drawAtlases");o(Bgt,"getPickingIndexes");o($gt,"findNearestElementsWebgl");o(Z$,"drawEle");o(W2e,"renderWebgl");Qf={};Qf.drawPolygonPath=function(e,t,r,n,i,a){var s=n/2,l=i/2;e.beginPath&&e.beginPath(),e.moveTo(t+s*a[0],r+l*a[1]);for(var u=1;u0&&s>0){m.clearRect(0,0,a,s),m.globalCompositeOperation="source-over";var g=this.getCachedZSortedEles();if(e.full)m.translate(-n.x1*h,-n.y1*h),m.scale(h,h),this.drawElements(m,g),m.scale(1/h,1/h),m.translate(n.x1*h,n.y1*h);else{var y=t.pan(),v={x:y.x*h,y:y.y*h};h*=t.zoom(),m.translate(v.x,v.y),m.scale(h,h),this.drawElements(m,g),m.scale(1/h,1/h),m.translate(-v.x,-v.y)}e.bg&&(m.globalCompositeOperation="destination-over",m.fillStyle=e.bg,m.rect(0,0,a,s),m.fill())}return p};o(Fgt,"b64ToBlob");o(abe,"b64UriToB64");o(H2e,"output");nC.png=function(e){return H2e(e,this.bufferCanvasImage(e),"image/png")};nC.jpg=function(e){return H2e(e,this.bufferCanvasImage(e),"image/jpeg")};U2e={};U2e.nodeShapeImpl=function(e,t,r,n,i,a,s,l){switch(e){case"ellipse":return this.drawEllipsePath(t,r,n,i,a);case"polygon":return this.drawPolygonPath(t,r,n,i,a,s);case"round-polygon":return this.drawRoundPolygonPath(t,r,n,i,a,s,l);case"roundrectangle":case"round-rectangle":return this.drawRoundRectanglePath(t,r,n,i,a,l);case"cutrectangle":case"cut-rectangle":return this.drawCutRectanglePath(t,r,n,i,a,s,l);case"bottomroundrectangle":case"bottom-round-rectangle":return this.drawBottomRoundRectanglePath(t,r,n,i,a,l);case"barrel":return this.drawBarrelPath(t,r,n,i,a)}};zgt=Y2e,Kr=Y2e.prototype;Kr.CANVAS_LAYERS=3;Kr.SELECT_BOX=0;Kr.DRAG=1;Kr.NODE=2;Kr.WEBGL=3;Kr.CANVAS_TYPES=["2d","2d","2d","webgl2"];Kr.BUFFER_COUNT=3;Kr.TEXTURE_BUFFER=0;Kr.MOTIONBLUR_BUFFER_NODE=1;Kr.MOTIONBLUR_BUFFER_DRAG=2;o(Y2e,"CanvasRenderer");Kr.redrawHint=function(e,t){var r=this;switch(e){case"eles":r.data.canvasNeedsRedraw[Kr.NODE]=t;break;case"drag":r.data.canvasNeedsRedraw[Kr.DRAG]=t;break;case"select":r.data.canvasNeedsRedraw[Kr.SELECT_BOX]=t;break;case"gc":r.data.gc=!0;break}};Ggt=typeof Path2D<"u";Kr.path2dEnabled=function(e){if(e===void 0)return this.pathsEnabled;this.pathsEnabled=!!e};Kr.usePaths=function(){return Ggt&&this.pathsEnabled};Kr.setImgSmoothing=function(e,t){e.imageSmoothingEnabled!=null?e.imageSmoothingEnabled=t:(e.webkitImageSmoothingEnabled=t,e.mozImageSmoothingEnabled=t,e.msImageSmoothingEnabled=t)};Kr.getImgSmoothing=function(e){return e.imageSmoothingEnabled!=null?e.imageSmoothingEnabled:e.webkitImageSmoothingEnabled||e.mozImageSmoothingEnabled||e.msImageSmoothingEnabled};Kr.makeOffscreenCanvas=function(e,t){var r;if((typeof OffscreenCanvas>"u"?"undefined":ca(OffscreenCanvas))!=="undefined")r=new OffscreenCanvas(e,t);else{var n=this.cy.window(),i=n.document;r=i.createElement("canvas"),r.width=e,r.height=t}return r};[P2e,ku,Fh,GF,cg,Zf,Ps,z2e,Qf,nC,U2e].forEach(function(e){br(Kr,e)});Vgt=[{name:"null",impl:b2e},{name:"base",impl:D2e},{name:"canvas",impl:zgt}],Wgt=[{type:"layout",extensions:dmt},{type:"renderer",extensions:Vgt}],j2e={},X2e={};o(K2e,"setExtension");o(Z2e,"getExtension");o(qgt,"setModule");o(Hgt,"getModule");yF=o(function(){if(arguments.length===2)return Z2e.apply(null,arguments);if(arguments.length===3)return K2e.apply(null,arguments);if(arguments.length===4)return Hgt.apply(null,arguments);if(arguments.length===5)return qgt.apply(null,arguments);pi("Invalid extension access syntax")},"extension");qT.prototype.extension=yF;Wgt.forEach(function(e){e.extensions.forEach(function(t){K2e(e.type,t.name,t.impl)})});cA=o(function(){if(!(this instanceof cA))return new cA;this.length=0},"Stylesheet"),og=cA.prototype;og.instanceString=function(){return"stylesheet"};og.selector=function(e){var t=this.length++;return this[t]={selector:e,properties:[]},this};og.css=function(e,t){var r=this.length-1;if(fr(e))this[r].properties.push({name:e,value:t});else if(cn(e))for(var n=e,i=Object.keys(n),a=0;a{"use strict";o((function(t,r){typeof iC=="object"&&typeof qF=="object"?qF.exports=r():typeof define=="function"&&define.amd?define([],r):typeof iC=="object"?iC.layoutBase=r():t.layoutBase=r()}),"webpackUniversalModuleDefinition")(iC,function(){return(function(e){var t={};function r(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return o(r,"__webpack_require__"),r.m=e,r.c=t,r.i=function(n){return n},r.d=function(n,i,a){r.o(n,i)||Object.defineProperty(n,i,{configurable:!1,enumerable:!0,get:a})},r.n=function(n){var i=n&&n.__esModule?o(function(){return n.default},"getDefault"):o(function(){return n},"getModuleExports");return r.d(i,"a",i),i},r.o=function(n,i){return Object.prototype.hasOwnProperty.call(n,i)},r.p="",r(r.s=26)})([(function(e,t,r){"use strict";function n(){}o(n,"LayoutConstants"),n.QUALITY=1,n.DEFAULT_CREATE_BENDS_AS_NEEDED=!1,n.DEFAULT_INCREMENTAL=!1,n.DEFAULT_ANIMATION_ON_LAYOUT=!0,n.DEFAULT_ANIMATION_DURING_LAYOUT=!1,n.DEFAULT_ANIMATION_PERIOD=50,n.DEFAULT_UNIFORM_LEAF_NODE_SIZES=!1,n.DEFAULT_GRAPH_MARGIN=15,n.NODE_DIMENSIONS_INCLUDE_LABELS=!1,n.SIMPLE_NODE_SIZE=40,n.SIMPLE_NODE_HALF_SIZE=n.SIMPLE_NODE_SIZE/2,n.EMPTY_COMPOUND_NODE_SIZE=40,n.MIN_EDGE_LENGTH=1,n.WORLD_BOUNDARY=1e6,n.INITIAL_WORLD_BOUNDARY=n.WORLD_BOUNDARY/1e3,n.WORLD_CENTER_X=1200,n.WORLD_CENTER_Y=900,e.exports=n}),(function(e,t,r){"use strict";var n=r(2),i=r(8),a=r(9);function s(u,h,d){n.call(this,d),this.isOverlapingSourceAndTarget=!1,this.vGraphObject=d,this.bendpoints=[],this.source=u,this.target=h}o(s,"LEdge"),s.prototype=Object.create(n.prototype);for(var l in n)s[l]=n[l];s.prototype.getSource=function(){return this.source},s.prototype.getTarget=function(){return this.target},s.prototype.isInterGraph=function(){return this.isInterGraph},s.prototype.getLength=function(){return this.length},s.prototype.isOverlapingSourceAndTarget=function(){return this.isOverlapingSourceAndTarget},s.prototype.getBendpoints=function(){return this.bendpoints},s.prototype.getLca=function(){return this.lca},s.prototype.getSourceInLca=function(){return this.sourceInLca},s.prototype.getTargetInLca=function(){return this.targetInLca},s.prototype.getOtherEnd=function(u){if(this.source===u)return this.target;if(this.target===u)return this.source;throw"Node is not incident with this edge"},s.prototype.getOtherEndInGraph=function(u,h){for(var d=this.getOtherEnd(u),f=h.getGraphManager().getRoot();;){if(d.getOwner()==h)return d;if(d.getOwner()==f)break;d=d.getOwner().getParent()}return null},s.prototype.updateLength=function(){var u=new Array(4);this.isOverlapingSourceAndTarget=i.getIntersection(this.target.getRect(),this.source.getRect(),u),this.isOverlapingSourceAndTarget||(this.lengthX=u[0]-u[2],this.lengthY=u[1]-u[3],Math.abs(this.lengthX)<1&&(this.lengthX=a.sign(this.lengthX)),Math.abs(this.lengthY)<1&&(this.lengthY=a.sign(this.lengthY)),this.length=Math.sqrt(this.lengthX*this.lengthX+this.lengthY*this.lengthY))},s.prototype.updateLengthSimple=function(){this.lengthX=this.target.getCenterX()-this.source.getCenterX(),this.lengthY=this.target.getCenterY()-this.source.getCenterY(),Math.abs(this.lengthX)<1&&(this.lengthX=a.sign(this.lengthX)),Math.abs(this.lengthY)<1&&(this.lengthY=a.sign(this.lengthY)),this.length=Math.sqrt(this.lengthX*this.lengthX+this.lengthY*this.lengthY)},e.exports=s}),(function(e,t,r){"use strict";function n(i){this.vGraphObject=i}o(n,"LGraphObject"),e.exports=n}),(function(e,t,r){"use strict";var n=r(2),i=r(10),a=r(13),s=r(0),l=r(16),u=r(4);function h(f,p,m,g){m==null&&g==null&&(g=p),n.call(this,g),f.graphManager!=null&&(f=f.graphManager),this.estimatedSize=i.MIN_VALUE,this.inclusionTreeDepth=i.MAX_VALUE,this.vGraphObject=g,this.edges=[],this.graphManager=f,m!=null&&p!=null?this.rect=new a(p.x,p.y,m.width,m.height):this.rect=new a}o(h,"LNode"),h.prototype=Object.create(n.prototype);for(var d in n)h[d]=n[d];h.prototype.getEdges=function(){return this.edges},h.prototype.getChild=function(){return this.child},h.prototype.getOwner=function(){return this.owner},h.prototype.getWidth=function(){return this.rect.width},h.prototype.setWidth=function(f){this.rect.width=f},h.prototype.getHeight=function(){return this.rect.height},h.prototype.setHeight=function(f){this.rect.height=f},h.prototype.getCenterX=function(){return this.rect.x+this.rect.width/2},h.prototype.getCenterY=function(){return this.rect.y+this.rect.height/2},h.prototype.getCenter=function(){return new u(this.rect.x+this.rect.width/2,this.rect.y+this.rect.height/2)},h.prototype.getLocation=function(){return new u(this.rect.x,this.rect.y)},h.prototype.getRect=function(){return this.rect},h.prototype.getDiagonal=function(){return Math.sqrt(this.rect.width*this.rect.width+this.rect.height*this.rect.height)},h.prototype.getHalfTheDiagonal=function(){return Math.sqrt(this.rect.height*this.rect.height+this.rect.width*this.rect.width)/2},h.prototype.setRect=function(f,p){this.rect.x=f.x,this.rect.y=f.y,this.rect.width=p.width,this.rect.height=p.height},h.prototype.setCenter=function(f,p){this.rect.x=f-this.rect.width/2,this.rect.y=p-this.rect.height/2},h.prototype.setLocation=function(f,p){this.rect.x=f,this.rect.y=p},h.prototype.moveBy=function(f,p){this.rect.x+=f,this.rect.y+=p},h.prototype.getEdgeListToNode=function(f){var p=[],m,g=this;return g.edges.forEach(function(y){if(y.target==f){if(y.source!=g)throw"Incorrect edge source!";p.push(y)}}),p},h.prototype.getEdgesBetween=function(f){var p=[],m,g=this;return g.edges.forEach(function(y){if(!(y.source==g||y.target==g))throw"Incorrect edge source and/or target";(y.target==f||y.source==f)&&p.push(y)}),p},h.prototype.getNeighborsList=function(){var f=new Set,p=this;return p.edges.forEach(function(m){if(m.source==p)f.add(m.target);else{if(m.target!=p)throw"Incorrect incidency!";f.add(m.source)}}),f},h.prototype.withChildren=function(){var f=new Set,p,m;if(f.add(this),this.child!=null)for(var g=this.child.getNodes(),y=0;yp&&(this.rect.x-=(this.labelWidth-p)/2,this.setWidth(this.labelWidth)),this.labelHeight>m&&(this.labelPos=="center"?this.rect.y-=(this.labelHeight-m)/2:this.labelPos=="top"&&(this.rect.y-=this.labelHeight-m),this.setHeight(this.labelHeight))}}},h.prototype.getInclusionTreeDepth=function(){if(this.inclusionTreeDepth==i.MAX_VALUE)throw"assert failed";return this.inclusionTreeDepth},h.prototype.transform=function(f){var p=this.rect.x;p>s.WORLD_BOUNDARY?p=s.WORLD_BOUNDARY:p<-s.WORLD_BOUNDARY&&(p=-s.WORLD_BOUNDARY);var m=this.rect.y;m>s.WORLD_BOUNDARY?m=s.WORLD_BOUNDARY:m<-s.WORLD_BOUNDARY&&(m=-s.WORLD_BOUNDARY);var g=new u(p,m),y=f.inverseTransformPoint(g);this.setLocation(y.x,y.y)},h.prototype.getLeft=function(){return this.rect.x},h.prototype.getRight=function(){return this.rect.x+this.rect.width},h.prototype.getTop=function(){return this.rect.y},h.prototype.getBottom=function(){return this.rect.y+this.rect.height},h.prototype.getParent=function(){return this.owner==null?null:this.owner.getParent()},e.exports=h}),(function(e,t,r){"use strict";function n(i,a){i==null&&a==null?(this.x=0,this.y=0):(this.x=i,this.y=a)}o(n,"PointD"),n.prototype.getX=function(){return this.x},n.prototype.getY=function(){return this.y},n.prototype.setX=function(i){this.x=i},n.prototype.setY=function(i){this.y=i},n.prototype.getDifference=function(i){return new DimensionD(this.x-i.x,this.y-i.y)},n.prototype.getCopy=function(){return new n(this.x,this.y)},n.prototype.translate=function(i){return this.x+=i.width,this.y+=i.height,this},e.exports=n}),(function(e,t,r){"use strict";var n=r(2),i=r(10),a=r(0),s=r(6),l=r(3),u=r(1),h=r(13),d=r(12),f=r(11);function p(g,y,v){n.call(this,v),this.estimatedSize=i.MIN_VALUE,this.margin=a.DEFAULT_GRAPH_MARGIN,this.edges=[],this.nodes=[],this.isConnected=!1,this.parent=g,y!=null&&y instanceof s?this.graphManager=y:y!=null&&y instanceof Layout&&(this.graphManager=y.graphManager)}o(p,"LGraph"),p.prototype=Object.create(n.prototype);for(var m in n)p[m]=n[m];p.prototype.getNodes=function(){return this.nodes},p.prototype.getEdges=function(){return this.edges},p.prototype.getGraphManager=function(){return this.graphManager},p.prototype.getParent=function(){return this.parent},p.prototype.getLeft=function(){return this.left},p.prototype.getRight=function(){return this.right},p.prototype.getTop=function(){return this.top},p.prototype.getBottom=function(){return this.bottom},p.prototype.isConnected=function(){return this.isConnected},p.prototype.add=function(g,y,v){if(y==null&&v==null){var x=g;if(this.graphManager==null)throw"Graph has no graph mgr!";if(this.getNodes().indexOf(x)>-1)throw"Node already in graph!";return x.owner=this,this.getNodes().push(x),x}else{var b=g;if(!(this.getNodes().indexOf(y)>-1&&this.getNodes().indexOf(v)>-1))throw"Source or target not in graph!";if(!(y.owner==v.owner&&y.owner==this))throw"Both owners must be this graph!";return y.owner!=v.owner?null:(b.source=y,b.target=v,b.isInterGraph=!1,this.getEdges().push(b),y.edges.push(b),v!=y&&v.edges.push(b),b)}},p.prototype.remove=function(g){var y=g;if(g instanceof l){if(y==null)throw"Node is null!";if(!(y.owner!=null&&y.owner==this))throw"Owner graph is invalid!";if(this.graphManager==null)throw"Owner graph manager is invalid!";for(var v=y.edges.slice(),x,b=v.length,T=0;T-1&&w>-1))throw"Source and/or target doesn't know this edge!";x.source.edges.splice(C,1),x.target!=x.source&&x.target.edges.splice(w,1);var k=x.source.owner.getEdges().indexOf(x);if(k==-1)throw"Not in owner's edge list!";x.source.owner.getEdges().splice(k,1)}},p.prototype.updateLeftTop=function(){for(var g=i.MAX_VALUE,y=i.MAX_VALUE,v,x,b,T=this.getNodes(),k=T.length,C=0;Cv&&(g=v),y>x&&(y=x)}return g==i.MAX_VALUE?null:(T[0].getParent().paddingLeft!=null?b=T[0].getParent().paddingLeft:b=this.margin,this.left=y-b,this.top=g-b,new d(this.left,this.top))},p.prototype.updateBounds=function(g){for(var y=i.MAX_VALUE,v=-i.MAX_VALUE,x=i.MAX_VALUE,b=-i.MAX_VALUE,T,k,C,w,S,R=this.nodes,L=R.length,N=0;NT&&(y=T),vC&&(x=C),bT&&(y=T),vC&&(x=C),b=this.nodes.length){var L=0;v.forEach(function(N){N.owner==g&&L++}),L==this.nodes.length&&(this.isConnected=!0)}},e.exports=p}),(function(e,t,r){"use strict";var n,i=r(1);function a(s){n=r(5),this.layout=s,this.graphs=[],this.edges=[]}o(a,"LGraphManager"),a.prototype.addRoot=function(){var s=this.layout.newGraph(),l=this.layout.newNode(null),u=this.add(s,l);return this.setRootGraph(u),this.rootGraph},a.prototype.add=function(s,l,u,h,d){if(u==null&&h==null&&d==null){if(s==null)throw"Graph is null!";if(l==null)throw"Parent node is null!";if(this.graphs.indexOf(s)>-1)throw"Graph already in this graph mgr!";if(this.graphs.push(s),s.parent!=null)throw"Already has a parent!";if(l.child!=null)throw"Already has a child!";return s.parent=l,l.child=s,s}else{d=u,h=l,u=s;var f=h.getOwner(),p=d.getOwner();if(!(f!=null&&f.getGraphManager()==this))throw"Source not in this graph mgr!";if(!(p!=null&&p.getGraphManager()==this))throw"Target not in this graph mgr!";if(f==p)return u.isInterGraph=!1,f.add(u,h,d);if(u.isInterGraph=!0,u.source=h,u.target=d,this.edges.indexOf(u)>-1)throw"Edge already in inter-graph edge list!";if(this.edges.push(u),!(u.source!=null&&u.target!=null))throw"Edge source and/or target is null!";if(!(u.source.edges.indexOf(u)==-1&&u.target.edges.indexOf(u)==-1))throw"Edge already in source and/or target incidency list!";return u.source.edges.push(u),u.target.edges.push(u),u}},a.prototype.remove=function(s){if(s instanceof n){var l=s;if(l.getGraphManager()!=this)throw"Graph not in this graph mgr";if(!(l==this.rootGraph||l.parent!=null&&l.parent.graphManager==this))throw"Invalid parent node!";var u=[];u=u.concat(l.getEdges());for(var h,d=u.length,f=0;f=s.getRight()?l[0]+=Math.min(s.getX()-a.getX(),a.getRight()-s.getRight()):s.getX()<=a.getX()&&s.getRight()>=a.getRight()&&(l[0]+=Math.min(a.getX()-s.getX(),s.getRight()-a.getRight())),a.getY()<=s.getY()&&a.getBottom()>=s.getBottom()?l[1]+=Math.min(s.getY()-a.getY(),a.getBottom()-s.getBottom()):s.getY()<=a.getY()&&s.getBottom()>=a.getBottom()&&(l[1]+=Math.min(a.getY()-s.getY(),s.getBottom()-a.getBottom()));var d=Math.abs((s.getCenterY()-a.getCenterY())/(s.getCenterX()-a.getCenterX()));s.getCenterY()===a.getCenterY()&&s.getCenterX()===a.getCenterX()&&(d=1);var f=d*l[0],p=l[1]/d;l[0]f)return l[0]=u,l[1]=m,l[2]=d,l[3]=R,!1;if(hd)return l[0]=p,l[1]=h,l[2]=w,l[3]=f,!1;if(ud?(l[0]=y,l[1]=v,_=!0):(l[0]=g,l[1]=m,_=!0):M===P&&(u>d?(l[0]=p,l[1]=m,_=!0):(l[0]=x,l[1]=v,_=!0)),-D===P?d>u?(l[2]=S,l[3]=R,A=!0):(l[2]=w,l[3]=C,A=!0):D===P&&(d>u?(l[2]=k,l[3]=C,A=!0):(l[2]=L,l[3]=R,A=!0)),_&&A)return!1;if(u>d?h>f?(B=this.getCardinalDirection(M,P,4),O=this.getCardinalDirection(D,P,2)):(B=this.getCardinalDirection(-M,P,3),O=this.getCardinalDirection(-D,P,1)):h>f?(B=this.getCardinalDirection(-M,P,1),O=this.getCardinalDirection(-D,P,3)):(B=this.getCardinalDirection(M,P,2),O=this.getCardinalDirection(D,P,4)),!_)switch(B){case 1:V=m,$=u+-T/P,l[0]=$,l[1]=V;break;case 2:$=x,V=h+b*P,l[0]=$,l[1]=V;break;case 3:V=v,$=u+T/P,l[0]=$,l[1]=V;break;case 4:$=y,V=h+-b*P,l[0]=$,l[1]=V;break}if(!A)switch(O){case 1:z=C,G=d+-I/P,l[2]=G,l[3]=z;break;case 2:G=L,z=f+N*P,l[2]=G,l[3]=z;break;case 3:z=R,G=d+I/P,l[2]=G,l[3]=z;break;case 4:G=S,z=f+-N*P,l[2]=G,l[3]=z;break}}return!1},i.getCardinalDirection=function(a,s,l){return a>s?l:1+l%4},i.getIntersection=function(a,s,l,u){if(u==null)return this.getIntersection2(a,s,l);var h=a.x,d=a.y,f=s.x,p=s.y,m=l.x,g=l.y,y=u.x,v=u.y,x=void 0,b=void 0,T=void 0,k=void 0,C=void 0,w=void 0,S=void 0,R=void 0,L=void 0;return T=p-d,C=h-f,S=f*d-h*p,k=v-g,w=m-y,R=y*g-m*v,L=T*w-k*C,L===0?null:(x=(C*R-w*S)/L,b=(k*S-T*R)/L,new n(x,b))},i.angleOfVector=function(a,s,l,u){var h=void 0;return a!==l?(h=Math.atan((u-s)/(l-a)),l0?1:i<0?-1:0},n.floor=function(i){return i<0?Math.ceil(i):Math.floor(i)},n.ceil=function(i){return i<0?Math.floor(i):Math.ceil(i)},e.exports=n}),(function(e,t,r){"use strict";function n(){}o(n,"Integer"),n.MAX_VALUE=2147483647,n.MIN_VALUE=-2147483648,e.exports=n}),(function(e,t,r){"use strict";var n=(function(){function h(d,f){for(var p=0;p"u"?"undefined":n(a);return a==null||s!="object"&&s!="function"},e.exports=i}),(function(e,t,r){"use strict";function n(m){if(Array.isArray(m)){for(var g=0,y=Array(m.length);g0&&g;){for(T.push(C[0]);T.length>0&&g;){var w=T[0];T.splice(0,1),b.add(w);for(var S=w.getEdges(),x=0;x-1&&C.splice(I,1)}b=new Set,k=new Map}}return m},p.prototype.createDummyNodesForBendpoints=function(m){for(var g=[],y=m.source,v=this.graphManager.calcLowestCommonAncestor(m.source,m.target),x=0;x0){for(var v=this.edgeToDummyNodes.get(y),x=0;x=0&&g.splice(R,1);var L=k.getNeighborsList();L.forEach(function(_){if(y.indexOf(_)<0){var A=v.get(_),M=A-1;M==1&&w.push(_),v.set(_,M)}})}y=y.concat(w),(g.length==1||g.length==2)&&(x=!0,b=g[0])}return b},p.prototype.setGraphManager=function(m){this.graphManager=m},e.exports=p}),(function(e,t,r){"use strict";function n(){}o(n,"RandomSeed"),n.seed=1,n.x=0,n.nextDouble=function(){return n.x=Math.sin(n.seed++)*1e4,n.x-Math.floor(n.x)},e.exports=n}),(function(e,t,r){"use strict";var n=r(4);function i(a,s){this.lworldOrgX=0,this.lworldOrgY=0,this.ldeviceOrgX=0,this.ldeviceOrgY=0,this.lworldExtX=1,this.lworldExtY=1,this.ldeviceExtX=1,this.ldeviceExtY=1}o(i,"Transform"),i.prototype.getWorldOrgX=function(){return this.lworldOrgX},i.prototype.setWorldOrgX=function(a){this.lworldOrgX=a},i.prototype.getWorldOrgY=function(){return this.lworldOrgY},i.prototype.setWorldOrgY=function(a){this.lworldOrgY=a},i.prototype.getWorldExtX=function(){return this.lworldExtX},i.prototype.setWorldExtX=function(a){this.lworldExtX=a},i.prototype.getWorldExtY=function(){return this.lworldExtY},i.prototype.setWorldExtY=function(a){this.lworldExtY=a},i.prototype.getDeviceOrgX=function(){return this.ldeviceOrgX},i.prototype.setDeviceOrgX=function(a){this.ldeviceOrgX=a},i.prototype.getDeviceOrgY=function(){return this.ldeviceOrgY},i.prototype.setDeviceOrgY=function(a){this.ldeviceOrgY=a},i.prototype.getDeviceExtX=function(){return this.ldeviceExtX},i.prototype.setDeviceExtX=function(a){this.ldeviceExtX=a},i.prototype.getDeviceExtY=function(){return this.ldeviceExtY},i.prototype.setDeviceExtY=function(a){this.ldeviceExtY=a},i.prototype.transformX=function(a){var s=0,l=this.lworldExtX;return l!=0&&(s=this.ldeviceOrgX+(a-this.lworldOrgX)*this.ldeviceExtX/l),s},i.prototype.transformY=function(a){var s=0,l=this.lworldExtY;return l!=0&&(s=this.ldeviceOrgY+(a-this.lworldOrgY)*this.ldeviceExtY/l),s},i.prototype.inverseTransformX=function(a){var s=0,l=this.ldeviceExtX;return l!=0&&(s=this.lworldOrgX+(a-this.ldeviceOrgX)*this.lworldExtX/l),s},i.prototype.inverseTransformY=function(a){var s=0,l=this.ldeviceExtY;return l!=0&&(s=this.lworldOrgY+(a-this.ldeviceOrgY)*this.lworldExtY/l),s},i.prototype.inverseTransformPoint=function(a){var s=new n(this.inverseTransformX(a.x),this.inverseTransformY(a.y));return s},e.exports=i}),(function(e,t,r){"use strict";function n(f){if(Array.isArray(f)){for(var p=0,m=Array(f.length);pa.ADAPTATION_LOWER_NODE_LIMIT&&(this.coolingFactor=Math.max(this.coolingFactor*a.COOLING_ADAPTATION_FACTOR,this.coolingFactor-(f-a.ADAPTATION_LOWER_NODE_LIMIT)/(a.ADAPTATION_UPPER_NODE_LIMIT-a.ADAPTATION_LOWER_NODE_LIMIT)*this.coolingFactor*(1-a.COOLING_ADAPTATION_FACTOR))),this.maxNodeDisplacement=a.MAX_NODE_DISPLACEMENT_INCREMENTAL):(f>a.ADAPTATION_LOWER_NODE_LIMIT?this.coolingFactor=Math.max(a.COOLING_ADAPTATION_FACTOR,1-(f-a.ADAPTATION_LOWER_NODE_LIMIT)/(a.ADAPTATION_UPPER_NODE_LIMIT-a.ADAPTATION_LOWER_NODE_LIMIT)*(1-a.COOLING_ADAPTATION_FACTOR)):this.coolingFactor=1,this.initialCoolingFactor=this.coolingFactor,this.maxNodeDisplacement=a.MAX_NODE_DISPLACEMENT),this.maxIterations=Math.max(this.getAllNodes().length*5,this.maxIterations),this.totalDisplacementThreshold=this.displacementThresholdPerNode*this.getAllNodes().length,this.repulsionRange=this.calcRepulsionRange()},h.prototype.calcSpringForces=function(){for(var f=this.getAllEdges(),p,m=0;m0&&arguments[0]!==void 0?arguments[0]:!0,p=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1,m,g,y,v,x=this.getAllNodes(),b;if(this.useFRGridVariant)for(this.totalIterations%a.GRID_CALCULATION_CHECK_PERIOD==1&&f&&this.updateGrid(),b=new Set,m=0;mT||b>T)&&(f.gravitationForceX=-this.gravityConstant*y,f.gravitationForceY=-this.gravityConstant*v)):(T=p.getEstimatedSize()*this.compoundGravityRangeFactor,(x>T||b>T)&&(f.gravitationForceX=-this.gravityConstant*y*this.compoundGravityConstant,f.gravitationForceY=-this.gravityConstant*v*this.compoundGravityConstant))},h.prototype.isConverged=function(){var f,p=!1;return this.totalIterations>this.maxIterations/3&&(p=Math.abs(this.totalDisplacement-this.oldTotalDisplacement)<2),f=this.totalDisplacement=x.length||T>=x[0].length)){for(var k=0;kh},"_defaultCompareFunction")}]),l})();e.exports=s}),(function(e,t,r){"use strict";var n=(function(){function s(l,u){for(var h=0;h2&&arguments[2]!==void 0?arguments[2]:1,d=arguments.length>3&&arguments[3]!==void 0?arguments[3]:-1,f=arguments.length>4&&arguments[4]!==void 0?arguments[4]:-1;i(this,s),this.sequence1=l,this.sequence2=u,this.match_score=h,this.mismatch_penalty=d,this.gap_penalty=f,this.iMax=l.length+1,this.jMax=u.length+1,this.grid=new Array(this.iMax);for(var p=0;p=0;l--){var u=this.listeners[l];u.event===a&&u.callback===s&&this.listeners.splice(l,1)}},i.emit=function(a,s){for(var l=0;l{"use strict";o((function(t,r){typeof aC=="object"&&typeof UF=="object"?UF.exports=r(HF()):typeof define=="function"&&define.amd?define(["layout-base"],r):typeof aC=="object"?aC.coseBase=r(HF()):t.coseBase=r(t.layoutBase)}),"webpackUniversalModuleDefinition")(aC,function(e){return(function(t){var r={};function n(i){if(r[i])return r[i].exports;var a=r[i]={i,l:!1,exports:{}};return t[i].call(a.exports,a,a.exports,n),a.l=!0,a.exports}return o(n,"__webpack_require__"),n.m=t,n.c=r,n.i=function(i){return i},n.d=function(i,a,s){n.o(i,a)||Object.defineProperty(i,a,{configurable:!1,enumerable:!0,get:s})},n.n=function(i){var a=i&&i.__esModule?o(function(){return i.default},"getDefault"):o(function(){return i},"getModuleExports");return n.d(a,"a",a),a},n.o=function(i,a){return Object.prototype.hasOwnProperty.call(i,a)},n.p="",n(n.s=7)})([(function(t,r){t.exports=e}),(function(t,r,n){"use strict";var i=n(0).FDLayoutConstants;function a(){}o(a,"CoSEConstants");for(var s in i)a[s]=i[s];a.DEFAULT_USE_MULTI_LEVEL_SCALING=!1,a.DEFAULT_RADIAL_SEPARATION=i.DEFAULT_EDGE_LENGTH,a.DEFAULT_COMPONENT_SEPERATION=60,a.TILE=!0,a.TILING_PADDING_VERTICAL=10,a.TILING_PADDING_HORIZONTAL=10,a.TREE_REDUCTION_ON_INCREMENTAL=!1,t.exports=a}),(function(t,r,n){"use strict";var i=n(0).FDLayoutEdge;function a(l,u,h){i.call(this,l,u,h)}o(a,"CoSEEdge"),a.prototype=Object.create(i.prototype);for(var s in i)a[s]=i[s];t.exports=a}),(function(t,r,n){"use strict";var i=n(0).LGraph;function a(l,u,h){i.call(this,l,u,h)}o(a,"CoSEGraph"),a.prototype=Object.create(i.prototype);for(var s in i)a[s]=i[s];t.exports=a}),(function(t,r,n){"use strict";var i=n(0).LGraphManager;function a(l){i.call(this,l)}o(a,"CoSEGraphManager"),a.prototype=Object.create(i.prototype);for(var s in i)a[s]=i[s];t.exports=a}),(function(t,r,n){"use strict";var i=n(0).FDLayoutNode,a=n(0).IMath;function s(u,h,d,f){i.call(this,u,h,d,f)}o(s,"CoSENode"),s.prototype=Object.create(i.prototype);for(var l in i)s[l]=i[l];s.prototype.move=function(){var u=this.graphManager.getLayout();this.displacementX=u.coolingFactor*(this.springForceX+this.repulsionForceX+this.gravitationForceX)/this.noOfChildren,this.displacementY=u.coolingFactor*(this.springForceY+this.repulsionForceY+this.gravitationForceY)/this.noOfChildren,Math.abs(this.displacementX)>u.coolingFactor*u.maxNodeDisplacement&&(this.displacementX=u.coolingFactor*u.maxNodeDisplacement*a.sign(this.displacementX)),Math.abs(this.displacementY)>u.coolingFactor*u.maxNodeDisplacement&&(this.displacementY=u.coolingFactor*u.maxNodeDisplacement*a.sign(this.displacementY)),this.child==null?this.moveBy(this.displacementX,this.displacementY):this.child.getNodes().length==0?this.moveBy(this.displacementX,this.displacementY):this.propogateDisplacementToChildren(this.displacementX,this.displacementY),u.totalDisplacement+=Math.abs(this.displacementX)+Math.abs(this.displacementY),this.springForceX=0,this.springForceY=0,this.repulsionForceX=0,this.repulsionForceY=0,this.gravitationForceX=0,this.gravitationForceY=0,this.displacementX=0,this.displacementY=0},s.prototype.propogateDisplacementToChildren=function(u,h){for(var d=this.getChild().getNodes(),f,p=0;p0)this.positionNodesRadially(C);else{this.reduceTrees(),this.graphManager.resetAllNodesToApplyGravitation();var w=new Set(this.getAllNodes()),S=this.nodesWithGravity.filter(function(R){return w.has(R)});this.graphManager.setAllNodesToApplyGravitation(S),this.positionNodesRandomly()}}return this.initSpringEmbedder(),this.runSpringEmbedder(),!0},T.prototype.tick=function(){if(this.totalIterations++,this.totalIterations===this.maxIterations&&!this.isTreeGrowing&&!this.isGrowthFinished)if(this.prunedNodesAll.length>0)this.isTreeGrowing=!0;else return!0;if(this.totalIterations%d.CONVERGENCE_CHECK_PERIOD==0&&!this.isTreeGrowing&&!this.isGrowthFinished){if(this.isConverged())if(this.prunedNodesAll.length>0)this.isTreeGrowing=!0;else return!0;this.coolingCycle++,this.layoutQuality==0?this.coolingAdjuster=this.coolingCycle:this.layoutQuality==1&&(this.coolingAdjuster=this.coolingCycle/3),this.coolingFactor=Math.max(this.initialCoolingFactor-Math.pow(this.coolingCycle,Math.log(100*(this.initialCoolingFactor-this.finalTemperature))/Math.log(this.maxCoolingCycle))/100*this.coolingAdjuster,this.finalTemperature),this.animationPeriod=Math.ceil(this.initialAnimationPeriod*Math.sqrt(this.coolingFactor))}if(this.isTreeGrowing){if(this.growTreeIterations%10==0)if(this.prunedNodesAll.length>0){this.graphManager.updateBounds(),this.updateGrid(),this.growTree(this.prunedNodesAll),this.graphManager.resetAllNodesToApplyGravitation();var C=new Set(this.getAllNodes()),w=this.nodesWithGravity.filter(function(L){return C.has(L)});this.graphManager.setAllNodesToApplyGravitation(w),this.graphManager.updateBounds(),this.updateGrid(),this.coolingFactor=d.DEFAULT_COOLING_FACTOR_INCREMENTAL}else this.isTreeGrowing=!1,this.isGrowthFinished=!0;this.growTreeIterations++}if(this.isGrowthFinished){if(this.isConverged())return!0;this.afterGrowthIterations%10==0&&(this.graphManager.updateBounds(),this.updateGrid()),this.coolingFactor=d.DEFAULT_COOLING_FACTOR_INCREMENTAL*((100-this.afterGrowthIterations)/100),this.afterGrowthIterations++}var S=!this.isTreeGrowing&&!this.isGrowthFinished,R=this.growTreeIterations%10==1&&this.isTreeGrowing||this.afterGrowthIterations%10==1&&this.isGrowthFinished;return this.totalDisplacement=0,this.graphManager.updateBounds(),this.calcSpringForces(),this.calcRepulsionForces(S,R),this.calcGravitationalForces(),this.moveNodes(),this.animate(),!1},T.prototype.getPositionsData=function(){for(var C=this.graphManager.getAllNodes(),w={},S=0;S1){var _;for(_=0;_R&&(R=Math.floor(I.y)),N=Math.floor(I.x+h.DEFAULT_COMPONENT_SEPERATION)}this.transform(new m(f.WORLD_CENTER_X-I.x/2,f.WORLD_CENTER_Y-I.y/2))},T.radialLayout=function(C,w,S){var R=Math.max(this.maxDiagonalInTree(C),h.DEFAULT_RADIAL_SEPARATION);T.branchRadialLayout(w,null,0,359,0,R);var L=x.calculateBounds(C),N=new b;N.setDeviceOrgX(L.getMinX()),N.setDeviceOrgY(L.getMinY()),N.setWorldOrgX(S.x),N.setWorldOrgY(S.y);for(var I=0;I1;){var W=z[0];z.splice(0,1);var H=B.indexOf(W);H>=0&&B.splice(H,1),V--,O--}w!=null?G=(B.indexOf(z[0])+1)%V:G=0;for(var j=Math.abs(R-S)/O,Q=G;$!=O;Q=++Q%V){var U=B[Q].getOtherEnd(C);if(U!=w){var oe=(S+$*j)%360,te=(oe+j)%360;T.branchRadialLayout(U,C,oe,te,L+N,N),$++}}},T.maxDiagonalInTree=function(C){for(var w=y.MIN_VALUE,S=0;Sw&&(w=L)}return w},T.prototype.calcRepulsionRange=function(){return 2*(this.level+1)*this.idealEdgeLength},T.prototype.groupZeroDegreeMembers=function(){var C=this,w={};this.memberGroups={},this.idToDummyNode={};for(var S=[],R=this.graphManager.getAllNodes(),L=0;L"u"&&(w[_]=[]),w[_]=w[_].concat(N)}Object.keys(w).forEach(function(A){if(w[A].length>1){var M="DummyCompound_"+A;C.memberGroups[M]=w[A];var D=w[A][0].getParent(),P=new l(C.graphManager);P.id=M,P.paddingLeft=D.paddingLeft||0,P.paddingRight=D.paddingRight||0,P.paddingBottom=D.paddingBottom||0,P.paddingTop=D.paddingTop||0,C.idToDummyNode[M]=P;var B=C.getGraphManager().add(C.newGraph(),P),O=D.getChild();O.add(P);for(var $=0;$=0;C--){var w=this.compoundOrder[C],S=w.id,R=w.paddingLeft,L=w.paddingTop;this.adjustLocations(this.tiledMemberPack[S],w.rect.x,w.rect.y,R,L)}},T.prototype.repopulateZeroDegreeMembers=function(){var C=this,w=this.tiledZeroDegreePack;Object.keys(w).forEach(function(S){var R=C.idToDummyNode[S],L=R.paddingLeft,N=R.paddingTop;C.adjustLocations(w[S],R.rect.x,R.rect.y,L,N)})},T.prototype.getToBeTiled=function(C){var w=C.id;if(this.toBeTiled[w]!=null)return this.toBeTiled[w];var S=C.getChild();if(S==null)return this.toBeTiled[w]=!1,!1;for(var R=S.getNodes(),L=0;L0)return this.toBeTiled[w]=!1,!1;if(N.getChild()==null){this.toBeTiled[N.id]=!1;continue}if(!this.getToBeTiled(N))return this.toBeTiled[w]=!1,!1}return this.toBeTiled[w]=!0,!0},T.prototype.getNodeDegree=function(C){for(var w=C.id,S=C.getEdges(),R=0,L=0;LA&&(A=D.rect.height)}S+=A+C.verticalPadding}},T.prototype.tileCompoundMembers=function(C,w){var S=this;this.tiledMemberPack=[],Object.keys(C).forEach(function(R){var L=w[R];S.tiledMemberPack[R]=S.tileNodes(C[R],L.paddingLeft+L.paddingRight),L.rect.width=S.tiledMemberPack[R].width,L.rect.height=S.tiledMemberPack[R].height})},T.prototype.tileNodes=function(C,w){var S=h.TILING_PADDING_VERTICAL,R=h.TILING_PADDING_HORIZONTAL,L={rows:[],rowWidth:[],rowHeight:[],width:0,height:w,verticalPadding:S,horizontalPadding:R};C.sort(function(_,A){return _.rect.width*_.rect.height>A.rect.width*A.rect.height?-1:_.rect.width*_.rect.height0&&(I+=C.horizontalPadding),C.rowWidth[S]=I,C.width0&&(_+=C.verticalPadding);var A=0;_>C.rowHeight[S]&&(A=C.rowHeight[S],C.rowHeight[S]=_,A=C.rowHeight[S]-A),C.height+=A,C.rows[S].push(w)},T.prototype.getShortestRowIndex=function(C){for(var w=-1,S=Number.MAX_VALUE,R=0;RS&&(w=R,S=C.rowWidth[R]);return w},T.prototype.canAddHorizontal=function(C,w,S){var R=this.getShortestRowIndex(C);if(R<0)return!0;var L=C.rowWidth[R];if(L+C.horizontalPadding+w<=C.width)return!0;var N=0;C.rowHeight[R]0&&(N=S+C.verticalPadding-C.rowHeight[R]);var I;C.width-L>=w+C.horizontalPadding?I=(C.height+N)/(L+w+C.horizontalPadding):I=(C.height+N)/C.width,N=S+C.verticalPadding;var _;return C.widthN&&w!=S){R.splice(-1,1),C.rows[S].push(L),C.rowWidth[w]=C.rowWidth[w]-N,C.rowWidth[S]=C.rowWidth[S]+N,C.width=C.rowWidth[instance.getLongestRowIndex(C)];for(var I=Number.MIN_VALUE,_=0;_I&&(I=R[_].height);w>0&&(I+=C.verticalPadding);var A=C.rowHeight[w]+C.rowHeight[S];C.rowHeight[w]=I,C.rowHeight[S]0)for(var O=L;O<=N;O++)B[0]+=this.grid[O][I-1].length+this.grid[O][I].length-1;if(N0)for(var O=I;O<=_;O++)B[3]+=this.grid[L-1][O].length+this.grid[L][O].length-1;for(var $=y.MAX_VALUE,V,G,z=0;z{"use strict";o((function(t,r){typeof sC=="object"&&typeof jF=="object"?jF.exports=r(YF()):typeof define=="function"&&define.amd?define(["cose-base"],r):typeof sC=="object"?sC.cytoscapeCoseBilkent=r(YF()):t.cytoscapeCoseBilkent=r(t.coseBase)}),"webpackUniversalModuleDefinition")(sC,function(e){return(function(t){var r={};function n(i){if(r[i])return r[i].exports;var a=r[i]={i,l:!1,exports:{}};return t[i].call(a.exports,a,a.exports,n),a.l=!0,a.exports}return o(n,"__webpack_require__"),n.m=t,n.c=r,n.i=function(i){return i},n.d=function(i,a,s){n.o(i,a)||Object.defineProperty(i,a,{configurable:!1,enumerable:!0,get:s})},n.n=function(i){var a=i&&i.__esModule?o(function(){return i.default},"getDefault"):o(function(){return i},"getModuleExports");return n.d(a,"a",a),a},n.o=function(i,a){return Object.prototype.hasOwnProperty.call(i,a)},n.p="",n(n.s=1)})([(function(t,r){t.exports=e}),(function(t,r,n){"use strict";var i=n(0).layoutBase.LayoutConstants,a=n(0).layoutBase.FDLayoutConstants,s=n(0).CoSEConstants,l=n(0).CoSELayout,u=n(0).CoSENode,h=n(0).layoutBase.PointD,d=n(0).layoutBase.DimensionD,f={ready:o(function(){},"ready"),stop:o(function(){},"stop"),quality:"default",nodeDimensionsIncludeLabels:!1,refresh:30,fit:!0,padding:10,randomize:!0,nodeRepulsion:4500,idealEdgeLength:50,edgeElasticity:.45,nestingFactor:.1,gravity:.25,numIter:2500,tile:!0,animate:"end",animationDuration:500,tilingPaddingVertical:10,tilingPaddingHorizontal:10,gravityRangeCompound:1.5,gravityCompound:1,gravityRange:3.8,initialEnergyOnIncremental:.5};function p(v,x){var b={};for(var T in v)b[T]=v[T];for(var T in x)b[T]=x[T];return b}o(p,"extend");function m(v){this.options=p(f,v),g(this.options)}o(m,"_CoSELayout");var g=o(function(x){x.nodeRepulsion!=null&&(s.DEFAULT_REPULSION_STRENGTH=a.DEFAULT_REPULSION_STRENGTH=x.nodeRepulsion),x.idealEdgeLength!=null&&(s.DEFAULT_EDGE_LENGTH=a.DEFAULT_EDGE_LENGTH=x.idealEdgeLength),x.edgeElasticity!=null&&(s.DEFAULT_SPRING_STRENGTH=a.DEFAULT_SPRING_STRENGTH=x.edgeElasticity),x.nestingFactor!=null&&(s.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR=a.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR=x.nestingFactor),x.gravity!=null&&(s.DEFAULT_GRAVITY_STRENGTH=a.DEFAULT_GRAVITY_STRENGTH=x.gravity),x.numIter!=null&&(s.MAX_ITERATIONS=a.MAX_ITERATIONS=x.numIter),x.gravityRange!=null&&(s.DEFAULT_GRAVITY_RANGE_FACTOR=a.DEFAULT_GRAVITY_RANGE_FACTOR=x.gravityRange),x.gravityCompound!=null&&(s.DEFAULT_COMPOUND_GRAVITY_STRENGTH=a.DEFAULT_COMPOUND_GRAVITY_STRENGTH=x.gravityCompound),x.gravityRangeCompound!=null&&(s.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR=a.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR=x.gravityRangeCompound),x.initialEnergyOnIncremental!=null&&(s.DEFAULT_COOLING_FACTOR_INCREMENTAL=a.DEFAULT_COOLING_FACTOR_INCREMENTAL=x.initialEnergyOnIncremental),x.quality=="draft"?i.QUALITY=0:x.quality=="proof"?i.QUALITY=2:i.QUALITY=1,s.NODE_DIMENSIONS_INCLUDE_LABELS=a.NODE_DIMENSIONS_INCLUDE_LABELS=i.NODE_DIMENSIONS_INCLUDE_LABELS=x.nodeDimensionsIncludeLabels,s.DEFAULT_INCREMENTAL=a.DEFAULT_INCREMENTAL=i.DEFAULT_INCREMENTAL=!x.randomize,s.ANIMATE=a.ANIMATE=i.ANIMATE=x.animate,s.TILE=x.tile,s.TILING_PADDING_VERTICAL=typeof x.tilingPaddingVertical=="function"?x.tilingPaddingVertical.call():x.tilingPaddingVertical,s.TILING_PADDING_HORIZONTAL=typeof x.tilingPaddingHorizontal=="function"?x.tilingPaddingHorizontal.call():x.tilingPaddingHorizontal},"getUserOptions");m.prototype.run=function(){var v,x,b=this.options,T=this.idToLNode={},k=this.layout=new l,C=this;C.stopped=!1,this.cy=this.options.cy,this.cy.trigger({type:"layoutstart",layout:this});var w=k.newGraphManager();this.gm=w;var S=this.options.eles.nodes(),R=this.options.eles.edges();this.root=w.addRoot(),this.processChildrenList(this.root,this.getTopMostNodes(S),k);for(var L=0;L0){var _;_=b.getGraphManager().add(b.newGraph(),S),this.processChildrenList(_,w,b)}}},m.prototype.stop=function(){return this.stopped=!0,this};var y=o(function(x){x("layout","cose-bilkent",m)},"register");typeof cytoscape<"u"&&y(cytoscape),t.exports=y})])})});function Ygt(e,t){e.forEach(r=>{let n={id:r.id,labelText:r.label,height:r.height,width:r.width,padding:r.padding??0};Object.keys(r).forEach(i=>{["id","label","height","width","padding","x","y"].includes(i)||(n[i]=r[i])}),t.add({group:"nodes",data:n,position:{x:r.x??0,y:r.y??0}})})}function jgt(e,t){e.forEach(r=>{let n={id:r.id,source:r.start,target:r.end};Object.keys(r).forEach(i=>{["id","start","end"].includes(i)||(n[i]=r[i])}),t.add({group:"edges",data:n})})}function eTe(e){return new Promise(t=>{let r=et("body").append("div").attr("id","cy").attr("style","display:none"),n=El({container:document.getElementById("cy"),style:[{selector:"edge",style:{"curve-style":"bezier"}}]});r.remove(),Ygt(e.nodes,n),jgt(e.edges,n),n.nodes().forEach(function(a){a.layoutDimensions=()=>{let s=a.data();return{w:s.width,h:s.height}}});let i={name:"cose-bilkent",quality:"proof",styleEnabled:!1,animate:!1};n.layout(i).run(),n.ready(a=>{Z.info("Cytoscape ready",a),t(n)})})}function tTe(e){return e.nodes().map(t=>{let r=t.data(),n=t.position(),i={id:r.id,x:n.x,y:n.y};return Object.keys(r).forEach(a=>{a!=="id"&&(i[a]=r[a])}),i})}function rTe(e){return e.edges().map(t=>{let r=t.data(),n=t._private.rscratch,i={id:r.id,source:r.source,target:r.target,startX:n.startX,startY:n.startY,midX:n.midX,midY:n.midY,endX:n.endX,endY:n.endY};return Object.keys(r).forEach(a=>{["id","source","target"].includes(a)||(i[a]=r[a])}),i})}var J2e,nTe=F(()=>{"use strict";WF();J2e=Xs(Q2e(),1);$r();vt();El.use(J2e.default);o(Ygt,"addNodes");o(jgt,"addEdges");o(eTe,"createCytoscapeInstance");o(tTe,"extractPositionedNodes");o(rTe,"extractPositionedEdges")});async function iTe(e,t){Z.debug("Starting cose-bilkent layout algorithm");try{Xgt(e);let r=await eTe(e),n=tTe(r),i=rTe(r);return Z.debug(`Layout completed: ${n.length} nodes, ${i.length} edges`),{nodes:n,edges:i}}catch(r){throw Z.error("Error in cose-bilkent layout algorithm:",r),r}}function Xgt(e){if(!e)throw new Error("Layout data is required");if(!e.config)throw new Error("Configuration is required in layout data");if(!e.rootNode)throw new Error("Root node is required");if(!e.nodes||!Array.isArray(e.nodes))throw new Error("No nodes found in layout data");if(!Array.isArray(e.edges))throw new Error("Edges array is required in layout data");return!0}var aTe=F(()=>{"use strict";vt();nTe();o(iTe,"executeCoseBilkentLayout");o(Xgt,"validateLayoutData")});var sTe,oTe=F(()=>{"use strict";aTe();sTe=o(async(e,t,{insertCluster:r,insertEdge:n,insertEdgeLabel:i,insertMarkers:a,insertNode:s,log:l,positionEdgeLabel:u},{algorithm:h})=>{let d={},f={},p=t.select("g");a(p,e.markers,e.type,e.diagramId);let m=p.insert("g").attr("class","subgraphs"),g=p.insert("g").attr("class","edgePaths"),y=p.insert("g").attr("class","edgeLabels"),v=p.insert("g").attr("class","nodes");l.debug("Inserting nodes into DOM for dimension calculation"),await Promise.all(e.nodes.map(async T=>{if(T.isGroup){let k={...T};f[T.id]=k,d[T.id]=k,await r(m,T)}else{let k={...T};d[T.id]=k;let C=await s(v,T,{config:e.config,dir:e.direction||"TB"}),w=C.node().getBBox();k.width=w.width,k.height=w.height,k.domId=C,l.debug(`Node ${T.id} dimensions: ${w.width}x${w.height}`)}})),l.debug("Running cose-bilkent layout algorithm");let x={...e,nodes:e.nodes.map(T=>{let k=d[T.id];return{...T,width:k.width,height:k.height}})},b=await iTe(x,e.config);l.debug("Positioning nodes based on layout results"),b.nodes.forEach(T=>{let k=d[T.id];k?.domId&&(k.domId.attr("transform",`translate(${T.x}, ${T.y})`),k.x=T.x,k.y=T.y,l.debug(`Positioned node ${k.id} at center (${T.x}, ${T.y})`))}),b.edges.forEach(T=>{let k=e.edges.find(C=>C.id===T.id);k&&(k.points=[{x:T.startX,y:T.startY},{x:T.midX,y:T.midY},{x:T.endX,y:T.endY}])}),l.debug("Inserting and positioning edges"),await Promise.all(e.edges.map(async T=>{let k=await i(y,T),C=d[T.start??""],w=d[T.end??""];if(C&&w){let S=b.edges.find(R=>R.id===T.id);if(S){l.debug("APA01 positionedEdge",S);let R={...T},L=n(g,R,f,e.type,C,w,e.diagramId);u(R,L)}else{let R={...T,points:[{x:C.x||0,y:C.y||0},{x:w.x||0,y:w.y||0}]},L=n(g,R,f,e.type,C,w,e.diagramId);u(R,L)}}})),l.debug("Cose-bilkent rendering completed")},"render")});var lTe={};ir(lTe,{render:()=>Kgt});var Kgt,cTe=F(()=>{"use strict";oTe();Kgt=sTe});var oC,XF,Zgt,Al,Su,Jf=F(()=>{"use strict";Jce();vt();oC={},XF=o(e=>{for(let t of e)oC[t.name]=t},"registerLayoutLoaders"),Zgt=o(()=>{XF([{name:"dagre",loader:o(async()=>await Promise.resolve().then(()=>(w0e(),C0e)),"loader")},{name:"swimlane",loader:o(async()=>await Promise.resolve().then(()=>(D1e(),L1e)),"loader")},{name:"cose-bilkent",loader:o(async()=>await Promise.resolve().then(()=>(cTe(),lTe)),"loader")}])},"registerDefaultLayoutLoaders");Zgt();Al=o(async(e,t,r)=>{if(!(e.layoutAlgorithm in oC))throw new Error(`Unknown layout algorithm: ${e.layoutAlgorithm}`);if(e.diagramId)for(let f of e.nodes){let p=f.domId||f.id;f.domId=`${e.diagramId}-${p}`}let n=oC[e.layoutAlgorithm],i=await n.loader(),{theme:a,themeVariables:s}=e.config,{useGradient:l,gradientStart:u,gradientStop:h}=s,d=t.attr("id");if(t.append("defs").append("filter").attr("id",`${d}-drop-shadow`).attr("height","130%").attr("width","130%").append("feDropShadow").attr("dx","4").attr("dy","4").attr("stdDeviation",0).attr("flood-opacity","0.06").attr("flood-color",`${a?.includes("dark")?"#FFFFFF":"#000000"}`),t.append("defs").append("filter").attr("id",`${d}-drop-shadow-small`).attr("height","150%").attr("width","150%").append("feDropShadow").attr("dx","2").attr("dy","2").attr("stdDeviation",0).attr("flood-opacity","0.06").attr("flood-color",`${a?.includes("dark")?"#FFFFFF":"#000000"}`),l){let f=t.append("linearGradient").attr("id",t.attr("id")+"-gradient").attr("gradientUnits","objectBoundingBox").attr("x1","0%").attr("y1","0%").attr("x2","100%").attr("y2","0%");f.append("svg:stop").attr("offset","0%").attr("stop-color",u).attr("stop-opacity",1),f.append("svg:stop").attr("offset","100%").attr("stop-color",h).attr("stop-opacity",1)}return i.render(e,t,Qce,{algorithm:n.algorithm},r)},"render"),Su=o((e="",{fallback:t="dagre"}={})=>{if(e in oC)return e;if(t in oC)return Z.warn(`Layout algorithm ${e} is not registered. Using ${t} as fallback.`),t;throw new Error(`Both layout algorithms ${e} and ${t} are not registered.`)},"getRegisteredLayoutAlgorithm")});var vo,Qgt,Jgt,ep=F(()=>{"use strict";$n();vt();vo=o((e,t,r,n)=>{e.attr("class",r);let{width:i,height:a,x:s,y:l}=Qgt(e,t);Wr(e,a,i,n);let u=Jgt(s,l,i,a,t);e.attr("viewBox",u),Z.debug(`viewBox configured: ${u} with padding: ${t}`)},"setupViewPortForSVG"),Qgt=o((e,t)=>{let r=e.node()?.getBBox()||{width:0,height:0,x:0,y:0};return{width:r.width+t*2,height:r.height+t*2,x:r.x,y:r.y}},"calculateDimensionsWithPadding"),Jgt=o((e,t,r,n,i)=>`${e-i} ${t-i} ${r} ${n}`,"createViewBox")});var e0t,t0t,uTe,hTe=F(()=>{"use strict";Xt();vt();Rm();Jf();ep();Qt();e0t=o(function(e,t){return t.db.getClasses()},"getClasses"),t0t=o(async function(e,t,r,n,i){Z.info("REF0:"),Z.info("Drawing state diagram (v2)",t);let{securityLevel:a,flowchart:s,layout:l}=Ae();n.db.setDiagramId(t),Z.debug("Before getData: ");let u=n.db.getData();Z.debug("Data: ",u);let h=pl(t,a),d=n.db.getDirection();u.type=n.type,u.layoutAlgorithm=Su(l),u.layoutAlgorithm==="dagre"&&l==="elk"&&Z.warn("flowchart-elk was moved to an external package in Mermaid v11. Please refer [release notes](https://github.com/mermaid-js/mermaid/releases/tag/v11.0.0) for more details. This diagram will be rendered using `dagre` layout as a fallback."),u.direction=d,u.nodeSpacing=s?.nodeSpacing||50,u.rankSpacing=s?.rankSpacing||50,u.markers=["point","circle","cross"],u.diagramId=t,Z.debug("REF1:",u),await Al(u,h,i);let f=u.config.flowchart?.diagramPadding??8;Zt.insertTitle(h,"flowchartTitleText",s?.titleTopMargin||0,n.db.getDiagramTitle()),vo(h,f,"flowchart",s?.useMaxWidth||!1)},"draw"),uTe={getClasses:e0t,draw:t0t}});var KF,ZF,dTe=F(()=>{"use strict";KF=(function(){var e=o(function(kt,Ct,Ot,Ft){for(Ot=Ot||{},Ft=kt.length;Ft--;Ot[kt[Ft]]=Ct);return Ot},"o"),t=[1,4],r=[1,3],n=[1,5],i=[1,8,9,10,11,27,34,36,38,44,60,84,85,86,87,88,89,102,105,106,109,111,114,115,116,121,122,123,124,125],a=[2,2],s=[1,13],l=[1,14],u=[1,15],h=[1,16],d=[1,23],f=[1,25],p=[1,26],m=[1,27],g=[1,50],y=[1,49],v=[1,29],x=[1,30],b=[1,31],T=[1,32],k=[1,33],C=[1,45],w=[1,47],S=[1,43],R=[1,48],L=[1,44],N=[1,51],I=[1,46],_=[1,52],A=[1,53],M=[1,34],D=[1,35],P=[1,36],B=[1,37],O=[1,38],$=[1,58],V=[1,8,9,10,11,27,32,34,36,38,44,60,84,85,86,87,88,89,102,105,106,109,111,114,115,116,121,122,123,124,125],G=[1,62],z=[1,61],W=[1,63],H=[8,9,11,75,77,78],j=[1,79],Q=[1,92],U=[1,97],oe=[1,96],te=[1,93],le=[1,89],ie=[1,95],ae=[1,91],Re=[1,98],be=[1,94],Pe=[1,99],Ge=[1,90],Oe=[8,9,10,11,40,75,77,78],ue=[8,9,10,11,40,46,75,77,78],ye=[8,9,10,11,29,40,44,46,48,50,52,54,56,58,60,63,65,67,68,70,75,77,78,89,102,105,106,109,111,114,115,116],ke=[8,9,11,44,60,75,77,78,89,102,105,106,109,111,114,115,116],ce=[44,60,89,102,105,106,109,111,114,115,116],re=[1,122],J=[1,123],se=[1,125],ge=[1,124],Te=[44,60,62,74,89,102,105,106,109,111,114,115,116],we=[1,134],Me=[1,148],ve=[1,149],ne=[1,150],q=[1,151],he=[1,136],X=[1,138],fe=[1,142],K=[1,143],qe=[1,144],_e=[1,145],Be=[1,146],Ne=[1,147],He=[1,152],$e=[1,153],Xe=[1,132],Fe=[1,133],Ke=[1,140],xe=[1,135],mt=[1,139],Le=[1,137],ft=[8,9,10,11,27,32,34,36,38,44,60,84,85,86,87,88,89,102,105,106,109,111,114,115,116,121,122,123,124,125],wt=[1,155],zt=[1,157],St=[8,9,11],At=[8,9,10,11,14,44,60,89,105,106,109,111,114,115,116],bt=[1,177],me=[1,173],lt=[1,174],gt=[1,178],Ze=[1,175],Ee=[1,176],tt=[77,116,119],at=[8,9,10,11,12,14,27,29,32,44,60,75,84,85,86,87,88,89,90,105,109,111,114,115,116],ot=[10,106],Wt=[31,49,51,53,55,57,62,64,66,67,69,71,116,117,118],Bt=[1,248],qt=[1,246],vr=[1,250],Tt=[1,244],De=[1,245],it=[1,247],We=[1,249],rt=[1,251],yt=[1,269],Yt=[8,9,11,106],Ht=[8,9,10,11,60,84,105,106,109,110,111,112],pr={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,graphConfig:4,document:5,line:6,statement:7,SEMI:8,NEWLINE:9,SPACE:10,EOF:11,GRAPH:12,NODIR:13,DIR:14,FirstStmtSeparator:15,ending:16,endToken:17,spaceList:18,spaceListNewline:19,vertexStatement:20,separator:21,styleStatement:22,linkStyleStatement:23,classDefStatement:24,classStatement:25,clickStatement:26,subgraph:27,textNoTags:28,SQS:29,text:30,SQE:31,end:32,direction:33,acc_title:34,acc_title_value:35,acc_descr:36,acc_descr_value:37,acc_descr_multiline_value:38,shapeData:39,SHAPE_DATA:40,link:41,node:42,styledVertex:43,AMP:44,vertex:45,STYLE_SEPARATOR:46,idString:47,DOUBLECIRCLESTART:48,DOUBLECIRCLEEND:49,PS:50,PE:51,"(-":52,"-)":53,STADIUMSTART:54,STADIUMEND:55,SUBROUTINESTART:56,SUBROUTINEEND:57,VERTEX_WITH_PROPS_START:58,"NODE_STRING[field]":59,COLON:60,"NODE_STRING[value]":61,PIPE:62,CYLINDERSTART:63,CYLINDEREND:64,DIAMOND_START:65,DIAMOND_STOP:66,TAGEND:67,TRAPSTART:68,TRAPEND:69,INVTRAPSTART:70,INVTRAPEND:71,linkStatement:72,arrowText:73,TESTSTR:74,START_LINK:75,edgeText:76,LINK:77,LINK_ID:78,edgeTextToken:79,STR:80,MD_STR:81,textToken:82,keywords:83,STYLE:84,LINKSTYLE:85,CLASSDEF:86,CLASS:87,CLICK:88,DOWN:89,UP:90,textNoTagsToken:91,stylesOpt:92,"idString[vertex]":93,"idString[class]":94,CALLBACKNAME:95,CALLBACKARGS:96,HREF:97,LINK_TARGET:98,"STR[link]":99,"STR[tooltip]":100,alphaNum:101,DEFAULT:102,numList:103,INTERPOLATE:104,NUM:105,COMMA:106,style:107,styleComponent:108,NODE_STRING:109,UNIT:110,BRKT:111,PCT:112,idStringToken:113,MINUS:114,MULT:115,UNICODE_TEXT:116,TEXT:117,TAGSTART:118,EDGE_TEXT:119,alphaNumToken:120,direction_tb:121,direction_bt:122,direction_rl:123,direction_lr:124,direction_td:125,$accept:0,$end:1},terminals_:{2:"error",8:"SEMI",9:"NEWLINE",10:"SPACE",11:"EOF",12:"GRAPH",13:"NODIR",14:"DIR",27:"subgraph",29:"SQS",31:"SQE",32:"end",34:"acc_title",35:"acc_title_value",36:"acc_descr",37:"acc_descr_value",38:"acc_descr_multiline_value",40:"SHAPE_DATA",44:"AMP",46:"STYLE_SEPARATOR",48:"DOUBLECIRCLESTART",49:"DOUBLECIRCLEEND",50:"PS",51:"PE",52:"(-",53:"-)",54:"STADIUMSTART",55:"STADIUMEND",56:"SUBROUTINESTART",57:"SUBROUTINEEND",58:"VERTEX_WITH_PROPS_START",59:"NODE_STRING[field]",60:"COLON",61:"NODE_STRING[value]",62:"PIPE",63:"CYLINDERSTART",64:"CYLINDEREND",65:"DIAMOND_START",66:"DIAMOND_STOP",67:"TAGEND",68:"TRAPSTART",69:"TRAPEND",70:"INVTRAPSTART",71:"INVTRAPEND",74:"TESTSTR",75:"START_LINK",77:"LINK",78:"LINK_ID",80:"STR",81:"MD_STR",84:"STYLE",85:"LINKSTYLE",86:"CLASSDEF",87:"CLASS",88:"CLICK",89:"DOWN",90:"UP",93:"idString[vertex]",94:"idString[class]",95:"CALLBACKNAME",96:"CALLBACKARGS",97:"HREF",98:"LINK_TARGET",99:"STR[link]",100:"STR[tooltip]",102:"DEFAULT",104:"INTERPOLATE",105:"NUM",106:"COMMA",109:"NODE_STRING",110:"UNIT",111:"BRKT",112:"PCT",114:"MINUS",115:"MULT",116:"UNICODE_TEXT",117:"TEXT",118:"TAGSTART",119:"EDGE_TEXT",121:"direction_tb",122:"direction_bt",123:"direction_rl",124:"direction_lr",125:"direction_td"},productions_:[0,[3,2],[5,0],[5,2],[6,1],[6,1],[6,1],[6,1],[6,1],[4,2],[4,2],[4,2],[4,3],[16,2],[16,1],[17,1],[17,1],[17,1],[15,1],[15,1],[15,2],[19,2],[19,2],[19,1],[19,1],[18,2],[18,1],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2],[7,9],[7,6],[7,4],[7,1],[7,2],[7,2],[7,1],[21,1],[21,1],[21,1],[39,2],[39,1],[20,4],[20,3],[20,4],[20,2],[20,2],[20,1],[42,1],[42,6],[42,5],[43,1],[43,3],[45,4],[45,4],[45,6],[45,4],[45,4],[45,4],[45,8],[45,4],[45,4],[45,4],[45,6],[45,4],[45,4],[45,4],[45,4],[45,4],[45,1],[41,2],[41,3],[41,3],[41,1],[41,3],[41,4],[76,1],[76,2],[76,1],[76,1],[72,1],[72,2],[73,3],[30,1],[30,2],[30,1],[30,1],[83,1],[83,1],[83,1],[83,1],[83,1],[83,1],[83,1],[83,1],[83,1],[83,1],[83,1],[28,1],[28,2],[28,1],[28,1],[24,5],[25,5],[26,2],[26,4],[26,3],[26,5],[26,3],[26,5],[26,5],[26,7],[26,2],[26,4],[26,2],[26,4],[26,4],[26,6],[22,5],[23,5],[23,5],[23,9],[23,9],[23,7],[23,7],[103,1],[103,3],[92,1],[92,3],[107,1],[107,2],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[108,1],[113,1],[113,1],[113,1],[113,1],[113,1],[113,1],[113,1],[113,1],[113,1],[113,1],[113,1],[82,1],[82,1],[82,1],[82,1],[91,1],[91,1],[91,1],[91,1],[91,1],[91,1],[91,1],[91,1],[91,1],[91,1],[91,1],[79,1],[79,1],[120,1],[120,1],[120,1],[120,1],[120,1],[120,1],[120,1],[120,1],[120,1],[120,1],[120,1],[47,1],[47,2],[101,1],[101,2],[33,1],[33,1],[33,1],[33,1],[33,1]],performAction:o(function(Ct,Ot,Ft,Rt,gr,Se,ti){var Ie=Se.length-1;switch(gr){case 2:this.$=[];break;case 3:(!Array.isArray(Se[Ie])||Se[Ie].length>0)&&Se[Ie-1].push(Se[Ie]),this.$=Se[Ie-1];break;case 4:case 183:this.$=Se[Ie];break;case 11:Rt.setDirection("TB"),this.$="TB";break;case 12:Rt.setDirection(Se[Ie-1]),this.$=Se[Ie-1];break;case 27:this.$=Se[Ie-1].nodes;break;case 28:case 29:case 30:case 31:case 32:this.$=[];break;case 33:this.$=Rt.addSubGraph(Se[Ie-6],Se[Ie-1],Se[Ie-4]);break;case 34:this.$=Rt.addSubGraph(Se[Ie-3],Se[Ie-1],Se[Ie-3]);break;case 35:this.$=Rt.addSubGraph(void 0,Se[Ie-1],void 0);break;case 37:this.$=Se[Ie].trim(),Rt.setAccTitle(this.$);break;case 38:case 39:this.$=Se[Ie].trim(),Rt.setAccDescription(this.$);break;case 43:this.$=Se[Ie-1]+Se[Ie];break;case 44:this.$=Se[Ie];break;case 45:Rt.addVertex(Se[Ie-1][Se[Ie-1].length-1],void 0,void 0,void 0,void 0,void 0,void 0,Se[Ie]),Rt.addLink(Se[Ie-3].stmt,Se[Ie-1],Se[Ie-2]),this.$={stmt:Se[Ie-1],nodes:Se[Ie-1].concat(Se[Ie-3].nodes)};break;case 46:Rt.addLink(Se[Ie-2].stmt,Se[Ie],Se[Ie-1]),this.$={stmt:Se[Ie],nodes:Se[Ie].concat(Se[Ie-2].nodes)};break;case 47:Rt.addLink(Se[Ie-3].stmt,Se[Ie-1],Se[Ie-2]),this.$={stmt:Se[Ie-1],nodes:Se[Ie-1].concat(Se[Ie-3].nodes)};break;case 48:this.$={stmt:Se[Ie-1],nodes:Se[Ie-1]};break;case 49:Rt.addVertex(Se[Ie-1][Se[Ie-1].length-1],void 0,void 0,void 0,void 0,void 0,void 0,Se[Ie]),this.$={stmt:Se[Ie-1],nodes:Se[Ie-1],shapeData:Se[Ie]};break;case 50:this.$={stmt:Se[Ie],nodes:Se[Ie]};break;case 51:this.$=[Se[Ie]];break;case 52:Rt.addVertex(Se[Ie-5][Se[Ie-5].length-1],void 0,void 0,void 0,void 0,void 0,void 0,Se[Ie-4]),this.$=Se[Ie-5].concat(Se[Ie]);break;case 53:this.$=Se[Ie-4].concat(Se[Ie]);break;case 54:this.$=Se[Ie];break;case 55:this.$=Se[Ie-2],Rt.setClass(Se[Ie-2],Se[Ie]);break;case 56:this.$=Se[Ie-3],Rt.addVertex(Se[Ie-3],Se[Ie-1],"square");break;case 57:this.$=Se[Ie-3],Rt.addVertex(Se[Ie-3],Se[Ie-1],"doublecircle");break;case 58:this.$=Se[Ie-5],Rt.addVertex(Se[Ie-5],Se[Ie-2],"circle");break;case 59:this.$=Se[Ie-3],Rt.addVertex(Se[Ie-3],Se[Ie-1],"ellipse");break;case 60:this.$=Se[Ie-3],Rt.addVertex(Se[Ie-3],Se[Ie-1],"stadium");break;case 61:this.$=Se[Ie-3],Rt.addVertex(Se[Ie-3],Se[Ie-1],"subroutine");break;case 62:this.$=Se[Ie-7],Rt.addVertex(Se[Ie-7],Se[Ie-1],"rect",void 0,void 0,void 0,Object.fromEntries([[Se[Ie-5],Se[Ie-3]]]));break;case 63:this.$=Se[Ie-3],Rt.addVertex(Se[Ie-3],Se[Ie-1],"cylinder");break;case 64:this.$=Se[Ie-3],Rt.addVertex(Se[Ie-3],Se[Ie-1],"round");break;case 65:this.$=Se[Ie-3],Rt.addVertex(Se[Ie-3],Se[Ie-1],"diamond");break;case 66:this.$=Se[Ie-5],Rt.addVertex(Se[Ie-5],Se[Ie-2],"hexagon");break;case 67:this.$=Se[Ie-3],Rt.addVertex(Se[Ie-3],Se[Ie-1],"odd");break;case 68:this.$=Se[Ie-3],Rt.addVertex(Se[Ie-3],Se[Ie-1],"trapezoid");break;case 69:this.$=Se[Ie-3],Rt.addVertex(Se[Ie-3],Se[Ie-1],"inv_trapezoid");break;case 70:this.$=Se[Ie-3],Rt.addVertex(Se[Ie-3],Se[Ie-1],"lean_right");break;case 71:this.$=Se[Ie-3],Rt.addVertex(Se[Ie-3],Se[Ie-1],"lean_left");break;case 72:this.$=Se[Ie],Rt.addVertex(Se[Ie]);break;case 73:Se[Ie-1].text=Se[Ie],this.$=Se[Ie-1];break;case 74:case 75:Se[Ie-2].text=Se[Ie-1],this.$=Se[Ie-2];break;case 76:this.$=Se[Ie];break;case 77:var Nr=Rt.destructLink(Se[Ie],Se[Ie-2]);this.$={type:Nr.type,stroke:Nr.stroke,length:Nr.length,text:Se[Ie-1]};break;case 78:var Nr=Rt.destructLink(Se[Ie],Se[Ie-2]);this.$={type:Nr.type,stroke:Nr.stroke,length:Nr.length,text:Se[Ie-1],id:Se[Ie-3]};break;case 79:this.$={text:Se[Ie],type:"text"};break;case 80:this.$={text:Se[Ie-1].text+""+Se[Ie],type:Se[Ie-1].type};break;case 81:this.$={text:Se[Ie],type:"string"};break;case 82:this.$={text:Se[Ie],type:"markdown"};break;case 83:var Nr=Rt.destructLink(Se[Ie]);this.$={type:Nr.type,stroke:Nr.stroke,length:Nr.length};break;case 84:var Nr=Rt.destructLink(Se[Ie]);this.$={type:Nr.type,stroke:Nr.stroke,length:Nr.length,id:Se[Ie-1]};break;case 85:this.$=Se[Ie-1];break;case 86:this.$={text:Se[Ie],type:"text"};break;case 87:this.$={text:Se[Ie-1].text+""+Se[Ie],type:Se[Ie-1].type};break;case 88:this.$={text:Se[Ie],type:"string"};break;case 89:case 104:this.$={text:Se[Ie],type:"markdown"};break;case 101:this.$={text:Se[Ie],type:"text"};break;case 102:this.$={text:Se[Ie-1].text+""+Se[Ie],type:Se[Ie-1].type};break;case 103:this.$={text:Se[Ie],type:"text"};break;case 105:this.$=Se[Ie-4],Rt.addClass(Se[Ie-2],Se[Ie]);break;case 106:this.$=Se[Ie-4],Rt.setClass(Se[Ie-2],Se[Ie]);break;case 107:case 115:this.$=Se[Ie-1],Rt.setClickEvent(Se[Ie-1],Se[Ie]);break;case 108:case 116:this.$=Se[Ie-3],Rt.setClickEvent(Se[Ie-3],Se[Ie-2]),Rt.setTooltip(Se[Ie-3],Se[Ie]);break;case 109:this.$=Se[Ie-2],Rt.setClickEvent(Se[Ie-2],Se[Ie-1],Se[Ie]);break;case 110:this.$=Se[Ie-4],Rt.setClickEvent(Se[Ie-4],Se[Ie-3],Se[Ie-2]),Rt.setTooltip(Se[Ie-4],Se[Ie]);break;case 111:this.$=Se[Ie-2],Rt.setLink(Se[Ie-2],Se[Ie]);break;case 112:this.$=Se[Ie-4],Rt.setLink(Se[Ie-4],Se[Ie-2]),Rt.setTooltip(Se[Ie-4],Se[Ie]);break;case 113:this.$=Se[Ie-4],Rt.setLink(Se[Ie-4],Se[Ie-2],Se[Ie]);break;case 114:this.$=Se[Ie-6],Rt.setLink(Se[Ie-6],Se[Ie-4],Se[Ie]),Rt.setTooltip(Se[Ie-6],Se[Ie-2]);break;case 117:this.$=Se[Ie-1],Rt.setLink(Se[Ie-1],Se[Ie]);break;case 118:this.$=Se[Ie-3],Rt.setLink(Se[Ie-3],Se[Ie-2]),Rt.setTooltip(Se[Ie-3],Se[Ie]);break;case 119:this.$=Se[Ie-3],Rt.setLink(Se[Ie-3],Se[Ie-2],Se[Ie]);break;case 120:this.$=Se[Ie-5],Rt.setLink(Se[Ie-5],Se[Ie-4],Se[Ie]),Rt.setTooltip(Se[Ie-5],Se[Ie-2]);break;case 121:this.$=Se[Ie-4],Rt.addVertex(Se[Ie-2],void 0,void 0,Se[Ie]);break;case 122:this.$=Se[Ie-4],Rt.updateLink([Se[Ie-2]],Se[Ie]);break;case 123:this.$=Se[Ie-4],Rt.updateLink(Se[Ie-2],Se[Ie]);break;case 124:this.$=Se[Ie-8],Rt.updateLinkInterpolate([Se[Ie-6]],Se[Ie-2]),Rt.updateLink([Se[Ie-6]],Se[Ie]);break;case 125:this.$=Se[Ie-8],Rt.updateLinkInterpolate(Se[Ie-6],Se[Ie-2]),Rt.updateLink(Se[Ie-6],Se[Ie]);break;case 126:this.$=Se[Ie-6],Rt.updateLinkInterpolate([Se[Ie-4]],Se[Ie]);break;case 127:this.$=Se[Ie-6],Rt.updateLinkInterpolate(Se[Ie-4],Se[Ie]);break;case 128:case 130:this.$=[Se[Ie]];break;case 129:case 131:Se[Ie-2].push(Se[Ie]),this.$=Se[Ie-2];break;case 133:this.$=Se[Ie-1]+Se[Ie];break;case 181:this.$=Se[Ie];break;case 182:this.$=Se[Ie-1]+""+Se[Ie];break;case 184:this.$=Se[Ie-1]+""+Se[Ie];break;case 185:this.$={stmt:"dir",value:"TB"};break;case 186:this.$={stmt:"dir",value:"BT"};break;case 187:this.$={stmt:"dir",value:"RL"};break;case 188:this.$={stmt:"dir",value:"LR"};break;case 189:this.$={stmt:"dir",value:"TD"};break}},"anonymous"),table:[{3:1,4:2,9:t,10:r,12:n},{1:[3]},e(i,a,{5:6}),{4:7,9:t,10:r,12:n},{4:8,9:t,10:r,12:n},{13:[1,9],14:[1,10]},{1:[2,1],6:11,7:12,8:s,9:l,10:u,11:h,20:17,22:18,23:19,24:20,25:21,26:22,27:d,33:24,34:f,36:p,38:m,42:28,43:39,44:g,45:40,47:41,60:y,84:v,85:x,86:b,87:T,88:k,89:C,102:w,105:S,106:R,109:L,111:N,113:42,114:I,115:_,116:A,121:M,122:D,123:P,124:B,125:O},e(i,[2,9]),e(i,[2,10]),e(i,[2,11]),{8:[1,55],9:[1,56],10:$,15:54,18:57},e(V,[2,3]),e(V,[2,4]),e(V,[2,5]),e(V,[2,6]),e(V,[2,7]),e(V,[2,8]),{8:G,9:z,11:W,21:59,41:60,72:64,75:[1,65],77:[1,67],78:[1,66]},{8:G,9:z,11:W,21:68},{8:G,9:z,11:W,21:69},{8:G,9:z,11:W,21:70},{8:G,9:z,11:W,21:71},{8:G,9:z,11:W,21:72},{8:G,9:z,10:[1,73],11:W,21:74},e(V,[2,36]),{35:[1,75]},{37:[1,76]},e(V,[2,39]),e(H,[2,50],{18:77,39:78,10:$,40:j}),{10:[1,80]},{10:[1,81]},{10:[1,82]},{10:[1,83]},{14:Q,44:U,60:oe,80:[1,87],89:te,95:[1,84],97:[1,85],101:86,105:le,106:ie,109:ae,111:Re,114:be,115:Pe,116:Ge,120:88},e(V,[2,185]),e(V,[2,186]),e(V,[2,187]),e(V,[2,188]),e(V,[2,189]),e(Oe,[2,51]),e(Oe,[2,54],{46:[1,100]}),e(ue,[2,72],{113:113,29:[1,101],44:g,48:[1,102],50:[1,103],52:[1,104],54:[1,105],56:[1,106],58:[1,107],60:y,63:[1,108],65:[1,109],67:[1,110],68:[1,111],70:[1,112],89:C,102:w,105:S,106:R,109:L,111:N,114:I,115:_,116:A}),e(ye,[2,181]),e(ye,[2,142]),e(ye,[2,143]),e(ye,[2,144]),e(ye,[2,145]),e(ye,[2,146]),e(ye,[2,147]),e(ye,[2,148]),e(ye,[2,149]),e(ye,[2,150]),e(ye,[2,151]),e(ye,[2,152]),e(i,[2,12]),e(i,[2,18]),e(i,[2,19]),{9:[1,114]},e(ke,[2,26],{18:115,10:$}),e(V,[2,27]),{42:116,43:39,44:g,45:40,47:41,60:y,89:C,102:w,105:S,106:R,109:L,111:N,113:42,114:I,115:_,116:A},e(V,[2,40]),e(V,[2,41]),e(V,[2,42]),e(ce,[2,76],{73:117,62:[1,119],74:[1,118]}),{76:120,79:121,80:re,81:J,116:se,119:ge},{75:[1,126],77:[1,127]},e(Te,[2,83]),e(V,[2,28]),e(V,[2,29]),e(V,[2,30]),e(V,[2,31]),e(V,[2,32]),{10:we,12:Me,14:ve,27:ne,28:128,32:q,44:he,60:X,75:fe,80:[1,130],81:[1,131],83:141,84:K,85:qe,86:_e,87:Be,88:Ne,89:He,90:$e,91:129,105:Xe,109:Fe,111:Ke,114:xe,115:mt,116:Le},e(ft,a,{5:154}),e(V,[2,37]),e(V,[2,38]),e(H,[2,48],{44:wt}),e(H,[2,49],{18:156,10:$,40:zt}),e(Oe,[2,44]),{44:g,47:158,60:y,89:C,102:w,105:S,106:R,109:L,111:N,113:42,114:I,115:_,116:A},{102:[1,159],103:160,105:[1,161]},{44:g,47:162,60:y,89:C,102:w,105:S,106:R,109:L,111:N,113:42,114:I,115:_,116:A},{44:g,47:163,60:y,89:C,102:w,105:S,106:R,109:L,111:N,113:42,114:I,115:_,116:A},e(St,[2,107],{10:[1,164],96:[1,165]}),{80:[1,166]},e(St,[2,115],{120:168,10:[1,167],14:Q,44:U,60:oe,89:te,105:le,106:ie,109:ae,111:Re,114:be,115:Pe,116:Ge}),e(St,[2,117],{10:[1,169]}),e(At,[2,183]),e(At,[2,170]),e(At,[2,171]),e(At,[2,172]),e(At,[2,173]),e(At,[2,174]),e(At,[2,175]),e(At,[2,176]),e(At,[2,177]),e(At,[2,178]),e(At,[2,179]),e(At,[2,180]),{44:g,47:170,60:y,89:C,102:w,105:S,106:R,109:L,111:N,113:42,114:I,115:_,116:A},{30:171,67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},{30:179,67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},{30:181,50:[1,180],67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},{30:182,67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},{30:183,67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},{30:184,67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},{109:[1,185]},{30:186,67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},{30:187,65:[1,188],67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},{30:189,67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},{30:190,67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},{30:191,67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},e(ye,[2,182]),e(i,[2,20]),e(ke,[2,25]),e(H,[2,46],{39:192,18:193,10:$,40:j}),e(ce,[2,73],{10:[1,194]}),{10:[1,195]},{30:196,67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},{77:[1,197],79:198,116:se,119:ge},e(tt,[2,79]),e(tt,[2,81]),e(tt,[2,82]),e(tt,[2,168]),e(tt,[2,169]),{76:199,79:121,80:re,81:J,116:se,119:ge},e(Te,[2,84]),{8:G,9:z,10:we,11:W,12:Me,14:ve,21:201,27:ne,29:[1,200],32:q,44:he,60:X,75:fe,83:141,84:K,85:qe,86:_e,87:Be,88:Ne,89:He,90:$e,91:202,105:Xe,109:Fe,111:Ke,114:xe,115:mt,116:Le},e(at,[2,101]),e(at,[2,103]),e(at,[2,104]),e(at,[2,157]),e(at,[2,158]),e(at,[2,159]),e(at,[2,160]),e(at,[2,161]),e(at,[2,162]),e(at,[2,163]),e(at,[2,164]),e(at,[2,165]),e(at,[2,166]),e(at,[2,167]),e(at,[2,90]),e(at,[2,91]),e(at,[2,92]),e(at,[2,93]),e(at,[2,94]),e(at,[2,95]),e(at,[2,96]),e(at,[2,97]),e(at,[2,98]),e(at,[2,99]),e(at,[2,100]),{6:11,7:12,8:s,9:l,10:u,11:h,20:17,22:18,23:19,24:20,25:21,26:22,27:d,32:[1,203],33:24,34:f,36:p,38:m,42:28,43:39,44:g,45:40,47:41,60:y,84:v,85:x,86:b,87:T,88:k,89:C,102:w,105:S,106:R,109:L,111:N,113:42,114:I,115:_,116:A,121:M,122:D,123:P,124:B,125:O},{10:$,18:204},{44:[1,205]},e(Oe,[2,43]),{10:[1,206],44:g,60:y,89:C,102:w,105:S,106:R,109:L,111:N,113:113,114:I,115:_,116:A},{10:[1,207]},{10:[1,208],106:[1,209]},e(ot,[2,128]),{10:[1,210],44:g,60:y,89:C,102:w,105:S,106:R,109:L,111:N,113:113,114:I,115:_,116:A},{10:[1,211],44:g,60:y,89:C,102:w,105:S,106:R,109:L,111:N,113:113,114:I,115:_,116:A},{80:[1,212]},e(St,[2,109],{10:[1,213]}),e(St,[2,111],{10:[1,214]}),{80:[1,215]},e(At,[2,184]),{80:[1,216],98:[1,217]},e(Oe,[2,55],{113:113,44:g,60:y,89:C,102:w,105:S,106:R,109:L,111:N,114:I,115:_,116:A}),{31:[1,218],67:bt,82:219,116:gt,117:Ze,118:Ee},e(Wt,[2,86]),e(Wt,[2,88]),e(Wt,[2,89]),e(Wt,[2,153]),e(Wt,[2,154]),e(Wt,[2,155]),e(Wt,[2,156]),{49:[1,220],67:bt,82:219,116:gt,117:Ze,118:Ee},{30:221,67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},{51:[1,222],67:bt,82:219,116:gt,117:Ze,118:Ee},{53:[1,223],67:bt,82:219,116:gt,117:Ze,118:Ee},{55:[1,224],67:bt,82:219,116:gt,117:Ze,118:Ee},{57:[1,225],67:bt,82:219,116:gt,117:Ze,118:Ee},{60:[1,226]},{64:[1,227],67:bt,82:219,116:gt,117:Ze,118:Ee},{66:[1,228],67:bt,82:219,116:gt,117:Ze,118:Ee},{30:229,67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},{31:[1,230],67:bt,82:219,116:gt,117:Ze,118:Ee},{67:bt,69:[1,231],71:[1,232],82:219,116:gt,117:Ze,118:Ee},{67:bt,69:[1,234],71:[1,233],82:219,116:gt,117:Ze,118:Ee},e(H,[2,45],{18:156,10:$,40:zt}),e(H,[2,47],{44:wt}),e(ce,[2,75]),e(ce,[2,74]),{62:[1,235],67:bt,82:219,116:gt,117:Ze,118:Ee},e(ce,[2,77]),e(tt,[2,80]),{77:[1,236],79:198,116:se,119:ge},{30:237,67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},e(ft,a,{5:238}),e(at,[2,102]),e(V,[2,35]),{43:239,44:g,45:40,47:41,60:y,89:C,102:w,105:S,106:R,109:L,111:N,113:42,114:I,115:_,116:A},{10:$,18:240},{10:Bt,60:qt,84:vr,92:241,105:Tt,107:242,108:243,109:De,110:it,111:We,112:rt},{10:Bt,60:qt,84:vr,92:252,104:[1,253],105:Tt,107:242,108:243,109:De,110:it,111:We,112:rt},{10:Bt,60:qt,84:vr,92:254,104:[1,255],105:Tt,107:242,108:243,109:De,110:it,111:We,112:rt},{105:[1,256]},{10:Bt,60:qt,84:vr,92:257,105:Tt,107:242,108:243,109:De,110:it,111:We,112:rt},{44:g,47:258,60:y,89:C,102:w,105:S,106:R,109:L,111:N,113:42,114:I,115:_,116:A},e(St,[2,108]),{80:[1,259]},{80:[1,260],98:[1,261]},e(St,[2,116]),e(St,[2,118],{10:[1,262]}),e(St,[2,119]),e(ue,[2,56]),e(Wt,[2,87]),e(ue,[2,57]),{51:[1,263],67:bt,82:219,116:gt,117:Ze,118:Ee},e(ue,[2,64]),e(ue,[2,59]),e(ue,[2,60]),e(ue,[2,61]),{109:[1,264]},e(ue,[2,63]),e(ue,[2,65]),{66:[1,265],67:bt,82:219,116:gt,117:Ze,118:Ee},e(ue,[2,67]),e(ue,[2,68]),e(ue,[2,70]),e(ue,[2,69]),e(ue,[2,71]),e([10,44,60,89,102,105,106,109,111,114,115,116],[2,85]),e(ce,[2,78]),{31:[1,266],67:bt,82:219,116:gt,117:Ze,118:Ee},{6:11,7:12,8:s,9:l,10:u,11:h,20:17,22:18,23:19,24:20,25:21,26:22,27:d,32:[1,267],33:24,34:f,36:p,38:m,42:28,43:39,44:g,45:40,47:41,60:y,84:v,85:x,86:b,87:T,88:k,89:C,102:w,105:S,106:R,109:L,111:N,113:42,114:I,115:_,116:A,121:M,122:D,123:P,124:B,125:O},e(Oe,[2,53]),{43:268,44:g,45:40,47:41,60:y,89:C,102:w,105:S,106:R,109:L,111:N,113:42,114:I,115:_,116:A},e(St,[2,121],{106:yt}),e(Yt,[2,130],{108:270,10:Bt,60:qt,84:vr,105:Tt,109:De,110:it,111:We,112:rt}),e(Ht,[2,132]),e(Ht,[2,134]),e(Ht,[2,135]),e(Ht,[2,136]),e(Ht,[2,137]),e(Ht,[2,138]),e(Ht,[2,139]),e(Ht,[2,140]),e(Ht,[2,141]),e(St,[2,122],{106:yt}),{10:[1,271]},e(St,[2,123],{106:yt}),{10:[1,272]},e(ot,[2,129]),e(St,[2,105],{106:yt}),e(St,[2,106],{113:113,44:g,60:y,89:C,102:w,105:S,106:R,109:L,111:N,114:I,115:_,116:A}),e(St,[2,110]),e(St,[2,112],{10:[1,273]}),e(St,[2,113]),{98:[1,274]},{51:[1,275]},{62:[1,276]},{66:[1,277]},{8:G,9:z,11:W,21:278},e(V,[2,34]),e(Oe,[2,52]),{10:Bt,60:qt,84:vr,105:Tt,107:279,108:243,109:De,110:it,111:We,112:rt},e(Ht,[2,133]),{14:Q,44:U,60:oe,89:te,101:280,105:le,106:ie,109:ae,111:Re,114:be,115:Pe,116:Ge,120:88},{14:Q,44:U,60:oe,89:te,101:281,105:le,106:ie,109:ae,111:Re,114:be,115:Pe,116:Ge,120:88},{98:[1,282]},e(St,[2,120]),e(ue,[2,58]),{30:283,67:bt,80:me,81:lt,82:172,116:gt,117:Ze,118:Ee},e(ue,[2,66]),e(ft,a,{5:284}),e(Yt,[2,131],{108:270,10:Bt,60:qt,84:vr,105:Tt,109:De,110:it,111:We,112:rt}),e(St,[2,126],{120:168,10:[1,285],14:Q,44:U,60:oe,89:te,105:le,106:ie,109:ae,111:Re,114:be,115:Pe,116:Ge}),e(St,[2,127],{120:168,10:[1,286],14:Q,44:U,60:oe,89:te,105:le,106:ie,109:ae,111:Re,114:be,115:Pe,116:Ge}),e(St,[2,114]),{31:[1,287],67:bt,82:219,116:gt,117:Ze,118:Ee},{6:11,7:12,8:s,9:l,10:u,11:h,20:17,22:18,23:19,24:20,25:21,26:22,27:d,32:[1,288],33:24,34:f,36:p,38:m,42:28,43:39,44:g,45:40,47:41,60:y,84:v,85:x,86:b,87:T,88:k,89:C,102:w,105:S,106:R,109:L,111:N,113:42,114:I,115:_,116:A,121:M,122:D,123:P,124:B,125:O},{10:Bt,60:qt,84:vr,92:289,105:Tt,107:242,108:243,109:De,110:it,111:We,112:rt},{10:Bt,60:qt,84:vr,92:290,105:Tt,107:242,108:243,109:De,110:it,111:We,112:rt},e(ue,[2,62]),e(V,[2,33]),e(St,[2,124],{106:yt}),e(St,[2,125],{106:yt})],defaultActions:{},parseError:o(function(Ct,Ot){if(Ot.recoverable)this.trace(Ct);else{var Ft=new Error(Ct);throw Ft.hash=Ot,Ft}},"parseError"),parse:o(function(Ct){var Ot=this,Ft=[0],Rt=[],gr=[null],Se=[],ti=this.table,Ie="",Nr=0,Pa=0,Ku=0,B0=2,$0=1,fk=Se.slice.call(arguments,1),Pi=Object.create(this.lexer),Mc={yy:{}};for(var Px in this.yy)Object.prototype.hasOwnProperty.call(this.yy,Px)&&(Mc.yy[Px]=this.yy[Px]);Pi.setInput(Ct,Mc.yy),Mc.yy.lexer=Pi,Mc.yy.parser=this,typeof Pi.yylloc>"u"&&(Pi.yylloc={});var Td=Pi.yylloc;Se.push(Td);var pk=Pi.options&&Pi.options.ranges;typeof Mc.yy.parseError=="function"?this.parseError=Mc.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function js(Do){Ft.length=Ft.length-2*Do,gr.length=gr.length-Do,Se.length=Se.length-Do}o(js,"popStack");function LD(){var Do;return Do=Rt.pop()||Pi.lex()||$0,typeof Do!="number"&&(Do instanceof Array&&(Rt=Do,Do=Rt.pop()),Do=Ot.symbols_[Do]||Do),Do}o(LD,"lex");for(var ys,DD,Bp,nl,iOt,ID,F0={},mk,Zu,$X,gk;;){if(Bp=Ft[Ft.length-1],this.defaultActions[Bp]?nl=this.defaultActions[Bp]:((ys===null||typeof ys>"u")&&(ys=LD()),nl=ti[Bp]&&ti[Bp][ys]),typeof nl>"u"||!nl.length||!nl[0]){var MD="";gk=[];for(mk in ti[Bp])this.terminals_[mk]&&mk>B0&&gk.push("'"+this.terminals_[mk]+"'");Pi.showPosition?MD="Parse error on line "+(Nr+1)+`: +`+Pi.showPosition()+` +Expecting `+gk.join(", ")+", got '"+(this.terminals_[ys]||ys)+"'":MD="Parse error on line "+(Nr+1)+": Unexpected "+(ys==$0?"end of input":"'"+(this.terminals_[ys]||ys)+"'"),this.parseError(MD,{text:Pi.match,token:this.terminals_[ys]||ys,line:Pi.yylineno,loc:Td,expected:gk})}if(nl[0]instanceof Array&&nl.length>1)throw new Error("Parse Error: multiple actions possible at state: "+Bp+", token: "+ys);switch(nl[0]){case 1:Ft.push(ys),gr.push(Pi.yytext),Se.push(Pi.yylloc),Ft.push(nl[1]),ys=null,DD?(ys=DD,DD=null):(Pa=Pi.yyleng,Ie=Pi.yytext,Nr=Pi.yylineno,Td=Pi.yylloc,Ku>0&&Ku--);break;case 2:if(Zu=this.productions_[nl[1]][1],F0.$=gr[gr.length-Zu],F0._$={first_line:Se[Se.length-(Zu||1)].first_line,last_line:Se[Se.length-1].last_line,first_column:Se[Se.length-(Zu||1)].first_column,last_column:Se[Se.length-1].last_column},pk&&(F0._$.range=[Se[Se.length-(Zu||1)].range[0],Se[Se.length-1].range[1]]),ID=this.performAction.apply(F0,[Ie,Pa,Nr,Mc.yy,nl[1],gr,Se].concat(fk)),typeof ID<"u")return ID;Zu&&(Ft=Ft.slice(0,-1*Zu*2),gr=gr.slice(0,-1*Zu),Se=Se.slice(0,-1*Zu)),Ft.push(this.productions_[nl[1]][0]),gr.push(F0.$),Se.push(F0._$),$X=ti[Ft[Ft.length-2]][Ft[Ft.length-1]],Ft.push($X);break;case 3:return!0}}return!0},"parse")},Hr=(function(){var kt={EOF:1,parseError:o(function(Ot,Ft){if(this.yy.parser)this.yy.parser.parseError(Ot,Ft);else throw new Error(Ot)},"parseError"),setInput:o(function(Ct,Ot){return this.yy=Ot||this.yy||{},this._input=Ct,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var Ct=this._input[0];this.yytext+=Ct,this.yyleng++,this.offset++,this.match+=Ct,this.matched+=Ct;var Ot=Ct.match(/(?:\r\n?|\n).*/g);return Ot?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),Ct},"input"),unput:o(function(Ct){var Ot=Ct.length,Ft=Ct.split(/(?:\r\n?|\n)/g);this._input=Ct+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-Ot),this.offset-=Ot;var Rt=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),Ft.length-1&&(this.yylineno-=Ft.length-1);var gr=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:Ft?(Ft.length===Rt.length?this.yylloc.first_column:0)+Rt[Rt.length-Ft.length].length-Ft[0].length:this.yylloc.first_column-Ot},this.options.ranges&&(this.yylloc.range=[gr[0],gr[0]+this.yyleng-Ot]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(Ct){this.unput(this.match.slice(Ct))},"less"),pastInput:o(function(){var Ct=this.matched.substr(0,this.matched.length-this.match.length);return(Ct.length>20?"...":"")+Ct.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var Ct=this.match;return Ct.length<20&&(Ct+=this._input.substr(0,20-Ct.length)),(Ct.substr(0,20)+(Ct.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var Ct=this.pastInput(),Ot=new Array(Ct.length+1).join("-");return Ct+this.upcomingInput()+` +`+Ot+"^"},"showPosition"),test_match:o(function(Ct,Ot){var Ft,Rt,gr;if(this.options.backtrack_lexer&&(gr={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(gr.yylloc.range=this.yylloc.range.slice(0))),Rt=Ct[0].match(/(?:\r\n?|\n).*/g),Rt&&(this.yylineno+=Rt.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:Rt?Rt[Rt.length-1].length-Rt[Rt.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+Ct[0].length},this.yytext+=Ct[0],this.match+=Ct[0],this.matches=Ct,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(Ct[0].length),this.matched+=Ct[0],Ft=this.performAction.call(this,this.yy,this,Ot,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),Ft)return Ft;if(this._backtrack){for(var Se in gr)this[Se]=gr[Se];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var Ct,Ot,Ft,Rt;this._more||(this.yytext="",this.match="");for(var gr=this._currentRules(),Se=0;SeOt[0].length)){if(Ot=Ft,Rt=Se,this.options.backtrack_lexer){if(Ct=this.test_match(Ft,gr[Se]),Ct!==!1)return Ct;if(this._backtrack){Ot=!1;continue}else return!1}else if(!this.options.flex)break}return Ot?(Ct=this.test_match(Ot,gr[Rt]),Ct!==!1?Ct:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var Ot=this.next();return Ot||this.lex()},"lex"),begin:o(function(Ot){this.conditionStack.push(Ot)},"begin"),popState:o(function(){var Ot=this.conditionStack.length-1;return Ot>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(Ot){return Ot=this.conditionStack.length-1-Math.abs(Ot||0),Ot>=0?this.conditionStack[Ot]:"INITIAL"},"topState"),pushState:o(function(Ot){this.begin(Ot)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{},performAction:o(function(Ot,Ft,Rt,gr){var Se=gr;switch(Rt){case 0:return this.begin("acc_title"),34;break;case 1:return this.popState(),"acc_title_value";break;case 2:return this.begin("acc_descr"),36;break;case 3:return this.popState(),"acc_descr_value";break;case 4:this.begin("acc_descr_multiline");break;case 5:this.popState();break;case 6:return"acc_descr_multiline_value";case 7:return this.pushState("shapeData"),Ft.yytext="",40;break;case 8:return this.pushState("shapeDataStr"),40;break;case 9:return this.popState(),40;break;case 10:let ti=/\n\s*/g;return Ft.yytext=Ft.yytext.replace(ti,"
"),40;break;case 11:return 40;case 12:this.popState();break;case 13:this.begin("callbackname");break;case 14:this.popState();break;case 15:this.popState(),this.begin("callbackargs");break;case 16:return 95;case 17:this.popState();break;case 18:return 96;case 19:return"MD_STR";case 20:this.popState();break;case 21:this.begin("md_string");break;case 22:return"STR";case 23:this.popState();break;case 24:this.pushState("string");break;case 25:return 84;case 26:return 102;case 27:return 85;case 28:return 104;case 29:return 86;case 30:return 87;case 31:return 97;case 32:this.begin("click");break;case 33:this.popState();break;case 34:return 88;case 35:return Ot.lex.firstGraph()&&this.begin("dir"),12;break;case 36:return Ot.lex.firstGraph()&&this.begin("dir"),12;break;case 37:return Ot.lex.firstGraph()&&this.begin("dir"),12;break;case 38:return Ot.lex.firstGraph()&&this.begin("dir"),12;break;case 39:return 27;case 40:return 32;case 41:return 98;case 42:return 98;case 43:return 98;case 44:return 98;case 45:return this.popState(),13;break;case 46:return this.popState(),14;break;case 47:return this.popState(),14;break;case 48:return this.popState(),14;break;case 49:return this.popState(),14;break;case 50:return this.popState(),14;break;case 51:return this.popState(),14;break;case 52:return this.popState(),14;break;case 53:return this.popState(),14;break;case 54:return this.popState(),14;break;case 55:return this.popState(),14;break;case 56:return 121;case 57:return 122;case 58:return 123;case 59:return 124;case 60:return 125;case 61:return 78;case 62:return 105;case 63:return 111;case 64:return 46;case 65:return 60;case 66:return 44;case 67:return 8;case 68:return 106;case 69:return 115;case 70:return this.popState(),77;break;case 71:return this.pushState("edgeText"),75;break;case 72:return 119;case 73:return this.popState(),77;break;case 74:return this.pushState("thickEdgeText"),75;break;case 75:return 119;case 76:return this.popState(),77;break;case 77:return this.pushState("dottedEdgeText"),75;break;case 78:return 119;case 79:return 77;case 80:return this.popState(),53;break;case 81:return"TEXT";case 82:return this.pushState("ellipseText"),52;break;case 83:return this.popState(),55;break;case 84:return this.pushState("text"),54;break;case 85:return this.popState(),57;break;case 86:return this.pushState("text"),56;break;case 87:return 58;case 88:return this.pushState("text"),67;break;case 89:return this.popState(),64;break;case 90:return this.pushState("text"),63;break;case 91:return this.popState(),49;break;case 92:return this.pushState("text"),48;break;case 93:return this.popState(),69;break;case 94:return this.popState(),71;break;case 95:return 117;case 96:return this.pushState("trapText"),68;break;case 97:return this.pushState("trapText"),70;break;case 98:return 118;case 99:return 67;case 100:return 90;case 101:return"SEP";case 102:return 89;case 103:return 115;case 104:return 111;case 105:return 44;case 106:return 109;case 107:return 114;case 108:return 116;case 109:return this.popState(),62;break;case 110:return this.pushState("text"),62;break;case 111:return this.popState(),51;break;case 112:return this.pushState("text"),50;break;case 113:return this.popState(),31;break;case 114:return this.pushState("text"),29;break;case 115:return this.popState(),66;break;case 116:return this.pushState("text"),65;break;case 117:return"TEXT";case 118:return"QUOTE";case 119:return 9;case 120:return 10;case 121:return 11}},"anonymous"),rules:[/^(?:accTitle\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*\{\s*)/,/^(?:[\}])/,/^(?:[^\}]*)/,/^(?:@\{)/,/^(?:["])/,/^(?:["])/,/^(?:[^\"]+)/,/^(?:[^}^"]+)/,/^(?:\})/,/^(?:call[\s]+)/,/^(?:\([\s]*\))/,/^(?:\()/,/^(?:[^(]*)/,/^(?:\))/,/^(?:[^)]*)/,/^(?:[^`"]+)/,/^(?:[`]["])/,/^(?:["][`])/,/^(?:[^"]+)/,/^(?:["])/,/^(?:["])/,/^(?:style\b)/,/^(?:default\b)/,/^(?:linkStyle\b)/,/^(?:interpolate\b)/,/^(?:classDef\b)/,/^(?:class\b)/,/^(?:href[\s])/,/^(?:click[\s]+)/,/^(?:[\s\n])/,/^(?:[^\s\n]*)/,/^(?:flowchart-elk\b)/,/^(?:swimlane-beta\b)/,/^(?:graph\b)/,/^(?:flowchart\b)/,/^(?:subgraph\b)/,/^(?:end\b\s*)/,/^(?:_self\b)/,/^(?:_blank\b)/,/^(?:_parent\b)/,/^(?:_top\b)/,/^(?:(\r?\n)*\s*\n)/,/^(?:\s*LR\b)/,/^(?:\s*RL\b)/,/^(?:\s*TB\b)/,/^(?:\s*BT\b)/,/^(?:\s*TD\b)/,/^(?:\s*BR\b)/,/^(?:\s*<)/,/^(?:\s*>)/,/^(?:\s*\^)/,/^(?:\s*v\b)/,/^(?:.*direction\s+TB[^\n]*)/,/^(?:.*direction\s+BT[^\n]*)/,/^(?:.*direction\s+RL[^\n]*)/,/^(?:.*direction\s+LR[^\n]*)/,/^(?:.*direction\s+TD[^\n]*)/,/^(?:[^\s\"]+@(?=[^\{\"]))/,/^(?:[0-9]+)/,/^(?:#)/,/^(?::::)/,/^(?::)/,/^(?:&)/,/^(?:;)/,/^(?:,)/,/^(?:\*)/,/^(?:\s*[xo<]?--+[-xo>]\s*)/,/^(?:\s*[xo<]?--\s*)/,/^(?:[^-]|-(?!-)+)/,/^(?:\s*[xo<]?==+[=xo>]\s*)/,/^(?:\s*[xo<]?==\s*)/,/^(?:[^=]|=(?!))/,/^(?:\s*[xo<]?-?\.+-[xo>]?\s*)/,/^(?:\s*[xo<]?-\.\s*)/,/^(?:[^\.]|\.(?!))/,/^(?:\s*~~[\~]+\s*)/,/^(?:[-/\)][\)])/,/^(?:[^\(\)\[\]\{\}]|!\)+)/,/^(?:\(-)/,/^(?:\]\))/,/^(?:\(\[)/,/^(?:\]\])/,/^(?:\[\[)/,/^(?:\[\|)/,/^(?:>)/,/^(?:\)\])/,/^(?:\[\()/,/^(?:\)\)\))/,/^(?:\(\(\()/,/^(?:[\\(?=\])][\]])/,/^(?:\/(?=\])\])/,/^(?:\/(?!\])|\\(?!\])|[^\\\[\]\(\)\{\}\/]+)/,/^(?:\[\/)/,/^(?:\[\\)/,/^(?:<)/,/^(?:>)/,/^(?:\^)/,/^(?:\\\|)/,/^(?:v\b)/,/^(?:\*)/,/^(?:#)/,/^(?:&)/,/^(?:([A-Za-z0-9!"\#$%&'*+\.`?\\_\/]|-(?=[^\>\-\.])|(?!))+)/,/^(?:-)/,/^(?:[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|[\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA]|[\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE]|[\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA]|[\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0]|[\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977]|[\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2]|[\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A]|[\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39]|[\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8]|[\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C]|[\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C]|[\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99]|[\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0]|[\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D]|[\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3]|[\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10]|[\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1]|[\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81]|[\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3]|[\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6]|[\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A]|[\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081]|[\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D]|[\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0]|[\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310]|[\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C]|[\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711]|[\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7]|[\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C]|[\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16]|[\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF]|[\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC]|[\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D]|[\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D]|[\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3]|[\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F]|[\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128]|[\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184]|[\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3]|[\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6]|[\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE]|[\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C]|[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D]|[\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC]|[\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B]|[\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788]|[\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805]|[\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB]|[\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28]|[\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5]|[\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4]|[\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E]|[\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D]|[\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36]|[\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D]|[\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC]|[\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]|[\uFFD2-\uFFD7\uFFDA-\uFFDC])/,/^(?:\|)/,/^(?:\|)/,/^(?:\))/,/^(?:\()/,/^(?:\])/,/^(?:\[)/,/^(?:(\}))/,/^(?:\{)/,/^(?:[^\[\]\(\)\{\}\|\"]+)/,/^(?:")/,/^(?:(\r?\n)+)/,/^(?:\s)/,/^(?:$)/],conditions:{shapeDataEndBracket:{rules:[21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},shapeDataStr:{rules:[9,10,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},shapeData:{rules:[8,11,12,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},callbackargs:{rules:[17,18,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},callbackname:{rules:[14,15,16,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},href:{rules:[21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},click:{rules:[21,24,33,34,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},dottedEdgeText:{rules:[21,24,76,78,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},thickEdgeText:{rules:[21,24,73,75,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},edgeText:{rules:[21,24,70,72,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},trapText:{rules:[21,24,79,82,84,86,90,92,93,94,95,96,97,110,112,114,116],inclusive:!1},ellipseText:{rules:[21,24,79,80,81,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},text:{rules:[21,24,79,82,83,84,85,86,89,90,91,92,96,97,109,110,111,112,113,114,115,116,117],inclusive:!1},vertex:{rules:[21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},dir:{rules:[21,24,45,46,47,48,49,50,51,52,53,54,55,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},acc_descr_multiline:{rules:[5,6,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},acc_descr:{rules:[3,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},acc_title:{rules:[1,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},md_string:{rules:[19,20,21,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},string:{rules:[21,22,23,24,79,82,84,86,90,92,96,97,110,112,114,116],inclusive:!1},INITIAL:{rules:[0,2,4,7,13,21,24,25,26,27,28,29,30,31,32,35,36,37,38,39,40,41,42,43,44,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,73,74,76,77,79,82,84,86,87,88,90,92,96,97,98,99,100,101,102,103,104,105,106,107,108,110,112,114,116,118,119,120,121],inclusive:!0}}};return kt})();pr.lexer=Hr;function Er(){this.yy={}}return o(Er,"Parser"),Er.prototype=pr,pr.Parser=Er,new Er})();KF.parser=KF;ZF=KF});var fTe,pTe,mTe=F(()=>{"use strict";dTe();fTe=Object.assign({},ZF);fTe.parse=e=>{let t=e.replace(/}\s*\n/g,`} +`);return ZF.parse(t)};pTe=fTe});var Eu,X1=F(()=>{"use strict";Eu=o(()=>` + /* Font Awesome icon styling - consolidated */ + .label-icon { + display: inline-block; + height: 1em; + overflow: visible; + vertical-align: -0.125em; + } + + .node .label-icon path { + fill: currentColor; + stroke: revert; + stroke-width: revert; + } +`,"getIconStyles")});var r0t,n0t,RA,QF=F(()=>{"use strict";zi();X1();r0t=o((e,t)=>{let r=Fp,n=r(e,"r"),i=r(e,"g"),a=r(e,"b");return Oi(n,i,a,t)},"fade"),n0t=o(e=>`.label { + font-family: ${e.fontFamily}; + color: ${e.nodeTextColor||e.textColor}; + } + .cluster-label text { + fill: ${e.titleColor}; + } + .cluster-label span { + color: ${e.titleColor}; + } + .cluster-label span p { + background-color: transparent; + } + + .label text,span { + fill: ${e.nodeTextColor||e.textColor}; + color: ${e.nodeTextColor||e.textColor}; + } + + .node rect, + .node circle, + .node ellipse, + .node polygon, + .node path { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; + stroke-width: ${e.strokeWidth??1}px; + } + .rough-node .label text , .node .label text, .image-shape .label, .icon-shape .label { + text-anchor: middle; + } + + .node .katex path { + fill: #000; + stroke: #000; + stroke-width: 1px; + } + + .rough-node .label,.node .label, .image-shape .label, .icon-shape .label { + text-align: center; + } + .node.clickable { + cursor: pointer; + } + + + .root .anchor path { + fill: ${e.lineColor} !important; + stroke-width: 0; + stroke: ${e.lineColor}; + } + + .arrowheadPath { + fill: ${e.arrowheadColor}; + } + + .edgePath .path { + stroke: ${e.lineColor}; + stroke-width: ${e.strokeWidth??2}px; + } + + .flowchart-link { + stroke: ${e.lineColor}; + fill: none; + } + + .edgeLabel { + background-color: ${e.edgeLabelBackground}; + p { + background-color: ${e.edgeLabelBackground}; + } + rect { + opacity: 0.5; + background-color: ${e.edgeLabelBackground}; + fill: ${e.edgeLabelBackground}; + } + text-align: center; + } + + /* For html labels only */ + .labelBkg { + background-color: ${r0t(e.edgeLabelBackground,.5)}; + // background-color: + } + + .cluster rect { + fill: ${e.clusterBkg}; + stroke: ${e.clusterBorder}; + stroke-width: 1px; + } + + .cluster text { + fill: ${e.titleColor}; + } + + .cluster span { + color: ${e.titleColor}; + } + /* .cluster div { + color: ${e.titleColor}; + } */ + + div.mermaidTooltip { + position: absolute; + text-align: center; + max-width: 200px; + padding: 2px; + font-family: ${e.fontFamily}; + font-size: 12px; + background: ${e.tertiaryColor}; + border: 1px solid ${e.border2}; + border-radius: 2px; + pointer-events: none; + z-index: 100; + } + + .flowchartTitleText { + text-anchor: middle; + font-size: 18px; + fill: ${e.textColor}; + } + + rect.text { + fill: none; + stroke-width: 0; + } + + .icon-shape, .image-shape { + background-color: ${e.edgeLabelBackground}; + p { + background-color: ${e.edgeLabelBackground}; + padding: 2px; + } + .label rect { + opacity: 0.5; + background-color: ${e.edgeLabelBackground}; + fill: ${e.edgeLabelBackground}; + } + text-align: center; + } + ${Eu()} +`,"getStyles"),RA=n0t});var LA={};ir(LA,{createFlowDiagram:()=>_A,diagram:()=>i0t});var _A,i0t,lC=F(()=>{"use strict";ur();Xt();zce();hTe();mTe();QF();_A=o(({defaultLayout:e,styles:t=RA}={})=>({parser:pTe,get db(){return new W4},renderer:uTe,styles:t,init:o(r=>{r.flowchart||(r.flowchart={});let n=Lk().layout??e??r.layout;n&&ub({layout:n}),r.flowchart.arrowMarkerAbsolute=r.arrowMarkerAbsolute,ub({flowchart:{arrowMarkerAbsolute:r.arrowMarkerAbsolute}})},"init")}),"createFlowDiagram"),i0t=_A()});var h0t,bTe,TTe=F(()=>{"use strict";QF();h0t=o(e=>`${RA(e)} + .swimlane.cluster rect { + stroke: ${e.clusterBorder} !important; + } + [data-look="neo"].cluster rect { + filter: none; + } +`,"getStyles"),bTe=h0t});var CTe={};ir(CTe,{diagram:()=>d0t});var d0t,wTe=F(()=>{"use strict";lC();TTe();d0t=_A({defaultLayout:"swimlane",styles:bTe})});var JF,ETe,ATe=F(()=>{"use strict";JF=(function(){var e=o(function(Pe,Ge,Oe,ue){for(Oe=Oe||{},ue=Pe.length;ue--;Oe[Pe[ue]]=Ge);return Oe},"o"),t=[6,8,10,22,24,26,28,33,34,35,36,37,40,43,44,48,50,51,52],r=[1,10],n=[1,11],i=[1,12],a=[1,13],s=[1,23],l=[1,24],u=[1,25],h=[1,26],d=[1,27],f=[1,19],p=[1,28],m=[1,29],g=[1,20],y=[1,18],v=[1,21],x=[1,22],b=[1,36],T=[1,37],k=[1,38],C=[1,39],w=[1,40],S=[6,8,10,13,15,17,20,21,22,24,26,28,33,34,35,36,37,40,43,44,48,50,51,52,66,67,68,69,70],R=[1,45],L=[1,46],N=[1,55],I=[40,48,50,51,52,71,72],_=[1,66],A=[1,64],M=[1,61],D=[1,65],P=[1,67],B=[6,8,10,13,17,22,24,26,28,33,34,35,36,37,40,41,42,43,44,48,49,50,51,52,66,67,68,69,70],O=[66,67,68,69,70],$=[1,85],V=[1,84],G=[1,82],z=[1,83],W=[6,10,42,47],H=[6,10,13,41,42,47,48,49],j=[1,93],Q=[1,92],U=[1,91],oe=[19,58],te=[1,102],le=[1,101],ie=[19,58,61,63],ae={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,ER_DIAGRAM:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,entityName:11,relSpec:12,COLON:13,role:14,STYLE_SEPARATOR:15,idList:16,BLOCK_START:17,attributes:18,BLOCK_STOP:19,SQS:20,SQE:21,title:22,title_value:23,acc_title:24,acc_title_value:25,acc_descr:26,acc_descr_value:27,acc_descr_multiline_value:28,direction:29,classDefStatement:30,classStatement:31,styleStatement:32,direction_tb:33,direction_bt:34,direction_rl:35,direction_lr:36,CLASSDEF:37,stylesOpt:38,separator:39,UNICODE_TEXT:40,STYLE_TEXT:41,COMMA:42,CLASS:43,STYLE:44,style:45,styleComponent:46,SEMI:47,NUM:48,BRKT:49,ENTITY_NAME:50,DECIMAL_NUM:51,ENTITY_ONE:52,attribute:53,attributeType:54,attributeName:55,attributeKeyTypeList:56,attributeComment:57,ATTRIBUTE_WORD:58,"?":59,attributeKeyType:60,",":61,ATTRIBUTE_KEY:62,COMMENT:63,cardinality:64,relType:65,ZERO_OR_ONE:66,ZERO_OR_MORE:67,ONE_OR_MORE:68,ONLY_ONE:69,MD_PARENT:70,NON_IDENTIFYING:71,IDENTIFYING:72,WORD:73,$accept:0,$end:1},terminals_:{2:"error",4:"ER_DIAGRAM",6:"EOF",8:"SPACE",10:"NEWLINE",13:"COLON",15:"STYLE_SEPARATOR",17:"BLOCK_START",19:"BLOCK_STOP",20:"SQS",21:"SQE",22:"title",23:"title_value",24:"acc_title",25:"acc_title_value",26:"acc_descr",27:"acc_descr_value",28:"acc_descr_multiline_value",33:"direction_tb",34:"direction_bt",35:"direction_rl",36:"direction_lr",37:"CLASSDEF",40:"UNICODE_TEXT",41:"STYLE_TEXT",42:"COMMA",43:"CLASS",44:"STYLE",47:"SEMI",48:"NUM",49:"BRKT",50:"ENTITY_NAME",51:"DECIMAL_NUM",52:"ENTITY_ONE",58:"ATTRIBUTE_WORD",59:"?",61:",",62:"ATTRIBUTE_KEY",63:"COMMENT",66:"ZERO_OR_ONE",67:"ZERO_OR_MORE",68:"ONE_OR_MORE",69:"ONLY_ONE",70:"MD_PARENT",71:"NON_IDENTIFYING",72:"IDENTIFYING",73:"WORD"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,5],[9,9],[9,7],[9,7],[9,4],[9,6],[9,3],[9,5],[9,1],[9,3],[9,7],[9,9],[9,6],[9,8],[9,4],[9,6],[9,2],[9,2],[9,2],[9,1],[9,1],[9,1],[9,1],[9,1],[29,1],[29,1],[29,1],[29,1],[30,4],[16,1],[16,1],[16,3],[16,3],[31,3],[32,4],[38,1],[38,3],[45,1],[45,2],[39,1],[39,1],[39,1],[46,1],[46,1],[46,1],[46,1],[11,1],[11,1],[11,1],[11,1],[11,1],[18,1],[18,2],[53,2],[53,3],[53,3],[53,4],[54,1],[54,2],[55,1],[56,1],[56,3],[60,1],[57,1],[12,3],[64,1],[64,1],[64,1],[64,1],[64,1],[65,1],[65,1],[14,1],[14,1],[14,1]],performAction:o(function(Ge,Oe,ue,ye,ke,ce,re){var J=ce.length-1;switch(ke){case 1:break;case 2:this.$=[];break;case 3:ce[J-1].push(ce[J]),this.$=ce[J-1];break;case 4:case 5:this.$=ce[J];break;case 6:case 7:this.$=[];break;case 8:ye.addEntity(ce[J-4]),ye.addEntity(ce[J-2]),ye.addRelationship(ce[J-4],ce[J],ce[J-2],ce[J-3]);break;case 9:ye.addEntity(ce[J-8]),ye.addEntity(ce[J-4]),ye.addRelationship(ce[J-8],ce[J],ce[J-4],ce[J-5]),ye.setClass([ce[J-8]],ce[J-6]),ye.setClass([ce[J-4]],ce[J-2]);break;case 10:ye.addEntity(ce[J-6]),ye.addEntity(ce[J-2]),ye.addRelationship(ce[J-6],ce[J],ce[J-2],ce[J-3]),ye.setClass([ce[J-6]],ce[J-4]);break;case 11:ye.addEntity(ce[J-6]),ye.addEntity(ce[J-4]),ye.addRelationship(ce[J-6],ce[J],ce[J-4],ce[J-5]),ye.setClass([ce[J-4]],ce[J-2]);break;case 12:ye.addEntity(ce[J-3]),ye.addAttributes(ce[J-3],ce[J-1]);break;case 13:ye.addEntity(ce[J-5]),ye.addAttributes(ce[J-5],ce[J-1]),ye.setClass([ce[J-5]],ce[J-3]);break;case 14:ye.addEntity(ce[J-2]);break;case 15:ye.addEntity(ce[J-4]),ye.setClass([ce[J-4]],ce[J-2]);break;case 16:ye.addEntity(ce[J]);break;case 17:ye.addEntity(ce[J-2]),ye.setClass([ce[J-2]],ce[J]);break;case 18:ye.addEntity(ce[J-6],ce[J-4]),ye.addAttributes(ce[J-6],ce[J-1]);break;case 19:ye.addEntity(ce[J-8],ce[J-6]),ye.addAttributes(ce[J-8],ce[J-1]),ye.setClass([ce[J-8]],ce[J-3]);break;case 20:ye.addEntity(ce[J-5],ce[J-3]);break;case 21:ye.addEntity(ce[J-7],ce[J-5]),ye.setClass([ce[J-7]],ce[J-2]);break;case 22:ye.addEntity(ce[J-3],ce[J-1]);break;case 23:ye.addEntity(ce[J-5],ce[J-3]),ye.setClass([ce[J-5]],ce[J]);break;case 24:case 25:this.$=ce[J].trim(),ye.setAccTitle(this.$);break;case 26:case 27:this.$=ce[J].trim(),ye.setAccDescription(this.$);break;case 32:ye.setDirection("TB");break;case 33:ye.setDirection("BT");break;case 34:ye.setDirection("RL");break;case 35:ye.setDirection("LR");break;case 36:this.$=ce[J-3],ye.addClass(ce[J-2],ce[J-1]);break;case 37:case 38:case 59:case 68:this.$=[ce[J]];break;case 39:case 40:this.$=ce[J-2].concat([ce[J]]);break;case 41:this.$=ce[J-2],ye.setClass(ce[J-1],ce[J]);break;case 42:this.$=ce[J-3],ye.addCssStyles(ce[J-2],ce[J-1]);break;case 43:this.$=[ce[J]];break;case 44:ce[J-2].push(ce[J]),this.$=ce[J-2];break;case 46:this.$=ce[J-1]+ce[J];break;case 54:case 80:case 81:this.$=ce[J].replace(/"/g,"");break;case 55:case 56:case 57:case 58:case 82:this.$=ce[J];break;case 60:ce[J].push(ce[J-1]),this.$=ce[J];break;case 61:this.$={type:ce[J-1],name:ce[J]};break;case 62:this.$={type:ce[J-2],name:ce[J-1],keys:ce[J]};break;case 63:this.$={type:ce[J-2],name:ce[J-1],comment:ce[J]};break;case 64:this.$={type:ce[J-3],name:ce[J-2],keys:ce[J-1],comment:ce[J]};break;case 65:case 67:case 70:this.$=ce[J];break;case 66:this.$=ce[J-1]+ce[J];break;case 69:ce[J-2].push(ce[J]),this.$=ce[J-2];break;case 71:this.$=ce[J].replace(/"/g,"");break;case 72:this.$={cardA:ce[J],relType:ce[J-1],cardB:ce[J-2]};break;case 73:this.$=ye.Cardinality.ZERO_OR_ONE;break;case 74:this.$=ye.Cardinality.ZERO_OR_MORE;break;case 75:this.$=ye.Cardinality.ONE_OR_MORE;break;case 76:this.$=ye.Cardinality.ONLY_ONE;break;case 77:this.$=ye.Cardinality.MD_PARENT;break;case 78:this.$=ye.Identification.NON_IDENTIFYING;break;case 79:this.$=ye.Identification.IDENTIFYING;break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},e(t,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:9,22:r,24:n,26:i,28:a,29:14,30:15,31:16,32:17,33:s,34:l,35:u,36:h,37:d,40:f,43:p,44:m,48:g,50:y,51:v,52:x},e(t,[2,7],{1:[2,1]}),e(t,[2,3]),{9:30,11:9,22:r,24:n,26:i,28:a,29:14,30:15,31:16,32:17,33:s,34:l,35:u,36:h,37:d,40:f,43:p,44:m,48:g,50:y,51:v,52:x},e(t,[2,5]),e(t,[2,6]),e(t,[2,16],{12:31,64:35,15:[1,32],17:[1,33],20:[1,34],66:b,67:T,68:k,69:C,70:w}),{23:[1,41]},{25:[1,42]},{27:[1,43]},e(t,[2,27]),e(t,[2,28]),e(t,[2,29]),e(t,[2,30]),e(t,[2,31]),e(S,[2,54]),e(S,[2,55]),e(S,[2,56]),e(S,[2,57]),e(S,[2,58]),e(t,[2,32]),e(t,[2,33]),e(t,[2,34]),e(t,[2,35]),{16:44,40:R,41:L},{16:47,40:R,41:L},{16:48,40:R,41:L},e(t,[2,4]),{11:49,40:f,48:g,50:y,51:v,52:x},{16:50,40:R,41:L},{18:51,19:[1,52],53:53,54:54,58:N},{11:56,40:f,48:g,50:y,51:v,52:x},{65:57,71:[1,58],72:[1,59]},e(I,[2,73]),e(I,[2,74]),e(I,[2,75]),e(I,[2,76]),e(I,[2,77]),e(t,[2,24]),e(t,[2,25]),e(t,[2,26]),{13:_,38:60,41:A,42:M,45:62,46:63,48:D,49:P},e(B,[2,37]),e(B,[2,38]),{16:68,40:R,41:L,42:M},{13:_,38:69,41:A,42:M,45:62,46:63,48:D,49:P},{13:[1,70],15:[1,71]},e(t,[2,17],{64:35,12:72,17:[1,73],42:M,66:b,67:T,68:k,69:C,70:w}),{19:[1,74]},e(t,[2,14]),{18:75,19:[2,59],53:53,54:54,58:N},{55:76,58:[1,77]},{58:[2,65],59:[1,78]},{21:[1,79]},{64:80,66:b,67:T,68:k,69:C,70:w},e(O,[2,78]),e(O,[2,79]),{6:$,10:V,39:81,42:G,47:z},{40:[1,86],41:[1,87]},e(W,[2,43],{46:88,13:_,41:A,48:D,49:P}),e(H,[2,45]),e(H,[2,50]),e(H,[2,51]),e(H,[2,52]),e(H,[2,53]),e(t,[2,41],{42:M}),{6:$,10:V,39:89,42:G,47:z},{14:90,40:j,50:Q,73:U},{16:94,40:R,41:L},{11:95,40:f,48:g,50:y,51:v,52:x},{18:96,19:[1,97],53:53,54:54,58:N},e(t,[2,12]),{19:[2,60]},e(oe,[2,61],{56:98,57:99,60:100,62:te,63:le}),e([19,58,62,63],[2,67]),{58:[2,66]},e(t,[2,22],{15:[1,104],17:[1,103]}),e([40,48,50,51,52],[2,72]),e(t,[2,36]),{13:_,41:A,45:105,46:63,48:D,49:P},e(t,[2,47]),e(t,[2,48]),e(t,[2,49]),e(B,[2,39]),e(B,[2,40]),e(H,[2,46]),e(t,[2,42]),e(t,[2,8]),e(t,[2,80]),e(t,[2,81]),e(t,[2,82]),{13:[1,106],42:M},{13:[1,108],15:[1,107]},{19:[1,109]},e(t,[2,15]),e(oe,[2,62],{57:110,61:[1,111],63:le}),e(oe,[2,63]),e(ie,[2,68]),e(oe,[2,71]),e(ie,[2,70]),{18:112,19:[1,113],53:53,54:54,58:N},{16:114,40:R,41:L},e(W,[2,44],{46:88,13:_,41:A,48:D,49:P}),{14:115,40:j,50:Q,73:U},{16:116,40:R,41:L},{14:117,40:j,50:Q,73:U},e(t,[2,13]),e(oe,[2,64]),{60:118,62:te},{19:[1,119]},e(t,[2,20]),e(t,[2,23],{17:[1,120],42:M}),e(t,[2,11]),{13:[1,121],42:M},e(t,[2,10]),e(ie,[2,69]),e(t,[2,18]),{18:122,19:[1,123],53:53,54:54,58:N},{14:124,40:j,50:Q,73:U},{19:[1,125]},e(t,[2,21]),e(t,[2,9]),e(t,[2,19])],defaultActions:{75:[2,60],78:[2,66]},parseError:o(function(Ge,Oe){if(Oe.recoverable)this.trace(Ge);else{var ue=new Error(Ge);throw ue.hash=Oe,ue}},"parseError"),parse:o(function(Ge){var Oe=this,ue=[0],ye=[],ke=[null],ce=[],re=this.table,J="",se=0,ge=0,Te=0,we=2,Me=1,ve=ce.slice.call(arguments,1),ne=Object.create(this.lexer),q={yy:{}};for(var he in this.yy)Object.prototype.hasOwnProperty.call(this.yy,he)&&(q.yy[he]=this.yy[he]);ne.setInput(Ge,q.yy),q.yy.lexer=ne,q.yy.parser=this,typeof ne.yylloc>"u"&&(ne.yylloc={});var X=ne.yylloc;ce.push(X);var fe=ne.options&&ne.options.ranges;typeof q.yy.parseError=="function"?this.parseError=q.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function K(wt){ue.length=ue.length-2*wt,ke.length=ke.length-wt,ce.length=ce.length-wt}o(K,"popStack");function qe(){var wt;return wt=ye.pop()||ne.lex()||Me,typeof wt!="number"&&(wt instanceof Array&&(ye=wt,wt=ye.pop()),wt=Oe.symbols_[wt]||wt),wt}o(qe,"lex");for(var _e,Be,Ne,He,$e,Xe,Fe={},Ke,xe,mt,Le;;){if(Ne=ue[ue.length-1],this.defaultActions[Ne]?He=this.defaultActions[Ne]:((_e===null||typeof _e>"u")&&(_e=qe()),He=re[Ne]&&re[Ne][_e]),typeof He>"u"||!He.length||!He[0]){var ft="";Le=[];for(Ke in re[Ne])this.terminals_[Ke]&&Ke>we&&Le.push("'"+this.terminals_[Ke]+"'");ne.showPosition?ft="Parse error on line "+(se+1)+`: +`+ne.showPosition()+` +Expecting `+Le.join(", ")+", got '"+(this.terminals_[_e]||_e)+"'":ft="Parse error on line "+(se+1)+": Unexpected "+(_e==Me?"end of input":"'"+(this.terminals_[_e]||_e)+"'"),this.parseError(ft,{text:ne.match,token:this.terminals_[_e]||_e,line:ne.yylineno,loc:X,expected:Le})}if(He[0]instanceof Array&&He.length>1)throw new Error("Parse Error: multiple actions possible at state: "+Ne+", token: "+_e);switch(He[0]){case 1:ue.push(_e),ke.push(ne.yytext),ce.push(ne.yylloc),ue.push(He[1]),_e=null,Be?(_e=Be,Be=null):(ge=ne.yyleng,J=ne.yytext,se=ne.yylineno,X=ne.yylloc,Te>0&&Te--);break;case 2:if(xe=this.productions_[He[1]][1],Fe.$=ke[ke.length-xe],Fe._$={first_line:ce[ce.length-(xe||1)].first_line,last_line:ce[ce.length-1].last_line,first_column:ce[ce.length-(xe||1)].first_column,last_column:ce[ce.length-1].last_column},fe&&(Fe._$.range=[ce[ce.length-(xe||1)].range[0],ce[ce.length-1].range[1]]),Xe=this.performAction.apply(Fe,[J,ge,se,q.yy,He[1],ke,ce].concat(ve)),typeof Xe<"u")return Xe;xe&&(ue=ue.slice(0,-1*xe*2),ke=ke.slice(0,-1*xe),ce=ce.slice(0,-1*xe)),ue.push(this.productions_[He[1]][0]),ke.push(Fe.$),ce.push(Fe._$),mt=re[ue[ue.length-2]][ue[ue.length-1]],ue.push(mt);break;case 3:return!0}}return!0},"parse")},Re=(function(){var Pe={EOF:1,parseError:o(function(Oe,ue){if(this.yy.parser)this.yy.parser.parseError(Oe,ue);else throw new Error(Oe)},"parseError"),setInput:o(function(Ge,Oe){return this.yy=Oe||this.yy||{},this._input=Ge,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var Ge=this._input[0];this.yytext+=Ge,this.yyleng++,this.offset++,this.match+=Ge,this.matched+=Ge;var Oe=Ge.match(/(?:\r\n?|\n).*/g);return Oe?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),Ge},"input"),unput:o(function(Ge){var Oe=Ge.length,ue=Ge.split(/(?:\r\n?|\n)/g);this._input=Ge+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-Oe),this.offset-=Oe;var ye=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),ue.length-1&&(this.yylineno-=ue.length-1);var ke=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:ue?(ue.length===ye.length?this.yylloc.first_column:0)+ye[ye.length-ue.length].length-ue[0].length:this.yylloc.first_column-Oe},this.options.ranges&&(this.yylloc.range=[ke[0],ke[0]+this.yyleng-Oe]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(Ge){this.unput(this.match.slice(Ge))},"less"),pastInput:o(function(){var Ge=this.matched.substr(0,this.matched.length-this.match.length);return(Ge.length>20?"...":"")+Ge.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var Ge=this.match;return Ge.length<20&&(Ge+=this._input.substr(0,20-Ge.length)),(Ge.substr(0,20)+(Ge.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var Ge=this.pastInput(),Oe=new Array(Ge.length+1).join("-");return Ge+this.upcomingInput()+` +`+Oe+"^"},"showPosition"),test_match:o(function(Ge,Oe){var ue,ye,ke;if(this.options.backtrack_lexer&&(ke={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(ke.yylloc.range=this.yylloc.range.slice(0))),ye=Ge[0].match(/(?:\r\n?|\n).*/g),ye&&(this.yylineno+=ye.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:ye?ye[ye.length-1].length-ye[ye.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+Ge[0].length},this.yytext+=Ge[0],this.match+=Ge[0],this.matches=Ge,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(Ge[0].length),this.matched+=Ge[0],ue=this.performAction.call(this,this.yy,this,Oe,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),ue)return ue;if(this._backtrack){for(var ce in ke)this[ce]=ke[ce];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var Ge,Oe,ue,ye;this._more||(this.yytext="",this.match="");for(var ke=this._currentRules(),ce=0;ceOe[0].length)){if(Oe=ue,ye=ce,this.options.backtrack_lexer){if(Ge=this.test_match(ue,ke[ce]),Ge!==!1)return Ge;if(this._backtrack){Oe=!1;continue}else return!1}else if(!this.options.flex)break}return Oe?(Ge=this.test_match(Oe,ke[ye]),Ge!==!1?Ge:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var Oe=this.next();return Oe||this.lex()},"lex"),begin:o(function(Oe){this.conditionStack.push(Oe)},"begin"),popState:o(function(){var Oe=this.conditionStack.length-1;return Oe>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(Oe){return Oe=this.conditionStack.length-1-Math.abs(Oe||0),Oe>=0?this.conditionStack[Oe]:"INITIAL"},"topState"),pushState:o(function(Oe){this.begin(Oe)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:o(function(Oe,ue,ye,ke){var ce=ke;switch(ye){case 0:return this.begin("acc_title"),24;break;case 1:return this.popState(),"acc_title_value";break;case 2:return this.begin("acc_descr"),26;break;case 3:return this.popState(),"acc_descr_value";break;case 4:this.begin("acc_descr_multiline");break;case 5:this.popState();break;case 6:return"acc_descr_multiline_value";case 7:return 33;case 8:return 34;case 9:return 35;case 10:return 36;case 11:return 10;case 12:break;case 13:return 8;case 14:return 50;case 15:return 73;case 16:return 4;case 17:return this.begin("block"),17;break;case 18:return 49;case 19:return 49;case 20:return 42;case 21:return 15;case 22:return 13;case 23:break;case 24:return 62;case 25:return 58;case 26:return 58;case 27:this.begin("block_bq");break;case 28:return 58;case 29:this.popState();break;case 30:return 63;case 31:break;case 32:return this.popState(),19;break;case 33:return ue.yytext[0];case 34:return 20;case 35:return 21;case 36:return this.begin("style"),44;break;case 37:return this.popState(),10;break;case 38:break;case 39:return 13;case 40:return 42;case 41:return 49;case 42:return this.begin("style"),37;break;case 43:return 43;case 44:return 66;case 45:return 68;case 46:return 68;case 47:return 68;case 48:return 66;case 49:return 66;case 50:return 67;case 51:return 67;case 52:return 67;case 53:return 67;case 54:return 67;case 55:return 68;case 56:return 67;case 57:return 68;case 58:return 69;case 59:return 69;case 60:return 51;case 61:return 69;case 62:return 69;case 63:return 69;case 64:return 52;case 65:return 48;case 66:return 69;case 67:return 66;case 68:return 67;case 69:return 68;case 70:return 70;case 71:return 71;case 72:return 72;case 73:return 72;case 74:return 71;case 75:return 71;case 76:return 71;case 77:return 41;case 78:return 47;case 79:return 40;case 80:return ue.yytext[0];case 81:return 6}},"anonymous"),rules:[/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:.*direction\s+TB[^\n]*)/i,/^(?:.*direction\s+BT[^\n]*)/i,/^(?:.*direction\s+RL[^\n]*)/i,/^(?:.*direction\s+LR[^\n]*)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:[\s]+)/i,/^(?:"[^"%\r\n\v\b\\]+")/i,/^(?:"[^"]*")/i,/^(?:erDiagram\b)/i,/^(?:\{)/i,/^(?:#)/i,/^(?:#)/i,/^(?:,)/i,/^(?::::)/i,/^(?::)/i,/^(?:\s+)/i,/^(?:\b((?:PK)|(?:FK)|(?:UK))\b)/i,/^(?:([^\s]*)[~].*[~]([^\s]*))/i,/^(?:([\*A-Za-z_\u00C0-\uFFFF][A-Za-z0-9\-\_\[\]\(\)\.,\u00C0-\uFFFF\*]*))/i,/^(?:[`])/i,/^(?:[^`]+)/i,/^(?:[`])/i,/^(?:"[^"]*")/i,/^(?:[\n]+)/i,/^(?:\})/i,/^(?:.)/i,/^(?:\[)/i,/^(?:\])/i,/^(?:style\b)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?::)/i,/^(?:,)/i,/^(?:#)/i,/^(?:classDef\b)/i,/^(?:class\b)/i,/^(?:one or zero\b)/i,/^(?:one or more\b)/i,/^(?:one or many\b)/i,/^(?:1\+)/i,/^(?:\|o\b)/i,/^(?:zero or one\b)/i,/^(?:zero or more\b)/i,/^(?:zero or many\b)/i,/^(?:0\+)/i,/^(?:\}o\b)/i,/^(?:many\(0\))/i,/^(?:many\(1\))/i,/^(?:many\b)/i,/^(?:\}\|)/i,/^(?:one\b)/i,/^(?:only one\b)/i,/^(?:[0-9]+\.[0-9]+)/i,/^(?:1(?=\s+[A-Za-z_"']))/i,/^(?:1(?=\s+[0-9]))/i,/^(?:1(?=(--|\.\.|\.-|-\.)))/i,/^(?:1\b)/i,/^(?:[0-9]+)/i,/^(?:\|\|)/i,/^(?:o\|)/i,/^(?:o\{)/i,/^(?:\|\{)/i,/^(?:u(?=[\.\-\|]))/i,/^(?:\.\.)/i,/^(?:--)/i,/^(?:to\b)/i,/^(?:optionally to\b)/i,/^(?:\.-)/i,/^(?:-\.)/i,/^(?:([^\x00-\x7F]|\w|-|\*)+)/i,/^(?:;)/i,/^(?:([^\x00-\x7F]|\w|-|\*|\.)+)/i,/^(?:.)/i,/^(?:$)/i],conditions:{style:{rules:[37,38,39,40,41,77,78],inclusive:!1},acc_descr_multiline:{rules:[5,6],inclusive:!1},acc_descr:{rules:[3],inclusive:!1},acc_title:{rules:[1],inclusive:!1},block_bq:{rules:[28,29],inclusive:!1},block:{rules:[23,24,25,26,27,30,31,32,33],inclusive:!1},INITIAL:{rules:[0,2,4,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,34,35,36,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,79,80,81],inclusive:!0}}};return Pe})();ae.lexer=Re;function be(){this.yy={}}return o(be,"Parser"),be.prototype=ae,ae.Parser=be,new be})();JF.parser=JF;ETe=JF});var DA,RTe=F(()=>{"use strict";vt();Xt();Nn();Qt();DA=class{constructor(){this.entities=new Map;this.relationships=[];this.classes=new Map;this.direction="TB";this.Cardinality={ZERO_OR_ONE:"ZERO_OR_ONE",ZERO_OR_MORE:"ZERO_OR_MORE",ONE_OR_MORE:"ONE_OR_MORE",ONLY_ONE:"ONLY_ONE",MD_PARENT:"MD_PARENT"};this.Identification={NON_IDENTIFYING:"NON_IDENTIFYING",IDENTIFYING:"IDENTIFYING"};this.setAccTitle=kr;this.getAccTitle=Ar;this.setAccDescription=Rr;this.getAccDescription=_r;this.setDiagramTitle=Or;this.getDiagramTitle=Lr;this.getConfig=o(()=>Ae().er,"getConfig");this.clear(),this.addEntity=this.addEntity.bind(this),this.addAttributes=this.addAttributes.bind(this),this.addRelationship=this.addRelationship.bind(this),this.setDirection=this.setDirection.bind(this),this.addCssStyles=this.addCssStyles.bind(this),this.addClass=this.addClass.bind(this),this.setClass=this.setClass.bind(this),this.setAccTitle=this.setAccTitle.bind(this),this.setAccDescription=this.setAccDescription.bind(this)}static{o(this,"ErDB")}addEntity(t,r=""){return this.entities.has(t)?!this.entities.get(t)?.alias&&r&&(this.entities.get(t).alias=r,Z.info(`Add alias '${r}' to entity '${t}'`)):(this.entities.set(t,{id:`entity-${t}-${this.entities.size}`,label:t,attributes:[],alias:r,shape:"erBox",look:Ae().look??"default",cssClasses:"default",cssStyles:[],labelType:"markdown"}),Z.info("Added new entity :",t)),this.entities.get(t)}getEntity(t){return this.entities.get(t)}getEntities(){return this.entities}getClasses(){return this.classes}addAttributes(t,r){let n=this.addEntity(t),i;for(i=r.length-1;i>=0;i--)r[i].keys||(r[i].keys=[]),r[i].comment||(r[i].comment=""),n.attributes.push(r[i]),Z.debug("Added attribute ",r[i].name)}addRelationship(t,r,n,i){let a=this.entities.get(t),s=this.entities.get(n);if(!a||!s)return;let l={entityA:a.id,roleA:r,entityB:s.id,relSpec:i};this.relationships.push(l),Z.debug("Added new relationship :",l)}getRelationships(){return this.relationships}getDirection(){return this.direction}setDirection(t){this.direction=t}getCompiledStyles(t){let r=[];for(let n of t){let i=this.classes.get(n);i?.styles&&(r=[...r,...i.styles??[]].map(a=>a.trim())),i?.textStyles&&(r=[...r,...i.textStyles??[]].map(a=>a.trim()))}return r}addCssStyles(t,r){for(let n of t){let i=this.entities.get(n);if(!r||!i)return;for(let a of r)i.cssStyles.push(a)}}addClass(t,r){t.forEach(n=>{let i=this.classes.get(n);i===void 0&&(i={id:n,styles:[],textStyles:[]},this.classes.set(n,i)),r&&r.forEach(function(a){if(/color/.exec(a)){let s=a.replace("fill","bgFill");i.textStyles.push(s)}i.styles.push(a)})})}setClass(t,r){for(let n of t){let i=this.entities.get(n);if(i)for(let a of r)i.cssClasses+=" "+a}}clear(){this.entities=new Map,this.classes=new Map,this.relationships=[],yr()}getData(){let t=[],r=[],n=Ae(),i=0;for(let s of this.entities.keys()){let l=this.entities.get(s);l&&(l.cssCompiledStyles=this.getCompiledStyles(l.cssClasses.split(" ")),l.colorIndex=i++,t.push(l))}let a=0;for(let s of this.relationships){let l={id:eu(s.entityA,s.entityB,{prefix:"id",counter:a++}),type:"normal",curve:"basis",start:s.entityA,end:s.entityB,label:s.roleA,labelpos:"c",thickness:"normal",classes:"relationshipLine",arrowTypeStart:s.relSpec.cardB.toLowerCase(),arrowTypeEnd:s.relSpec.cardA.toLowerCase(),pattern:s.relSpec.relType=="IDENTIFYING"?"solid":"dashed",look:n.look,labelType:"markdown"};r.push(l)}return{nodes:t,edges:r,other:{},config:n,direction:"TB"}}}});var ez={};ir(ez,{draw:()=>g0t});var g0t,_Te=F(()=>{"use strict";Xt();vt();Rm();Jf();ep();Qt();$r();g0t=o(async function(e,t,r,n){Z.info("REF0:"),Z.info("Drawing er diagram (unified)",t);let{securityLevel:i,er:a,layout:s}=Ae(),l=n.db.getData(),u=pl(t,i);l.type=n.type,l.layoutAlgorithm=Su(s),l.config.flowchart.nodeSpacing=a?.nodeSpacing||140,l.config.flowchart.rankSpacing=a?.rankSpacing||80,l.direction=n.db.getDirection();let{config:h}=l,{look:d}=h;d==="neo"?l.markers=["only_one_neo","zero_or_one_neo","one_or_more_neo","zero_or_more_neo"]:l.markers=["only_one","zero_or_one","one_or_more","zero_or_more"],l.diagramId=t,await Al(l,u),l.layoutAlgorithm==="elk"&&u.select(".edges").lower();let f=u.selectAll('[id*="-background"]');Array.from(f).length>0&&f.each(function(){let m=et(this),y=m.attr("id").replace("-background",""),v=u.select(`#${CSS.escape(y)}`);if(!v.empty()){let x=v.attr("transform");m.attr("transform",x)}});let p=8;Zt.insertTitle(u,"erDiagramTitleText",a?.titleTopMargin??25,n.db.getDiagramTitle()),vo(u,p,"erDiagram",a?.useMaxWidth??!0)},"draw")});var LTe,IA,y0t,v0t,DTe,ITe=F(()=>{"use strict";zi();LTe=o((e,t)=>{let r=Fp,n=r(e,"r"),i=r(e,"g"),a=r(e,"b");return Oi(n,i,a,t)},"fade"),IA=new Set(["redux-color","redux-dark-color"]),y0t=o(e=>{let{theme:t,look:r,bkgColorArray:n,borderColorArray:i}=e;if(!IA.has(t))return"";let a=n?.length>0,s="";for(let l=0;l{let{look:t,theme:r,erEdgeLabelBackground:n,strokeWidth:i}=e;return` + ${y0t(e)} + .entityBox { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; + } + + .relationshipLabelBox { + fill: ${e.tertiaryColor}; + opacity: 0.7; + background-color: ${e.tertiaryColor}; + rect { + opacity: 0.5; + } + } + + .labelBkg { + background-color: ${IA.has(r)&&n?n:LTe(e.tertiaryColor,.5)}; + } + + .edgeLabel { + background-color: ${IA.has(r)&&n?n:e.edgeLabelBackground}; + } + .edgeLabel .label rect { + fill: ${IA.has(r)&&n?n:e.edgeLabelBackground}; + } + .edgeLabel .label text { + fill: ${e.textColor}; + } + + .edgeLabel .label { + fill: ${e.nodeBorder}; + font-size: 14px; + } + + .label { + font-family: ${e.fontFamily}; + color: ${e.nodeTextColor||e.textColor}; + } + + .edge-pattern-dashed { + stroke-dasharray: 8,8; + } + + .node rect, + .node circle, + .node ellipse, + .node polygon + { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; + stroke-width: ${t==="neo"?i:"1px"}; + } + + .relationshipLine { + stroke: ${e.lineColor}; + stroke-width: ${t==="neo"?i:"1px"}; + fill: none; + } + + .marker { + fill: none !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; + } + [data-look=neo].labelBkg { + background-color: ${LTe(e.tertiaryColor,.5)}; + } +`},"getStyles"),DTe=v0t});var MTe={};ir(MTe,{diagram:()=>x0t});var x0t,NTe=F(()=>{"use strict";ATe();RTe();_Te();ITe();x0t={parser:ETe,get db(){return new DA},renderer:ez,styles:DTe}});function Zi(e){return typeof e=="object"&&e!==null&&typeof e.$type=="string"}function Bs(e){return typeof e=="object"&&e!==null&&typeof e.$refText=="string"&&"ref"in e}function Ou(e){return typeof e=="object"&&e!==null&&typeof e.$refText=="string"&&"items"in e}function sW(e){return typeof e=="object"&&e!==null&&typeof e.name=="string"&&typeof e.type=="string"&&typeof e.path=="string"}function Cg(e){return typeof e=="object"&&e!==null&&typeof e.info=="object"&&typeof e.message=="string"}function Uh(e){return typeof e=="object"&&e!==null&&Array.isArray(e.content)}function e0(e){return typeof e=="object"&&e!==null&&typeof e.tokenType=="object"}function vR(e){return Uh(e)&&typeof e.fullText=="string"}function Bwe(e){return typeof e=="string"?e:typeof e>"u"?"undefined":typeof e.toString=="function"?e.toString():Object.prototype.toString.call(e)}function jC(e){return!!e&&typeof e[Symbol.iterator]=="function"}function Bn(...e){if(e.length===1){let t=e[0];if(t instanceof Nu)return t;if(jC(t))return new Nu(()=>t[Symbol.iterator](),r=>r.next());if(typeof t.length=="number")return new Nu(()=>({index:0}),r=>r.index1?new Nu(()=>({collIndex:0,arrIndex:0}),t=>{do{if(t.iterator){let r=t.iterator.next();if(!r.done)return r;t.iterator=void 0}if(t.array){if(t.arrIndex{Zi(i)&&(i.$container=e,i.$containerProperty=r,i.$containerIndex=a,t.deep&&wv(i,t))}):Zi(n)&&(n.$container=e,n.$containerProperty=r,t.deep&&wv(n,t)))}function t0(e,t){let r=e;for(;r;){if(t(r))return r;r=r.$container}}function $we(e,t){let r=e;for(;r;){if(t(r))return!0;r=r.$container}return!1}function wc(e){let r=yv(e).$document;if(!r)throw new Error("AST node has no document.");return r}function yv(e){for(;e.$container;)e=e.$container;return e}function A6(e){return Bs(e)?e.ref?[e.ref]:[]:Ou(e)?e.items.map(t=>t.ref):[]}function pw(e,t){if(!e)throw new Error("Node must be an AstNode.");let r=t?.range;return new Nu(()=>({keys:Object.keys(e),keyIndex:0,arrayIndex:0}),n=>{for(;n.keyIndexpw(r,t))}function kc(e,t){if(e){if(t?.range&&!R6(e,t.range))return new Cv(e,()=>[])}else throw new Error("Root node must be an AstNode.");return new Cv(e,r=>pw(r,t),{includeRoot:!0})}function R6(e,t){if(!t)return!0;let r=e.$cstNode?.range;return r?IW(r,t):!1}function kv(e){return new Nu(()=>({keys:Object.keys(e),keyIndex:0,arrayIndex:0}),t=>{for(;t.keyIndexUh(t)?t.content:[],{includeRoot:!0})}function nke(e){return Ev(e).filter(e0)}function LW(e,t){for(;e.container;)if(e=e.container,e===t)return!0;return!1}function KC(e){return{start:{character:e.startColumn-1,line:e.startLine-1},end:{character:e.endColumn,line:e.endLine-1}}}function Av(e){if(!e)return;let{offset:t,end:r,range:n}=e;return{range:n,offset:t,end:r,length:r-t}}function DW(e,t){if(e.end.linet.end.line||e.start.line===t.end.line&&e.start.character>=t.end.character)return Mu.After;let r=e.start.line>t.start.line||e.start.line===t.start.line&&e.start.character>=t.start.character,n=e.end.lineMu.After}function ike(e,t,r=MW){if(e){if(t>0){let n=t-e.offset,i=e.text.charAt(n);r.test(i)||t--}return SR(e,t)}}function NW(e,t){if(e){let r=BW(e,!0);if(r&&W6(r,t))return r;if(vR(e)){let n=e.content.findIndex(i=>!i.hidden);for(let i=n-1;i>=0;i--){let a=e.content[i];if(W6(a,t))return a}}}}function W6(e,t){return e0(e)&&t.includes(e.tokenType.name)}function SR(e,t){if(e0(e))return e;if(Uh(e)){let r=OW(e,t,!1);if(r)return SR(r,t)}}function PW(e,t){if(e0(e))return e;if(Uh(e)){let r=OW(e,t,!0);if(r)return PW(r,t)}}function OW(e,t,r){let n=0,i=e.content.length-1,a;for(;n<=i;){let s=Math.floor((n+i)/2),l=e.content[s];if(l.offset<=t&&l.end>t)return l;l.end<=t?(a=r?l:void 0,n=s+1):i=s-1}return a}function BW(e,t=!0){for(;e.container;){let r=e.container,n=r.content.indexOf(e);for(;n>0;){n--;let i=r.content[n];if(t||!i.hidden)return i}e=r}}function ake(e,t=!0){for(;e.container;){let r=e.container,n=r.content.indexOf(e),i=r.content.length-1;for(;nt.test(r))}function Nv(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function VW(e,t){let r=WW(e),n=t.match(r);return!!n&&n[0].length>0}function WW(e){typeof e=="string"&&(e=new RegExp(e));let t=e,r=e.source,n=0;function i(){let a="",s;function l(h){a+=r.substr(n,h),n+=h}o(l,"appendRaw"),E(l,"appendRaw");function u(h){a+="(?:"+r.substr(n,h)+"|$)",n+=h}for(o(u,"appendOptional"),E(u,"appendOptional");n",n)-n+1);break;default:u(2);break}break;case"[":s=/\[(?:\\.|.)*?\]/g,s.lastIndex=n,s=s.exec(r)||[],u(s[0].length);break;case"|":case"^":case"$":case"*":case"+":case"?":l(1);break;case"{":s=/\{\d+,?\d*\}/g,s.lastIndex=n,s=s.exec(r),s?l(s[0].length):u(1);break;case"(":if(r[n+1]==="?")switch(r[n+2]){case":":a+="(?:",n+=3,a+=i()+"|$)";break;case"=":a+="(?=",n+=3,a+=i()+")";break;case"!":s=n,n+=3,i(),a+=r.substr(s,n-s);break;case"<":switch(r[n+3]){case"=":case"!":s=n,n+=4,i(),a+=r.substr(s,n-s);break;default:l(r.indexOf(">",n)-n+1),a+=i()+"|$)";break}break}else l(1),a+=i()+"|$)";break;case")":return++n,a;default:u(1);break}return a}return o(i,"process2"),E(i,"process"),new RegExp(i(),e.flags)}function qW(e){return e.rules.find(t=>zs(t)&&t.entry)}function HW(e){return e.rules.filter(t=>Il(t)&&t.hidden)}function _R(e,t){let r=new Set,n=qW(e);if(!n)return new Set(e.rules);let i=[n].concat(HW(e));for(let s of i)UW(s,r,t);let a=new Set;for(let s of e.rules)(r.has(s.name)||Il(s)&&s.hidden)&&a.add(s);return a}function UW(e,t,r){t.add(e.name),rd(e).forEach(n=>{if(Xh(n)||r&&CR(n)){let i=n.rule.ref;i&&!t.has(i.name)&&UW(i,t,r)}})}function pke(e){let t=new Set;return rd(e).forEach(r=>{n0(r)&&(zs(r.type.ref)&&t.add(r.type.ref),mw(r.type.ref)&&zs(r.type.ref.$container)&&t.add(r.type.ref.$container))}),t}function YW(e){if(e.terminal)return e.terminal;if(e.type.ref)return MR(e.type.ref)?.terminal}function jW(e){return e.hidden&&!RR(vw(e))}function XW(e,t){return!e||!t?[]:DR(e,t,e.astNode,!0)}function LR(e,t,r){if(!e||!t)return;let n=DR(e,t,e.astNode,!0);if(n.length!==0)return r!==void 0?r=Math.max(0,Math.min(r,n.length-1)):r=0,n[r]}function DR(e,t,r,n){if(!n){let i=t0(e.grammarSource,Yh);if(i&&i.feature===t)return[e]}return Uh(e)&&e.astNode===r?e.content.flatMap(i=>DR(i,t,r,!1)):[]}function mke(e,t){return e?IR(e,t,e?.astNode):[]}function KW(e,t,r){if(!e)return;let n=IR(e,t,e?.astNode);if(n.length!==0)return r!==void 0?r=Math.max(0,Math.min(r,n.length-1)):r=0,n[r]}function IR(e,t,r){if(e.astNode!==r)return[];if(jh(e.grammarSource)&&e.grammarSource.value===t)return[e];let n=Ev(e).iterator(),i,a=[];do if(i=n.next(),!i.done){let s=i.value;s.astNode===r?jh(s.grammarSource)&&s.grammarSource.value===t&&a.push(s):n.prune()}while(!i.done);return a}function ZW(e){let t=e.astNode;for(;t===e.container?.astNode;){let r=t0(e.grammarSource,Yh);if(r)return r;e=e.container}}function MR(e){let t=e;return mw(t)&&(fp(t.$container)?t=t.$container.$container:r0(t.$container)?t=t.$container:xp(t.$container)),QW(e,t,new Map)}function QW(e,t,r){function n(i,a){let s;return t0(i,Yh)||(s=QW(a,a,r)),r.set(e,s),s}if(o(n,"go"),E(n,"go"),r.has(e))return r.get(e);r.set(e,void 0);for(let i of rd(t)){if(Yh(i)&&i.feature.toLowerCase()==="name")return r.set(e,i),i;if(Xh(i)&&zs(i.rule.ref))return n(i,i.rule.ref);if(TR(i)&&i.typeRef?.ref)return n(i,i.typeRef.ref)}}function JW(e){let t=e.$container;if(i0(t)){let r=t.elements,n=r.indexOf(e);for(let i=n-1;i>=0;i--){let a=r[i];if(fp(a))return a;{let s=rd(r[i]).find(fp);if(s)return s}}}if(xR(t))return JW(t)}function gke(e,t){return e==="?"||e==="*"||i0(t)&&!!t.guardCondition}function yke(e){return e==="*"||e==="+"}function vke(e){return e==="+="}function gw(e){return eq(e,new Set)}function eq(e,t){if(t.has(e))return!0;t.add(e);for(let r of rd(e))if(Xh(r)){if(!r.rule.ref||zs(r.rule.ref)&&!eq(r.rule.ref,t)||Sv(r.rule.ref))return!1}else{if(Yh(r))return!1;if(fp(r))return!1}return!!e.definition}function xke(e){return U6(e.type,new Set)}function U6(e,t){if(t.has(e))return!0;if(t.add(e),hW(e))return!1;if(TW(e))return!1;if(EW(e))return e.types.every(r=>U6(r,t));if(TR(e)){if(e.primitiveType!==void 0)return!0;if(e.stringType!==void 0)return!0;if(e.typeRef!==void 0){let r=e.typeRef.ref;return wR(r)?U6(r.type,t):!1}else return!1}else return!1}function yw(e){if(!Il(e)){if(e.inferredType)return e.inferredType.name;if(e.dataType)return e.dataType;if(e.returnType){let t=e.returnType.ref;if(t)return t.name}}}function Xg(e){if(r0(e))return zs(e)&&gw(e)?e.name:yw(e)??e.name;if(yW(e)||wR(e)||wW(e))return e.name;if(fp(e)){let t=tq(e);if(t)return t}else if(mw(e))return e.name;throw new Error("Cannot get name of Unknown Type")}function tq(e){if(e.inferredType)return e.inferredType.name;if(e.type?.ref)return Xg(e.type.ref)}function bke(e){return Il(e)?e.type?.name??"string":zs(e)&&gw(e)?e.name:yw(e)??e.name}function rq(e){return Il(e)?e.type?.name??"string":yw(e)??e.name}function vw(e){let t={s:!1,i:!1,u:!1},r=a0(e.definition,t),n=Object.entries(t).filter(([,i])=>i).map(([i])=>i).join("");return new RegExp(r,n)}function a0(e,t){if(kW(e))return Tke(e);if(SW(e))return Cke(e);if(fW(e))return Ske(e);if(CR(e)){let r=e.rule.ref;if(!r)throw new Error("Missing rule reference.");return Bu(a0(r.definition),{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized})}else{if(vW(e))return kke(e);if(AW(e))return wke(e);if(CW(e)){let r=e.regex.lastIndexOf("/"),n=e.regex.substring(1,r),i=e.regex.substring(r+1);return t&&(t.i=i.includes("i"),t.s=i.includes("s"),t.u=i.includes("u")),Bu(n,{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized,wrap:!1})}else{if(RW(e))return Bu(nq,{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized});throw new Error(`Invalid terminal element: ${e?.$type}, ${e?.$cstNode?.text}`)}}}function Tke(e){return Bu(e.elements.map(t=>a0(t)).join("|"),{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized,wrap:!1})}function Cke(e){return Bu(e.elements.map(t=>a0(t)).join(""),{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized,wrap:!1})}function wke(e){return Bu(`${nq}*?${a0(e.terminal)}`,{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized})}function kke(e){return Bu(`(?!${a0(e.terminal)})${nq}*?`,{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized})}function Ske(e){return e.right?Bu(`[${s6(e.left)}-${s6(e.right)}]`,{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized,wrap:!1}):Bu(s6(e.left),{cardinality:e.cardinality,lookahead:e.lookahead,parenthesized:e.parenthesized,wrap:!1})}function s6(e){return Nv(e.value)}function Bu(e,t){return(t.parenthesized||t.lookahead||t.wrap!==!1)&&(e=`(${t.lookahead??(t.parenthesized?"":"?:")}${e})`),t.cardinality?`${e}${t.cardinality}`:e}function iq(e){let t=[],r=e.Grammar;for(let n of r.rules)Il(n)&&jW(n)&&GW(vw(n))&&t.push(n.name);return{multilineCommentRules:t,nameRegexp:MW}}function Rke(e){var t=pyt.call(e,uC),r=e[uC];try{e[uC]=void 0;var n=!0}catch{}var i=myt.call(e);return n&&(t?e[uC]=r:delete e[uC]),i}function _ke(e){return vyt.call(e)}function Lke(e){return e==null?e===void 0?Tyt:byt:FTe&&FTe in Object(e)?gyt(e):xyt(e)}function Dke(e){return e!=null&&typeof e=="object"}function Ike(e){return typeof e=="symbol"||Ac(e)&&bp(e)==Cyt}function Mke(e,t){for(var r=-1,n=e==null?0:e.length,i=Array(n);++r0){if(++t>=a1t)return arguments[0]}else t=0;return e.apply(void 0,arguments)}}function Zke(e){return function(){return e}}function Qke(e,t){for(var r=-1,n=e==null?0:e.length;++r-1}function oSe(e,t){var r=typeof e;return t=t??y1t,!!t&&(r=="number"||r!="symbol"&&v1t.test(e))&&e>-1&&e%1==0&&e-1&&e%1==0&&e<=C1t}function mSe(e){return e!=null&&cq(e.length)&&!nd(e)}function gSe(e,t,r){if(!Dl(r))return!1;var n=typeof t;return(n=="number"?zu(r)&&PR(t,r.length):n=="string"&&t in r)?Cw(r[t],e):!1}function ySe(e){return lq(function(t,r){var n=-1,i=r.length,a=i>1?r[i-1]:void 0,s=i>2?r[2]:void 0;for(a=e.length>3&&typeof a=="function"?(i--,a):void 0,s&&BR(r[0],r[1],s)&&(a=i<3?void 0:a,i=1),t=Object(t);++n-1}function jSe(e,t){var r=this.__data__,n=zR(r,e);return n<0?(++this.size,r.push([e,t])):r[n][1]=t,this}function c0(e){var t=-1,r=e==null?0:e.length;for(this.clear();++t0&&r(l)?t>1?pq(l,t-1,r,n,i):fq(i,l):n||(i[i.length]=l)}return i}function uEe(e){var t=e==null?0:e.length;return t?mq(e,1):[]}function dEe(e,t,r){var n=-1,i=e.length;t<0&&(t=-t>i?0:i+t),r=r>i?i:r,r<0&&(r+=i),i=t>r?0:r-t>>>0,t>>>=0;for(var a=Array(i);++nl))return!1;var h=a.get(e),d=a.get(t);if(h&&d)return h==t&&d==e;var f=-1,p=!0,m=r&Zbt?new xq:void 0;for(a.set(e,t),a.set(t,e);++f=q2t&&(a=bq,s=!1,t=new xq(t));e:for(;++i-1?i[a?t[s]:s]:void 0}}function j4e(e,t,r){var n=e==null?0:e.length;if(!n)return-1;var i=r==null?0:bw(r);return i<0&&(i=Z2t(n+i,0)),tSe(e,Gu(t,3),i)}function X4e(e){return e&&e.length?e[0]:void 0}function K4e(e,t){var r=-1,n=zu(e)?Array(e.length):[];return d0(e,function(i,a,s){n[++r]=t(i,a,s)}),n}function Z4e(e,t){var r=un(e)?xw:eTt;return r(e,Gu(t,3))}function Q4e(e,t){return mq(hr(e,t),1)}function J4e(e,t){return e!=null&&sTt.call(e,t)}function e3e(e,t){return e!=null&&v4e(e,t,oTt)}function t3e(e){return typeof e=="string"||!un(e)&&Ac(e)&&bp(e)==lTt}function r3e(e,t){return xw(t,function(r){return e[r]})}function n3e(e){return e==null?[]:cTt(e,jo(e))}function i3e(e,t,r,n){e=zu(e)?e:ha(e),r=r&&!n?bw(r):0;var i=e.length;return r<0&&(r=uTt(i+r,0)),To(e)?r<=i&&e.indexOf(t,r)>-1:!!i&&sq(e,t,r)>-1}function a3e(e,t,r){var n=e==null?0:e.length;if(!n)return-1;var i=r==null?0:bw(r);return i<0&&(i=hTt(n+i,0)),sq(e,t,i)}function s3e(e){if(e==null)return!0;if(zu(e)&&(un(e)||typeof e=="string"||typeof e.splice=="function"||ZC(e)||uq(e)||$R(e)))return!e.length;var t=Rv(e);if(t==dTt||t==fTt)return!e.size;if(kw(e))return!ISe(e).length;for(var r in e)if(mTt.call(e,r))return!1;return!0}function o3e(e){return Ac(e)&&bp(e)==gTt}function l3e(e){return e===void 0}function c3e(e){if(typeof e!="function")throw new TypeError(xTt);return function(){var t=arguments;switch(t.length){case 0:return!e.call(this);case 1:return!e.call(this,t[0]);case 2:return!e.call(this,t[0],t[1]);case 3:return!e.call(this,t[0],t[1],t[2])}return!e.apply(this,t)}}function u3e(e,t,r,n){if(!Dl(e))return e;t=HR(t,e);for(var i=-1,a=t.length,s=a-1,l=e;l!=null&&++i=RTt){var h=t?null:ATt(e);if(h)return Tq(h);s=!1,i=bq,u=new xq}else u=t?[]:l;e:for(;++nrw(n,t)):e instanceof Gs&&qs(t,e)?!1:e instanceof Vu?(e instanceof Gs&&t.push(e),Ec(e.definition,n=>rw(n,t))):!1}function S3e(e){return e instanceof ko}function Tc(e){if(e instanceof Gs)return"SUBRULE";if(e instanceof Ua)return"OPTION";if(e instanceof ko)return"OR";if(e instanceof Zo)return"AT_LEAST_ONE";if(e instanceof Qo)return"AT_LEAST_ONE_SEP";if(e instanceof wo)return"MANY_SEP";if(e instanceof Mi)return"MANY";if(e instanceof ci)return"CONSUME";throw Error("non exhaustive match")}function LG(e,t,r){return[new Ua({definition:[new ci({terminalType:e.separator})].concat(e.definition)})].concat(t,r)}function Bv(e){if(e instanceof Gs)return Bv(e.referencedRule);if(e instanceof ci)return R3e(e);if(k3e(e))return E3e(e);if(S3e(e))return A3e(e);throw Error("non exhaustive match")}function E3e(e){let t=[],r=e.definition,n=0,i=r.length>n,a,s=!0;for(;i&&s;)a=r[n],s=rw(a),t=t.concat(Bv(a)),n=n+1,i=r.length>n;return kq(t)}function A3e(e){let t=hr(e.definition,r=>Bv(r));return kq(Sc(t))}function R3e(e){return[e.terminalType]}function L3e(e){let t={};return Ir(e,r=>{let n=new LTt(r).startWalking();Xo(t,n)}),t}function D3e(e,t){return e.name+t+_3e}function Rw(e){let t=e.toString();if(o6.hasOwnProperty(t))return o6[t];{let r=DTt.pattern(t);return o6[t]=r,r}}function I3e(){o6={}}function N3e(e,t=!1){try{let r=Rw(e);return Z6(r.value,{},r.flags.ignoreCase)}catch(r){if(r.message===M3e)t&&Sq(`${K6} Unable to optimize: < ${e.toString()} > + Complement Sets cannot be automatically optimized. + This will disable the lexer's first char optimizations. + See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#COMPLEMENT for details.`);else{let n="";t&&(n=` + This will disable the lexer's first char optimizations. + See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#REGEXP_PARSING for details.`),X6(`${K6} + Failed parsing: < ${e.toString()} > + Using the @chevrotain/regexp-to-ast library + Please open an issue at: https://github.com/chevrotain/chevrotain/issues`+n)}}return[]}function Z6(e,t,r){switch(e.type){case"Disjunction":for(let i=0;i{if(typeof u=="number")EC(u,t,r);else{let h=u;if(r===!0)for(let d=h.from;d<=h.to;d++)EC(d,t,r);else{for(let d=h.from;d<=h.to&&d=RC){let d=h.from>=RC?h.from:RC,f=h.to,p=Qh(d),m=Qh(f);for(let g=p;g<=m;g++)t[g]=g}}}});break;case"Group":Z6(s.value,t,r);break;default:throw Error("Non Exhaustive Match")}let l=s.quantifier!==void 0&&s.quantifier.atLeast===0;if(s.type==="Group"&&Q6(s)===!1||s.type!=="Group"&&l===!1)break}break;default:throw Error("non exhaustive match!")}return ha(t)}function EC(e,t,r){let n=Qh(e);t[n]=n,r===!0&&P3e(e,t)}function P3e(e,t){let r=String.fromCharCode(e),n=r.toUpperCase();if(n!==r){let i=Qh(n.charCodeAt(0));t[i]=i}else{let i=r.toLowerCase();if(i!==r){let a=Qh(i.charCodeAt(0));t[a]=a}}}function DG(e,t){return _v(e.value,r=>{if(typeof r=="number")return qs(t,r);{let n=r;return _v(t,i=>n.from<=i&&i<=n.to)!==void 0}})}function Q6(e){let t=e.quantifier;return t&&t.atLeast===0?!0:e.value?un(e.value)?Ec(e.value,Q6):Q6(e.value):!1}function XR(e,t){if(t instanceof RegExp){let r=Rw(t),n=new ITt(e);return n.visit(r),n.found}else return _v(t,r=>qs(e,r.charCodeAt(0)))!==void 0}function O3e(e,t){t=wq(t,{debug:!1,safeMode:!1,positionTracking:"full",lineTerminatorCharacters:["\r",` +`],tracer:E((b,T)=>T(),"tracer")});let r=t.tracer;r("initCharCodeToOptimizedIndexMap",()=>{n5e()});let n;r("Reject Lexer.NA",()=>{n=YR(e,b=>b[Zg]===Fs.NA)});let i=!1,a;r("Transform Patterns",()=>{i=!1,a=hr(n,b=>{let T=b[Zg];if(Kh(T)){let k=T.source;return k.length===1&&k!=="^"&&k!=="$"&&k!=="."&&!T.ignoreCase?k:k.length===2&&k[0]==="\\"&&!qs(["d","D","s","S","t","r","n","t","0","c","b","B","f","v","w","W"],k[1])?k[1]:IG(T)}else{if(nd(T))return i=!0,{exec:T};if(typeof T=="object")return i=!0,T;if(typeof T=="string"){if(T.length===1)return T;{let k=T.replace(/[\\^$.*+?()[\]{}|]/g,"\\$&"),C=new RegExp(k);return IG(C)}}else throw Error("non exhaustive match")}})});let s,l,u,h,d;r("misc mapping",()=>{s=hr(n,b=>b.tokenTypeIdx),l=hr(n,b=>{let T=b.GROUP;if(T!==Fs.SKIPPED){if(To(T))return T;if(Zh(T))return!1;throw Error("non exhaustive match")}}),u=hr(n,b=>{let T=b.LONGER_ALT;if(T)return un(T)?hr(T,C=>yCe(n,C)):[yCe(n,T)]}),h=hr(n,b=>b.PUSH_MODE),d=hr(n,b=>wr(b,"POP_MODE"))});let f;r("Line Terminator Handling",()=>{let b=Lq(t.lineTerminatorCharacters);f=hr(n,T=>!1),t.positionTracking!=="onlyOffset"&&(f=hr(n,T=>wr(T,"LINE_BREAKS")?!!T.LINE_BREAKS:_q(T,b)===!1&&XR(b,T.PATTERN)))});let p,m,g,y;r("Misc Mapping #2",()=>{p=hr(n,Rq),m=hr(a,t5e),g=Ko(n,(b,T)=>{let k=T.GROUP;return To(k)&&k!==Fs.SKIPPED&&(b[k]=[]),b},{}),y=hr(a,(b,T)=>({pattern:a[T],longerAlt:u[T],canLineTerminator:f[T],isCustom:p[T],short:m[T],group:l[T],push:h[T],pop:d[T],tokenTypeIdx:s[T],tokenType:n[T]}))});let v=!0,x=[];return t.safeMode||r("First Char Optimization",()=>{x=Ko(n,(b,T,k)=>{if(typeof T.PATTERN=="string"){let C=T.PATTERN.charCodeAt(0),w=Qh(C);l6(b,w,y[k])}else if(un(T.START_CHARS_HINT)){let C;Ir(T.START_CHARS_HINT,w=>{let S=typeof w=="string"?w.charCodeAt(0):w,R=Qh(S);C!==R&&(C=R,l6(b,R,y[k]))})}else if(Kh(T.PATTERN))if(T.PATTERN.unicode)v=!1,t.ensureOptimizations&&X6(`${K6} Unable to analyze < ${T.PATTERN.toString()} > pattern. + The regexp unicode flag is not currently supported by the regexp-to-ast library. + This will disable the lexer's first char optimizations. + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#UNICODE_OPTIMIZE`);else{let C=N3e(T.PATTERN,t.ensureOptimizations);Jn(C)&&(v=!1),Ir(C,w=>{l6(b,w,y[k])})}else t.ensureOptimizations&&X6(`${K6} TokenType: <${T.name}> is using a custom token pattern without providing parameter. + This will disable the lexer's first char optimizations. + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#CUSTOM_OPTIMIZE`),v=!1;return b},[])}),{emptyGroups:g,patternIdxToConfig:y,charCodeToPatternIdxToConfig:x,hasCustom:i,canBeOptimized:v}}function B3e(e,t){let r=[],n=F3e(e);r=r.concat(n.errors);let i=z3e(n.valid),a=i.valid;return r=r.concat(i.errors),r=r.concat($3e(a)),r=r.concat(U3e(a)),r=r.concat(Y3e(a,t)),r=r.concat(j3e(a)),r}function $3e(e){let t=[],r=Ml(e,n=>Kh(n[Zg]));return t=t.concat(G3e(r)),t=t.concat(W3e(r)),t=t.concat(q3e(r)),t=t.concat(H3e(r)),t=t.concat(V3e(r)),t}function F3e(e){let t=Ml(e,i=>!wr(i,Zg)),r=hr(t,i=>({message:"Token Type: ->"+i.name+"<- missing static 'PATTERN' property",type:Ni.MISSING_PATTERN,tokenTypes:[i]})),n=UR(e,t);return{errors:r,valid:n}}function z3e(e){let t=Ml(e,i=>{let a=i[Zg];return!Kh(a)&&!nd(a)&&!wr(a,"exec")&&!To(a)}),r=hr(t,i=>({message:"Token Type: ->"+i.name+"<- static 'PATTERN' can only be a RegExp, a Function matching the {CustomPatternMatcherFunc} type or an Object matching the {ICustomPattern} interface.",type:Ni.INVALID_PATTERN,tokenTypes:[i]})),n=UR(e,t);return{errors:r,valid:n}}function G3e(e){class t extends AR{static{o(this,"EndAnchorFinder")}static{E(this,"EndAnchorFinder")}constructor(){super(...arguments),this.found=!1}visitEndAnchor(a){this.found=!0}}let r=Ml(e,i=>{let a=i.PATTERN;try{let s=Rw(a),l=new t;return l.visit(s),l.found}catch{return MTt.test(a.source)}});return hr(r,i=>({message:`Unexpected RegExp Anchor Error: + Token Type: ->`+i.name+`<- static 'PATTERN' cannot contain end of input anchor '$' + See chevrotain.io/docs/guide/resolving_lexer_errors.html#ANCHORS for details.`,type:Ni.EOI_ANCHOR_FOUND,tokenTypes:[i]}))}function V3e(e){let t=Ml(e,n=>n.PATTERN.test(""));return hr(t,n=>({message:"Token Type: ->"+n.name+"<- static 'PATTERN' must not match an empty string",type:Ni.EMPTY_MATCH_PATTERN,tokenTypes:[n]}))}function W3e(e){class t extends AR{static{o(this,"StartAnchorFinder")}static{E(this,"StartAnchorFinder")}constructor(){super(...arguments),this.found=!1}visitStartAnchor(a){this.found=!0}}let r=Ml(e,i=>{let a=i.PATTERN;try{let s=Rw(a),l=new t;return l.visit(s),l.found}catch{return NTt.test(a.source)}});return hr(r,i=>({message:`Unexpected RegExp Anchor Error: + Token Type: ->`+i.name+`<- static 'PATTERN' cannot contain start of input anchor '^' + See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#ANCHORS for details.`,type:Ni.SOI_ANCHOR_FOUND,tokenTypes:[i]}))}function q3e(e){let t=Ml(e,n=>{let i=n[Zg];return i instanceof RegExp&&(i.multiline||i.global)});return hr(t,n=>({message:"Token Type: ->"+n.name+"<- static 'PATTERN' may NOT contain global('g') or multiline('m')",type:Ni.UNSUPPORTED_FLAGS_FOUND,tokenTypes:[n]}))}function H3e(e){let t=[],r=hr(e,a=>Ko(e,(s,l)=>(a.PATTERN.source===l.PATTERN.source&&!qs(t,l)&&l.PATTERN!==Fs.NA&&(t.push(l),s.push(l)),s),[]));r=Aw(r);let n=Ml(r,a=>a.length>1);return hr(n,a=>{let s=hr(a,u=>u.name);return{message:`The same RegExp pattern ->${Rc(a).PATTERN}<-has been used in all of the following Token Types: ${s.join(", ")} <-`,type:Ni.DUPLICATE_PATTERNS_FOUND,tokenTypes:a}})}function U3e(e){let t=Ml(e,n=>{if(!wr(n,"GROUP"))return!1;let i=n.GROUP;return i!==Fs.SKIPPED&&i!==Fs.NA&&!To(i)});return hr(t,n=>({message:"Token Type: ->"+n.name+"<- static 'GROUP' can only be Lexer.SKIPPED/Lexer.NA/A String",type:Ni.INVALID_GROUP_TYPE_FOUND,tokenTypes:[n]}))}function Y3e(e,t){let r=Ml(e,i=>i.PUSH_MODE!==void 0&&!qs(t,i.PUSH_MODE));return hr(r,i=>({message:`Token Type: ->${i.name}<- static 'PUSH_MODE' value cannot refer to a Lexer Mode ->${i.PUSH_MODE}<-which does not exist`,type:Ni.PUSH_MODE_DOES_NOT_EXIST,tokenTypes:[i]}))}function j3e(e){let t=[],r=Ko(e,(n,i,a)=>{let s=i.PATTERN;return s===Fs.NA||(To(s)?n.push({str:s,idx:a,tokenType:i}):Kh(s)&&K3e(s)&&n.push({str:s.source,idx:a,tokenType:i})),n},[]);return Ir(e,(n,i)=>{Ir(r,({str:a,idx:s,tokenType:l})=>{if(i${l.name}<- can never be matched. +Because it appears AFTER the Token Type ->${n.name}<-in the lexer's definition. +See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#UNREACHABLE`;t.push({message:u,type:Ni.UNREACHABLE_PATTERN,tokenTypes:[n,l]})}})}),t}function X3e(e,t){if(Kh(t)){if(Z3e(t))return!1;let r=t.exec(e);return r!==null&&r.index===0}else{if(nd(t))return t(e,0,[],{});if(wr(t,"exec"))return t.exec(e,0,[],{});if(typeof t=="string")return t===e;throw Error("non exhaustive match")}}function K3e(e){return _v([".","\\","[","]","|","^","$","(",")","?","*","+","{"],r=>e.source.indexOf(r)!==-1)===void 0}function Z3e(e){return/(\(\?=)|(\(\?!)|(\(\?<=)|(\(\? property in its definition +`,type:Ni.MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE}),wr(e,PA)||n.push({message:"A MultiMode Lexer cannot be initialized without a <"+PA+`> property in its definition +`,type:Ni.MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY}),wr(e,PA)&&wr(e,AC)&&!wr(e.modes,e.defaultMode)&&n.push({message:`A MultiMode Lexer cannot be initialized with a ${AC}: <${e.defaultMode}>which does not exist +`,type:Ni.MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST}),wr(e,PA)&&Ir(e.modes,(i,a)=>{Ir(i,(s,l)=>{if(Zh(s))n.push({message:`A Lexer cannot be initialized using an undefined Token Type. Mode:<${a}> at index: <${l}> +`,type:Ni.LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED});else if(wr(s,"LONGER_ALT")){let u=un(s.LONGER_ALT)?s.LONGER_ALT:[s.LONGER_ALT];Ir(u,h=>{!Zh(h)&&!qs(i,h)&&n.push({message:`A MultiMode Lexer cannot be initialized with a longer_alt <${h.name}> on token <${s.name}> outside of mode <${a}> +`,type:Ni.MULTI_MODE_LEXER_LONGER_ALT_NOT_IN_CURRENT_MODE})})}})}),n}function J3e(e,t,r){let n=[],i=!1,a=Aw(Sc(ha(e.modes))),s=YR(a,u=>u[Zg]===Fs.NA),l=Lq(r);return t&&Ir(s,u=>{let h=_q(u,l);if(h!==!1){let f={message:r5e(u,h),type:h.issue,tokenType:u};n.push(f)}else wr(u,"LINE_BREAKS")?u.LINE_BREAKS===!0&&(i=!0):XR(l,u.PATTERN)&&(i=!0)}),t&&!i&&n.push({message:`Warning: No LINE_BREAKS Found. + This Lexer has been defined to track line and column information, + But none of the Token Types can be identified as matching a line terminator. + See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#LINE_BREAKS + for details.`,type:Ni.NO_LINE_BREAKS_FLAGS}),n}function e5e(e){let t={},r=jo(e);return Ir(r,n=>{let i=e[n];if(un(i))t[n]=[];else throw Error("non exhaustive match")}),t}function Rq(e){let t=e.PATTERN;if(Kh(t))return!1;if(nd(t))return!0;if(wr(t,"exec"))return!0;if(To(t))return!1;throw Error("non exhaustive match")}function t5e(e){return To(e)&&e.length===1?e.charCodeAt(0):!1}function _q(e,t){if(wr(e,"LINE_BREAKS"))return!1;if(Kh(e.PATTERN)){try{XR(t,e.PATTERN)}catch(r){return{issue:Ni.IDENTIFY_TERMINATOR,errMsg:r.message}}return!1}else{if(To(e.PATTERN))return!1;if(Rq(e))return{issue:Ni.CUSTOM_LINE_BREAK};throw Error("non exhaustive match")}}function r5e(e,t){if(t.issue===Ni.IDENTIFY_TERMINATOR)return`Warning: unable to identify line terminator usage in pattern. + The problem is in the <${e.name}> Token Type + Root cause: ${t.errMsg}. + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#IDENTIFY_TERMINATOR`;if(t.issue===Ni.CUSTOM_LINE_BREAK)return`Warning: A Custom Token Pattern should specify the option. + The problem is in the <${e.name}> Token Type + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#CUSTOM_LINE_BREAK`;throw Error("non exhaustive match")}function Lq(e){return hr(e,r=>To(r)?r.charCodeAt(0):r)}function l6(e,t,r){e[t]===void 0?e[t]=[r]:e[t].push(r)}function Qh(e){return e255?255+~~(e/255):e}}function $v(e,t){let r=e.tokenTypeIdx;return r===t.tokenTypeIdx?!0:t.isParent===!0&&t.categoryMatchesMap[r]===!0}function nw(e,t){return e.tokenTypeIdx===t.tokenTypeIdx}function Fv(e){let t=a5e(e);s5e(t),l5e(t),o5e(t),Ir(t,r=>{r.isParent=r.categoryMatches.length>0})}function a5e(e){let t=Ya(e),r=e,n=!0;for(;n;){r=Aw(Sc(hr(r,a=>a.CATEGORIES)));let i=UR(r,t);t=t.concat(i),Jn(i)?n=!1:r=i}return t}function s5e(e){Ir(e,t=>{Iq(t)||(i5e[xCe]=t,t.tokenTypeIdx=xCe++),MG(t)&&!un(t.CATEGORIES)&&(t.CATEGORIES=[t.CATEGORIES]),MG(t)||(t.CATEGORIES=[]),c5e(t)||(t.categoryMatches=[]),u5e(t)||(t.categoryMatchesMap={})})}function o5e(e){Ir(e,t=>{t.categoryMatches=[],Ir(t.categoryMatchesMap,(r,n)=>{t.categoryMatches.push(i5e[n].tokenTypeIdx)})})}function l5e(e){Ir(e,t=>{Dq([],t)})}function Dq(e,t){Ir(e,r=>{t.categoryMatchesMap[r.tokenTypeIdx]=!0}),Ir(t.CATEGORIES,r=>{let n=e.concat(t);qs(n,r)||Dq(n,r)})}function Iq(e){return wr(e,"tokenTypeIdx")}function MG(e){return wr(e,"CATEGORIES")}function c5e(e){return wr(e,"categoryMatches")}function u5e(e){return wr(e,"categoryMatchesMap")}function h5e(e){return wr(e,"tokenTypeIdx")}function Yg(e){return Mq(e)?e.LABEL:e.name}function Mq(e){return To(e.LABEL)&&e.LABEL!==""}function xv(e){return d5e(e)}function d5e(e){let t=e.pattern,r={};if(r.name=e.name,Zh(t)||(r.PATTERN=t),wr(e,OTt))throw`The parent property is no longer supported. +See: https://github.com/chevrotain/chevrotain/issues/564#issuecomment-349062346 for details.`;return wr(e,bCe)&&(r.CATEGORIES=e[bCe]),Fv([r]),wr(e,TCe)&&(r.LABEL=e[TCe]),wr(e,CCe)&&(r.GROUP=e[CCe]),wr(e,kCe)&&(r.POP_MODE=e[kCe]),wr(e,wCe)&&(r.PUSH_MODE=e[wCe]),wr(e,SCe)&&(r.LONGER_ALT=e[SCe]),wr(e,ECe)&&(r.LINE_BREAKS=e[ECe]),wr(e,ACe)&&(r.START_CHARS_HINT=e[ACe]),r}function _w(e,t,r,n,i,a,s,l){return{image:t,startOffset:r,endOffset:n,startLine:i,endLine:a,startColumn:s,endColumn:l,tokenTypeIdx:e.tokenTypeIdx,tokenType:e}}function Nq(e,t){return $v(e,t)}function f5e(e,t){let r=new $Tt(e,t);return r.resolveRefs(),r.errors}function J6(e,t,r=[]){r=Ya(r);let n=[],i=0;function a(l){return l.concat(Ha(e,i+1))}o(a,"remainingPathWith"),E(a,"remainingPathWith");function s(l){let u=J6(a(l),t,r);return n.concat(u)}for(o(s,"getAlternativesForProd"),E(s,"getAlternativesForProd");r.length{Jn(u.definition)===!1&&(n=s(u.definition))}),n;if(l instanceof ci)r.push(l.terminalType);else throw Error("non exhaustive match")}i++}return n.push({partialPath:r,suffixDef:Ha(e,i)}),n}function Pq(e,t,r,n){let i="EXIT_NONE_TERMINAL",a=[i],s="EXIT_ALTERNATIVE",l=!1,u=t.length,h=u-n-1,d=[],f=[];for(f.push({idx:-1,def:e,ruleStack:[],occurrenceStack:[]});!Jn(f);){let p=f.pop();if(p===s){l&&Kg(f).idx<=h&&f.pop();continue}let m=p.def,g=p.idx,y=p.ruleStack,v=p.occurrenceStack;if(Jn(m))continue;let x=m[0];if(x===i){let b={idx:g,def:Ha(m),ruleStack:tw(y),occurrenceStack:tw(v)};f.push(b)}else if(x instanceof ci)if(g=0;b--){let T=x.definition[b],k={idx:g,def:T.definition.concat(Ha(m)),ruleStack:y,occurrenceStack:v};f.push(k),f.push(s)}else if(x instanceof Co)f.push({idx:g,def:x.definition.concat(Ha(m)),ruleStack:y,occurrenceStack:v});else if(x instanceof Pv)f.push(p5e(x,g,y,v));else throw Error("non exhaustive match")}return d}function p5e(e,t,r,n){let i=Ya(r);i.push(e.name);let a=Ya(n);return a.push(1),{idx:t,def:e.definition,ruleStack:i,occurrenceStack:a}}function ZR(e){if(e instanceof Ua||e==="Option")return wi.OPTION;if(e instanceof Mi||e==="Repetition")return wi.REPETITION;if(e instanceof Zo||e==="RepetitionMandatory")return wi.REPETITION_MANDATORY;if(e instanceof Qo||e==="RepetitionMandatoryWithSeparator")return wi.REPETITION_MANDATORY_WITH_SEPARATOR;if(e instanceof wo||e==="RepetitionWithSeparator")return wi.REPETITION_WITH_SEPARATOR;if(e instanceof ko||e==="Alternation")return wi.ALTERNATION;throw Error("non exhaustive match")}function PG(e){let{occurrence:t,rule:r,prodType:n,maxLookahead:i}=e,a=ZR(n);return a===wi.ALTERNATION?Lw(t,r,i):Dw(t,r,a,i)}function m5e(e,t,r,n,i,a){let s=Lw(e,t,r),l=Bq(s)?nw:$v;return a(s,n,l,i)}function g5e(e,t,r,n,i,a){let s=Dw(e,t,i,r),l=Bq(s)?nw:$v;return a(s[0],l,n)}function y5e(e,t,r,n){let i=e.length,a=Ec(e,s=>Ec(s,l=>l.length===1));if(t)return function(s){let l=hr(s,u=>u.GATE);for(let u=0;uSc(u)),l=Ko(s,(u,h,d)=>(Ir(h,f=>{wr(u,f.tokenTypeIdx)||(u[f.tokenTypeIdx]=d),Ir(f.categoryMatches,p=>{wr(u,p)||(u[p]=d)})}),u),{});return function(){let u=this.LA(1);return l[u.tokenTypeIdx]}}else return function(){for(let s=0;sa.length===1),i=e.length;if(n&&!r){let a=Sc(e);if(a.length===1&&Jn(a[0].categoryMatches)){let l=a[0].tokenTypeIdx;return function(){return this.LA(1).tokenTypeIdx===l}}else{let s=Ko(a,(l,u,h)=>(l[u.tokenTypeIdx]=!0,Ir(u.categoryMatches,d=>{l[d]=!0}),l),[]);return function(){let l=this.LA(1);return s[l.tokenTypeIdx]===!0}}}else return function(){e:for(let a=0;aJ6([s],1)),n=OG(r.length),i=hr(r,s=>{let l={};return Ir(s,u=>{let h=u6(u.partialPath);Ir(h,d=>{l[d]=!0})}),l}),a=r;for(let s=1;s<=t;s++){let l=a;a=OG(l.length);for(let u=0;u{let x=u6(v.partialPath);Ir(x,b=>{i[u][b]=!0})})}}}}return n}function Lw(e,t,r,n){let i=new x5e(e,wi.ALTERNATION,n);return t.accept(i),Oq(i.result,r)}function Dw(e,t,r,n){let i=new x5e(e,r);t.accept(i);let a=i.result,l=new WTt(t,e,r).startWalking(),u=new Co({definition:a}),h=new Co({definition:l});return Oq([u,h],n)}function eR(e,t){e:for(let r=0;r{let i=t[n];return r===i||i.categoryMatchesMap[r.tokenTypeIdx]})}function Bq(e){return Ec(e,t=>Ec(t,r=>Ec(r,n=>Jn(n.categoryMatches))))}function C5e(e){let t=e.lookaheadStrategy.validate({rules:e.rules,tokenTypes:e.tokenTypes,grammarName:e.grammarName});return hr(t,r=>Object.assign({type:Vs.CUSTOM_LOOKAHEAD_VALIDATION},r))}function w5e(e,t,r,n){let i=_l(e,u=>k5e(u,r)),a=N5e(e,t,r),s=_l(e,u=>L5e(u,r)),l=_l(e,u=>E5e(u,e,n,r));return i.concat(a,s,l)}function k5e(e,t){let r=new qTt;e.accept(r);let n=r.allProductions,i=iTt(n,S5e),a=_c(i,l=>l.length>1);return hr(ha(a),l=>{let u=Rc(l),h=t.buildDuplicateFoundError(e,l),d=Tc(u),f={message:h,type:Vs.DUPLICATE_PRODUCTIONS,ruleName:e.name,dslName:d,occurrence:u.idx},p=$q(u);return p&&(f.parameter=p),f})}function S5e(e){return`${Tc(e)}_#_${e.idx}_#_${$q(e)}`}function $q(e){return e instanceof ci?e.terminalType.name:e instanceof Gs?e.nonTerminalName:""}function E5e(e,t,r,n){let i=[];if(Ko(t,(s,l)=>l.name===e.name?s+1:s,0)>1){let s=n.buildDuplicateRuleNameError({topLevelRule:e,grammarName:r});i.push({message:s,type:Vs.DUPLICATE_RULE_NAME,ruleName:e.name})}return i}function A5e(e,t,r){let n=[],i;return qs(t,e)||(i=`Invalid rule override, rule: ->${e}<- cannot be overridden in the grammar: ->${r}<-as it is not defined in any of the super grammars `,n.push({message:i,type:Vs.INVALID_RULE_OVERRIDE,ruleName:e})),n}function Fq(e,t,r,n=[]){let i=[],a=VC(t.definition);if(Jn(a))return[];{let s=e.name;qs(a,e)&&i.push({message:r.buildLeftRecursionError({topLevelRule:e,leftRecursionPath:n}),type:Vs.LEFT_RECURSION,ruleName:s});let u=UR(a,n.concat([e])),h=_l(u,d=>{let f=Ya(n);return f.push(d),Fq(e,d,r,f)});return i.concat(h)}}function VC(e){let t=[];if(Jn(e))return t;let r=Rc(e);if(r instanceof Gs)t.push(r.referencedRule);else if(r instanceof Co||r instanceof Ua||r instanceof Zo||r instanceof Qo||r instanceof wo||r instanceof Mi)t=t.concat(VC(r.definition));else if(r instanceof ko)t=Sc(hr(r.definition,a=>VC(a.definition)));else if(!(r instanceof ci))throw Error("non exhaustive match");let n=rw(r),i=e.length>1;if(n&&i){let a=Ha(e);return t.concat(VC(a))}else return t}function R5e(e,t){let r=new zq;e.accept(r);let n=r.alternations;return _l(n,a=>{let s=tw(a.definition);return _l(s,(l,u)=>{let h=Pq([l],[],$v,1);return Jn(h)?[{message:t.buildEmptyAlternationError({topLevelRule:e,alternation:a,emptyChoiceIdx:u}),type:Vs.NONE_LAST_EMPTY_ALT,ruleName:e.name,occurrence:a.idx,alternative:u+1}]:[]})})}function _5e(e,t,r){let n=new zq;e.accept(n);let i=n.alternations;return i=YR(i,s=>s.ignoreAmbiguities===!0),_l(i,s=>{let l=s.idx,u=s.maxLookahead||t,h=Lw(l,e,u,s),d=I5e(h,s,e,r),f=M5e(h,s,e,r);return d.concat(f)})}function L5e(e,t){let r=new zq;e.accept(r);let n=r.alternations;return _l(n,a=>a.definition.length>255?[{message:t.buildTooManyAlternativesError({topLevelRule:e,alternation:a}),type:Vs.TOO_MANY_ALTS,ruleName:e.name,occurrence:a.idx}]:[])}function D5e(e,t,r){let n=[];return Ir(e,i=>{let a=new HTt;i.accept(a);let s=a.allProductions;Ir(s,l=>{let u=ZR(l),h=l.maxLookahead||t,d=l.idx,p=Dw(d,i,u,h)[0];if(Jn(Sc(p))){let m=r.buildEmptyRepetitionError({topLevelRule:i,repetition:l});n.push({message:m,type:Vs.NO_NON_EMPTY_LOOKAHEAD,ruleName:i.name})}})}),n}function I5e(e,t,r,n){let i=[],a=Ko(e,(l,u,h)=>(t.definition[h].ignoreAmbiguities===!0||Ir(u,d=>{let f=[h];Ir(e,(p,m)=>{h!==m&&eR(p,d)&&t.definition[m].ignoreAmbiguities!==!0&&f.push(m)}),f.length>1&&!eR(i,d)&&(i.push(d),l.push({alts:f,path:d}))}),l),[]);return hr(a,l=>{let u=hr(l.alts,d=>d+1);return{message:n.buildAlternationAmbiguityError({topLevelRule:r,alternation:t,ambiguityIndices:u,prefixPath:l.path}),type:Vs.AMBIGUOUS_ALTS,ruleName:r.name,occurrence:t.idx,alternatives:l.alts}})}function M5e(e,t,r,n){let i=Ko(e,(s,l,u)=>{let h=hr(l,d=>({idx:u,path:d}));return s.concat(h)},[]);return Aw(_l(i,s=>{if(t.definition[s.idx].ignoreAmbiguities===!0)return[];let u=s.idx,h=s.path,d=Ml(i,p=>t.definition[p.idx].ignoreAmbiguities!==!0&&p.idx{let m=[p.idx+1,u+1],g=t.idx===0?"":t.idx;return{message:n.buildAlternationPrefixAmbiguityError({topLevelRule:r,alternation:t,ambiguityIndices:m,prefixPath:p.path}),type:Vs.AMBIGUOUS_PREFIX_ALTS,ruleName:r.name,occurrence:g,alternatives:m}})}))}function N5e(e,t,r){let n=[],i=hr(t,a=>a.name);return Ir(e,a=>{let s=a.name;if(qs(i,s)){let l=r.buildNamespaceConflictError(a);n.push({message:l,type:Vs.CONFLICT_TOKENS_RULES_NAMESPACE,ruleName:s})}}),n}function P5e(e){let t=wq(e,{errMsgProvider:BTt}),r={};return Ir(e.rules,n=>{r[n.name]=n}),f5e(r,t.errMsgProvider)}function O5e(e){return e=wq(e,{errMsgProvider:qg}),w5e(e.rules,e.tokenTypes,e.errMsgProvider,e.grammarName)}function iw(e){return qs(G5e,e.name)}function q5e(e,t,r,n,i,a,s){let l=this.getKeyForAutomaticLookahead(n,i),u=this.firstAfterRepMap[l];if(u===void 0){let p=this.getCurrRuleFullName(),m=this.getGAstProductions()[p];u=new a(m,i).startWalking(),this.firstAfterRepMap[l]=u}let h=u.token,d=u.occurrence,f=u.isEndOfRule;this.RULE_STACK.length===1&&f&&h===void 0&&(h=mp,d=1),!(h===void 0||d===void 0)&&this.shouldInRepetitionRecoveryBeTried(h,d,s)&&this.tryInRepetitionRecovery(e,t,r,h)}function d6(e,t,r){return r|t|e}function Y5e(e){OA.reset(),e.accept(OA);let t=OA.dslMethods;return OA.reset(),t}function zG(e,t){isNaN(e.startOffset)===!0?(e.startOffset=t.startOffset,e.endOffset=t.endOffset):e.endOffsets.msg);throw Error(`Errors Detected in CST Visitor <${this.constructor.name}>: + ${a.join(` + +`).replace(/\n/g,` + `)}`)}},"validateVisitor")};return r.prototype=n,r.prototype.constructor=r,r._RULE_NAMES=t,r}function Q5e(e,t,r){let n=E(function(){},"derivedConstructor");Vq(n,e+"BaseSemanticsWithDefaults");let i=Object.create(r.prototype);return Ir(t,a=>{i[a]=K5e}),n.prototype=i,n.prototype.constructor=n,n}function J5e(e,t){return eAe(e,t)}function eAe(e,t){let r=Ml(t,i=>nd(e[i])===!1),n=hr(r,i=>({msg:`Missing visitor method: <${i}> on ${e.constructor.name} CST Visitor.`,type:VG.MISSING_METHOD,methodName:i}));return Aw(n)}function nv(e,t,r,n=!1){aw(r);let i=Kg(this.recordingProdStack),a=nd(t)?t:t.DEF,s=new e({definition:[],idx:r});return n&&(s.separator=t.SEP),wr(t,"MAX_LOOKAHEAD")&&(s.maxLookahead=t.MAX_LOOKAHEAD),this.recordingProdStack.push(s),a.call(this),i.definition.push(s),this.recordingProdStack.pop(),JR}function nAe(e,t){aw(t);let r=Kg(this.recordingProdStack),n=un(e)===!1,i=n===!1?e:e.DEF,a=new ko({definition:[],idx:t,ignoreAmbiguities:n&&e.IGNORE_AMBIGUITIES===!0});wr(e,"MAX_LOOKAHEAD")&&(a.maxLookahead=e.MAX_LOOKAHEAD);let s=v3e(i,l=>nd(l.GATE));return a.hasPredicates=s,r.definition.push(a),Ir(i,l=>{let u=new Co({definition:[]});a.definition.push(u),wr(l,"IGNORE_AMBIGUITIES")?u.ignoreAmbiguities=l.IGNORE_AMBIGUITIES:wr(l,"GATE")&&(u.ignoreAmbiguities=!0),this.recordingProdStack.push(u),l.ALT.call(this),this.recordingProdStack.pop()}),JR}function WG(e){return e===0?"":`${e}`}function aw(e){if(e<0||e>DCe){let t=new Error(`Invalid DSL Method idx value: <${e}> + Idx value must be a none negative value smaller than ${DCe+1}`);throw t.KNOWN_RECORDER_ERROR=!0,t}}function iAe(e,t){t.forEach(r=>{let n=r.prototype;Object.getOwnPropertyNames(n).forEach(i=>{if(i==="constructor")return;let a=Object.getOwnPropertyDescriptor(n,i);a&&(a.get||a.set)?Object.defineProperty(e.prototype,i,a):e.prototype[i]=r.prototype[i]})})}function qG(e=void 0){return function(){return e}}function sAe(e,t){for(var r=-1,n=e==null?0:e.length,i=Array(n);++r-1}function mAe(e,t){var r=this.__data__,n=e_(r,e);return n<0?(++this.size,r.push([e,t])):r[n][1]=t,this}function f0(e){var t=-1,r=e==null?0:e.length;for(this.clear();++tl))return!1;var h=a.get(e),d=a.get(t);if(h&&d)return h==t&&d==e;var f=-1,p=!0,m=r&kwt?new jAe:void 0;for(a.set(e,t),a.set(t,e);++f-1&&e%1==0&&e-1&&e%1==0&&e<=akt}function v6e(e){return Lv(e)&&Uq(e.length)&&!!li[zv(e)]}function x6e(e){return function(t){return e(t)}}function T6e(e,t){var r=Ws(e),n=!r&&i_(e),i=!r&&!n&&nR(e),a=!r&&!n&&!i&&Yq(e),s=r||n||i||a,l=s?jwt(e.length,String):[],u=l.length;for(var h in e)(t||Okt.call(e,h))&&!(s&&(h=="length"||i&&(h=="offset"||h=="parent")||a&&(h=="buffer"||h=="byteLength"||h=="byteOffset")||g6e(h,u)))&&l.push(h);return l}function C6e(e){var t=e&&e.constructor,r=typeof t=="function"&&t.prototype||$kt;return e===r}function k6e(e,t){return function(r){return e(t(r))}}function S6e(e){if(!w6e(e))return Gkt(e);var t=[];for(var r in Object(e))Wkt.call(e,r)&&r!="constructor"&&t.push(r);return t}function A6e(e){return e!=null&&Uq(e.length)&&!AAe(e)}function R6e(e){return a_(e)?Bkt(e):E6e(e)}function _6e(e){return Vwt(e,jq,Ywt)}function L6e(e,t,r,n,i,a){var s=r&qkt,l=WCe(e),u=l.length,h=WCe(t),d=h.length;if(u!=d&&!s)return!1;for(var f=u;f--;){var p=l[f];if(!(s?p in t:Ukt.call(t,p)))return!1}var m=a.get(e),g=a.get(t);if(m&&g)return m==t&&g==e;var y=!0;a.set(e,t),a.set(t,e);for(var v=s;++frH(e,t,s));return v0(e,t,n,r,...i)}function wRe(e,t,r){let n=da(e,t,r,{type:gp});ad(e,n);let i=v0(e,t,n,r,Cp(e,t,r));return kRe(e,t,r,i)}function Cp(e,t,r){let n=HSt(Hh(r.definition,i=>rH(e,t,i)),i=>i!==void 0);return n.length===1?n[0]:n.length===0?void 0:ERe(e,n)}function nH(e,t,r,n,i){let a=n.left,s=n.right,l=da(e,t,r,{type:KSt});ad(e,l);let u=da(e,t,r,{type:pRe});return a.loopback=l,u.loopback=l,e.decisionMap[Qg(t,i?"RepetitionMandatoryWithSeparator":"RepetitionMandatory",r.idx)]=l,$i(s,l),i===void 0?($i(l,a),$i(l,u)):($i(l,u),$i(l,i.left),$i(i.right,a)),{left:a,right:u}}function iH(e,t,r,n,i){let a=n.left,s=n.right,l=da(e,t,r,{type:XSt});ad(e,l);let u=da(e,t,r,{type:pRe}),h=da(e,t,r,{type:jSt});return l.loopback=h,u.loopback=h,$i(l,a),$i(l,u),$i(s,h),i!==void 0?($i(h,u),$i(h,i.left),$i(i.right,a)):$i(h,l),e.decisionMap[Qg(t,i?"RepetitionWithSeparator":"Repetition",r.idx)]=l,{left:l,right:u}}function kRe(e,t,r,n){let i=n.left,a=n.right;return $i(i,a),e.decisionMap[Qg(t,"Option",r.idx)]=i,n}function ad(e,t){return e.decisionStates.push(t),t.decision=e.decisionStates.length-1,t.decision}function v0(e,t,r,n,...i){let a=da(e,t,n,{type:YSt,start:r});r.end=a;for(let l of i)l!==void 0?($i(r,l.left),$i(l.right,a)):$i(r,a);let s={left:r,right:a};return e.decisionMap[Qg(t,SRe(n),n.idx)]=r,s}function SRe(e){if(e instanceof ko)return"Alternation";if(e instanceof Ua)return"Option";if(e instanceof Mi)return"Repetition";if(e instanceof wo)return"RepetitionWithSeparator";if(e instanceof Zo)return"RepetitionMandatory";if(e instanceof Qo)return"RepetitionMandatoryWithSeparator";throw new Error("Invalid production type encountered")}function ERe(e,t){let r=t.length;for(let a=0;ar.stateNumber.toString()).join("_")}`}function LRe(e,t,r){for(var n=-1,i=e.length;++n0&&r(l)?t>1?sH(l,t-1,r,n,i):i6e(i,l):n||(i[i.length]=l)}return i}function PRe(e,t){return NRe(Hh(e,t),1)}function ORe(e,t,r,n){for(var i=e.length,a=r+(n?1:-1);n?a--:++a-1}function GRe(e,t,r){for(var n=-1,i=e==null?0:e.length;++n=dEt){var h=t?null:hEt(e);if(h)return Hq(h);s=!1,i=ZAe,u=new jAe}else u=t?[]:l;e:for(;++n{let i=n.toString(),a=r[i];return a!==void 0||(a={atnStartState:e,decision:t,states:{}},r[i]=a),a}}function KG(e,t=!0){let r=new Set;for(let n of e){let i=new Set;for(let a of n){if(a===void 0){if(t)break;return!1}let s=[a.tokenTypeIdx].concat(a.categoryMatches);for(let l of s)if(r.has(l)){if(!i.has(l))return!1}else r.add(l),i.add(l)}}return!0}function t_e(e){let t=e.decisionStates.length,r=Array(t);for(let n=0;nYg(i)).join(", "),r=e.production.idx===0?"":e.production.idx,n=`Ambiguous Alternatives Detected: <${e.ambiguityIndices.join(", ")}> in <${s_e(e.production)}${r}> inside <${e.topLevelRule.name}> Rule, +<${t}> may appears as a prefix path in all these alternatives. +`;return n=n+`See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#AMBIGUOUS_ALTERNATIVES +For Further details.`,n}function s_e(e){if(e instanceof Gs)return"SUBRULE";if(e instanceof Ua)return"OPTION";if(e instanceof ko)return"OR";if(e instanceof Zo)return"AT_LEAST_ONE";if(e instanceof Qo)return"AT_LEAST_ONE_SEP";if(e instanceof wo)return"MANY_SEP";if(e instanceof Mi)return"MANY";if(e instanceof ci)return"CONSUME";throw Error("non exhaustive match")}function o_e(e,t,r){let n=tEt(t.configs.elements,a=>a.state.transitions),i=pEt(n.filter(a=>a instanceof eH).map(a=>a.tokenType),a=>a.tokenTypeIdx);return{actualToken:r,possibleTokenTypes:i,tokenPath:e}}function l_e(e,t){return e.edges[t.tokenTypeIdx]}function c_e(e,t,r){let n=new XG,i=[];for(let s of e.elements){if(r.is(s.alt)===!1)continue;if(s.state.type===Iw){i.push(s);continue}let l=s.state.transitions.length;for(let u=0;u0&&!p_e(a))for(let s of i)a.add(s);return a}function u_e(e,t){if(e instanceof eH&&Nq(t,e.tokenType))return e.target}function h_e(e,t){let r;for(let n of e.elements)if(t.is(n.alt)===!0){if(r===void 0)r=n.alt;else if(r!==n.alt)return}return r}function oH(e){return{configs:e,edges:{},isAcceptState:!1,prediction:-1}}function ZG(e,t,r,n){return n=lH(e,n),t.edges[r.tokenTypeIdx]=n,n}function lH(e,t){if(t===iR)return t;let r=t.configs.key,n=e.states[r];return n!==void 0?n:(t.configs.finalize(),e.states[r]=t,t)}function d_e(e){let t=new XG,r=e.transitions.length;for(let n=0;n0){let i=[...e.stack],s={state:i.pop(),alt:e.alt,stack:i};cw(s,t)}else t.add(e);return}r.epsilonOnlyTransitions||t.add(e);let n=r.transitions.length;for(let i=0;i1)return!0;return!1}function x_e(e){for(let t of Array.from(e.values()))if(Object.keys(t).length===1)return!0;return!1}function m6(e){return e.$type===sR}function p_(e,t,r){return A_e({parser:t,tokens:r,ruleNames:new Map},e),t}function A_e(e,t){let r=_R(t,!1),n=Bn(t.rules).filter(zs).filter(a=>r.has(a));for(let a of n){let s={...e,consume:1,optional:1,subrule:1,many:1,or:1};e.parser.rule(a,yp(s,a.definition))}let i=Bn(t.rules).filter(Sv).filter(a=>r.has(a));for(let a of i)e.parser.rule(a,R_e(e,a))}function R_e(e,t){let r=t.call.rule.ref;if(!r)throw new Error("Could not resolve reference to infix operator rule: "+t.call.rule.$refText);if(Il(r))throw new Error("Cannot use terminal rule in infix expression");let n=t.operators.precedences.flatMap(m=>m.operators),i={$type:"Group",elements:[]},a={$container:i,$type:"Assignment",feature:"parts",operator:"+=",terminal:t.call},s={$container:i,$type:"Group",elements:[],cardinality:"*"};i.elements.push(a,s);let u={$container:s,$type:"Assignment",feature:"operators",operator:"+=",terminal:{$type:"Alternatives",elements:n}},h={...a,$container:s};s.elements.push(u,h);let f=n.map(m=>e.tokens[m.value]).map((m,g)=>({ALT:E(()=>e.parser.consume(g,m,u),"ALT")})),p;return m=>{p??(p=m_(e,r)),e.parser.subrule(0,p,!1,a,m),e.parser.many(0,{DEF:E(()=>{e.parser.alternatives(0,f),e.parser.subrule(1,p,!1,h,m)},"DEF")})}}function yp(e,t,r=!1){let n;if(jh(t))n=P_e(e,t);else if(fp(t))n=__e(e,t);else if(Yh(t))n=yp(e,t.terminal);else if(n0(t))n=fH(e,t);else if(Xh(t))n=L_e(e,t);else if(bR(t))n=I_e(e,t);else if(kR(t))n=M_e(e,t);else if(i0(t))n=N_e(e,t);else if(gW(t)){let i=e.consume++;n=E(()=>e.parser.consume(i,mp,t),"method")}else throw new ER(t.$cstNode,`Unexpected element type: ${t.$type}`);return pH(e,r?void 0:uw(t),n,t.cardinality)}function __e(e,t){let r=Xg(t);return()=>e.parser.action(r,t)}function L_e(e,t){let r=t.rule.ref;if(r0(r)){let n=e.subrule++,i=zs(r)&&r.fragment,a=t.arguments.length>0?D_e(r,t.arguments):()=>({}),s;return l=>{s??(s=m_(e,r)),e.parser.subrule(n,s,i,t,a(l))}}else if(Il(r)){let n=e.consume++,i=oR(e,r.name);return()=>e.parser.consume(n,i,t)}else if(r)xp(r);else throw new ER(t.$cstNode,`Undefined rule: ${t.rule.$refText}`)}function D_e(e,t){if(t.some(n=>n.calledByName)){let n=t.map(i=>({parameterName:i.parameter?.ref?.name,predicate:Cc(i.value)}));return i=>{let a={};for(let{parameterName:s,predicate:l}of n)s&&(a[s]=l(i));return a}}else{let n=t.map(i=>Cc(i.value));return i=>{let a={};for(let s=0;st(n)||r(n)}else if(pW(e)){let t=Cc(e.left),r=Cc(e.right);return n=>t(n)&&r(n)}else if(xW(e)){let t=Cc(e.value);return r=>!t(r)}else if(bW(e)){let t=e.parameter.ref.name;return r=>r!==void 0&&r[t]===!0}else if(dW(e)){let t=!!e.true;return()=>t}xp(e)}function I_e(e,t){if(t.elements.length===1)return yp(e,t.elements[0]);{let r=[];for(let i of t.elements){let a={ALT:yp(e,i,!0)},s=uw(i);s&&(a.GATE=Cc(s)),r.push(a)}let n=e.or++;return i=>e.parser.alternatives(n,r.map(a=>{let s={ALT:E(()=>a.ALT(i),"ALT")},l=a.GATE;return l&&(s.GATE=()=>l(i)),s}))}}function M_e(e,t){if(t.elements.length===1)return yp(e,t.elements[0]);let r=[];for(let l of t.elements){let u={ALT:yp(e,l,!0)},h=uw(l);h&&(u.GATE=Cc(h)),r.push(u)}let n=e.or++,i=E((l,u)=>{let h=u.getRuleStack().join("-");return`uGroup_${l}_${h}`},"idFunc"),a=E(l=>e.parser.alternatives(n,r.map((u,h)=>{let d={ALT:E(()=>!0,"ALT")},f=e.parser;d.ALT=()=>{if(u.ALT(l),!f.isRecording()){let m=i(n,f);f.unorderedGroups.get(m)||f.unorderedGroups.set(m,[]);let g=f.unorderedGroups.get(m);typeof g?.[h]>"u"&&(g[h]=!0)}};let p=u.GATE;return p?d.GATE=()=>p(l):d.GATE=()=>!f.unorderedGroups.get(i(n,f))?.[h],d})),"alternatives"),s=pH(e,uw(t),a,"*");return l=>{s(l),e.parser.isRecording()||e.parser.unorderedGroups.delete(i(n,e.parser))}}function N_e(e,t){let r=t.elements.map(n=>yp(e,n));return n=>r.forEach(i=>i(n))}function uw(e){if(i0(e))return e.guardCondition}function fH(e,t,r=t.terminal){if(r)if(Xh(r)&&zs(r.rule.ref)){let n=r.rule.ref,i=e.subrule++,a;return s=>{a??(a=m_(e,n)),e.parser.subrule(i,a,!1,t,s)}}else if(Xh(r)&&Il(r.rule.ref)){let n=e.consume++,i=oR(e,r.rule.ref.name);return()=>e.parser.consume(n,i,t)}else if(jh(r)){let n=e.consume++,i=oR(e,r.value);return()=>e.parser.consume(n,i,t)}else throw new Error("Could not build cross reference parser");else{if(!t.type.ref)throw new Error("Could not resolve reference to type: "+t.type.$refText);let i=MR(t.type.ref)?.terminal;if(!i)throw new Error("Could not find name assignment for type: "+Xg(t.type.ref));return fH(e,t,i)}}function P_e(e,t){let r=e.consume++,n=e.tokens[t.value];if(!n)throw new Error("Could not find token for keyword: "+t.value);return()=>e.parser.consume(r,n,t)}function pH(e,t,r,n){let i=t&&Cc(t);if(!n)if(i){let a=e.or++;return s=>e.parser.alternatives(a,[{ALT:E(()=>r(s),"ALT"),GATE:E(()=>i(s),"GATE")},{ALT:qG(),GATE:E(()=>!i(s),"GATE")}])}else return r;if(n==="*"){let a=e.many++;return s=>e.parser.many(a,{DEF:E(()=>r(s),"DEF"),GATE:i?()=>i(s):void 0})}else if(n==="+"){let a=e.many++;if(i){let s=e.or++;return l=>e.parser.alternatives(s,[{ALT:E(()=>e.parser.atLeastOne(a,{DEF:E(()=>r(l),"DEF")}),"ALT"),GATE:E(()=>i(l),"GATE")},{ALT:qG(),GATE:E(()=>!i(l),"GATE")}])}else return s=>e.parser.atLeastOne(a,{DEF:E(()=>r(s),"DEF")})}else if(n==="?"){let a=e.optional++;return s=>e.parser.optional(a,{DEF:E(()=>r(s),"DEF"),GATE:i?()=>i(s):void 0})}else xp(n)}function m_(e,t){let r=O_e(e,t),n=e.parser.getRule(r);if(!n)throw new Error(`Rule "${r}" not found."`);return n}function O_e(e,t){if(r0(t))return t.name;if(e.ruleNames.has(t))return e.ruleNames.get(t);{let r=t,n=r.$container,i=t.$type;for(;!zs(n);)(i0(n)||bR(n)||kR(n))&&(i=n.elements.indexOf(r).toString()+":"+i),r=n,n=n.$container;return i=n.name+":"+i,e.ruleNames.set(t,i),i}}function oR(e,t){let r=e.tokens[t];if(!r)throw new Error(`Token "${t}" not found."`);return r}function mH(e){let t=e.Grammar,r=e.parser.Lexer,n=new S_e(e);return p_(t,n,r.definition),n.finalize(),n}function gH(e){let t=yH(e);return t.finalize(),t}function yH(e){let t=e.Grammar,r=e.parser.Lexer,n=new w_e(e);return p_(t,n,r.definition)}function y_(){return new Promise(e=>{typeof setImmediate>"u"?setTimeout(e,0):setImmediate(e)})}function v_(){return g6=performance.now(),new Qn.CancellationTokenSource}function xH(e){B_e=e}function x0(e){return e===Pu}async function Ia(e){if(e===Qn.CancellationToken.None)return;let t=performance.now();if(t-g6>=B_e&&(g6=t,await y_(),g6=performance.now()),e.isCancellationRequested)throw Pu}function cR(e,t){if(e.length<=1)return e;let r=e.length/2|0,n=e.slice(0,r),i=e.slice(r);cR(n,t),cR(i,t);let a=0,s=0,l=0;for(;ar.line||t.line===r.line&&t.character>r.character?{start:r,end:t}:e}function $_e(e){let t=TH(e.range);return t!==e.range?{newText:e.newText,range:t}:e}function wH(e){return typeof e.name=="string"}function EH(e){return typeof e.$comment=="string"}function tV(e){return typeof e=="object"&&!!e&&("$ref"in e||"$error"in e)}function Hg(e){return{code:e}}function AH(e){if(e.range)return e.range;let t;return typeof e.property=="string"?t=LR(e.node.$cstNode,e.property,e.index):typeof e.keyword=="string"&&(t=KW(e.node.$cstNode,e.keyword,e.index)),t??(t=e.node.$cstNode),t?t.range:{start:{line:0,character:0},end:{line:0,character:0}}}function qC(e){switch(e){case"error":return 1;case"warning":return 2;case"info":return 3;case"hint":return 4;default:throw new Error("Invalid diagnostic severity: "+e)}}function RH(e){switch(e){case"error":return Hg(Rl.LexingError);case"warning":return Hg(Rl.LexingWarning);case"info":return Hg(Rl.LexingInfo);case"hint":return Hg(Rl.LexingHint);default:throw new Error("Invalid diagnostic severity: "+e)}}function C_(e){return Array.isArray(e)&&(e.length===0||"name"in e[0])}function w_(e){return e&&"modes"in e&&"defaultMode"in e}function dR(e){return!C_(e)&&!w_(e)}function DH(e,t,r){let n,i;typeof e=="string"?(i=t,n=r):(i=e.range.start,n=t),i||(i=En.create(0,0));let a=MH(e),s=k_(n),l=lLe({lines:a,position:i,options:s});return hLe({index:0,tokens:l,position:i})}function IH(e,t){let r=k_(t),n=MH(e);if(n.length===0)return!1;let i=n[0],a=n[n.length-1],s=r.start,l=r.end;return!!s?.exec(i)&&!!l?.exec(a)}function MH(e){let t="";return typeof e=="string"?t=e:t=e.text,t.split(uke)}function lLe(e){let t=[],r=e.position.line,n=e.position.character;for(let i=0;i=l.length){if(t.length>0){let d=En.create(r,n);t.push({type:"break",content:"",range:an.create(d,d)})}}else{awe.lastIndex=u;let d=awe.exec(l);if(d){let f=d[0],p=d[1],m=En.create(r,n+u),g=En.create(r,n+u+f.length);t.push({type:"tag",content:p,range:an.create(m,g)}),u+=f.length,u=fR(l,u)}if(u0&&t[t.length-1].type==="break"?t.slice(0,-1):t}function cLe(e,t,r,n){let i=[];if(e.length===0){let a=En.create(r,n),s=En.create(r,n+t.length);i.push({type:"text",content:t,range:an.create(a,s)})}else{let a=0;for(let l of e){let u=l.index,h=t.substring(a,u);h.length>0&&i.push({type:"text",content:t.substring(a,u),range:an.create(En.create(r,a+n),En.create(r,u+n))});let d=h.length+1,f=l[1];if(i.push({type:"inline-tag",content:f,range:an.create(En.create(r,a+d+n),En.create(r,a+d+f.length+n))}),d+=f.length,l.length===4){d+=l[2].length;let p=l[3];i.push({type:"text",content:p,range:an.create(En.create(r,a+d+n),En.create(r,a+d+p.length+n))})}else i.push({type:"text",content:"",range:an.create(En.create(r,a+d+n),En.create(r,a+d+n))});a=u+l[0].length}let s=t.substring(a);s.length>0&&i.push({type:"text",content:s,range:an.create(En.create(r,a+n),En.create(r,a+n+s.length))})}return i}function fR(e,t){let r=e.substring(t).match(IEt);return r?t+r.index:e.length}function uLe(e){let t=e.match(MEt);if(t&&typeof t.index=="number")return t.index}function hLe(e){let t=En.create(e.position.line,e.position.character);if(e.tokens.length===0)return new swe([],an.create(t,t));let r=[];for(;e.index0){let s=fR(t,n);i=t.substring(s),t=t.substring(0,n)}return(e==="linkcode"||e==="link"&&r.link==="code")&&(i=`\`${i}\``),r.renderLink?.(t,i)??gLe(t,i)}}function gLe(e,t){try{return Yo.parse(e,!0),`[${t}](${e})`}catch{return e}}function nV(e){return e.endsWith(` +`)?` +`:` + +`}function hn(e){return{documentation:{CommentProvider:E(t=>new xLe(t),"CommentProvider"),DocumentationProvider:E(t=>new vLe(t),"DocumentationProvider")},parser:{AsyncParser:E(t=>new bLe(t),"AsyncParser"),GrammarConfig:E(t=>iq(t),"GrammarConfig"),LangiumParser:E(t=>gH(t),"LangiumParser"),CompletionParser:E(t=>mH(t),"CompletionParser"),ValueConverter:E(()=>new vH,"ValueConverter"),TokenBuilder:E(()=>new g_,"TokenBuilder"),Lexer:E(t=>new LH(t),"Lexer"),ParserErrorMessageProvider:E(()=>new dH,"ParserErrorMessageProvider"),LexerErrorMessageProvider:E(()=>new oLe,"LexerErrorMessageProvider")},workspace:{AstNodeLocator:E(()=>new rLe,"AstNodeLocator"),AstNodeDescriptionProvider:E(t=>new eLe(t),"AstNodeDescriptionProvider"),ReferenceDescriptionProvider:E(t=>new tLe(t),"ReferenceDescriptionProvider")},references:{Linker:E(t=>new V_e(t),"Linker"),NameProvider:E(()=>new W_e,"NameProvider"),ScopeProvider:E(t=>new j_e(t),"ScopeProvider"),ScopeComputation:E(t=>new H_e(t),"ScopeComputation"),References:E(t=>new q_e(t),"References")},serializer:{Hydrator:E(t=>new CLe(t),"Hydrator"),JsonSerializer:E(t=>new X_e(t),"JsonSerializer")},validation:{DocumentValidator:E(t=>new J_e(t),"DocumentValidator"),ValidationRegistry:E(t=>new Z_e(t),"ValidationRegistry")},shared:E(()=>e.shared,"shared")}}function dn(e){return{ServiceRegistry:E(t=>new K_e(t),"ServiceRegistry"),workspace:{LangiumDocuments:E(t=>new G_e(t),"LangiumDocuments"),LangiumDocumentFactory:E(t=>new z_e(t),"LangiumDocumentFactory"),DocumentBuilder:E(t=>new iLe(t),"DocumentBuilder"),IndexManager:E(t=>new aLe(t),"IndexManager"),WorkspaceManager:E(t=>new sLe(t),"WorkspaceManager"),FileSystemProvider:E(t=>e.fileSystemProvider(t),"FileSystemProvider"),WorkspaceLock:E(()=>new TLe,"WorkspaceLock"),ConfigurationProvider:E(t=>new nLe(t),"ConfigurationProvider")},profilers:{}}}function Mr(e,t,r,n,i,a,s,l,u){let h=[e,t,r,n,i,a,s,l,u].reduce(Dv,{});return $H(h)}function BH(e){if(e&&e[wLe])for(let t of Object.values(e))BH(t);return e}function $H(e,t){let r=new Proxy({},{deleteProperty:E(()=>!1,"deleteProperty"),set:E(()=>{throw new Error("Cannot set property on injected service container")},"set"),get:E((n,i)=>i===wLe?!0:aV(n,i,e,t||r),"get"),getOwnPropertyDescriptor:E((n,i)=>(aV(n,i,e,t||r),Object.getOwnPropertyDescriptor(n,i)),"getOwnPropertyDescriptor"),has:E((n,i)=>i in e,"has"),ownKeys:E(()=>[...Object.getOwnPropertyNames(e)],"ownKeys")});return r}function aV(e,t,r,n){if(t in e){if(e[t]instanceof Error)throw new Error("Construction failure. Please make sure that your dependencies are constructable. Cause: "+e[t]);if(e[t]===owe)throw new Error('Cycle detected. Please make "'+String(t)+'" lazy. Visit https://langium.org/docs/reference/configuration-services/#resolving-cyclic-dependencies');return e[t]}else if(t in r){let i=r[t];e[t]=owe;try{e[t]=typeof i=="function"?i(n):$H(i,n)}catch(a){throw e[t]=a instanceof Error?a:void 0,a}return e[t]}else return}function Dv(e,t){if(t){for(let[r,n]of Object.entries(t))if(n!=null)if(typeof n=="object"){let i=e[r];typeof i=="object"&&i!==null?e[r]=Dv(i,n):e[r]=Dv({},n)}else e[r]=n}return e}function ELe(){let e=Mr(dn(yn),$Et),t=Mr(hn({shared:e}),BEt);return e.ServiceRegistry.register(t),t}function Ma(e){let t=ELe(),r=t.serializer.JsonSerializer.deserialize(e);return t.shared.workspace.LangiumDocumentFactory.fromModel(r,Yo.parse(`memory:/${r.name??"grammar"}.langium`)),r}function RLe(e){return Fi.isInstance(e,bc.$type)}function _Le(e){return Fi.isInstance(e,HC.$type)}function LLe(e){return Fi.isInstance(e,Ng.$type)}function DLe(e){return Fi.isInstance(e,up.$type)}function ILe(e){return Fi.isInstance(e,UC.$type)}function MLe(e){return Fi.isInstance(e,pR.$type)}function NLe(e){return e==="rmo"||e==="readmodel"||e==="ui"||e==="cmd"||e==="command"||e==="evt"||e==="event"||e==="pcr"||e==="processor"}function S_(e){return Fi.isInstance(e,Vh.$type)}function PLe(e){return Fi.isInstance(e,hp.$type)}function OLe(e){return Fi.isInstance(e,mv.$type)}function BLe(e){return Fi.isInstance(e,Pg.$type)}function $Le(e){return Fi.isInstance(e,Og.$type)}function FLe(e){return Fi.isInstance(e,Bg.$type)}function zLe(e){return Fi.isInstance(e,dp.$type)}function GLe(e){return Fi.isInstance(e,YC.$type)}function VLe(e){return Fi.isInstance(e,$g.$type)}function WLe(e){return Fi.isInstance(e,Fg.$type)}function qLe(e){return Fi.isInstance(e,zg.$type)}function HLe(e){return Fi.isInstance(e,Gg.$type)}function ULe(e){return Fi.isInstance(e,gv.$type)}function YLe(e){return Fi.isInstance(e,Vg.$type)}function jLe(e){return Fi.isInstance(e,La.$type)}var w0t,hw,k0t,tW,S0t,E0t,E,A0t,zr,vp,E6,mR,rW,nW,gR,wz,ZA,kz,vC,En,an,xC,Sz,QA,Ez,Az,Rz,_z,JA,Lz,Dz,Iz,bC,dg,Ru,fg,qa,zh,TC,Q1,J1,ev,e6,cC,tz,Awe,Mz,Nz,CC,Pz,t6,tv,Oz,Bz,$z,Fz,zz,Gz,Vz,Wz,wC,qz,Hz,Uz,Yz,jz,Xz,Kz,Zz,Qz,Jz,eG,kC,tG,rG,nG,iG,aG,sG,oG,lG,cG,uG,hG,dG,fG,r6,n6,pG,mG,gG,yG,vG,xG,bG,TG,Rwe,CG,OTe,nt,dw,Jg,fw,Iv,yR,Lwe,Iwe,R0t,_0t,Mwe,L0t,D0t,I0t,M0t,wG,N0t,Mv,BTe,ki,iW,P0t,O0t,B0t,$0t,F0t,z0t,G0t,V0t,W0t,q0t,H0t,U0t,Y0t,j0t,X0t,K0t,Z0t,Q0t,J0t,eyt,tyt,ryt,nyt,iyt,ayt,Owe,aW,oW,Nu,Tv,Os,Cv,XC,lW,Fwe,syt,xo,LC,ov,Uo,ap,DC,L6,D6,sp,I6,op,lp,IC,cp,MC,M6,Wh,N6,wg,P6,Du,NC,O6,lv,cv,uv,kg,B6,$6,hv,F6,xc,PC,Sg,z6,Eg,dv,G6,Ag,bo,Rg,qh,_g,OC,Lg,Dg,V6,BC,Ig,Mg,fv,_W,Tr,Mu,MW,$W,ER,zW,q6,H6,$Te,oyt,MA,lyt,cke,AR,uke,hke,cyt,Wg,fke,nq,uyt,Eke,hyt,dyt,Fu,fyt,Ll,Ake,pyt,myt,uC,gyt,yyt,vyt,xyt,byt,Tyt,FTe,bp,Ac,Cyt,NR,xw,wyt,un,kyt,zTe,GTe,Syt,Eyt,Ayt,Ryt,_yt,Dl,VTe,Lyt,Dyt,Iyt,Myt,Nyt,WTe,Pyt,Oyt,bw,Tw,Byt,$yt,Fyt,zyt,nd,Gyt,rz,qTe,Vyt,Wyt,qyt,s0,Hyt,Uyt,Yyt,jyt,Xyt,Kyt,Zyt,Qyt,Jyt,o0,e1t,EG,HTe,t1t,r1t,n1t,Da,i1t,a1t,s1t,o1t,l1t,c1t,u1t,Y6,h1t,d1t,f1t,p1t,Jke,tSe,m1t,g1t,sq,sSe,y1t,v1t,PR,oq,Cw,x1t,b1t,OR,ww,UTe,T1t,lq,C1t,cq,zu,BR,w1t,k1t,kw,S1t,E1t,YTe,TSe,A1t,R1t,_1t,$R,L1t,wSe,jTe,D1t,XTe,I1t,M1t,ZC,N1t,P1t,O1t,B1t,$1t,F1t,z1t,G1t,V1t,W1t,q1t,H1t,U1t,Y1t,j1t,X1t,K1t,Z1t,Q1t,J1t,evt,tvt,rvt,nvt,oi,ivt,Sw,ESe,$C,avt,nz,svt,pp,KTe,ovt,uq,lvt,cvt,RSe,LSe,uvt,hvt,dvt,fvt,ISe,jo,pvt,mvt,gvt,Xo,yvt,vvt,xvt,bvt,FR,Tvt,Cvt,hq,wvt,QC,kvt,Svt,Evt,Avt,Rvt,_vt,Lvt,Dvt,Ivt,Mvt,Nvt,ZTe,Pvt,zR,Ovt,Bvt,$vt,Fvt,zvt,Gvt,GR,Vvt,JC,Wvt,qvt,VR,Hvt,Uvt,Yvt,jvt,WR,Xvt,Kvt,Zvt,Qvt,Jvt,ext,txt,rxt,nxt,HR,ixt,Ew,dq,axt,fq,QTe,sxt,mq,Sc,oxt,hEe,fEe,lxt,cxt,uxt,hxt,dxt,fxt,pxt,FC,mxt,gxt,CEe,JTe,yxt,eCe,tCe,vxt,gq,EEe,xxt,bxt,rCe,Txt,yq,Cxt,wxt,kxt,REe,Sxt,DEe,AG,NEe,Ext,RG,Axt,_G,Rxt,vv,nCe,_xt,iCe,aCe,sCe,oCe,Lxt,Dxt,Ixt,Mxt,Nxt,mg,Rv,Pxt,Oxt,Bxt,$xt,j6,vq,Fxt,zxt,Gxt,lCe,cCe,Vxt,Wxt,qxt,Hxt,Uxt,Yxt,jxt,Xxt,Kxt,Zxt,Qxt,Jxt,ebt,tbt,rbt,nbt,ibt,abt,sbt,obt,lbt,cbt,ubt,hbt,dbt,uCe,fbt,pbt,mbt,gbt,hCe,ybt,vbt,xbt,bbt,Tbt,HEe,Cbt,wbt,kbt,Sbt,UEe,Ebt,Abt,Rbt,YEe,_bt,Lbt,Dbt,Ibt,Mbt,Nbt,Pbt,Obt,Bbt,$bt,Fbt,zbt,Gbt,Vbt,Wbt,qbt,Zn,Hbt,Ubt,Ya,Aw,Ybt,jbt,Xbt,xq,JEe,bq,Kbt,Zbt,r4e,Qbt,Tq,Jbt,e2t,t2t,r2t,n2t,i2t,a2t,s2t,o2t,l2t,c2t,u2t,h2t,dCe,iz,d2t,f2t,p2t,m2t,g2t,y2t,fCe,pCe,NA,v2t,mCe,x2t,l4e,b2t,T2t,C2t,h4e,w2t,p4e,k2t,S2t,v4e,E2t,A2t,R2t,_2t,L2t,D2t,I2t,Gu,M2t,N2t,P2t,O2t,B2t,$2t,F2t,d0,z2t,G2t,D4e,V2t,W2t,wq,gCe,N4e,q2t,H2t,U2t,UR,Kg,Ha,tw,Y2t,Ir,j2t,X2t,Ec,H4e,Ml,K2t,Z2t,Q2t,J2t,_v,Rc,eTt,hr,_l,tTt,rTt,nTt,iTt,aTt,sTt,oTt,wr,lTt,To,cTt,ha,uTt,qs,hTt,yCe,dTt,fTt,pTt,mTt,Jn,gTt,yTt,vCe,vTt,Kh,Zh,xTt,bTt,TTt,CTt,_c,wTt,Ko,YR,kTt,v3e,STt,ETt,ATt,RTt,_Tt,kq,Vu,Gs,Pv,Co,Ua,Zo,Qo,Mi,wo,ko,ci,Ov,jR,_3e,LTt,o6,DTt,M3e,K6,ITt,Zg,AC,PA,MTt,NTt,PTt,RC,c6,xCe,i5e,NG,Ni,_C,Fs,OTt,bCe,TCe,CCe,wCe,kCe,SCe,ECe,ACe,mp,pv,BTt,qg,$Tt,FTt,zTt,KR,GTt,RCe,VTt,_Ce,wi,WTt,x5e,qTt,zq,HTt,B5e,$5e,F5e,z5e,G5e,QR,V5e,UTt,YTt,jTt,az,W5e,XTt,KTt,ZTt,Tp,QTt,H5e,U5e,BG,$G,FG,h6,VIr,Gq,JTt,eCt,OA,tCt,VG,rCt,nCt,iCt,aCt,sCt,oCt,JR,LCe,DCe,tAe,rAe,lCt,cCt,uCt,tR,Jh,rR,Vs,Wq,hCt,oAe,dCt,uAe,e_,fCt,pCt,mCt,gCt,yCt,vCt,t_,xCt,bCt,TCt,CCt,wCt,bAe,kCt,SCt,id,ECt,$u,TAe,ACt,RCt,hC,_Ct,LCt,DCt,ICt,MCt,NCt,ICe,zv,qq,PCt,OCt,BCt,$Ct,AAe,FCt,sz,MCe,zCt,GCt,VCt,p0,WCt,qCt,HCt,UCt,YCt,jCt,XCt,KCt,ZCt,Gv,QCt,sw,JCt,ow,ewt,twt,rwt,nwt,iwt,awt,swt,owt,lwt,cwt,uwt,NCe,hwt,dwt,r_,fwt,pwt,mwt,gwt,n_,ywt,vwt,f6,xwt,bwt,Twt,jAe,Cwt,ZAe,wwt,kwt,JAe,Swt,PCe,Ewt,Hq,Awt,Rwt,_wt,Lwt,Dwt,Iwt,Mwt,Nwt,Pwt,Owt,Bwt,$wt,Fwt,OCe,oz,zwt,i6e,Gwt,Ws,Vwt,o6e,Wwt,qwt,Hwt,BCe,Uwt,Ywt,jwt,Lv,Xwt,$Ce,d6e,Kwt,Zwt,Qwt,i_,Jwt,p6e,FCe,ekt,zCe,tkt,rkt,nR,nkt,ikt,g6e,akt,Uq,skt,okt,lkt,ckt,ukt,hkt,dkt,fkt,pkt,mkt,gkt,ykt,vkt,xkt,bkt,Tkt,Ckt,wkt,kkt,Skt,Ekt,Akt,Rkt,_kt,li,Lkt,Dkt,b6e,WC,Ikt,lz,Mkt,GCe,VCe,Nkt,Yq,Pkt,Okt,Bkt,$kt,w6e,Fkt,zkt,Gkt,Vkt,Wkt,E6e,a_,jq,WCe,qkt,Hkt,Ukt,Ykt,jkt,HG,Xkt,UG,Kkt,bv,Zkt,YG,qCe,Qkt,HCe,UCe,YCe,jCe,Jkt,eSt,tSt,rSt,nSt,gg,jG,iSt,XCe,KCe,BA,aSt,ZCe,sSt,I6e,oSt,lSt,cSt,P6e,uSt,$6e,hSt,dSt,s_,fSt,pSt,Kq,mSt,gSt,ySt,vSt,xSt,bSt,TSt,CSt,wSt,QCe,JCe,kSt,SSt,H6e,ESt,l_,j6e,ASt,RSt,_St,LSt,DSt,ISt,MSt,Qq,NSt,PSt,OSt,c_,BSt,$St,FSt,zSt,GSt,VSt,u_,WSt,Hh,qSt,HSt,gp,USt,dRe,fRe,Iw,YSt,jSt,XSt,KSt,pRe,Jq,eH,mRe,tH,iR,XG,ZSt,QSt,JSt,ewe,eEt,NRe,tEt,rEt,nEt,iEt,aEt,sEt,oEt,lEt,cEt,uEt,hEt,dEt,fEt,pEt,mEt,gEt,yEt,cz,vEt,xEt,bEt,TEt,CEt,wEt,kEt,twe,e_e,rwe,SEt,b_e,cH,aR,f_,EEt,uH,sR,nwe,C_e,hH,w_e,k_e,dH,S_e,AEt,E_e,REt,g_,vH,Iu,Qn,g6,B_e,Pu,ed,iwe,lR,F_e,Yo,dC,$s,CH,en,z_e,G_e,yg,V_e,W_e,q_e,td,uR,H_e,eV,_Et,U_e,LEt,x_,kH,b_,Y_e,SH,j_e,X_e,K_e,hR,Z_e,Q_e,J_e,Rl,eLe,tLe,rLe,T_,nLe,$A,jg,iLe,aLe,sLe,oLe,_H,LH,awe,DEt,IEt,MEt,swe,uz,rV,yLe,vLe,xLe,bLe,NEt,PEt,TLe,CLe,iV,wLe,owe,sV,Ug,kLe,OEt,FH,SLe,yn,BEt,$Et,FEt,ALe,oV,lV,cV,uV,hV,dV,fV,pV,mV,gV,yV,vV,xV,bV,TV,WIr,CV,wV,v6,kV,SV,EV,vg,x6,AV,RV,FA,hz,zA,fC,dz,bc,GA,HC,lwe,VA,fz,Ng,WA,ug,qA,up,HA,cwe,K1,UC,pR,_V,LV,DV,IV,MV,NV,PV,iv,rp,OV,b6,BV,$V,T6,FV,zV,Au,xg,np,pC,uwe,pz,UA,Vh,tp,mz,_u,hwe,YA,gz,hp,mC,mv,gC,yz,yC,jA,hg,Pg,XA,vz,Og,Bg,GV,VV,WV,qV,HV,C6,av,w6,UV,k6,dp,YC,xz,KA,ip,$g,Fg,YV,zg,Lu,jV,XV,KV,Gg,S6,ZV,QV,JV,eW,bz,Z1,Tz,bg,gv,Vg,Cz,Tg,sv,La,XLe,Fi,dwe,zEt,fwe,GEt,pwe,VEt,mwe,WEt,gwe,qEt,ywe,HEt,vwe,UEt,xwe,YEt,bwe,jEt,Twe,XEt,Cwe,KEt,wwe,ZEt,kwe,QEt,Swe,JEt,Ewe,e4t,t4t,r4t,n4t,i4t,a4t,s4t,o4t,l4t,c4t,u4t,h4t,d4t,f4t,p4t,m4t,An,zH,GH,VH,WH,qH,HH,UH,YH,jH,XH,KH,ZH,QH,JH,eU,g4t,y4t,v4t,x4t,Qi,Jo,Mn,b4t,vn=F(()=>{"use strict";w0t=Object.create,hw=Object.defineProperty,k0t=Object.getOwnPropertyDescriptor,tW=Object.getOwnPropertyNames,S0t=Object.getPrototypeOf,E0t=Object.prototype.hasOwnProperty,E=o((e,t)=>hw(e,"name",{value:t,configurable:!0}),"__name"),A0t=o((e,t)=>o(function(){return e&&(t=(0,e[tW(e)[0]])(e=0)),t},"__init"),"__esm"),zr=o((e,t)=>o(function(){return t||(0,e[tW(e)[0]])((t={exports:{}}).exports,t),t.exports},"__require"),"__commonJS"),vp=o((e,t)=>{for(var r in t)hw(e,r,{get:t[r],enumerable:!0})},"__export"),E6=o((e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of tW(t))!E0t.call(e,i)&&i!==r&&hw(e,i,{get:o(()=>t[i],"get"),enumerable:!(n=k0t(t,i))||n.enumerable});return e},"__copyProps"),mR=o((e,t,r)=>(E6(e,t,"default"),r&&E6(r,t,"default")),"__reExport"),rW=o((e,t,r)=>(r=e!=null?w0t(S0t(e)):{},E6(t||!e||!e.__esModule?hw(r,"default",{value:e,enumerable:!0}):r,e)),"__toESM"),nW=o(e=>E6(hw({},"__esModule",{value:!0}),e),"__toCommonJS"),gR={};vp(gR,{AnnotatedTextEdit:o(()=>zh,"AnnotatedTextEdit"),ChangeAnnotation:o(()=>fg,"ChangeAnnotation"),ChangeAnnotationIdentifier:o(()=>qa,"ChangeAnnotationIdentifier"),CodeAction:o(()=>rG,"CodeAction"),CodeActionContext:o(()=>tG,"CodeActionContext"),CodeActionKind:o(()=>eG,"CodeActionKind"),CodeActionTriggerKind:o(()=>kC,"CodeActionTriggerKind"),CodeDescription:o(()=>Iz,"CodeDescription"),CodeLens:o(()=>nG,"CodeLens"),Color:o(()=>QA,"Color"),ColorInformation:o(()=>Ez,"ColorInformation"),ColorPresentation:o(()=>Az,"ColorPresentation"),Command:o(()=>dg,"Command"),CompletionItem:o(()=>Vz,"CompletionItem"),CompletionItemKind:o(()=>Oz,"CompletionItemKind"),CompletionItemLabelDetails:o(()=>Gz,"CompletionItemLabelDetails"),CompletionItemTag:o(()=>$z,"CompletionItemTag"),CompletionList:o(()=>Wz,"CompletionList"),CreateFile:o(()=>Q1,"CreateFile"),DeleteFile:o(()=>ev,"DeleteFile"),Diagnostic:o(()=>bC,"Diagnostic"),DiagnosticRelatedInformation:o(()=>JA,"DiagnosticRelatedInformation"),DiagnosticSeverity:o(()=>Lz,"DiagnosticSeverity"),DiagnosticTag:o(()=>Dz,"DiagnosticTag"),DocumentHighlight:o(()=>jz,"DocumentHighlight"),DocumentHighlightKind:o(()=>Yz,"DocumentHighlightKind"),DocumentLink:o(()=>aG,"DocumentLink"),DocumentSymbol:o(()=>Jz,"DocumentSymbol"),DocumentUri:o(()=>wz,"DocumentUri"),EOL:o(()=>Rwe,"EOL"),FoldingRange:o(()=>_z,"FoldingRange"),FoldingRangeKind:o(()=>Rz,"FoldingRangeKind"),FormattingOptions:o(()=>iG,"FormattingOptions"),Hover:o(()=>qz,"Hover"),InlayHint:o(()=>pG,"InlayHint"),InlayHintKind:o(()=>r6,"InlayHintKind"),InlayHintLabelPart:o(()=>n6,"InlayHintLabelPart"),InlineCompletionContext:o(()=>bG,"InlineCompletionContext"),InlineCompletionItem:o(()=>gG,"InlineCompletionItem"),InlineCompletionList:o(()=>yG,"InlineCompletionList"),InlineCompletionTriggerKind:o(()=>vG,"InlineCompletionTriggerKind"),InlineValueContext:o(()=>fG,"InlineValueContext"),InlineValueEvaluatableExpression:o(()=>dG,"InlineValueEvaluatableExpression"),InlineValueText:o(()=>uG,"InlineValueText"),InlineValueVariableLookup:o(()=>hG,"InlineValueVariableLookup"),InsertReplaceEdit:o(()=>Fz,"InsertReplaceEdit"),InsertTextFormat:o(()=>Bz,"InsertTextFormat"),InsertTextMode:o(()=>zz,"InsertTextMode"),Location:o(()=>xC,"Location"),LocationLink:o(()=>Sz,"LocationLink"),MarkedString:o(()=>wC,"MarkedString"),MarkupContent:o(()=>tv,"MarkupContent"),MarkupKind:o(()=>t6,"MarkupKind"),OptionalVersionedTextDocumentIdentifier:o(()=>CC,"OptionalVersionedTextDocumentIdentifier"),ParameterInformation:o(()=>Hz,"ParameterInformation"),Position:o(()=>En,"Position"),Range:o(()=>an,"Range"),RenameFile:o(()=>J1,"RenameFile"),SelectedCompletionInfo:o(()=>xG,"SelectedCompletionInfo"),SelectionRange:o(()=>sG,"SelectionRange"),SemanticTokenModifiers:o(()=>lG,"SemanticTokenModifiers"),SemanticTokenTypes:o(()=>oG,"SemanticTokenTypes"),SemanticTokens:o(()=>cG,"SemanticTokens"),SignatureInformation:o(()=>Uz,"SignatureInformation"),StringValue:o(()=>mG,"StringValue"),SymbolInformation:o(()=>Zz,"SymbolInformation"),SymbolKind:o(()=>Xz,"SymbolKind"),SymbolTag:o(()=>Kz,"SymbolTag"),TextDocument:o(()=>CG,"TextDocument"),TextDocumentEdit:o(()=>TC,"TextDocumentEdit"),TextDocumentIdentifier:o(()=>Mz,"TextDocumentIdentifier"),TextDocumentItem:o(()=>Pz,"TextDocumentItem"),TextEdit:o(()=>Ru,"TextEdit"),URI:o(()=>ZA,"URI"),VersionedTextDocumentIdentifier:o(()=>Nz,"VersionedTextDocumentIdentifier"),WorkspaceChange:o(()=>Awe,"WorkspaceChange"),WorkspaceEdit:o(()=>e6,"WorkspaceEdit"),WorkspaceFolder:o(()=>TG,"WorkspaceFolder"),WorkspaceSymbol:o(()=>Qz,"WorkspaceSymbol"),integer:o(()=>kz,"integer"),uinteger:o(()=>vC,"uinteger")});dw=A0t({"../../node_modules/.pnpm/vscode-languageserver-types@3.17.5/node_modules/vscode-languageserver-types/lib/esm/main.js"(){"use strict";(function(e){function t(r){return typeof r=="string"}o(t,"is"),E(t,"is"),e.is=t})(wz||(wz={})),(function(e){function t(r){return typeof r=="string"}o(t,"is"),E(t,"is"),e.is=t})(ZA||(ZA={})),(function(e){e.MIN_VALUE=-2147483648,e.MAX_VALUE=2147483647;function t(r){return typeof r=="number"&&e.MIN_VALUE<=r&&r<=e.MAX_VALUE}o(t,"is"),E(t,"is"),e.is=t})(kz||(kz={})),(function(e){e.MIN_VALUE=0,e.MAX_VALUE=2147483647;function t(r){return typeof r=="number"&&e.MIN_VALUE<=r&&r<=e.MAX_VALUE}o(t,"is"),E(t,"is"),e.is=t})(vC||(vC={})),(function(e){function t(n,i){return n===Number.MAX_VALUE&&(n=vC.MAX_VALUE),i===Number.MAX_VALUE&&(i=vC.MAX_VALUE),{line:n,character:i}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.objectLiteral(i)&&nt.uinteger(i.line)&&nt.uinteger(i.character)}o(r,"is"),E(r,"is"),e.is=r})(En||(En={})),(function(e){function t(n,i,a,s){if(nt.uinteger(n)&&nt.uinteger(i)&&nt.uinteger(a)&&nt.uinteger(s))return{start:En.create(n,i),end:En.create(a,s)};if(En.is(n)&&En.is(i))return{start:n,end:i};throw new Error(`Range#create called with invalid arguments[${n}, ${i}, ${a}, ${s}]`)}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.objectLiteral(i)&&En.is(i.start)&&En.is(i.end)}o(r,"is"),E(r,"is"),e.is=r})(an||(an={})),(function(e){function t(n,i){return{uri:n,range:i}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.objectLiteral(i)&&an.is(i.range)&&(nt.string(i.uri)||nt.undefined(i.uri))}o(r,"is"),E(r,"is"),e.is=r})(xC||(xC={})),(function(e){function t(n,i,a,s){return{targetUri:n,targetRange:i,targetSelectionRange:a,originSelectionRange:s}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.objectLiteral(i)&&an.is(i.targetRange)&&nt.string(i.targetUri)&&an.is(i.targetSelectionRange)&&(an.is(i.originSelectionRange)||nt.undefined(i.originSelectionRange))}o(r,"is"),E(r,"is"),e.is=r})(Sz||(Sz={})),(function(e){function t(n,i,a,s){return{red:n,green:i,blue:a,alpha:s}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.objectLiteral(i)&&nt.numberRange(i.red,0,1)&&nt.numberRange(i.green,0,1)&&nt.numberRange(i.blue,0,1)&&nt.numberRange(i.alpha,0,1)}o(r,"is"),E(r,"is"),e.is=r})(QA||(QA={})),(function(e){function t(n,i){return{range:n,color:i}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.objectLiteral(i)&&an.is(i.range)&&QA.is(i.color)}o(r,"is"),E(r,"is"),e.is=r})(Ez||(Ez={})),(function(e){function t(n,i,a){return{label:n,textEdit:i,additionalTextEdits:a}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.objectLiteral(i)&&nt.string(i.label)&&(nt.undefined(i.textEdit)||Ru.is(i))&&(nt.undefined(i.additionalTextEdits)||nt.typedArray(i.additionalTextEdits,Ru.is))}o(r,"is"),E(r,"is"),e.is=r})(Az||(Az={})),(function(e){e.Comment="comment",e.Imports="imports",e.Region="region"})(Rz||(Rz={})),(function(e){function t(n,i,a,s,l,u){let h={startLine:n,endLine:i};return nt.defined(a)&&(h.startCharacter=a),nt.defined(s)&&(h.endCharacter=s),nt.defined(l)&&(h.kind=l),nt.defined(u)&&(h.collapsedText=u),h}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.objectLiteral(i)&&nt.uinteger(i.startLine)&&nt.uinteger(i.startLine)&&(nt.undefined(i.startCharacter)||nt.uinteger(i.startCharacter))&&(nt.undefined(i.endCharacter)||nt.uinteger(i.endCharacter))&&(nt.undefined(i.kind)||nt.string(i.kind))}o(r,"is"),E(r,"is"),e.is=r})(_z||(_z={})),(function(e){function t(n,i){return{location:n,message:i}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.defined(i)&&xC.is(i.location)&&nt.string(i.message)}o(r,"is"),E(r,"is"),e.is=r})(JA||(JA={})),(function(e){e.Error=1,e.Warning=2,e.Information=3,e.Hint=4})(Lz||(Lz={})),(function(e){e.Unnecessary=1,e.Deprecated=2})(Dz||(Dz={})),(function(e){function t(r){let n=r;return nt.objectLiteral(n)&&nt.string(n.href)}o(t,"is"),E(t,"is"),e.is=t})(Iz||(Iz={})),(function(e){function t(n,i,a,s,l,u){let h={range:n,message:i};return nt.defined(a)&&(h.severity=a),nt.defined(s)&&(h.code=s),nt.defined(l)&&(h.source=l),nt.defined(u)&&(h.relatedInformation=u),h}o(t,"create"),E(t,"create"),e.create=t;function r(n){var i;let a=n;return nt.defined(a)&&an.is(a.range)&&nt.string(a.message)&&(nt.number(a.severity)||nt.undefined(a.severity))&&(nt.integer(a.code)||nt.string(a.code)||nt.undefined(a.code))&&(nt.undefined(a.codeDescription)||nt.string((i=a.codeDescription)===null||i===void 0?void 0:i.href))&&(nt.string(a.source)||nt.undefined(a.source))&&(nt.undefined(a.relatedInformation)||nt.typedArray(a.relatedInformation,JA.is))}o(r,"is"),E(r,"is"),e.is=r})(bC||(bC={})),(function(e){function t(n,i,...a){let s={title:n,command:i};return nt.defined(a)&&a.length>0&&(s.arguments=a),s}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.defined(i)&&nt.string(i.title)&&nt.string(i.command)}o(r,"is"),E(r,"is"),e.is=r})(dg||(dg={})),(function(e){function t(a,s){return{range:a,newText:s}}o(t,"replace"),E(t,"replace"),e.replace=t;function r(a,s){return{range:{start:a,end:a},newText:s}}o(r,"insert"),E(r,"insert"),e.insert=r;function n(a){return{range:a,newText:""}}o(n,"del"),E(n,"del"),e.del=n;function i(a){let s=a;return nt.objectLiteral(s)&&nt.string(s.newText)&&an.is(s.range)}o(i,"is"),E(i,"is"),e.is=i})(Ru||(Ru={})),(function(e){function t(n,i,a){let s={label:n};return i!==void 0&&(s.needsConfirmation=i),a!==void 0&&(s.description=a),s}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.objectLiteral(i)&&nt.string(i.label)&&(nt.boolean(i.needsConfirmation)||i.needsConfirmation===void 0)&&(nt.string(i.description)||i.description===void 0)}o(r,"is"),E(r,"is"),e.is=r})(fg||(fg={})),(function(e){function t(r){let n=r;return nt.string(n)}o(t,"is"),E(t,"is"),e.is=t})(qa||(qa={})),(function(e){function t(a,s,l){return{range:a,newText:s,annotationId:l}}o(t,"replace"),E(t,"replace"),e.replace=t;function r(a,s,l){return{range:{start:a,end:a},newText:s,annotationId:l}}o(r,"insert"),E(r,"insert"),e.insert=r;function n(a,s){return{range:a,newText:"",annotationId:s}}o(n,"del"),E(n,"del"),e.del=n;function i(a){let s=a;return Ru.is(s)&&(fg.is(s.annotationId)||qa.is(s.annotationId))}o(i,"is"),E(i,"is"),e.is=i})(zh||(zh={})),(function(e){function t(n,i){return{textDocument:n,edits:i}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.defined(i)&&CC.is(i.textDocument)&&Array.isArray(i.edits)}o(r,"is"),E(r,"is"),e.is=r})(TC||(TC={})),(function(e){function t(n,i,a){let s={kind:"create",uri:n};return i!==void 0&&(i.overwrite!==void 0||i.ignoreIfExists!==void 0)&&(s.options=i),a!==void 0&&(s.annotationId=a),s}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i&&i.kind==="create"&&nt.string(i.uri)&&(i.options===void 0||(i.options.overwrite===void 0||nt.boolean(i.options.overwrite))&&(i.options.ignoreIfExists===void 0||nt.boolean(i.options.ignoreIfExists)))&&(i.annotationId===void 0||qa.is(i.annotationId))}o(r,"is"),E(r,"is"),e.is=r})(Q1||(Q1={})),(function(e){function t(n,i,a,s){let l={kind:"rename",oldUri:n,newUri:i};return a!==void 0&&(a.overwrite!==void 0||a.ignoreIfExists!==void 0)&&(l.options=a),s!==void 0&&(l.annotationId=s),l}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i&&i.kind==="rename"&&nt.string(i.oldUri)&&nt.string(i.newUri)&&(i.options===void 0||(i.options.overwrite===void 0||nt.boolean(i.options.overwrite))&&(i.options.ignoreIfExists===void 0||nt.boolean(i.options.ignoreIfExists)))&&(i.annotationId===void 0||qa.is(i.annotationId))}o(r,"is"),E(r,"is"),e.is=r})(J1||(J1={})),(function(e){function t(n,i,a){let s={kind:"delete",uri:n};return i!==void 0&&(i.recursive!==void 0||i.ignoreIfNotExists!==void 0)&&(s.options=i),a!==void 0&&(s.annotationId=a),s}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i&&i.kind==="delete"&&nt.string(i.uri)&&(i.options===void 0||(i.options.recursive===void 0||nt.boolean(i.options.recursive))&&(i.options.ignoreIfNotExists===void 0||nt.boolean(i.options.ignoreIfNotExists)))&&(i.annotationId===void 0||qa.is(i.annotationId))}o(r,"is"),E(r,"is"),e.is=r})(ev||(ev={})),(function(e){function t(r){let n=r;return n&&(n.changes!==void 0||n.documentChanges!==void 0)&&(n.documentChanges===void 0||n.documentChanges.every(i=>nt.string(i.kind)?Q1.is(i)||J1.is(i)||ev.is(i):TC.is(i)))}o(t,"is"),E(t,"is"),e.is=t})(e6||(e6={})),cC=class{static{o(this,"TextEditChangeImpl")}static{E(this,"TextEditChangeImpl")}constructor(e,t){this.edits=e,this.changeAnnotations=t}insert(e,t,r){let n,i;if(r===void 0?n=Ru.insert(e,t):qa.is(r)?(i=r,n=zh.insert(e,t,r)):(this.assertChangeAnnotations(this.changeAnnotations),i=this.changeAnnotations.manage(r),n=zh.insert(e,t,i)),this.edits.push(n),i!==void 0)return i}replace(e,t,r){let n,i;if(r===void 0?n=Ru.replace(e,t):qa.is(r)?(i=r,n=zh.replace(e,t,r)):(this.assertChangeAnnotations(this.changeAnnotations),i=this.changeAnnotations.manage(r),n=zh.replace(e,t,i)),this.edits.push(n),i!==void 0)return i}delete(e,t){let r,n;if(t===void 0?r=Ru.del(e):qa.is(t)?(n=t,r=zh.del(e,t)):(this.assertChangeAnnotations(this.changeAnnotations),n=this.changeAnnotations.manage(t),r=zh.del(e,n)),this.edits.push(r),n!==void 0)return n}add(e){this.edits.push(e)}all(){return this.edits}clear(){this.edits.splice(0,this.edits.length)}assertChangeAnnotations(e){if(e===void 0)throw new Error("Text edit change is not configured to manage change annotations.")}},tz=class{static{o(this,"ChangeAnnotations")}static{E(this,"ChangeAnnotations")}constructor(e){this._annotations=e===void 0?Object.create(null):e,this._counter=0,this._size=0}all(){return this._annotations}get size(){return this._size}manage(e,t){let r;if(qa.is(e)?r=e:(r=this.nextId(),t=e),this._annotations[r]!==void 0)throw new Error(`Id ${r} is already in use.`);if(t===void 0)throw new Error(`No annotation provided for id ${r}`);return this._annotations[r]=t,this._size++,r}nextId(){return this._counter++,this._counter.toString()}},Awe=class{static{o(this,"WorkspaceChange")}static{E(this,"WorkspaceChange")}constructor(e){this._textEditChanges=Object.create(null),e!==void 0?(this._workspaceEdit=e,e.documentChanges?(this._changeAnnotations=new tz(e.changeAnnotations),e.changeAnnotations=this._changeAnnotations.all(),e.documentChanges.forEach(t=>{if(TC.is(t)){let r=new cC(t.edits,this._changeAnnotations);this._textEditChanges[t.textDocument.uri]=r}})):e.changes&&Object.keys(e.changes).forEach(t=>{let r=new cC(e.changes[t]);this._textEditChanges[t]=r})):this._workspaceEdit={}}get edit(){return this.initDocumentChanges(),this._changeAnnotations!==void 0&&(this._changeAnnotations.size===0?this._workspaceEdit.changeAnnotations=void 0:this._workspaceEdit.changeAnnotations=this._changeAnnotations.all()),this._workspaceEdit}getTextEditChange(e){if(CC.is(e)){if(this.initDocumentChanges(),this._workspaceEdit.documentChanges===void 0)throw new Error("Workspace edit is not configured for document changes.");let t={uri:e.uri,version:e.version},r=this._textEditChanges[t.uri];if(!r){let n=[],i={textDocument:t,edits:n};this._workspaceEdit.documentChanges.push(i),r=new cC(n,this._changeAnnotations),this._textEditChanges[t.uri]=r}return r}else{if(this.initChanges(),this._workspaceEdit.changes===void 0)throw new Error("Workspace edit is not configured for normal text edit changes.");let t=this._textEditChanges[e];if(!t){let r=[];this._workspaceEdit.changes[e]=r,t=new cC(r),this._textEditChanges[e]=t}return t}}initDocumentChanges(){this._workspaceEdit.documentChanges===void 0&&this._workspaceEdit.changes===void 0&&(this._changeAnnotations=new tz,this._workspaceEdit.documentChanges=[],this._workspaceEdit.changeAnnotations=this._changeAnnotations.all())}initChanges(){this._workspaceEdit.documentChanges===void 0&&this._workspaceEdit.changes===void 0&&(this._workspaceEdit.changes=Object.create(null))}createFile(e,t,r){if(this.initDocumentChanges(),this._workspaceEdit.documentChanges===void 0)throw new Error("Workspace edit is not configured for document changes.");let n;fg.is(t)||qa.is(t)?n=t:r=t;let i,a;if(n===void 0?i=Q1.create(e,r):(a=qa.is(n)?n:this._changeAnnotations.manage(n),i=Q1.create(e,r,a)),this._workspaceEdit.documentChanges.push(i),a!==void 0)return a}renameFile(e,t,r,n){if(this.initDocumentChanges(),this._workspaceEdit.documentChanges===void 0)throw new Error("Workspace edit is not configured for document changes.");let i;fg.is(r)||qa.is(r)?i=r:n=r;let a,s;if(i===void 0?a=J1.create(e,t,n):(s=qa.is(i)?i:this._changeAnnotations.manage(i),a=J1.create(e,t,n,s)),this._workspaceEdit.documentChanges.push(a),s!==void 0)return s}deleteFile(e,t,r){if(this.initDocumentChanges(),this._workspaceEdit.documentChanges===void 0)throw new Error("Workspace edit is not configured for document changes.");let n;fg.is(t)||qa.is(t)?n=t:r=t;let i,a;if(n===void 0?i=ev.create(e,r):(a=qa.is(n)?n:this._changeAnnotations.manage(n),i=ev.create(e,r,a)),this._workspaceEdit.documentChanges.push(i),a!==void 0)return a}},(function(e){function t(n){return{uri:n}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.defined(i)&&nt.string(i.uri)}o(r,"is"),E(r,"is"),e.is=r})(Mz||(Mz={})),(function(e){function t(n,i){return{uri:n,version:i}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.defined(i)&&nt.string(i.uri)&&nt.integer(i.version)}o(r,"is"),E(r,"is"),e.is=r})(Nz||(Nz={})),(function(e){function t(n,i){return{uri:n,version:i}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.defined(i)&&nt.string(i.uri)&&(i.version===null||nt.integer(i.version))}o(r,"is"),E(r,"is"),e.is=r})(CC||(CC={})),(function(e){function t(n,i,a,s){return{uri:n,languageId:i,version:a,text:s}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.defined(i)&&nt.string(i.uri)&&nt.string(i.languageId)&&nt.integer(i.version)&&nt.string(i.text)}o(r,"is"),E(r,"is"),e.is=r})(Pz||(Pz={})),(function(e){e.PlainText="plaintext",e.Markdown="markdown";function t(r){let n=r;return n===e.PlainText||n===e.Markdown}o(t,"is"),E(t,"is"),e.is=t})(t6||(t6={})),(function(e){function t(r){let n=r;return nt.objectLiteral(r)&&t6.is(n.kind)&&nt.string(n.value)}o(t,"is"),E(t,"is"),e.is=t})(tv||(tv={})),(function(e){e.Text=1,e.Method=2,e.Function=3,e.Constructor=4,e.Field=5,e.Variable=6,e.Class=7,e.Interface=8,e.Module=9,e.Property=10,e.Unit=11,e.Value=12,e.Enum=13,e.Keyword=14,e.Snippet=15,e.Color=16,e.File=17,e.Reference=18,e.Folder=19,e.EnumMember=20,e.Constant=21,e.Struct=22,e.Event=23,e.Operator=24,e.TypeParameter=25})(Oz||(Oz={})),(function(e){e.PlainText=1,e.Snippet=2})(Bz||(Bz={})),(function(e){e.Deprecated=1})($z||($z={})),(function(e){function t(n,i,a){return{newText:n,insert:i,replace:a}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i&&nt.string(i.newText)&&an.is(i.insert)&&an.is(i.replace)}o(r,"is"),E(r,"is"),e.is=r})(Fz||(Fz={})),(function(e){e.asIs=1,e.adjustIndentation=2})(zz||(zz={})),(function(e){function t(r){let n=r;return n&&(nt.string(n.detail)||n.detail===void 0)&&(nt.string(n.description)||n.description===void 0)}o(t,"is"),E(t,"is"),e.is=t})(Gz||(Gz={})),(function(e){function t(r){return{label:r}}o(t,"create"),E(t,"create"),e.create=t})(Vz||(Vz={})),(function(e){function t(r,n){return{items:r||[],isIncomplete:!!n}}o(t,"create"),E(t,"create"),e.create=t})(Wz||(Wz={})),(function(e){function t(n){return n.replace(/[\\`*_{}[\]()#+\-.!]/g,"\\$&")}o(t,"fromPlainText"),E(t,"fromPlainText"),e.fromPlainText=t;function r(n){let i=n;return nt.string(i)||nt.objectLiteral(i)&&nt.string(i.language)&&nt.string(i.value)}o(r,"is"),E(r,"is"),e.is=r})(wC||(wC={})),(function(e){function t(r){let n=r;return!!n&&nt.objectLiteral(n)&&(tv.is(n.contents)||wC.is(n.contents)||nt.typedArray(n.contents,wC.is))&&(r.range===void 0||an.is(r.range))}o(t,"is"),E(t,"is"),e.is=t})(qz||(qz={})),(function(e){function t(r,n){return n?{label:r,documentation:n}:{label:r}}o(t,"create"),E(t,"create"),e.create=t})(Hz||(Hz={})),(function(e){function t(r,n,...i){let a={label:r};return nt.defined(n)&&(a.documentation=n),nt.defined(i)?a.parameters=i:a.parameters=[],a}o(t,"create"),E(t,"create"),e.create=t})(Uz||(Uz={})),(function(e){e.Text=1,e.Read=2,e.Write=3})(Yz||(Yz={})),(function(e){function t(r,n){let i={range:r};return nt.number(n)&&(i.kind=n),i}o(t,"create"),E(t,"create"),e.create=t})(jz||(jz={})),(function(e){e.File=1,e.Module=2,e.Namespace=3,e.Package=4,e.Class=5,e.Method=6,e.Property=7,e.Field=8,e.Constructor=9,e.Enum=10,e.Interface=11,e.Function=12,e.Variable=13,e.Constant=14,e.String=15,e.Number=16,e.Boolean=17,e.Array=18,e.Object=19,e.Key=20,e.Null=21,e.EnumMember=22,e.Struct=23,e.Event=24,e.Operator=25,e.TypeParameter=26})(Xz||(Xz={})),(function(e){e.Deprecated=1})(Kz||(Kz={})),(function(e){function t(r,n,i,a,s){let l={name:r,kind:n,location:{uri:a,range:i}};return s&&(l.containerName=s),l}o(t,"create"),E(t,"create"),e.create=t})(Zz||(Zz={})),(function(e){function t(r,n,i,a){return a!==void 0?{name:r,kind:n,location:{uri:i,range:a}}:{name:r,kind:n,location:{uri:i}}}o(t,"create"),E(t,"create"),e.create=t})(Qz||(Qz={})),(function(e){function t(n,i,a,s,l,u){let h={name:n,detail:i,kind:a,range:s,selectionRange:l};return u!==void 0&&(h.children=u),h}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i&&nt.string(i.name)&&nt.number(i.kind)&&an.is(i.range)&&an.is(i.selectionRange)&&(i.detail===void 0||nt.string(i.detail))&&(i.deprecated===void 0||nt.boolean(i.deprecated))&&(i.children===void 0||Array.isArray(i.children))&&(i.tags===void 0||Array.isArray(i.tags))}o(r,"is"),E(r,"is"),e.is=r})(Jz||(Jz={})),(function(e){e.Empty="",e.QuickFix="quickfix",e.Refactor="refactor",e.RefactorExtract="refactor.extract",e.RefactorInline="refactor.inline",e.RefactorRewrite="refactor.rewrite",e.Source="source",e.SourceOrganizeImports="source.organizeImports",e.SourceFixAll="source.fixAll"})(eG||(eG={})),(function(e){e.Invoked=1,e.Automatic=2})(kC||(kC={})),(function(e){function t(n,i,a){let s={diagnostics:n};return i!=null&&(s.only=i),a!=null&&(s.triggerKind=a),s}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.defined(i)&&nt.typedArray(i.diagnostics,bC.is)&&(i.only===void 0||nt.typedArray(i.only,nt.string))&&(i.triggerKind===void 0||i.triggerKind===kC.Invoked||i.triggerKind===kC.Automatic)}o(r,"is"),E(r,"is"),e.is=r})(tG||(tG={})),(function(e){function t(n,i,a){let s={title:n},l=!0;return typeof i=="string"?(l=!1,s.kind=i):dg.is(i)?s.command=i:s.edit=i,l&&a!==void 0&&(s.kind=a),s}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i&&nt.string(i.title)&&(i.diagnostics===void 0||nt.typedArray(i.diagnostics,bC.is))&&(i.kind===void 0||nt.string(i.kind))&&(i.edit!==void 0||i.command!==void 0)&&(i.command===void 0||dg.is(i.command))&&(i.isPreferred===void 0||nt.boolean(i.isPreferred))&&(i.edit===void 0||e6.is(i.edit))}o(r,"is"),E(r,"is"),e.is=r})(rG||(rG={})),(function(e){function t(n,i){let a={range:n};return nt.defined(i)&&(a.data=i),a}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.defined(i)&&an.is(i.range)&&(nt.undefined(i.command)||dg.is(i.command))}o(r,"is"),E(r,"is"),e.is=r})(nG||(nG={})),(function(e){function t(n,i){return{tabSize:n,insertSpaces:i}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.defined(i)&&nt.uinteger(i.tabSize)&&nt.boolean(i.insertSpaces)}o(r,"is"),E(r,"is"),e.is=r})(iG||(iG={})),(function(e){function t(n,i,a){return{range:n,target:i,data:a}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.defined(i)&&an.is(i.range)&&(nt.undefined(i.target)||nt.string(i.target))}o(r,"is"),E(r,"is"),e.is=r})(aG||(aG={})),(function(e){function t(n,i){return{range:n,parent:i}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.objectLiteral(i)&&an.is(i.range)&&(i.parent===void 0||e.is(i.parent))}o(r,"is"),E(r,"is"),e.is=r})(sG||(sG={})),(function(e){e.namespace="namespace",e.type="type",e.class="class",e.enum="enum",e.interface="interface",e.struct="struct",e.typeParameter="typeParameter",e.parameter="parameter",e.variable="variable",e.property="property",e.enumMember="enumMember",e.event="event",e.function="function",e.method="method",e.macro="macro",e.keyword="keyword",e.modifier="modifier",e.comment="comment",e.string="string",e.number="number",e.regexp="regexp",e.operator="operator",e.decorator="decorator"})(oG||(oG={})),(function(e){e.declaration="declaration",e.definition="definition",e.readonly="readonly",e.static="static",e.deprecated="deprecated",e.abstract="abstract",e.async="async",e.modification="modification",e.documentation="documentation",e.defaultLibrary="defaultLibrary"})(lG||(lG={})),(function(e){function t(r){let n=r;return nt.objectLiteral(n)&&(n.resultId===void 0||typeof n.resultId=="string")&&Array.isArray(n.data)&&(n.data.length===0||typeof n.data[0]=="number")}o(t,"is"),E(t,"is"),e.is=t})(cG||(cG={})),(function(e){function t(n,i){return{range:n,text:i}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i!=null&&an.is(i.range)&&nt.string(i.text)}o(r,"is"),E(r,"is"),e.is=r})(uG||(uG={})),(function(e){function t(n,i,a){return{range:n,variableName:i,caseSensitiveLookup:a}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i!=null&&an.is(i.range)&&nt.boolean(i.caseSensitiveLookup)&&(nt.string(i.variableName)||i.variableName===void 0)}o(r,"is"),E(r,"is"),e.is=r})(hG||(hG={})),(function(e){function t(n,i){return{range:n,expression:i}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return i!=null&&an.is(i.range)&&(nt.string(i.expression)||i.expression===void 0)}o(r,"is"),E(r,"is"),e.is=r})(dG||(dG={})),(function(e){function t(n,i){return{frameId:n,stoppedLocation:i}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.defined(i)&&an.is(n.stoppedLocation)}o(r,"is"),E(r,"is"),e.is=r})(fG||(fG={})),(function(e){e.Type=1,e.Parameter=2;function t(r){return r===1||r===2}o(t,"is"),E(t,"is"),e.is=t})(r6||(r6={})),(function(e){function t(n){return{value:n}}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.objectLiteral(i)&&(i.tooltip===void 0||nt.string(i.tooltip)||tv.is(i.tooltip))&&(i.location===void 0||xC.is(i.location))&&(i.command===void 0||dg.is(i.command))}o(r,"is"),E(r,"is"),e.is=r})(n6||(n6={})),(function(e){function t(n,i,a){let s={position:n,label:i};return a!==void 0&&(s.kind=a),s}o(t,"create"),E(t,"create"),e.create=t;function r(n){let i=n;return nt.objectLiteral(i)&&En.is(i.position)&&(nt.string(i.label)||nt.typedArray(i.label,n6.is))&&(i.kind===void 0||r6.is(i.kind))&&i.textEdits===void 0||nt.typedArray(i.textEdits,Ru.is)&&(i.tooltip===void 0||nt.string(i.tooltip)||tv.is(i.tooltip))&&(i.paddingLeft===void 0||nt.boolean(i.paddingLeft))&&(i.paddingRight===void 0||nt.boolean(i.paddingRight))}o(r,"is"),E(r,"is"),e.is=r})(pG||(pG={})),(function(e){function t(r){return{kind:"snippet",value:r}}o(t,"createSnippet"),E(t,"createSnippet"),e.createSnippet=t})(mG||(mG={})),(function(e){function t(r,n,i,a){return{insertText:r,filterText:n,range:i,command:a}}o(t,"create"),E(t,"create"),e.create=t})(gG||(gG={})),(function(e){function t(r){return{items:r}}o(t,"create"),E(t,"create"),e.create=t})(yG||(yG={})),(function(e){e.Invoked=0,e.Automatic=1})(vG||(vG={})),(function(e){function t(r,n){return{range:r,text:n}}o(t,"create"),E(t,"create"),e.create=t})(xG||(xG={})),(function(e){function t(r,n){return{triggerKind:r,selectedCompletionInfo:n}}o(t,"create"),E(t,"create"),e.create=t})(bG||(bG={})),(function(e){function t(r){let n=r;return nt.objectLiteral(n)&&ZA.is(n.uri)&&nt.string(n.name)}o(t,"is"),E(t,"is"),e.is=t})(TG||(TG={})),Rwe=[` +`,`\r +`,"\r"],(function(e){function t(a,s,l,u){return new OTe(a,s,l,u)}o(t,"create"),E(t,"create"),e.create=t;function r(a){let s=a;return!!(nt.defined(s)&&nt.string(s.uri)&&(nt.undefined(s.languageId)||nt.string(s.languageId))&&nt.uinteger(s.lineCount)&&nt.func(s.getText)&&nt.func(s.positionAt)&&nt.func(s.offsetAt))}o(r,"is"),E(r,"is"),e.is=r;function n(a,s){let l=a.getText(),u=i(s,(d,f)=>{let p=d.range.start.line-f.range.start.line;return p===0?d.range.start.character-f.range.start.character:p}),h=l.length;for(let d=u.length-1;d>=0;d--){let f=u[d],p=a.offsetAt(f.range.start),m=a.offsetAt(f.range.end);if(m<=h)l=l.substring(0,p)+f.newText+l.substring(m,l.length);else throw new Error("Overlapping edit");h=p}return l}o(n,"applyEdits"),E(n,"applyEdits"),e.applyEdits=n;function i(a,s){if(a.length<=1)return a;let l=a.length/2|0,u=a.slice(0,l),h=a.slice(l);i(u,s),i(h,s);let d=0,f=0,p=0;for(;d0&&e.push(t.length),this._lineOffsets=e}return this._lineOffsets}positionAt(e){e=Math.max(Math.min(e,this._content.length),0);let t=this.getLineOffsets(),r=0,n=t.length;if(n===0)return En.create(0,e);for(;re?n=a:r=a+1}let i=r-1;return En.create(i,e-t[i])}offsetAt(e){let t=this.getLineOffsets();if(e.line>=t.length)return this._content.length;if(e.line<0)return 0;let r=t[e.line],n=e.line+1"u"}o(n,"undefined2"),E(n,"undefined"),e.undefined=n;function i(m){return m===!0||m===!1}o(i,"boolean"),E(i,"boolean"),e.boolean=i;function a(m){return t.call(m)==="[object String]"}o(a,"string"),E(a,"string"),e.string=a;function s(m){return t.call(m)==="[object Number]"}o(s,"number"),E(s,"number"),e.number=s;function l(m,g,y){return t.call(m)==="[object Number]"&&g<=m&&m<=y}o(l,"numberRange"),E(l,"numberRange"),e.numberRange=l;function u(m){return t.call(m)==="[object Number]"&&-2147483648<=m&&m<=2147483647}o(u,"integer2"),E(u,"integer"),e.integer=u;function h(m){return t.call(m)==="[object Number]"&&0<=m&&m<=2147483647}o(h,"uinteger2"),E(h,"uinteger"),e.uinteger=h;function d(m){return t.call(m)==="[object Function]"}o(d,"func"),E(d,"func"),e.func=d;function f(m){return m!==null&&typeof m=="object"}o(f,"objectLiteral"),E(f,"objectLiteral"),e.objectLiteral=f;function p(m,g){return Array.isArray(m)&&m.every(g)}o(p,"typedArray"),E(p,"typedArray"),e.typedArray=p})(nt||(nt={}))}}),Jg=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/ral.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var t;function r(){if(t===void 0)throw new Error("No runtime abstraction layer installed");return t}o(r,"RAL"),E(r,"RAL"),(function(n){function i(a){if(a===void 0)throw new Error("No runtime abstraction layer provided");t=a}o(i,"install"),E(i,"install"),n.install=i})(r||(r={})),e.default=r}}),fw=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/is.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.stringArray=e.array=e.func=e.error=e.number=e.string=e.boolean=void 0;function t(u){return u===!0||u===!1}o(t,"boolean"),E(t,"boolean"),e.boolean=t;function r(u){return typeof u=="string"||u instanceof String}o(r,"string"),E(r,"string"),e.string=r;function n(u){return typeof u=="number"||u instanceof Number}o(n,"number"),E(n,"number"),e.number=n;function i(u){return u instanceof Error}o(i,"error"),E(i,"error"),e.error=i;function a(u){return typeof u=="function"}o(a,"func"),E(a,"func"),e.func=a;function s(u){return Array.isArray(u)}o(s,"array"),E(s,"array"),e.array=s;function l(u){return s(u)&&u.every(h=>r(h))}o(l,"stringArray"),E(l,"stringArray"),e.stringArray=l}}),Iv=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/events.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Emitter=e.Event=void 0;var t=Jg(),r;(function(a){let s={dispose(){}};a.None=function(){return s}})(r||(e.Event=r={}));var n=class{static{o(this,"CallbackList")}static{E(this,"CallbackList")}add(a,s=null,l){this._callbacks||(this._callbacks=[],this._contexts=[]),this._callbacks.push(a),this._contexts.push(s),Array.isArray(l)&&l.push({dispose:E(()=>this.remove(a,s),"dispose")})}remove(a,s=null){if(!this._callbacks)return;let l=!1;for(let u=0,h=this._callbacks.length;u{this._callbacks||(this._callbacks=new n),this._options&&this._options.onFirstListenerAdd&&this._callbacks.isEmpty()&&this._options.onFirstListenerAdd(this),this._callbacks.add(s,l);let h={dispose:E(()=>{this._callbacks&&(this._callbacks.remove(s,l),h.dispose=_we._noop,this._options&&this._options.onLastListenerRemove&&this._callbacks.isEmpty()&&this._options.onLastListenerRemove(this))},"dispose")};return Array.isArray(u)&&u.push(h),h}),this._event}fire(s){this._callbacks&&this._callbacks.invoke.call(this._callbacks,s)}dispose(){this._callbacks&&(this._callbacks.dispose(),this._callbacks=void 0)}};e.Emitter=i,i._noop=function(){}}}),yR=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/cancellation.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CancellationTokenSource=e.CancellationToken=void 0;var t=Jg(),r=fw(),n=Iv(),i;(function(u){u.None=Object.freeze({isCancellationRequested:!1,onCancellationRequested:n.Event.None}),u.Cancelled=Object.freeze({isCancellationRequested:!0,onCancellationRequested:n.Event.None});function h(d){let f=d;return f&&(f===u.None||f===u.Cancelled||r.boolean(f.isCancellationRequested)&&!!f.onCancellationRequested)}o(h,"is"),E(h,"is"),u.is=h})(i||(e.CancellationToken=i={}));var a=Object.freeze(function(u,h){let d=(0,t.default)().timer.setTimeout(u.bind(h),0);return{dispose(){d.dispose()}}}),s=class{static{o(this,"MutableToken")}static{E(this,"MutableToken")}constructor(){this._isCancelled=!1}cancel(){this._isCancelled||(this._isCancelled=!0,this._emitter&&(this._emitter.fire(void 0),this.dispose()))}get isCancellationRequested(){return this._isCancelled}get onCancellationRequested(){return this._isCancelled?a:(this._emitter||(this._emitter=new n.Emitter),this._emitter.event)}dispose(){this._emitter&&(this._emitter.dispose(),this._emitter=void 0)}},l=class{static{o(this,"CancellationTokenSource3")}static{E(this,"CancellationTokenSource")}get token(){return this._token||(this._token=new s),this._token}cancel(){this._token?this._token.cancel():this._token=i.Cancelled}dispose(){this._token?this._token instanceof s&&this._token.dispose():this._token=i.None}};e.CancellationTokenSource=l}}),Lwe=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/messages.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Message=e.NotificationType9=e.NotificationType8=e.NotificationType7=e.NotificationType6=e.NotificationType5=e.NotificationType4=e.NotificationType3=e.NotificationType2=e.NotificationType1=e.NotificationType0=e.NotificationType=e.RequestType9=e.RequestType8=e.RequestType7=e.RequestType6=e.RequestType5=e.RequestType4=e.RequestType3=e.RequestType2=e.RequestType1=e.RequestType=e.RequestType0=e.AbstractMessageSignature=e.ParameterStructures=e.ResponseError=e.ErrorCodes=void 0;var t=fw(),r;(function(A){A.ParseError=-32700,A.InvalidRequest=-32600,A.MethodNotFound=-32601,A.InvalidParams=-32602,A.InternalError=-32603,A.jsonrpcReservedErrorRangeStart=-32099,A.serverErrorStart=-32099,A.MessageWriteError=-32099,A.MessageReadError=-32098,A.PendingResponseRejected=-32097,A.ConnectionInactive=-32096,A.ServerNotInitialized=-32002,A.UnknownErrorCode=-32001,A.jsonrpcReservedErrorRangeEnd=-32e3,A.serverErrorEnd=-32e3})(r||(e.ErrorCodes=r={}));var n=class Dwe extends Error{static{o(this,"_ResponseError")}static{E(this,"ResponseError")}constructor(M,D,P){super(D),this.code=t.number(M)?M:r.UnknownErrorCode,this.data=P,Object.setPrototypeOf(this,Dwe.prototype)}toJson(){let M={code:this.code,message:this.message};return this.data!==void 0&&(M.data=this.data),M}};e.ResponseError=n;var i=class i6{static{o(this,"_ParameterStructures")}static{E(this,"ParameterStructures")}constructor(M){this.kind=M}static is(M){return M===i6.auto||M===i6.byName||M===i6.byPosition}toString(){return this.kind}};e.ParameterStructures=i,i.auto=new i("auto"),i.byPosition=new i("byPosition"),i.byName=new i("byName");var a=class{static{o(this,"AbstractMessageSignature")}static{E(this,"AbstractMessageSignature")}constructor(A,M){this.method=A,this.numberOfParams=M}get parameterStructures(){return i.auto}};e.AbstractMessageSignature=a;var s=class extends a{static{o(this,"RequestType0")}static{E(this,"RequestType0")}constructor(A){super(A,0)}};e.RequestType0=s;var l=class extends a{static{o(this,"RequestType")}static{E(this,"RequestType")}constructor(A,M=i.auto){super(A,1),this._parameterStructures=M}get parameterStructures(){return this._parameterStructures}};e.RequestType=l;var u=class extends a{static{o(this,"RequestType1")}static{E(this,"RequestType1")}constructor(A,M=i.auto){super(A,1),this._parameterStructures=M}get parameterStructures(){return this._parameterStructures}};e.RequestType1=u;var h=class extends a{static{o(this,"RequestType2")}static{E(this,"RequestType2")}constructor(A){super(A,2)}};e.RequestType2=h;var d=class extends a{static{o(this,"RequestType3")}static{E(this,"RequestType3")}constructor(A){super(A,3)}};e.RequestType3=d;var f=class extends a{static{o(this,"RequestType4")}static{E(this,"RequestType4")}constructor(A){super(A,4)}};e.RequestType4=f;var p=class extends a{static{o(this,"RequestType5")}static{E(this,"RequestType5")}constructor(A){super(A,5)}};e.RequestType5=p;var m=class extends a{static{o(this,"RequestType6")}static{E(this,"RequestType6")}constructor(A){super(A,6)}};e.RequestType6=m;var g=class extends a{static{o(this,"RequestType7")}static{E(this,"RequestType7")}constructor(A){super(A,7)}};e.RequestType7=g;var y=class extends a{static{o(this,"RequestType8")}static{E(this,"RequestType8")}constructor(A){super(A,8)}};e.RequestType8=y;var v=class extends a{static{o(this,"RequestType9")}static{E(this,"RequestType9")}constructor(A){super(A,9)}};e.RequestType9=v;var x=class extends a{static{o(this,"NotificationType")}static{E(this,"NotificationType")}constructor(A,M=i.auto){super(A,1),this._parameterStructures=M}get parameterStructures(){return this._parameterStructures}};e.NotificationType=x;var b=class extends a{static{o(this,"NotificationType0")}static{E(this,"NotificationType0")}constructor(A){super(A,0)}};e.NotificationType0=b;var T=class extends a{static{o(this,"NotificationType1")}static{E(this,"NotificationType1")}constructor(A,M=i.auto){super(A,1),this._parameterStructures=M}get parameterStructures(){return this._parameterStructures}};e.NotificationType1=T;var k=class extends a{static{o(this,"NotificationType2")}static{E(this,"NotificationType2")}constructor(A){super(A,2)}};e.NotificationType2=k;var C=class extends a{static{o(this,"NotificationType3")}static{E(this,"NotificationType3")}constructor(A){super(A,3)}};e.NotificationType3=C;var w=class extends a{static{o(this,"NotificationType4")}static{E(this,"NotificationType4")}constructor(A){super(A,4)}};e.NotificationType4=w;var S=class extends a{static{o(this,"NotificationType5")}static{E(this,"NotificationType5")}constructor(A){super(A,5)}};e.NotificationType5=S;var R=class extends a{static{o(this,"NotificationType6")}static{E(this,"NotificationType6")}constructor(A){super(A,6)}};e.NotificationType6=R;var L=class extends a{static{o(this,"NotificationType7")}static{E(this,"NotificationType7")}constructor(A){super(A,7)}};e.NotificationType7=L;var N=class extends a{static{o(this,"NotificationType8")}static{E(this,"NotificationType8")}constructor(A){super(A,8)}};e.NotificationType8=N;var I=class extends a{static{o(this,"NotificationType9")}static{E(this,"NotificationType9")}constructor(A){super(A,9)}};e.NotificationType9=I;var _;(function(A){function M(B){let O=B;return O&&t.string(O.method)&&(t.string(O.id)||t.number(O.id))}o(M,"isRequest"),E(M,"isRequest"),A.isRequest=M;function D(B){let O=B;return O&&t.string(O.method)&&B.id===void 0}o(D,"isNotification"),E(D,"isNotification"),A.isNotification=D;function P(B){let O=B;return O&&(O.result!==void 0||!!O.error)&&(t.string(O.id)||t.number(O.id)||O.id===null)}o(P,"isResponse"),E(P,"isResponse"),A.isResponse=P})(_||(e.Message=_={}))}}),Iwe=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/linkedMap.js"(e){"use strict";var t;Object.defineProperty(e,"__esModule",{value:!0}),e.LRUCache=e.LinkedMap=e.Touch=void 0;var r;(function(a){a.None=0,a.First=1,a.AsOld=a.First,a.Last=2,a.AsNew=a.Last})(r||(e.Touch=r={}));var n=class{static{o(this,"LinkedMap")}static{E(this,"LinkedMap")}constructor(){this[t]="LinkedMap",this._map=new Map,this._head=void 0,this._tail=void 0,this._size=0,this._state=0}clear(){this._map.clear(),this._head=void 0,this._tail=void 0,this._size=0,this._state++}isEmpty(){return!this._head&&!this._tail}get size(){return this._size}get first(){return this._head?.value}get last(){return this._tail?.value}has(a){return this._map.has(a)}get(a,s=r.None){let l=this._map.get(a);if(l)return s!==r.None&&this.touch(l,s),l.value}set(a,s,l=r.None){let u=this._map.get(a);if(u)u.value=s,l!==r.None&&this.touch(u,l);else{switch(u={key:a,value:s,next:void 0,previous:void 0},l){case r.None:this.addItemLast(u);break;case r.First:this.addItemFirst(u);break;case r.Last:this.addItemLast(u);break;default:this.addItemLast(u);break}this._map.set(a,u),this._size++}return this}delete(a){return!!this.remove(a)}remove(a){let s=this._map.get(a);if(s)return this._map.delete(a),this.removeItem(s),this._size--,s.value}shift(){if(!this._head&&!this._tail)return;if(!this._head||!this._tail)throw new Error("Invalid list");let a=this._head;return this._map.delete(a.key),this.removeItem(a),this._size--,a.value}forEach(a,s){let l=this._state,u=this._head;for(;u;){if(s?a.bind(s)(u.value,u.key,this):a(u.value,u.key,this),this._state!==l)throw new Error("LinkedMap got modified during iteration.");u=u.next}}keys(){let a=this._state,s=this._head,l={[Symbol.iterator]:()=>l,next:E(()=>{if(this._state!==a)throw new Error("LinkedMap got modified during iteration.");if(s){let u={value:s.key,done:!1};return s=s.next,u}else return{value:void 0,done:!0}},"next")};return l}values(){let a=this._state,s=this._head,l={[Symbol.iterator]:()=>l,next:E(()=>{if(this._state!==a)throw new Error("LinkedMap got modified during iteration.");if(s){let u={value:s.value,done:!1};return s=s.next,u}else return{value:void 0,done:!0}},"next")};return l}entries(){let a=this._state,s=this._head,l={[Symbol.iterator]:()=>l,next:E(()=>{if(this._state!==a)throw new Error("LinkedMap got modified during iteration.");if(s){let u={value:[s.key,s.value],done:!1};return s=s.next,u}else return{value:void 0,done:!0}},"next")};return l}[(t=Symbol.toStringTag,Symbol.iterator)](){return this.entries()}trimOld(a){if(a>=this.size)return;if(a===0){this.clear();return}let s=this._head,l=this.size;for(;s&&l>a;)this._map.delete(s.key),s=s.next,l--;this._head=s,this._size=l,s&&(s.previous=void 0),this._state++}addItemFirst(a){if(!this._head&&!this._tail)this._tail=a;else if(this._head)a.next=this._head,this._head.previous=a;else throw new Error("Invalid list");this._head=a,this._state++}addItemLast(a){if(!this._head&&!this._tail)this._head=a;else if(this._tail)a.previous=this._tail,this._tail.next=a;else throw new Error("Invalid list");this._tail=a,this._state++}removeItem(a){if(a===this._head&&a===this._tail)this._head=void 0,this._tail=void 0;else if(a===this._head){if(!a.next)throw new Error("Invalid list");a.next.previous=void 0,this._head=a.next}else if(a===this._tail){if(!a.previous)throw new Error("Invalid list");a.previous.next=void 0,this._tail=a.previous}else{let s=a.next,l=a.previous;if(!s||!l)throw new Error("Invalid list");s.previous=l,l.next=s}a.next=void 0,a.previous=void 0,this._state++}touch(a,s){if(!this._head||!this._tail)throw new Error("Invalid list");if(!(s!==r.First&&s!==r.Last)){if(s===r.First){if(a===this._head)return;let l=a.next,u=a.previous;a===this._tail?(u.next=void 0,this._tail=u):(l.previous=u,u.next=l),a.previous=void 0,a.next=this._head,this._head.previous=a,this._head=a,this._state++}else if(s===r.Last){if(a===this._tail)return;let l=a.next,u=a.previous;a===this._head?(l.previous=void 0,this._head=l):(l.previous=u,u.next=l),a.next=void 0,a.previous=this._tail,this._tail.next=a,this._tail=a,this._state++}}}toJSON(){let a=[];return this.forEach((s,l)=>{a.push([l,s])}),a}fromJSON(a){this.clear();for(let[s,l]of a)this.set(s,l)}};e.LinkedMap=n;var i=class extends n{static{o(this,"LRUCache")}static{E(this,"LRUCache")}constructor(a,s=1){super(),this._limit=a,this._ratio=Math.min(Math.max(0,s),1)}get limit(){return this._limit}set limit(a){this._limit=a,this.checkTrim()}get ratio(){return this._ratio}set ratio(a){this._ratio=Math.min(Math.max(0,a),1),this.checkTrim()}get(a,s=r.AsNew){return super.get(a,s)}peek(a){return super.get(a,r.None)}set(a,s){return super.set(a,s,r.Last),this.checkTrim(),this}checkTrim(){this.size>this._limit&&this.trimOld(Math.round(this._limit*this._ratio))}};e.LRUCache=i}}),R0t=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/disposable.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Disposable=void 0;var t;(function(r){function n(i){return{dispose:i}}o(n,"create"),E(n,"create"),r.create=n})(t||(e.Disposable=t={}))}}),_0t=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/sharedArrayCancellation.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SharedArrayReceiverStrategy=e.SharedArraySenderStrategy=void 0;var t=yR(),r;(function(l){l.Continue=0,l.Cancelled=1})(r||(r={}));var n=class{static{o(this,"SharedArraySenderStrategy")}static{E(this,"SharedArraySenderStrategy")}constructor(){this.buffers=new Map}enableCancellation(l){if(l.id===null)return;let u=new SharedArrayBuffer(4),h=new Int32Array(u,0,1);h[0]=r.Continue,this.buffers.set(l.id,u),l.$cancellationData=u}async sendCancellation(l,u){let h=this.buffers.get(u);if(h===void 0)return;let d=new Int32Array(h,0,1);Atomics.store(d,0,r.Cancelled)}cleanup(l){this.buffers.delete(l)}dispose(){this.buffers.clear()}};e.SharedArraySenderStrategy=n;var i=class{static{o(this,"SharedArrayBufferCancellationToken")}static{E(this,"SharedArrayBufferCancellationToken")}constructor(l){this.data=new Int32Array(l,0,1)}get isCancellationRequested(){return Atomics.load(this.data,0)===r.Cancelled}get onCancellationRequested(){throw new Error("Cancellation over SharedArrayBuffer doesn't support cancellation events")}},a=class{static{o(this,"SharedArrayBufferCancellationTokenSource")}static{E(this,"SharedArrayBufferCancellationTokenSource")}constructor(l){this.token=new i(l)}cancel(){}dispose(){}},s=class{static{o(this,"SharedArrayReceiverStrategy")}static{E(this,"SharedArrayReceiverStrategy")}constructor(){this.kind="request"}createCancellationTokenSource(l){let u=l.$cancellationData;return u===void 0?new t.CancellationTokenSource:new a(u)}};e.SharedArrayReceiverStrategy=s}}),Mwe=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/semaphore.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Semaphore=void 0;var t=Jg(),r=class{static{o(this,"Semaphore")}static{E(this,"Semaphore")}constructor(n=1){if(n<=0)throw new Error("Capacity must be greater than 0");this._capacity=n,this._active=0,this._waiting=[]}lock(n){return new Promise((i,a)=>{this._waiting.push({thunk:n,resolve:i,reject:a}),this.runNext()})}get active(){return this._active}runNext(){this._waiting.length===0||this._active===this._capacity||(0,t.default)().timer.setImmediate(()=>this.doRunNext())}doRunNext(){if(this._waiting.length===0||this._active===this._capacity)return;let n=this._waiting.shift();if(this._active++,this._active>this._capacity)throw new Error("To many thunks active");try{let i=n.thunk();i instanceof Promise?i.then(a=>{this._active--,n.resolve(a),this.runNext()},a=>{this._active--,n.reject(a),this.runNext()}):(this._active--,n.resolve(i),this.runNext())}catch(i){this._active--,n.reject(i),this.runNext()}}};e.Semaphore=r}}),L0t=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/messageReader.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ReadableStreamMessageReader=e.AbstractMessageReader=e.MessageReader=void 0;var t=Jg(),r=fw(),n=Iv(),i=Mwe(),a;(function(h){function d(f){let p=f;return p&&r.func(p.listen)&&r.func(p.dispose)&&r.func(p.onError)&&r.func(p.onClose)&&r.func(p.onPartialMessage)}o(d,"is"),E(d,"is"),h.is=d})(a||(e.MessageReader=a={}));var s=class{static{o(this,"AbstractMessageReader")}static{E(this,"AbstractMessageReader")}constructor(){this.errorEmitter=new n.Emitter,this.closeEmitter=new n.Emitter,this.partialMessageEmitter=new n.Emitter}dispose(){this.errorEmitter.dispose(),this.closeEmitter.dispose()}get onError(){return this.errorEmitter.event}fireError(h){this.errorEmitter.fire(this.asError(h))}get onClose(){return this.closeEmitter.event}fireClose(){this.closeEmitter.fire(void 0)}get onPartialMessage(){return this.partialMessageEmitter.event}firePartialMessage(h){this.partialMessageEmitter.fire(h)}asError(h){return h instanceof Error?h:new Error(`Reader received error. Reason: ${r.string(h.message)?h.message:"unknown"}`)}};e.AbstractMessageReader=s;var l;(function(h){function d(f){let p,m,g,y=new Map,v,x=new Map;if(f===void 0||typeof f=="string")p=f??"utf-8";else{if(p=f.charset??"utf-8",f.contentDecoder!==void 0&&(g=f.contentDecoder,y.set(g.name,g)),f.contentDecoders!==void 0)for(let b of f.contentDecoders)y.set(b.name,b);if(f.contentTypeDecoder!==void 0&&(v=f.contentTypeDecoder,x.set(v.name,v)),f.contentTypeDecoders!==void 0)for(let b of f.contentTypeDecoders)x.set(b.name,b)}return v===void 0&&(v=(0,t.default)().applicationJson.decoder,x.set(v.name,v)),{charset:p,contentDecoder:g,contentDecoders:y,contentTypeDecoder:v,contentTypeDecoders:x}}o(d,"fromOptions"),E(d,"fromOptions"),h.fromOptions=d})(l||(l={}));var u=class extends s{static{o(this,"ReadableStreamMessageReader")}static{E(this,"ReadableStreamMessageReader")}constructor(h,d){super(),this.readable=h,this.options=l.fromOptions(d),this.buffer=(0,t.default)().messageBuffer.create(this.options.charset),this._partialMessageTimeout=1e4,this.nextMessageLength=-1,this.messageToken=0,this.readSemaphore=new i.Semaphore(1)}set partialMessageTimeout(h){this._partialMessageTimeout=h}get partialMessageTimeout(){return this._partialMessageTimeout}listen(h){this.nextMessageLength=-1,this.messageToken=0,this.partialMessageTimer=void 0,this.callback=h;let d=this.readable.onData(f=>{this.onData(f)});return this.readable.onError(f=>this.fireError(f)),this.readable.onClose(()=>this.fireClose()),d}onData(h){try{for(this.buffer.append(h);;){if(this.nextMessageLength===-1){let f=this.buffer.tryReadHeaders(!0);if(!f)return;let p=f.get("content-length");if(!p){this.fireError(new Error(`Header must provide a Content-Length property. +${JSON.stringify(Object.fromEntries(f))}`));return}let m=parseInt(p);if(isNaN(m)){this.fireError(new Error(`Content-Length value must be a number. Got ${p}`));return}this.nextMessageLength=m}let d=this.buffer.tryReadBody(this.nextMessageLength);if(d===void 0){this.setPartialMessageTimer();return}this.clearPartialMessageTimer(),this.nextMessageLength=-1,this.readSemaphore.lock(async()=>{let f=this.options.contentDecoder!==void 0?await this.options.contentDecoder.decode(d):d,p=await this.options.contentTypeDecoder.decode(f,this.options);this.callback(p)}).catch(f=>{this.fireError(f)})}}catch(d){this.fireError(d)}}clearPartialMessageTimer(){this.partialMessageTimer&&(this.partialMessageTimer.dispose(),this.partialMessageTimer=void 0)}setPartialMessageTimer(){this.clearPartialMessageTimer(),!(this._partialMessageTimeout<=0)&&(this.partialMessageTimer=(0,t.default)().timer.setTimeout((h,d)=>{this.partialMessageTimer=void 0,h===this.messageToken&&(this.firePartialMessage({messageToken:h,waitingTime:d}),this.setPartialMessageTimer())},this._partialMessageTimeout,this.messageToken,this._partialMessageTimeout))}};e.ReadableStreamMessageReader=u}}),D0t=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/messageWriter.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.WriteableStreamMessageWriter=e.AbstractMessageWriter=e.MessageWriter=void 0;var t=Jg(),r=fw(),n=Mwe(),i=Iv(),a="Content-Length: ",s=`\r +`,l;(function(f){function p(m){let g=m;return g&&r.func(g.dispose)&&r.func(g.onClose)&&r.func(g.onError)&&r.func(g.write)}o(p,"is"),E(p,"is"),f.is=p})(l||(e.MessageWriter=l={}));var u=class{static{o(this,"AbstractMessageWriter")}static{E(this,"AbstractMessageWriter")}constructor(){this.errorEmitter=new i.Emitter,this.closeEmitter=new i.Emitter}dispose(){this.errorEmitter.dispose(),this.closeEmitter.dispose()}get onError(){return this.errorEmitter.event}fireError(f,p,m){this.errorEmitter.fire([this.asError(f),p,m])}get onClose(){return this.closeEmitter.event}fireClose(){this.closeEmitter.fire(void 0)}asError(f){return f instanceof Error?f:new Error(`Writer received error. Reason: ${r.string(f.message)?f.message:"unknown"}`)}};e.AbstractMessageWriter=u;var h;(function(f){function p(m){return m===void 0||typeof m=="string"?{charset:m??"utf-8",contentTypeEncoder:(0,t.default)().applicationJson.encoder}:{charset:m.charset??"utf-8",contentEncoder:m.contentEncoder,contentTypeEncoder:m.contentTypeEncoder??(0,t.default)().applicationJson.encoder}}o(p,"fromOptions"),E(p,"fromOptions"),f.fromOptions=p})(h||(h={}));var d=class extends u{static{o(this,"WriteableStreamMessageWriter")}static{E(this,"WriteableStreamMessageWriter")}constructor(f,p){super(),this.writable=f,this.options=h.fromOptions(p),this.errorCount=0,this.writeSemaphore=new n.Semaphore(1),this.writable.onError(m=>this.fireError(m)),this.writable.onClose(()=>this.fireClose())}async write(f){return this.writeSemaphore.lock(async()=>this.options.contentTypeEncoder.encode(f,this.options).then(m=>this.options.contentEncoder!==void 0?this.options.contentEncoder.encode(m):m).then(m=>{let g=[];return g.push(a,m.byteLength.toString(),s),g.push(s),this.doWrite(f,g,m)},m=>{throw this.fireError(m),m}))}async doWrite(f,p,m){try{return await this.writable.write(p.join(""),"ascii"),this.writable.write(m)}catch(g){return this.handleError(g,f),Promise.reject(g)}}handleError(f,p){this.errorCount++,this.fireError(f,p,this.errorCount)}end(){this.writable.end()}};e.WriteableStreamMessageWriter=d}}),I0t=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/messageBuffer.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractMessageBuffer=void 0;var t=13,r=10,n=`\r +`,i=class{static{o(this,"AbstractMessageBuffer")}static{E(this,"AbstractMessageBuffer")}constructor(a="utf-8"){this._encoding=a,this._chunks=[],this._totalLength=0}get encoding(){return this._encoding}append(a){let s=typeof a=="string"?this.fromString(a,this._encoding):a;this._chunks.push(s),this._totalLength+=s.byteLength}tryReadHeaders(a=!1){if(this._chunks.length===0)return;let s=0,l=0,u=0,h=0;e:for(;lthis._totalLength)throw new Error("Cannot read so many bytes!");if(this._chunks[0].byteLength===a){let h=this._chunks[0];return this._chunks.shift(),this._totalLength-=a,this.asNative(h)}if(this._chunks[0].byteLength>a){let h=this._chunks[0],d=this.asNative(h,a);return this._chunks[0]=h.slice(a),this._totalLength-=a,d}let s=this.allocNative(a),l=0,u=0;for(;a>0;){let h=this._chunks[u];if(h.byteLength>a){let d=h.slice(0,a);s.set(d,l),l+=a,this._chunks[u]=h.slice(a),this._totalLength-=a,a-=a}else s.set(h,l),l+=h.byteLength,this._chunks.shift(),this._totalLength-=h.byteLength,a-=h.byteLength}return s}};e.AbstractMessageBuffer=i}}),M0t=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/connection.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.createMessageConnection=e.ConnectionOptions=e.MessageStrategy=e.CancellationStrategy=e.CancellationSenderStrategy=e.CancellationReceiverStrategy=e.RequestCancellationReceiverStrategy=e.IdCancellationReceiverStrategy=e.ConnectionStrategy=e.ConnectionError=e.ConnectionErrors=e.LogTraceNotification=e.SetTraceNotification=e.TraceFormat=e.TraceValues=e.Trace=e.NullLogger=e.ProgressType=e.ProgressToken=void 0;var t=Jg(),r=fw(),n=Lwe(),i=Iwe(),a=Iv(),s=yR(),l;(function(A){A.type=new n.NotificationType("$/cancelRequest")})(l||(l={}));var u;(function(A){function M(D){return typeof D=="string"||typeof D=="number"}o(M,"is"),E(M,"is"),A.is=M})(u||(e.ProgressToken=u={}));var h;(function(A){A.type=new n.NotificationType("$/progress")})(h||(h={}));var d=class{static{o(this,"ProgressType")}static{E(this,"ProgressType")}constructor(){}};e.ProgressType=d;var f;(function(A){function M(D){return r.func(D)}o(M,"is"),E(M,"is"),A.is=M})(f||(f={})),e.NullLogger=Object.freeze({error:E(()=>{},"error"),warn:E(()=>{},"warn"),info:E(()=>{},"info"),log:E(()=>{},"log")});var p;(function(A){A[A.Off=0]="Off",A[A.Messages=1]="Messages",A[A.Compact=2]="Compact",A[A.Verbose=3]="Verbose"})(p||(e.Trace=p={}));var m;(function(A){A.Off="off",A.Messages="messages",A.Compact="compact",A.Verbose="verbose"})(m||(e.TraceValues=m={})),(function(A){function M(P){if(!r.string(P))return A.Off;switch(P=P.toLowerCase(),P){case"off":return A.Off;case"messages":return A.Messages;case"compact":return A.Compact;case"verbose":return A.Verbose;default:return A.Off}}o(M,"fromString"),E(M,"fromString"),A.fromString=M;function D(P){switch(P){case A.Off:return"off";case A.Messages:return"messages";case A.Compact:return"compact";case A.Verbose:return"verbose";default:return"off"}}o(D,"toString4"),E(D,"toString"),A.toString=D})(p||(e.Trace=p={}));var g;(function(A){A.Text="text",A.JSON="json"})(g||(e.TraceFormat=g={})),(function(A){function M(D){return r.string(D)?(D=D.toLowerCase(),D==="json"?A.JSON:A.Text):A.Text}o(M,"fromString"),E(M,"fromString"),A.fromString=M})(g||(e.TraceFormat=g={}));var y;(function(A){A.type=new n.NotificationType("$/setTrace")})(y||(e.SetTraceNotification=y={}));var v;(function(A){A.type=new n.NotificationType("$/logTrace")})(v||(e.LogTraceNotification=v={}));var x;(function(A){A[A.Closed=1]="Closed",A[A.Disposed=2]="Disposed",A[A.AlreadyListening=3]="AlreadyListening"})(x||(e.ConnectionErrors=x={}));var b=class Nwe extends Error{static{o(this,"_ConnectionError")}static{E(this,"ConnectionError")}constructor(M,D){super(D),this.code=M,Object.setPrototypeOf(this,Nwe.prototype)}};e.ConnectionError=b;var T;(function(A){function M(D){let P=D;return P&&r.func(P.cancelUndispatched)}o(M,"is"),E(M,"is"),A.is=M})(T||(e.ConnectionStrategy=T={}));var k;(function(A){function M(D){let P=D;return P&&(P.kind===void 0||P.kind==="id")&&r.func(P.createCancellationTokenSource)&&(P.dispose===void 0||r.func(P.dispose))}o(M,"is"),E(M,"is"),A.is=M})(k||(e.IdCancellationReceiverStrategy=k={}));var C;(function(A){function M(D){let P=D;return P&&P.kind==="request"&&r.func(P.createCancellationTokenSource)&&(P.dispose===void 0||r.func(P.dispose))}o(M,"is"),E(M,"is"),A.is=M})(C||(e.RequestCancellationReceiverStrategy=C={}));var w;(function(A){A.Message=Object.freeze({createCancellationTokenSource(D){return new s.CancellationTokenSource}});function M(D){return k.is(D)||C.is(D)}o(M,"is"),E(M,"is"),A.is=M})(w||(e.CancellationReceiverStrategy=w={}));var S;(function(A){A.Message=Object.freeze({sendCancellation(D,P){return D.sendNotification(l.type,{id:P})},cleanup(D){}});function M(D){let P=D;return P&&r.func(P.sendCancellation)&&r.func(P.cleanup)}o(M,"is"),E(M,"is"),A.is=M})(S||(e.CancellationSenderStrategy=S={}));var R;(function(A){A.Message=Object.freeze({receiver:w.Message,sender:S.Message});function M(D){let P=D;return P&&w.is(P.receiver)&&S.is(P.sender)}o(M,"is"),E(M,"is"),A.is=M})(R||(e.CancellationStrategy=R={}));var L;(function(A){function M(D){let P=D;return P&&r.func(P.handleMessage)}o(M,"is"),E(M,"is"),A.is=M})(L||(e.MessageStrategy=L={}));var N;(function(A){function M(D){let P=D;return P&&(R.is(P.cancellationStrategy)||T.is(P.connectionStrategy)||L.is(P.messageStrategy))}o(M,"is"),E(M,"is"),A.is=M})(N||(e.ConnectionOptions=N={}));var I;(function(A){A[A.New=1]="New",A[A.Listening=2]="Listening",A[A.Closed=3]="Closed",A[A.Disposed=4]="Disposed"})(I||(I={}));function _(A,M,D,P){let B=D!==void 0?D:e.NullLogger,O=0,$=0,V=0,G="2.0",z,W=new Map,H,j=new Map,Q=new Map,U,oe=new i.LinkedMap,te=new Map,le=new Set,ie=new Map,ae=p.Off,Re=g.Text,be,Pe=I.New,Ge=new a.Emitter,Oe=new a.Emitter,ue=new a.Emitter,ye=new a.Emitter,ke=new a.Emitter,ce=P&&P.cancellationStrategy?P.cancellationStrategy:R.Message;function re(Ee){if(Ee===null)throw new Error("Can't send requests with id null since the response can't be correlated.");return"req-"+Ee.toString()}o(re,"createRequestQueueKey"),E(re,"createRequestQueueKey");function J(Ee){return Ee===null?"res-unknown-"+(++V).toString():"res-"+Ee.toString()}o(J,"createResponseQueueKey"),E(J,"createResponseQueueKey");function se(){return"not-"+(++$).toString()}o(se,"createNotificationQueueKey"),E(se,"createNotificationQueueKey");function ge(Ee,tt){n.Message.isRequest(tt)?Ee.set(re(tt.id),tt):n.Message.isResponse(tt)?Ee.set(J(tt.id),tt):Ee.set(se(),tt)}o(ge,"addMessageToQueue"),E(ge,"addMessageToQueue");function Te(Ee){}o(Te,"cancelUndispatched"),E(Te,"cancelUndispatched");function we(){return Pe===I.Listening}o(we,"isListening"),E(we,"isListening");function Me(){return Pe===I.Closed}o(Me,"isClosed"),E(Me,"isClosed");function ve(){return Pe===I.Disposed}o(ve,"isDisposed"),E(ve,"isDisposed");function ne(){(Pe===I.New||Pe===I.Listening)&&(Pe=I.Closed,Oe.fire(void 0))}o(ne,"closeHandler"),E(ne,"closeHandler");function q(Ee){Ge.fire([Ee,void 0,void 0])}o(q,"readErrorHandler"),E(q,"readErrorHandler");function he(Ee){Ge.fire(Ee)}o(he,"writeErrorHandler"),E(he,"writeErrorHandler"),A.onClose(ne),A.onError(q),M.onClose(ne),M.onError(he);function X(){U||oe.size===0||(U=(0,t.default)().timer.setImmediate(()=>{U=void 0,K()}))}o(X,"triggerMessageQueue"),E(X,"triggerMessageQueue");function fe(Ee){n.Message.isRequest(Ee)?_e(Ee):n.Message.isNotification(Ee)?Ne(Ee):n.Message.isResponse(Ee)?Be(Ee):He(Ee)}o(fe,"handleMessage"),E(fe,"handleMessage");function K(){if(oe.size===0)return;let Ee=oe.shift();try{let tt=P?.messageStrategy;L.is(tt)?tt.handleMessage(Ee,fe):fe(Ee)}finally{X()}}o(K,"processMessageQueue"),E(K,"processMessageQueue");let qe=E(Ee=>{try{if(n.Message.isNotification(Ee)&&Ee.method===l.type.method){let tt=Ee.params.id,at=re(tt),ot=oe.get(at);if(n.Message.isRequest(ot)){let Bt=P?.connectionStrategy,qt=Bt&&Bt.cancelUndispatched?Bt.cancelUndispatched(ot,Te):void 0;if(qt&&(qt.error!==void 0||qt.result!==void 0)){oe.delete(at),ie.delete(tt),qt.id=ot.id,Ke(qt,Ee.method,Date.now()),M.write(qt).catch(()=>B.error("Sending response for canceled message failed."));return}}let Wt=ie.get(tt);if(Wt!==void 0){Wt.cancel(),mt(Ee);return}else le.add(tt)}ge(oe,Ee)}finally{X()}},"callback");function _e(Ee){if(ve())return;function tt(Tt,De,it){let We={jsonrpc:G,id:Ee.id};Tt instanceof n.ResponseError?We.error=Tt.toJson():We.result=Tt===void 0?null:Tt,Ke(We,De,it),M.write(We).catch(()=>B.error("Sending response failed."))}o(tt,"reply"),E(tt,"reply");function at(Tt,De,it){let We={jsonrpc:G,id:Ee.id,error:Tt.toJson()};Ke(We,De,it),M.write(We).catch(()=>B.error("Sending response failed."))}o(at,"replyError"),E(at,"replyError");function ot(Tt,De,it){Tt===void 0&&(Tt=null);let We={jsonrpc:G,id:Ee.id,result:Tt};Ke(We,De,it),M.write(We).catch(()=>B.error("Sending response failed."))}o(ot,"replySuccess"),E(ot,"replySuccess"),xe(Ee);let Wt=W.get(Ee.method),Bt,qt;Wt&&(Bt=Wt.type,qt=Wt.handler);let vr=Date.now();if(qt||z){let Tt=Ee.id??String(Date.now()),De=k.is(ce.receiver)?ce.receiver.createCancellationTokenSource(Tt):ce.receiver.createCancellationTokenSource(Ee);Ee.id!==null&&le.has(Ee.id)&&De.cancel(),Ee.id!==null&&ie.set(Tt,De);try{let it;if(qt)if(Ee.params===void 0){if(Bt!==void 0&&Bt.numberOfParams!==0){at(new n.ResponseError(n.ErrorCodes.InvalidParams,`Request ${Ee.method} defines ${Bt.numberOfParams} params but received none.`),Ee.method,vr);return}it=qt(De.token)}else if(Array.isArray(Ee.params)){if(Bt!==void 0&&Bt.parameterStructures===n.ParameterStructures.byName){at(new n.ResponseError(n.ErrorCodes.InvalidParams,`Request ${Ee.method} defines parameters by name but received parameters by position`),Ee.method,vr);return}it=qt(...Ee.params,De.token)}else{if(Bt!==void 0&&Bt.parameterStructures===n.ParameterStructures.byPosition){at(new n.ResponseError(n.ErrorCodes.InvalidParams,`Request ${Ee.method} defines parameters by position but received parameters by name`),Ee.method,vr);return}it=qt(Ee.params,De.token)}else z&&(it=z(Ee.method,Ee.params,De.token));let We=it;it?We.then?We.then(rt=>{ie.delete(Tt),tt(rt,Ee.method,vr)},rt=>{ie.delete(Tt),rt instanceof n.ResponseError?at(rt,Ee.method,vr):rt&&r.string(rt.message)?at(new n.ResponseError(n.ErrorCodes.InternalError,`Request ${Ee.method} failed with message: ${rt.message}`),Ee.method,vr):at(new n.ResponseError(n.ErrorCodes.InternalError,`Request ${Ee.method} failed unexpectedly without providing any details.`),Ee.method,vr)}):(ie.delete(Tt),tt(it,Ee.method,vr)):(ie.delete(Tt),ot(it,Ee.method,vr))}catch(it){ie.delete(Tt),it instanceof n.ResponseError?tt(it,Ee.method,vr):it&&r.string(it.message)?at(new n.ResponseError(n.ErrorCodes.InternalError,`Request ${Ee.method} failed with message: ${it.message}`),Ee.method,vr):at(new n.ResponseError(n.ErrorCodes.InternalError,`Request ${Ee.method} failed unexpectedly without providing any details.`),Ee.method,vr)}}else at(new n.ResponseError(n.ErrorCodes.MethodNotFound,`Unhandled method ${Ee.method}`),Ee.method,vr)}o(_e,"handleRequest"),E(_e,"handleRequest");function Be(Ee){if(!ve())if(Ee.id===null)Ee.error?B.error(`Received response message without id: Error is: +${JSON.stringify(Ee.error,void 0,4)}`):B.error("Received response message without id. No further error information provided.");else{let tt=Ee.id,at=te.get(tt);if(Le(Ee,at),at!==void 0){te.delete(tt);try{if(Ee.error){let ot=Ee.error;at.reject(new n.ResponseError(ot.code,ot.message,ot.data))}else if(Ee.result!==void 0)at.resolve(Ee.result);else throw new Error("Should never happen.")}catch(ot){ot.message?B.error(`Response handler '${at.method}' failed with message: ${ot.message}`):B.error(`Response handler '${at.method}' failed unexpectedly.`)}}}}o(Be,"handleResponse"),E(Be,"handleResponse");function Ne(Ee){if(ve())return;let tt,at;if(Ee.method===l.type.method){let ot=Ee.params.id;le.delete(ot),mt(Ee);return}else{let ot=j.get(Ee.method);ot&&(at=ot.handler,tt=ot.type)}if(at||H)try{if(mt(Ee),at)if(Ee.params===void 0)tt!==void 0&&tt.numberOfParams!==0&&tt.parameterStructures!==n.ParameterStructures.byName&&B.error(`Notification ${Ee.method} defines ${tt.numberOfParams} params but received none.`),at();else if(Array.isArray(Ee.params)){let ot=Ee.params;Ee.method===h.type.method&&ot.length===2&&u.is(ot[0])?at({token:ot[0],value:ot[1]}):(tt!==void 0&&(tt.parameterStructures===n.ParameterStructures.byName&&B.error(`Notification ${Ee.method} defines parameters by name but received parameters by position`),tt.numberOfParams!==Ee.params.length&&B.error(`Notification ${Ee.method} defines ${tt.numberOfParams} params but received ${ot.length} arguments`)),at(...ot))}else tt!==void 0&&tt.parameterStructures===n.ParameterStructures.byPosition&&B.error(`Notification ${Ee.method} defines parameters by position but received parameters by name`),at(Ee.params);else H&&H(Ee.method,Ee.params)}catch(ot){ot.message?B.error(`Notification handler '${Ee.method}' failed with message: ${ot.message}`):B.error(`Notification handler '${Ee.method}' failed unexpectedly.`)}else ue.fire(Ee)}o(Ne,"handleNotification"),E(Ne,"handleNotification");function He(Ee){if(!Ee){B.error("Received empty message.");return}B.error(`Received message which is neither a response nor a notification message: +${JSON.stringify(Ee,null,4)}`);let tt=Ee;if(r.string(tt.id)||r.number(tt.id)){let at=tt.id,ot=te.get(at);ot&&ot.reject(new Error("The received response has neither a result nor an error property."))}}o(He,"handleInvalidMessage"),E(He,"handleInvalidMessage");function $e(Ee){if(Ee!=null)switch(ae){case p.Verbose:return JSON.stringify(Ee,null,4);case p.Compact:return JSON.stringify(Ee);default:return}}o($e,"stringifyTrace"),E($e,"stringifyTrace");function Xe(Ee){if(!(ae===p.Off||!be))if(Re===g.Text){let tt;(ae===p.Verbose||ae===p.Compact)&&Ee.params&&(tt=`Params: ${$e(Ee.params)} + +`),be.log(`Sending request '${Ee.method} - (${Ee.id})'.`,tt)}else ft("send-request",Ee)}o(Xe,"traceSendingRequest"),E(Xe,"traceSendingRequest");function Fe(Ee){if(!(ae===p.Off||!be))if(Re===g.Text){let tt;(ae===p.Verbose||ae===p.Compact)&&(Ee.params?tt=`Params: ${$e(Ee.params)} + +`:tt=`No parameters provided. + +`),be.log(`Sending notification '${Ee.method}'.`,tt)}else ft("send-notification",Ee)}o(Fe,"traceSendingNotification"),E(Fe,"traceSendingNotification");function Ke(Ee,tt,at){if(!(ae===p.Off||!be))if(Re===g.Text){let ot;(ae===p.Verbose||ae===p.Compact)&&(Ee.error&&Ee.error.data?ot=`Error data: ${$e(Ee.error.data)} + +`:Ee.result?ot=`Result: ${$e(Ee.result)} + +`:Ee.error===void 0&&(ot=`No result returned. + +`)),be.log(`Sending response '${tt} - (${Ee.id})'. Processing request took ${Date.now()-at}ms`,ot)}else ft("send-response",Ee)}o(Ke,"traceSendingResponse"),E(Ke,"traceSendingResponse");function xe(Ee){if(!(ae===p.Off||!be))if(Re===g.Text){let tt;(ae===p.Verbose||ae===p.Compact)&&Ee.params&&(tt=`Params: ${$e(Ee.params)} + +`),be.log(`Received request '${Ee.method} - (${Ee.id})'.`,tt)}else ft("receive-request",Ee)}o(xe,"traceReceivedRequest"),E(xe,"traceReceivedRequest");function mt(Ee){if(!(ae===p.Off||!be||Ee.method===v.type.method))if(Re===g.Text){let tt;(ae===p.Verbose||ae===p.Compact)&&(Ee.params?tt=`Params: ${$e(Ee.params)} + +`:tt=`No parameters provided. + +`),be.log(`Received notification '${Ee.method}'.`,tt)}else ft("receive-notification",Ee)}o(mt,"traceReceivedNotification"),E(mt,"traceReceivedNotification");function Le(Ee,tt){if(!(ae===p.Off||!be))if(Re===g.Text){let at;if((ae===p.Verbose||ae===p.Compact)&&(Ee.error&&Ee.error.data?at=`Error data: ${$e(Ee.error.data)} + +`:Ee.result?at=`Result: ${$e(Ee.result)} + +`:Ee.error===void 0&&(at=`No result returned. + +`)),tt){let ot=Ee.error?` Request failed: ${Ee.error.message} (${Ee.error.code}).`:"";be.log(`Received response '${tt.method} - (${Ee.id})' in ${Date.now()-tt.timerStart}ms.${ot}`,at)}else be.log(`Received response ${Ee.id} without active response promise.`,at)}else ft("receive-response",Ee)}o(Le,"traceReceivedResponse"),E(Le,"traceReceivedResponse");function ft(Ee,tt){if(!be||ae===p.Off)return;let at={isLSPMessage:!0,type:Ee,message:tt,timestamp:Date.now()};be.log(at)}o(ft,"logLSPMessage"),E(ft,"logLSPMessage");function wt(){if(Me())throw new b(x.Closed,"Connection is closed.");if(ve())throw new b(x.Disposed,"Connection is disposed.")}o(wt,"throwIfClosedOrDisposed"),E(wt,"throwIfClosedOrDisposed");function zt(){if(we())throw new b(x.AlreadyListening,"Connection is already listening")}o(zt,"throwIfListening"),E(zt,"throwIfListening");function St(){if(!we())throw new Error("Call listen() first.")}o(St,"throwIfNotListening"),E(St,"throwIfNotListening");function At(Ee){return Ee===void 0?null:Ee}o(At,"undefinedToNull"),E(At,"undefinedToNull");function bt(Ee){if(Ee!==null)return Ee}o(bt,"nullToUndefined"),E(bt,"nullToUndefined");function me(Ee){return Ee!=null&&!Array.isArray(Ee)&&typeof Ee=="object"}o(me,"isNamedParam"),E(me,"isNamedParam");function lt(Ee,tt){switch(Ee){case n.ParameterStructures.auto:return me(tt)?bt(tt):[At(tt)];case n.ParameterStructures.byName:if(!me(tt))throw new Error("Received parameters by name but param is not an object literal.");return bt(tt);case n.ParameterStructures.byPosition:return[At(tt)];default:throw new Error(`Unknown parameter structure ${Ee.toString()}`)}}o(lt,"computeSingleParam"),E(lt,"computeSingleParam");function gt(Ee,tt){let at,ot=Ee.numberOfParams;switch(ot){case 0:at=void 0;break;case 1:at=lt(Ee.parameterStructures,tt[0]);break;default:at=[];for(let Wt=0;Wt{wt();let at,ot;if(r.string(Ee)){at=Ee;let Bt=tt[0],qt=0,vr=n.ParameterStructures.auto;n.ParameterStructures.is(Bt)&&(qt=1,vr=Bt);let Tt=tt.length,De=Tt-qt;switch(De){case 0:ot=void 0;break;case 1:ot=lt(vr,tt[qt]);break;default:if(vr===n.ParameterStructures.byName)throw new Error(`Received ${De} parameters for 'by Name' notification parameter structure.`);ot=tt.slice(qt,Tt).map(it=>At(it));break}}else{let Bt=tt;at=Ee.method,ot=gt(Ee,Bt)}let Wt={jsonrpc:G,method:at,params:ot};return Fe(Wt),M.write(Wt).catch(Bt=>{throw B.error("Sending notification failed."),Bt})},"sendNotification"),onNotification:E((Ee,tt)=>{wt();let at;return r.func(Ee)?H=Ee:tt&&(r.string(Ee)?(at=Ee,j.set(Ee,{type:void 0,handler:tt})):(at=Ee.method,j.set(Ee.method,{type:Ee,handler:tt}))),{dispose:E(()=>{at!==void 0?j.delete(at):H=void 0},"dispose")}},"onNotification"),onProgress:E((Ee,tt,at)=>{if(Q.has(tt))throw new Error(`Progress handler for token ${tt} already registered`);return Q.set(tt,at),{dispose:E(()=>{Q.delete(tt)},"dispose")}},"onProgress"),sendProgress:E((Ee,tt,at)=>Ze.sendNotification(h.type,{token:tt,value:at}),"sendProgress"),onUnhandledProgress:ye.event,sendRequest:E((Ee,...tt)=>{wt(),St();let at,ot,Wt;if(r.string(Ee)){at=Ee;let Tt=tt[0],De=tt[tt.length-1],it=0,We=n.ParameterStructures.auto;n.ParameterStructures.is(Tt)&&(it=1,We=Tt);let rt=tt.length;s.CancellationToken.is(De)&&(rt=rt-1,Wt=De);let yt=rt-it;switch(yt){case 0:ot=void 0;break;case 1:ot=lt(We,tt[it]);break;default:if(We===n.ParameterStructures.byName)throw new Error(`Received ${yt} parameters for 'by Name' request parameter structure.`);ot=tt.slice(it,rt).map(Yt=>At(Yt));break}}else{let Tt=tt;at=Ee.method,ot=gt(Ee,Tt);let De=Ee.numberOfParams;Wt=s.CancellationToken.is(Tt[De])?Tt[De]:void 0}let Bt=O++,qt;Wt&&(qt=Wt.onCancellationRequested(()=>{let Tt=ce.sender.sendCancellation(Ze,Bt);return Tt===void 0?(B.log(`Received no promise from cancellation strategy when cancelling id ${Bt}`),Promise.resolve()):Tt.catch(()=>{B.log(`Sending cancellation messages for id ${Bt} failed`)})}));let vr={jsonrpc:G,id:Bt,method:at,params:ot};return Xe(vr),typeof ce.sender.enableCancellation=="function"&&ce.sender.enableCancellation(vr),new Promise(async(Tt,De)=>{let it=E(yt=>{Tt(yt),ce.sender.cleanup(Bt),qt?.dispose()},"resolveWithCleanup"),We=E(yt=>{De(yt),ce.sender.cleanup(Bt),qt?.dispose()},"rejectWithCleanup"),rt={method:at,timerStart:Date.now(),resolve:it,reject:We};try{await M.write(vr),te.set(Bt,rt)}catch(yt){throw B.error("Sending request failed."),rt.reject(new n.ResponseError(n.ErrorCodes.MessageWriteError,yt.message?yt.message:"Unknown reason")),yt}})},"sendRequest"),onRequest:E((Ee,tt)=>{wt();let at=null;return f.is(Ee)?(at=void 0,z=Ee):r.string(Ee)?(at=null,tt!==void 0&&(at=Ee,W.set(Ee,{handler:tt,type:void 0}))):tt!==void 0&&(at=Ee.method,W.set(Ee.method,{type:Ee,handler:tt})),{dispose:E(()=>{at!==null&&(at!==void 0?W.delete(at):z=void 0)},"dispose")}},"onRequest"),hasPendingResponse:E(()=>te.size>0,"hasPendingResponse"),trace:E(async(Ee,tt,at)=>{let ot=!1,Wt=g.Text;at!==void 0&&(r.boolean(at)?ot=at:(ot=at.sendNotification||!1,Wt=at.traceFormat||g.Text)),ae=Ee,Re=Wt,ae===p.Off?be=void 0:be=tt,ot&&!Me()&&!ve()&&await Ze.sendNotification(y.type,{value:p.toString(Ee)})},"trace"),onError:Ge.event,onClose:Oe.event,onUnhandledNotification:ue.event,onDispose:ke.event,end:E(()=>{M.end()},"end"),dispose:E(()=>{if(ve())return;Pe=I.Disposed,ke.fire(void 0);let Ee=new n.ResponseError(n.ErrorCodes.PendingResponseRejected,"Pending response rejected since connection got disposed");for(let tt of te.values())tt.reject(Ee);te=new Map,ie=new Map,le=new Set,oe=new i.LinkedMap,r.func(M.dispose)&&M.dispose(),r.func(A.dispose)&&A.dispose()},"dispose"),listen:E(()=>{wt(),zt(),Pe=I.Listening,A.listen(qe)},"listen"),inspect:E(()=>{(0,t.default)().console.log("inspect")},"inspect")};return Ze.onNotification(v.type,Ee=>{if(ae===p.Off||!be)return;let tt=ae===p.Verbose||ae===p.Compact;be.log(Ee.message,tt?Ee.verbose:void 0)}),Ze.onNotification(h.type,Ee=>{let tt=Q.get(Ee.token);tt?tt(Ee.value):ye.fire(Ee)}),Ze}o(_,"createMessageConnection"),E(_,"createMessageConnection"),e.createMessageConnection=_}}),wG=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/common/api.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ProgressType=e.ProgressToken=e.createMessageConnection=e.NullLogger=e.ConnectionOptions=e.ConnectionStrategy=e.AbstractMessageBuffer=e.WriteableStreamMessageWriter=e.AbstractMessageWriter=e.MessageWriter=e.ReadableStreamMessageReader=e.AbstractMessageReader=e.MessageReader=e.SharedArrayReceiverStrategy=e.SharedArraySenderStrategy=e.CancellationToken=e.CancellationTokenSource=e.Emitter=e.Event=e.Disposable=e.LRUCache=e.Touch=e.LinkedMap=e.ParameterStructures=e.NotificationType9=e.NotificationType8=e.NotificationType7=e.NotificationType6=e.NotificationType5=e.NotificationType4=e.NotificationType3=e.NotificationType2=e.NotificationType1=e.NotificationType0=e.NotificationType=e.ErrorCodes=e.ResponseError=e.RequestType9=e.RequestType8=e.RequestType7=e.RequestType6=e.RequestType5=e.RequestType4=e.RequestType3=e.RequestType2=e.RequestType1=e.RequestType0=e.RequestType=e.Message=e.RAL=void 0,e.MessageStrategy=e.CancellationStrategy=e.CancellationSenderStrategy=e.CancellationReceiverStrategy=e.ConnectionError=e.ConnectionErrors=e.LogTraceNotification=e.SetTraceNotification=e.TraceFormat=e.TraceValues=e.Trace=void 0;var t=Lwe();Object.defineProperty(e,"Message",{enumerable:!0,get:E(function(){return t.Message},"get")}),Object.defineProperty(e,"RequestType",{enumerable:!0,get:E(function(){return t.RequestType},"get")}),Object.defineProperty(e,"RequestType0",{enumerable:!0,get:E(function(){return t.RequestType0},"get")}),Object.defineProperty(e,"RequestType1",{enumerable:!0,get:E(function(){return t.RequestType1},"get")}),Object.defineProperty(e,"RequestType2",{enumerable:!0,get:E(function(){return t.RequestType2},"get")}),Object.defineProperty(e,"RequestType3",{enumerable:!0,get:E(function(){return t.RequestType3},"get")}),Object.defineProperty(e,"RequestType4",{enumerable:!0,get:E(function(){return t.RequestType4},"get")}),Object.defineProperty(e,"RequestType5",{enumerable:!0,get:E(function(){return t.RequestType5},"get")}),Object.defineProperty(e,"RequestType6",{enumerable:!0,get:E(function(){return t.RequestType6},"get")}),Object.defineProperty(e,"RequestType7",{enumerable:!0,get:E(function(){return t.RequestType7},"get")}),Object.defineProperty(e,"RequestType8",{enumerable:!0,get:E(function(){return t.RequestType8},"get")}),Object.defineProperty(e,"RequestType9",{enumerable:!0,get:E(function(){return t.RequestType9},"get")}),Object.defineProperty(e,"ResponseError",{enumerable:!0,get:E(function(){return t.ResponseError},"get")}),Object.defineProperty(e,"ErrorCodes",{enumerable:!0,get:E(function(){return t.ErrorCodes},"get")}),Object.defineProperty(e,"NotificationType",{enumerable:!0,get:E(function(){return t.NotificationType},"get")}),Object.defineProperty(e,"NotificationType0",{enumerable:!0,get:E(function(){return t.NotificationType0},"get")}),Object.defineProperty(e,"NotificationType1",{enumerable:!0,get:E(function(){return t.NotificationType1},"get")}),Object.defineProperty(e,"NotificationType2",{enumerable:!0,get:E(function(){return t.NotificationType2},"get")}),Object.defineProperty(e,"NotificationType3",{enumerable:!0,get:E(function(){return t.NotificationType3},"get")}),Object.defineProperty(e,"NotificationType4",{enumerable:!0,get:E(function(){return t.NotificationType4},"get")}),Object.defineProperty(e,"NotificationType5",{enumerable:!0,get:E(function(){return t.NotificationType5},"get")}),Object.defineProperty(e,"NotificationType6",{enumerable:!0,get:E(function(){return t.NotificationType6},"get")}),Object.defineProperty(e,"NotificationType7",{enumerable:!0,get:E(function(){return t.NotificationType7},"get")}),Object.defineProperty(e,"NotificationType8",{enumerable:!0,get:E(function(){return t.NotificationType8},"get")}),Object.defineProperty(e,"NotificationType9",{enumerable:!0,get:E(function(){return t.NotificationType9},"get")}),Object.defineProperty(e,"ParameterStructures",{enumerable:!0,get:E(function(){return t.ParameterStructures},"get")});var r=Iwe();Object.defineProperty(e,"LinkedMap",{enumerable:!0,get:E(function(){return r.LinkedMap},"get")}),Object.defineProperty(e,"LRUCache",{enumerable:!0,get:E(function(){return r.LRUCache},"get")}),Object.defineProperty(e,"Touch",{enumerable:!0,get:E(function(){return r.Touch},"get")});var n=R0t();Object.defineProperty(e,"Disposable",{enumerable:!0,get:E(function(){return n.Disposable},"get")});var i=Iv();Object.defineProperty(e,"Event",{enumerable:!0,get:E(function(){return i.Event},"get")}),Object.defineProperty(e,"Emitter",{enumerable:!0,get:E(function(){return i.Emitter},"get")});var a=yR();Object.defineProperty(e,"CancellationTokenSource",{enumerable:!0,get:E(function(){return a.CancellationTokenSource},"get")}),Object.defineProperty(e,"CancellationToken",{enumerable:!0,get:E(function(){return a.CancellationToken},"get")});var s=_0t();Object.defineProperty(e,"SharedArraySenderStrategy",{enumerable:!0,get:E(function(){return s.SharedArraySenderStrategy},"get")}),Object.defineProperty(e,"SharedArrayReceiverStrategy",{enumerable:!0,get:E(function(){return s.SharedArrayReceiverStrategy},"get")});var l=L0t();Object.defineProperty(e,"MessageReader",{enumerable:!0,get:E(function(){return l.MessageReader},"get")}),Object.defineProperty(e,"AbstractMessageReader",{enumerable:!0,get:E(function(){return l.AbstractMessageReader},"get")}),Object.defineProperty(e,"ReadableStreamMessageReader",{enumerable:!0,get:E(function(){return l.ReadableStreamMessageReader},"get")});var u=D0t();Object.defineProperty(e,"MessageWriter",{enumerable:!0,get:E(function(){return u.MessageWriter},"get")}),Object.defineProperty(e,"AbstractMessageWriter",{enumerable:!0,get:E(function(){return u.AbstractMessageWriter},"get")}),Object.defineProperty(e,"WriteableStreamMessageWriter",{enumerable:!0,get:E(function(){return u.WriteableStreamMessageWriter},"get")});var h=I0t();Object.defineProperty(e,"AbstractMessageBuffer",{enumerable:!0,get:E(function(){return h.AbstractMessageBuffer},"get")});var d=M0t();Object.defineProperty(e,"ConnectionStrategy",{enumerable:!0,get:E(function(){return d.ConnectionStrategy},"get")}),Object.defineProperty(e,"ConnectionOptions",{enumerable:!0,get:E(function(){return d.ConnectionOptions},"get")}),Object.defineProperty(e,"NullLogger",{enumerable:!0,get:E(function(){return d.NullLogger},"get")}),Object.defineProperty(e,"createMessageConnection",{enumerable:!0,get:E(function(){return d.createMessageConnection},"get")}),Object.defineProperty(e,"ProgressToken",{enumerable:!0,get:E(function(){return d.ProgressToken},"get")}),Object.defineProperty(e,"ProgressType",{enumerable:!0,get:E(function(){return d.ProgressType},"get")}),Object.defineProperty(e,"Trace",{enumerable:!0,get:E(function(){return d.Trace},"get")}),Object.defineProperty(e,"TraceValues",{enumerable:!0,get:E(function(){return d.TraceValues},"get")}),Object.defineProperty(e,"TraceFormat",{enumerable:!0,get:E(function(){return d.TraceFormat},"get")}),Object.defineProperty(e,"SetTraceNotification",{enumerable:!0,get:E(function(){return d.SetTraceNotification},"get")}),Object.defineProperty(e,"LogTraceNotification",{enumerable:!0,get:E(function(){return d.LogTraceNotification},"get")}),Object.defineProperty(e,"ConnectionErrors",{enumerable:!0,get:E(function(){return d.ConnectionErrors},"get")}),Object.defineProperty(e,"ConnectionError",{enumerable:!0,get:E(function(){return d.ConnectionError},"get")}),Object.defineProperty(e,"CancellationReceiverStrategy",{enumerable:!0,get:E(function(){return d.CancellationReceiverStrategy},"get")}),Object.defineProperty(e,"CancellationSenderStrategy",{enumerable:!0,get:E(function(){return d.CancellationSenderStrategy},"get")}),Object.defineProperty(e,"CancellationStrategy",{enumerable:!0,get:E(function(){return d.CancellationStrategy},"get")}),Object.defineProperty(e,"MessageStrategy",{enumerable:!0,get:E(function(){return d.MessageStrategy},"get")});var f=Jg();e.RAL=f.default}}),N0t=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/browser/ril.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var t=wG(),r=class Pwe extends t.AbstractMessageBuffer{static{o(this,"_MessageBuffer")}static{E(this,"MessageBuffer")}constructor(h="utf-8"){super(h),this.asciiDecoder=new TextDecoder("ascii")}emptyBuffer(){return Pwe.emptyBuffer}fromString(h,d){return new TextEncoder().encode(h)}toString(h,d){return d==="ascii"?this.asciiDecoder.decode(h):new TextDecoder(d).decode(h)}asNative(h,d){return d===void 0?h:h.slice(0,d)}allocNative(h){return new Uint8Array(h)}};r.emptyBuffer=new Uint8Array(0);var n=class{static{o(this,"ReadableStreamWrapper")}static{E(this,"ReadableStreamWrapper")}constructor(u){this.socket=u,this._onData=new t.Emitter,this._messageListener=h=>{h.data.arrayBuffer().then(f=>{this._onData.fire(new Uint8Array(f))},()=>{(0,t.RAL)().console.error("Converting blob to array buffer failed.")})},this.socket.addEventListener("message",this._messageListener)}onClose(u){return this.socket.addEventListener("close",u),t.Disposable.create(()=>this.socket.removeEventListener("close",u))}onError(u){return this.socket.addEventListener("error",u),t.Disposable.create(()=>this.socket.removeEventListener("error",u))}onEnd(u){return this.socket.addEventListener("end",u),t.Disposable.create(()=>this.socket.removeEventListener("end",u))}onData(u){return this._onData.event(u)}},i=class{static{o(this,"WritableStreamWrapper")}static{E(this,"WritableStreamWrapper")}constructor(u){this.socket=u}onClose(u){return this.socket.addEventListener("close",u),t.Disposable.create(()=>this.socket.removeEventListener("close",u))}onError(u){return this.socket.addEventListener("error",u),t.Disposable.create(()=>this.socket.removeEventListener("error",u))}onEnd(u){return this.socket.addEventListener("end",u),t.Disposable.create(()=>this.socket.removeEventListener("end",u))}write(u,h){if(typeof u=="string"){if(h!==void 0&&h!=="utf-8")throw new Error(`In a Browser environments only utf-8 text encoding is supported. But got encoding: ${h}`);this.socket.send(u)}else this.socket.send(u);return Promise.resolve()}end(){this.socket.close()}},a=new TextEncoder,s=Object.freeze({messageBuffer:Object.freeze({create:E(u=>new r(u),"create")}),applicationJson:Object.freeze({encoder:Object.freeze({name:"application/json",encode:E((u,h)=>{if(h.charset!=="utf-8")throw new Error(`In a Browser environments only utf-8 text encoding is supported. But got encoding: ${h.charset}`);return Promise.resolve(a.encode(JSON.stringify(u,void 0,0)))},"encode")}),decoder:Object.freeze({name:"application/json",decode:E((u,h)=>{if(!(u instanceof Uint8Array))throw new Error("In a Browser environments only Uint8Arrays are supported.");return Promise.resolve(JSON.parse(new TextDecoder(h.charset).decode(u)))},"decode")})}),stream:Object.freeze({asReadableStream:E(u=>new n(u),"asReadableStream"),asWritableStream:E(u=>new i(u),"asWritableStream")}),console,timer:Object.freeze({setTimeout(u,h,...d){let f=setTimeout(u,h,...d);return{dispose:E(()=>clearTimeout(f),"dispose")}},setImmediate(u,...h){let d=setTimeout(u,0,...h);return{dispose:E(()=>clearTimeout(d),"dispose")}},setInterval(u,h,...d){let f=setInterval(u,h,...d);return{dispose:E(()=>clearInterval(f),"dispose")}}})});function l(){return s}o(l,"RIL"),E(l,"RIL"),(function(u){function h(){t.RAL.install(s)}o(h,"install"),E(h,"install"),u.install=h})(l||(l={})),e.default=l}}),Mv=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/lib/browser/main.js"(e){"use strict";var t=e&&e.__createBinding||(Object.create?(function(u,h,d,f){f===void 0&&(f=d);var p=Object.getOwnPropertyDescriptor(h,d);(!p||("get"in p?!h.__esModule:p.writable||p.configurable))&&(p={enumerable:!0,get:E(function(){return h[d]},"get")}),Object.defineProperty(u,f,p)}):(function(u,h,d,f){f===void 0&&(f=d),u[f]=h[d]})),r=e&&e.__exportStar||function(u,h){for(var d in u)d!=="default"&&!Object.prototype.hasOwnProperty.call(h,d)&&t(h,u,d)};Object.defineProperty(e,"__esModule",{value:!0}),e.createMessageConnection=e.BrowserMessageWriter=e.BrowserMessageReader=void 0;var n=N0t();n.default.install();var i=wG();r(wG(),e);var a=class extends i.AbstractMessageReader{static{o(this,"BrowserMessageReader")}static{E(this,"BrowserMessageReader")}constructor(u){super(),this._onData=new i.Emitter,this._messageListener=h=>{this._onData.fire(h.data)},u.addEventListener("error",h=>this.fireError(h)),u.onmessage=this._messageListener}listen(u){return this._onData.event(u)}};e.BrowserMessageReader=a;var s=class extends i.AbstractMessageWriter{static{o(this,"BrowserMessageWriter")}static{E(this,"BrowserMessageWriter")}constructor(u){super(),this.port=u,this.errorCount=0,u.addEventListener("error",h=>this.fireError(h))}write(u){try{return this.port.postMessage(u),Promise.resolve()}catch(h){return this.handleError(h,u),Promise.reject(h)}}handleError(u,h){this.errorCount++,this.fireError(u,h,this.errorCount)}end(){}};e.BrowserMessageWriter=s;function l(u,h,d,f){return d===void 0&&(d=i.NullLogger),i.ConnectionStrategy.is(f)&&(f={connectionStrategy:f}),(0,i.createMessageConnection)(u,h,d,f)}o(l,"createMessageConnection"),E(l,"createMessageConnection"),e.createMessageConnection=l}}),BTe=zr({"../../node_modules/.pnpm/vscode-jsonrpc@8.2.0/node_modules/vscode-jsonrpc/browser.js"(e,t){"use strict";t.exports=Mv()}}),ki=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/messages.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ProtocolNotificationType=e.ProtocolNotificationType0=e.ProtocolRequestType=e.ProtocolRequestType0=e.RegistrationType=e.MessageDirection=void 0;var t=Mv(),r;(function(u){u.clientToServer="clientToServer",u.serverToClient="serverToClient",u.both="both"})(r||(e.MessageDirection=r={}));var n=class{static{o(this,"RegistrationType")}static{E(this,"RegistrationType")}constructor(u){this.method=u}};e.RegistrationType=n;var i=class extends t.RequestType0{static{o(this,"ProtocolRequestType0")}static{E(this,"ProtocolRequestType0")}constructor(u){super(u)}};e.ProtocolRequestType0=i;var a=class extends t.RequestType{static{o(this,"ProtocolRequestType")}static{E(this,"ProtocolRequestType")}constructor(u){super(u,t.ParameterStructures.byName)}};e.ProtocolRequestType=a;var s=class extends t.NotificationType0{static{o(this,"ProtocolNotificationType0")}static{E(this,"ProtocolNotificationType0")}constructor(u){super(u)}};e.ProtocolNotificationType0=s;var l=class extends t.NotificationType{static{o(this,"ProtocolNotificationType")}static{E(this,"ProtocolNotificationType")}constructor(u){super(u,t.ParameterStructures.byName)}};e.ProtocolNotificationType=l}}),iW=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/utils/is.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.objectLiteral=e.typedArray=e.stringArray=e.array=e.func=e.error=e.number=e.string=e.boolean=void 0;function t(d){return d===!0||d===!1}o(t,"boolean"),E(t,"boolean"),e.boolean=t;function r(d){return typeof d=="string"||d instanceof String}o(r,"string"),E(r,"string"),e.string=r;function n(d){return typeof d=="number"||d instanceof Number}o(n,"number"),E(n,"number"),e.number=n;function i(d){return d instanceof Error}o(i,"error"),E(i,"error"),e.error=i;function a(d){return typeof d=="function"}o(a,"func"),E(a,"func"),e.func=a;function s(d){return Array.isArray(d)}o(s,"array"),E(s,"array"),e.array=s;function l(d){return s(d)&&d.every(f=>r(f))}o(l,"stringArray"),E(l,"stringArray"),e.stringArray=l;function u(d,f){return Array.isArray(d)&&d.every(f)}o(u,"typedArray"),E(u,"typedArray"),e.typedArray=u;function h(d){return d!==null&&typeof d=="object"}o(h,"objectLiteral"),E(h,"objectLiteral"),e.objectLiteral=h}}),P0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.implementation.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ImplementationRequest=void 0;var t=ki(),r;(function(n){n.method="textDocument/implementation",n.messageDirection=t.MessageDirection.clientToServer,n.type=new t.ProtocolRequestType(n.method)})(r||(e.ImplementationRequest=r={}))}}),O0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.typeDefinition.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TypeDefinitionRequest=void 0;var t=ki(),r;(function(n){n.method="textDocument/typeDefinition",n.messageDirection=t.MessageDirection.clientToServer,n.type=new t.ProtocolRequestType(n.method)})(r||(e.TypeDefinitionRequest=r={}))}}),B0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.workspaceFolder.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DidChangeWorkspaceFoldersNotification=e.WorkspaceFoldersRequest=void 0;var t=ki(),r;(function(i){i.method="workspace/workspaceFolders",i.messageDirection=t.MessageDirection.serverToClient,i.type=new t.ProtocolRequestType0(i.method)})(r||(e.WorkspaceFoldersRequest=r={}));var n;(function(i){i.method="workspace/didChangeWorkspaceFolders",i.messageDirection=t.MessageDirection.clientToServer,i.type=new t.ProtocolNotificationType(i.method)})(n||(e.DidChangeWorkspaceFoldersNotification=n={}))}}),$0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.configuration.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ConfigurationRequest=void 0;var t=ki(),r;(function(n){n.method="workspace/configuration",n.messageDirection=t.MessageDirection.serverToClient,n.type=new t.ProtocolRequestType(n.method)})(r||(e.ConfigurationRequest=r={}))}}),F0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.colorProvider.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ColorPresentationRequest=e.DocumentColorRequest=void 0;var t=ki(),r;(function(i){i.method="textDocument/documentColor",i.messageDirection=t.MessageDirection.clientToServer,i.type=new t.ProtocolRequestType(i.method)})(r||(e.DocumentColorRequest=r={}));var n;(function(i){i.method="textDocument/colorPresentation",i.messageDirection=t.MessageDirection.clientToServer,i.type=new t.ProtocolRequestType(i.method)})(n||(e.ColorPresentationRequest=n={}))}}),z0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.foldingRange.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.FoldingRangeRefreshRequest=e.FoldingRangeRequest=void 0;var t=ki(),r;(function(i){i.method="textDocument/foldingRange",i.messageDirection=t.MessageDirection.clientToServer,i.type=new t.ProtocolRequestType(i.method)})(r||(e.FoldingRangeRequest=r={}));var n;(function(i){i.method="workspace/foldingRange/refresh",i.messageDirection=t.MessageDirection.serverToClient,i.type=new t.ProtocolRequestType0(i.method)})(n||(e.FoldingRangeRefreshRequest=n={}))}}),G0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.declaration.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DeclarationRequest=void 0;var t=ki(),r;(function(n){n.method="textDocument/declaration",n.messageDirection=t.MessageDirection.clientToServer,n.type=new t.ProtocolRequestType(n.method)})(r||(e.DeclarationRequest=r={}))}}),V0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.selectionRange.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SelectionRangeRequest=void 0;var t=ki(),r;(function(n){n.method="textDocument/selectionRange",n.messageDirection=t.MessageDirection.clientToServer,n.type=new t.ProtocolRequestType(n.method)})(r||(e.SelectionRangeRequest=r={}))}}),W0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.progress.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.WorkDoneProgressCancelNotification=e.WorkDoneProgressCreateRequest=e.WorkDoneProgress=void 0;var t=Mv(),r=ki(),n;(function(s){s.type=new t.ProgressType;function l(u){return u===s.type}o(l,"is"),E(l,"is"),s.is=l})(n||(e.WorkDoneProgress=n={}));var i;(function(s){s.method="window/workDoneProgress/create",s.messageDirection=r.MessageDirection.serverToClient,s.type=new r.ProtocolRequestType(s.method)})(i||(e.WorkDoneProgressCreateRequest=i={}));var a;(function(s){s.method="window/workDoneProgress/cancel",s.messageDirection=r.MessageDirection.clientToServer,s.type=new r.ProtocolNotificationType(s.method)})(a||(e.WorkDoneProgressCancelNotification=a={}))}}),q0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.callHierarchy.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CallHierarchyOutgoingCallsRequest=e.CallHierarchyIncomingCallsRequest=e.CallHierarchyPrepareRequest=void 0;var t=ki(),r;(function(a){a.method="textDocument/prepareCallHierarchy",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(r||(e.CallHierarchyPrepareRequest=r={}));var n;(function(a){a.method="callHierarchy/incomingCalls",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(n||(e.CallHierarchyIncomingCallsRequest=n={}));var i;(function(a){a.method="callHierarchy/outgoingCalls",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(i||(e.CallHierarchyOutgoingCallsRequest=i={}))}}),H0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.semanticTokens.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SemanticTokensRefreshRequest=e.SemanticTokensRangeRequest=e.SemanticTokensDeltaRequest=e.SemanticTokensRequest=e.SemanticTokensRegistrationType=e.TokenFormat=void 0;var t=ki(),r;(function(u){u.Relative="relative"})(r||(e.TokenFormat=r={}));var n;(function(u){u.method="textDocument/semanticTokens",u.type=new t.RegistrationType(u.method)})(n||(e.SemanticTokensRegistrationType=n={}));var i;(function(u){u.method="textDocument/semanticTokens/full",u.messageDirection=t.MessageDirection.clientToServer,u.type=new t.ProtocolRequestType(u.method),u.registrationMethod=n.method})(i||(e.SemanticTokensRequest=i={}));var a;(function(u){u.method="textDocument/semanticTokens/full/delta",u.messageDirection=t.MessageDirection.clientToServer,u.type=new t.ProtocolRequestType(u.method),u.registrationMethod=n.method})(a||(e.SemanticTokensDeltaRequest=a={}));var s;(function(u){u.method="textDocument/semanticTokens/range",u.messageDirection=t.MessageDirection.clientToServer,u.type=new t.ProtocolRequestType(u.method),u.registrationMethod=n.method})(s||(e.SemanticTokensRangeRequest=s={}));var l;(function(u){u.method="workspace/semanticTokens/refresh",u.messageDirection=t.MessageDirection.serverToClient,u.type=new t.ProtocolRequestType0(u.method)})(l||(e.SemanticTokensRefreshRequest=l={}))}}),U0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.showDocument.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ShowDocumentRequest=void 0;var t=ki(),r;(function(n){n.method="window/showDocument",n.messageDirection=t.MessageDirection.serverToClient,n.type=new t.ProtocolRequestType(n.method)})(r||(e.ShowDocumentRequest=r={}))}}),Y0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.linkedEditingRange.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LinkedEditingRangeRequest=void 0;var t=ki(),r;(function(n){n.method="textDocument/linkedEditingRange",n.messageDirection=t.MessageDirection.clientToServer,n.type=new t.ProtocolRequestType(n.method)})(r||(e.LinkedEditingRangeRequest=r={}))}}),j0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.fileOperations.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.WillDeleteFilesRequest=e.DidDeleteFilesNotification=e.DidRenameFilesNotification=e.WillRenameFilesRequest=e.DidCreateFilesNotification=e.WillCreateFilesRequest=e.FileOperationPatternKind=void 0;var t=ki(),r;(function(h){h.file="file",h.folder="folder"})(r||(e.FileOperationPatternKind=r={}));var n;(function(h){h.method="workspace/willCreateFiles",h.messageDirection=t.MessageDirection.clientToServer,h.type=new t.ProtocolRequestType(h.method)})(n||(e.WillCreateFilesRequest=n={}));var i;(function(h){h.method="workspace/didCreateFiles",h.messageDirection=t.MessageDirection.clientToServer,h.type=new t.ProtocolNotificationType(h.method)})(i||(e.DidCreateFilesNotification=i={}));var a;(function(h){h.method="workspace/willRenameFiles",h.messageDirection=t.MessageDirection.clientToServer,h.type=new t.ProtocolRequestType(h.method)})(a||(e.WillRenameFilesRequest=a={}));var s;(function(h){h.method="workspace/didRenameFiles",h.messageDirection=t.MessageDirection.clientToServer,h.type=new t.ProtocolNotificationType(h.method)})(s||(e.DidRenameFilesNotification=s={}));var l;(function(h){h.method="workspace/didDeleteFiles",h.messageDirection=t.MessageDirection.clientToServer,h.type=new t.ProtocolNotificationType(h.method)})(l||(e.DidDeleteFilesNotification=l={}));var u;(function(h){h.method="workspace/willDeleteFiles",h.messageDirection=t.MessageDirection.clientToServer,h.type=new t.ProtocolRequestType(h.method)})(u||(e.WillDeleteFilesRequest=u={}))}}),X0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.moniker.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MonikerRequest=e.MonikerKind=e.UniquenessLevel=void 0;var t=ki(),r;(function(a){a.document="document",a.project="project",a.group="group",a.scheme="scheme",a.global="global"})(r||(e.UniquenessLevel=r={}));var n;(function(a){a.$import="import",a.$export="export",a.local="local"})(n||(e.MonikerKind=n={}));var i;(function(a){a.method="textDocument/moniker",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(i||(e.MonikerRequest=i={}))}}),K0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.typeHierarchy.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TypeHierarchySubtypesRequest=e.TypeHierarchySupertypesRequest=e.TypeHierarchyPrepareRequest=void 0;var t=ki(),r;(function(a){a.method="textDocument/prepareTypeHierarchy",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(r||(e.TypeHierarchyPrepareRequest=r={}));var n;(function(a){a.method="typeHierarchy/supertypes",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(n||(e.TypeHierarchySupertypesRequest=n={}));var i;(function(a){a.method="typeHierarchy/subtypes",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(i||(e.TypeHierarchySubtypesRequest=i={}))}}),Z0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.inlineValue.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.InlineValueRefreshRequest=e.InlineValueRequest=void 0;var t=ki(),r;(function(i){i.method="textDocument/inlineValue",i.messageDirection=t.MessageDirection.clientToServer,i.type=new t.ProtocolRequestType(i.method)})(r||(e.InlineValueRequest=r={}));var n;(function(i){i.method="workspace/inlineValue/refresh",i.messageDirection=t.MessageDirection.serverToClient,i.type=new t.ProtocolRequestType0(i.method)})(n||(e.InlineValueRefreshRequest=n={}))}}),Q0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.inlayHint.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.InlayHintRefreshRequest=e.InlayHintResolveRequest=e.InlayHintRequest=void 0;var t=ki(),r;(function(a){a.method="textDocument/inlayHint",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(r||(e.InlayHintRequest=r={}));var n;(function(a){a.method="inlayHint/resolve",a.messageDirection=t.MessageDirection.clientToServer,a.type=new t.ProtocolRequestType(a.method)})(n||(e.InlayHintResolveRequest=n={}));var i;(function(a){a.method="workspace/inlayHint/refresh",a.messageDirection=t.MessageDirection.serverToClient,a.type=new t.ProtocolRequestType0(a.method)})(i||(e.InlayHintRefreshRequest=i={}))}}),J0t=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.diagnostic.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DiagnosticRefreshRequest=e.WorkspaceDiagnosticRequest=e.DocumentDiagnosticRequest=e.DocumentDiagnosticReportKind=e.DiagnosticServerCancellationData=void 0;var t=Mv(),r=iW(),n=ki(),i;(function(h){function d(f){let p=f;return p&&r.boolean(p.retriggerRequest)}o(d,"is"),E(d,"is"),h.is=d})(i||(e.DiagnosticServerCancellationData=i={}));var a;(function(h){h.Full="full",h.Unchanged="unchanged"})(a||(e.DocumentDiagnosticReportKind=a={}));var s;(function(h){h.method="textDocument/diagnostic",h.messageDirection=n.MessageDirection.clientToServer,h.type=new n.ProtocolRequestType(h.method),h.partialResult=new t.ProgressType})(s||(e.DocumentDiagnosticRequest=s={}));var l;(function(h){h.method="workspace/diagnostic",h.messageDirection=n.MessageDirection.clientToServer,h.type=new n.ProtocolRequestType(h.method),h.partialResult=new t.ProgressType})(l||(e.WorkspaceDiagnosticRequest=l={}));var u;(function(h){h.method="workspace/diagnostic/refresh",h.messageDirection=n.MessageDirection.serverToClient,h.type=new n.ProtocolRequestType0(h.method)})(u||(e.DiagnosticRefreshRequest=u={}))}}),eyt=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/protocol.notebook.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DidCloseNotebookDocumentNotification=e.DidSaveNotebookDocumentNotification=e.DidChangeNotebookDocumentNotification=e.NotebookCellArrayChange=e.DidOpenNotebookDocumentNotification=e.NotebookDocumentSyncRegistrationType=e.NotebookDocument=e.NotebookCell=e.ExecutionSummary=e.NotebookCellKind=void 0;var t=(dw(),nW(gR)),r=iW(),n=ki(),i;(function(g){g.Markup=1,g.Code=2;function y(v){return v===1||v===2}o(y,"is"),E(y,"is"),g.is=y})(i||(e.NotebookCellKind=i={}));var a;(function(g){function y(b,T){let k={executionOrder:b};return(T===!0||T===!1)&&(k.success=T),k}o(y,"create"),E(y,"create"),g.create=y;function v(b){let T=b;return r.objectLiteral(T)&&t.uinteger.is(T.executionOrder)&&(T.success===void 0||r.boolean(T.success))}o(v,"is"),E(v,"is"),g.is=v;function x(b,T){return b===T?!0:b==null||T===null||T===void 0?!1:b.executionOrder===T.executionOrder&&b.success===T.success}o(x,"equals"),E(x,"equals"),g.equals=x})(a||(e.ExecutionSummary=a={}));var s;(function(g){function y(T,k){return{kind:T,document:k}}o(y,"create"),E(y,"create"),g.create=y;function v(T){let k=T;return r.objectLiteral(k)&&i.is(k.kind)&&t.DocumentUri.is(k.document)&&(k.metadata===void 0||r.objectLiteral(k.metadata))}o(v,"is"),E(v,"is"),g.is=v;function x(T,k){let C=new Set;return T.document!==k.document&&C.add("document"),T.kind!==k.kind&&C.add("kind"),T.executionSummary!==k.executionSummary&&C.add("executionSummary"),(T.metadata!==void 0||k.metadata!==void 0)&&!b(T.metadata,k.metadata)&&C.add("metadata"),(T.executionSummary!==void 0||k.executionSummary!==void 0)&&!a.equals(T.executionSummary,k.executionSummary)&&C.add("executionSummary"),C}o(x,"diff"),E(x,"diff"),g.diff=x;function b(T,k){if(T===k)return!0;if(T==null||k===null||k===void 0||typeof T!=typeof k||typeof T!="object")return!1;let C=Array.isArray(T),w=Array.isArray(k);if(C!==w)return!1;if(C&&w){if(T.length!==k.length)return!1;for(let S=0;S0}o(lt,"hasId"),E(lt,"hasId"),me.hasId=lt})(O||(e.StaticRegistrationOptions=O={}));var $;(function(me){function lt(gt){let Ze=gt;return Ze&&(Ze.documentSelector===null||_.is(Ze.documentSelector))}o(lt,"is"),E(lt,"is"),me.is=lt})($||(e.TextDocumentRegistrationOptions=$={}));var V;(function(me){function lt(Ze){let Ee=Ze;return n.objectLiteral(Ee)&&(Ee.workDoneProgress===void 0||n.boolean(Ee.workDoneProgress))}o(lt,"is"),E(lt,"is"),me.is=lt;function gt(Ze){let Ee=Ze;return Ee&&n.boolean(Ee.workDoneProgress)}o(gt,"hasWorkDoneProgress"),E(gt,"hasWorkDoneProgress"),me.hasWorkDoneProgress=gt})(V||(e.WorkDoneProgressOptions=V={}));var G;(function(me){me.method="initialize",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(G||(e.InitializeRequest=G={}));var z;(function(me){me.unknownProtocolVersion=1})(z||(e.InitializeErrorCodes=z={}));var W;(function(me){me.method="initialized",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolNotificationType(me.method)})(W||(e.InitializedNotification=W={}));var H;(function(me){me.method="shutdown",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType0(me.method)})(H||(e.ShutdownRequest=H={}));var j;(function(me){me.method="exit",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolNotificationType0(me.method)})(j||(e.ExitNotification=j={}));var Q;(function(me){me.method="workspace/didChangeConfiguration",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolNotificationType(me.method)})(Q||(e.DidChangeConfigurationNotification=Q={}));var U;(function(me){me.Error=1,me.Warning=2,me.Info=3,me.Log=4,me.Debug=5})(U||(e.MessageType=U={}));var oe;(function(me){me.method="window/showMessage",me.messageDirection=t.MessageDirection.serverToClient,me.type=new t.ProtocolNotificationType(me.method)})(oe||(e.ShowMessageNotification=oe={}));var te;(function(me){me.method="window/showMessageRequest",me.messageDirection=t.MessageDirection.serverToClient,me.type=new t.ProtocolRequestType(me.method)})(te||(e.ShowMessageRequest=te={}));var le;(function(me){me.method="window/logMessage",me.messageDirection=t.MessageDirection.serverToClient,me.type=new t.ProtocolNotificationType(me.method)})(le||(e.LogMessageNotification=le={}));var ie;(function(me){me.method="telemetry/event",me.messageDirection=t.MessageDirection.serverToClient,me.type=new t.ProtocolNotificationType(me.method)})(ie||(e.TelemetryEventNotification=ie={}));var ae;(function(me){me.None=0,me.Full=1,me.Incremental=2})(ae||(e.TextDocumentSyncKind=ae={}));var Re;(function(me){me.method="textDocument/didOpen",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolNotificationType(me.method)})(Re||(e.DidOpenTextDocumentNotification=Re={}));var be;(function(me){function lt(Ze){let Ee=Ze;return Ee!=null&&typeof Ee.text=="string"&&Ee.range!==void 0&&(Ee.rangeLength===void 0||typeof Ee.rangeLength=="number")}o(lt,"isIncremental"),E(lt,"isIncremental"),me.isIncremental=lt;function gt(Ze){let Ee=Ze;return Ee!=null&&typeof Ee.text=="string"&&Ee.range===void 0&&Ee.rangeLength===void 0}o(gt,"isFull"),E(gt,"isFull"),me.isFull=gt})(be||(e.TextDocumentContentChangeEvent=be={}));var Pe;(function(me){me.method="textDocument/didChange",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolNotificationType(me.method)})(Pe||(e.DidChangeTextDocumentNotification=Pe={}));var Ge;(function(me){me.method="textDocument/didClose",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolNotificationType(me.method)})(Ge||(e.DidCloseTextDocumentNotification=Ge={}));var Oe;(function(me){me.method="textDocument/didSave",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolNotificationType(me.method)})(Oe||(e.DidSaveTextDocumentNotification=Oe={}));var ue;(function(me){me.Manual=1,me.AfterDelay=2,me.FocusOut=3})(ue||(e.TextDocumentSaveReason=ue={}));var ye;(function(me){me.method="textDocument/willSave",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolNotificationType(me.method)})(ye||(e.WillSaveTextDocumentNotification=ye={}));var ke;(function(me){me.method="textDocument/willSaveWaitUntil",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(ke||(e.WillSaveTextDocumentWaitUntilRequest=ke={}));var ce;(function(me){me.method="workspace/didChangeWatchedFiles",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolNotificationType(me.method)})(ce||(e.DidChangeWatchedFilesNotification=ce={}));var re;(function(me){me.Created=1,me.Changed=2,me.Deleted=3})(re||(e.FileChangeType=re={}));var J;(function(me){function lt(gt){let Ze=gt;return n.objectLiteral(Ze)&&(r.URI.is(Ze.baseUri)||r.WorkspaceFolder.is(Ze.baseUri))&&n.string(Ze.pattern)}o(lt,"is"),E(lt,"is"),me.is=lt})(J||(e.RelativePattern=J={}));var se;(function(me){me.Create=1,me.Change=2,me.Delete=4})(se||(e.WatchKind=se={}));var ge;(function(me){me.method="textDocument/publishDiagnostics",me.messageDirection=t.MessageDirection.serverToClient,me.type=new t.ProtocolNotificationType(me.method)})(ge||(e.PublishDiagnosticsNotification=ge={}));var Te;(function(me){me.Invoked=1,me.TriggerCharacter=2,me.TriggerForIncompleteCompletions=3})(Te||(e.CompletionTriggerKind=Te={}));var we;(function(me){me.method="textDocument/completion",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(we||(e.CompletionRequest=we={}));var Me;(function(me){me.method="completionItem/resolve",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(Me||(e.CompletionResolveRequest=Me={}));var ve;(function(me){me.method="textDocument/hover",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(ve||(e.HoverRequest=ve={}));var ne;(function(me){me.Invoked=1,me.TriggerCharacter=2,me.ContentChange=3})(ne||(e.SignatureHelpTriggerKind=ne={}));var q;(function(me){me.method="textDocument/signatureHelp",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(q||(e.SignatureHelpRequest=q={}));var he;(function(me){me.method="textDocument/definition",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(he||(e.DefinitionRequest=he={}));var X;(function(me){me.method="textDocument/references",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(X||(e.ReferencesRequest=X={}));var fe;(function(me){me.method="textDocument/documentHighlight",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(fe||(e.DocumentHighlightRequest=fe={}));var K;(function(me){me.method="textDocument/documentSymbol",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(K||(e.DocumentSymbolRequest=K={}));var qe;(function(me){me.method="textDocument/codeAction",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(qe||(e.CodeActionRequest=qe={}));var _e;(function(me){me.method="codeAction/resolve",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(_e||(e.CodeActionResolveRequest=_e={}));var Be;(function(me){me.method="workspace/symbol",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(Be||(e.WorkspaceSymbolRequest=Be={}));var Ne;(function(me){me.method="workspaceSymbol/resolve",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(Ne||(e.WorkspaceSymbolResolveRequest=Ne={}));var He;(function(me){me.method="textDocument/codeLens",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(He||(e.CodeLensRequest=He={}));var $e;(function(me){me.method="codeLens/resolve",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})($e||(e.CodeLensResolveRequest=$e={}));var Xe;(function(me){me.method="workspace/codeLens/refresh",me.messageDirection=t.MessageDirection.serverToClient,me.type=new t.ProtocolRequestType0(me.method)})(Xe||(e.CodeLensRefreshRequest=Xe={}));var Fe;(function(me){me.method="textDocument/documentLink",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(Fe||(e.DocumentLinkRequest=Fe={}));var Ke;(function(me){me.method="documentLink/resolve",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(Ke||(e.DocumentLinkResolveRequest=Ke={}));var xe;(function(me){me.method="textDocument/formatting",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(xe||(e.DocumentFormattingRequest=xe={}));var mt;(function(me){me.method="textDocument/rangeFormatting",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(mt||(e.DocumentRangeFormattingRequest=mt={}));var Le;(function(me){me.method="textDocument/rangesFormatting",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(Le||(e.DocumentRangesFormattingRequest=Le={}));var ft;(function(me){me.method="textDocument/onTypeFormatting",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(ft||(e.DocumentOnTypeFormattingRequest=ft={}));var wt;(function(me){me.Identifier=1})(wt||(e.PrepareSupportDefaultBehavior=wt={}));var zt;(function(me){me.method="textDocument/rename",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(zt||(e.RenameRequest=zt={}));var St;(function(me){me.method="textDocument/prepareRename",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(St||(e.PrepareRenameRequest=St={}));var At;(function(me){me.method="workspace/executeCommand",me.messageDirection=t.MessageDirection.clientToServer,me.type=new t.ProtocolRequestType(me.method)})(At||(e.ExecuteCommandRequest=At={}));var bt;(function(me){me.method="workspace/applyEdit",me.messageDirection=t.MessageDirection.serverToClient,me.type=new t.ProtocolRequestType("workspace/applyEdit")})(bt||(e.ApplyWorkspaceEditRequest=bt={}))}}),nyt=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/connection.js"(e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.createProtocolConnection=void 0;var t=Mv();function r(n,i,a,s){return t.ConnectionStrategy.is(s)&&(s={connectionStrategy:s}),(0,t.createMessageConnection)(n,i,a,s)}o(r,"createProtocolConnection"),E(r,"createProtocolConnection"),e.createProtocolConnection=r}}),iyt=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/common/api.js"(e){"use strict";var t=e&&e.__createBinding||(Object.create?(function(a,s,l,u){u===void 0&&(u=l);var h=Object.getOwnPropertyDescriptor(s,l);(!h||("get"in h?!s.__esModule:h.writable||h.configurable))&&(h={enumerable:!0,get:E(function(){return s[l]},"get")}),Object.defineProperty(a,u,h)}):(function(a,s,l,u){u===void 0&&(u=l),a[u]=s[l]})),r=e&&e.__exportStar||function(a,s){for(var l in a)l!=="default"&&!Object.prototype.hasOwnProperty.call(s,l)&&t(s,a,l)};Object.defineProperty(e,"__esModule",{value:!0}),e.LSPErrorCodes=e.createProtocolConnection=void 0,r(Mv(),e),r((dw(),nW(gR)),e),r(ki(),e),r(ryt(),e);var n=nyt();Object.defineProperty(e,"createProtocolConnection",{enumerable:!0,get:E(function(){return n.createProtocolConnection},"get")});var i;(function(a){a.lspReservedErrorRangeStart=-32899,a.RequestFailed=-32803,a.ServerCancelled=-32802,a.ContentModified=-32801,a.RequestCancelled=-32800,a.lspReservedErrorRangeEnd=-32800})(i||(e.LSPErrorCodes=i={}))}}),ayt=zr({"../../node_modules/.pnpm/vscode-languageserver-protocol@3.17.5/node_modules/vscode-languageserver-protocol/lib/browser/main.js"(e){"use strict";var t=e&&e.__createBinding||(Object.create?(function(a,s,l,u){u===void 0&&(u=l);var h=Object.getOwnPropertyDescriptor(s,l);(!h||("get"in h?!s.__esModule:h.writable||h.configurable))&&(h={enumerable:!0,get:E(function(){return s[l]},"get")}),Object.defineProperty(a,u,h)}):(function(a,s,l,u){u===void 0&&(u=l),a[u]=s[l]})),r=e&&e.__exportStar||function(a,s){for(var l in a)l!=="default"&&!Object.prototype.hasOwnProperty.call(s,l)&&t(s,a,l)};Object.defineProperty(e,"__esModule",{value:!0}),e.createProtocolConnection=void 0;var n=BTe();r(BTe(),e),r(iyt(),e);function i(a,s,l,u){return(0,n.createMessageConnection)(a,s,l,u)}o(i,"createProtocolConnection"),E(i,"createProtocolConnection"),e.createProtocolConnection=i}}),Owe={};vp(Owe,{AbstractAstReflection:o(()=>oW,"AbstractAstReflection"),AbstractCstNode:o(()=>cH,"AbstractCstNode"),AbstractLangiumParser:o(()=>hH,"AbstractLangiumParser"),AbstractParserErrorMessageProvider:o(()=>k_e,"AbstractParserErrorMessageProvider"),AbstractThreadedAsyncParser:o(()=>NEt,"AbstractThreadedAsyncParser"),AstUtils:o(()=>lW,"AstUtils"),BiMap:o(()=>uR,"BiMap"),Cancellation:o(()=>Qn,"Cancellation"),CompositeCstNodeImpl:o(()=>f_,"CompositeCstNodeImpl"),ContextCache:o(()=>b_,"ContextCache"),CstNodeBuilder:o(()=>b_e,"CstNodeBuilder"),CstUtils:o(()=>aW,"CstUtils"),DEFAULT_TOKENIZE_OPTIONS:o(()=>_H,"DEFAULT_TOKENIZE_OPTIONS"),DONE_RESULT:o(()=>Os,"DONE_RESULT"),DatatypeSymbol:o(()=>sR,"DatatypeSymbol"),DefaultAstNodeDescriptionProvider:o(()=>eLe,"DefaultAstNodeDescriptionProvider"),DefaultAstNodeLocator:o(()=>rLe,"DefaultAstNodeLocator"),DefaultAsyncParser:o(()=>bLe,"DefaultAsyncParser"),DefaultCommentProvider:o(()=>xLe,"DefaultCommentProvider"),DefaultConfigurationProvider:o(()=>nLe,"DefaultConfigurationProvider"),DefaultDocumentBuilder:o(()=>iLe,"DefaultDocumentBuilder"),DefaultDocumentValidator:o(()=>J_e,"DefaultDocumentValidator"),DefaultHydrator:o(()=>CLe,"DefaultHydrator"),DefaultIndexManager:o(()=>aLe,"DefaultIndexManager"),DefaultJsonSerializer:o(()=>X_e,"DefaultJsonSerializer"),DefaultLangiumDocumentFactory:o(()=>z_e,"DefaultLangiumDocumentFactory"),DefaultLangiumDocuments:o(()=>G_e,"DefaultLangiumDocuments"),DefaultLangiumProfiler:o(()=>FEt,"DefaultLangiumProfiler"),DefaultLexer:o(()=>LH,"DefaultLexer"),DefaultLexerErrorMessageProvider:o(()=>oLe,"DefaultLexerErrorMessageProvider"),DefaultLinker:o(()=>V_e,"DefaultLinker"),DefaultNameProvider:o(()=>W_e,"DefaultNameProvider"),DefaultReferenceDescriptionProvider:o(()=>tLe,"DefaultReferenceDescriptionProvider"),DefaultReferences:o(()=>q_e,"DefaultReferences"),DefaultScopeComputation:o(()=>H_e,"DefaultScopeComputation"),DefaultScopeProvider:o(()=>j_e,"DefaultScopeProvider"),DefaultServiceRegistry:o(()=>K_e,"DefaultServiceRegistry"),DefaultTokenBuilder:o(()=>g_,"DefaultTokenBuilder"),DefaultValueConverter:o(()=>vH,"DefaultValueConverter"),DefaultWorkspaceLock:o(()=>TLe,"DefaultWorkspaceLock"),DefaultWorkspaceManager:o(()=>sLe,"DefaultWorkspaceManager"),Deferred:o(()=>ed,"Deferred"),Disposable:o(()=>jg,"Disposable"),DisposableCache:o(()=>x_,"DisposableCache"),DocumentCache:o(()=>Y_e,"DocumentCache"),DocumentState:o(()=>en,"DocumentState"),DocumentValidator:o(()=>Rl,"DocumentValidator"),EMPTY_SCOPE:o(()=>LEt,"EMPTY_SCOPE"),EMPTY_STREAM:o(()=>Tv,"EMPTY_STREAM"),EmptyFileSystem:o(()=>yn,"EmptyFileSystem"),EmptyFileSystemProvider:o(()=>SLe,"EmptyFileSystemProvider"),ErrorWithLocation:o(()=>ER,"ErrorWithLocation"),GrammarAST:o(()=>Fwe,"GrammarAST"),GrammarUtils:o(()=>$W,"GrammarUtils"),IndentationAwareLexer:o(()=>OEt,"IndentationAwareLexer"),IndentationAwareTokenBuilder:o(()=>kLe,"IndentationAwareTokenBuilder"),JSDocDocumentationProvider:o(()=>vLe,"JSDocDocumentationProvider"),LangiumCompletionParser:o(()=>S_e,"LangiumCompletionParser"),LangiumParser:o(()=>w_e,"LangiumParser"),LangiumParserErrorMessageProvider:o(()=>dH,"LangiumParserErrorMessageProvider"),LeafCstNodeImpl:o(()=>aR,"LeafCstNodeImpl"),LexingMode:o(()=>Ug,"LexingMode"),MapScope:o(()=>_Et,"MapScope"),Module:o(()=>iV,"Module"),MultiMap:o(()=>td,"MultiMap"),MultiMapScope:o(()=>U_e,"MultiMapScope"),OperationCancelled:o(()=>Pu,"OperationCancelled"),ParserWorker:o(()=>PEt,"ParserWorker"),ProfilingTask:o(()=>ALe,"ProfilingTask"),Reduction:o(()=>XC,"Reduction"),RefResolving:o(()=>yg,"RefResolving"),RegExpUtils:o(()=>zW,"RegExpUtils"),RootCstNodeImpl:o(()=>uH,"RootCstNodeImpl"),SimpleCache:o(()=>kH,"SimpleCache"),StreamImpl:o(()=>Nu,"StreamImpl"),StreamScope:o(()=>eV,"StreamScope"),TextDocument:o(()=>lR,"TextDocument"),TreeStreamImpl:o(()=>Cv,"TreeStreamImpl"),URI:o(()=>Yo,"URI"),UriTrie:o(()=>CH,"UriTrie"),UriUtils:o(()=>$s,"UriUtils"),VALIDATE_EACH_NODE:o(()=>Q_e,"VALIDATE_EACH_NODE"),ValidationCategory:o(()=>hR,"ValidationCategory"),ValidationRegistry:o(()=>Z_e,"ValidationRegistry"),ValueConverter:o(()=>Iu,"ValueConverter"),WorkspaceCache:o(()=>SH,"WorkspaceCache"),assertCondition:o(()=>FW,"assertCondition"),assertUnreachable:o(()=>xp,"assertUnreachable"),createCompletionParser:o(()=>mH,"createCompletionParser"),createDefaultCoreModule:o(()=>hn,"createDefaultCoreModule"),createDefaultSharedCoreModule:o(()=>dn,"createDefaultSharedCoreModule"),createGrammarConfig:o(()=>iq,"createGrammarConfig"),createLangiumParser:o(()=>gH,"createLangiumParser"),createParser:o(()=>p_,"createParser"),delayNextTick:o(()=>y_,"delayNextTick"),diagnosticData:o(()=>Hg,"diagnosticData"),eagerLoad:o(()=>BH,"eagerLoad"),getDiagnosticRange:o(()=>AH,"getDiagnosticRange"),indentationBuilderDefaultOptions:o(()=>sV,"indentationBuilderDefaultOptions"),inject:o(()=>Mr,"inject"),interruptAndCheck:o(()=>Ia,"interruptAndCheck"),isAstNode:o(()=>Zi,"isAstNode"),isAstNodeDescription:o(()=>sW,"isAstNodeDescription"),isAstNodeWithComment:o(()=>EH,"isAstNodeWithComment"),isCompositeCstNode:o(()=>Uh,"isCompositeCstNode"),isIMultiModeLexerDefinition:o(()=>w_,"isIMultiModeLexerDefinition"),isJSDoc:o(()=>IH,"isJSDoc"),isLeafCstNode:o(()=>e0,"isLeafCstNode"),isLinkingError:o(()=>Cg,"isLinkingError"),isMultiReference:o(()=>Ou,"isMultiReference"),isNamed:o(()=>wH,"isNamed"),isOperationCancelled:o(()=>x0,"isOperationCancelled"),isReference:o(()=>Bs,"isReference"),isRootCstNode:o(()=>vR,"isRootCstNode"),isTokenTypeArray:o(()=>C_,"isTokenTypeArray"),isTokenTypeDictionary:o(()=>dR,"isTokenTypeDictionary"),loadGrammarFromJson:o(()=>Ma,"loadGrammarFromJson"),parseJSDoc:o(()=>DH,"parseJSDoc"),prepareLangiumParser:o(()=>yH,"prepareLangiumParser"),setInterruptionPeriod:o(()=>xH,"setInterruptionPeriod"),startCancelableOperation:o(()=>v_,"startCancelableOperation"),stream:o(()=>Bn,"stream"),toDiagnosticData:o(()=>RH,"toDiagnosticData"),toDiagnosticSeverity:o(()=>qC,"toDiagnosticSeverity")});aW={};vp(aW,{DefaultNameRegexp:o(()=>MW,"DefaultNameRegexp"),RangeComparison:o(()=>Mu,"RangeComparison"),compareRange:o(()=>DW,"compareRange"),findCommentNode:o(()=>NW,"findCommentNode"),findDeclarationNodeAtOffset:o(()=>ike,"findDeclarationNodeAtOffset"),findLeafNodeAtOffset:o(()=>SR,"findLeafNodeAtOffset"),findLeafNodeBeforeOffset:o(()=>PW,"findLeafNodeBeforeOffset"),flattenCst:o(()=>nke,"flattenCst"),getDatatypeNode:o(()=>rke,"getDatatypeNode"),getInteriorNodes:o(()=>oke,"getInteriorNodes"),getNextNode:o(()=>ake,"getNextNode"),getPreviousNode:o(()=>BW,"getPreviousNode"),getStartlineNode:o(()=>ske,"getStartlineNode"),inRange:o(()=>IW,"inRange"),isChildNode:o(()=>LW,"isChildNode"),isCommentNode:o(()=>W6,"isCommentNode"),streamCst:o(()=>Ev,"streamCst"),toDocumentSegment:o(()=>Av,"toDocumentSegment"),tokenToRange:o(()=>KC,"tokenToRange")});o(Zi,"isAstNode");E(Zi,"isAstNode");o(Bs,"isReference");E(Bs,"isReference");o(Ou,"isMultiReference");E(Ou,"isMultiReference");o(sW,"isAstNodeDescription");E(sW,"isAstNodeDescription");o(Cg,"isLinkingError");E(Cg,"isLinkingError");oW=class{static{o(this,"AbstractAstReflection")}static{E(this,"AbstractAstReflection")}constructor(){this.subtypes={},this.allSubtypes={}}getAllTypes(){return Object.keys(this.types)}getReferenceType(e){let t=this.types[e.container.$type];if(!t)throw new Error(`Type ${e.container.$type||"undefined"} not found.`);let r=t.properties[e.property]?.referenceType;if(!r)throw new Error(`Property ${e.property||"undefined"} of type ${e.container.$type} is not a reference.`);return r}getTypeMetaData(e){let t=this.types[e];return t||{name:e,properties:{},superTypes:[]}}isInstance(e,t){return Zi(e)&&this.isSubtype(e.$type,t)}isSubtype(e,t){if(e===t)return!0;let r=this.subtypes[e];r||(r=this.subtypes[e]={});let n=r[t];if(n!==void 0)return n;{let i=this.types[e],a=i?i.superTypes.some(s=>this.isSubtype(s,t)):!1;return r[t]=a,a}}getAllSubTypes(e){let t=this.allSubtypes[e];if(t)return t;{let r=this.getAllTypes(),n=[];for(let i of r)this.isSubtype(i,e)&&n.push(i);return this.allSubtypes[e]=n,n}}};o(Uh,"isCompositeCstNode");E(Uh,"isCompositeCstNode");o(e0,"isLeafCstNode");E(e0,"isLeafCstNode");o(vR,"isRootCstNode");E(vR,"isRootCstNode");Nu=class Gh{static{o(this,"_StreamImpl")}static{E(this,"StreamImpl")}constructor(t,r){this.startFn=t,this.nextFn=r}iterator(){let t={state:this.startFn(),next:E(()=>this.nextFn(t.state),"next"),[Symbol.iterator]:()=>t};return t}[Symbol.iterator](){return this.iterator()}isEmpty(){return!!this.iterator().next().done}count(){let t=this.iterator(),r=0,n=t.next();for(;!n.done;)r++,n=t.next();return r}toArray(){let t=[],r=this.iterator(),n;do n=r.next(),n.value!==void 0&&t.push(n.value);while(!n.done);return t}toSet(){return new Set(this)}toMap(t,r){let n=this.map(i=>[t?t(i):i,r?r(i):i]);return new Map(n)}toString(){return this.join()}concat(t){return new Gh(()=>({first:this.startFn(),firstDone:!1,iterator:t[Symbol.iterator]()}),r=>{let n;if(!r.firstDone){do if(n=this.nextFn(r.first),!n.done)return n;while(!n.done);r.firstDone=!0}do if(n=r.iterator.next(),!n.done)return n;while(!n.done);return Os})}join(t=","){let r=this.iterator(),n="",i,a=!1;do i=r.next(),i.done||(a&&(n+=t),n+=Bwe(i.value)),a=!0;while(!i.done);return n}indexOf(t,r=0){let n=this.iterator(),i=0,a=n.next();for(;!a.done;){if(i>=r&&a.value===t)return i;a=n.next(),i++}return-1}every(t){let r=this.iterator(),n=r.next();for(;!n.done;){if(!t(n.value))return!1;n=r.next()}return!0}some(t){let r=this.iterator(),n=r.next();for(;!n.done;){if(t(n.value))return!0;n=r.next()}return!1}forEach(t){let r=this.iterator(),n=0,i=r.next();for(;!i.done;)t(i.value,n),i=r.next(),n++}map(t){return new Gh(this.startFn,r=>{let{done:n,value:i}=this.nextFn(r);return n?Os:{done:!1,value:t(i)}})}filter(t){return new Gh(this.startFn,r=>{let n;do if(n=this.nextFn(r),!n.done&&t(n.value))return n;while(!n.done);return Os})}nonNullable(){return this.filter(t=>t!=null)}reduce(t,r){let n=this.iterator(),i=r,a=n.next();for(;!a.done;)i===void 0?i=a.value:i=t(i,a.value),a=n.next();return i}reduceRight(t,r){return this.recursiveReduce(this.iterator(),t,r)}recursiveReduce(t,r,n){let i=t.next();if(i.done)return n;let a=this.recursiveReduce(t,r,n);return a===void 0?i.value:r(a,i.value)}find(t){let r=this.iterator(),n=r.next();for(;!n.done;){if(t(n.value))return n.value;n=r.next()}}findIndex(t){let r=this.iterator(),n=0,i=r.next();for(;!i.done;){if(t(i.value))return n;i=r.next(),n++}return-1}includes(t){let r=this.iterator(),n=r.next();for(;!n.done;){if(n.value===t)return!0;n=r.next()}return!1}flatMap(t){return new Gh(()=>({this:this.startFn()}),r=>{do{if(r.iterator){let a=r.iterator.next();if(a.done)r.iterator=void 0;else return a}let{done:n,value:i}=this.nextFn(r.this);if(!n){let a=t(i);if(jC(a))r.iterator=a[Symbol.iterator]();else return{done:!1,value:a}}}while(r.iterator);return Os})}flat(t){if(t===void 0&&(t=1),t<=0)return this;let r=t>1?this.flat(t-1):this;return new Gh(()=>({this:r.startFn()}),n=>{do{if(n.iterator){let s=n.iterator.next();if(s.done)n.iterator=void 0;else return s}let{done:i,value:a}=r.nextFn(n.this);if(!i)if(jC(a))n.iterator=a[Symbol.iterator]();else return{done:!1,value:a}}while(n.iterator);return Os})}head(){let r=this.iterator().next();if(!r.done)return r.value}tail(t=1){return new Gh(()=>{let r=this.startFn();for(let n=0;n({size:0,state:this.startFn()}),r=>(r.size++,r.size>t?Os:this.nextFn(r.state)))}distinct(t){return new Gh(()=>({set:new Set,internalState:this.startFn()}),r=>{let n;do if(n=this.nextFn(r.internalState),!n.done){let i=t?t(n.value):n.value;if(!r.set.has(i))return r.set.add(i),n}while(!n.done);return Os})}exclude(t,r){let n=new Set;for(let i of t){let a=r?r(i):i;n.add(a)}return this.filter(i=>{let a=r?r(i):i;return!n.has(a)})}};o(Bwe,"toString");E(Bwe,"toString");o(jC,"isIterable");E(jC,"isIterable");Tv=new Nu(()=>{},()=>Os),Os=Object.freeze({done:!0,value:void 0});o(Bn,"stream");E(Bn,"stream");Cv=class extends Nu{static{o(this,"TreeStreamImpl")}static{E(this,"TreeStreamImpl")}constructor(e,t,r){super(()=>({iterators:r?.includeRoot?[[e][Symbol.iterator]()]:[t(e)[Symbol.iterator]()],pruned:!1}),n=>{for(n.pruned&&(n.iterators.pop(),n.pruned=!1);n.iterators.length>0;){let a=n.iterators[n.iterators.length-1].next();if(a.done)n.iterators.pop();else return n.iterators.push(t(a.value)[Symbol.iterator]()),a}return Os})}iterator(){let e={state:this.startFn(),next:E(()=>this.nextFn(e.state),"next"),prune:E(()=>{e.state.pruned=!0},"prune"),[Symbol.iterator]:()=>e};return e}};(function(e){function t(a){return a.reduce((s,l)=>s+l,0)}o(t,"sum"),E(t,"sum"),e.sum=t;function r(a){return a.reduce((s,l)=>s*l,0)}o(r,"product"),E(r,"product"),e.product=r;function n(a){return a.reduce((s,l)=>Math.min(s,l))}o(n,"min2"),E(n,"min"),e.min=n;function i(a){return a.reduce((s,l)=>Math.max(s,l))}o(i,"max"),E(i,"max"),e.max=i})(XC||(XC={}));lW={};vp(lW,{assignMandatoryProperties:o(()=>cW,"assignMandatoryProperties"),copyAstNode:o(()=>_6,"copyAstNode"),findRootNode:o(()=>yv,"findRootNode"),getContainerOfType:o(()=>t0,"getContainerOfType"),getDocument:o(()=>wc,"getDocument"),getReferenceNodes:o(()=>A6,"getReferenceNodes"),hasContainerOfType:o(()=>$we,"hasContainerOfType"),linkContentToContainer:o(()=>wv,"linkContentToContainer"),streamAllContents:o(()=>rd,"streamAllContents"),streamAst:o(()=>kc,"streamAst"),streamContents:o(()=>pw,"streamContents"),streamReferences:o(()=>kv,"streamReferences")});o(wv,"linkContentToContainer");E(wv,"linkContentToContainer");o(t0,"getContainerOfType");E(t0,"getContainerOfType");o($we,"hasContainerOfType");E($we,"hasContainerOfType");o(wc,"getDocument");E(wc,"getDocument");o(yv,"findRootNode");E(yv,"findRootNode");o(A6,"getReferenceNodes");E(A6,"getReferenceNodes");o(pw,"streamContents");E(pw,"streamContents");o(rd,"streamAllContents");E(rd,"streamAllContents");o(kc,"streamAst");E(kc,"streamAst");o(R6,"isAstNodeInRange");E(R6,"isAstNodeInRange");o(kv,"streamReferences");E(kv,"streamReferences");o(cW,"assignMandatoryProperties");E(cW,"assignMandatoryProperties");o(uW,"copyDefaultValue");E(uW,"copyDefaultValue");o(_6,"copyAstNode");E(_6,"copyAstNode");Fwe={};vp(Fwe,{AbstractElement:o(()=>xo,"AbstractElement"),AbstractParserRule:o(()=>LC,"AbstractParserRule"),AbstractRule:o(()=>ov,"AbstractRule"),AbstractType:o(()=>Uo,"AbstractType"),Action:o(()=>ap,"Action"),Alternatives:o(()=>DC,"Alternatives"),ArrayLiteral:o(()=>L6,"ArrayLiteral"),ArrayType:o(()=>D6,"ArrayType"),Assignment:o(()=>sp,"Assignment"),BooleanLiteral:o(()=>I6,"BooleanLiteral"),CharacterRange:o(()=>op,"CharacterRange"),Condition:o(()=>lp,"Condition"),Conjunction:o(()=>IC,"Conjunction"),CrossReference:o(()=>cp,"CrossReference"),Disjunction:o(()=>MC,"Disjunction"),EndOfFile:o(()=>M6,"EndOfFile"),Grammar:o(()=>Wh,"Grammar"),GrammarImport:o(()=>N6,"GrammarImport"),Group:o(()=>wg,"Group"),InferredType:o(()=>P6,"InferredType"),InfixRule:o(()=>Du,"InfixRule"),InfixRuleOperatorList:o(()=>NC,"InfixRuleOperatorList"),InfixRuleOperators:o(()=>O6,"InfixRuleOperators"),Interface:o(()=>lv,"Interface"),Keyword:o(()=>cv,"Keyword"),LangiumGrammarAstReflection:o(()=>_W,"LangiumGrammarAstReflection"),LangiumGrammarTerminals:o(()=>syt,"LangiumGrammarTerminals"),NamedArgument:o(()=>uv,"NamedArgument"),NegatedToken:o(()=>kg,"NegatedToken"),Negation:o(()=>B6,"Negation"),NumberLiteral:o(()=>$6,"NumberLiteral"),Parameter:o(()=>hv,"Parameter"),ParameterReference:o(()=>F6,"ParameterReference"),ParserRule:o(()=>xc,"ParserRule"),ReferenceType:o(()=>PC,"ReferenceType"),RegexToken:o(()=>Sg,"RegexToken"),ReturnType:o(()=>z6,"ReturnType"),RuleCall:o(()=>Eg,"RuleCall"),SimpleType:o(()=>dv,"SimpleType"),StringLiteral:o(()=>G6,"StringLiteral"),TerminalAlternatives:o(()=>Ag,"TerminalAlternatives"),TerminalElement:o(()=>bo,"TerminalElement"),TerminalGroup:o(()=>Rg,"TerminalGroup"),TerminalRule:o(()=>qh,"TerminalRule"),TerminalRuleCall:o(()=>_g,"TerminalRuleCall"),Type:o(()=>OC,"Type"),TypeAttribute:o(()=>Lg,"TypeAttribute"),TypeDefinition:o(()=>Dg,"TypeDefinition"),UnionType:o(()=>V6,"UnionType"),UnorderedGroup:o(()=>BC,"UnorderedGroup"),UntilToken:o(()=>Ig,"UntilToken"),ValueLiteral:o(()=>Mg,"ValueLiteral"),Wildcard:o(()=>fv,"Wildcard"),isAbstractElement:o(()=>xR,"isAbstractElement"),isAbstractParserRule:o(()=>r0,"isAbstractParserRule"),isAbstractRule:o(()=>zwe,"isAbstractRule"),isAbstractType:o(()=>Gwe,"isAbstractType"),isAction:o(()=>fp,"isAction"),isAlternatives:o(()=>bR,"isAlternatives"),isArrayLiteral:o(()=>Vwe,"isArrayLiteral"),isArrayType:o(()=>hW,"isArrayType"),isAssignment:o(()=>Yh,"isAssignment"),isBooleanLiteral:o(()=>dW,"isBooleanLiteral"),isCharacterRange:o(()=>fW,"isCharacterRange"),isCondition:o(()=>Wwe,"isCondition"),isConjunction:o(()=>pW,"isConjunction"),isCrossReference:o(()=>n0,"isCrossReference"),isDisjunction:o(()=>mW,"isDisjunction"),isEndOfFile:o(()=>gW,"isEndOfFile"),isGrammar:o(()=>qwe,"isGrammar"),isGrammarImport:o(()=>Hwe,"isGrammarImport"),isGroup:o(()=>i0,"isGroup"),isInferredType:o(()=>mw,"isInferredType"),isInfixRule:o(()=>Sv,"isInfixRule"),isInfixRuleOperatorList:o(()=>Uwe,"isInfixRuleOperatorList"),isInfixRuleOperators:o(()=>Ywe,"isInfixRuleOperators"),isInterface:o(()=>yW,"isInterface"),isKeyword:o(()=>jh,"isKeyword"),isNamedArgument:o(()=>jwe,"isNamedArgument"),isNegatedToken:o(()=>vW,"isNegatedToken"),isNegation:o(()=>xW,"isNegation"),isNumberLiteral:o(()=>Xwe,"isNumberLiteral"),isParameter:o(()=>Kwe,"isParameter"),isParameterReference:o(()=>bW,"isParameterReference"),isParserRule:o(()=>zs,"isParserRule"),isReferenceType:o(()=>TW,"isReferenceType"),isRegexToken:o(()=>CW,"isRegexToken"),isReturnType:o(()=>wW,"isReturnType"),isRuleCall:o(()=>Xh,"isRuleCall"),isSimpleType:o(()=>TR,"isSimpleType"),isStringLiteral:o(()=>Zwe,"isStringLiteral"),isTerminalAlternatives:o(()=>kW,"isTerminalAlternatives"),isTerminalElement:o(()=>Qwe,"isTerminalElement"),isTerminalGroup:o(()=>SW,"isTerminalGroup"),isTerminalRule:o(()=>Il,"isTerminalRule"),isTerminalRuleCall:o(()=>CR,"isTerminalRuleCall"),isType:o(()=>wR,"isType"),isTypeAttribute:o(()=>Jwe,"isTypeAttribute"),isTypeDefinition:o(()=>eke,"isTypeDefinition"),isUnionType:o(()=>EW,"isUnionType"),isUnorderedGroup:o(()=>kR,"isUnorderedGroup"),isUntilToken:o(()=>AW,"isUntilToken"),isValueLiteral:o(()=>tke,"isValueLiteral"),isWildcard:o(()=>RW,"isWildcard"),reflection:o(()=>Tr,"reflection")});syt={ID:/\^?[_a-zA-Z][\w_]*/,STRING:/"(\\.|[^"\\])*"|'(\\.|[^'\\])*'/,NUMBER:/NaN|-?((\d*\.\d+|\d+)([Ee][+-]?\d+)?|Infinity)/,RegexLiteral:/\/(?![*+?])(?:[^\r\n\[/\\]|\\.|\[(?:[^\r\n\]\\]|\\.)*\])+\/[a-z]*/,WS:/\s+/,ML_COMMENT:/\/\*[\s\S]*?\*\//,SL_COMMENT:/\/\/[^\n\r]*/},xo={$type:"AbstractElement",cardinality:"cardinality"};o(xR,"isAbstractElement");E(xR,"isAbstractElement");LC={$type:"AbstractParserRule"};o(r0,"isAbstractParserRule");E(r0,"isAbstractParserRule");ov={$type:"AbstractRule"};o(zwe,"isAbstractRule");E(zwe,"isAbstractRule");Uo={$type:"AbstractType"};o(Gwe,"isAbstractType");E(Gwe,"isAbstractType");ap={$type:"Action",cardinality:"cardinality",feature:"feature",inferredType:"inferredType",operator:"operator",type:"type"};o(fp,"isAction");E(fp,"isAction");DC={$type:"Alternatives",cardinality:"cardinality",elements:"elements"};o(bR,"isAlternatives");E(bR,"isAlternatives");L6={$type:"ArrayLiteral",elements:"elements"};o(Vwe,"isArrayLiteral");E(Vwe,"isArrayLiteral");D6={$type:"ArrayType",elementType:"elementType"};o(hW,"isArrayType");E(hW,"isArrayType");sp={$type:"Assignment",cardinality:"cardinality",feature:"feature",operator:"operator",predicate:"predicate",terminal:"terminal"};o(Yh,"isAssignment");E(Yh,"isAssignment");I6={$type:"BooleanLiteral",true:"true"};o(dW,"isBooleanLiteral");E(dW,"isBooleanLiteral");op={$type:"CharacterRange",cardinality:"cardinality",left:"left",lookahead:"lookahead",parenthesized:"parenthesized",right:"right"};o(fW,"isCharacterRange");E(fW,"isCharacterRange");lp={$type:"Condition"};o(Wwe,"isCondition");E(Wwe,"isCondition");IC={$type:"Conjunction",left:"left",right:"right"};o(pW,"isConjunction");E(pW,"isConjunction");cp={$type:"CrossReference",cardinality:"cardinality",deprecatedSyntax:"deprecatedSyntax",isMulti:"isMulti",terminal:"terminal",type:"type"};o(n0,"isCrossReference");E(n0,"isCrossReference");MC={$type:"Disjunction",left:"left",right:"right"};o(mW,"isDisjunction");E(mW,"isDisjunction");M6={$type:"EndOfFile",cardinality:"cardinality"};o(gW,"isEndOfFile");E(gW,"isEndOfFile");Wh={$type:"Grammar",imports:"imports",interfaces:"interfaces",isDeclared:"isDeclared",name:"name",rules:"rules",types:"types"};o(qwe,"isGrammar");E(qwe,"isGrammar");N6={$type:"GrammarImport",path:"path"};o(Hwe,"isGrammarImport");E(Hwe,"isGrammarImport");wg={$type:"Group",cardinality:"cardinality",elements:"elements",guardCondition:"guardCondition",predicate:"predicate"};o(i0,"isGroup");E(i0,"isGroup");P6={$type:"InferredType",name:"name"};o(mw,"isInferredType");E(mw,"isInferredType");Du={$type:"InfixRule",call:"call",dataType:"dataType",inferredType:"inferredType",name:"name",operators:"operators",parameters:"parameters",returnType:"returnType"};o(Sv,"isInfixRule");E(Sv,"isInfixRule");NC={$type:"InfixRuleOperatorList",associativity:"associativity",operators:"operators"};o(Uwe,"isInfixRuleOperatorList");E(Uwe,"isInfixRuleOperatorList");O6={$type:"InfixRuleOperators",precedences:"precedences"};o(Ywe,"isInfixRuleOperators");E(Ywe,"isInfixRuleOperators");lv={$type:"Interface",attributes:"attributes",name:"name",superTypes:"superTypes"};o(yW,"isInterface");E(yW,"isInterface");cv={$type:"Keyword",cardinality:"cardinality",predicate:"predicate",value:"value"};o(jh,"isKeyword");E(jh,"isKeyword");uv={$type:"NamedArgument",calledByName:"calledByName",parameter:"parameter",value:"value"};o(jwe,"isNamedArgument");E(jwe,"isNamedArgument");kg={$type:"NegatedToken",cardinality:"cardinality",lookahead:"lookahead",parenthesized:"parenthesized",terminal:"terminal"};o(vW,"isNegatedToken");E(vW,"isNegatedToken");B6={$type:"Negation",value:"value"};o(xW,"isNegation");E(xW,"isNegation");$6={$type:"NumberLiteral",value:"value"};o(Xwe,"isNumberLiteral");E(Xwe,"isNumberLiteral");hv={$type:"Parameter",name:"name"};o(Kwe,"isParameter");E(Kwe,"isParameter");F6={$type:"ParameterReference",parameter:"parameter"};o(bW,"isParameterReference");E(bW,"isParameterReference");xc={$type:"ParserRule",dataType:"dataType",definition:"definition",entry:"entry",fragment:"fragment",inferredType:"inferredType",name:"name",parameters:"parameters",returnType:"returnType"};o(zs,"isParserRule");E(zs,"isParserRule");PC={$type:"ReferenceType",isMulti:"isMulti",referenceType:"referenceType"};o(TW,"isReferenceType");E(TW,"isReferenceType");Sg={$type:"RegexToken",cardinality:"cardinality",lookahead:"lookahead",parenthesized:"parenthesized",regex:"regex"};o(CW,"isRegexToken");E(CW,"isRegexToken");z6={$type:"ReturnType",name:"name"};o(wW,"isReturnType");E(wW,"isReturnType");Eg={$type:"RuleCall",arguments:"arguments",cardinality:"cardinality",predicate:"predicate",rule:"rule"};o(Xh,"isRuleCall");E(Xh,"isRuleCall");dv={$type:"SimpleType",primitiveType:"primitiveType",stringType:"stringType",typeRef:"typeRef"};o(TR,"isSimpleType");E(TR,"isSimpleType");G6={$type:"StringLiteral",value:"value"};o(Zwe,"isStringLiteral");E(Zwe,"isStringLiteral");Ag={$type:"TerminalAlternatives",cardinality:"cardinality",elements:"elements",lookahead:"lookahead",parenthesized:"parenthesized"};o(kW,"isTerminalAlternatives");E(kW,"isTerminalAlternatives");bo={$type:"TerminalElement",cardinality:"cardinality",lookahead:"lookahead",parenthesized:"parenthesized"};o(Qwe,"isTerminalElement");E(Qwe,"isTerminalElement");Rg={$type:"TerminalGroup",cardinality:"cardinality",elements:"elements",lookahead:"lookahead",parenthesized:"parenthesized"};o(SW,"isTerminalGroup");E(SW,"isTerminalGroup");qh={$type:"TerminalRule",definition:"definition",fragment:"fragment",hidden:"hidden",name:"name",type:"type"};o(Il,"isTerminalRule");E(Il,"isTerminalRule");_g={$type:"TerminalRuleCall",cardinality:"cardinality",lookahead:"lookahead",parenthesized:"parenthesized",rule:"rule"};o(CR,"isTerminalRuleCall");E(CR,"isTerminalRuleCall");OC={$type:"Type",name:"name",type:"type"};o(wR,"isType");E(wR,"isType");Lg={$type:"TypeAttribute",defaultValue:"defaultValue",isOptional:"isOptional",name:"name",type:"type"};o(Jwe,"isTypeAttribute");E(Jwe,"isTypeAttribute");Dg={$type:"TypeDefinition"};o(eke,"isTypeDefinition");E(eke,"isTypeDefinition");V6={$type:"UnionType",types:"types"};o(EW,"isUnionType");E(EW,"isUnionType");BC={$type:"UnorderedGroup",cardinality:"cardinality",elements:"elements"};o(kR,"isUnorderedGroup");E(kR,"isUnorderedGroup");Ig={$type:"UntilToken",cardinality:"cardinality",lookahead:"lookahead",parenthesized:"parenthesized",terminal:"terminal"};o(AW,"isUntilToken");E(AW,"isUntilToken");Mg={$type:"ValueLiteral"};o(tke,"isValueLiteral");E(tke,"isValueLiteral");fv={$type:"Wildcard",cardinality:"cardinality",lookahead:"lookahead",parenthesized:"parenthesized"};o(RW,"isWildcard");E(RW,"isWildcard");_W=class extends oW{static{o(this,"LangiumGrammarAstReflection")}static{E(this,"LangiumGrammarAstReflection")}constructor(){super(...arguments),this.types={AbstractElement:{name:xo.$type,properties:{cardinality:{name:xo.cardinality}},superTypes:[]},AbstractParserRule:{name:LC.$type,properties:{},superTypes:[ov.$type,Uo.$type]},AbstractRule:{name:ov.$type,properties:{},superTypes:[]},AbstractType:{name:Uo.$type,properties:{},superTypes:[]},Action:{name:ap.$type,properties:{cardinality:{name:ap.cardinality},feature:{name:ap.feature},inferredType:{name:ap.inferredType},operator:{name:ap.operator},type:{name:ap.type,referenceType:Uo.$type}},superTypes:[xo.$type]},Alternatives:{name:DC.$type,properties:{cardinality:{name:DC.cardinality},elements:{name:DC.elements,defaultValue:[]}},superTypes:[xo.$type]},ArrayLiteral:{name:L6.$type,properties:{elements:{name:L6.elements,defaultValue:[]}},superTypes:[Mg.$type]},ArrayType:{name:D6.$type,properties:{elementType:{name:D6.elementType}},superTypes:[Dg.$type]},Assignment:{name:sp.$type,properties:{cardinality:{name:sp.cardinality},feature:{name:sp.feature},operator:{name:sp.operator},predicate:{name:sp.predicate},terminal:{name:sp.terminal}},superTypes:[xo.$type]},BooleanLiteral:{name:I6.$type,properties:{true:{name:I6.true,defaultValue:!1}},superTypes:[lp.$type,Mg.$type]},CharacterRange:{name:op.$type,properties:{cardinality:{name:op.cardinality},left:{name:op.left},lookahead:{name:op.lookahead},parenthesized:{name:op.parenthesized,defaultValue:!1},right:{name:op.right}},superTypes:[bo.$type]},Condition:{name:lp.$type,properties:{},superTypes:[]},Conjunction:{name:IC.$type,properties:{left:{name:IC.left},right:{name:IC.right}},superTypes:[lp.$type]},CrossReference:{name:cp.$type,properties:{cardinality:{name:cp.cardinality},deprecatedSyntax:{name:cp.deprecatedSyntax,defaultValue:!1},isMulti:{name:cp.isMulti,defaultValue:!1},terminal:{name:cp.terminal},type:{name:cp.type,referenceType:Uo.$type}},superTypes:[xo.$type]},Disjunction:{name:MC.$type,properties:{left:{name:MC.left},right:{name:MC.right}},superTypes:[lp.$type]},EndOfFile:{name:M6.$type,properties:{cardinality:{name:M6.cardinality}},superTypes:[xo.$type]},Grammar:{name:Wh.$type,properties:{imports:{name:Wh.imports,defaultValue:[]},interfaces:{name:Wh.interfaces,defaultValue:[]},isDeclared:{name:Wh.isDeclared,defaultValue:!1},name:{name:Wh.name},rules:{name:Wh.rules,defaultValue:[]},types:{name:Wh.types,defaultValue:[]}},superTypes:[]},GrammarImport:{name:N6.$type,properties:{path:{name:N6.path}},superTypes:[]},Group:{name:wg.$type,properties:{cardinality:{name:wg.cardinality},elements:{name:wg.elements,defaultValue:[]},guardCondition:{name:wg.guardCondition},predicate:{name:wg.predicate}},superTypes:[xo.$type]},InferredType:{name:P6.$type,properties:{name:{name:P6.name}},superTypes:[Uo.$type]},InfixRule:{name:Du.$type,properties:{call:{name:Du.call},dataType:{name:Du.dataType},inferredType:{name:Du.inferredType},name:{name:Du.name},operators:{name:Du.operators},parameters:{name:Du.parameters,defaultValue:[]},returnType:{name:Du.returnType,referenceType:Uo.$type}},superTypes:[LC.$type]},InfixRuleOperatorList:{name:NC.$type,properties:{associativity:{name:NC.associativity},operators:{name:NC.operators,defaultValue:[]}},superTypes:[]},InfixRuleOperators:{name:O6.$type,properties:{precedences:{name:O6.precedences,defaultValue:[]}},superTypes:[]},Interface:{name:lv.$type,properties:{attributes:{name:lv.attributes,defaultValue:[]},name:{name:lv.name},superTypes:{name:lv.superTypes,defaultValue:[],referenceType:Uo.$type}},superTypes:[Uo.$type]},Keyword:{name:cv.$type,properties:{cardinality:{name:cv.cardinality},predicate:{name:cv.predicate},value:{name:cv.value}},superTypes:[xo.$type]},NamedArgument:{name:uv.$type,properties:{calledByName:{name:uv.calledByName,defaultValue:!1},parameter:{name:uv.parameter,referenceType:hv.$type},value:{name:uv.value}},superTypes:[]},NegatedToken:{name:kg.$type,properties:{cardinality:{name:kg.cardinality},lookahead:{name:kg.lookahead},parenthesized:{name:kg.parenthesized,defaultValue:!1},terminal:{name:kg.terminal}},superTypes:[bo.$type]},Negation:{name:B6.$type,properties:{value:{name:B6.value}},superTypes:[lp.$type]},NumberLiteral:{name:$6.$type,properties:{value:{name:$6.value}},superTypes:[Mg.$type]},Parameter:{name:hv.$type,properties:{name:{name:hv.name}},superTypes:[]},ParameterReference:{name:F6.$type,properties:{parameter:{name:F6.parameter,referenceType:hv.$type}},superTypes:[lp.$type]},ParserRule:{name:xc.$type,properties:{dataType:{name:xc.dataType},definition:{name:xc.definition},entry:{name:xc.entry,defaultValue:!1},fragment:{name:xc.fragment,defaultValue:!1},inferredType:{name:xc.inferredType},name:{name:xc.name},parameters:{name:xc.parameters,defaultValue:[]},returnType:{name:xc.returnType,referenceType:Uo.$type}},superTypes:[LC.$type]},ReferenceType:{name:PC.$type,properties:{isMulti:{name:PC.isMulti,defaultValue:!1},referenceType:{name:PC.referenceType}},superTypes:[Dg.$type]},RegexToken:{name:Sg.$type,properties:{cardinality:{name:Sg.cardinality},lookahead:{name:Sg.lookahead},parenthesized:{name:Sg.parenthesized,defaultValue:!1},regex:{name:Sg.regex}},superTypes:[bo.$type]},ReturnType:{name:z6.$type,properties:{name:{name:z6.name}},superTypes:[]},RuleCall:{name:Eg.$type,properties:{arguments:{name:Eg.arguments,defaultValue:[]},cardinality:{name:Eg.cardinality},predicate:{name:Eg.predicate},rule:{name:Eg.rule,referenceType:ov.$type}},superTypes:[xo.$type]},SimpleType:{name:dv.$type,properties:{primitiveType:{name:dv.primitiveType},stringType:{name:dv.stringType},typeRef:{name:dv.typeRef,referenceType:Uo.$type}},superTypes:[Dg.$type]},StringLiteral:{name:G6.$type,properties:{value:{name:G6.value}},superTypes:[Mg.$type]},TerminalAlternatives:{name:Ag.$type,properties:{cardinality:{name:Ag.cardinality},elements:{name:Ag.elements,defaultValue:[]},lookahead:{name:Ag.lookahead},parenthesized:{name:Ag.parenthesized,defaultValue:!1}},superTypes:[bo.$type]},TerminalElement:{name:bo.$type,properties:{cardinality:{name:bo.cardinality},lookahead:{name:bo.lookahead},parenthesized:{name:bo.parenthesized,defaultValue:!1}},superTypes:[xo.$type]},TerminalGroup:{name:Rg.$type,properties:{cardinality:{name:Rg.cardinality},elements:{name:Rg.elements,defaultValue:[]},lookahead:{name:Rg.lookahead},parenthesized:{name:Rg.parenthesized,defaultValue:!1}},superTypes:[bo.$type]},TerminalRule:{name:qh.$type,properties:{definition:{name:qh.definition},fragment:{name:qh.fragment,defaultValue:!1},hidden:{name:qh.hidden,defaultValue:!1},name:{name:qh.name},type:{name:qh.type}},superTypes:[ov.$type]},TerminalRuleCall:{name:_g.$type,properties:{cardinality:{name:_g.cardinality},lookahead:{name:_g.lookahead},parenthesized:{name:_g.parenthesized,defaultValue:!1},rule:{name:_g.rule,referenceType:qh.$type}},superTypes:[bo.$type]},Type:{name:OC.$type,properties:{name:{name:OC.name},type:{name:OC.type}},superTypes:[Uo.$type]},TypeAttribute:{name:Lg.$type,properties:{defaultValue:{name:Lg.defaultValue},isOptional:{name:Lg.isOptional,defaultValue:!1},name:{name:Lg.name},type:{name:Lg.type}},superTypes:[]},TypeDefinition:{name:Dg.$type,properties:{},superTypes:[]},UnionType:{name:V6.$type,properties:{types:{name:V6.types,defaultValue:[]}},superTypes:[Dg.$type]},UnorderedGroup:{name:BC.$type,properties:{cardinality:{name:BC.cardinality},elements:{name:BC.elements,defaultValue:[]}},superTypes:[xo.$type]},UntilToken:{name:Ig.$type,properties:{cardinality:{name:Ig.cardinality},lookahead:{name:Ig.lookahead},parenthesized:{name:Ig.parenthesized,defaultValue:!1},terminal:{name:Ig.terminal}},superTypes:[bo.$type]},ValueLiteral:{name:Mg.$type,properties:{},superTypes:[]},Wildcard:{name:fv.$type,properties:{cardinality:{name:fv.cardinality},lookahead:{name:fv.lookahead},parenthesized:{name:fv.parenthesized,defaultValue:!1}},superTypes:[bo.$type]}}}},Tr=new _W;o(rke,"getDatatypeNode");E(rke,"getDatatypeNode");o(Ev,"streamCst");E(Ev,"streamCst");o(nke,"flattenCst");E(nke,"flattenCst");o(LW,"isChildNode");E(LW,"isChildNode");o(KC,"tokenToRange");E(KC,"tokenToRange");o(Av,"toDocumentSegment");E(Av,"toDocumentSegment");(function(e){e[e.Before=0]="Before",e[e.After=1]="After",e[e.OverlapFront=2]="OverlapFront",e[e.OverlapBack=3]="OverlapBack",e[e.Inside=4]="Inside",e[e.Outside=5]="Outside"})(Mu||(Mu={}));o(DW,"compareRange");E(DW,"compareRange");o(IW,"inRange");E(IW,"inRange");MW=/^[\w\p{L}]$/u;o(ike,"findDeclarationNodeAtOffset");E(ike,"findDeclarationNodeAtOffset");o(NW,"findCommentNode");E(NW,"findCommentNode");o(W6,"isCommentNode");E(W6,"isCommentNode");o(SR,"findLeafNodeAtOffset");E(SR,"findLeafNodeAtOffset");o(PW,"findLeafNodeBeforeOffset");E(PW,"findLeafNodeBeforeOffset");o(OW,"binarySearch");E(OW,"binarySearch");o(BW,"getPreviousNode");E(BW,"getPreviousNode");o(ake,"getNextNode");E(ake,"getNextNode");o(ske,"getStartlineNode");E(ske,"getStartlineNode");o(oke,"getInteriorNodes");E(oke,"getInteriorNodes");o(lke,"getCommonParent");E(lke,"getCommonParent");o(kG,"getParentChain");E(kG,"getParentChain");$W={};vp($W,{findAssignment:o(()=>ZW,"findAssignment"),findNameAssignment:o(()=>MR,"findNameAssignment"),findNodeForKeyword:o(()=>KW,"findNodeForKeyword"),findNodeForProperty:o(()=>LR,"findNodeForProperty"),findNodesForKeyword:o(()=>mke,"findNodesForKeyword"),findNodesForKeywordInternal:o(()=>IR,"findNodesForKeywordInternal"),findNodesForProperty:o(()=>XW,"findNodesForProperty"),getActionAtElement:o(()=>JW,"getActionAtElement"),getActionType:o(()=>tq,"getActionType"),getAllReachableRules:o(()=>_R,"getAllReachableRules"),getAllRulesUsedForCrossReferences:o(()=>pke,"getAllRulesUsedForCrossReferences"),getCrossReferenceTerminal:o(()=>YW,"getCrossReferenceTerminal"),getEntryRule:o(()=>qW,"getEntryRule"),getExplicitRuleType:o(()=>yw,"getExplicitRuleType"),getHiddenRules:o(()=>HW,"getHiddenRules"),getRuleType:o(()=>rq,"getRuleType"),getRuleTypeName:o(()=>bke,"getRuleTypeName"),getTypeName:o(()=>Xg,"getTypeName"),isArrayCardinality:o(()=>yke,"isArrayCardinality"),isArrayOperator:o(()=>vke,"isArrayOperator"),isCommentTerminal:o(()=>jW,"isCommentTerminal"),isDataType:o(()=>xke,"isDataType"),isDataTypeRule:o(()=>gw,"isDataTypeRule"),isOptionalCardinality:o(()=>gke,"isOptionalCardinality"),terminalRegex:o(()=>vw,"terminalRegex")});ER=class extends Error{static{o(this,"ErrorWithLocation")}static{E(this,"ErrorWithLocation")}constructor(e,t){super(e?`${t} at ${e.range.start.line}:${e.range.start.character}`:t)}};o(xp,"assertUnreachable");E(xp,"assertUnreachable");o(FW,"assertCondition");E(FW,"assertCondition");zW={};vp(zW,{NEWLINE_REGEXP:o(()=>uke,"NEWLINE_REGEXP"),escapeRegExp:o(()=>Nv,"escapeRegExp"),getTerminalParts:o(()=>dke,"getTerminalParts"),isMultilineComment:o(()=>GW,"isMultilineComment"),isWhitespace:o(()=>RR,"isWhitespace"),partialMatches:o(()=>VW,"partialMatches"),partialRegExp:o(()=>WW,"partialRegExp"),whitespaceCharacters:o(()=>fke,"whitespaceCharacters")});o(Dr,"cc");E(Dr,"cc");o(a6,"insertToSet");E(a6,"insertToSet");o(rv,"addFlag");E(rv,"addFlag");o(pg,"ASSERT_EXISTS");E(pg,"ASSERT_EXISTS");o(SC,"ASSERT_NEVER_REACH_HERE");E(SC,"ASSERT_NEVER_REACH_HERE");o(SG,"isCharacter");E(SG,"isCharacter");q6=[];for(let e=Dr("0");e<=Dr("9");e++)q6.push(e);H6=[Dr("_")].concat(q6);for(let e=Dr("a");e<=Dr("z");e++)H6.push(e);for(let e=Dr("A");e<=Dr("Z");e++)H6.push(e);$Te=[Dr(" "),Dr("\f"),Dr(` +`),Dr("\r"),Dr(" "),Dr("\v"),Dr(" "),Dr("\xA0"),Dr("\u1680"),Dr("\u2000"),Dr("\u2001"),Dr("\u2002"),Dr("\u2003"),Dr("\u2004"),Dr("\u2005"),Dr("\u2006"),Dr("\u2007"),Dr("\u2008"),Dr("\u2009"),Dr("\u200A"),Dr("\u2028"),Dr("\u2029"),Dr("\u202F"),Dr("\u205F"),Dr("\u3000"),Dr("\uFEFF")],oyt=/[0-9a-fA-F]/,MA=/[0-9]/,lyt=/[1-9]/,cke=class{static{o(this,"RegExpParser")}static{E(this,"RegExpParser")}constructor(){this.idx=0,this.input="",this.groupIdx=0}saveState(){return{idx:this.idx,input:this.input,groupIdx:this.groupIdx}}restoreState(e){this.idx=e.idx,this.input=e.input,this.groupIdx=e.groupIdx}pattern(e){this.idx=0,this.input=e,this.groupIdx=0,this.consumeChar("/");let t=this.disjunction();this.consumeChar("/");let r={type:"Flags",loc:{begin:this.idx,end:e.length},global:!1,ignoreCase:!1,multiLine:!1,unicode:!1,sticky:!1};for(;this.isRegExpFlag();)switch(this.popChar()){case"g":rv(r,"global");break;case"i":rv(r,"ignoreCase");break;case"m":rv(r,"multiLine");break;case"u":rv(r,"unicode");break;case"y":rv(r,"sticky");break}if(this.idx!==this.input.length)throw Error("Redundant input: "+this.input.substring(this.idx));return{type:"Pattern",flags:r,value:t,loc:this.loc(0)}}disjunction(){let e=[],t=this.idx;for(e.push(this.alternative());this.peekChar()==="|";)this.consumeChar("|"),e.push(this.alternative());return{type:"Disjunction",value:e,loc:this.loc(t)}}alternative(){let e=[],t=this.idx;for(;this.isTerm();)e.push(this.term());return{type:"Alternative",value:e,loc:this.loc(t)}}term(){return this.isAssertion()?this.assertion():this.atom()}assertion(){let e=this.idx;switch(this.popChar()){case"^":return{type:"StartAnchor",loc:this.loc(e)};case"$":return{type:"EndAnchor",loc:this.loc(e)};case"\\":switch(this.popChar()){case"b":return{type:"WordBoundary",loc:this.loc(e)};case"B":return{type:"NonWordBoundary",loc:this.loc(e)}}throw Error("Invalid Assertion Escape");case"(":this.consumeChar("?");let t;switch(this.popChar()){case"=":t="Lookahead";break;case"!":t="NegativeLookahead";break;case"<":{switch(this.popChar()){case"=":t="Lookbehind";break;case"!":t="NegativeLookbehind"}break}}pg(t);let r=this.disjunction();return this.consumeChar(")"),{type:t,value:r,loc:this.loc(e)}}return SC()}quantifier(e=!1){let t,r=this.idx;switch(this.popChar()){case"*":t={atLeast:0,atMost:1/0};break;case"+":t={atLeast:1,atMost:1/0};break;case"?":t={atLeast:0,atMost:1};break;case"{":let n=this.integerIncludingZero();switch(this.popChar()){case"}":t={atLeast:n,atMost:n};break;case",":let i;this.isDigit()?(i=this.integerIncludingZero(),t={atLeast:n,atMost:i}):t={atLeast:n,atMost:1/0},this.consumeChar("}");break}if(e===!0&&t===void 0)return;pg(t);break}if(!(e===!0&&t===void 0)&&pg(t))return this.peekChar(0)==="?"?(this.consumeChar("?"),t.greedy=!1):t.greedy=!0,t.type="Quantifier",t.loc=this.loc(r),t}atom(){let e,t=this.idx;switch(this.peekChar()){case".":e=this.dotAll();break;case"\\":e=this.atomEscape();break;case"[":e=this.characterClass();break;case"(":e=this.group();break}return e===void 0&&this.isPatternCharacter()&&(e=this.patternCharacter()),pg(e)?(e.loc=this.loc(t),this.isQuantifier()&&(e.quantifier=this.quantifier()),e):SC()}dotAll(){return this.consumeChar("."),{type:"Set",complement:!0,value:[Dr(` +`),Dr("\r"),Dr("\u2028"),Dr("\u2029")]}}atomEscape(){switch(this.consumeChar("\\"),this.peekChar()){case"1":case"2":case"3":case"4":case"5":case"6":case"7":case"8":case"9":return this.decimalEscapeAtom();case"d":case"D":case"s":case"S":case"w":case"W":return this.characterClassEscape();case"f":case"n":case"r":case"t":case"v":return this.controlEscapeAtom();case"c":return this.controlLetterEscapeAtom();case"0":return this.nulCharacterAtom();case"x":return this.hexEscapeSequenceAtom();case"u":return this.regExpUnicodeEscapeSequenceAtom();default:return this.identityEscapeAtom()}}decimalEscapeAtom(){return{type:"GroupBackReference",value:this.positiveInteger()}}characterClassEscape(){let e,t=!1;switch(this.popChar()){case"d":e=q6;break;case"D":e=q6,t=!0;break;case"s":e=$Te;break;case"S":e=$Te,t=!0;break;case"w":e=H6;break;case"W":e=H6,t=!0;break}return pg(e)?{type:"Set",value:e,complement:t}:SC()}controlEscapeAtom(){let e;switch(this.popChar()){case"f":e=Dr("\f");break;case"n":e=Dr(` +`);break;case"r":e=Dr("\r");break;case"t":e=Dr(" ");break;case"v":e=Dr("\v");break}return pg(e)?{type:"Character",value:e}:SC()}controlLetterEscapeAtom(){this.consumeChar("c");let e=this.popChar();if(/[a-zA-Z]/.test(e)===!1)throw Error("Invalid ");return{type:"Character",value:e.toUpperCase().charCodeAt(0)-64}}nulCharacterAtom(){return this.consumeChar("0"),{type:"Character",value:Dr("\0")}}hexEscapeSequenceAtom(){return this.consumeChar("x"),this.parseHexDigits(2)}regExpUnicodeEscapeSequenceAtom(){return this.consumeChar("u"),this.parseHexDigits(4)}identityEscapeAtom(){let e=this.popChar();return{type:"Character",value:Dr(e)}}classPatternCharacterAtom(){switch(this.peekChar()){case` +`:case"\r":case"\u2028":case"\u2029":case"\\":case"]":throw Error("TBD");default:let e=this.popChar();return{type:"Character",value:Dr(e)}}}characterClass(){let e=[],t=!1;for(this.consumeChar("["),this.peekChar(0)==="^"&&(this.consumeChar("^"),t=!0);this.isClassAtom();){let r=this.classAtom(),n=r.type==="Character";if(SG(r)&&this.isRangeDash()){this.consumeChar("-");let i=this.classAtom(),a=i.type==="Character";if(SG(i)){if(i.value=this.input.length)throw Error("Unexpected end of input");this.idx++}loc(e){return{begin:e,end:this.idx}}},AR=class{static{o(this,"BaseRegExpVisitor")}static{E(this,"BaseRegExpVisitor")}visitChildren(e){for(let t in e){let r=e[t];e.hasOwnProperty(t)&&(r.type!==void 0?this.visit(r):Array.isArray(r)&&r.forEach(n=>{this.visit(n)},this))}}visit(e){switch(e.type){case"Pattern":this.visitPattern(e);break;case"Flags":this.visitFlags(e);break;case"Disjunction":this.visitDisjunction(e);break;case"Alternative":this.visitAlternative(e);break;case"StartAnchor":this.visitStartAnchor(e);break;case"EndAnchor":this.visitEndAnchor(e);break;case"WordBoundary":this.visitWordBoundary(e);break;case"NonWordBoundary":this.visitNonWordBoundary(e);break;case"Lookahead":this.visitLookahead(e);break;case"NegativeLookahead":this.visitNegativeLookahead(e);break;case"Lookbehind":this.visitLookbehind(e);break;case"NegativeLookbehind":this.visitNegativeLookbehind(e);break;case"Character":this.visitCharacter(e);break;case"Set":this.visitSet(e);break;case"Group":this.visitGroup(e);break;case"GroupBackReference":this.visitGroupBackReference(e);break;case"Quantifier":this.visitQuantifier(e);break}this.visitChildren(e)}visitPattern(e){}visitFlags(e){}visitDisjunction(e){}visitAlternative(e){}visitStartAnchor(e){}visitEndAnchor(e){}visitWordBoundary(e){}visitNonWordBoundary(e){}visitLookahead(e){}visitNegativeLookahead(e){}visitLookbehind(e){}visitNegativeLookbehind(e){}visitCharacter(e){}visitSet(e){}visitGroup(e){}visitGroupBackReference(e){}visitQuantifier(e){}},uke=/\r?\n/gm,hke=new cke,cyt=class extends AR{static{o(this,"TerminalRegExpVisitor")}static{E(this,"TerminalRegExpVisitor")}constructor(){super(...arguments),this.isStarting=!0,this.endRegexpStack=[],this.multiline=!1}get endRegex(){return this.endRegexpStack.join("")}reset(e){this.multiline=!1,this.regex=e,this.startRegexp="",this.isStarting=!0,this.endRegexpStack=[]}visitGroup(e){e.quantifier&&(this.isStarting=!1,this.endRegexpStack=[])}visitCharacter(e){let t=String.fromCharCode(e.value);if(!this.multiline&&t===` +`&&(this.multiline=!0),e.quantifier)this.isStarting=!1,this.endRegexpStack=[];else{let r=Nv(t);this.endRegexpStack.push(r),this.isStarting&&(this.startRegexp+=r)}}visitSet(e){if(!this.multiline){let t=this.regex.substring(e.loc.begin,e.loc.end),r=new RegExp(t);this.multiline=!!` +`.match(r)}if(e.quantifier)this.isStarting=!1,this.endRegexpStack=[];else{let t=this.regex.substring(e.loc.begin,e.loc.end);this.endRegexpStack.push(t),this.isStarting&&(this.startRegexp+=t)}}visitChildren(e){e.type==="Group"&&e.quantifier||super.visitChildren(e)}},Wg=new cyt;o(dke,"getTerminalParts");E(dke,"getTerminalParts");o(GW,"isMultilineComment");E(GW,"isMultilineComment");fke=`\f +\r \v \xA0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000\uFEFF`.split("");o(RR,"isWhitespace");E(RR,"isWhitespace");o(Nv,"escapeRegExp");E(Nv,"escapeRegExp");o(VW,"partialMatches");E(VW,"partialMatches");o(WW,"partialRegExp");E(WW,"partialRegExp");o(qW,"getEntryRule");E(qW,"getEntryRule");o(HW,"getHiddenRules");E(HW,"getHiddenRules");o(_R,"getAllReachableRules");E(_R,"getAllReachableRules");o(UW,"ruleDfs");E(UW,"ruleDfs");o(pke,"getAllRulesUsedForCrossReferences");E(pke,"getAllRulesUsedForCrossReferences");o(YW,"getCrossReferenceTerminal");E(YW,"getCrossReferenceTerminal");o(jW,"isCommentTerminal");E(jW,"isCommentTerminal");o(XW,"findNodesForProperty");E(XW,"findNodesForProperty");o(LR,"findNodeForProperty");E(LR,"findNodeForProperty");o(DR,"findNodesForPropertyInternal");E(DR,"findNodesForPropertyInternal");o(mke,"findNodesForKeyword");E(mke,"findNodesForKeyword");o(KW,"findNodeForKeyword");E(KW,"findNodeForKeyword");o(IR,"findNodesForKeywordInternal");E(IR,"findNodesForKeywordInternal");o(ZW,"findAssignment");E(ZW,"findAssignment");o(MR,"findNameAssignment");E(MR,"findNameAssignment");o(QW,"findNameAssignmentInternal");E(QW,"findNameAssignmentInternal");o(JW,"getActionAtElement");E(JW,"getActionAtElement");o(gke,"isOptionalCardinality");E(gke,"isOptionalCardinality");o(yke,"isArrayCardinality");E(yke,"isArrayCardinality");o(vke,"isArrayOperator");E(vke,"isArrayOperator");o(gw,"isDataTypeRule");E(gw,"isDataTypeRule");o(eq,"isDataTypeRuleInternal");E(eq,"isDataTypeRuleInternal");o(xke,"isDataType");E(xke,"isDataType");o(U6,"isDataTypeInternal");E(U6,"isDataTypeInternal");o(yw,"getExplicitRuleType");E(yw,"getExplicitRuleType");o(Xg,"getTypeName");E(Xg,"getTypeName");o(tq,"getActionType");E(tq,"getActionType");o(bke,"getRuleTypeName");E(bke,"getRuleTypeName");o(rq,"getRuleType");E(rq,"getRuleType");o(vw,"terminalRegex");E(vw,"terminalRegex");nq=/[\s\S]/.source;o(a0,"abstractElementToRegex");E(a0,"abstractElementToRegex");o(Tke,"terminalAlternativesToRegex");E(Tke,"terminalAlternativesToRegex");o(Cke,"terminalGroupToRegex");E(Cke,"terminalGroupToRegex");o(wke,"untilTokenToRegex");E(wke,"untilTokenToRegex");o(kke,"negateTokenToRegex");E(kke,"negateTokenToRegex");o(Ske,"characterRangeToRegex");E(Ske,"characterRangeToRegex");o(s6,"keywordToRegex");E(s6,"keywordToRegex");o(Bu,"withCardinality");E(Bu,"withCardinality");o(iq,"createGrammarConfig");E(iq,"createGrammarConfig");uyt=typeof global=="object"&&global&&global.Object===Object&&global,Eke=uyt,hyt=typeof self=="object"&&self&&self.Object===Object&&self,dyt=Eke||hyt||Function("return this")(),Fu=dyt,fyt=Fu.Symbol,Ll=fyt,Ake=Object.prototype,pyt=Ake.hasOwnProperty,myt=Ake.toString,uC=Ll?Ll.toStringTag:void 0;o(Rke,"getRawTag");E(Rke,"getRawTag");gyt=Rke,yyt=Object.prototype,vyt=yyt.toString;o(_ke,"objectToString");E(_ke,"objectToString");xyt=_ke,byt="[object Null]",Tyt="[object Undefined]",FTe=Ll?Ll.toStringTag:void 0;o(Lke,"baseGetTag");E(Lke,"baseGetTag");bp=Lke;o(Dke,"isObjectLike");E(Dke,"isObjectLike");Ac=Dke,Cyt="[object Symbol]";o(Ike,"isSymbol");E(Ike,"isSymbol");NR=Ike;o(Mke,"arrayMap");E(Mke,"arrayMap");xw=Mke,wyt=Array.isArray,un=wyt,kyt=1/0,zTe=Ll?Ll.prototype:void 0,GTe=zTe?zTe.toString:void 0;o(aq,"baseToString");E(aq,"baseToString");Syt=aq,Eyt=/\s/;o(Nke,"trimmedEndIndex");E(Nke,"trimmedEndIndex");Ayt=Nke,Ryt=/^\s+/;o(Pke,"baseTrim");E(Pke,"baseTrim");_yt=Pke;o(Oke,"isObject");E(Oke,"isObject");Dl=Oke,VTe=NaN,Lyt=/^[-+]0x[0-9a-f]+$/i,Dyt=/^0b[01]+$/i,Iyt=/^0o[0-7]+$/i,Myt=parseInt;o(Bke,"toNumber");E(Bke,"toNumber");Nyt=Bke,WTe=1/0,Pyt=17976931348623157e292;o($ke,"toFinite");E($ke,"toFinite");Oyt=$ke;o(Fke,"toInteger");E(Fke,"toInteger");bw=Fke;o(zke,"identity");E(zke,"identity");Tw=zke,Byt="[object AsyncFunction]",$yt="[object Function]",Fyt="[object GeneratorFunction]",zyt="[object Proxy]";o(Gke,"isFunction");E(Gke,"isFunction");nd=Gke,Gyt=Fu["__core-js_shared__"],rz=Gyt,qTe=(function(){var e=/[^.]+$/.exec(rz&&rz.keys&&rz.keys.IE_PROTO||"");return e?"Symbol(src)_1."+e:""})();o(Vke,"isMasked");E(Vke,"isMasked");Vyt=Vke,Wyt=Function.prototype,qyt=Wyt.toString;o(Wke,"toSource");E(Wke,"toSource");s0=Wke,Hyt=/[\\^$.*+?()[\]{}|]/g,Uyt=/^\[object .+?Constructor\]$/,Yyt=Function.prototype,jyt=Object.prototype,Xyt=Yyt.toString,Kyt=jyt.hasOwnProperty,Zyt=RegExp("^"+Xyt.call(Kyt).replace(Hyt,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");o(qke,"baseIsNative");E(qke,"baseIsNative");Qyt=qke;o(Hke,"getValue");E(Hke,"getValue");Jyt=Hke;o(Uke,"getNative");E(Uke,"getNative");o0=Uke,e1t=o0(Fu,"WeakMap"),EG=e1t,HTe=Object.create,t1t=(function(){function e(){}return o(e,"object"),E(e,"object"),function(t){if(!Dl(t))return{};if(HTe)return HTe(t);e.prototype=t;var r=new e;return e.prototype=void 0,r}})(),r1t=t1t;o(Yke,"apply");E(Yke,"apply");n1t=Yke;o(jke,"noop");E(jke,"noop");Da=jke;o(Xke,"copyArray");E(Xke,"copyArray");i1t=Xke,a1t=800,s1t=16,o1t=Date.now;o(Kke,"shortOut");E(Kke,"shortOut");l1t=Kke;o(Zke,"constant");E(Zke,"constant");c1t=Zke,u1t=(function(){try{var e=o0(Object,"defineProperty");return e({},"",{}),e}catch{}})(),Y6=u1t,h1t=Y6?function(e,t){return Y6(e,"toString",{configurable:!0,enumerable:!1,value:c1t(t),writable:!0})}:Tw,d1t=h1t,f1t=l1t(d1t),p1t=f1t;o(Qke,"arrayEach");E(Qke,"arrayEach");Jke=Qke;o(eSe,"baseFindIndex");E(eSe,"baseFindIndex");tSe=eSe;o(rSe,"baseIsNaN");E(rSe,"baseIsNaN");m1t=rSe;o(nSe,"strictIndexOf");E(nSe,"strictIndexOf");g1t=nSe;o(iSe,"baseIndexOf");E(iSe,"baseIndexOf");sq=iSe;o(aSe,"arrayIncludes");E(aSe,"arrayIncludes");sSe=aSe,y1t=9007199254740991,v1t=/^(?:0|[1-9]\d*)$/;o(oSe,"isIndex");E(oSe,"isIndex");PR=oSe;o(lSe,"baseAssignValue");E(lSe,"baseAssignValue");oq=lSe;o(cSe,"eq");E(cSe,"eq");Cw=cSe,x1t=Object.prototype,b1t=x1t.hasOwnProperty;o(uSe,"assignValue");E(uSe,"assignValue");OR=uSe;o(hSe,"copyObject");E(hSe,"copyObject");ww=hSe,UTe=Math.max;o(dSe,"overRest");E(dSe,"overRest");T1t=dSe;o(fSe,"baseRest");E(fSe,"baseRest");lq=fSe,C1t=9007199254740991;o(pSe,"isLength");E(pSe,"isLength");cq=pSe;o(mSe,"isArrayLike");E(mSe,"isArrayLike");zu=mSe;o(gSe,"isIterateeCall");E(gSe,"isIterateeCall");BR=gSe;o(ySe,"createAssigner");E(ySe,"createAssigner");w1t=ySe,k1t=Object.prototype;o(vSe,"isPrototype");E(vSe,"isPrototype");kw=vSe;o(xSe,"baseTimes");E(xSe,"baseTimes");S1t=xSe,E1t="[object Arguments]";o(bSe,"baseIsArguments");E(bSe,"baseIsArguments");YTe=bSe,TSe=Object.prototype,A1t=TSe.hasOwnProperty,R1t=TSe.propertyIsEnumerable,_1t=YTe((function(){return arguments})())?YTe:function(e){return Ac(e)&&A1t.call(e,"callee")&&!R1t.call(e,"callee")},$R=_1t;o(CSe,"stubFalse");E(CSe,"stubFalse");L1t=CSe,wSe=typeof exports=="object"&&exports&&!exports.nodeType&&exports,jTe=wSe&&typeof module=="object"&&module&&!module.nodeType&&module,D1t=jTe&&jTe.exports===wSe,XTe=D1t?Fu.Buffer:void 0,I1t=XTe?XTe.isBuffer:void 0,M1t=I1t||L1t,ZC=M1t,N1t="[object Arguments]",P1t="[object Array]",O1t="[object Boolean]",B1t="[object Date]",$1t="[object Error]",F1t="[object Function]",z1t="[object Map]",G1t="[object Number]",V1t="[object Object]",W1t="[object RegExp]",q1t="[object Set]",H1t="[object String]",U1t="[object WeakMap]",Y1t="[object ArrayBuffer]",j1t="[object DataView]",X1t="[object Float32Array]",K1t="[object Float64Array]",Z1t="[object Int8Array]",Q1t="[object Int16Array]",J1t="[object Int32Array]",evt="[object Uint8Array]",tvt="[object Uint8ClampedArray]",rvt="[object Uint16Array]",nvt="[object Uint32Array]",oi={};oi[X1t]=oi[K1t]=oi[Z1t]=oi[Q1t]=oi[J1t]=oi[evt]=oi[tvt]=oi[rvt]=oi[nvt]=!0;oi[N1t]=oi[P1t]=oi[Y1t]=oi[O1t]=oi[j1t]=oi[B1t]=oi[$1t]=oi[F1t]=oi[z1t]=oi[G1t]=oi[V1t]=oi[W1t]=oi[q1t]=oi[H1t]=oi[U1t]=!1;o(kSe,"baseIsTypedArray");E(kSe,"baseIsTypedArray");ivt=kSe;o(SSe,"baseUnary");E(SSe,"baseUnary");Sw=SSe,ESe=typeof exports=="object"&&exports&&!exports.nodeType&&exports,$C=ESe&&typeof module=="object"&&module&&!module.nodeType&&module,avt=$C&&$C.exports===ESe,nz=avt&&Eke.process,svt=(function(){try{var e=$C&&$C.require&&$C.require("util").types;return e||nz&&nz.binding&&nz.binding("util")}catch{}})(),pp=svt,KTe=pp&&pp.isTypedArray,ovt=KTe?Sw(KTe):ivt,uq=ovt,lvt=Object.prototype,cvt=lvt.hasOwnProperty;o(ASe,"arrayLikeKeys");E(ASe,"arrayLikeKeys");RSe=ASe;o(_Se,"overArg");E(_Se,"overArg");LSe=_Se,uvt=LSe(Object.keys,Object),hvt=uvt,dvt=Object.prototype,fvt=dvt.hasOwnProperty;o(DSe,"baseKeys");E(DSe,"baseKeys");ISe=DSe;o(MSe,"keys");E(MSe,"keys");jo=MSe,pvt=Object.prototype,mvt=pvt.hasOwnProperty,gvt=w1t(function(e,t){if(kw(t)||zu(t)){ww(t,jo(t),e);return}for(var r in t)mvt.call(t,r)&&OR(e,r,t[r])}),Xo=gvt;o(NSe,"nativeKeysIn");E(NSe,"nativeKeysIn");yvt=NSe,vvt=Object.prototype,xvt=vvt.hasOwnProperty;o(PSe,"baseKeysIn");E(PSe,"baseKeysIn");bvt=PSe;o(OSe,"keysIn");E(OSe,"keysIn");FR=OSe,Tvt=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,Cvt=/^\w*$/;o(BSe,"isKey");E(BSe,"isKey");hq=BSe,wvt=o0(Object,"create"),QC=wvt;o($Se,"hashClear");E($Se,"hashClear");kvt=$Se;o(FSe,"hashDelete");E(FSe,"hashDelete");Svt=FSe,Evt="__lodash_hash_undefined__",Avt=Object.prototype,Rvt=Avt.hasOwnProperty;o(zSe,"hashGet");E(zSe,"hashGet");_vt=zSe,Lvt=Object.prototype,Dvt=Lvt.hasOwnProperty;o(GSe,"hashHas");E(GSe,"hashHas");Ivt=GSe,Mvt="__lodash_hash_undefined__";o(VSe,"hashSet");E(VSe,"hashSet");Nvt=VSe;o(l0,"Hash");E(l0,"Hash");l0.prototype.clear=kvt;l0.prototype.delete=Svt;l0.prototype.get=_vt;l0.prototype.has=Ivt;l0.prototype.set=Nvt;ZTe=l0;o(WSe,"listCacheClear");E(WSe,"listCacheClear");Pvt=WSe;o(qSe,"assocIndexOf");E(qSe,"assocIndexOf");zR=qSe,Ovt=Array.prototype,Bvt=Ovt.splice;o(HSe,"listCacheDelete");E(HSe,"listCacheDelete");$vt=HSe;o(USe,"listCacheGet");E(USe,"listCacheGet");Fvt=USe;o(YSe,"listCacheHas");E(YSe,"listCacheHas");zvt=YSe;o(jSe,"listCacheSet");E(jSe,"listCacheSet");Gvt=jSe;o(c0,"ListCache");E(c0,"ListCache");c0.prototype.clear=Pvt;c0.prototype.delete=$vt;c0.prototype.get=Fvt;c0.prototype.has=zvt;c0.prototype.set=Gvt;GR=c0,Vvt=o0(Fu,"Map"),JC=Vvt;o(XSe,"mapCacheClear");E(XSe,"mapCacheClear");Wvt=XSe;o(KSe,"isKeyable");E(KSe,"isKeyable");qvt=KSe;o(ZSe,"getMapData");E(ZSe,"getMapData");VR=ZSe;o(QSe,"mapCacheDelete");E(QSe,"mapCacheDelete");Hvt=QSe;o(JSe,"mapCacheGet");E(JSe,"mapCacheGet");Uvt=JSe;o(eEe,"mapCacheHas");E(eEe,"mapCacheHas");Yvt=eEe;o(tEe,"mapCacheSet");E(tEe,"mapCacheSet");jvt=tEe;o(u0,"MapCache");E(u0,"MapCache");u0.prototype.clear=Wvt;u0.prototype.delete=Hvt;u0.prototype.get=Uvt;u0.prototype.has=Yvt;u0.prototype.set=jvt;WR=u0,Xvt="Expected a function";o(qR,"memoize");E(qR,"memoize");qR.Cache=WR;Kvt=qR,Zvt=500;o(rEe,"memoizeCapped");E(rEe,"memoizeCapped");Qvt=rEe,Jvt=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,ext=/\\(\\)?/g,txt=Qvt(function(e){var t=[];return e.charCodeAt(0)===46&&t.push(""),e.replace(Jvt,function(r,n,i,a){t.push(i?a.replace(ext,"$1"):n||r)}),t}),rxt=txt;o(nEe,"toString2");E(nEe,"toString");nxt=nEe;o(iEe,"castPath");E(iEe,"castPath");HR=iEe,ixt=1/0;o(aEe,"toKey");E(aEe,"toKey");Ew=aEe;o(sEe,"baseGet");E(sEe,"baseGet");dq=sEe;o(oEe,"get");E(oEe,"get");axt=oEe;o(lEe,"arrayPush");E(lEe,"arrayPush");fq=lEe,QTe=Ll?Ll.isConcatSpreadable:void 0;o(cEe,"isFlattenable");E(cEe,"isFlattenable");sxt=cEe;o(pq,"baseFlatten");E(pq,"baseFlatten");mq=pq;o(uEe,"flatten");E(uEe,"flatten");Sc=uEe,oxt=LSe(Object.getPrototypeOf,Object),hEe=oxt;o(dEe,"baseSlice");E(dEe,"baseSlice");fEe=dEe;o(pEe,"arrayReduce");E(pEe,"arrayReduce");lxt=pEe;o(mEe,"stackClear");E(mEe,"stackClear");cxt=mEe;o(gEe,"stackDelete");E(gEe,"stackDelete");uxt=gEe;o(yEe,"stackGet");E(yEe,"stackGet");hxt=yEe;o(vEe,"stackHas");E(vEe,"stackHas");dxt=vEe,fxt=200;o(xEe,"stackSet");E(xEe,"stackSet");pxt=xEe;o(h0,"Stack");E(h0,"Stack");h0.prototype.clear=cxt;h0.prototype.delete=uxt;h0.prototype.get=hxt;h0.prototype.has=dxt;h0.prototype.set=pxt;FC=h0;o(bEe,"baseAssign");E(bEe,"baseAssign");mxt=bEe;o(TEe,"baseAssignIn");E(TEe,"baseAssignIn");gxt=TEe,CEe=typeof exports=="object"&&exports&&!exports.nodeType&&exports,JTe=CEe&&typeof module=="object"&&module&&!module.nodeType&&module,yxt=JTe&&JTe.exports===CEe,eCe=yxt?Fu.Buffer:void 0,tCe=eCe?eCe.allocUnsafe:void 0;o(wEe,"cloneBuffer");E(wEe,"cloneBuffer");vxt=wEe;o(kEe,"arrayFilter");E(kEe,"arrayFilter");gq=kEe;o(SEe,"stubArray");E(SEe,"stubArray");EEe=SEe,xxt=Object.prototype,bxt=xxt.propertyIsEnumerable,rCe=Object.getOwnPropertySymbols,Txt=rCe?function(e){return e==null?[]:(e=Object(e),gq(rCe(e),function(t){return bxt.call(e,t)}))}:EEe,yq=Txt;o(AEe,"copySymbols");E(AEe,"copySymbols");Cxt=AEe,wxt=Object.getOwnPropertySymbols,kxt=wxt?function(e){for(var t=[];e;)fq(t,yq(e)),e=hEe(e);return t}:EEe,REe=kxt;o(_Ee,"copySymbolsIn");E(_Ee,"copySymbolsIn");Sxt=_Ee;o(LEe,"baseGetAllKeys");E(LEe,"baseGetAllKeys");DEe=LEe;o(IEe,"getAllKeys");E(IEe,"getAllKeys");AG=IEe;o(MEe,"getAllKeysIn");E(MEe,"getAllKeysIn");NEe=MEe,Ext=o0(Fu,"DataView"),RG=Ext,Axt=o0(Fu,"Promise"),_G=Axt,Rxt=o0(Fu,"Set"),vv=Rxt,nCe="[object Map]",_xt="[object Object]",iCe="[object Promise]",aCe="[object Set]",sCe="[object WeakMap]",oCe="[object DataView]",Lxt=s0(RG),Dxt=s0(JC),Ixt=s0(_G),Mxt=s0(vv),Nxt=s0(EG),mg=bp;(RG&&mg(new RG(new ArrayBuffer(1)))!=oCe||JC&&mg(new JC)!=nCe||_G&&mg(_G.resolve())!=iCe||vv&&mg(new vv)!=aCe||EG&&mg(new EG)!=sCe)&&(mg=E(function(e){var t=bp(e),r=t==_xt?e.constructor:void 0,n=r?s0(r):"";if(n)switch(n){case Lxt:return oCe;case Dxt:return nCe;case Ixt:return iCe;case Mxt:return aCe;case Nxt:return sCe}return t},"getTag"));Rv=mg,Pxt=Object.prototype,Oxt=Pxt.hasOwnProperty;o(PEe,"initCloneArray");E(PEe,"initCloneArray");Bxt=PEe,$xt=Fu.Uint8Array,j6=$xt;o(OEe,"cloneArrayBuffer");E(OEe,"cloneArrayBuffer");vq=OEe;o(BEe,"cloneDataView");E(BEe,"cloneDataView");Fxt=BEe,zxt=/\w*$/;o($Ee,"cloneRegExp");E($Ee,"cloneRegExp");Gxt=$Ee,lCe=Ll?Ll.prototype:void 0,cCe=lCe?lCe.valueOf:void 0;o(FEe,"cloneSymbol");E(FEe,"cloneSymbol");Vxt=FEe;o(zEe,"cloneTypedArray");E(zEe,"cloneTypedArray");Wxt=zEe,qxt="[object Boolean]",Hxt="[object Date]",Uxt="[object Map]",Yxt="[object Number]",jxt="[object RegExp]",Xxt="[object Set]",Kxt="[object String]",Zxt="[object Symbol]",Qxt="[object ArrayBuffer]",Jxt="[object DataView]",ebt="[object Float32Array]",tbt="[object Float64Array]",rbt="[object Int8Array]",nbt="[object Int16Array]",ibt="[object Int32Array]",abt="[object Uint8Array]",sbt="[object Uint8ClampedArray]",obt="[object Uint16Array]",lbt="[object Uint32Array]";o(GEe,"initCloneByTag");E(GEe,"initCloneByTag");cbt=GEe;o(VEe,"initCloneObject");E(VEe,"initCloneObject");ubt=VEe,hbt="[object Map]";o(WEe,"baseIsMap");E(WEe,"baseIsMap");dbt=WEe,uCe=pp&&pp.isMap,fbt=uCe?Sw(uCe):dbt,pbt=fbt,mbt="[object Set]";o(qEe,"baseIsSet");E(qEe,"baseIsSet");gbt=qEe,hCe=pp&&pp.isSet,ybt=hCe?Sw(hCe):gbt,vbt=ybt,xbt=1,bbt=2,Tbt=4,HEe="[object Arguments]",Cbt="[object Array]",wbt="[object Boolean]",kbt="[object Date]",Sbt="[object Error]",UEe="[object Function]",Ebt="[object GeneratorFunction]",Abt="[object Map]",Rbt="[object Number]",YEe="[object Object]",_bt="[object RegExp]",Lbt="[object Set]",Dbt="[object String]",Ibt="[object Symbol]",Mbt="[object WeakMap]",Nbt="[object ArrayBuffer]",Pbt="[object DataView]",Obt="[object Float32Array]",Bbt="[object Float64Array]",$bt="[object Int8Array]",Fbt="[object Int16Array]",zbt="[object Int32Array]",Gbt="[object Uint8Array]",Vbt="[object Uint8ClampedArray]",Wbt="[object Uint16Array]",qbt="[object Uint32Array]",Zn={};Zn[HEe]=Zn[Cbt]=Zn[Nbt]=Zn[Pbt]=Zn[wbt]=Zn[kbt]=Zn[Obt]=Zn[Bbt]=Zn[$bt]=Zn[Fbt]=Zn[zbt]=Zn[Abt]=Zn[Rbt]=Zn[YEe]=Zn[_bt]=Zn[Lbt]=Zn[Dbt]=Zn[Ibt]=Zn[Gbt]=Zn[Vbt]=Zn[Wbt]=Zn[qbt]=!0;Zn[Sbt]=Zn[UEe]=Zn[Mbt]=!1;o(zC,"baseClone");E(zC,"baseClone");Hbt=zC,Ubt=4;o(jEe,"clone");E(jEe,"clone");Ya=jEe;o(XEe,"compact");E(XEe,"compact");Aw=XEe,Ybt="__lodash_hash_undefined__";o(KEe,"setCacheAdd");E(KEe,"setCacheAdd");jbt=KEe;o(ZEe,"setCacheHas");E(ZEe,"setCacheHas");Xbt=ZEe;o(ew,"SetCache");E(ew,"SetCache");ew.prototype.add=ew.prototype.push=jbt;ew.prototype.has=Xbt;xq=ew;o(QEe,"arraySome");E(QEe,"arraySome");JEe=QEe;o(e4e,"cacheHas");E(e4e,"cacheHas");bq=e4e,Kbt=1,Zbt=2;o(t4e,"equalArrays");E(t4e,"equalArrays");r4e=t4e;o(n4e,"mapToArray");E(n4e,"mapToArray");Qbt=n4e;o(i4e,"setToArray");E(i4e,"setToArray");Tq=i4e,Jbt=1,e2t=2,t2t="[object Boolean]",r2t="[object Date]",n2t="[object Error]",i2t="[object Map]",a2t="[object Number]",s2t="[object RegExp]",o2t="[object Set]",l2t="[object String]",c2t="[object Symbol]",u2t="[object ArrayBuffer]",h2t="[object DataView]",dCe=Ll?Ll.prototype:void 0,iz=dCe?dCe.valueOf:void 0;o(a4e,"equalByTag");E(a4e,"equalByTag");d2t=a4e,f2t=1,p2t=Object.prototype,m2t=p2t.hasOwnProperty;o(s4e,"equalObjects");E(s4e,"equalObjects");g2t=s4e,y2t=1,fCe="[object Arguments]",pCe="[object Array]",NA="[object Object]",v2t=Object.prototype,mCe=v2t.hasOwnProperty;o(o4e,"baseIsEqualDeep");E(o4e,"baseIsEqualDeep");x2t=o4e;o(Cq,"baseIsEqual");E(Cq,"baseIsEqual");l4e=Cq,b2t=1,T2t=2;o(c4e,"baseIsMatch");E(c4e,"baseIsMatch");C2t=c4e;o(u4e,"isStrictComparable");E(u4e,"isStrictComparable");h4e=u4e;o(d4e,"getMatchData");E(d4e,"getMatchData");w2t=d4e;o(f4e,"matchesStrictComparable");E(f4e,"matchesStrictComparable");p4e=f4e;o(m4e,"baseMatches");E(m4e,"baseMatches");k2t=m4e;o(g4e,"baseHasIn");E(g4e,"baseHasIn");S2t=g4e;o(y4e,"hasPath");E(y4e,"hasPath");v4e=y4e;o(x4e,"hasIn");E(x4e,"hasIn");E2t=x4e,A2t=1,R2t=2;o(b4e,"baseMatchesProperty");E(b4e,"baseMatchesProperty");_2t=b4e;o(T4e,"baseProperty");E(T4e,"baseProperty");L2t=T4e;o(C4e,"basePropertyDeep");E(C4e,"basePropertyDeep");D2t=C4e;o(w4e,"property");E(w4e,"property");I2t=w4e;o(k4e,"baseIteratee");E(k4e,"baseIteratee");Gu=k4e;o(S4e,"arrayAggregator");E(S4e,"arrayAggregator");M2t=S4e;o(E4e,"createBaseFor");E(E4e,"createBaseFor");N2t=E4e,P2t=N2t(),O2t=P2t;o(A4e,"baseForOwn");E(A4e,"baseForOwn");B2t=A4e;o(R4e,"createBaseEach");E(R4e,"createBaseEach");$2t=R4e,F2t=$2t(B2t),d0=F2t;o(_4e,"baseAggregator");E(_4e,"baseAggregator");z2t=_4e;o(L4e,"createAggregator");E(L4e,"createAggregator");G2t=L4e,D4e=Object.prototype,V2t=D4e.hasOwnProperty,W2t=lq(function(e,t){e=Object(e);var r=-1,n=t.length,i=n>2?t[2]:void 0;for(i&&BR(t[0],t[1],i)&&(n=1);++r{t.accept(e)})}},Gs=class extends Vu{static{o(this,"NonTerminal")}static{E(this,"NonTerminal")}constructor(e){super([]),this.idx=1,Xo(this,_c(e,t=>t!==void 0))}set definition(e){}get definition(){return this.referencedRule!==void 0?this.referencedRule.definition:[]}accept(e){e.visit(this)}},Pv=class extends Vu{static{o(this,"Rule")}static{E(this,"Rule")}constructor(e){super(e.definition),this.orgText="",Xo(this,_c(e,t=>t!==void 0))}},Co=class extends Vu{static{o(this,"Alternative")}static{E(this,"Alternative")}constructor(e){super(e.definition),this.ignoreAmbiguities=!1,Xo(this,_c(e,t=>t!==void 0))}},Ua=class extends Vu{static{o(this,"Option")}static{E(this,"Option")}constructor(e){super(e.definition),this.idx=1,Xo(this,_c(e,t=>t!==void 0))}},Zo=class extends Vu{static{o(this,"RepetitionMandatory")}static{E(this,"RepetitionMandatory")}constructor(e){super(e.definition),this.idx=1,Xo(this,_c(e,t=>t!==void 0))}},Qo=class extends Vu{static{o(this,"RepetitionMandatoryWithSeparator")}static{E(this,"RepetitionMandatoryWithSeparator")}constructor(e){super(e.definition),this.idx=1,Xo(this,_c(e,t=>t!==void 0))}},Mi=class extends Vu{static{o(this,"Repetition")}static{E(this,"Repetition")}constructor(e){super(e.definition),this.idx=1,Xo(this,_c(e,t=>t!==void 0))}},wo=class extends Vu{static{o(this,"RepetitionWithSeparator")}static{E(this,"RepetitionWithSeparator")}constructor(e){super(e.definition),this.idx=1,Xo(this,_c(e,t=>t!==void 0))}},ko=class extends Vu{static{o(this,"Alternation")}static{E(this,"Alternation")}get definition(){return this._definition}set definition(e){this._definition=e}constructor(e){super(e.definition),this.idx=1,this.ignoreAmbiguities=!1,this.hasPredicates=!1,Xo(this,_c(e,t=>t!==void 0))}},ci=class{static{o(this,"Terminal")}static{E(this,"Terminal")}constructor(e){this.idx=1,Xo(this,_c(e,t=>t!==void 0))}accept(e){e.visit(this)}};o(w3e,"serializeGrammar");E(w3e,"serializeGrammar");o(GC,"serializeProduction");E(GC,"serializeProduction");Ov=class{static{o(this,"GAstVisitor")}static{E(this,"GAstVisitor")}visit(e){let t=e;switch(t.constructor){case Gs:return this.visitNonTerminal(t);case Co:return this.visitAlternative(t);case Ua:return this.visitOption(t);case Zo:return this.visitRepetitionMandatory(t);case Qo:return this.visitRepetitionMandatoryWithSeparator(t);case wo:return this.visitRepetitionWithSeparator(t);case Mi:return this.visitRepetition(t);case ko:return this.visitAlternation(t);case ci:return this.visitTerminal(t);case Pv:return this.visitRule(t);default:throw Error("non exhaustive match")}}visitNonTerminal(e){}visitAlternative(e){}visitOption(e){}visitRepetition(e){}visitRepetitionMandatory(e){}visitRepetitionMandatoryWithSeparator(e){}visitRepetitionWithSeparator(e){}visitAlternation(e){}visitTerminal(e){}visitRule(e){}};o(k3e,"isSequenceProd");E(k3e,"isSequenceProd");o(rw,"isOptionalProd");E(rw,"isOptionalProd");o(S3e,"isBranchingProd");E(S3e,"isBranchingProd");o(Tc,"getProductionDslName");E(Tc,"getProductionDslName");jR=class{static{o(this,"RestWalker")}static{E(this,"RestWalker")}walk(e,t=[]){Ir(e.definition,(r,n)=>{let i=Ha(e.definition,n+1);if(r instanceof Gs)this.walkProdRef(r,i,t);else if(r instanceof ci)this.walkTerminal(r,i,t);else if(r instanceof Co)this.walkFlat(r,i,t);else if(r instanceof Ua)this.walkOption(r,i,t);else if(r instanceof Zo)this.walkAtLeastOne(r,i,t);else if(r instanceof Qo)this.walkAtLeastOneSep(r,i,t);else if(r instanceof wo)this.walkManySep(r,i,t);else if(r instanceof Mi)this.walkMany(r,i,t);else if(r instanceof ko)this.walkOr(r,i,t);else throw Error("non exhaustive match")})}walkTerminal(e,t,r){}walkProdRef(e,t,r){}walkFlat(e,t,r){let n=t.concat(r);this.walk(e,n)}walkOption(e,t,r){let n=t.concat(r);this.walk(e,n)}walkAtLeastOne(e,t,r){let n=[new Ua({definition:e.definition})].concat(t,r);this.walk(e,n)}walkAtLeastOneSep(e,t,r){let n=LG(e,t,r);this.walk(e,n)}walkMany(e,t,r){let n=[new Ua({definition:e.definition})].concat(t,r);this.walk(e,n)}walkManySep(e,t,r){let n=LG(e,t,r);this.walk(e,n)}walkOr(e,t,r){let n=t.concat(r);Ir(e.definition,i=>{let a=new Co({definition:[i]});this.walk(a,n)})}};o(LG,"restForRepetitionWithSeparator");E(LG,"restForRepetitionWithSeparator");o(Bv,"first");E(Bv,"first");o(E3e,"firstForSequence");E(E3e,"firstForSequence");o(A3e,"firstForBranching");E(A3e,"firstForBranching");o(R3e,"firstForTerminal");E(R3e,"firstForTerminal");_3e="_~IN~_",LTt=class extends jR{static{o(this,"ResyncFollowsWalker")}static{E(this,"ResyncFollowsWalker")}constructor(e){super(),this.topProd=e,this.follows={}}startWalking(){return this.walk(this.topProd),this.follows}walkTerminal(e,t,r){}walkProdRef(e,t,r){let n=D3e(e.referencedRule,e.idx)+this.topProd.name,i=t.concat(r),a=new Co({definition:i}),s=Bv(a);this.follows[n]=s}};o(L3e,"computeAllProdsFollows");E(L3e,"computeAllProdsFollows");o(D3e,"buildBetweenProdsFollowPrefix");E(D3e,"buildBetweenProdsFollowPrefix");o6={},DTt=new cke;o(Rw,"getRegExpAst");E(Rw,"getRegExpAst");o(I3e,"clearRegExpParserCache");E(I3e,"clearRegExpParserCache");M3e="Complement Sets are not supported for first char optimization",K6=`Unable to use "first char" lexer optimizations: +`;o(N3e,"getOptimizedStartCodesIndices");E(N3e,"getOptimizedStartCodesIndices");o(Z6,"firstCharOptimizedIndices");E(Z6,"firstCharOptimizedIndices");o(EC,"addOptimizedIdxToResult");E(EC,"addOptimizedIdxToResult");o(P3e,"handleIgnoreCase");E(P3e,"handleIgnoreCase");o(DG,"findCode");E(DG,"findCode");o(Q6,"isWholeOptional");E(Q6,"isWholeOptional");ITt=class extends AR{static{o(this,"CharCodeFinder")}static{E(this,"CharCodeFinder")}constructor(e){super(),this.targetCharCodes=e,this.found=!1}visitChildren(e){if(this.found!==!0){switch(e.type){case"Lookahead":this.visitLookahead(e);return;case"NegativeLookahead":this.visitNegativeLookahead(e);return;case"Lookbehind":this.visitLookbehind(e);return;case"NegativeLookbehind":this.visitNegativeLookbehind(e);return}super.visitChildren(e)}}visitCharacter(e){qs(this.targetCharCodes,e.value)&&(this.found=!0)}visitSet(e){e.complement?DG(e,this.targetCharCodes)===void 0&&(this.found=!0):DG(e,this.targetCharCodes)!==void 0&&(this.found=!0)}};o(XR,"canMatchCharCode");E(XR,"canMatchCharCode");Zg="PATTERN",AC="defaultMode",PA="modes";o(O3e,"analyzeTokenTypes");E(O3e,"analyzeTokenTypes");o(B3e,"validatePatterns");E(B3e,"validatePatterns");o($3e,"validateRegExpPattern");E($3e,"validateRegExpPattern");o(F3e,"findMissingPatterns");E(F3e,"findMissingPatterns");o(z3e,"findInvalidPatterns");E(z3e,"findInvalidPatterns");MTt=/[^\\][$]/;o(G3e,"findEndOfInputAnchor");E(G3e,"findEndOfInputAnchor");o(V3e,"findEmptyMatchRegExps");E(V3e,"findEmptyMatchRegExps");NTt=/[^\\[][\^]|^\^/;o(W3e,"findStartOfInputAnchor");E(W3e,"findStartOfInputAnchor");o(q3e,"findUnsupportedFlags");E(q3e,"findUnsupportedFlags");o(H3e,"findDuplicatePatterns");E(H3e,"findDuplicatePatterns");o(U3e,"findInvalidGroupType");E(U3e,"findInvalidGroupType");o(Y3e,"findModesThatDoNotExist");E(Y3e,"findModesThatDoNotExist");o(j3e,"findUnreachablePatterns");E(j3e,"findUnreachablePatterns");o(X3e,"tryToMatchStrToPattern");E(X3e,"tryToMatchStrToPattern");o(K3e,"noMetaChar");E(K3e,"noMetaChar");o(Z3e,"usesLookAheadOrBehind");E(Z3e,"usesLookAheadOrBehind");o(IG,"addStickyFlag");E(IG,"addStickyFlag");o(Q3e,"performRuntimeChecks");E(Q3e,"performRuntimeChecks");o(J3e,"performWarningRuntimeChecks");E(J3e,"performWarningRuntimeChecks");o(e5e,"cloneEmptyGroups");E(e5e,"cloneEmptyGroups");o(Rq,"isCustomPattern");E(Rq,"isCustomPattern");o(t5e,"isShortPattern");E(t5e,"isShortPattern");PTt={test:E(function(e){let t=e.length;for(let r=this.lastIndex;r${e.image}<- The Mode Stack is empty`},buildUnexpectedCharactersMessage(e,t,r,n,i,a){return`unexpected character: ->${e.charAt(t)}<- at offset: ${t}, skipped ${r} characters.`}};(function(e){e[e.MISSING_PATTERN=0]="MISSING_PATTERN",e[e.INVALID_PATTERN=1]="INVALID_PATTERN",e[e.EOI_ANCHOR_FOUND=2]="EOI_ANCHOR_FOUND",e[e.UNSUPPORTED_FLAGS_FOUND=3]="UNSUPPORTED_FLAGS_FOUND",e[e.DUPLICATE_PATTERNS_FOUND=4]="DUPLICATE_PATTERNS_FOUND",e[e.INVALID_GROUP_TYPE_FOUND=5]="INVALID_GROUP_TYPE_FOUND",e[e.PUSH_MODE_DOES_NOT_EXIST=6]="PUSH_MODE_DOES_NOT_EXIST",e[e.MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE=7]="MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE",e[e.MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY=8]="MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY",e[e.MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST=9]="MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST",e[e.LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED=10]="LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED",e[e.SOI_ANCHOR_FOUND=11]="SOI_ANCHOR_FOUND",e[e.EMPTY_MATCH_PATTERN=12]="EMPTY_MATCH_PATTERN",e[e.NO_LINE_BREAKS_FLAGS=13]="NO_LINE_BREAKS_FLAGS",e[e.UNREACHABLE_PATTERN=14]="UNREACHABLE_PATTERN",e[e.IDENTIFY_TERMINATOR=15]="IDENTIFY_TERMINATOR",e[e.CUSTOM_LINE_BREAK=16]="CUSTOM_LINE_BREAK",e[e.MULTI_MODE_LEXER_LONGER_ALT_NOT_IN_CURRENT_MODE=17]="MULTI_MODE_LEXER_LONGER_ALT_NOT_IN_CURRENT_MODE"})(Ni||(Ni={}));_C={deferDefinitionErrorsHandling:!1,positionTracking:"full",lineTerminatorsPattern:/\n|\r\n?/g,lineTerminatorCharacters:[` +`,"\r"],ensureOptimizations:!1,safeMode:!1,errorMessageProvider:NG,traceInitPerf:!1,skipValidations:!1,recoveryEnabled:!0};Object.freeze(_C);Fs=class{static{o(this,"Lexer")}static{E(this,"Lexer")}constructor(e,t=_C){if(this.lexerDefinition=e,this.lexerDefinitionErrors=[],this.lexerDefinitionWarning=[],this.patternIdxToConfig={},this.charCodeToPatternIdxToConfig={},this.modes=[],this.emptyGroups={},this.trackStartLines=!0,this.trackEndLines=!0,this.hasCustom=!1,this.canModeBeOptimized={},this.TRACE_INIT=(n,i)=>{if(this.traceInitPerf===!0){this.traceInitIndent++;let a=new Array(this.traceInitIndent+1).join(" ");this.traceInitIndent <${n}>`);let{time:s,value:l}=Eq(i),u=s>10?console.warn:console.log;return this.traceInitIndent time: ${s}ms`),this.traceInitIndent--,l}else return i()},typeof t=="boolean")throw Error(`The second argument to the Lexer constructor is now an ILexerConfig Object. +a boolean 2nd argument is no longer supported`);this.config=Xo({},_C,t);let r=this.config.traceInitPerf;r===!0?(this.traceInitMaxIdent=1/0,this.traceInitPerf=!0):typeof r=="number"&&(this.traceInitMaxIdent=r,this.traceInitPerf=!0),this.traceInitIndent=-1,this.TRACE_INIT("Lexer Constructor",()=>{let n,i=!0;this.TRACE_INIT("Lexer Config handling",()=>{if(this.config.lineTerminatorsPattern===_C.lineTerminatorsPattern)this.config.lineTerminatorsPattern=PTt;else if(this.config.lineTerminatorCharacters===_C.lineTerminatorCharacters)throw Error(`Error: Missing property on the Lexer config. + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#MISSING_LINE_TERM_CHARS`);if(t.safeMode&&t.ensureOptimizations)throw Error('"safeMode" and "ensureOptimizations" flags are mutually exclusive.');this.trackStartLines=/full|onlyStart/i.test(this.config.positionTracking),this.trackEndLines=/full/i.test(this.config.positionTracking),un(e)?n={modes:{defaultMode:Ya(e)},defaultMode:AC}:(i=!1,n=Ya(e))}),this.config.skipValidations===!1&&(this.TRACE_INIT("performRuntimeChecks",()=>{this.lexerDefinitionErrors=this.lexerDefinitionErrors.concat(Q3e(n,this.trackStartLines,this.config.lineTerminatorCharacters))}),this.TRACE_INIT("performWarningRuntimeChecks",()=>{this.lexerDefinitionWarning=this.lexerDefinitionWarning.concat(J3e(n,this.trackStartLines,this.config.lineTerminatorCharacters))})),n.modes=n.modes?n.modes:{},Ir(n.modes,(s,l)=>{n.modes[l]=YR(s,u=>Zh(u))});let a=jo(n.modes);if(Ir(n.modes,(s,l)=>{this.TRACE_INIT(`Mode: <${l}> processing`,()=>{if(this.modes.push(l),this.config.skipValidations===!1&&this.TRACE_INIT("validatePatterns",()=>{this.lexerDefinitionErrors=this.lexerDefinitionErrors.concat(B3e(s,a))}),Jn(this.lexerDefinitionErrors)){Fv(s);let u;this.TRACE_INIT("analyzeTokenTypes",()=>{u=O3e(s,{lineTerminatorCharacters:this.config.lineTerminatorCharacters,positionTracking:t.positionTracking,ensureOptimizations:t.ensureOptimizations,safeMode:t.safeMode,tracer:this.TRACE_INIT})}),this.patternIdxToConfig[l]=u.patternIdxToConfig,this.charCodeToPatternIdxToConfig[l]=u.charCodeToPatternIdxToConfig,this.emptyGroups=Xo({},this.emptyGroups,u.emptyGroups),this.hasCustom=u.hasCustom||this.hasCustom,this.canModeBeOptimized[l]=u.canBeOptimized}})}),this.defaultMode=n.defaultMode,!Jn(this.lexerDefinitionErrors)&&!this.config.deferDefinitionErrorsHandling){let l=hr(this.lexerDefinitionErrors,u=>u.message).join(`----------------------- +`);throw new Error(`Errors detected in definition of Lexer: +`+l)}Ir(this.lexerDefinitionWarning,s=>{Sq(s.message)}),this.TRACE_INIT("Choosing sub-methods implementations",()=>{if(i&&(this.handleModes=Da),this.trackStartLines===!1&&(this.computeNewColumn=Tw),this.trackEndLines===!1&&(this.updateTokenEndLineColumnLocation=Da),/full/i.test(this.config.positionTracking))this.createTokenInstance=this.createFullToken;else if(/onlyStart/i.test(this.config.positionTracking))this.createTokenInstance=this.createStartOnlyToken;else if(/onlyOffset/i.test(this.config.positionTracking))this.createTokenInstance=this.createOffsetOnlyToken;else throw Error(`Invalid config option: "${this.config.positionTracking}"`);this.hasCustom?(this.addToken=this.addTokenUsingPush,this.handlePayload=this.handlePayloadWithCustom):(this.addToken=this.addTokenUsingMemberAccess,this.handlePayload=this.handlePayloadNoCustom)}),this.TRACE_INIT("Failed Optimization Warnings",()=>{let s=Ko(this.canModeBeOptimized,(l,u,h)=>(u===!1&&l.push(h),l),[]);if(t.ensureOptimizations&&!Jn(s))throw Error(`Lexer Modes: < ${s.join(", ")} > cannot be optimized. + Disable the "ensureOptimizations" lexer config flag to silently ignore this and run the lexer in an un-optimized mode. + Or inspect the console log for details on how to resolve these issues.`)}),this.TRACE_INIT("clearRegExpParserCache",()=>{I3e()}),this.TRACE_INIT("toFastProperties",()=>{Aq(this)})})}tokenize(e,t=this.defaultMode){if(!Jn(this.lexerDefinitionErrors)){let n=hr(this.lexerDefinitionErrors,i=>i.message).join(`----------------------- +`);throw new Error(`Unable to Tokenize because Errors detected in definition of Lexer: +`+n)}return this.tokenizeInternal(e,t)}tokenizeInternal(e,t){let r,n,i,a,s,l,u,h,d,f,p,m,g,y,v,x=e,b=x.length,T=0,k=0,C=this.hasCustom?0:Math.floor(e.length/10),w=new Array(C),S=[],R=this.trackStartLines?1:void 0,L=this.trackStartLines?1:void 0,N=e5e(this.emptyGroups),I=this.trackStartLines,_=this.config.lineTerminatorsPattern,A=0,M=[],D=[],P=[],B=[];Object.freeze(B);let O=!1,$=E(W=>{if(P.length===1&&W.tokenType.PUSH_MODE===void 0){let H=this.config.errorMessageProvider.buildUnableToPopLexerModeMessage(W);S.push({offset:W.startOffset,line:W.startLine,column:W.startColumn,length:W.image.length,message:H})}else{P.pop();let H=Kg(P);M=this.patternIdxToConfig[H],D=this.charCodeToPatternIdxToConfig[H],A=M.length;let j=this.canModeBeOptimized[H]&&this.config.safeMode===!1;D&&j?O=!0:O=!1}},"pop_mode");function V(W){P.push(W),D=this.charCodeToPatternIdxToConfig[W],M=this.patternIdxToConfig[W],A=M.length,A=M.length;let H=this.canModeBeOptimized[W]&&this.config.safeMode===!1;D&&H?O=!0:O=!1}o(V,"push_mode"),E(V,"push_mode"),V.call(this,t);let G,z=this.config.recoveryEnabled;for(;Tl.length){l=a,d=a.length,u=h,G=te;break}}}break}}if(d!==-1){if(f=G.group,f!==void 0&&(l=l!==null?l:e.substring(T,T+d),p=G.tokenTypeIdx,m=this.createTokenInstance(l,T,p,G.tokenType,R,L,d),this.handlePayload(m,u),f===!1?k=this.addToken(w,k,m):N[f].push(m)),I===!0&&G.canLineTerminator===!0){let Q=0,U,oe;_.lastIndex=0;do l=l!==null?l:e.substring(T,T+d),U=_.test(l),U===!0&&(oe=_.lastIndex-1,Q++);while(U===!0);Q!==0?(R=R+Q,L=d-oe,this.updateTokenEndLineColumnLocation(m,f,oe,Q,R,L,d)):L=this.computeNewColumn(L,d)}else L=this.computeNewColumn(L,d);T=T+d,this.handleModes(G,$,V,m)}else{let Q=T,U=R,oe=L,te=z===!1;for(;te===!1&&T ${Yg(e)} <--`:`token of type --> ${e.name} <--`} but found --> '${t.image}' <--`},buildNotAllInputParsedMessage({firstRedundant:e,ruleName:t}){return"Redundant input, expecting EOF but found: "+e.image},buildNoViableAltMessage({expectedPathsPerAlt:e,actual:t,previous:r,customUserDescription:n,ruleName:i}){let a="Expecting: ",l=` +but found: '`+Rc(t).image+"'";if(n)return a+n+l;{let u=Ko(e,(p,m)=>p.concat(m),[]),h=hr(u,p=>`[${hr(p,m=>Yg(m)).join(", ")}]`),f=`one of these possible Token sequences: +${hr(h,(p,m)=>` ${m+1}. ${p}`).join(` +`)}`;return a+f+l}},buildEarlyExitMessage({expectedIterationPaths:e,actual:t,customUserDescription:r,ruleName:n}){let i="Expecting: ",s=` +but found: '`+Rc(t).image+"'";if(r)return i+r+s;{let u=`expecting at least one iteration which starts with one of these possible Token sequences:: + <${hr(e,h=>`[${hr(h,d=>Yg(d)).join(",")}]`).join(" ,")}>`;return i+u+s}}};Object.freeze(pv);BTt={buildRuleNotFoundError(e,t){return"Invalid grammar, reference to a rule which is not defined: ->"+t.nonTerminalName+`<- +inside top level rule: ->`+e.name+"<-"}},qg={buildDuplicateFoundError(e,t){function r(d){return d instanceof ci?d.terminalType.name:d instanceof Gs?d.nonTerminalName:""}o(r,"getExtraProductionArgument2"),E(r,"getExtraProductionArgument");let n=e.name,i=Rc(t),a=i.idx,s=Tc(i),l=r(i),u=a>0,h=`->${s}${u?a:""}<- ${l?`with argument: ->${l}<-`:""} + appears more than once (${t.length} times) in the top level rule: ->${n}<-. + For further details see: https://chevrotain.io/docs/FAQ.html#NUMERICAL_SUFFIXES + `;return h=h.replace(/[ \t]+/g," "),h=h.replace(/\s\s+/g,` +`),h},buildNamespaceConflictError(e){return`Namespace conflict found in grammar. +The grammar has both a Terminal(Token) and a Non-Terminal(Rule) named: <${e.name}>. +To resolve this make sure each Terminal and Non-Terminal names are unique +This is easy to accomplish by using the convention that Terminal names start with an uppercase letter +and Non-Terminal names start with a lower case letter.`},buildAlternationPrefixAmbiguityError(e){let t=hr(e.prefixPath,i=>Yg(i)).join(", "),r=e.alternation.idx===0?"":e.alternation.idx;return`Ambiguous alternatives: <${e.ambiguityIndices.join(" ,")}> due to common lookahead prefix +in inside <${e.topLevelRule.name}> Rule, +<${t}> may appears as a prefix path in all these alternatives. +See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#COMMON_PREFIX +For Further details.`},buildAlternationAmbiguityError(e){let t=e.alternation.idx===0?"":e.alternation.idx,r=e.prefixPath.length===0,n=`Ambiguous Alternatives Detected: <${e.ambiguityIndices.join(" ,")}> in inside <${e.topLevelRule.name}> Rule, +`;if(r)n+=`These alternatives are all empty (match no tokens), making them indistinguishable. +Only the last alternative may be empty. +`;else{let i=hr(e.prefixPath,a=>Yg(a)).join(", ");n+=`<${i}> may appears as a prefix path in all these alternatives. +`}return n+=`See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#AMBIGUOUS_ALTERNATIVES +For Further details.`,n},buildEmptyRepetitionError(e){let t=Tc(e.repetition);return e.repetition.idx!==0&&(t+=e.repetition.idx),`The repetition <${t}> within Rule <${e.topLevelRule.name}> can never consume any tokens. +This could lead to an infinite loop.`},buildTokenNameError(e){return"deprecated"},buildEmptyAlternationError(e){return`Ambiguous empty alternative: <${e.emptyChoiceIdx+1}> in inside <${e.topLevelRule.name}> Rule. +Only the last alternative may be an empty alternative.`},buildTooManyAlternativesError(e){return`An Alternation cannot have more than 256 alternatives: + inside <${e.topLevelRule.name}> Rule. + has ${e.alternation.definition.length+1} alternatives.`},buildLeftRecursionError(e){let t=e.topLevelRule.name,r=hr(e.leftRecursionPath,a=>a.name),n=`${t} --> ${r.concat([t]).join(" --> ")}`;return`Left Recursion found in grammar. +rule: <${t}> can be invoked from itself (directly or indirectly) +without consuming any Tokens. The grammar path that causes this is: + ${n} + To fix this refactor your grammar to remove the left recursion. +see: https://en.wikipedia.org/wiki/LL_parser#Left_factoring.`},buildInvalidRuleNameError(e){return"deprecated"},buildDuplicateRuleNameError(e){let t;return e.topLevelRule instanceof Pv?t=e.topLevelRule.name:t=e.topLevelRule,`Duplicate definition, rule: ->${t}<- is already defined in the grammar: ->${e.grammarName}<-`}};o(f5e,"resolveGrammar");E(f5e,"resolveGrammar");$Tt=class extends Ov{static{o(this,"GastRefResolverVisitor")}static{E(this,"GastRefResolverVisitor")}constructor(e,t){super(),this.nameToTopRule=e,this.errMsgProvider=t,this.errors=[]}resolveRefs(){Ir(ha(this.nameToTopRule),e=>{this.currTopLevel=e,e.accept(this)})}visitNonTerminal(e){let t=this.nameToTopRule[e.nonTerminalName];if(t)e.referencedRule=t;else{let r=this.errMsgProvider.buildRuleNotFoundError(this.currTopLevel,e);this.errors.push({message:r,type:Vs.UNRESOLVED_SUBRULE_REF,ruleName:this.currTopLevel.name,unresolvedRefName:e.nonTerminalName})}}},FTt=class extends jR{static{o(this,"AbstractNextPossibleTokensWalker")}static{E(this,"AbstractNextPossibleTokensWalker")}constructor(e,t){super(),this.topProd=e,this.path=t,this.possibleTokTypes=[],this.nextProductionName="",this.nextProductionOccurrence=0,this.found=!1,this.isAtEndOfPath=!1}startWalking(){if(this.found=!1,this.path.ruleStack[0]!==this.topProd.name)throw Error("The path does not start with the walker's top Rule!");return this.ruleStack=Ya(this.path.ruleStack).reverse(),this.occurrenceStack=Ya(this.path.occurrenceStack).reverse(),this.ruleStack.pop(),this.occurrenceStack.pop(),this.updateExpectedNext(),this.walk(this.topProd),this.possibleTokTypes}walk(e,t=[]){this.found||super.walk(e,t)}walkProdRef(e,t,r){if(e.referencedRule.name===this.nextProductionName&&e.idx===this.nextProductionOccurrence){let n=t.concat(r);this.updateExpectedNext(),this.walk(e.referencedRule,n)}}updateExpectedNext(){Jn(this.ruleStack)?(this.nextProductionName="",this.nextProductionOccurrence=0,this.isAtEndOfPath=!0):(this.nextProductionName=this.ruleStack.pop(),this.nextProductionOccurrence=this.occurrenceStack.pop())}},zTt=class extends FTt{static{o(this,"NextAfterTokenWalker")}static{E(this,"NextAfterTokenWalker")}constructor(e,t){super(e,t),this.path=t,this.nextTerminalName="",this.nextTerminalOccurrence=0,this.nextTerminalName=this.path.lastTok.name,this.nextTerminalOccurrence=this.path.lastTokOccurrence}walkTerminal(e,t,r){if(this.isAtEndOfPath&&e.terminalType.name===this.nextTerminalName&&e.idx===this.nextTerminalOccurrence&&!this.found){let n=t.concat(r),i=new Co({definition:n});this.possibleTokTypes=Bv(i),this.found=!0}}},KR=class extends jR{static{o(this,"AbstractNextTerminalAfterProductionWalker")}static{E(this,"AbstractNextTerminalAfterProductionWalker")}constructor(e,t){super(),this.topRule=e,this.occurrence=t,this.result={token:void 0,occurrence:void 0,isEndOfRule:void 0}}startWalking(){return this.walk(this.topRule),this.result}},GTt=class extends KR{static{o(this,"NextTerminalAfterManyWalker")}static{E(this,"NextTerminalAfterManyWalker")}walkMany(e,t,r){if(e.idx===this.occurrence){let n=Rc(t.concat(r));this.result.isEndOfRule=n===void 0,n instanceof ci&&(this.result.token=n.terminalType,this.result.occurrence=n.idx)}else super.walkMany(e,t,r)}},RCe=class extends KR{static{o(this,"NextTerminalAfterManySepWalker")}static{E(this,"NextTerminalAfterManySepWalker")}walkManySep(e,t,r){if(e.idx===this.occurrence){let n=Rc(t.concat(r));this.result.isEndOfRule=n===void 0,n instanceof ci&&(this.result.token=n.terminalType,this.result.occurrence=n.idx)}else super.walkManySep(e,t,r)}},VTt=class extends KR{static{o(this,"NextTerminalAfterAtLeastOneWalker")}static{E(this,"NextTerminalAfterAtLeastOneWalker")}walkAtLeastOne(e,t,r){if(e.idx===this.occurrence){let n=Rc(t.concat(r));this.result.isEndOfRule=n===void 0,n instanceof ci&&(this.result.token=n.terminalType,this.result.occurrence=n.idx)}else super.walkAtLeastOne(e,t,r)}},_Ce=class extends KR{static{o(this,"NextTerminalAfterAtLeastOneSepWalker")}static{E(this,"NextTerminalAfterAtLeastOneSepWalker")}walkAtLeastOneSep(e,t,r){if(e.idx===this.occurrence){let n=Rc(t.concat(r));this.result.isEndOfRule=n===void 0,n instanceof ci&&(this.result.token=n.terminalType,this.result.occurrence=n.idx)}else super.walkAtLeastOneSep(e,t,r)}};o(J6,"possiblePathsFrom");E(J6,"possiblePathsFrom");o(Pq,"nextPossibleTokensAfter");E(Pq,"nextPossibleTokensAfter");o(p5e,"expandTopLevelRule");E(p5e,"expandTopLevelRule");(function(e){e[e.OPTION=0]="OPTION",e[e.REPETITION=1]="REPETITION",e[e.REPETITION_MANDATORY=2]="REPETITION_MANDATORY",e[e.REPETITION_MANDATORY_WITH_SEPARATOR=3]="REPETITION_MANDATORY_WITH_SEPARATOR",e[e.REPETITION_WITH_SEPARATOR=4]="REPETITION_WITH_SEPARATOR",e[e.ALTERNATION=5]="ALTERNATION"})(wi||(wi={}));o(ZR,"getProdType");E(ZR,"getProdType");o(PG,"getLookaheadPaths");E(PG,"getLookaheadPaths");o(m5e,"buildLookaheadFuncForOr");E(m5e,"buildLookaheadFuncForOr");o(g5e,"buildLookaheadFuncForOptionalProd");E(g5e,"buildLookaheadFuncForOptionalProd");o(y5e,"buildAlternativesLookAheadFunc");E(y5e,"buildAlternativesLookAheadFunc");o(v5e,"buildSingleAlternativeLookaheadFunction");E(v5e,"buildSingleAlternativeLookaheadFunction");WTt=class extends jR{static{o(this,"RestDefinitionFinderWalker")}static{E(this,"RestDefinitionFinderWalker")}constructor(e,t,r){super(),this.topProd=e,this.targetOccurrence=t,this.targetProdType=r}startWalking(){return this.walk(this.topProd),this.restDef}checkIsTarget(e,t,r,n){return e.idx===this.targetOccurrence&&this.targetProdType===t?(this.restDef=r.concat(n),!0):!1}walkOption(e,t,r){this.checkIsTarget(e,wi.OPTION,t,r)||super.walkOption(e,t,r)}walkAtLeastOne(e,t,r){this.checkIsTarget(e,wi.REPETITION_MANDATORY,t,r)||super.walkOption(e,t,r)}walkAtLeastOneSep(e,t,r){this.checkIsTarget(e,wi.REPETITION_MANDATORY_WITH_SEPARATOR,t,r)||super.walkOption(e,t,r)}walkMany(e,t,r){this.checkIsTarget(e,wi.REPETITION,t,r)||super.walkOption(e,t,r)}walkManySep(e,t,r){this.checkIsTarget(e,wi.REPETITION_WITH_SEPARATOR,t,r)||super.walkOption(e,t,r)}},x5e=class extends Ov{static{o(this,"InsideDefinitionFinderVisitor")}static{E(this,"InsideDefinitionFinderVisitor")}constructor(e,t,r){super(),this.targetOccurrence=e,this.targetProdType=t,this.targetRef=r,this.result=[]}checkIsTarget(e,t){e.idx===this.targetOccurrence&&this.targetProdType===t&&(this.targetRef===void 0||e===this.targetRef)&&(this.result=e.definition)}visitOption(e){this.checkIsTarget(e,wi.OPTION)}visitRepetition(e){this.checkIsTarget(e,wi.REPETITION)}visitRepetitionMandatory(e){this.checkIsTarget(e,wi.REPETITION_MANDATORY)}visitRepetitionMandatoryWithSeparator(e){this.checkIsTarget(e,wi.REPETITION_MANDATORY_WITH_SEPARATOR)}visitRepetitionWithSeparator(e){this.checkIsTarget(e,wi.REPETITION_WITH_SEPARATOR)}visitAlternation(e){this.checkIsTarget(e,wi.ALTERNATION)}};o(OG,"initializeArrayOfArrays");E(OG,"initializeArrayOfArrays");o(u6,"pathToHashKeys");E(u6,"pathToHashKeys");o(b5e,"isUniquePrefixHash");E(b5e,"isUniquePrefixHash");o(Oq,"lookAheadSequenceFromAlternatives");E(Oq,"lookAheadSequenceFromAlternatives");o(Lw,"getLookaheadPathsForOr");E(Lw,"getLookaheadPathsForOr");o(Dw,"getLookaheadPathsForOptionalProd");E(Dw,"getLookaheadPathsForOptionalProd");o(eR,"containsPath");E(eR,"containsPath");o(T5e,"isStrictPrefixOfPath");E(T5e,"isStrictPrefixOfPath");o(Bq,"areTokenCategoriesNotUsed");E(Bq,"areTokenCategoriesNotUsed");o(C5e,"validateLookahead");E(C5e,"validateLookahead");o(w5e,"validateGrammar");E(w5e,"validateGrammar");o(k5e,"validateDuplicateProductions");E(k5e,"validateDuplicateProductions");o(S5e,"identifyProductionForDuplicates");E(S5e,"identifyProductionForDuplicates");o($q,"getExtraProductionArgument");E($q,"getExtraProductionArgument");qTt=class extends Ov{static{o(this,"OccurrenceValidationCollector")}static{E(this,"OccurrenceValidationCollector")}constructor(){super(...arguments),this.allProductions=[]}visitNonTerminal(e){this.allProductions.push(e)}visitOption(e){this.allProductions.push(e)}visitRepetitionWithSeparator(e){this.allProductions.push(e)}visitRepetitionMandatory(e){this.allProductions.push(e)}visitRepetitionMandatoryWithSeparator(e){this.allProductions.push(e)}visitRepetition(e){this.allProductions.push(e)}visitAlternation(e){this.allProductions.push(e)}visitTerminal(e){this.allProductions.push(e)}};o(E5e,"validateRuleDoesNotAlreadyExist");E(E5e,"validateRuleDoesNotAlreadyExist");o(A5e,"validateRuleIsOverridden");E(A5e,"validateRuleIsOverridden");o(Fq,"validateNoLeftRecursion");E(Fq,"validateNoLeftRecursion");o(VC,"getFirstNoneTerminal");E(VC,"getFirstNoneTerminal");zq=class extends Ov{static{o(this,"OrCollector")}static{E(this,"OrCollector")}constructor(){super(...arguments),this.alternations=[]}visitAlternation(e){this.alternations.push(e)}};o(R5e,"validateEmptyOrAlternative");E(R5e,"validateEmptyOrAlternative");o(_5e,"validateAmbiguousAlternationAlternatives");E(_5e,"validateAmbiguousAlternationAlternatives");HTt=class extends Ov{static{o(this,"RepetitionCollector")}static{E(this,"RepetitionCollector")}constructor(){super(...arguments),this.allProductions=[]}visitRepetitionWithSeparator(e){this.allProductions.push(e)}visitRepetitionMandatory(e){this.allProductions.push(e)}visitRepetitionMandatoryWithSeparator(e){this.allProductions.push(e)}visitRepetition(e){this.allProductions.push(e)}};o(L5e,"validateTooManyAlts");E(L5e,"validateTooManyAlts");o(D5e,"validateSomeNonEmptyLookaheadPath");E(D5e,"validateSomeNonEmptyLookaheadPath");o(I5e,"checkAlternativesAmbiguities");E(I5e,"checkAlternativesAmbiguities");o(M5e,"checkPrefixAlternativesAmbiguities");E(M5e,"checkPrefixAlternativesAmbiguities");o(N5e,"checkTerminalAndNoneTerminalsNameSpace");E(N5e,"checkTerminalAndNoneTerminalsNameSpace");o(P5e,"resolveGrammar2");E(P5e,"resolveGrammar");o(O5e,"validateGrammar2");E(O5e,"validateGrammar");B5e="MismatchedTokenException",$5e="NoViableAltException",F5e="EarlyExitException",z5e="NotAllInputParsedException",G5e=[B5e,$5e,F5e,z5e];Object.freeze(G5e);o(iw,"isRecognitionException");E(iw,"isRecognitionException");QR=class extends Error{static{o(this,"RecognitionException")}static{E(this,"RecognitionException")}constructor(e,t){super(e),this.token=t,this.resyncedTokens=[],Object.setPrototypeOf(this,new.target.prototype),Error.captureStackTrace&&Error.captureStackTrace(this,this.constructor)}},V5e=class extends QR{static{o(this,"MismatchedTokenException")}static{E(this,"MismatchedTokenException")}constructor(e,t,r){super(e,t),this.previousToken=r,this.name=B5e}},UTt=class extends QR{static{o(this,"NoViableAltException")}static{E(this,"NoViableAltException")}constructor(e,t,r){super(e,t),this.previousToken=r,this.name=$5e}},YTt=class extends QR{static{o(this,"NotAllInputParsedException")}static{E(this,"NotAllInputParsedException")}constructor(e,t){super(e,t),this.name=z5e}},jTt=class extends QR{static{o(this,"EarlyExitException")}static{E(this,"EarlyExitException")}constructor(e,t,r){super(e,t),this.previousToken=r,this.name=F5e}},az={},W5e="InRuleRecoveryException",XTt=class extends Error{static{o(this,"InRuleRecoveryException")}static{E(this,"InRuleRecoveryException")}constructor(e){super(e),this.name=W5e}},KTt=class{static{o(this,"Recoverable")}static{E(this,"Recoverable")}initRecoverable(e){this.firstAfterRepMap={},this.resyncFollows={},this.recoveryEnabled=wr(e,"recoveryEnabled")?e.recoveryEnabled:Jh.recoveryEnabled,this.recoveryEnabled&&(this.attemptInRepetitionRecovery=q5e)}getTokenToInsert(e){let t=_w(e,"",NaN,NaN,NaN,NaN,NaN,NaN);return t.isInsertedInRecovery=!0,t}canTokenTypeBeInsertedInRecovery(e){return!0}canTokenTypeBeDeletedInRecovery(e){return!0}tryInRepetitionRecovery(e,t,r,n){let i=this.findReSyncTokenType(),a=this.exportLexerState(),s=[],l=!1,u=this.LA(1),h=this.LA(1),d=E(()=>{let f=this.LA(0),p=this.errorMessageProvider.buildMismatchTokenMessage({expected:n,actual:u,previous:f,ruleName:this.getCurrRuleFullName()}),m=new V5e(p,u,this.LA(0));m.resyncedTokens=tw(s),this.SAVE_ERROR(m)},"generateErrorMessage");for(;!l;)if(this.tokenMatcher(h,n)){d();return}else if(r.call(this)){d(),e.apply(this,t);return}else this.tokenMatcher(h,i)?l=!0:(h=this.SKIP_TOKEN(),this.addToResyncTokens(h,s));this.importLexerState(a)}shouldInRepetitionRecoveryBeTried(e,t,r){return!(r===!1||this.tokenMatcher(this.LA(1),e)||this.isBackTracking()||this.canPerformInRuleRecovery(e,this.getFollowsForInRuleRecovery(e,t)))}getFollowsForInRuleRecovery(e,t){let r=this.getCurrentGrammarPath(e,t);return this.getNextPossibleTokenTypes(r)}tryInRuleRecovery(e,t){if(this.canRecoverWithSingleTokenInsertion(e,t))return this.getTokenToInsert(e);if(this.canRecoverWithSingleTokenDeletion(e)){let r=this.SKIP_TOKEN();return this.consumeToken(),r}throw new XTt("sad sad panda")}canPerformInRuleRecovery(e,t){return this.canRecoverWithSingleTokenInsertion(e,t)||this.canRecoverWithSingleTokenDeletion(e)}canRecoverWithSingleTokenInsertion(e,t){if(!this.canTokenTypeBeInsertedInRecovery(e)||Jn(t))return!1;let r=this.LA(1);return _v(t,i=>this.tokenMatcher(r,i))!==void 0}canRecoverWithSingleTokenDeletion(e){return this.canTokenTypeBeDeletedInRecovery(e)?this.tokenMatcher(this.LA(2),e):!1}isInCurrentRuleReSyncSet(e){let t=this.getCurrFollowKey(),r=this.getFollowSetFromFollowKey(t);return qs(r,e)}findReSyncTokenType(){let e=this.flattenFollowSet(),t=this.LA(1),r=2;for(;;){let n=_v(e,i=>Nq(t,i));if(n!==void 0)return n;t=this.LA(r),r++}}getCurrFollowKey(){if(this.RULE_STACK.length===1)return az;let e=this.getLastExplicitRuleShortName(),t=this.getLastExplicitRuleOccurrenceIndex(),r=this.getPreviousExplicitRuleShortName();return{ruleName:this.shortRuleNameToFullName(e),idxInCallingRule:t,inRule:this.shortRuleNameToFullName(r)}}buildFullFollowKeyStack(){let e=this.RULE_STACK,t=this.RULE_OCCURRENCE_STACK;return hr(e,(r,n)=>n===0?az:{ruleName:this.shortRuleNameToFullName(r),idxInCallingRule:t[n],inRule:this.shortRuleNameToFullName(e[n-1])})}flattenFollowSet(){let e=hr(this.buildFullFollowKeyStack(),t=>this.getFollowSetFromFollowKey(t));return Sc(e)}getFollowSetFromFollowKey(e){if(e===az)return[mp];let t=e.ruleName+e.idxInCallingRule+_3e+e.inRule;return this.resyncFollows[t]}addToResyncTokens(e,t){return this.tokenMatcher(e,mp)||t.push(e),t}reSyncTo(e){let t=[],r=this.LA(1);for(;this.tokenMatcher(r,e)===!1;)r=this.SKIP_TOKEN(),this.addToResyncTokens(r,t);return tw(t)}attemptInRepetitionRecovery(e,t,r,n,i,a,s){}getCurrentGrammarPath(e,t){let r=this.getHumanReadableRuleStack(),n=Ya(this.RULE_OCCURRENCE_STACK);return{ruleStack:r,occurrenceStack:n,lastTok:e,lastTokOccurrence:t}}getHumanReadableRuleStack(){return hr(this.RULE_STACK,e=>this.shortRuleNameToFullName(e))}};o(q5e,"attemptInRepetitionRecovery");E(q5e,"attemptInRepetitionRecovery");ZTt=4,Tp=8,QTt=8,H5e=1<Fq(t,t,qg))}validateEmptyOrAlternatives(e){return _l(e,t=>R5e(t,qg))}validateAmbiguousAlternationAlternatives(e,t){return _l(e,r=>_5e(r,t,qg))}validateSomeNonEmptyLookaheadPath(e,t){return D5e(e,t,qg)}buildLookaheadForAlternation(e){return m5e(e.prodOccurrence,e.rule,e.maxLookahead,e.hasPredicates,e.dynamicTokensEnabled,y5e)}buildLookaheadForOptional(e){return g5e(e.prodOccurrence,e.rule,e.maxLookahead,e.dynamicTokensEnabled,ZR(e.prodType),v5e)}},JTt=class{static{o(this,"LooksAhead")}static{E(this,"LooksAhead")}initLooksAhead(e){this.dynamicTokensEnabled=wr(e,"dynamicTokensEnabled")?e.dynamicTokensEnabled:Jh.dynamicTokensEnabled,this.maxLookahead=wr(e,"maxLookahead")?e.maxLookahead:Jh.maxLookahead,this.lookaheadStrategy=wr(e,"lookaheadStrategy")?e.lookaheadStrategy:new Gq({maxLookahead:this.maxLookahead}),this.lookAheadFuncsCache=new Map}preComputeLookaheadFunctions(e){Ir(e,t=>{this.TRACE_INIT(`${t.name} Rule Lookahead`,()=>{let{alternation:r,repetition:n,option:i,repetitionMandatory:a,repetitionMandatoryWithSeparator:s,repetitionWithSeparator:l}=Y5e(t);Ir(r,u=>{let h=u.idx===0?"":u.idx;this.TRACE_INIT(`${Tc(u)}${h}`,()=>{let d=this.lookaheadStrategy.buildLookaheadForAlternation({prodOccurrence:u.idx,rule:t,maxLookahead:u.maxLookahead||this.maxLookahead,hasPredicates:u.hasPredicates,dynamicTokensEnabled:this.dynamicTokensEnabled}),f=d6(this.fullRuleNameToShort[t.name],H5e,u.idx);this.setLaFuncCache(f,d)})}),Ir(n,u=>{this.computeLookaheadFunc(t,u.idx,BG,"Repetition",u.maxLookahead,Tc(u))}),Ir(i,u=>{this.computeLookaheadFunc(t,u.idx,U5e,"Option",u.maxLookahead,Tc(u))}),Ir(a,u=>{this.computeLookaheadFunc(t,u.idx,$G,"RepetitionMandatory",u.maxLookahead,Tc(u))}),Ir(s,u=>{this.computeLookaheadFunc(t,u.idx,h6,"RepetitionMandatoryWithSeparator",u.maxLookahead,Tc(u))}),Ir(l,u=>{this.computeLookaheadFunc(t,u.idx,FG,"RepetitionWithSeparator",u.maxLookahead,Tc(u))})})})}computeLookaheadFunc(e,t,r,n,i,a){this.TRACE_INIT(`${a}${t===0?"":t}`,()=>{let s=this.lookaheadStrategy.buildLookaheadForOptional({prodOccurrence:t,rule:e,maxLookahead:i||this.maxLookahead,dynamicTokensEnabled:this.dynamicTokensEnabled,prodType:n}),l=d6(this.fullRuleNameToShort[e.name],r,t);this.setLaFuncCache(l,s)})}getKeyForAutomaticLookahead(e,t){let r=this.getLastExplicitRuleShortName();return d6(r,e,t)}getLaFuncFromCache(e){return this.lookAheadFuncsCache.get(e)}setLaFuncCache(e,t){this.lookAheadFuncsCache.set(e,t)}},eCt=class extends Ov{static{o(this,"DslMethodsCollectorVisitor")}static{E(this,"DslMethodsCollectorVisitor")}constructor(){super(...arguments),this.dslMethods={option:[],alternation:[],repetition:[],repetitionWithSeparator:[],repetitionMandatory:[],repetitionMandatoryWithSeparator:[]}}reset(){this.dslMethods={option:[],alternation:[],repetition:[],repetitionWithSeparator:[],repetitionMandatory:[],repetitionMandatoryWithSeparator:[]}}visitOption(e){this.dslMethods.option.push(e)}visitRepetitionWithSeparator(e){this.dslMethods.repetitionWithSeparator.push(e)}visitRepetitionMandatory(e){this.dslMethods.repetitionMandatory.push(e)}visitRepetitionMandatoryWithSeparator(e){this.dslMethods.repetitionMandatoryWithSeparator.push(e)}visitRepetition(e){this.dslMethods.repetition.push(e)}visitAlternation(e){this.dslMethods.alternation.push(e)}},OA=new eCt;o(Y5e,"collectMethods");E(Y5e,"collectMethods");o(zG,"setNodeLocationOnlyOffset");E(zG,"setNodeLocationOnlyOffset");o(GG,"setNodeLocationFull");E(GG,"setNodeLocationFull");o(j5e,"addTerminalToCst");E(j5e,"addTerminalToCst");o(X5e,"addNoneTerminalToCst");E(X5e,"addNoneTerminalToCst");tCt="name";o(Vq,"defineNameProp");E(Vq,"defineNameProp");o(K5e,"defaultVisit");E(K5e,"defaultVisit");o(Z5e,"createBaseSemanticVisitorConstructor");E(Z5e,"createBaseSemanticVisitorConstructor");o(Q5e,"createBaseVisitorConstructorWithDefaults");E(Q5e,"createBaseVisitorConstructorWithDefaults");(function(e){e[e.REDUNDANT_METHOD=0]="REDUNDANT_METHOD",e[e.MISSING_METHOD=1]="MISSING_METHOD"})(VG||(VG={}));o(J5e,"validateVisitor");E(J5e,"validateVisitor");o(eAe,"validateMissingCstMethods");E(eAe,"validateMissingCstMethods");rCt=class{static{o(this,"TreeBuilder")}static{E(this,"TreeBuilder")}initTreeBuilder(e){if(this.CST_STACK=[],this.outputCst=e.outputCst,this.nodeLocationTracking=wr(e,"nodeLocationTracking")?e.nodeLocationTracking:Jh.nodeLocationTracking,!this.outputCst)this.cstInvocationStateUpdate=Da,this.cstFinallyStateUpdate=Da,this.cstPostTerminal=Da,this.cstPostNonTerminal=Da,this.cstPostRule=Da;else if(/full/i.test(this.nodeLocationTracking))this.recoveryEnabled?(this.setNodeLocationFromToken=GG,this.setNodeLocationFromNode=GG,this.cstPostRule=Da,this.setInitialNodeLocation=this.setInitialNodeLocationFullRecovery):(this.setNodeLocationFromToken=Da,this.setNodeLocationFromNode=Da,this.cstPostRule=this.cstPostRuleFull,this.setInitialNodeLocation=this.setInitialNodeLocationFullRegular);else if(/onlyOffset/i.test(this.nodeLocationTracking))this.recoveryEnabled?(this.setNodeLocationFromToken=zG,this.setNodeLocationFromNode=zG,this.cstPostRule=Da,this.setInitialNodeLocation=this.setInitialNodeLocationOnlyOffsetRecovery):(this.setNodeLocationFromToken=Da,this.setNodeLocationFromNode=Da,this.cstPostRule=this.cstPostRuleOnlyOffset,this.setInitialNodeLocation=this.setInitialNodeLocationOnlyOffsetRegular);else if(/none/i.test(this.nodeLocationTracking))this.setNodeLocationFromToken=Da,this.setNodeLocationFromNode=Da,this.cstPostRule=Da,this.setInitialNodeLocation=Da;else throw Error(`Invalid config option: "${e.nodeLocationTracking}"`)}setInitialNodeLocationOnlyOffsetRecovery(e){e.location={startOffset:NaN,endOffset:NaN}}setInitialNodeLocationOnlyOffsetRegular(e){e.location={startOffset:this.LA(1).startOffset,endOffset:NaN}}setInitialNodeLocationFullRecovery(e){e.location={startOffset:NaN,startLine:NaN,startColumn:NaN,endOffset:NaN,endLine:NaN,endColumn:NaN}}setInitialNodeLocationFullRegular(e){let t=this.LA(1);e.location={startOffset:t.startOffset,startLine:t.startLine,startColumn:t.startColumn,endOffset:NaN,endLine:NaN,endColumn:NaN}}cstInvocationStateUpdate(e){let t={name:e,children:Object.create(null)};this.setInitialNodeLocation(t),this.CST_STACK.push(t)}cstFinallyStateUpdate(){this.CST_STACK.pop()}cstPostRuleFull(e){let t=this.LA(0),r=e.location;r.startOffset<=t.startOffset?(r.endOffset=t.endOffset,r.endLine=t.endLine,r.endColumn=t.endColumn):(r.startOffset=NaN,r.startLine=NaN,r.startColumn=NaN)}cstPostRuleOnlyOffset(e){let t=this.LA(0),r=e.location;r.startOffset<=t.startOffset?r.endOffset=t.endOffset:r.startOffset=NaN}cstPostTerminal(e,t){let r=this.CST_STACK[this.CST_STACK.length-1];j5e(r,t,e),this.setNodeLocationFromToken(r.location,t)}cstPostNonTerminal(e,t){let r=this.CST_STACK[this.CST_STACK.length-1];X5e(r,t,e),this.setNodeLocationFromNode(r.location,e.location)}getBaseCstVisitorConstructor(){if(Zh(this.baseCstVisitorConstructor)){let e=Z5e(this.className,jo(this.gastProductionsCache));return this.baseCstVisitorConstructor=e,e}return this.baseCstVisitorConstructor}getBaseCstVisitorConstructorWithDefaults(){if(Zh(this.baseCstVisitorWithDefaultsConstructor)){let e=Q5e(this.className,jo(this.gastProductionsCache),this.getBaseCstVisitorConstructor());return this.baseCstVisitorWithDefaultsConstructor=e,e}return this.baseCstVisitorWithDefaultsConstructor}getLastExplicitRuleShortName(){let e=this.RULE_STACK;return e[e.length-1]}getPreviousExplicitRuleShortName(){let e=this.RULE_STACK;return e[e.length-2]}getLastExplicitRuleOccurrenceIndex(){let e=this.RULE_OCCURRENCE_STACK;return e[e.length-1]}},nCt=class{static{o(this,"LexerAdapter")}static{E(this,"LexerAdapter")}initLexerAdapter(){this.tokVector=[],this.tokVectorLength=0,this.currIdx=-1}set input(e){if(this.selfAnalysisDone!==!0)throw Error("Missing invocation at the end of the Parser's constructor.");this.reset(),this.tokVector=e,this.tokVectorLength=e.length}get input(){return this.tokVector}SKIP_TOKEN(){return this.currIdx<=this.tokVector.length-2?(this.consumeToken(),this.LA(1)):tR}LA(e){let t=this.currIdx+e;return t<0||this.tokVectorLength<=t?tR:this.tokVector[t]}consumeToken(){this.currIdx++}exportLexerState(){return this.currIdx}importLexerState(e){this.currIdx=e}resetLexerState(){this.currIdx=-1}moveToTerminatedState(){this.currIdx=this.tokVector.length-1}getLexerPosition(){return this.exportLexerState()}},iCt=class{static{o(this,"RecognizerApi")}static{E(this,"RecognizerApi")}ACTION(e){return e.call(this)}consume(e,t,r){return this.consumeInternal(t,e,r)}subrule(e,t,r){return this.subruleInternal(t,e,r)}option(e,t){return this.optionInternal(t,e)}or(e,t){return this.orInternal(t,e)}many(e,t){return this.manyInternal(e,t)}atLeastOne(e,t){return this.atLeastOneInternal(e,t)}CONSUME(e,t){return this.consumeInternal(e,0,t)}CONSUME1(e,t){return this.consumeInternal(e,1,t)}CONSUME2(e,t){return this.consumeInternal(e,2,t)}CONSUME3(e,t){return this.consumeInternal(e,3,t)}CONSUME4(e,t){return this.consumeInternal(e,4,t)}CONSUME5(e,t){return this.consumeInternal(e,5,t)}CONSUME6(e,t){return this.consumeInternal(e,6,t)}CONSUME7(e,t){return this.consumeInternal(e,7,t)}CONSUME8(e,t){return this.consumeInternal(e,8,t)}CONSUME9(e,t){return this.consumeInternal(e,9,t)}SUBRULE(e,t){return this.subruleInternal(e,0,t)}SUBRULE1(e,t){return this.subruleInternal(e,1,t)}SUBRULE2(e,t){return this.subruleInternal(e,2,t)}SUBRULE3(e,t){return this.subruleInternal(e,3,t)}SUBRULE4(e,t){return this.subruleInternal(e,4,t)}SUBRULE5(e,t){return this.subruleInternal(e,5,t)}SUBRULE6(e,t){return this.subruleInternal(e,6,t)}SUBRULE7(e,t){return this.subruleInternal(e,7,t)}SUBRULE8(e,t){return this.subruleInternal(e,8,t)}SUBRULE9(e,t){return this.subruleInternal(e,9,t)}OPTION(e){return this.optionInternal(e,0)}OPTION1(e){return this.optionInternal(e,1)}OPTION2(e){return this.optionInternal(e,2)}OPTION3(e){return this.optionInternal(e,3)}OPTION4(e){return this.optionInternal(e,4)}OPTION5(e){return this.optionInternal(e,5)}OPTION6(e){return this.optionInternal(e,6)}OPTION7(e){return this.optionInternal(e,7)}OPTION8(e){return this.optionInternal(e,8)}OPTION9(e){return this.optionInternal(e,9)}OR(e){return this.orInternal(e,0)}OR1(e){return this.orInternal(e,1)}OR2(e){return this.orInternal(e,2)}OR3(e){return this.orInternal(e,3)}OR4(e){return this.orInternal(e,4)}OR5(e){return this.orInternal(e,5)}OR6(e){return this.orInternal(e,6)}OR7(e){return this.orInternal(e,7)}OR8(e){return this.orInternal(e,8)}OR9(e){return this.orInternal(e,9)}MANY(e){this.manyInternal(0,e)}MANY1(e){this.manyInternal(1,e)}MANY2(e){this.manyInternal(2,e)}MANY3(e){this.manyInternal(3,e)}MANY4(e){this.manyInternal(4,e)}MANY5(e){this.manyInternal(5,e)}MANY6(e){this.manyInternal(6,e)}MANY7(e){this.manyInternal(7,e)}MANY8(e){this.manyInternal(8,e)}MANY9(e){this.manyInternal(9,e)}MANY_SEP(e){this.manySepFirstInternal(0,e)}MANY_SEP1(e){this.manySepFirstInternal(1,e)}MANY_SEP2(e){this.manySepFirstInternal(2,e)}MANY_SEP3(e){this.manySepFirstInternal(3,e)}MANY_SEP4(e){this.manySepFirstInternal(4,e)}MANY_SEP5(e){this.manySepFirstInternal(5,e)}MANY_SEP6(e){this.manySepFirstInternal(6,e)}MANY_SEP7(e){this.manySepFirstInternal(7,e)}MANY_SEP8(e){this.manySepFirstInternal(8,e)}MANY_SEP9(e){this.manySepFirstInternal(9,e)}AT_LEAST_ONE(e){this.atLeastOneInternal(0,e)}AT_LEAST_ONE1(e){return this.atLeastOneInternal(1,e)}AT_LEAST_ONE2(e){this.atLeastOneInternal(2,e)}AT_LEAST_ONE3(e){this.atLeastOneInternal(3,e)}AT_LEAST_ONE4(e){this.atLeastOneInternal(4,e)}AT_LEAST_ONE5(e){this.atLeastOneInternal(5,e)}AT_LEAST_ONE6(e){this.atLeastOneInternal(6,e)}AT_LEAST_ONE7(e){this.atLeastOneInternal(7,e)}AT_LEAST_ONE8(e){this.atLeastOneInternal(8,e)}AT_LEAST_ONE9(e){this.atLeastOneInternal(9,e)}AT_LEAST_ONE_SEP(e){this.atLeastOneSepFirstInternal(0,e)}AT_LEAST_ONE_SEP1(e){this.atLeastOneSepFirstInternal(1,e)}AT_LEAST_ONE_SEP2(e){this.atLeastOneSepFirstInternal(2,e)}AT_LEAST_ONE_SEP3(e){this.atLeastOneSepFirstInternal(3,e)}AT_LEAST_ONE_SEP4(e){this.atLeastOneSepFirstInternal(4,e)}AT_LEAST_ONE_SEP5(e){this.atLeastOneSepFirstInternal(5,e)}AT_LEAST_ONE_SEP6(e){this.atLeastOneSepFirstInternal(6,e)}AT_LEAST_ONE_SEP7(e){this.atLeastOneSepFirstInternal(7,e)}AT_LEAST_ONE_SEP8(e){this.atLeastOneSepFirstInternal(8,e)}AT_LEAST_ONE_SEP9(e){this.atLeastOneSepFirstInternal(9,e)}RULE(e,t,r=rR){if(qs(this.definedRulesNames,e)){let a={message:qg.buildDuplicateRuleNameError({topLevelRule:e,grammarName:this.className}),type:Vs.DUPLICATE_RULE_NAME,ruleName:e};this.definitionErrors.push(a)}this.definedRulesNames.push(e);let n=this.defineRule(e,t,r);return this[e]=n,n}OVERRIDE_RULE(e,t,r=rR){let n=A5e(e,this.definedRulesNames,this.className);this.definitionErrors=this.definitionErrors.concat(n);let i=this.defineRule(e,t,r);return this[e]=i,i}BACKTRACK(e,t){return function(){this.isBackTrackingStack.push(1);let r=this.saveRecogState();try{return e.apply(this,t),!0}catch(n){if(iw(n))return!1;throw n}finally{this.reloadRecogState(r),this.isBackTrackingStack.pop()}}}getGAstProductions(){return this.gastProductionsCache}getSerializedGastProductions(){return w3e(ha(this.gastProductionsCache))}},aCt=class{static{o(this,"RecognizerEngine")}static{E(this,"RecognizerEngine")}initRecognizerEngine(e,t){if(this.className=this.constructor.name,this.shortRuleNameToFull={},this.fullRuleNameToShort={},this.ruleShortNameIdx=256,this.tokenMatcher=nw,this.subruleIdx=0,this.definedRulesNames=[],this.tokensMap={},this.isBackTrackingStack=[],this.RULE_STACK=[],this.RULE_OCCURRENCE_STACK=[],this.gastProductionsCache={},wr(t,"serializedGrammar"))throw Error(`The Parser's configuration can no longer contain a property. + See: https://chevrotain.io/docs/changes/BREAKING_CHANGES.html#_6-0-0 + For Further details.`);if(un(e)){if(Jn(e))throw Error(`A Token Vocabulary cannot be empty. + Note that the first argument for the parser constructor + is no longer a Token vector (since v4.0).`);if(typeof e[0].startOffset=="number")throw Error(`The Parser constructor no longer accepts a token vector as the first argument. + See: https://chevrotain.io/docs/changes/BREAKING_CHANGES.html#_4-0-0 + For Further details.`)}if(un(e))this.tokensMap=Ko(e,(i,a)=>(i[a.name]=a,i),{});else if(wr(e,"modes")&&Ec(Sc(ha(e.modes)),h5e)){let i=Sc(ha(e.modes)),a=kq(i);this.tokensMap=Ko(a,(s,l)=>(s[l.name]=l,s),{})}else if(Dl(e))this.tokensMap=Ya(e);else throw new Error(" argument must be An Array of Token constructors, A dictionary of Token constructors or an IMultiModeLexerDefinition");this.tokensMap.EOF=mp;let r=wr(e,"modes")?Sc(ha(e.modes)):ha(e),n=Ec(r,i=>Jn(i.categoryMatches));this.tokenMatcher=n?nw:$v,Fv(ha(this.tokensMap))}defineRule(e,t,r){if(this.selfAnalysisDone)throw Error(`Grammar rule <${e}> may not be defined after the 'performSelfAnalysis' method has been called' +Make sure that all grammar rule definitions are done before 'performSelfAnalysis' is called.`);let n=wr(r,"resyncEnabled")?r.resyncEnabled:rR.resyncEnabled,i=wr(r,"recoveryValueFunc")?r.recoveryValueFunc:rR.recoveryValueFunc,a=this.ruleShortNameIdx<a.call(this)&&s.call(this),"lookAheadFunc")}}else i=e;if(n.call(this)===!0)return i.call(this)}atLeastOneInternal(e,t){let r=this.getKeyForAutomaticLookahead($G,e);return this.atLeastOneInternalLogic(e,t,r)}atLeastOneInternalLogic(e,t,r){let n=this.getLaFuncFromCache(r),i;if(typeof t!="function"){i=t.DEF;let a=t.GATE;if(a!==void 0){let s=n;n=E(()=>a.call(this)&&s.call(this),"lookAheadFunc")}}else i=t;if(n.call(this)===!0){let a=this.doSingleRepetition(i);for(;n.call(this)===!0&&a===!0;)a=this.doSingleRepetition(i)}else throw this.raiseEarlyExitException(e,wi.REPETITION_MANDATORY,t.ERR_MSG);this.attemptInRepetitionRecovery(this.atLeastOneInternal,[e,t],n,$G,e,VTt)}atLeastOneSepFirstInternal(e,t){let r=this.getKeyForAutomaticLookahead(h6,e);this.atLeastOneSepFirstInternalLogic(e,t,r)}atLeastOneSepFirstInternalLogic(e,t,r){let n=t.DEF,i=t.SEP;if(this.getLaFuncFromCache(r).call(this)===!0){n.call(this);let s=E(()=>this.tokenMatcher(this.LA(1),i),"separatorLookAheadFunc");for(;this.tokenMatcher(this.LA(1),i)===!0;)this.CONSUME(i),n.call(this);this.attemptInRepetitionRecovery(this.repetitionSepSecondInternal,[e,i,s,n,_Ce],s,h6,e,_Ce)}else throw this.raiseEarlyExitException(e,wi.REPETITION_MANDATORY_WITH_SEPARATOR,t.ERR_MSG)}manyInternal(e,t){let r=this.getKeyForAutomaticLookahead(BG,e);return this.manyInternalLogic(e,t,r)}manyInternalLogic(e,t,r){let n=this.getLaFuncFromCache(r),i;if(typeof t!="function"){i=t.DEF;let s=t.GATE;if(s!==void 0){let l=n;n=E(()=>s.call(this)&&l.call(this),"lookaheadFunction")}}else i=t;let a=!0;for(;n.call(this)===!0&&a===!0;)a=this.doSingleRepetition(i);this.attemptInRepetitionRecovery(this.manyInternal,[e,t],n,BG,e,GTt,a)}manySepFirstInternal(e,t){let r=this.getKeyForAutomaticLookahead(FG,e);this.manySepFirstInternalLogic(e,t,r)}manySepFirstInternalLogic(e,t,r){let n=t.DEF,i=t.SEP;if(this.getLaFuncFromCache(r).call(this)===!0){n.call(this);let s=E(()=>this.tokenMatcher(this.LA(1),i),"separatorLookAheadFunc");for(;this.tokenMatcher(this.LA(1),i)===!0;)this.CONSUME(i),n.call(this);this.attemptInRepetitionRecovery(this.repetitionSepSecondInternal,[e,i,s,n,RCe],s,FG,e,RCe)}}repetitionSepSecondInternal(e,t,r,n,i){for(;r();)this.CONSUME(t),n.call(this);this.attemptInRepetitionRecovery(this.repetitionSepSecondInternal,[e,t,r,n,i],r,h6,e,i)}doSingleRepetition(e){let t=this.getLexerPosition();return e.call(this),this.getLexerPosition()>t}orInternal(e,t){let r=this.getKeyForAutomaticLookahead(H5e,t),n=un(e)?e:e.DEF,a=this.getLaFuncFromCache(r).call(this,n);if(a!==void 0)return n[a].ALT.call(this);this.raiseNoAltException(t,e.ERR_MSG)}ruleFinallyStateUpdate(){if(this.RULE_STACK.pop(),this.RULE_OCCURRENCE_STACK.pop(),this.cstFinallyStateUpdate(),this.RULE_STACK.length===0&&this.isAtEndOfInput()===!1){let e=this.LA(1),t=this.errorMessageProvider.buildNotAllInputParsedMessage({firstRedundant:e,ruleName:this.getCurrRuleFullName()});this.SAVE_ERROR(new YTt(t,e))}}subruleInternal(e,t,r){let n;try{let i=r!==void 0?r.ARGS:void 0;return this.subruleIdx=t,n=e.apply(this,i),this.cstPostNonTerminal(n,r!==void 0&&r.LABEL!==void 0?r.LABEL:e.ruleName),n}catch(i){throw this.subruleInternalError(i,r,e.ruleName)}}subruleInternalError(e,t,r){throw iw(e)&&e.partialCstResult!==void 0&&(this.cstPostNonTerminal(e.partialCstResult,t!==void 0&&t.LABEL!==void 0?t.LABEL:r),delete e.partialCstResult),e}consumeInternal(e,t,r){let n;try{let i=this.LA(1);this.tokenMatcher(i,e)===!0?(this.consumeToken(),n=i):this.consumeInternalError(e,i,r)}catch(i){n=this.consumeInternalRecovery(e,t,i)}return this.cstPostTerminal(r!==void 0&&r.LABEL!==void 0?r.LABEL:e.name,n),n}consumeInternalError(e,t,r){let n,i=this.LA(0);throw r!==void 0&&r.ERR_MSG?n=r.ERR_MSG:n=this.errorMessageProvider.buildMismatchTokenMessage({expected:e,actual:t,previous:i,ruleName:this.getCurrRuleFullName()}),this.SAVE_ERROR(new V5e(n,t,i))}consumeInternalRecovery(e,t,r){if(this.recoveryEnabled&&r.name==="MismatchedTokenException"&&!this.isBackTracking()){let n=this.getFollowsForInRuleRecovery(e,t);try{return this.tryInRuleRecovery(e,n)}catch(i){throw i.name===W5e?r:i}}else throw r}saveRecogState(){let e=this.errors,t=Ya(this.RULE_STACK);return{errors:e,lexerState:this.exportLexerState(),RULE_STACK:t,CST_STACK:this.CST_STACK}}reloadRecogState(e){this.errors=e.errors,this.importLexerState(e.lexerState),this.RULE_STACK=e.RULE_STACK}ruleInvocationStateUpdate(e,t,r){this.RULE_OCCURRENCE_STACK.push(r),this.RULE_STACK.push(e),this.cstInvocationStateUpdate(t)}isBackTracking(){return this.isBackTrackingStack.length!==0}getCurrRuleFullName(){let e=this.getLastExplicitRuleShortName();return this.shortRuleNameToFull[e]}shortRuleNameToFullName(e){return this.shortRuleNameToFull[e]}isAtEndOfInput(){return this.tokenMatcher(this.LA(1),mp)}reset(){this.resetLexerState(),this.subruleIdx=0,this.isBackTrackingStack=[],this.errors=[],this.RULE_STACK=[],this.CST_STACK=[],this.RULE_OCCURRENCE_STACK=[]}},sCt=class{static{o(this,"ErrorHandler")}static{E(this,"ErrorHandler")}initErrorHandler(e){this._errors=[],this.errorMessageProvider=wr(e,"errorMessageProvider")?e.errorMessageProvider:Jh.errorMessageProvider}SAVE_ERROR(e){if(iw(e))return e.context={ruleStack:this.getHumanReadableRuleStack(),ruleOccurrenceStack:Ya(this.RULE_OCCURRENCE_STACK)},this._errors.push(e),e;throw Error("Trying to save an Error which is not a RecognitionException")}get errors(){return Ya(this._errors)}set errors(e){this._errors=e}raiseEarlyExitException(e,t,r){let n=this.getCurrRuleFullName(),i=this.getGAstProductions()[n],s=Dw(e,i,t,this.maxLookahead)[0],l=[];for(let h=1;h<=this.maxLookahead;h++)l.push(this.LA(h));let u=this.errorMessageProvider.buildEarlyExitMessage({expectedIterationPaths:s,actual:l,previous:this.LA(0),customUserDescription:r,ruleName:n});throw this.SAVE_ERROR(new jTt(u,this.LA(1),this.LA(0)))}raiseNoAltException(e,t){let r=this.getCurrRuleFullName(),n=this.getGAstProductions()[r],i=Lw(e,n,this.maxLookahead),a=[];for(let u=1;u<=this.maxLookahead;u++)a.push(this.LA(u));let s=this.LA(0),l=this.errorMessageProvider.buildNoViableAltMessage({expectedPathsPerAlt:i,actual:a,previous:s,customUserDescription:t,ruleName:this.getCurrRuleFullName()});throw this.SAVE_ERROR(new UTt(l,this.LA(1),s))}},oCt=class{static{o(this,"ContentAssist")}static{E(this,"ContentAssist")}initContentAssist(){}computeContentAssist(e,t){let r=this.gastProductionsCache[e];if(Zh(r))throw Error(`Rule ->${e}<- does not exist in this grammar.`);return Pq([r],t,this.tokenMatcher,this.maxLookahead)}getNextPossibleTokenTypes(e){let t=Rc(e.ruleStack),n=this.getGAstProductions()[t];return new zTt(n,e).startWalking()}},JR={description:"This Object indicates the Parser is during Recording Phase"};Object.freeze(JR);LCe=!0,DCe=Math.pow(2,Tp)-1,tAe=xv({name:"RECORDING_PHASE_TOKEN",pattern:Fs.NA});Fv([tAe]);rAe=_w(tAe,`This IToken indicates the Parser is in Recording Phase + See: https://chevrotain.io/docs/guide/internals.html#grammar-recording for details`,-1,-1,-1,-1,-1,-1);Object.freeze(rAe);lCt={name:`This CSTNode indicates the Parser is in Recording Phase + See: https://chevrotain.io/docs/guide/internals.html#grammar-recording for details`,children:{}},cCt=class{static{o(this,"GastRecorder")}static{E(this,"GastRecorder")}initGastRecorder(e){this.recordingProdStack=[],this.RECORDING_PHASE=!1}enableRecording(){this.RECORDING_PHASE=!0,this.TRACE_INIT("Enable Recording",()=>{for(let e=0;e<10;e++){let t=e>0?e:"";this[`CONSUME${t}`]=function(r,n){return this.consumeInternalRecord(r,e,n)},this[`SUBRULE${t}`]=function(r,n){return this.subruleInternalRecord(r,e,n)},this[`OPTION${t}`]=function(r){return this.optionInternalRecord(r,e)},this[`OR${t}`]=function(r){return this.orInternalRecord(r,e)},this[`MANY${t}`]=function(r){this.manyInternalRecord(e,r)},this[`MANY_SEP${t}`]=function(r){this.manySepFirstInternalRecord(e,r)},this[`AT_LEAST_ONE${t}`]=function(r){this.atLeastOneInternalRecord(e,r)},this[`AT_LEAST_ONE_SEP${t}`]=function(r){this.atLeastOneSepFirstInternalRecord(e,r)}}this.consume=function(e,t,r){return this.consumeInternalRecord(t,e,r)},this.subrule=function(e,t,r){return this.subruleInternalRecord(t,e,r)},this.option=function(e,t){return this.optionInternalRecord(t,e)},this.or=function(e,t){return this.orInternalRecord(t,e)},this.many=function(e,t){this.manyInternalRecord(e,t)},this.atLeastOne=function(e,t){this.atLeastOneInternalRecord(e,t)},this.ACTION=this.ACTION_RECORD,this.BACKTRACK=this.BACKTRACK_RECORD,this.LA=this.LA_RECORD})}disableRecording(){this.RECORDING_PHASE=!1,this.TRACE_INIT("Deleting Recording methods",()=>{let e=this;for(let t=0;t<10;t++){let r=t>0?t:"";delete e[`CONSUME${r}`],delete e[`SUBRULE${r}`],delete e[`OPTION${r}`],delete e[`OR${r}`],delete e[`MANY${r}`],delete e[`MANY_SEP${r}`],delete e[`AT_LEAST_ONE${r}`],delete e[`AT_LEAST_ONE_SEP${r}`]}delete e.consume,delete e.subrule,delete e.option,delete e.or,delete e.many,delete e.atLeastOne,delete e.ACTION,delete e.BACKTRACK,delete e.LA})}ACTION_RECORD(e){}BACKTRACK_RECORD(e,t){return()=>!0}LA_RECORD(e){return tR}topLevelRuleRecord(e,t){try{let r=new Pv({definition:[],name:e});return r.name=e,this.recordingProdStack.push(r),t.call(this),this.recordingProdStack.pop(),r}catch(r){if(r.KNOWN_RECORDER_ERROR!==!0)try{r.message=r.message+` + This error was thrown during the "grammar recording phase" For more info see: + https://chevrotain.io/docs/guide/internals.html#grammar-recording`}catch{throw r}throw r}}optionInternalRecord(e,t){return nv.call(this,Ua,e,t)}atLeastOneInternalRecord(e,t){nv.call(this,Zo,t,e)}atLeastOneSepFirstInternalRecord(e,t){nv.call(this,Qo,t,e,LCe)}manyInternalRecord(e,t){nv.call(this,Mi,t,e)}manySepFirstInternalRecord(e,t){nv.call(this,wo,t,e,LCe)}orInternalRecord(e,t){return nAe.call(this,e,t)}subruleInternalRecord(e,t,r){if(aw(t),!e||wr(e,"ruleName")===!1){let s=new Error(` argument is invalid expecting a Parser method reference but got: <${JSON.stringify(e)}> + inside top level rule: <${this.recordingProdStack[0].name}>`);throw s.KNOWN_RECORDER_ERROR=!0,s}let n=Kg(this.recordingProdStack),i=e.ruleName,a=new Gs({idx:t,nonTerminalName:i,label:r?.LABEL,referencedRule:void 0});return n.definition.push(a),this.outputCst?lCt:JR}consumeInternalRecord(e,t,r){if(aw(t),!Iq(e)){let a=new Error(` argument is invalid expecting a TokenType reference but got: <${JSON.stringify(e)}> + inside top level rule: <${this.recordingProdStack[0].name}>`);throw a.KNOWN_RECORDER_ERROR=!0,a}let n=Kg(this.recordingProdStack),i=new ci({idx:t,terminalType:e,label:r?.LABEL});return n.definition.push(i),rAe}};o(nv,"recordProd");E(nv,"recordProd");o(nAe,"recordOrProd");E(nAe,"recordOrProd");o(WG,"getIdxSuffix");E(WG,"getIdxSuffix");o(aw,"assertMethodIdxIsValid");E(aw,"assertMethodIdxIsValid");uCt=class{static{o(this,"PerformanceTracer")}static{E(this,"PerformanceTracer")}initPerformanceTracer(e){if(wr(e,"traceInitPerf")){let t=e.traceInitPerf,r=typeof t=="number";this.traceInitMaxIdent=r?t:1/0,this.traceInitPerf=r?t>0:t}else this.traceInitMaxIdent=0,this.traceInitPerf=Jh.traceInitPerf;this.traceInitIndent=-1}TRACE_INIT(e,t){if(this.traceInitPerf===!0){this.traceInitIndent++;let r=new Array(this.traceInitIndent+1).join(" ");this.traceInitIndent <${e}>`);let{time:n,value:i}=Eq(t),a=n>10?console.warn:console.log;return this.traceInitIndent time: ${n}ms`),this.traceInitIndent--,i}else return t()}};o(iAe,"applyMixins");E(iAe,"applyMixins");tR=_w(mp,"",NaN,NaN,NaN,NaN,NaN,NaN);Object.freeze(tR);Jh=Object.freeze({recoveryEnabled:!1,maxLookahead:3,dynamicTokensEnabled:!1,outputCst:!0,errorMessageProvider:pv,nodeLocationTracking:"none",traceInitPerf:!1,skipValidations:!1}),rR=Object.freeze({recoveryValueFunc:E(()=>{},"recoveryValueFunc"),resyncEnabled:!0});(function(e){e[e.INVALID_RULE_NAME=0]="INVALID_RULE_NAME",e[e.DUPLICATE_RULE_NAME=1]="DUPLICATE_RULE_NAME",e[e.INVALID_RULE_OVERRIDE=2]="INVALID_RULE_OVERRIDE",e[e.DUPLICATE_PRODUCTIONS=3]="DUPLICATE_PRODUCTIONS",e[e.UNRESOLVED_SUBRULE_REF=4]="UNRESOLVED_SUBRULE_REF",e[e.LEFT_RECURSION=5]="LEFT_RECURSION",e[e.NONE_LAST_EMPTY_ALT=6]="NONE_LAST_EMPTY_ALT",e[e.AMBIGUOUS_ALTS=7]="AMBIGUOUS_ALTS",e[e.CONFLICT_TOKENS_RULES_NAMESPACE=8]="CONFLICT_TOKENS_RULES_NAMESPACE",e[e.INVALID_TOKEN_NAME=9]="INVALID_TOKEN_NAME",e[e.NO_NON_EMPTY_LOOKAHEAD=10]="NO_NON_EMPTY_LOOKAHEAD",e[e.AMBIGUOUS_PREFIX_ALTS=11]="AMBIGUOUS_PREFIX_ALTS",e[e.TOO_MANY_ALTS=12]="TOO_MANY_ALTS",e[e.CUSTOM_LOOKAHEAD_VALIDATION=13]="CUSTOM_LOOKAHEAD_VALIDATION"})(Vs||(Vs={}));o(qG,"EMPTY_ALT");E(qG,"EMPTY_ALT");Wq=class aAe{static{o(this,"_Parser")}static{E(this,"Parser")}static performSelfAnalysis(t){throw Error("The **static** `performSelfAnalysis` method has been deprecated. \nUse the **instance** method with the same name instead.")}performSelfAnalysis(){this.TRACE_INIT("performSelfAnalysis",()=>{let t;this.selfAnalysisDone=!0;let r=this.className;this.TRACE_INIT("toFastProps",()=>{Aq(this)}),this.TRACE_INIT("Grammar Recording",()=>{try{this.enableRecording(),Ir(this.definedRulesNames,i=>{let s=this[i].originalGrammarAction,l;this.TRACE_INIT(`${i} Rule`,()=>{l=this.topLevelRuleRecord(i,s)}),this.gastProductionsCache[i]=l})}finally{this.disableRecording()}});let n=[];if(this.TRACE_INIT("Grammar Resolving",()=>{n=P5e({rules:ha(this.gastProductionsCache)}),this.definitionErrors=this.definitionErrors.concat(n)}),this.TRACE_INIT("Grammar Validations",()=>{if(Jn(n)&&this.skipValidations===!1){let i=O5e({rules:ha(this.gastProductionsCache),tokenTypes:ha(this.tokensMap),errMsgProvider:qg,grammarName:r}),a=C5e({lookaheadStrategy:this.lookaheadStrategy,rules:ha(this.gastProductionsCache),tokenTypes:ha(this.tokensMap),grammarName:r});this.definitionErrors=this.definitionErrors.concat(i,a)}}),Jn(this.definitionErrors)&&(this.recoveryEnabled&&this.TRACE_INIT("computeAllProdsFollows",()=>{let i=L3e(ha(this.gastProductionsCache));this.resyncFollows=i}),this.TRACE_INIT("ComputeLookaheadFunctions",()=>{var i,a;(a=(i=this.lookaheadStrategy).initialize)===null||a===void 0||a.call(i,{rules:ha(this.gastProductionsCache)}),this.preComputeLookaheadFunctions(ha(this.gastProductionsCache))})),!aAe.DEFER_DEFINITION_ERRORS_HANDLING&&!Jn(this.definitionErrors))throw t=hr(this.definitionErrors,i=>i.message),new Error(`Parser Definition Errors detected: + ${t.join(` +------------------------------- +`)}`)})}constructor(t,r){this.definitionErrors=[],this.selfAnalysisDone=!1;let n=this;if(n.initErrorHandler(r),n.initLexerAdapter(),n.initLooksAhead(r),n.initRecognizerEngine(t,r),n.initRecoverable(r),n.initTreeBuilder(r),n.initContentAssist(),n.initGastRecorder(r),n.initPerformanceTracer(r),wr(r,"ignoredIssues"))throw new Error(`The IParserConfig property has been deprecated. + Please use the flag on the relevant DSL method instead. + See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#IGNORING_AMBIGUITIES + For further details.`);this.skipValidations=wr(r,"skipValidations")?r.skipValidations:Jh.skipValidations}};Wq.DEFER_DEFINITION_ERRORS_HANDLING=!1;iAe(Wq,[KTt,JTt,rCt,nCt,aCt,iCt,sCt,oCt,cCt,uCt]);hCt=class extends Wq{static{o(this,"EmbeddedActionsParser")}static{E(this,"EmbeddedActionsParser")}constructor(e,t=Jh){let r=Ya(t);r.outputCst=!1,super(e,r)}};o(sAe,"arrayMap2");E(sAe,"arrayMap");oAe=sAe;o(lAe,"listCacheClear2");E(lAe,"listCacheClear");dCt=lAe;o(cAe,"eq2");E(cAe,"eq");uAe=cAe;o(hAe,"assocIndexOf2");E(hAe,"assocIndexOf");e_=hAe,fCt=Array.prototype,pCt=fCt.splice;o(dAe,"listCacheDelete2");E(dAe,"listCacheDelete");mCt=dAe;o(fAe,"listCacheGet2");E(fAe,"listCacheGet");gCt=fAe;o(pAe,"listCacheHas2");E(pAe,"listCacheHas");yCt=pAe;o(mAe,"listCacheSet2");E(mAe,"listCacheSet");vCt=mAe;o(f0,"ListCache2");E(f0,"ListCache");f0.prototype.clear=dCt;f0.prototype.delete=mCt;f0.prototype.get=gCt;f0.prototype.has=yCt;f0.prototype.set=vCt;t_=f0;o(gAe,"stackClear2");E(gAe,"stackClear");xCt=gAe;o(yAe,"stackDelete2");E(yAe,"stackDelete");bCt=yAe;o(vAe,"stackGet2");E(vAe,"stackGet");TCt=vAe;o(xAe,"stackHas2");E(xAe,"stackHas");CCt=xAe,wCt=typeof global=="object"&&global&&global.Object===Object&&global,bAe=wCt,kCt=typeof self=="object"&&self&&self.Object===Object&&self,SCt=bAe||kCt||Function("return this")(),id=SCt,ECt=id.Symbol,$u=ECt,TAe=Object.prototype,ACt=TAe.hasOwnProperty,RCt=TAe.toString,hC=$u?$u.toStringTag:void 0;o(CAe,"getRawTag2");E(CAe,"getRawTag");_Ct=CAe,LCt=Object.prototype,DCt=LCt.toString;o(wAe,"objectToString2");E(wAe,"objectToString");ICt=wAe,MCt="[object Null]",NCt="[object Undefined]",ICe=$u?$u.toStringTag:void 0;o(kAe,"baseGetTag2");E(kAe,"baseGetTag");zv=kAe;o(SAe,"isObject2");E(SAe,"isObject");qq=SAe,PCt="[object AsyncFunction]",OCt="[object Function]",BCt="[object GeneratorFunction]",$Ct="[object Proxy]";o(EAe,"isFunction2");E(EAe,"isFunction");AAe=EAe,FCt=id["__core-js_shared__"],sz=FCt,MCe=(function(){var e=/[^.]+$/.exec(sz&&sz.keys&&sz.keys.IE_PROTO||"");return e?"Symbol(src)_1."+e:""})();o(RAe,"isMasked2");E(RAe,"isMasked");zCt=RAe,GCt=Function.prototype,VCt=GCt.toString;o(_Ae,"toSource2");E(_Ae,"toSource");p0=_Ae,WCt=/[\\^$.*+?()[\]{}|]/g,qCt=/^\[object .+?Constructor\]$/,HCt=Function.prototype,UCt=Object.prototype,YCt=HCt.toString,jCt=UCt.hasOwnProperty,XCt=RegExp("^"+YCt.call(jCt).replace(WCt,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");o(LAe,"baseIsNative2");E(LAe,"baseIsNative");KCt=LAe;o(DAe,"getValue2");E(DAe,"getValue");ZCt=DAe;o(IAe,"getNative2");E(IAe,"getNative");Gv=IAe,QCt=Gv(id,"Map"),sw=QCt,JCt=Gv(Object,"create"),ow=JCt;o(MAe,"hashClear2");E(MAe,"hashClear");ewt=MAe;o(NAe,"hashDelete2");E(NAe,"hashDelete");twt=NAe,rwt="__lodash_hash_undefined__",nwt=Object.prototype,iwt=nwt.hasOwnProperty;o(PAe,"hashGet2");E(PAe,"hashGet");awt=PAe,swt=Object.prototype,owt=swt.hasOwnProperty;o(OAe,"hashHas2");E(OAe,"hashHas");lwt=OAe,cwt="__lodash_hash_undefined__";o(BAe,"hashSet2");E(BAe,"hashSet");uwt=BAe;o(m0,"Hash2");E(m0,"Hash");m0.prototype.clear=ewt;m0.prototype.delete=twt;m0.prototype.get=awt;m0.prototype.has=lwt;m0.prototype.set=uwt;NCe=m0;o($Ae,"mapCacheClear2");E($Ae,"mapCacheClear");hwt=$Ae;o(FAe,"isKeyable2");E(FAe,"isKeyable");dwt=FAe;o(zAe,"getMapData2");E(zAe,"getMapData");r_=zAe;o(GAe,"mapCacheDelete2");E(GAe,"mapCacheDelete");fwt=GAe;o(VAe,"mapCacheGet2");E(VAe,"mapCacheGet");pwt=VAe;o(WAe,"mapCacheHas2");E(WAe,"mapCacheHas");mwt=WAe;o(qAe,"mapCacheSet2");E(qAe,"mapCacheSet");gwt=qAe;o(g0,"MapCache2");E(g0,"MapCache");g0.prototype.clear=hwt;g0.prototype.delete=fwt;g0.prototype.get=pwt;g0.prototype.has=mwt;g0.prototype.set=gwt;n_=g0,ywt=200;o(HAe,"stackSet2");E(HAe,"stackSet");vwt=HAe;o(y0,"Stack2");E(y0,"Stack");y0.prototype.clear=xCt;y0.prototype.delete=bCt;y0.prototype.get=TCt;y0.prototype.has=CCt;y0.prototype.set=vwt;f6=y0,xwt="__lodash_hash_undefined__";o(UAe,"setCacheAdd2");E(UAe,"setCacheAdd");bwt=UAe;o(YAe,"setCacheHas2");E(YAe,"setCacheHas");Twt=YAe;o(lw,"SetCache2");E(lw,"SetCache");lw.prototype.add=lw.prototype.push=bwt;lw.prototype.has=Twt;jAe=lw;o(XAe,"arraySome2");E(XAe,"arraySome");Cwt=XAe;o(KAe,"cacheHas2");E(KAe,"cacheHas");ZAe=KAe,wwt=1,kwt=2;o(QAe,"equalArrays2");E(QAe,"equalArrays");JAe=QAe,Swt=id.Uint8Array,PCe=Swt;o(e6e,"mapToArray2");E(e6e,"mapToArray");Ewt=e6e;o(t6e,"setToArray2");E(t6e,"setToArray");Hq=t6e,Awt=1,Rwt=2,_wt="[object Boolean]",Lwt="[object Date]",Dwt="[object Error]",Iwt="[object Map]",Mwt="[object Number]",Nwt="[object RegExp]",Pwt="[object Set]",Owt="[object String]",Bwt="[object Symbol]",$wt="[object ArrayBuffer]",Fwt="[object DataView]",OCe=$u?$u.prototype:void 0,oz=OCe?OCe.valueOf:void 0;o(r6e,"equalByTag2");E(r6e,"equalByTag");zwt=r6e;o(n6e,"arrayPush2");E(n6e,"arrayPush");i6e=n6e,Gwt=Array.isArray,Ws=Gwt;o(a6e,"baseGetAllKeys2");E(a6e,"baseGetAllKeys");Vwt=a6e;o(s6e,"arrayFilter2");E(s6e,"arrayFilter");o6e=s6e;o(l6e,"stubArray2");E(l6e,"stubArray");Wwt=l6e,qwt=Object.prototype,Hwt=qwt.propertyIsEnumerable,BCe=Object.getOwnPropertySymbols,Uwt=BCe?function(e){return e==null?[]:(e=Object(e),o6e(BCe(e),function(t){return Hwt.call(e,t)}))}:Wwt,Ywt=Uwt;o(c6e,"baseTimes2");E(c6e,"baseTimes");jwt=c6e;o(u6e,"isObjectLike2");E(u6e,"isObjectLike");Lv=u6e,Xwt="[object Arguments]";o(h6e,"baseIsArguments2");E(h6e,"baseIsArguments");$Ce=h6e,d6e=Object.prototype,Kwt=d6e.hasOwnProperty,Zwt=d6e.propertyIsEnumerable,Qwt=$Ce((function(){return arguments})())?$Ce:function(e){return Lv(e)&&Kwt.call(e,"callee")&&!Zwt.call(e,"callee")},i_=Qwt;o(f6e,"stubFalse2");E(f6e,"stubFalse");Jwt=f6e,p6e=typeof exports=="object"&&exports&&!exports.nodeType&&exports,FCe=p6e&&typeof module=="object"&&module&&!module.nodeType&&module,ekt=FCe&&FCe.exports===p6e,zCe=ekt?id.Buffer:void 0,tkt=zCe?zCe.isBuffer:void 0,rkt=tkt||Jwt,nR=rkt,nkt=9007199254740991,ikt=/^(?:0|[1-9]\d*)$/;o(m6e,"isIndex2");E(m6e,"isIndex");g6e=m6e,akt=9007199254740991;o(y6e,"isLength2");E(y6e,"isLength");Uq=y6e,skt="[object Arguments]",okt="[object Array]",lkt="[object Boolean]",ckt="[object Date]",ukt="[object Error]",hkt="[object Function]",dkt="[object Map]",fkt="[object Number]",pkt="[object Object]",mkt="[object RegExp]",gkt="[object Set]",ykt="[object String]",vkt="[object WeakMap]",xkt="[object ArrayBuffer]",bkt="[object DataView]",Tkt="[object Float32Array]",Ckt="[object Float64Array]",wkt="[object Int8Array]",kkt="[object Int16Array]",Skt="[object Int32Array]",Ekt="[object Uint8Array]",Akt="[object Uint8ClampedArray]",Rkt="[object Uint16Array]",_kt="[object Uint32Array]",li={};li[Tkt]=li[Ckt]=li[wkt]=li[kkt]=li[Skt]=li[Ekt]=li[Akt]=li[Rkt]=li[_kt]=!0;li[skt]=li[okt]=li[xkt]=li[lkt]=li[bkt]=li[ckt]=li[ukt]=li[hkt]=li[dkt]=li[fkt]=li[pkt]=li[mkt]=li[gkt]=li[ykt]=li[vkt]=!1;o(v6e,"baseIsTypedArray2");E(v6e,"baseIsTypedArray");Lkt=v6e;o(x6e,"baseUnary2");E(x6e,"baseUnary");Dkt=x6e,b6e=typeof exports=="object"&&exports&&!exports.nodeType&&exports,WC=b6e&&typeof module=="object"&&module&&!module.nodeType&&module,Ikt=WC&&WC.exports===b6e,lz=Ikt&&bAe.process,Mkt=(function(){try{var e=WC&&WC.require&&WC.require("util").types;return e||lz&&lz.binding&&lz.binding("util")}catch{}})(),GCe=Mkt,VCe=GCe&&GCe.isTypedArray,Nkt=VCe?Dkt(VCe):Lkt,Yq=Nkt,Pkt=Object.prototype,Okt=Pkt.hasOwnProperty;o(T6e,"arrayLikeKeys2");E(T6e,"arrayLikeKeys");Bkt=T6e,$kt=Object.prototype;o(C6e,"isPrototype2");E(C6e,"isPrototype");w6e=C6e;o(k6e,"overArg2");E(k6e,"overArg");Fkt=k6e,zkt=Fkt(Object.keys,Object),Gkt=zkt,Vkt=Object.prototype,Wkt=Vkt.hasOwnProperty;o(S6e,"baseKeys2");E(S6e,"baseKeys");E6e=S6e;o(A6e,"isArrayLike2");E(A6e,"isArrayLike");a_=A6e;o(R6e,"keys2");E(R6e,"keys");jq=R6e;o(_6e,"getAllKeys2");E(_6e,"getAllKeys");WCe=_6e,qkt=1,Hkt=Object.prototype,Ukt=Hkt.hasOwnProperty;o(L6e,"equalObjects2");E(L6e,"equalObjects");Ykt=L6e,jkt=Gv(id,"DataView"),HG=jkt,Xkt=Gv(id,"Promise"),UG=Xkt,Kkt=Gv(id,"Set"),bv=Kkt,Zkt=Gv(id,"WeakMap"),YG=Zkt,qCe="[object Map]",Qkt="[object Object]",HCe="[object Promise]",UCe="[object Set]",YCe="[object WeakMap]",jCe="[object DataView]",Jkt=p0(HG),eSt=p0(sw),tSt=p0(UG),rSt=p0(bv),nSt=p0(YG),gg=zv;(HG&&gg(new HG(new ArrayBuffer(1)))!=jCe||sw&&gg(new sw)!=qCe||UG&&gg(UG.resolve())!=HCe||bv&&gg(new bv)!=UCe||YG&&gg(new YG)!=YCe)&&(gg=E(function(e){var t=zv(e),r=t==Qkt?e.constructor:void 0,n=r?p0(r):"";if(n)switch(n){case Jkt:return jCe;case eSt:return qCe;case tSt:return HCe;case rSt:return UCe;case nSt:return YCe}return t},"getTag"));jG=gg,iSt=1,XCe="[object Arguments]",KCe="[object Array]",BA="[object Object]",aSt=Object.prototype,ZCe=aSt.hasOwnProperty;o(D6e,"baseIsEqualDeep2");E(D6e,"baseIsEqualDeep");sSt=D6e;o(Xq,"baseIsEqual2");E(Xq,"baseIsEqual");I6e=Xq,oSt=1,lSt=2;o(M6e,"baseIsMatch2");E(M6e,"baseIsMatch");cSt=M6e;o(N6e,"isStrictComparable2");E(N6e,"isStrictComparable");P6e=N6e;o(O6e,"getMatchData2");E(O6e,"getMatchData");uSt=O6e;o(B6e,"matchesStrictComparable2");E(B6e,"matchesStrictComparable");$6e=B6e;o(F6e,"baseMatches2");E(F6e,"baseMatches");hSt=F6e,dSt="[object Symbol]";o(z6e,"isSymbol2");E(z6e,"isSymbol");s_=z6e,fSt=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,pSt=/^\w*$/;o(G6e,"isKey2");E(G6e,"isKey");Kq=G6e,mSt="Expected a function";o(o_,"memoize2");E(o_,"memoize");o_.Cache=n_;gSt=o_,ySt=500;o(V6e,"memoizeCapped2");E(V6e,"memoizeCapped");vSt=V6e,xSt=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,bSt=/\\(\\)?/g,TSt=vSt(function(e){var t=[];return e.charCodeAt(0)===46&&t.push(""),e.replace(xSt,function(r,n,i,a){t.push(i?a.replace(bSt,"$1"):n||r)}),t}),CSt=TSt,wSt=1/0,QCe=$u?$u.prototype:void 0,JCe=QCe?QCe.toString:void 0;o(Zq,"baseToString2");E(Zq,"baseToString");kSt=Zq;o(W6e,"toString3");E(W6e,"toString");SSt=W6e;o(q6e,"castPath2");E(q6e,"castPath");H6e=q6e,ESt=1/0;o(U6e,"toKey2");E(U6e,"toKey");l_=U6e;o(Y6e,"baseGet2");E(Y6e,"baseGet");j6e=Y6e;o(X6e,"get2");E(X6e,"get");ASt=X6e;o(K6e,"baseHasIn2");E(K6e,"baseHasIn");RSt=K6e;o(Z6e,"hasPath2");E(Z6e,"hasPath");_St=Z6e;o(Q6e,"hasIn2");E(Q6e,"hasIn");LSt=Q6e,DSt=1,ISt=2;o(J6e,"baseMatchesProperty2");E(J6e,"baseMatchesProperty");MSt=J6e;o(eRe,"identity2");E(eRe,"identity");Qq=eRe;o(tRe,"baseProperty2");E(tRe,"baseProperty");NSt=tRe;o(rRe,"basePropertyDeep2");E(rRe,"basePropertyDeep");PSt=rRe;o(nRe,"property2");E(nRe,"property");OSt=nRe;o(iRe,"baseIteratee2");E(iRe,"baseIteratee");c_=iRe;o(aRe,"createBaseFor2");E(aRe,"createBaseFor");BSt=aRe,$St=BSt(),FSt=$St;o(sRe,"baseForOwn2");E(sRe,"baseForOwn");zSt=sRe;o(oRe,"createBaseEach2");E(oRe,"createBaseEach");GSt=oRe,VSt=GSt(zSt),u_=VSt;o(lRe,"baseMap2");E(lRe,"baseMap");WSt=lRe;o(cRe,"map2");E(cRe,"map");Hh=cRe;o(uRe,"baseFilter2");E(uRe,"baseFilter");qSt=uRe;o(hRe,"filter2");E(hRe,"filter");HSt=hRe;o(Qg,"buildATNKey");E(Qg,"buildATNKey");gp=1,USt=2,dRe=4,fRe=5,Iw=7,YSt=8,jSt=9,XSt=10,KSt=11,pRe=12,Jq=class{static{o(this,"AbstractTransition")}static{E(this,"AbstractTransition")}constructor(e){this.target=e}isEpsilon(){return!1}},eH=class extends Jq{static{o(this,"AtomTransition")}static{E(this,"AtomTransition")}constructor(e,t){super(e),this.tokenType=t}},mRe=class extends Jq{static{o(this,"EpsilonTransition")}static{E(this,"EpsilonTransition")}constructor(e){super(e)}isEpsilon(){return!0}},tH=class extends Jq{static{o(this,"RuleTransition")}static{E(this,"RuleTransition")}constructor(e,t,r){super(e),this.rule=t,this.followState=r}isEpsilon(){return!0}};o(gRe,"createATN");E(gRe,"createATN");o(yRe,"createRuleStartAndStopATNStates");E(yRe,"createRuleStartAndStopATNStates");o(rH,"atom");E(rH,"atom");o(vRe,"repetition");E(vRe,"repetition");o(xRe,"repetitionSep");E(xRe,"repetitionSep");o(bRe,"repetitionMandatory");E(bRe,"repetitionMandatory");o(TRe,"repetitionMandatorySep");E(TRe,"repetitionMandatorySep");o(CRe,"alternation");E(CRe,"alternation");o(wRe,"option");E(wRe,"option");o(Cp,"block");E(Cp,"block");o(nH,"plus");E(nH,"plus");o(iH,"star");E(iH,"star");o(kRe,"optional");E(kRe,"optional");o(ad,"defineDecisionState");E(ad,"defineDecisionState");o(v0,"makeAlts");E(v0,"makeAlts");o(SRe,"getProdType2");E(SRe,"getProdType");o(ERe,"makeBlock");E(ERe,"makeBlock");o(h_,"tokenRef");E(h_,"tokenRef");o(ARe,"ruleRef");E(ARe,"ruleRef");o(RRe,"buildRuleHandle");E(RRe,"buildRuleHandle");o($i,"epsilon");E($i,"epsilon");o(da,"newState");E(da,"newState");o(d_,"addTransition");E(d_,"addTransition");o(_Re,"removeState");E(_Re,"removeState");iR={},XG=class{static{o(this,"ATNConfigSet")}static{E(this,"ATNConfigSet")}constructor(){this.map={},this.configs=[]}get size(){return this.configs.length}finalize(){this.map={}}add(e){let t=aH(e);t in this.map||(this.map[t]=this.configs.length,this.configs.push(e))}get elements(){return this.configs}get alts(){return Hh(this.configs,e=>e.alt)}get key(){let e="";for(let t in this.map)e+=t+":";return e}};o(aH,"getATNConfigKey");E(aH,"getATNConfigKey");o(LRe,"baseExtremum");E(LRe,"baseExtremum");ZSt=LRe;o(DRe,"baseLt");E(DRe,"baseLt");QSt=DRe;o(IRe,"min");E(IRe,"min");JSt=IRe,ewe=$u?$u.isConcatSpreadable:void 0;o(MRe,"isFlattenable2");E(MRe,"isFlattenable");eEt=MRe;o(sH,"baseFlatten2");E(sH,"baseFlatten");NRe=sH;o(PRe,"flatMap2");E(PRe,"flatMap");tEt=PRe;o(ORe,"baseFindIndex2");E(ORe,"baseFindIndex");rEt=ORe;o(BRe,"baseIsNaN2");E(BRe,"baseIsNaN");nEt=BRe;o($Re,"strictIndexOf2");E($Re,"strictIndexOf");iEt=$Re;o(FRe,"baseIndexOf2");E(FRe,"baseIndexOf");aEt=FRe;o(zRe,"arrayIncludes2");E(zRe,"arrayIncludes");sEt=zRe;o(GRe,"arrayIncludesWith2");E(GRe,"arrayIncludesWith");oEt=GRe;o(VRe,"noop2");E(VRe,"noop");lEt=VRe,cEt=1/0,uEt=bv&&1/Hq(new bv([,-0]))[1]==cEt?function(e){return new bv(e)}:lEt,hEt=uEt,dEt=200;o(WRe,"baseUniq2");E(WRe,"baseUniq");fEt=WRe;o(qRe,"uniqBy");E(qRe,"uniqBy");pEt=qRe;o(HRe,"flatten2");E(HRe,"flatten");mEt=HRe;o(URe,"arrayEach2");E(URe,"arrayEach");gEt=URe;o(YRe,"castFunction2");E(YRe,"castFunction");yEt=YRe;o(jRe,"forEach2");E(jRe,"forEach");cz=jRe,vEt="[object Map]",xEt="[object Set]",bEt=Object.prototype,TEt=bEt.hasOwnProperty;o(XRe,"isEmpty2");E(XRe,"isEmpty");CEt=XRe;o(KRe,"arrayReduce2");E(KRe,"arrayReduce");wEt=KRe;o(ZRe,"baseReduce2");E(ZRe,"baseReduce");kEt=ZRe;o(QRe,"reduce2");E(QRe,"reduce");twe=QRe;o(JRe,"createDFACache");E(JRe,"createDFACache");e_e=class{static{o(this,"PredicateSet")}static{E(this,"PredicateSet")}constructor(){this.predicates=[]}is(e){return e>=this.predicates.length||this.predicates[e]}set(e,t){this.predicates[e]=t}toString(){let e="",t=this.predicates.length;for(let r=0;rconsole.log(r))}initialize(e){this.atn=gRe(e.rules),this.dfas=t_e(this.atn)}validateAmbiguousAlternationAlternatives(){return[]}validateEmptyOrAlternatives(){return[]}buildLookaheadForAlternation(e){let{prodOccurrence:t,rule:r,hasPredicates:n,dynamicTokensEnabled:i}=e,a=this.dfas,s=this.logging,l=Qg(r,"Alternation",t),h=this.atn.decisionMap[l].decision,d=Hh(PG({maxLookahead:1,occurrence:t,prodType:"Alternation",rule:r}),f=>Hh(f,p=>p[0]));if(KG(d,!1)&&!i){let f=twe(d,(p,m,g)=>(cz(m,y=>{y&&(p[y.tokenTypeIdx]=g,cz(y.categoryMatches,v=>{p[v]=g}))}),p),{});return n?function(p){var m;let g=this.LA(1),y=f[g.tokenTypeIdx];if(p!==void 0&&y!==void 0){let v=(m=p[y])===null||m===void 0?void 0:m.GATE;if(v!==void 0&&v.call(this)===!1)return}return y}:function(){let p=this.LA(1);return f[p.tokenTypeIdx]}}else return n?function(f){let p=new e_e,m=f===void 0?0:f.length;for(let y=0;yHh(f,p=>p[0]));if(KG(d)&&d[0][0]&&!i){let f=d[0],p=mEt(f);if(p.length===1&&CEt(p[0].categoryMatches)){let g=p[0].tokenTypeIdx;return function(){return this.LA(1).tokenTypeIdx===g}}else{let m=twe(p,(g,y)=>(y!==void 0&&(g[y.tokenTypeIdx]=!0,cz(y.categoryMatches,v=>{g[v]=!0})),g),{});return function(){let g=this.LA(1);return m[g.tokenTypeIdx]===!0}}}return function(){let f=p6.call(this,a,h,rwe,s);return typeof f=="object"?!1:f===0}}};o(KG,"isLL1Sequence");E(KG,"isLL1Sequence");o(t_e,"initATNSimulator");E(t_e,"initATNSimulator");o(p6,"adaptivePredict");E(p6,"adaptivePredict");o(r_e,"performLookahead");E(r_e,"performLookahead");o(n_e,"computeLookaheadTarget");E(n_e,"computeLookaheadTarget");o(i_e,"reportLookaheadAmbiguity");E(i_e,"reportLookaheadAmbiguity");o(a_e,"buildAmbiguityError");E(a_e,"buildAmbiguityError");o(s_e,"getProductionDslName2");E(s_e,"getProductionDslName");o(o_e,"buildAdaptivePredictError");E(o_e,"buildAdaptivePredictError");o(l_e,"getExistingTargetState");E(l_e,"getExistingTargetState");o(c_e,"computeReachSet");E(c_e,"computeReachSet");o(u_e,"getReachableTarget");E(u_e,"getReachableTarget");o(h_e,"getUniqueAlt");E(h_e,"getUniqueAlt");o(oH,"newDFAState");E(oH,"newDFAState");o(ZG,"addDFAEdge");E(ZG,"addDFAEdge");o(lH,"addDFAState");E(lH,"addDFAState");o(d_e,"computeStartState");E(d_e,"computeStartState");o(cw,"closure");E(cw,"closure");o(f_e,"getEpsilonTarget");E(f_e,"getEpsilonTarget");o(p_e,"hasConfigInRuleStopState");E(p_e,"hasConfigInRuleStopState");o(m_e,"allConfigsInRuleStopStates");E(m_e,"allConfigsInRuleStopStates");o(g_e,"hasConflictTerminatingPrediction");E(g_e,"hasConflictTerminatingPrediction");o(y_e,"getConflictingAltSets");E(y_e,"getConflictingAltSets");o(v_e,"hasConflictingAltSet");E(v_e,"hasConflictingAltSet");o(x_e,"hasStateAssociatedWithOneAlt");E(x_e,"hasStateAssociatedWithOneAlt");dw();b_e=class{static{o(this,"CstNodeBuilder")}static{E(this,"CstNodeBuilder")}constructor(){this.nodeStack=[]}get current(){return this.nodeStack[this.nodeStack.length-1]??this.rootNode}buildRootNode(e){return this.rootNode=new uH(e),this.rootNode.root=this.rootNode,this.nodeStack=[this.rootNode],this.rootNode}buildCompositeNode(e){let t=new f_;return t.grammarSource=e,t.root=this.rootNode,this.current.content.push(t),this.nodeStack.push(t),t}buildLeafNode(e,t){let r=new aR(e.startOffset,e.image.length,KC(e),e.tokenType,!t);return r.grammarSource=t,r.root=this.rootNode,this.current.content.push(r),r}removeNode(e){let t=e.container;if(t){let r=t.content.indexOf(e);r>=0&&t.content.splice(r,1)}}addHiddenNodes(e){let t=[];for(let i of e){let a=new aR(i.startOffset,i.image.length,KC(i),i.tokenType,!0);a.root=this.rootNode,t.push(a)}let r=this.current,n=!1;if(r.content.length>0){r.content.push(...t);return}for(;r.container;){let i=r.container.content.indexOf(r);if(i>0){r.container.content.splice(i,0,...t),n=!0;break}r=r.container}n||this.rootNode.content.unshift(...t)}construct(e){let t=this.current;typeof e.$type=="string"&&!e.$infix&&(this.current.astNode=e),e.$cstNode=t;let r=this.nodeStack.pop();r?.content.length===0&&this.removeNode(r)}},cH=class{static{o(this,"AbstractCstNode")}static{E(this,"AbstractCstNode")}get hidden(){return!1}get astNode(){let e=typeof this._astNode?.$type=="string"?this._astNode:this.container?.astNode;if(!e)throw new Error("This node has no associated AST element");return e}set astNode(e){this._astNode=e}get text(){return this.root.fullText.substring(this.offset,this.end)}},aR=class extends cH{static{o(this,"LeafCstNodeImpl")}static{E(this,"LeafCstNodeImpl")}get offset(){return this._offset}get length(){return this._length}get end(){return this._offset+this._length}get hidden(){return this._hidden}get tokenType(){return this._tokenType}get range(){return this._range}constructor(e,t,r,n,i=!1){super(),this._hidden=i,this._offset=e,this._tokenType=n,this._length=t,this._range=r}},f_=class extends cH{static{o(this,"CompositeCstNodeImpl")}static{E(this,"CompositeCstNodeImpl")}constructor(){super(...arguments),this.content=new EEt(this)}get offset(){return this.firstNonHiddenNode?.offset??0}get length(){return this.end-this.offset}get end(){return this.lastNonHiddenNode?.end??0}get range(){let e=this.firstNonHiddenNode,t=this.lastNonHiddenNode;if(e&&t){if(this._rangeCache===void 0){let{range:r}=e,{range:n}=t;this._rangeCache={start:r.start,end:n.end.line=0;e--){let t=this.content[e];if(!t.hidden)return t}return this.content[this.content.length-1]}},EEt=class T_e extends Array{static{o(this,"_CstNodeContainer")}static{E(this,"CstNodeContainer")}constructor(t){super(),this.parent=t,Object.setPrototypeOf(this,T_e.prototype)}push(...t){return this.addParents(t),super.push(...t)}unshift(...t){return this.addParents(t),super.unshift(...t)}splice(t,r,...n){return this.addParents(n),super.splice(t,r,...n)}addParents(t){for(let r of t)r.container=this.parent}},uH=class extends f_{static{o(this,"RootCstNodeImpl")}static{E(this,"RootCstNodeImpl")}get text(){return this._text.substring(this.offset,this.end)}get fullText(){return this._text}constructor(e){super(),this._text="",this._text=e??""}},sR=Symbol("Datatype");o(m6,"isDataTypeNode");E(m6,"isDataTypeNode");nwe="\u200B",C_e=E(e=>e.endsWith(nwe)?e:e+nwe,"withRuleSuffix"),hH=class{static{o(this,"AbstractLangiumParser")}static{E(this,"AbstractLangiumParser")}constructor(e){this._unorderedGroups=new Map,this.allRules=new Map,this.lexer=e.parser.Lexer;let t=this.lexer.definition,r=e.LanguageMetaData.mode==="production";e.shared.profilers.LangiumProfiler?.isActive("parsing")?this.wrapper=new REt(t,{...e.parser.ParserConfig,skipValidations:r,errorMessageProvider:e.parser.ParserErrorMessageProvider},e.shared.profilers.LangiumProfiler.createTask("parsing",e.LanguageMetaData.languageId)):this.wrapper=new E_e(t,{...e.parser.ParserConfig,skipValidations:r,errorMessageProvider:e.parser.ParserErrorMessageProvider})}alternatives(e,t){this.wrapper.wrapOr(e,t)}optional(e,t){this.wrapper.wrapOption(e,t)}many(e,t){this.wrapper.wrapMany(e,t)}atLeastOne(e,t){this.wrapper.wrapAtLeastOne(e,t)}getRule(e){return this.allRules.get(e)}isRecording(){return this.wrapper.IS_RECORDING}get unorderedGroups(){return this._unorderedGroups}getRuleStack(){return this.wrapper.RULE_STACK}finalize(){this.wrapper.wrapSelfAnalysis()}},w_e=class extends hH{static{o(this,"LangiumParser")}static{E(this,"LangiumParser")}get current(){return this.stack[this.stack.length-1]}constructor(e){super(e),this.nodeBuilder=new b_e,this.stack=[],this.assignmentMap=new Map,this.operatorPrecedence=new Map,this.linker=e.references.Linker,this.converter=e.parser.ValueConverter,this.astReflection=e.shared.AstReflection}rule(e,t){let r=this.computeRuleType(e),n;Sv(e)&&(n=e.name,this.registerPrecedenceMap(e));let i=this.wrapper.DEFINE_RULE(C_e(e.name),this.startImplementation(r,n,t).bind(this));return this.allRules.set(e.name,i),zs(e)&&e.entry&&(this.mainRule=i),i}registerPrecedenceMap(e){let t=e.name,r=new Map;for(let n=0;n0&&(t=this.construct()),t===void 0)throw new Error("No result from parser");if(this.stack.length>0)throw new Error("Parser stack is not empty after parsing");return t}startImplementation(e,t,r){return n=>{let i=!this.isRecording()&&e!==void 0;if(i){let a={$type:e};this.stack.push(a),e===sR?a.value="":t!==void 0&&(a.$infixName=t)}return r(n),i?this.construct():void 0}}extractHiddenTokens(e){let t=this.lexerResult.hidden;if(!t.length)return[];let r=e.startOffset;for(let n=0;nr)return t.splice(0,n);return t.splice(0,t.length)}consume(e,t,r){let n=this.wrapper.wrapConsume(e,t);if(!this.isRecording()&&this.isValidToken(n)){let i=this.extractHiddenTokens(n);this.nodeBuilder.addHiddenNodes(i);let a=this.nodeBuilder.buildLeafNode(n,r),{assignment:s,crossRef:l}=this.getAssignment(r),u=this.current;if(s){let h=jh(r)?n.image:this.converter.convert(n.image,a);this.assign(s.operator,s.feature,h,a,l)}else if(m6(u)){let h=n.image;jh(r)||(h=this.converter.convert(h,a).toString()),u.value+=h}}}isValidToken(e){return!e.isInsertedInRecovery&&!isNaN(e.startOffset)&&typeof e.endOffset=="number"&&!isNaN(e.endOffset)}subrule(e,t,r,n,i){let a;!this.isRecording()&&!r&&(a=this.nodeBuilder.buildCompositeNode(n));let s;try{s=this.wrapper.wrapSubrule(e,t,i)}finally{this.isRecording()||(s===void 0&&!r&&(s=this.construct()),s!==void 0&&a&&a.length>0&&this.performSubruleAssignment(s,n,a))}}performSubruleAssignment(e,t,r){let{assignment:n,crossRef:i}=this.getAssignment(t);if(n)this.assign(n.operator,n.feature,e,r,i);else if(!n){let a=this.current;if(m6(a))a.value+=e.toString();else if(typeof e=="object"&&e){let l=this.assignWithoutOverride(e,a);this.stack.pop(),this.stack.push(l)}}}action(e,t){if(!this.isRecording()){let r=this.current;if(t.feature&&t.operator){r=this.construct(),this.nodeBuilder.removeNode(r.$cstNode),this.nodeBuilder.buildCompositeNode(t).content.push(r.$cstNode);let i={$type:e};this.stack.push(i),this.assign(t.operator,t.feature,r,r.$cstNode)}else r.$type=e}}construct(){if(this.isRecording())return;let e=this.stack.pop();return this.nodeBuilder.construct(e),"$infixName"in e?this.constructInfix(e,this.operatorPrecedence.get(e.$infixName)):m6(e)?this.converter.convert(e.value,e.$cstNode):(cW(this.astReflection,e),e)}constructInfix(e,t){let r=e.parts;if(!Array.isArray(r)||r.length===0)return;let n=e.operators;if(!Array.isArray(n)||r.length<2)return r[0];let i=0,a=-1;for(let g=0;ga?(a=v.precedence,i=g):v.precedence===a&&(v.rightAssoc||(i=g))}let s=n.slice(0,i),l=n.slice(i+1),u=r.slice(0,i+1),h=r.slice(i+1),d={$infixName:e.$infixName,$type:e.$type,$cstNode:e.$cstNode,parts:u,operators:s},f={$infixName:e.$infixName,$type:e.$type,$cstNode:e.$cstNode,parts:h,operators:l},p=this.constructInfix(d,t),m=this.constructInfix(f,t);return{$type:e.$type,$cstNode:e.$cstNode,left:p,operator:n[i],right:m}}getAssignment(e){if(!this.assignmentMap.has(e)){let t=t0(e,Yh);this.assignmentMap.set(e,{assignment:t,crossRef:t&&n0(t.terminal)?t.terminal.isMulti?"multi":"single":void 0})}return this.assignmentMap.get(e)}assign(e,t,r,n,i){let a=this.current,s;switch(i==="single"&&typeof r=="string"?s=this.linker.buildReference(a,t,n,r):i==="multi"&&typeof r=="string"?s=this.linker.buildMultiReference(a,t,n,r):s=r,e){case"=":{a[t]=s;break}case"?=":{a[t]=!0;break}case"+=":Array.isArray(a[t])||(a[t]=[]),a[t].push(s)}}assignWithoutOverride(e,t){for(let[n,i]of Object.entries(t)){let a=e[n];a===void 0?e[n]=i:Array.isArray(a)&&Array.isArray(i)&&(i.push(...a),e[n]=i)}let r=e.$cstNode;return r&&(r.astNode=void 0,e.$cstNode=void 0),e}get definitionErrors(){return this.wrapper.definitionErrors}},k_e=class{static{o(this,"AbstractParserErrorMessageProvider")}static{E(this,"AbstractParserErrorMessageProvider")}buildMismatchTokenMessage(e){return pv.buildMismatchTokenMessage(e)}buildNotAllInputParsedMessage(e){return pv.buildNotAllInputParsedMessage(e)}buildNoViableAltMessage(e){return pv.buildNoViableAltMessage(e)}buildEarlyExitMessage(e){return pv.buildEarlyExitMessage(e)}},dH=class extends k_e{static{o(this,"LangiumParserErrorMessageProvider")}static{E(this,"LangiumParserErrorMessageProvider")}buildMismatchTokenMessage({expected:e,actual:t}){return`Expecting ${e.LABEL?"`"+e.LABEL+"`":e.name.endsWith(":KW")?`keyword '${e.name.substring(0,e.name.length-3)}'`:`token of type '${e.name}'`} but found \`${t.image}\`.`}buildNotAllInputParsedMessage({firstRedundant:e}){return`Expecting end of file but found \`${e.image}\`.`}},S_e=class extends hH{static{o(this,"LangiumCompletionParser")}static{E(this,"LangiumCompletionParser")}constructor(){super(...arguments),this.tokens=[],this.elementStack=[],this.lastElementStack=[],this.nextTokenIndex=0,this.stackSize=0}action(){}construct(){}parse(e){this.resetState();let t=this.lexer.tokenize(e,{mode:"partial"});return this.tokens=t.tokens,this.wrapper.input=[...this.tokens],this.mainRule.call(this.wrapper,{}),this.unorderedGroups.clear(),{tokens:this.tokens,elementStack:[...this.lastElementStack],tokenIndex:this.nextTokenIndex}}rule(e,t){let r=this.wrapper.DEFINE_RULE(C_e(e.name),this.startImplementation(t).bind(this));return this.allRules.set(e.name,r),e.entry&&(this.mainRule=r),r}resetState(){this.elementStack=[],this.lastElementStack=[],this.nextTokenIndex=0,this.stackSize=0}startImplementation(e){return t=>{let r=this.keepStackSize();try{e(t)}finally{this.resetStackSize(r)}}}removeUnexpectedElements(){this.elementStack.splice(this.stackSize)}keepStackSize(){let e=this.elementStack.length;return this.stackSize=e,e}resetStackSize(e){this.removeUnexpectedElements(),this.stackSize=e}consume(e,t,r){this.wrapper.wrapConsume(e,t),this.isRecording()||(this.lastElementStack=[...this.elementStack,r],this.nextTokenIndex=this.currIdx+1)}subrule(e,t,r,n,i){this.before(n),this.wrapper.wrapSubrule(e,t,i),this.after(n)}before(e){this.isRecording()||this.elementStack.push(e)}after(e){if(!this.isRecording()){let t=this.elementStack.lastIndexOf(e);t>=0&&this.elementStack.splice(t)}}get currIdx(){return this.wrapper.currIdx}},AEt={recoveryEnabled:!0,nodeLocationTracking:"full",skipValidations:!0,errorMessageProvider:new dH},E_e=class extends hCt{static{o(this,"ChevrotainWrapper")}static{E(this,"ChevrotainWrapper")}constructor(e,t){let r=t&&"maxLookahead"in t;super(e,{...AEt,lookaheadStrategy:r?new Gq({maxLookahead:t.maxLookahead}):new SEt({logging:t.skipValidations?()=>{}:void 0}),...t})}get IS_RECORDING(){return this.RECORDING_PHASE}DEFINE_RULE(e,t,r){return this.RULE(e,t,r)}wrapSelfAnalysis(){this.performSelfAnalysis()}wrapConsume(e,t){return this.consume(e,t,void 0)}wrapSubrule(e,t,r){return this.subrule(e,t,{ARGS:[r]})}wrapOr(e,t){this.or(e,t)}wrapOption(e,t){this.option(e,t)}wrapMany(e,t){this.many(e,t)}wrapAtLeastOne(e,t){this.atLeastOne(e,t)}rule(e){return e.call(this,{})}},REt=class extends E_e{static{o(this,"ProfilerWrapper")}static{E(this,"ProfilerWrapper")}constructor(e,t,r){super(e,t),this.task=r}rule(e){this.task.start(),this.task.startSubTask(this.ruleName(e));try{return super.rule(e)}finally{this.task.stopSubTask(this.ruleName(e)),this.task.stop()}}ruleName(e){return e.ruleName}subrule(e,t,r){this.task.startSubTask(this.ruleName(t));try{return super.subrule(e,t,r)}finally{this.task.stopSubTask(this.ruleName(t))}}};o(p_,"createParser");E(p_,"createParser");o(A_e,"buildRules");E(A_e,"buildRules");o(R_e,"buildInfixRule");E(R_e,"buildInfixRule");o(yp,"buildElement");E(yp,"buildElement");o(__e,"buildAction");E(__e,"buildAction");o(L_e,"buildRuleCall");E(L_e,"buildRuleCall");o(D_e,"buildRuleCallPredicate");E(D_e,"buildRuleCallPredicate");o(Cc,"buildPredicate");E(Cc,"buildPredicate");o(I_e,"buildAlternatives");E(I_e,"buildAlternatives");o(M_e,"buildUnorderedGroup");E(M_e,"buildUnorderedGroup");o(N_e,"buildGroup");E(N_e,"buildGroup");o(uw,"getGuardCondition");E(uw,"getGuardCondition");o(fH,"buildCrossReference");E(fH,"buildCrossReference");o(P_e,"buildKeyword");E(P_e,"buildKeyword");o(pH,"wrap");E(pH,"wrap");o(m_,"getRule");E(m_,"getRule");o(O_e,"getRuleName");E(O_e,"getRuleName");o(oR,"getToken");E(oR,"getToken");o(mH,"createCompletionParser");E(mH,"createCompletionParser");o(gH,"createLangiumParser");E(gH,"createLangiumParser");o(yH,"prepareLangiumParser");E(yH,"prepareLangiumParser");g_=class{static{o(this,"DefaultTokenBuilder")}static{E(this,"DefaultTokenBuilder")}constructor(){this.diagnostics=[]}buildTokens(e,t){let r=Bn(_R(e,!1)),n=this.buildTerminalTokens(r),i=this.buildKeywordTokens(r,n,t);return i.push(...n),i}flushLexingReport(e){return{diagnostics:this.popDiagnostics()}}popDiagnostics(){let e=[...this.diagnostics];return this.diagnostics=[],e}buildTerminalTokens(e){return e.filter(Il).filter(t=>!t.fragment).map(t=>this.buildTerminalToken(t)).toArray()}buildTerminalToken(e){let t=vw(e),r=this.requiresCustomPattern(t)?this.regexPatternFunction(t):t,n={name:e.name,PATTERN:r};return typeof r=="function"&&(n.LINE_BREAKS=!0),e.hidden&&(n.GROUP=RR(t)?Fs.SKIPPED:"hidden"),n}requiresCustomPattern(e){return!!(e.flags.includes("u")||e.flags.includes("s"))}regexPatternFunction(e){let t=new RegExp(e,e.flags+"y");return(r,n)=>(t.lastIndex=n,t.exec(r))}buildKeywordTokens(e,t,r){return e.filter(r0).flatMap(n=>rd(n).filter(jh)).distinct(n=>n.value).toArray().sort((n,i)=>i.value.length-n.value.length).map(n=>this.buildKeywordToken(n,t,!!r?.caseInsensitive))}buildKeywordToken(e,t,r){let n=this.buildKeywordPattern(e,r),i={name:e.value,PATTERN:n,LONGER_ALT:this.findLongerAlt(e,t)};return typeof n=="function"&&(i.LINE_BREAKS=!0),i}buildKeywordPattern(e,t){return t?new RegExp(Nv(e.value),"i"):e.value}findLongerAlt(e,t){return t.reduce((r,n)=>{let i=n?.PATTERN;return i?.source&&VW("^"+i.source+"$",e.value)&&r.push(n),r},[])}},vH=class{static{o(this,"DefaultValueConverter")}static{E(this,"DefaultValueConverter")}convert(e,t){let r=t.grammarSource;if(n0(r)&&(r=YW(r)),Xh(r)){let n=r.rule.ref;if(!n)throw new Error("This cst node was not parsed by a rule.");return this.runConverter(n,e,t)}return e}runConverter(e,t,r){switch(e.name.toUpperCase()){case"INT":return Iu.convertInt(t);case"STRING":return Iu.convertString(t);case"ID":return Iu.convertID(t)}switch(rq(e)?.toLowerCase()){case"number":return Iu.convertNumber(t);case"boolean":return Iu.convertBoolean(t);case"bigint":return Iu.convertBigint(t);case"date":return Iu.convertDate(t);default:return t}}};(function(e){function t(h){let d="";for(let f=1;f{this.resolve=r=>(e(r),this),this.reject=r=>(t(r),this)})}},iwe=class QG{static{o(this,"_FullTextDocument")}static{E(this,"FullTextDocument")}constructor(t,r,n,i){this._uri=t,this._languageId=r,this._version=n,this._content=i,this._lineOffsets=void 0}get uri(){return this._uri}get languageId(){return this._languageId}get version(){return this._version}getText(t){if(t){let r=this.offsetAt(t.start),n=this.offsetAt(t.end);return this._content.substring(r,n)}return this._content}update(t,r){for(let n of t)if(QG.isIncremental(n)){let i=TH(n.range),a=this.offsetAt(i.start),s=this.offsetAt(i.end);this._content=this._content.substring(0,a)+n.text+this._content.substring(s,this._content.length);let l=Math.max(i.start.line,0),u=Math.max(i.end.line,0),h=this._lineOffsets,d=JG(n.text,!1,a);if(u-l===d.length)for(let p=0,m=d.length;pt?i=s:n=s+1}let a=n-1;return t=this.ensureBeforeEOL(t,r[a]),{line:a,character:t-r[a]}}offsetAt(t){let r=this.getLineOffsets();if(t.line>=r.length)return this._content.length;if(t.line<0)return 0;let n=r[t.line];if(t.character<=0)return n;let i=t.line+1r&&bH(this._content.charCodeAt(t-1));)t--;return t}get lineCount(){return this.getLineOffsets().length}static isIncremental(t){let r=t;return r!=null&&typeof r.text=="string"&&r.range!==void 0&&(r.rangeLength===void 0||typeof r.rangeLength=="number")}static isFull(t){let r=t;return r!=null&&typeof r.text=="string"&&r.range===void 0&&r.rangeLength===void 0}};(function(e){function t(i,a,s,l){return new iwe(i,a,s,l)}o(t,"create"),E(t,"create"),e.create=t;function r(i,a,s){if(i instanceof iwe)return i.update(a,s),i;throw new Error("TextDocument.update: document must be created by TextDocument.create")}o(r,"update"),E(r,"update"),e.update=r;function n(i,a){let s=i.getText(),l=cR(a.map($_e),(d,f)=>{let p=d.range.start.line-f.range.start.line;return p===0?d.range.start.character-f.range.start.character:p}),u=0,h=[];for(let d of l){let f=i.offsetAt(d.range.start);if(fu&&h.push(s.substring(u,f)),d.newText.length&&h.push(d.newText),u=i.offsetAt(d.range.end)}return h.push(s.substr(u)),h.join("")}o(n,"applyEdits"),E(n,"applyEdits"),e.applyEdits=n})(lR||(lR={}));o(cR,"mergeSort");E(cR,"mergeSort");o(JG,"computeLineOffsets");E(JG,"computeLineOffsets");o(bH,"isEOL");E(bH,"isEOL");o(TH,"getWellformedRange");E(TH,"getWellformedRange");o($_e,"getWellformedEdit");E($_e,"getWellformedEdit");(()=>{"use strict";var e={975:I=>{function _(D){if(typeof D!="string")throw new TypeError("Path must be a string. Received "+JSON.stringify(D))}o(_,"e2"),E(_,"e");function A(D,P){for(var B,O="",$=0,V=-1,G=0,z=0;z<=D.length;++z){if(z2){var W=O.lastIndexOf("/");if(W!==O.length-1){W===-1?(O="",$=0):$=(O=O.slice(0,W)).length-1-O.lastIndexOf("/"),V=z,G=0;continue}}else if(O.length===2||O.length===1){O="",$=0,V=z,G=0;continue}}P&&(O.length>0?O+="/..":O="..",$=2)}else O.length>0?O+="/"+D.slice(V+1,z):O=D.slice(V+1,z),$=z-V-1;V=z,G=0}else B===46&&G!==-1?++G:G=-1}return O}o(A,"r2"),E(A,"r");var M={resolve:E(function(){for(var D,P="",B=!1,O=arguments.length-1;O>=-1&&!B;O--){var $;O>=0?$=arguments[O]:(D===void 0&&(D=process.cwd()),$=D),_($),$.length!==0&&(P=$+"/"+P,B=$.charCodeAt(0)===47)}return P=A(P,!B),B?P.length>0?"/"+P:"/":P.length>0?P:"."},"resolve"),normalize:E(function(D){if(_(D),D.length===0)return".";var P=D.charCodeAt(0)===47,B=D.charCodeAt(D.length-1)===47;return(D=A(D,!P)).length!==0||P||(D="."),D.length>0&&B&&(D+="/"),P?"/"+D:D},"normalize"),isAbsolute:E(function(D){return _(D),D.length>0&&D.charCodeAt(0)===47},"isAbsolute"),join:E(function(){if(arguments.length===0)return".";for(var D,P=0;P0&&(D===void 0?D=B:D+="/"+B)}return D===void 0?".":M.normalize(D)},"join"),relative:E(function(D,P){if(_(D),_(P),D===P||(D=M.resolve(D))===(P=M.resolve(P)))return"";for(var B=1;Bz){if(P.charCodeAt(V+H)===47)return P.slice(V+H+1);if(H===0)return P.slice(V+H)}else $>z&&(D.charCodeAt(B+H)===47?W=H:H===0&&(W=0));break}var j=D.charCodeAt(B+H);if(j!==P.charCodeAt(V+H))break;j===47&&(W=H)}var Q="";for(H=B+W+1;H<=O;++H)H!==O&&D.charCodeAt(H)!==47||(Q.length===0?Q+="..":Q+="/..");return Q.length>0?Q+P.slice(V+W):(V+=W,P.charCodeAt(V)===47&&++V,P.slice(V))},"relative"),_makeLong:E(function(D){return D},"_makeLong"),dirname:E(function(D){if(_(D),D.length===0)return".";for(var P=D.charCodeAt(0),B=P===47,O=-1,$=!0,V=D.length-1;V>=1;--V)if((P=D.charCodeAt(V))===47){if(!$){O=V;break}}else $=!1;return O===-1?B?"/":".":B&&O===1?"//":D.slice(0,O)},"dirname"),basename:E(function(D,P){if(P!==void 0&&typeof P!="string")throw new TypeError('"ext" argument must be a string');_(D);var B,O=0,$=-1,V=!0;if(P!==void 0&&P.length>0&&P.length<=D.length){if(P.length===D.length&&P===D)return"";var G=P.length-1,z=-1;for(B=D.length-1;B>=0;--B){var W=D.charCodeAt(B);if(W===47){if(!V){O=B+1;break}}else z===-1&&(V=!1,z=B+1),G>=0&&(W===P.charCodeAt(G)?--G==-1&&($=B):(G=-1,$=z))}return O===$?$=z:$===-1&&($=D.length),D.slice(O,$)}for(B=D.length-1;B>=0;--B)if(D.charCodeAt(B)===47){if(!V){O=B+1;break}}else $===-1&&(V=!1,$=B+1);return $===-1?"":D.slice(O,$)},"basename"),extname:E(function(D){_(D);for(var P=-1,B=0,O=-1,$=!0,V=0,G=D.length-1;G>=0;--G){var z=D.charCodeAt(G);if(z!==47)O===-1&&($=!1,O=G+1),z===46?P===-1?P=G:V!==1&&(V=1):P!==-1&&(V=-1);else if(!$){B=G+1;break}}return P===-1||O===-1||V===0||V===1&&P===O-1&&P===B+1?"":D.slice(P,O)},"extname"),format:E(function(D){if(D===null||typeof D!="object")throw new TypeError('The "pathObject" argument must be of type Object. Received type '+typeof D);return(function(P,B){var O=B.dir||B.root,$=B.base||(B.name||"")+(B.ext||"");return O?O===B.root?O+$:O+"/"+$:$})(0,D)},"format"),parse:E(function(D){_(D);var P={root:"",dir:"",base:"",ext:"",name:""};if(D.length===0)return P;var B,O=D.charCodeAt(0),$=O===47;$?(P.root="/",B=1):B=0;for(var V=-1,G=0,z=-1,W=!0,H=D.length-1,j=0;H>=B;--H)if((O=D.charCodeAt(H))!==47)z===-1&&(W=!1,z=H+1),O===46?V===-1?V=H:j!==1&&(j=1):V!==-1&&(j=-1);else if(!W){G=H+1;break}return V===-1||z===-1||j===0||j===1&&V===z-1&&V===G+1?z!==-1&&(P.base=P.name=G===0&&$?D.slice(1,z):D.slice(G,z)):(G===0&&$?(P.name=D.slice(1,V),P.base=D.slice(1,z)):(P.name=D.slice(G,V),P.base=D.slice(G,z)),P.ext=D.slice(V,z)),G>0?P.dir=D.slice(0,G-1):$&&(P.dir="/"),P},"parse"),sep:"/",delimiter:":",win32:null,posix:null};M.posix=M,I.exports=M}},t={};function r(I){var _=t[I];if(_!==void 0)return _.exports;var A=t[I]={exports:{}};return e[I](A,A.exports,r),A.exports}o(r,"r"),E(r,"r"),r.d=(I,_)=>{for(var A in _)r.o(_,A)&&!r.o(I,A)&&Object.defineProperty(I,A,{enumerable:!0,get:_[A]})},r.o=(I,_)=>Object.prototype.hasOwnProperty.call(I,_),r.r=I=>{typeof Symbol<"u"&&Symbol.toStringTag&&Object.defineProperty(I,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(I,"__esModule",{value:!0})};var n={};let i;r.r(n),r.d(n,{URI:E(()=>p,"URI"),Utils:E(()=>N,"Utils")}),typeof process=="object"?i=process.platform==="win32":typeof navigator=="object"&&(i=navigator.userAgent.indexOf("Windows")>=0);let a=/^\w[\w\d+.-]*$/,s=/^\//,l=/^\/\//;function u(I,_){if(!I.scheme&&_)throw new Error(`[UriError]: Scheme is missing: {scheme: "", authority: "${I.authority}", path: "${I.path}", query: "${I.query}", fragment: "${I.fragment}"}`);if(I.scheme&&!a.test(I.scheme))throw new Error("[UriError]: Scheme contains illegal characters.");if(I.path){if(I.authority){if(!s.test(I.path))throw new Error('[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character')}else if(l.test(I.path))throw new Error('[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")')}}o(u,"a"),E(u,"a");let h="",d="/",f=/^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;class p{static{o(this,"l")}static{E(this,"l")}static isUri(_){return _ instanceof p||!!_&&typeof _.authority=="string"&&typeof _.fragment=="string"&&typeof _.path=="string"&&typeof _.query=="string"&&typeof _.scheme=="string"&&typeof _.fsPath=="string"&&typeof _.with=="function"&&typeof _.toString=="function"}scheme;authority;path;query;fragment;constructor(_,A,M,D,P,B=!1){typeof _=="object"?(this.scheme=_.scheme||h,this.authority=_.authority||h,this.path=_.path||h,this.query=_.query||h,this.fragment=_.fragment||h):(this.scheme=(function(O,$){return O||$?O:"file"})(_,B),this.authority=A||h,this.path=(function(O,$){switch(O){case"https":case"http":case"file":$?$[0]!==d&&($=d+$):$=d}return $})(this.scheme,M||h),this.query=D||h,this.fragment=P||h,u(this,B))}get fsPath(){return b(this,!1)}with(_){if(!_)return this;let{scheme:A,authority:M,path:D,query:P,fragment:B}=_;return A===void 0?A=this.scheme:A===null&&(A=h),M===void 0?M=this.authority:M===null&&(M=h),D===void 0?D=this.path:D===null&&(D=h),P===void 0?P=this.query:P===null&&(P=h),B===void 0?B=this.fragment:B===null&&(B=h),A===this.scheme&&M===this.authority&&D===this.path&&P===this.query&&B===this.fragment?this:new g(A,M,D,P,B)}static parse(_,A=!1){let M=f.exec(_);return M?new g(M[2]||h,w(M[4]||h),w(M[5]||h),w(M[7]||h),w(M[9]||h),A):new g(h,h,h,h,h)}static file(_){let A=h;if(i&&(_=_.replace(/\\/g,d)),_[0]===d&&_[1]===d){let M=_.indexOf(d,2);M===-1?(A=_.substring(2),_=d):(A=_.substring(2,M),_=_.substring(M)||d)}return new g("file",A,_,h,h)}static from(_){let A=new g(_.scheme,_.authority,_.path,_.query,_.fragment);return u(A,!0),A}toString(_=!1){return T(this,_)}toJSON(){return this}static revive(_){if(_){if(_ instanceof p)return _;{let A=new g(_);return A._formatted=_.external,A._fsPath=_._sep===m?_.fsPath:null,A}}return _}}let m=i?1:void 0;class g extends p{static{o(this,"d")}static{E(this,"d")}_formatted=null;_fsPath=null;get fsPath(){return this._fsPath||(this._fsPath=b(this,!1)),this._fsPath}toString(_=!1){return _?T(this,!0):(this._formatted||(this._formatted=T(this,!1)),this._formatted)}toJSON(){let _={$mid:1};return this._fsPath&&(_.fsPath=this._fsPath,_._sep=m),this._formatted&&(_.external=this._formatted),this.path&&(_.path=this.path),this.scheme&&(_.scheme=this.scheme),this.authority&&(_.authority=this.authority),this.query&&(_.query=this.query),this.fragment&&(_.fragment=this.fragment),_}}let y={58:"%3A",47:"%2F",63:"%3F",35:"%23",91:"%5B",93:"%5D",64:"%40",33:"%21",36:"%24",38:"%26",39:"%27",40:"%28",41:"%29",42:"%2A",43:"%2B",44:"%2C",59:"%3B",61:"%3D",32:"%20"};function v(I,_,A){let M,D=-1;for(let P=0;P=97&&B<=122||B>=65&&B<=90||B>=48&&B<=57||B===45||B===46||B===95||B===126||_&&B===47||A&&B===91||A&&B===93||A&&B===58)D!==-1&&(M+=encodeURIComponent(I.substring(D,P)),D=-1),M!==void 0&&(M+=I.charAt(P));else{M===void 0&&(M=I.substr(0,P));let O=y[B];O!==void 0?(D!==-1&&(M+=encodeURIComponent(I.substring(D,P)),D=-1),M+=O):D===-1&&(D=P)}}return D!==-1&&(M+=encodeURIComponent(I.substring(D))),M!==void 0?M:I}o(v,"m"),E(v,"m");function x(I){let _;for(let A=0;A1&&I.scheme==="file"?`//${I.authority}${I.path}`:I.path.charCodeAt(0)===47&&(I.path.charCodeAt(1)>=65&&I.path.charCodeAt(1)<=90||I.path.charCodeAt(1)>=97&&I.path.charCodeAt(1)<=122)&&I.path.charCodeAt(2)===58?_?I.path.substr(1):I.path[1].toLowerCase()+I.path.substr(2):I.path,i&&(A=A.replace(/\//g,"\\")),A}o(b,"v"),E(b,"v");function T(I,_){let A=_?x:v,M="",{scheme:D,authority:P,path:B,query:O,fragment:$}=I;if(D&&(M+=D,M+=":"),(P||D==="file")&&(M+=d,M+=d),P){let V=P.indexOf("@");if(V!==-1){let G=P.substr(0,V);P=P.substr(V+1),V=G.lastIndexOf(":"),V===-1?M+=A(G,!1,!1):(M+=A(G.substr(0,V),!1,!1),M+=":",M+=A(G.substr(V+1),!1,!0)),M+="@"}P=P.toLowerCase(),V=P.lastIndexOf(":"),V===-1?M+=A(P,!1,!0):(M+=A(P.substr(0,V),!1,!0),M+=P.substr(V))}if(B){if(B.length>=3&&B.charCodeAt(0)===47&&B.charCodeAt(2)===58){let V=B.charCodeAt(1);V>=65&&V<=90&&(B=`/${String.fromCharCode(V+32)}:${B.substr(3)}`)}else if(B.length>=2&&B.charCodeAt(1)===58){let V=B.charCodeAt(0);V>=65&&V<=90&&(B=`${String.fromCharCode(V+32)}:${B.substr(2)}`)}M+=A(B,!0,!1)}return O&&(M+="?",M+=A(O,!1,!1)),$&&(M+="#",M+=_?$:v($,!1,!1)),M}o(T,"b"),E(T,"b");function k(I){try{return decodeURIComponent(I)}catch{return I.length>3?I.substr(0,3)+k(I.substr(3)):I}}o(k,"C"),E(k,"C");let C=/(%[0-9A-Za-z][0-9A-Za-z])+/g;function w(I){return I.match(C)?I.replace(C,(_=>k(_))):I}o(w,"w"),E(w,"w");var S=r(975);let R=S.posix||S,L="/";var N;(function(I){I.joinPath=function(_,...A){return _.with({path:R.join(_.path,...A)})},I.resolvePath=function(_,...A){let M=_.path,D=!1;M[0]!==L&&(M=L+M,D=!0);let P=R.resolve(M,...A);return D&&P[0]===L&&!_.authority&&(P=P.substring(1)),_.with({path:P})},I.dirname=function(_){if(_.path.length===0||_.path===L)return _;let A=R.dirname(_.path);return A.length===1&&A.charCodeAt(0)===46&&(A=""),_.with({path:A})},I.basename=function(_){return R.basename(_.path)},I.extname=function(_){return R.extname(_.path)}})(N||(N={})),F_e=n})();({URI:Yo,Utils:dC}=F_e);(function(e){e.basename=dC.basename,e.dirname=dC.dirname,e.extname=dC.extname,e.joinPath=dC.joinPath,e.resolvePath=dC.resolvePath;let t=typeof process=="object"&&process?.platform==="win32";function r(s,l){return s?.toString()===l?.toString()}o(r,"equals"),E(r,"equals"),e.equals=r;function n(s,l){let u=typeof s=="string"?Yo.parse(s).path:s.path,h=typeof l=="string"?Yo.parse(l).path:l.path,d=u.split("/").filter(y=>y.length>0),f=h.split("/").filter(y=>y.length>0);if(t){let y=/^[A-Z]:$/;if(d[0]&&y.test(d[0])&&(d[0]=d[0].toLowerCase()),f[0]&&y.test(f[0])&&(f[0]=f[0].toLowerCase()),d[0]!==f[0])return h.substring(1)}let p=0;for(;p({name:n.name,uri:$s.joinPath(Yo.parse(t),n.name).toString(),element:n.element})):[]}all(){return this.collectValues(this.root)}findAll(e){let t=this.getNode($s.normalize(e),!1);return t?this.collectValues(t):[]}getNode(e,t){let r=e.split("/");e.charAt(e.length-1)==="/"&&r.pop();let n=this.root;for(let i of r){let a=n.children.get(i);if(!a)if(t)a={name:i,children:new Map,parent:n},n.children.set(i,a);else return;n=a}return n}collectValues(e){let t=[];e.element&&t.push(e.element);for(let r of e.children.values())t.push(...this.collectValues(r));return t}};(function(e){e[e.Changed=0]="Changed",e[e.Parsed=1]="Parsed",e[e.IndexedContent=2]="IndexedContent",e[e.ComputedScopes=3]="ComputedScopes",e[e.Linked=4]="Linked",e[e.IndexedReferences=5]="IndexedReferences",e[e.Validated=6]="Validated"})(en||(en={}));z_e=class{static{o(this,"DefaultLangiumDocumentFactory")}static{E(this,"DefaultLangiumDocumentFactory")}constructor(e){this.serviceRegistry=e.ServiceRegistry,this.textDocuments=e.workspace.TextDocuments,this.fileSystemProvider=e.workspace.FileSystemProvider}async fromUri(e,t=Qn.CancellationToken.None){let r=await this.fileSystemProvider.readFile(e);return this.createAsync(e,r,t)}fromTextDocument(e,t,r){return t=t??Yo.parse(e.uri),Qn.CancellationToken.is(r)?this.createAsync(t,e,r):this.create(t,e,r)}fromString(e,t,r){return Qn.CancellationToken.is(r)?this.createAsync(t,e,r):this.create(t,e,r)}fromModel(e,t){return this.create(t,{$model:e})}create(e,t,r){if(typeof t=="string"){let n=this.parse(e,t,r);return this.createLangiumDocument(n,e,void 0,t)}else if("$model"in t){let n={value:t.$model,parserErrors:[],lexerErrors:[]};return this.createLangiumDocument(n,e)}else{let n=this.parse(e,t.getText(),r);return this.createLangiumDocument(n,e,t)}}async createAsync(e,t,r){if(typeof t=="string"){let n=await this.parseAsync(e,t,r);return this.createLangiumDocument(n,e,void 0,t)}else{let n=await this.parseAsync(e,t.getText(),r);return this.createLangiumDocument(n,e,t)}}createLangiumDocument(e,t,r,n){let i;if(r)i={parseResult:e,uri:t,state:en.Parsed,references:[],textDocument:r};else{let a=this.createTextDocumentGetter(t,n);i={parseResult:e,uri:t,state:en.Parsed,references:[],get textDocument(){return a()}}}return e.value.$document=i,i}async update(e,t){let r=e.parseResult.value.$cstNode?.root.fullText,n=this.textDocuments?.get(e.uri.toString()),i=n?n.getText():await this.fileSystemProvider.readFile(e.uri);if(n)Object.defineProperty(e,"textDocument",{value:n});else{let a=this.createTextDocumentGetter(e.uri,i);Object.defineProperty(e,"textDocument",{get:a})}return r!==i&&(e.parseResult=await this.parseAsync(e.uri,i,t),e.parseResult.value.$document=e),e.state=en.Parsed,e}parse(e,t,r){return this.serviceRegistry.getServices(e).parser.LangiumParser.parse(t,r)}parseAsync(e,t,r){return this.serviceRegistry.getServices(e).parser.AsyncParser.parse(t,r)}createTextDocumentGetter(e,t){let r=this.serviceRegistry,n;return()=>n??(n=lR.create(e.toString(),r.getServices(e).LanguageMetaData.languageId,0,t??""))}},G_e=class{static{o(this,"DefaultLangiumDocuments")}static{E(this,"DefaultLangiumDocuments")}constructor(e){this.documentTrie=new CH,this.services=e,this.langiumDocumentFactory=e.workspace.LangiumDocumentFactory,this.documentBuilder=()=>e.workspace.DocumentBuilder}get all(){return Bn(this.documentTrie.all())}addDocument(e){let t=e.uri.toString();if(this.documentTrie.has(t))throw new Error(`A document with the URI '${t}' is already present.`);this.documentTrie.insert(t,e)}getDocument(e){let t=e.toString();return this.documentTrie.find(t)}getDocuments(e){let t=e.toString();return this.documentTrie.findAll(t)}async getOrCreateDocument(e,t){let r=this.getDocument(e);return r||(r=await this.langiumDocumentFactory.fromUri(e,t),this.addDocument(r),r)}createDocument(e,t,r){if(r)return this.langiumDocumentFactory.fromString(t,e,r).then(n=>(this.addDocument(n),n));{let n=this.langiumDocumentFactory.fromString(t,e);return this.addDocument(n),n}}hasDocument(e){return this.documentTrie.has(e.toString())}invalidateDocument(e){let t=e.toString(),r=this.documentTrie.find(t);return r&&this.documentBuilder().resetToState(r,en.Changed),r}deleteDocument(e){let t=e.toString(),r=this.documentTrie.find(t);return r&&(r.state=en.Changed,this.documentTrie.delete(t)),r}deleteDocuments(e){let t=e.toString(),r=this.documentTrie.findAll(t);for(let n of r)n.state=en.Changed;return this.documentTrie.delete(t),r}},yg=Symbol("RefResolving"),V_e=class{static{o(this,"DefaultLinker")}static{E(this,"DefaultLinker")}constructor(e){this.reflection=e.shared.AstReflection,this.langiumDocuments=()=>e.shared.workspace.LangiumDocuments,this.scopeProvider=e.references.ScopeProvider,this.astNodeLocator=e.workspace.AstNodeLocator,this.profiler=e.shared.profilers.LangiumProfiler,this.languageId=e.LanguageMetaData.languageId}async link(e,t=Qn.CancellationToken.None){if(this.profiler?.isActive("linking")){let r=this.profiler.createTask("linking",this.languageId);r.start();try{for(let n of kc(e.parseResult.value))await Ia(t),kv(n).forEach(i=>{let a=`${n.$type}:${i.property}`;r.startSubTask(a);try{this.doLink(i,e)}finally{r.stopSubTask(a)}})}finally{r.stop()}}else for(let r of kc(e.parseResult.value))await Ia(t),kv(r).forEach(n=>this.doLink(n,e))}doLink(e,t){let r=e.reference;if("_ref"in r&&r._ref===void 0){r._ref=yg;try{let n=this.getCandidate(e);if(Cg(n))r._ref=n;else{r._nodeDescription=n;let i=this.loadAstNode(n);r._ref=i??this.createLinkingError(e,n)}}catch(n){console.error(`An error occurred while resolving reference to '${r.$refText}':`,n);let i=n.message??String(n);r._ref={info:e,message:`An error occurred while resolving reference to '${r.$refText}': ${i}`}}t.references.push(r)}else if("_items"in r&&r._items===void 0){r._items=yg;try{let n=this.getCandidates(e),i=[];if(Cg(n))r._linkingError=n;else for(let a of n){let s=this.loadAstNode(a);s&&i.push({ref:s,$nodeDescription:a})}r._items=i}catch(n){r._linkingError={info:e,message:`An error occurred while resolving reference to '${r.$refText}': ${n}`},r._items=[]}t.references.push(r)}}unlink(e){for(let t of e.references)"_ref"in t?(t._ref=void 0,delete t._nodeDescription):"_items"in t&&(t._items=void 0,delete t._linkingError);e.references=[]}getCandidate(e){return this.scopeProvider.getScope(e).getElement(e.reference.$refText)??this.createLinkingError(e)}getCandidates(e){let r=this.scopeProvider.getScope(e).getElements(e.reference.$refText).distinct(n=>`${n.documentUri}#${n.path}`).toArray();return r.length>0?r:this.createLinkingError(e)}buildReference(e,t,r,n){let i=this,a={$refNode:r,$refText:n,_ref:void 0,get ref(){if(Zi(this._ref))return this._ref;if(sW(this._nodeDescription)){let s=i.loadAstNode(this._nodeDescription);this._ref=s??i.createLinkingError({reference:a,container:e,property:t},this._nodeDescription)}else if(this._ref===void 0){this._ref=yg;let s=yv(e).$document,l=i.getLinkedNode({reference:a,container:e,property:t});if(l.error&&s&&s.state0))return this._linkingError=i.createLinkingError({reference:a,container:e,property:t})}};return a}throwCyclicReferenceError(e,t,r){throw new Error(`Cyclic reference resolution detected: ${this.astNodeLocator.getAstNodePath(e)}/${t} (symbol '${r}')`)}getLinkedNode(e){try{let t=this.getCandidate(e);if(Cg(t))return{error:t};let r=this.loadAstNode(t);return r?{node:r,descr:t}:{descr:t,error:this.createLinkingError(e,t)}}catch(t){console.error(`An error occurred while resolving reference to '${e.reference.$refText}':`,t);let r=t.message??String(t);return{error:{info:e,message:`An error occurred while resolving reference to '${e.reference.$refText}': ${r}`}}}}loadAstNode(e){if(e.node)return e.node;let t=this.langiumDocuments().getDocument(e.documentUri);if(t)return this.astNodeLocator.getAstNode(t.parseResult.value,e.path)}createLinkingError(e,t){let r=yv(e.container).$document;r&&r.staten0(t)&&t.isMulti)}findDeclarations(e){if(e){let t=ZW(e),r=e.astNode;if(t&&r){let n=r[t.feature];if(Bs(n)||Ou(n))return A6(n);if(Array.isArray(n)){for(let i of n)if((Bs(i)||Ou(i))&&i.$refNode&&i.$refNode.offset<=e.offset&&i.$refNode.end>=e.end)return A6(i)}}if(r){let n=this.nameProvider.getNameNode(r);if(n&&(n===e||LW(e,n)))return this.getSelfNodes(r)}}return[]}getSelfNodes(e){if(this.hasMultiReference){let t=this.index.findAllReferences(e,this.nodeLocator.getAstNodePath(e)),r=this.getNodeFromReferenceDescription(t.head());if(r){for(let n of kv(r))if(Ou(n.reference)&&n.reference.items.some(i=>i.ref===e))return n.reference.items.map(i=>i.ref)}return[e]}else return[e]}getNodeFromReferenceDescription(e){if(!e)return;let t=this.documents.getDocument(e.sourceUri);if(t)return this.nodeLocator.getAstNode(t.parseResult.value,e.sourcePath)}findDeclarationNodes(e){let t=this.findDeclarations(e),r=[];for(let n of t){let i=this.nameProvider.getNameNode(n)??n.$cstNode;i&&r.push(i)}return r}findReferences(e,t){let r=[];t.includeDeclaration&&r.push(...this.getSelfReferences(e));let n=this.index.findAllReferences(e,this.nodeLocator.getAstNodePath(e));return t.documentUri&&(n=n.filter(i=>$s.equals(i.sourceUri,t.documentUri))),r.push(...n),Bn(r)}getSelfReferences(e){let t=this.getSelfNodes(e),r=[];for(let n of t){let i=this.nameProvider.getNameNode(n);if(i){let a=wc(n),s=this.nodeLocator.getAstNodePath(n);r.push({sourceUri:a.uri,sourcePath:s,targetUri:a.uri,targetPath:s,segment:Av(i),local:!0})}}return r}},td=class{static{o(this,"MultiMap")}static{E(this,"MultiMap")}constructor(e){if(this.map=new Map,e)for(let[t,r]of e)this.add(t,r)}get size(){return XC.sum(Bn(this.map.values()).map(e=>e.length))}clear(){this.map.clear()}delete(e,t){if(t===void 0)return this.map.delete(e);{let r=this.map.get(e);if(r){let n=r.indexOf(t);if(n>=0)return r.length===1?this.map.delete(e):r.splice(n,1),!0}return!1}}get(e){return this.map.get(e)??[]}getStream(e){let t=this.map.get(e);return t?Bn(t):Tv}has(e,t){if(t===void 0)return this.map.has(e);{let r=this.map.get(e);return r?r.indexOf(t)>=0:!1}}add(e,t){return this.map.has(e)?this.map.get(e).push(t):this.map.set(e,[t]),this}addAll(e,t){return this.map.has(e)?this.map.get(e).push(...t):this.map.set(e,Array.from(t)),this}forEach(e){this.map.forEach((t,r)=>t.forEach(n=>e(n,r,this)))}[Symbol.iterator](){return this.entries().iterator()}entries(){return Bn(this.map.entries()).flatMap(([e,t])=>t.map(r=>[e,r]))}keys(){return Bn(this.map.keys())}values(){return Bn(this.map.values()).flat()}entriesGroupedByKey(){return Bn(this.map.entries())}},uR=class{static{o(this,"BiMap")}static{E(this,"BiMap")}get size(){return this.map.size}constructor(e){if(this.map=new Map,this.inverse=new Map,e)for(let[t,r]of e)this.set(t,r)}clear(){this.map.clear(),this.inverse.clear()}set(e,t){return this.map.set(e,t),this.inverse.set(t,e),this}get(e){return this.map.get(e)}getKey(e){return this.inverse.get(e)}delete(e){let t=this.map.get(e);return t!==void 0?(this.map.delete(e),this.inverse.delete(t),!0):!1}},H_e=class{static{o(this,"DefaultScopeComputation")}static{E(this,"DefaultScopeComputation")}constructor(e){this.nameProvider=e.references.NameProvider,this.descriptions=e.workspace.AstNodeDescriptionProvider}async collectExportedSymbols(e,t=Qn.CancellationToken.None){return this.collectExportedSymbolsForNode(e.parseResult.value,e,void 0,t)}async collectExportedSymbolsForNode(e,t,r=pw,n=Qn.CancellationToken.None){let i=[];this.addExportedSymbol(e,i,t);for(let a of r(e))await Ia(n),this.addExportedSymbol(a,i,t);return i}addExportedSymbol(e,t,r){let n=this.nameProvider.getName(e);n&&t.push(this.descriptions.createDescription(e,n,r))}async collectLocalSymbols(e,t=Qn.CancellationToken.None){let r=e.parseResult.value,n=new td;for(let i of rd(r))await Ia(t),this.addLocalSymbol(i,e,n);return n}addLocalSymbol(e,t,r){let n=e.$container;if(n){let i=this.nameProvider.getName(e);i&&r.add(n,this.descriptions.createDescription(e,i,t))}}},eV=class{static{o(this,"StreamScope")}static{E(this,"StreamScope")}constructor(e,t,r){this.elements=e,this.outerScope=t,this.caseInsensitive=r?.caseInsensitive??!1,this.concatOuterScope=r?.concatOuterScope??!0}getAllElements(){return this.outerScope?this.elements.concat(this.outerScope.getAllElements()):this.elements}getElement(e){let t=this.caseInsensitive?e.toLowerCase():e,r=this.caseInsensitive?this.elements.find(n=>n.name.toLowerCase()===t):this.elements.find(n=>n.name===e);if(r)return r;if(this.outerScope)return this.outerScope.getElement(e)}getElements(e){let t=this.caseInsensitive?e.toLowerCase():e,r=this.caseInsensitive?this.elements.filter(n=>n.name.toLowerCase()===t):this.elements.filter(n=>n.name===e);return(this.concatOuterScope||r.isEmpty())&&this.outerScope?r.concat(this.outerScope.getElements(e)):r}},_Et=class{static{o(this,"MapScope")}static{E(this,"MapScope")}constructor(e,t,r){this.elements=new Map,this.caseInsensitive=r?.caseInsensitive??!1,this.concatOuterScope=r?.concatOuterScope??!0;for(let n of e){let i=this.caseInsensitive?n.name.toLowerCase():n.name;this.elements.set(i,n)}this.outerScope=t}getElement(e){let t=this.caseInsensitive?e.toLowerCase():e,r=this.elements.get(t);if(r)return r;if(this.outerScope)return this.outerScope.getElement(e)}getElements(e){let t=this.caseInsensitive?e.toLowerCase():e,r=this.elements.get(t),n=r?[r]:[];return(this.concatOuterScope||n.length>0)&&this.outerScope?Bn(n).concat(this.outerScope.getElements(e)):Bn(n)}getAllElements(){let e=Bn(this.elements.values());return this.outerScope&&(e=e.concat(this.outerScope.getAllElements())),e}},U_e=class{static{o(this,"MultiMapScope")}static{E(this,"MultiMapScope")}constructor(e,t,r){this.elements=new td,this.caseInsensitive=r?.caseInsensitive??!1,this.concatOuterScope=r?.concatOuterScope??!0;for(let n of e){let i=this.caseInsensitive?n.name.toLowerCase():n.name;this.elements.add(i,n)}this.outerScope=t}getElement(e){let t=this.caseInsensitive?e.toLowerCase():e,r=this.elements.get(t)[0];if(r)return r;if(this.outerScope)return this.outerScope.getElement(e)}getElements(e){let t=this.caseInsensitive?e.toLowerCase():e,r=this.elements.get(t);return(this.concatOuterScope||r.length===0)&&this.outerScope?Bn(r).concat(this.outerScope.getElements(e)):Bn(r)}getAllElements(){let e=Bn(this.elements.values());return this.outerScope&&(e=e.concat(this.outerScope.getAllElements())),e}},LEt={getElement(){},getElements(){return Tv},getAllElements(){return Tv}},x_=class{static{o(this,"DisposableCache")}static{E(this,"DisposableCache")}constructor(){this.toDispose=[],this.isDisposed=!1}onDispose(e){this.toDispose.push(e)}dispose(){this.throwIfDisposed(),this.clear(),this.isDisposed=!0,this.toDispose.forEach(e=>e.dispose())}throwIfDisposed(){if(this.isDisposed)throw new Error("This cache has already been disposed")}},kH=class extends x_{static{o(this,"SimpleCache")}static{E(this,"SimpleCache")}constructor(){super(...arguments),this.cache=new Map}has(e){return this.throwIfDisposed(),this.cache.has(e)}set(e,t){this.throwIfDisposed(),this.cache.set(e,t)}get(e,t){if(this.throwIfDisposed(),this.cache.has(e))return this.cache.get(e);if(t){let r=t();return this.cache.set(e,r),r}else return}delete(e){return this.throwIfDisposed(),this.cache.delete(e)}clear(){this.throwIfDisposed(),this.cache.clear()}},b_=class extends x_{static{o(this,"ContextCache")}static{E(this,"ContextCache")}constructor(e){super(),this.cache=new Map,this.converter=e??(t=>t)}has(e,t){return this.throwIfDisposed(),this.cacheForContext(e).has(t)}set(e,t,r){this.throwIfDisposed(),this.cacheForContext(e).set(t,r)}get(e,t,r){this.throwIfDisposed();let n=this.cacheForContext(e);if(n.has(t))return n.get(t);if(r){let i=r();return n.set(t,i),i}else return}delete(e,t){return this.throwIfDisposed(),this.cacheForContext(e).delete(t)}clear(e){if(this.throwIfDisposed(),e){let t=this.converter(e);this.cache.delete(t)}else this.cache.clear()}cacheForContext(e){let t=this.converter(e),r=this.cache.get(t);return r||(r=new Map,this.cache.set(t,r)),r}},Y_e=class extends b_{static{o(this,"DocumentCache")}static{E(this,"DocumentCache")}constructor(e,t){super(r=>r.toString()),t?(this.toDispose.push(e.workspace.DocumentBuilder.onDocumentPhase(t,r=>{this.clear(r.uri.toString())})),this.toDispose.push(e.workspace.DocumentBuilder.onUpdate((r,n)=>{for(let i of n)this.clear(i)}))):this.toDispose.push(e.workspace.DocumentBuilder.onUpdate((r,n)=>{let i=r.concat(n);for(let a of i)this.clear(a)}))}},SH=class extends kH{static{o(this,"WorkspaceCache")}static{E(this,"WorkspaceCache")}constructor(e,t){super(),t?(this.toDispose.push(e.workspace.DocumentBuilder.onBuildPhase(t,()=>{this.clear()})),this.toDispose.push(e.workspace.DocumentBuilder.onUpdate((r,n)=>{n.length>0&&this.clear()}))):this.toDispose.push(e.workspace.DocumentBuilder.onUpdate(()=>{this.clear()}))}},j_e=class{static{o(this,"DefaultScopeProvider")}static{E(this,"DefaultScopeProvider")}constructor(e){this.reflection=e.shared.AstReflection,this.nameProvider=e.references.NameProvider,this.descriptions=e.workspace.AstNodeDescriptionProvider,this.indexManager=e.shared.workspace.IndexManager,this.globalScopeCache=new SH(e.shared)}getScope(e){let t=[],r=this.reflection.getReferenceType(e),n=wc(e.container).localSymbols;if(n){let a=e.container;do n.has(a)&&t.push(n.getStream(a).filter(s=>this.reflection.isSubtype(s.type,r))),a=a.$container;while(a)}let i=this.getGlobalScope(r,e);for(let a=t.length-1;a>=0;a--)i=this.createScope(t[a],i);return i}createScope(e,t,r){return new eV(Bn(e),t,r)}createScopeForNodes(e,t,r){let n=Bn(e).map(i=>{let a=this.nameProvider.getName(i);if(a)return this.descriptions.createDescription(i,a)}).nonNullable();return new eV(n,t,r)}getGlobalScope(e,t){return this.globalScopeCache.get(e,()=>new U_e(this.indexManager.allElements(e)))}};o(EH,"isAstNodeWithComment");E(EH,"isAstNodeWithComment");o(tV,"isIntermediateReference");E(tV,"isIntermediateReference");X_e=class{static{o(this,"DefaultJsonSerializer")}static{E(this,"DefaultJsonSerializer")}constructor(e){this.ignoreProperties=new Set(["$container","$containerProperty","$containerIndex","$document","$cstNode"]),this.langiumDocuments=e.shared.workspace.LangiumDocuments,this.astNodeLocator=e.workspace.AstNodeLocator,this.nameProvider=e.references.NameProvider,this.commentProvider=e.documentation.CommentProvider}serialize(e,t){let r=t??{},n=t?.replacer,i=E((s,l)=>this.replacer(s,l,r),"defaultReplacer"),a=n?(s,l)=>n(s,l,i):i;try{return this.currentDocument=wc(e),JSON.stringify(e,a,t?.space)}finally{this.currentDocument=void 0}}deserialize(e,t){let r=t??{},n=JSON.parse(e);return this.linkNode(n,n,r),n}replacer(e,t,{refText:r,sourceText:n,textRegions:i,comments:a,uriConverter:s}){if(!this.ignoreProperties.has(e))if(Bs(t)){let l=t.ref,u=r?t.$refText:void 0;if(l){let h=wc(l),d="";this.currentDocument&&this.currentDocument!==h&&(s?d=s(h.uri,l):d=h.uri.toString());let f=this.astNodeLocator.getAstNodePath(l);return{$ref:`${d}#${f}`,$refText:u}}else return{$error:t.error?.message??"Could not resolve reference",$refText:u}}else if(Ou(t)){let l=r?t.$refText:void 0,u=[];for(let h of t.items){let d=h.ref,f=wc(h.ref),p="";this.currentDocument&&this.currentDocument!==f&&(s?p=s(f.uri,d):p=f.uri.toString());let m=this.astNodeLocator.getAstNodePath(d);u.push(`${p}#${m}`)}return{$refs:u,$refText:l}}else if(Zi(t)){let l;if(i&&(l=this.addAstNodeRegionWithAssignmentsTo({...t}),(!e||t.$document)&&l?.$textRegion&&(l.$textRegion.documentURI=this.currentDocument?.uri.toString())),n&&!e&&(l??(l={...t}),l.$sourceText=t.$cstNode?.text),a){l??(l={...t});let u=this.commentProvider.getComment(t);u&&(l.$comment=u.replace(/\r/g,""))}return l??t}else return t}addAstNodeRegionWithAssignmentsTo(e){let t=E(r=>({offset:r.offset,end:r.end,length:r.length,range:r.range}),"createDocumentSegment");if(e.$cstNode){let r=e.$textRegion=t(e.$cstNode),n=r.assignments={};return Object.keys(e).filter(i=>!i.startsWith("$")).forEach(i=>{let a=XW(e.$cstNode,i).map(t);a.length!==0&&(n[i]=a)}),e}}linkNode(e,t,r,n,i,a){for(let[l,u]of Object.entries(e))if(Array.isArray(u))for(let h=0;h{await this.handleException(()=>e.call(t,r,n,i),"An error occurred during validation",n,r)}}async handleException(e,t,r,n){try{await e()}catch(i){if(x0(i))throw i;console.error(`${t}:`,i),i instanceof Error&&i.stack&&console.error(i.stack);let a=i instanceof Error?i.message:String(i);r("error",`${t}: ${a}`,{node:n})}}addEntry(e,t){if(e==="AstNode"){this.entries.add("AstNode",t);return}for(let r of this.reflection.getAllSubTypes(e))this.entries.add(r,t)}getChecks(e,t){let r=Bn(this.entries.get(e)).concat(this.entries.get("AstNode"));return t&&(r=r.filter(n=>t.includes(n.category))),r.map(n=>n.check)}registerBeforeDocument(e,t=this){this.entriesBefore.push(this.wrapPreparationException(e,"An error occurred during set-up of the validation",t))}registerAfterDocument(e,t=this){this.entriesAfter.push(this.wrapPreparationException(e,"An error occurred during tear-down of the validation",t))}wrapPreparationException(e,t,r){return async(n,i,a,s)=>{await this.handleException(()=>e.call(r,n,i,a,s),t,i,n)}}get checksBefore(){return this.entriesBefore}get checksAfter(){return this.entriesAfter}getAllValidationCategories(e){return this.knownCategories}},Q_e=Object.freeze({validateNode:!0,validateChildren:!0}),J_e=class{static{o(this,"DefaultDocumentValidator")}static{E(this,"DefaultDocumentValidator")}constructor(e){this.validationRegistry=e.validation.ValidationRegistry,this.metadata=e.LanguageMetaData,this.profiler=e.shared.profilers.LangiumProfiler,this.languageId=e.LanguageMetaData.languageId}async validateDocument(e,t={},r=Qn.CancellationToken.None){let n=e.parseResult,i=[];if(await Ia(r),(!t.categories||t.categories.includes("built-in"))&&(this.processLexingErrors(n,i,t),t.stopAfterLexingErrors&&i.some(a=>a.data?.code===Rl.LexingError)||(this.processParsingErrors(n,i,t),t.stopAfterParsingErrors&&i.some(a=>a.data?.code===Rl.ParsingError))||(this.processLinkingErrors(e,i,t),t.stopAfterLinkingErrors&&i.some(a=>a.data?.code===Rl.LinkingError))))return i;try{i.push(...await this.validateAst(n.value,t,r))}catch(a){if(x0(a))throw a;console.error("An error occurred during validation:",a)}return await Ia(r),i}processLexingErrors(e,t,r){let n=[...e.lexerErrors,...e.lexerReport?.diagnostics??[]];for(let i of n){let a=i.severity??"error",s={severity:qC(a),range:{start:{line:i.line-1,character:i.column-1},end:{line:i.line-1,character:i.column+i.length-1}},message:i.message,data:RH(a),source:this.getSource()};t.push(s)}}processParsingErrors(e,t,r){for(let n of e.parserErrors){let i;if(isNaN(n.token.startOffset)){if("previousToken"in n){let a=n.previousToken;if(isNaN(a.startOffset)){let s={line:0,character:0};i={start:s,end:s}}else{let s={line:a.endLine-1,character:a.endColumn};i={start:s,end:s}}}}else i=KC(n.token);if(i){let a={severity:qC("error"),range:i,message:n.message,data:Hg(Rl.ParsingError),source:this.getSource()};t.push(a)}}}processLinkingErrors(e,t,r){for(let n of e.references){let i=n.error;if(i){let a={node:i.info.container,range:n.$refNode?.range,property:i.info.property,index:i.info.index,data:{code:Rl.LinkingError,containerType:i.info.container.$type,property:i.info.property,refText:i.info.reference.$refText}};t.push(this.toDiagnostic("error",i.message,a))}}}async validateAst(e,t,r=Qn.CancellationToken.None){let n=[],i=E((a,s,l)=>{n.push(this.toDiagnostic(a,s,l))},"acceptor");return await this.validateAstBefore(e,t,i,r),await this.validateAstNodes(e,t,i,r),await this.validateAstAfter(e,t,i,r),n}async validateAstBefore(e,t,r,n=Qn.CancellationToken.None){let i=this.validationRegistry.checksBefore;for(let a of i)await Ia(n),await a(e,r,t.categories??[],n)}async validateAstNodes(e,t,r,n=Qn.CancellationToken.None){if(this.profiler?.isActive("validating")){let i=this.profiler.createTask("validating",this.languageId);i.start();try{let a=kc(e).iterator();for(let s of a){i.startSubTask(s.$type);let l=this.validateSingleNodeOptions(s,t);if(l.validateNode)try{let u=this.validationRegistry.getChecks(s.$type,t.categories);for(let h of u)await h(s,r,n)}finally{i.stopSubTask(s.$type)}l.validateChildren||a.prune()}}finally{i.stop()}}else{let i=kc(e).iterator();for(let a of i){await Ia(n);let s=this.validateSingleNodeOptions(a,t);if(s.validateNode){let l=this.validationRegistry.getChecks(a.$type,t.categories);for(let u of l)await u(a,r,n)}s.validateChildren||i.prune()}}}validateSingleNodeOptions(e,t){return Q_e}async validateAstAfter(e,t,r,n=Qn.CancellationToken.None){let i=this.validationRegistry.checksAfter;for(let a of i)await Ia(n),await a(e,r,t.categories??[],n)}toDiagnostic(e,t,r){return{message:t,range:AH(r),severity:qC(e),code:r.code,codeDescription:r.codeDescription,tags:r.tags,relatedInformation:r.relatedInformation,data:r.data,source:this.getSource()}}getSource(){return this.metadata.languageId}};o(AH,"getDiagnosticRange");E(AH,"getDiagnosticRange");o(qC,"toDiagnosticSeverity");E(qC,"toDiagnosticSeverity");o(RH,"toDiagnosticData");E(RH,"toDiagnosticData");(function(e){e.LexingError="lexing-error",e.LexingWarning="lexing-warning",e.LexingInfo="lexing-info",e.LexingHint="lexing-hint",e.ParsingError="parsing-error",e.LinkingError="linking-error"})(Rl||(Rl={}));eLe=class{static{o(this,"DefaultAstNodeDescriptionProvider")}static{E(this,"DefaultAstNodeDescriptionProvider")}constructor(e){this.astNodeLocator=e.workspace.AstNodeLocator,this.nameProvider=e.references.NameProvider}createDescription(e,t,r){let n=r??wc(e);t??(t=this.nameProvider.getName(e));let i=this.astNodeLocator.getAstNodePath(e);if(!t)throw new Error(`Node at path ${i} has no name.`);let a,s=E(()=>a??(a=Av(this.nameProvider.getNameNode(e)??e.$cstNode)),"nameSegmentGetter");return{node:e,name:t,get nameSegment(){return s()},selectionSegment:Av(e.$cstNode),type:e.$type,documentUri:n.uri,path:i}}},tLe=class{static{o(this,"DefaultReferenceDescriptionProvider")}static{E(this,"DefaultReferenceDescriptionProvider")}constructor(e){this.nodeLocator=e.workspace.AstNodeLocator}async createDescriptions(e,t=Qn.CancellationToken.None){let r=[],n=e.parseResult.value;for(let i of kc(n))await Ia(t),kv(i).forEach(a=>{a.reference.error||r.push(...this.createInfoDescriptions(a))});return r}createInfoDescriptions(e){let t=e.reference;if(t.error||!t.$refNode)return[];let r=[];Bs(t)&&t.$nodeDescription?r=[t.$nodeDescription]:Ou(t)&&(r=t.items.map(l=>l.$nodeDescription).filter(l=>l!==void 0));let n=wc(e.container).uri,i=this.nodeLocator.getAstNodePath(e.container),a=[],s=Av(t.$refNode);for(let l of r)a.push({sourceUri:n,sourcePath:i,targetUri:l.documentUri,targetPath:l.path,segment:s,local:$s.equals(l.documentUri,n)});return a}},rLe=class{static{o(this,"DefaultAstNodeLocator")}static{E(this,"DefaultAstNodeLocator")}constructor(){this.segmentSeparator="/",this.indexSeparator="@"}getAstNodePath(e){if(e.$container){let t=this.getAstNodePath(e.$container),r=this.getPathSegment(e);return t+this.segmentSeparator+r}return""}getPathSegment({$containerProperty:e,$containerIndex:t}){if(!e)throw new Error("Missing '$containerProperty' in AST node.");return t!==void 0?e+this.indexSeparator+t:e}getAstNode(e,t){return t.split(this.segmentSeparator).reduce((n,i)=>{if(!n||i.length===0)return n;let a=i.indexOf(this.indexSeparator);if(a>0){let s=i.substring(0,a),l=parseInt(i.substring(a+1));return n[s]?.[l]}return n[i]},e)}},T_={};mR(T_,rW(Iv(),1));nLe=class{static{o(this,"DefaultConfigurationProvider")}static{E(this,"DefaultConfigurationProvider")}constructor(e){this._ready=new ed,this.onConfigurationSectionUpdateEmitter=new T_.Emitter,this.settings={},this.workspaceConfig=!1,this.serviceRegistry=e.ServiceRegistry}get ready(){return this._ready.promise}initialize(e){this.workspaceConfig=e.capabilities.workspace?.configuration??!1}async initialized(e){if(this.workspaceConfig){if(e.register){let t=this.serviceRegistry.all;e.register({section:t.map(r=>this.toSectionName(r.LanguageMetaData.languageId))})}if(e.fetchConfiguration){let t=this.serviceRegistry.all.map(n=>({section:this.toSectionName(n.LanguageMetaData.languageId)})),r=await e.fetchConfiguration(t);t.forEach((n,i)=>{this.updateSectionConfiguration(n.section,r[i])})}}this._ready.resolve()}updateConfiguration(e){typeof e.settings!="object"||e.settings===null||Object.entries(e.settings).forEach(([t,r])=>{this.updateSectionConfiguration(t,r),this.onConfigurationSectionUpdateEmitter.fire({section:t,configuration:r})})}updateSectionConfiguration(e,t){this.settings[e]=t}async getConfiguration(e,t){await this.ready;let r=this.toSectionName(e);if(this.settings[r])return this.settings[r][t]}toSectionName(e){return`${e}`}get onConfigurationSectionUpdate(){return this.onConfigurationSectionUpdateEmitter.event}},$A=rW(ayt(),1);(function(e){function t(r){return{dispose:E(async()=>await r(),"dispose")}}o(t,"create"),E(t,"create"),e.create=t})(jg||(jg={}));iLe=class{static{o(this,"DefaultDocumentBuilder")}static{E(this,"DefaultDocumentBuilder")}constructor(e){this.updateBuildOptions={validation:{categories:["built-in","fast"]}},this.updateListeners=[],this.buildPhaseListeners=new td,this.documentPhaseListeners=new td,this.buildState=new Map,this.documentBuildWaiters=new Map,this.currentState=en.Changed,this.langiumDocuments=e.workspace.LangiumDocuments,this.langiumDocumentFactory=e.workspace.LangiumDocumentFactory,this.textDocuments=e.workspace.TextDocuments,this.indexManager=e.workspace.IndexManager,this.fileSystemProvider=e.workspace.FileSystemProvider,this.workspaceManager=()=>e.workspace.WorkspaceManager,this.serviceRegistry=e.ServiceRegistry}async build(e,t={},r=Qn.CancellationToken.None){for(let n of e){let i=n.uri.toString();if(n.state===en.Validated){if(typeof t.validation=="boolean"&&t.validation)this.resetToState(n,en.IndexedReferences);else if(typeof t.validation=="object"){let a=this.findMissingValidationCategories(n,t);a.length>0&&(this.buildState.set(i,{completed:!1,options:{validation:{categories:a}},result:this.buildState.get(i)?.result}),n.state=en.IndexedReferences)}}else this.buildState.delete(i)}this.currentState=en.Changed,await this.emitUpdate(e.map(n=>n.uri),[]),await this.buildDocuments(e,t,r)}async update(e,t,r=Qn.CancellationToken.None){this.currentState=en.Changed;let n=[];for(let l of t){let u=this.langiumDocuments.deleteDocuments(l);for(let h of u)n.push(h.uri),this.cleanUpDeleted(h)}let i=(await Promise.all(e.map(l=>this.findChangedUris(l)))).flat();for(let l of i){let u=this.langiumDocuments.getDocument(l);u===void 0&&(u=this.langiumDocumentFactory.fromModel({$type:"INVALID"},l),u.state=en.Changed,this.langiumDocuments.addDocument(u)),this.resetToState(u,en.Changed)}let a=Bn(i).concat(n).map(l=>l.toString()).toSet();this.langiumDocuments.all.filter(l=>!a.has(l.uri.toString())&&this.shouldRelink(l,a)).forEach(l=>this.resetToState(l,en.ComputedScopes)),await this.emitUpdate(i,n),await Ia(r);let s=this.sortDocuments(this.langiumDocuments.all.filter(l=>l.state=1}findMissingValidationCategories(e,t){let r=this.buildState.get(e.uri.toString()),n=this.serviceRegistry.getServices(e.uri).validation.ValidationRegistry.getAllValidationCategories(e),i=r?.result?.validationChecks?new Set(r?.result?.validationChecks):r?.completed?n:new Set,a=t===void 0||t.validation===!0?n:typeof t.validation=="object"?t.validation.categories??n:[];return Bn(a).filter(s=>!i.has(s)).toArray()}async findChangedUris(e){if(this.langiumDocuments.getDocument(e)??this.textDocuments?.get(e))return[e];try{let r=await this.fileSystemProvider.stat(e);if(r.isDirectory)return await this.workspaceManager().searchFolder(e);if(this.workspaceManager().shouldIncludeEntry(r))return[e]}catch{}return[]}async emitUpdate(e,t){await Promise.all(this.updateListeners.map(r=>r(e,t)))}sortDocuments(e){let t=0,r=e.length-1;for(;t=0&&!this.hasTextDocument(e[r]);)r--;tr.error!==void 0)?!0:this.indexManager.isAffected(e,t)}onUpdate(e){return this.updateListeners.push(e),jg.create(()=>{let t=this.updateListeners.indexOf(e);t>=0&&this.updateListeners.splice(t,1)})}resetToState(e,t){switch(t){case en.Changed:case en.Parsed:this.indexManager.removeContent(e.uri);case en.IndexedContent:e.localSymbols=void 0;case en.ComputedScopes:this.serviceRegistry.getServices(e.uri).references.Linker.unlink(e);case en.Linked:this.indexManager.removeReferences(e.uri);case en.IndexedReferences:e.diagnostics=void 0,this.buildState.delete(e.uri.toString());case en.Validated:}e.state>t&&(e.state=t)}cleanUpDeleted(e){this.buildState.delete(e.uri.toString()),this.indexManager.remove(e.uri),e.state=en.Changed}async buildDocuments(e,t,r){this.prepareBuild(e,t),await this.runCancelable(e,en.Parsed,r,a=>this.langiumDocumentFactory.update(a,r)),await this.runCancelable(e,en.IndexedContent,r,a=>this.indexManager.updateContent(a,r)),await this.runCancelable(e,en.ComputedScopes,r,async a=>{let s=this.serviceRegistry.getServices(a.uri).references.ScopeComputation;a.localSymbols=await s.collectLocalSymbols(a,r)});let n=e.filter(a=>this.shouldLink(a));await this.runCancelable(n,en.Linked,r,a=>this.serviceRegistry.getServices(a.uri).references.Linker.link(a,r)),await this.runCancelable(n,en.IndexedReferences,r,a=>this.indexManager.updateReferences(a,r));let i=e.filter(a=>this.shouldValidate(a)?!0:(this.markAsCompleted(a),!1));await this.runCancelable(i,en.Validated,r,async a=>{await this.validate(a,r),this.markAsCompleted(a)})}markAsCompleted(e){let t=this.buildState.get(e.uri.toString());t&&(t.completed=!0)}prepareBuild(e,t){for(let r of e){let n=r.uri.toString(),i=this.buildState.get(n);(!i||i.completed)&&this.buildState.set(n,{completed:!1,options:t,result:i?.result})}}async runCancelable(e,t,r,n){for(let a of e)a.statea.state===t);await this.notifyBuildPhase(i,t,r),this.currentState=t}onBuildPhase(e,t){return this.buildPhaseListeners.add(e,t),jg.create(()=>{this.buildPhaseListeners.delete(e,t)})}onDocumentPhase(e,t){return this.documentPhaseListeners.add(e,t),jg.create(()=>{this.documentPhaseListeners.delete(e,t)})}waitUntil(e,t,r){let n;return t&&"path"in t?n=t:r=t,r??(r=Qn.CancellationToken.None),n?this.awaitDocumentState(e,n,r):this.awaitBuilderState(e,r)}awaitDocumentState(e,t,r){let n=this.langiumDocuments.getDocument(t);if(n){if(n.state>=e)return Promise.resolve(t);if(r.isCancellationRequested)return Promise.reject(Pu);if(this.currentState>=e&&e>n.state)return Promise.reject(new $A.ResponseError($A.LSPErrorCodes.RequestFailed,`Document state of ${t.toString()} is ${en[n.state]}, requiring ${en[e]}, but workspace state is already ${en[this.currentState]}. Returning undefined.`))}else return Promise.reject(new $A.ResponseError($A.LSPErrorCodes.ServerCancelled,`No document found for URI: ${t.toString()}`));return new Promise((i,a)=>{let s=this.onDocumentPhase(e,u=>{$s.equals(u.uri,t)&&(s.dispose(),l.dispose(),i(u.uri))}),l=r.onCancellationRequested(()=>{s.dispose(),l.dispose(),a(Pu)})})}awaitBuilderState(e,t){return this.currentState>=e?Promise.resolve():t.isCancellationRequested?Promise.reject(Pu):new Promise((r,n)=>{let i=this.onBuildPhase(e,()=>{i.dispose(),a.dispose(),r()}),a=t.onCancellationRequested(()=>{i.dispose(),a.dispose(),n(Pu)})})}async notifyDocumentPhase(e,t,r){let i=this.documentPhaseListeners.get(t).slice();for(let a of i)try{await Ia(r),await a(e,r)}catch(s){if(!x0(s))throw s}}async notifyBuildPhase(e,t,r){if(e.length===0)return;let i=this.buildPhaseListeners.get(t).slice();for(let a of i)await Ia(r),await a(e,r)}shouldLink(e){return this.getBuildOptions(e).eagerLinking??!0}shouldValidate(e){return!!this.getBuildOptions(e).validation}async validate(e,t){let r=this.serviceRegistry.getServices(e.uri).validation.DocumentValidator,n=this.getBuildOptions(e),i=typeof n.validation=="object"?{...n.validation}:{};i.categories=this.findMissingValidationCategories(e,n);let a=await r.validateDocument(e,i,t);e.diagnostics?e.diagnostics.push(...a):e.diagnostics=a;let s=this.buildState.get(e.uri.toString());s&&(s.result??(s.result={}),s.result.validationChecks?s.result.validationChecks=Bn(s.result.validationChecks).concat(i.categories).distinct().toArray():s.result.validationChecks=[...i.categories])}getBuildOptions(e){return this.buildState.get(e.uri.toString())?.options??{}}},aLe=class{static{o(this,"DefaultIndexManager")}static{E(this,"DefaultIndexManager")}constructor(e){this.symbolIndex=new Map,this.symbolByTypeIndex=new b_,this.referenceIndex=new Map,this.documents=e.workspace.LangiumDocuments,this.serviceRegistry=e.ServiceRegistry,this.astReflection=e.AstReflection}findAllReferences(e,t){let r=wc(e).uri,n=[];return this.referenceIndex.forEach(i=>{i.forEach(a=>{$s.equals(a.targetUri,r)&&a.targetPath===t&&n.push(a)})}),Bn(n)}allElements(e,t){let r=Bn(this.symbolIndex.keys());return t&&(r=r.filter(n=>!t||t.has(n))),r.map(n=>this.getFileDescriptions(n,e)).flat()}getFileDescriptions(e,t){return t?this.symbolByTypeIndex.get(e,t,()=>(this.symbolIndex.get(e)??[]).filter(i=>this.astReflection.isSubtype(i.type,t))):this.symbolIndex.get(e)??[]}remove(e){this.removeContent(e),this.removeReferences(e)}removeContent(e){let t=e.toString();this.symbolIndex.delete(t),this.symbolByTypeIndex.clear(t)}removeReferences(e){let t=e.toString();this.referenceIndex.delete(t)}async updateContent(e,t=Qn.CancellationToken.None){let n=await this.serviceRegistry.getServices(e.uri).references.ScopeComputation.collectExportedSymbols(e,t),i=e.uri.toString();this.symbolIndex.set(i,n),this.symbolByTypeIndex.clear(i)}async updateReferences(e,t=Qn.CancellationToken.None){let n=await this.serviceRegistry.getServices(e.uri).workspace.ReferenceDescriptionProvider.createDescriptions(e,t);this.referenceIndex.set(e.uri.toString(),n)}isAffected(e,t){let r=this.referenceIndex.get(e.uri.toString());return r?r.some(n=>!n.local&&t.has(n.targetUri.toString())):!1}},sLe=class{static{o(this,"DefaultWorkspaceManager")}static{E(this,"DefaultWorkspaceManager")}constructor(e){this.initialBuildOptions={},this._ready=new ed,this.serviceRegistry=e.ServiceRegistry,this.langiumDocuments=e.workspace.LangiumDocuments,this.documentBuilder=e.workspace.DocumentBuilder,this.fileSystemProvider=e.workspace.FileSystemProvider,this.mutex=e.workspace.WorkspaceLock}get ready(){return this._ready.promise}get workspaceFolders(){return this.folders}initialize(e){this.folders=e.workspaceFolders??void 0}initialized(e){return this.mutex.write(t=>this.initializeWorkspace(this.folders??[],t))}async initializeWorkspace(e,t=Qn.CancellationToken.None){let r=await this.performStartup(e);await Ia(t),await this.documentBuilder.build(r,this.initialBuildOptions,t)}async performStartup(e){let t=[],r=E(a=>{t.push(a),this.langiumDocuments.hasDocument(a.uri)||this.langiumDocuments.addDocument(a)},"collector");await this.loadAdditionalDocuments(e,r);let n=[];await Promise.all(e.map(a=>this.getRootFolder(a)).map(async a=>this.traverseFolder(a,n)));let i=Bn(n).distinct(a=>a.toString()).filter(a=>!this.langiumDocuments.hasDocument(a));return await this.loadWorkspaceDocuments(i,r),this._ready.resolve(),t}async loadWorkspaceDocuments(e,t){await Promise.all(e.map(async r=>{let n=await this.langiumDocuments.getOrCreateDocument(r);t(n)}))}loadAdditionalDocuments(e,t){return Promise.resolve()}getRootFolder(e){return Yo.parse(e.uri)}async traverseFolder(e,t){try{let r=await this.fileSystemProvider.readDirectory(e);await Promise.all(r.map(async n=>{this.shouldIncludeEntry(n)&&(n.isDirectory?await this.traverseFolder(n.uri,t):n.isFile&&t.push(n.uri))}))}catch(r){console.error("Failure to read directory content of "+e.toString(!0),r)}}async searchFolder(e){let t=[];return await this.traverseFolder(e,t),t}shouldIncludeEntry(e){let t=$s.basename(e.uri);return t.startsWith(".")?!1:e.isDirectory?t!=="node_modules"&&t!=="out":e.isFile?this.serviceRegistry.hasServices(e.uri):!1}},oLe=class{static{o(this,"DefaultLexerErrorMessageProvider")}static{E(this,"DefaultLexerErrorMessageProvider")}buildUnexpectedCharactersMessage(e,t,r,n,i){return NG.buildUnexpectedCharactersMessage(e,t,r,n,i)}buildUnableToPopLexerModeMessage(e){return NG.buildUnableToPopLexerModeMessage(e)}},_H={mode:"full"},LH=class{static{o(this,"DefaultLexer")}static{E(this,"DefaultLexer")}constructor(e){this.errorMessageProvider=e.parser.LexerErrorMessageProvider,this.tokenBuilder=e.parser.TokenBuilder;let t=this.tokenBuilder.buildTokens(e.Grammar,{caseInsensitive:e.LanguageMetaData.caseInsensitive});this.tokenTypes=this.toTokenTypeDictionary(t);let r=dR(t)?Object.values(t):t,n=e.LanguageMetaData.mode==="production";this.chevrotainLexer=new Fs(r,{positionTracking:"full",skipValidations:n,errorMessageProvider:this.errorMessageProvider})}get definition(){return this.tokenTypes}tokenize(e,t=_H){let r=this.chevrotainLexer.tokenize(e);return{tokens:r.tokens,errors:r.errors,hidden:r.groups.hidden??[],report:this.tokenBuilder.flushLexingReport?.(e)}}toTokenTypeDictionary(e){if(dR(e))return e;let t=w_(e)?Object.values(e.modes).flat():e,r={};return t.forEach(n=>r[n.name]=n),r}};o(C_,"isTokenTypeArray");E(C_,"isTokenTypeArray");o(w_,"isIMultiModeLexerDefinition");E(w_,"isIMultiModeLexerDefinition");o(dR,"isTokenTypeDictionary");E(dR,"isTokenTypeDictionary");dw();o(DH,"parseJSDoc");E(DH,"parseJSDoc");o(IH,"isJSDoc");E(IH,"isJSDoc");o(MH,"getLines");E(MH,"getLines");awe=/\s*(@([\p{L}][\p{L}\p{N}]*)?)/uy,DEt=/\{(@[\p{L}][\p{L}\p{N}]*)(\s*)([^\r\n}]+)?\}/gu;o(lLe,"tokenize");E(lLe,"tokenize");o(cLe,"buildInlineTokens");E(cLe,"buildInlineTokens");IEt=/\S/,MEt=/\s*$/;o(fR,"skipWhitespace");E(fR,"skipWhitespace");o(uLe,"lastCharacter");E(uLe,"lastCharacter");o(hLe,"parseJSDocComment");E(hLe,"parseJSDocComment");o(dLe,"parseJSDocElement");E(dLe,"parseJSDocElement");o(fLe,"appendEmptyLine");E(fLe,"appendEmptyLine");o(NH,"parseJSDocText");E(NH,"parseJSDocText");o(pLe,"parseJSDocInline");E(pLe,"parseJSDocInline");o(PH,"parseJSDocTag");E(PH,"parseJSDocTag");o(OH,"parseJSDocLine");E(OH,"parseJSDocLine");o(k_,"normalizeOptions");E(k_,"normalizeOptions");o(y6,"normalizeOption");E(y6,"normalizeOption");swe=class{static{o(this,"JSDocCommentImpl")}static{E(this,"JSDocCommentImpl")}constructor(e,t){this.elements=e,this.range=t}getTag(e){return this.getAllTags().find(t=>t.name===e)}getTags(e){return this.getAllTags().filter(t=>t.name===e)}getAllTags(){return this.elements.filter(e=>"name"in e)}toString(){let e="";for(let t of this.elements)if(e.length===0)e=t.toString();else{let r=t.toString();e+=nV(e)+r}return e.trim()}toMarkdown(e){let t="";for(let r of this.elements)if(t.length===0)t=r.toMarkdown(e);else{let n=r.toMarkdown(e);t+=nV(t)+n}return t.trim()}},uz=class{static{o(this,"JSDocTagImpl")}static{E(this,"JSDocTagImpl")}constructor(e,t,r,n){this.name=e,this.content=t,this.inline=r,this.range=n}toString(){let e=`@${this.name}`,t=this.content.toString();return this.content.inlines.length===1?e=`${e} ${t}`:this.content.inlines.length>1&&(e=`${e} +${t}`),this.inline?`{${e}}`:e}toMarkdown(e){return e?.renderTag?.(this)??this.toMarkdownDefault(e)}toMarkdownDefault(e){let t=this.content.toMarkdown(e);if(this.inline){let i=mLe(this.name,t,e??{});if(typeof i=="string")return i}let r="";e?.tag==="italic"||e?.tag===void 0?r="*":e?.tag==="bold"?r="**":e?.tag==="bold-italic"&&(r="***");let n=`${r}@${this.name}${r}`;return this.content.inlines.length===1?n=`${n} \u2014 ${t}`:this.content.inlines.length>1&&(n=`${n} +${t}`),this.inline?`{${n}}`:n}};o(mLe,"renderInlineTag");E(mLe,"renderInlineTag");o(gLe,"renderLinkDefault");E(gLe,"renderLinkDefault");rV=class{static{o(this,"JSDocTextImpl")}static{E(this,"JSDocTextImpl")}constructor(e,t){this.inlines=e,this.range=t}toString(){let e="";for(let t=0;tr.range.start.line&&(e+=` +`)}return e}toMarkdown(e){let t="";for(let r=0;rn.range.start.line&&(t+=` +`)}return t}},yLe=class{static{o(this,"JSDocLineImpl")}static{E(this,"JSDocLineImpl")}constructor(e,t){this.text=e,this.range=t}toString(){return this.text}toMarkdown(){return this.text}};o(nV,"fillNewlines");E(nV,"fillNewlines");vLe=class{static{o(this,"JSDocDocumentationProvider")}static{E(this,"JSDocDocumentationProvider")}constructor(e){this.indexManager=e.shared.workspace.IndexManager,this.commentProvider=e.documentation.CommentProvider}getDocumentation(e){let t=this.commentProvider.getComment(e);if(t&&IH(t))return DH(t).toMarkdown({renderLink:E((n,i)=>this.documentationLinkRenderer(e,n,i),"renderLink"),renderTag:E(n=>this.documentationTagRenderer(e,n),"renderTag")})}documentationLinkRenderer(e,t,r){let n=this.findNameInLocalSymbols(e,t)??this.findNameInGlobalScope(e,t);if(n&&n.nameSegment){let i=n.nameSegment.range.start.line+1,a=n.nameSegment.range.start.character+1,s=n.documentUri.with({fragment:`L${i},${a}`});return`[${r}](${s.toString()})`}else return}documentationTagRenderer(e,t){}findNameInLocalSymbols(e,t){let n=wc(e).localSymbols;if(!n)return;let i=e;do{let s=n.getStream(i).find(l=>l.name===t);if(s)return s;i=i.$container}while(i)}findNameInGlobalScope(e,t){return this.indexManager.allElements().find(n=>n.name===t)}},xLe=class{static{o(this,"DefaultCommentProvider")}static{E(this,"DefaultCommentProvider")}constructor(e){this.grammarConfig=()=>e.parser.GrammarConfig}getComment(e){return EH(e)?e.$comment:NW(e.$cstNode,this.grammarConfig().multilineCommentRules)?.text}},bLe=class{static{o(this,"DefaultAsyncParser")}static{E(this,"DefaultAsyncParser")}constructor(e){this.syncParser=e.parser.LangiumParser}parse(e,t){return Promise.resolve(this.syncParser.parse(e))}},NEt=class{static{o(this,"AbstractThreadedAsyncParser")}static{E(this,"AbstractThreadedAsyncParser")}constructor(e){this.threadCount=8,this.terminationDelay=200,this.workerPool=[],this.queue=[],this.hydrator=e.serializer.Hydrator}initializeWorkers(){for(;this.workerPool.length{if(this.queue.length>0){let t=this.queue.shift();t&&(e.lock(),t.resolve(e))}}),this.workerPool.push(e)}}async parse(e,t){let r=await this.acquireParserWorker(t),n=new ed,i,a=t.onCancellationRequested(()=>{i=setTimeout(()=>{this.terminateWorker(r)},this.terminationDelay)});return r.parse(e).then(s=>{let l=this.hydrator.hydrate(s);n.resolve(l)}).catch(s=>{n.reject(s)}).finally(()=>{a.dispose(),clearTimeout(i)}),n.promise}terminateWorker(e){e.terminate();let t=this.workerPool.indexOf(e);t>=0&&this.workerPool.splice(t,1)}async acquireParserWorker(e){this.initializeWorkers();for(let r of this.workerPool)if(r.ready)return r.lock(),r;let t=new ed;return e.onCancellationRequested(()=>{let r=this.queue.indexOf(t);r>=0&&this.queue.splice(r,1),t.reject(Pu)}),this.queue.push(t),t.promise}},PEt=class{static{o(this,"ParserWorker")}static{E(this,"ParserWorker")}get ready(){return this._ready}get onReady(){return this.onReadyEmitter.event}constructor(e,t,r,n){this.onReadyEmitter=new T_.Emitter,this.deferred=new ed,this._ready=!0,this._parsing=!1,this.sendMessage=e,this._terminate=n,t(i=>{let a=i;this.deferred.resolve(a),this.unlock()}),r(i=>{this.deferred.reject(i),this.unlock()})}terminate(){this.deferred.reject(Pu),this._terminate()}lock(){this._ready=!1}unlock(){this._parsing=!1,this._ready=!0,this.onReadyEmitter.fire()}parse(e){if(this._parsing)throw new Error("Parser worker is busy");return this._parsing=!0,this.deferred=new ed,this.sendMessage(e),this.deferred.promise}},TLe=class{static{o(this,"DefaultWorkspaceLock")}static{E(this,"DefaultWorkspaceLock")}constructor(){this.previousTokenSource=new Qn.CancellationTokenSource,this.writeQueue=[],this.readQueue=[],this.done=!0}write(e){this.cancelWrite();let t=v_();return this.previousTokenSource=t,this.enqueue(this.writeQueue,e,t.token)}read(e){return this.enqueue(this.readQueue,e)}enqueue(e,t,r=Qn.CancellationToken.None){let n=new ed,i={action:t,deferred:n,cancellationToken:r};return e.push(i),this.performNextOperation(),n.promise}async performNextOperation(){if(!this.done)return;let e=[];if(this.writeQueue.length>0)e.push(this.writeQueue.shift());else if(this.readQueue.length>0)e.push(...this.readQueue.splice(0,this.readQueue.length));else return;this.done=!1,await Promise.all(e.map(async({action:t,deferred:r,cancellationToken:n})=>{try{let i=await Promise.resolve().then(()=>t(n));r.resolve(i)}catch(i){x0(i)?r.resolve(void 0):r.reject(i)}})),this.done=!0,this.performNextOperation()}cancelWrite(){this.previousTokenSource.cancel()}},CLe=class{static{o(this,"DefaultHydrator")}static{E(this,"DefaultHydrator")}constructor(e){this.grammarElementIdMap=new uR,this.tokenTypeIdMap=new uR,this.grammar=e.Grammar,this.lexer=e.parser.Lexer,this.linker=e.references.Linker}dehydrate(e){return{lexerErrors:e.lexerErrors,lexerReport:e.lexerReport?this.dehydrateLexerReport(e.lexerReport):void 0,parserErrors:e.parserErrors.map(t=>({...t,message:t.message})),value:this.dehydrateAstNode(e.value,this.createDehyrationContext(e.value))}}dehydrateLexerReport(e){return e}createDehyrationContext(e){let t=new Map,r=new Map;for(let n of kc(e))t.set(n,{});if(e.$cstNode)for(let n of Ev(e.$cstNode))r.set(n,{});return{astNodes:t,cstNodes:r}}dehydrateAstNode(e,t){let r=t.astNodes.get(e);r.$type=e.$type,r.$containerIndex=e.$containerIndex,r.$containerProperty=e.$containerProperty,e.$cstNode!==void 0&&(r.$cstNode=this.dehydrateCstNode(e.$cstNode,t));for(let[n,i]of Object.entries(e))if(!n.startsWith("$"))if(Array.isArray(i)){let a=[];r[n]=a;for(let s of i)Zi(s)?a.push(this.dehydrateAstNode(s,t)):Bs(s)?a.push(this.dehydrateReference(s,t)):a.push(s)}else Zi(i)?r[n]=this.dehydrateAstNode(i,t):Bs(i)?r[n]=this.dehydrateReference(i,t):i!==void 0&&(r[n]=i);return r}dehydrateReference(e,t){let r={};return r.$refText=e.$refText,e.$refNode&&(r.$refNode=t.cstNodes.get(e.$refNode)),r}dehydrateCstNode(e,t){let r=t.cstNodes.get(e);return vR(e)?r.fullText=e.fullText:r.grammarSource=this.getGrammarElementId(e.grammarSource),r.hidden=e.hidden,r.astNode=t.astNodes.get(e.astNode),Uh(e)?r.content=e.content.map(n=>this.dehydrateCstNode(n,t)):e0(e)&&(r.tokenType=e.tokenType.name,r.offset=e.offset,r.length=e.length,r.startLine=e.range.start.line,r.startColumn=e.range.start.character,r.endLine=e.range.end.line,r.endColumn=e.range.end.character),r}hydrate(e){let t=e.value,r=this.createHydrationContext(t);return"$cstNode"in t&&this.hydrateCstNode(t.$cstNode,r),{lexerErrors:e.lexerErrors,lexerReport:e.lexerReport,parserErrors:e.parserErrors,value:this.hydrateAstNode(t,r)}}createHydrationContext(e){let t=new Map,r=new Map;for(let i of kc(e))t.set(i,{});let n;if(e.$cstNode)for(let i of Ev(e.$cstNode)){let a;"fullText"in i?(a=new uH(i.fullText),n=a):"content"in i?a=new f_:"tokenType"in i&&(a=this.hydrateCstLeafNode(i)),a&&(r.set(i,a),a.root=n)}return{astNodes:t,cstNodes:r}}hydrateAstNode(e,t){let r=t.astNodes.get(e);r.$type=e.$type,r.$containerIndex=e.$containerIndex,r.$containerProperty=e.$containerProperty,e.$cstNode&&(r.$cstNode=t.cstNodes.get(e.$cstNode));for(let[n,i]of Object.entries(e))if(!n.startsWith("$"))if(Array.isArray(i)){let a=[];r[n]=a;for(let s of i)Zi(s)?a.push(this.setParent(this.hydrateAstNode(s,t),r)):Bs(s)?a.push(this.hydrateReference(s,r,n,t)):a.push(s)}else Zi(i)?r[n]=this.setParent(this.hydrateAstNode(i,t),r):Bs(i)?r[n]=this.hydrateReference(i,r,n,t):i!==void 0&&(r[n]=i);return r}setParent(e,t){return e.$container=t,e}hydrateReference(e,t,r,n){return this.linker.buildReference(t,r,n.cstNodes.get(e.$refNode),e.$refText)}hydrateCstNode(e,t,r=0){let n=t.cstNodes.get(e);if(typeof e.grammarSource=="number"&&(n.grammarSource=this.getGrammarElement(e.grammarSource)),n.astNode=t.astNodes.get(e.astNode),Uh(n))for(let i of e.content){let a=this.hydrateCstNode(i,t,r++);n.content.push(a)}return n}hydrateCstLeafNode(e){let t=this.getTokenType(e.tokenType),r=e.offset,n=e.length,i=e.startLine,a=e.startColumn,s=e.endLine,l=e.endColumn,u=e.hidden;return new aR(r,n,{start:{line:i,character:a},end:{line:s,character:l}},t,u)}getTokenType(e){return this.lexer.definition[e]}getGrammarElementId(e){if(e)return this.grammarElementIdMap.size===0&&this.createGrammarElementIdMap(),this.grammarElementIdMap.get(e)}getGrammarElement(e){return this.grammarElementIdMap.size===0&&this.createGrammarElementIdMap(),this.grammarElementIdMap.getKey(e)}createGrammarElementIdMap(){let e=0;for(let t of kc(this.grammar))xR(t)&&this.grammarElementIdMap.set(t,e++)}};o(hn,"createDefaultCoreModule");E(hn,"createDefaultCoreModule");o(dn,"createDefaultSharedCoreModule");E(dn,"createDefaultSharedCoreModule");(function(e){e.merge=(t,r)=>Dv(Dv({},t),r)})(iV||(iV={}));o(Mr,"inject");E(Mr,"inject");wLe=Symbol("isProxy");o(BH,"eagerLoad");E(BH,"eagerLoad");o($H,"_inject");E($H,"_inject");owe=Symbol();o(aV,"_resolve");E(aV,"_resolve");o(Dv,"_merge");E(Dv,"_merge");sV={indentTokenName:"INDENT",dedentTokenName:"DEDENT",whitespaceTokenName:"WS",ignoreIndentationDelimiters:[]};(function(e){e.REGULAR="indentation-sensitive",e.IGNORE_INDENTATION="ignore-indentation"})(Ug||(Ug={}));kLe=class extends g_{static{o(this,"IndentationAwareTokenBuilder")}static{E(this,"IndentationAwareTokenBuilder")}constructor(e=sV){super(),this.indentationStack=[0],this.whitespaceRegExp=/[ \t]+/y,this.options={...sV,...e},this.indentTokenType=xv({name:this.options.indentTokenName,pattern:this.indentMatcher.bind(this),line_breaks:!1}),this.dedentTokenType=xv({name:this.options.dedentTokenName,pattern:this.dedentMatcher.bind(this),line_breaks:!1})}buildTokens(e,t){let r=super.buildTokens(e,t);if(!C_(r))throw new Error("Invalid tokens built by default builder");let{indentTokenName:n,dedentTokenName:i,whitespaceTokenName:a,ignoreIndentationDelimiters:s}=this.options,l,u,h,d=[];for(let f of r){for(let[p,m]of s)f.name===p?f.PUSH_MODE=Ug.IGNORE_INDENTATION:f.name===m&&(f.POP_MODE=!0);f.name===i?l=f:f.name===n?u=f:f.name===a?h=f:d.push(f)}if(!l||!u||!h)throw new Error("Some indentation/whitespace tokens not found!");return s.length>0?{modes:{[Ug.REGULAR]:[l,u,...d,h],[Ug.IGNORE_INDENTATION]:[...d,h]},defaultMode:Ug.REGULAR}:[l,u,h,...d]}flushLexingReport(e){return{...super.flushLexingReport(e),remainingDedents:this.flushRemainingDedents(e)}}isStartOfLine(e,t){return t===0||`\r +`.includes(e[t-1])}matchWhitespace(e,t,r,n){this.whitespaceRegExp.lastIndex=t;let i=this.whitespaceRegExp.exec(e);return{currIndentLevel:i?.[0].length??0,prevIndentLevel:this.indentationStack.at(-1),match:i}}createIndentationTokenInstance(e,t,r,n){let i=this.getLineNumber(t,n);return _w(e,r,n,n+r.length,i,i,1,r.length)}getLineNumber(e,t){return e.substring(0,t).split(/\r\n|\r|\n/).length}indentMatcher(e,t,r,n){if(!this.isStartOfLine(e,t))return null;let{currIndentLevel:i,prevIndentLevel:a,match:s}=this.matchWhitespace(e,t,r,n);return i<=a?null:(this.indentationStack.push(i),s)}dedentMatcher(e,t,r,n){if(!this.isStartOfLine(e,t))return null;let{currIndentLevel:i,prevIndentLevel:a,match:s}=this.matchWhitespace(e,t,r,n);if(i>=a)return null;let l=this.indentationStack.lastIndexOf(i);if(l===-1)return this.diagnostics.push({severity:"error",message:`Invalid dedent level ${i} at offset: ${t}. Current indentation stack: ${this.indentationStack}`,offset:t,length:s?.[0]?.length??0,line:this.getLineNumber(e,t),column:1}),null;let u=this.indentationStack.length-l-1,h=e.substring(0,t).match(/[\r\n]+$/)?.[0].length??1;for(let d=0;d1;)t.push(this.createIndentationTokenInstance(this.dedentTokenType,e,"",e.length)),this.indentationStack.pop();return this.indentationStack=[0],t}},OEt=class extends LH{static{o(this,"IndentationAwareLexer")}static{E(this,"IndentationAwareLexer")}constructor(e){if(super(e),e.parser.TokenBuilder instanceof kLe)this.indentationTokenBuilder=e.parser.TokenBuilder;else throw new Error("IndentationAwareLexer requires an accompanying IndentationAwareTokenBuilder")}tokenize(e,t=_H){let r=super.tokenize(e),n=r.report;t?.mode==="full"&&r.tokens.push(...n.remainingDedents),n.remainingDedents=[];let{indentTokenType:i,dedentTokenType:a}=this.indentationTokenBuilder,s=i.tokenTypeIdx,l=a.tokenTypeIdx,u=[],h=r.tokens.length-1;for(let d=0;d=0&&u.push(r.tokens[h]),r.tokens=u,r}},FH={};vp(FH,{AstUtils:o(()=>lW,"AstUtils"),BiMap:o(()=>uR,"BiMap"),Cancellation:o(()=>Qn,"Cancellation"),ContextCache:o(()=>b_,"ContextCache"),CstUtils:o(()=>aW,"CstUtils"),DONE_RESULT:o(()=>Os,"DONE_RESULT"),Deferred:o(()=>ed,"Deferred"),Disposable:o(()=>jg,"Disposable"),DisposableCache:o(()=>x_,"DisposableCache"),DocumentCache:o(()=>Y_e,"DocumentCache"),EMPTY_STREAM:o(()=>Tv,"EMPTY_STREAM"),ErrorWithLocation:o(()=>ER,"ErrorWithLocation"),GrammarUtils:o(()=>$W,"GrammarUtils"),MultiMap:o(()=>td,"MultiMap"),OperationCancelled:o(()=>Pu,"OperationCancelled"),Reduction:o(()=>XC,"Reduction"),RegExpUtils:o(()=>zW,"RegExpUtils"),SimpleCache:o(()=>kH,"SimpleCache"),StreamImpl:o(()=>Nu,"StreamImpl"),TreeStreamImpl:o(()=>Cv,"TreeStreamImpl"),URI:o(()=>Yo,"URI"),UriTrie:o(()=>CH,"UriTrie"),UriUtils:o(()=>$s,"UriUtils"),WorkspaceCache:o(()=>SH,"WorkspaceCache"),assertCondition:o(()=>FW,"assertCondition"),assertUnreachable:o(()=>xp,"assertUnreachable"),delayNextTick:o(()=>y_,"delayNextTick"),interruptAndCheck:o(()=>Ia,"interruptAndCheck"),isOperationCancelled:o(()=>x0,"isOperationCancelled"),loadGrammarFromJson:o(()=>Ma,"loadGrammarFromJson"),setInterruptionPeriod:o(()=>xH,"setInterruptionPeriod"),startCancelableOperation:o(()=>v_,"startCancelableOperation"),stream:o(()=>Bn,"stream")});mR(FH,T_);SLe=class{static{o(this,"EmptyFileSystemProvider")}static{E(this,"EmptyFileSystemProvider")}stat(e){throw new Error("No file system is available.")}statSync(e){throw new Error("No file system is available.")}async exists(){return!1}existsSync(){return!1}readBinary(){throw new Error("No file system is available.")}readBinarySync(){throw new Error("No file system is available.")}readFile(){throw new Error("No file system is available.")}readFileSync(){throw new Error("No file system is available.")}async readDirectory(){return[]}readDirectorySync(){return[]}},yn={fileSystemProvider:E(()=>new SLe,"fileSystemProvider")},BEt={Grammar:E(()=>{},"Grammar"),LanguageMetaData:E(()=>({caseInsensitive:!1,fileExtensions:[".langium"],languageId:"langium"}),"LanguageMetaData")},$Et={AstReflection:E(()=>new _W,"AstReflection")};o(ELe,"createMinimalGrammarServices");E(ELe,"createMinimalGrammarServices");o(Ma,"loadGrammarFromJson");E(Ma,"loadGrammarFromJson");mR(Owe,FH);FEt=class{static{o(this,"DefaultLangiumProfiler")}static{E(this,"DefaultLangiumProfiler")}constructor(e){this.activeCategories=new Set,this.allCategories=new Set(["validating","parsing","linking"]),this.activeCategories=e??new Set(this.allCategories),this.records=new td}isActive(e){return this.activeCategories.has(e)}start(...e){e?e.forEach(t=>this.activeCategories.add(t)):this.activeCategories=new Set(this.allCategories)}stop(...e){e?e.forEach(t=>this.activeCategories.delete(t)):this.activeCategories.clear()}createTask(e,t){if(!this.isActive(e))throw new Error(`Category "${e}" is not active.`);return console.log(`Creating profiling task for '${e}.${t}'.`),new ALe(r=>this.records.add(e,this.dumpRecord(e,r)),t)}dumpRecord(e,t){console.info(`Task ${e}.${t.identifier} executed in ${t.duration.toFixed(2)}ms and ended at ${t.date.toISOString()}`);let r=[];for(let a of t.entries.keys()){let s=t.entries.get(a),l=s.reduce((u,h)=>u+h);r.push({name:`${t.identifier}.${a}`,count:s.length,duration:l})}let n=t.duration-r.map(a=>a.duration).reduce((a,s)=>a+s,0);r.push({name:t.identifier,count:1,duration:n}),r.sort((a,s)=>s.duration-a.duration);function i(a){return Math.round(100*a)/100}return o(i,"Round"),E(i,"Round"),console.table(r.map(a=>({Element:a.name,Count:a.count,"Self %":i(100*a.duration/t.duration),"Time (ms)":i(a.duration)}))),t}getRecords(...e){return e.length===0?this.records.values():this.records.entries().filter(t=>e.some(r=>r===t[0])).flatMap(t=>t[1])}},ALe=class{static{o(this,"ProfilingTask")}static{E(this,"ProfilingTask")}constructor(e,t){this.stack=[],this.entries=new td,this.addRecord=e,this.identifier=t}start(){if(this.startTime!==void 0)throw new Error(`Task "${this.identifier}" is already started.`);this.startTime=performance.now()}stop(){if(this.startTime===void 0)throw new Error(`Task "${this.identifier}" was not started.`);if(this.stack.length!==0)throw new Error(`Task "${this.identifier}" cannot be stopped before sub-task(s): ${this.stack.map(t=>t.id).join(", ")}.`);let e={identifier:this.identifier,date:new Date,duration:performance.now()-this.startTime,entries:this.entries};this.addRecord(e),this.startTime=void 0,this.entries.clear()}startSubTask(e){this.stack.push({id:e,start:performance.now(),content:0})}stopSubTask(e){let t=this.stack.pop();if(!t)throw new Error(`Task "${this.identifier}.${e}" was not started.`);if(t.id!==e)throw new Error(`Sub-Task "${t.id}" is not already stopped.`);let r=performance.now()-t.start;this.stack.at(-1)!==void 0&&(this.stack[this.stack.length-1].content+=r);let n=r-t.content;this.entries.add(e,n)}};(e=>{e.Terminals={ARROW_DIRECTION:/L|R|T|B/,ARROW_GROUP:/\{group\}/,ARROW_INTO:/<|>/,ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,ID:/[\w]([-\w]*\w)?/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/,ARCH_ICON:/\([\w-:]+\)/,ARCH_TITLE:/\[(?:"([^"\\]|\\.)*"|'([^'\\]|\\.)*'|[\w ]+)\]/}})(oV||(oV={}));(e=>{e.Terminals={DOMAIN_NAME:/complex|complicated|clear|chaotic|confusion/,ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/}})(lV||(lV={}));(e=>{e.Terminals={EM_ID:/[_a-zA-Z][\w_]*/,EM_FID:/\d{1,3}/,EM_DATA_INLINE:/\{(.*)\}|"(.*)"|'(.*)'/,EM_DATA_BLOCK:/\{[\t ]*\r?\n(?:[\S\s]*?\r?\n)?\}(?:\r?\n|(?!\S))/,EM_ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,EM_ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,EM_TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,EM_WS:/\s+/,EM_YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,EM_DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,EM_SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/,EM_ML_COMMENT:/\/\*[\s\S]*?\*\//,EM_SL_COMMENT:/\/\/[^\n\r]*/}})(cV||(cV={}));(e=>{e.Terminals={ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,INT:/0|[1-9][0-9]*(?!\.)/,STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/,REFERENCE:/\w([-\./\w]*[-\w])?/}})(uV||(uV={}));(e=>{e.Terminals={ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/}})(hV||(hV={}));(e=>{e.Terminals={ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,INT:/0|[1-9][0-9]*(?!\.)/,STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/}})(dV||(dV={}));(e=>{e.Terminals={NUMBER_PIE:/(?:-?[0-9]+\.[0-9]+(?!\.))|(?:-?(0|[1-9][0-9]*)(?!\.))/,ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/}})(fV||(fV={}));(e=>{e.Terminals={GRATICULE:/circle|polygon/,BOOLEAN:/true|false/,ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,NUMBER:/(?:[0-9]+\.[0-9]+(?!\.))|(?:0|[1-9][0-9]*(?!\.))/,STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,ID:/[\w]([-\w]*\w)?/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/}})(pV||(pV={}));(e=>{e.Terminals={TITLE:/title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,ACC_TITLE:/accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,ACC_DESCR:/accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ABNF_RULENAME:/[A-Za-z][A-Za-z0-9-]*/,ABNF_STRING:/"[^"]*"/,ABNF_NUMVAL:/%[xXdDbB][0-9A-Fa-f]+(?:-[0-9A-Fa-f]+|\.[0-9A-Fa-f]+)*/,ABNF_REPEAT:/[0-9]*\*[0-9]*/,ABNF_EXACT_REPEAT:/[0-9]+/,ABNF_WHITESPACE:/[\t \r\n]+/,ABNF_YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,ABNF_DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,ABNF_SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/,ABNF_COMMENT:/;[^\n\r]*/}})(mV||(mV={}));(e=>{e.Terminals={TITLE:/title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,ACC_TITLE:/accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,ACC_DESCR:/accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,EBNF_ID:/[A-Z_a-z][\w-]*/,EBNF_STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,EBNF_SPECIAL_SEQUENCE:/\?(?=[^?;]*[^?\s;][^?;]*\?)[^?;]*\?/,EBNF_WHITESPACE:/[\t \r\n]+/,EBNF_YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,EBNF_DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,EBNF_SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/,EBNF_BLOCK_COMMENT:/\/\*[\s\S]*?\*\//,EBNF_ISO_COMMENT:/\(\*[\s\S]*?\*\)/}})(gV||(gV={}));(e=>{e.Terminals={TITLE:/title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,ACC_TITLE:/accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,ACC_DESCR:/accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,RR_ID:/[A-Z_a-z][\w-]*/,RR_STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,RR_WHITESPACE:/[\t \r\n]+/,RR_YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,RR_DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,RR_SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/,RR_BLOCK_COMMENT:/\/\*[\s\S]*?\*\//}})(yV||(yV={}));(e=>{e.Terminals={TITLE:/title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,ACC_TITLE:/accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,ACC_DESCR:/accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,PEG_ID:/[A-Z_a-z][\w-]*/,PEG_STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,PEG_WHITESPACE:/[\t \r\n]+/,PEG_YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,PEG_DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,PEG_SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/,PEG_LINE_COMMENT:/#[^\n\r]*/}})(vV||(vV={}));(e=>{e.Terminals={ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,TREEMAP_KEYWORD:/treemap-beta|treemap/,CLASS_DEF:/classDef\s+([a-zA-Z_][a-zA-Z0-9_]+)(?:\s+([^;\r\n]*))?(?:;)?/,STYLE_SEPARATOR:/:::/,SEPARATOR:/:/,COMMA:/,/,INDENTATION:/[ \t]{1,}/,WS:/[ \t]+/,ML_COMMENT:/\%\%[^\n]*/,NL:/\r?\n/,ID2:/[a-zA-Z_][a-zA-Z0-9_]*/,NUMBER2:/[0-9_\.\,]+/,STRING2:/"[^"]*"|'[^']*'/}})(xV||(xV={}));(e=>{e.Terminals={ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,CLASS_ANNOTATION:/[ \t]+:::[ \t]*[A-Za-z_][\w-]*/,ICON_ANNOTATION:/[ \t]+icon\([\w-]*(?::[\w-]+)?\)/,DESC_ANNOTATION:/[ \t]+##[^\n\r]*/,INDENTATION:/[ \t]{1,}/,QUOTED_NAME:/"[^"]*"|'[^']*'/,WS:/[ \t]+/,ML_COMMENT:/\%\%[^\n]*/,NL:/\r?\n/,BARE_NAME:/(?!:::|icon\(|##)[^ \t\n\r"'](?:(?![ \t]+:::[ \t]*[A-Za-z_]|[ \t]+icon\(|[ \t]+##)[^\n\r])*/}})(bV||(bV={}));(e=>{e.Terminals={WARDLEY_NUMBER:/[0-9]+\.[0-9]+/,ARROW:/->/,LINK_PORT:/\+<>|\+>|\+|-\.->|>|\+'[^']*'<>|\+'[^']*'<|\+'[^']*'>/,LINK_LABEL:/;[^\n\r]+/,STRATEGY:/build|buy|outsource|market/,KW_WARDLEY:/wardley-beta/,KW_SIZE:/size/,KW_EVOLUTION:/evolution/,KW_ANCHOR:/anchor/,KW_COMPONENT:/component/,KW_LABEL:/label/,KW_INERTIA:/inertia/,KW_EVOLVE:/evolve/,KW_PIPELINE:/pipeline/,KW_NOTE:/note/,KW_ANNOTATIONS:/annotations/,KW_ANNOTATION:/annotation/,KW_ACCELERATOR:/accelerator/,KW_DEACCELERATOR:/deaccelerator/,NAME_WITH_SPACES:/(?!title\s|accTitle|accDescr)[A-Za-z](?:[A-Za-z0-9_()&]|-(?!>))*(?:[ \t]+[A-Za-z(](?:[A-Za-z0-9_()&]|-(?!>))*)*/,WS:/[ \t]+/,ACC_DESCR:/[\t ]*accDescr(?:[\t ]*:([^\n\r]*?(?=%%)|[^\n\r]*)|\s*{([^}]*)})/,ACC_TITLE:/[\t ]*accTitle[\t ]*:(?:[^\n\r]*?(?=%%)|[^\n\r]*)/,TITLE:/[\t ]*title(?:[\t ][^\n\r]*?(?=%%)|[\t ][^\n\r]*|)/,INT:/0|[1-9][0-9]*(?!\.)/,STRING:/"([^"\\]|\\.)*"|'([^'\\]|\\.)*'/,ID:/[\w]([-\w]*\w)?/,NEWLINE:/\r?\n/,WHITESPACE:/[\t ]+/,YAML:/---[\t ]*\r?\n(?:[\S\s]*?\r?\n)?---(?:\r?\n|(?!\S))/,DIRECTIVE:/[\t ]*%%{[\S\s]*?}%%(?:\r?\n|(?!\S))/,SINGLE_LINE_COMMENT:/[\t ]*%%[^\n\r]*/}})(TV||(TV={}));WIr={...oV.Terminals,...lV.Terminals,...cV.Terminals,...uV.Terminals,...hV.Terminals,...dV.Terminals,...fV.Terminals,...pV.Terminals,...mV.Terminals,...gV.Terminals,...yV.Terminals,...vV.Terminals,...bV.Terminals,...xV.Terminals,...TV.Terminals},CV={$type:"AbnfAlternation",alternatives:"alternatives"},wV={$type:"AbnfConcatenation",elements:"elements"},v6={$type:"AbnfElement",primary:"primary",repeat:"repeat"},kV={$type:"AbnfGroup",element:"element"},SV={$type:"AbnfNumVal",value:"value"},EV={$type:"AbnfOptionalGroup",element:"element"},vg={$type:"AbnfPrimary"},x6={$type:"AbnfRule",definition:"definition",name:"name"},AV={$type:"AbnfRuleName",name:"name"},RV={$type:"AbnfStringLiteral",value:"value"},FA={$type:"Accelerator",name:"name",x:"x",y:"y"},hz={$type:"Alignment",direction:"direction",members:"members"},zA={$type:"Anchor",evolution:"evolution",name:"name",visibility:"visibility"},fC={$type:"Annotation",number:"number",text:"text",x:"x",y:"y"},dz={$type:"Annotations",x:"x",y:"y"},bc={$type:"Architecture",accDescr:"accDescr",accTitle:"accTitle",alignments:"alignments",edges:"edges",groups:"groups",junctions:"junctions",services:"services",title:"title"};o(RLe,"isArchitecture");E(RLe,"isArchitecture");GA={$type:"Axis",label:"label",name:"name"},HC={$type:"Branch",name:"name",order:"order"};o(_Le,"isBranch");E(_Le,"isBranch");lwe={$type:"Checkout",branch:"branch"},VA={$type:"CherryPicking",id:"id",parent:"parent",tags:"tags"},fz={$type:"ClassDefStatement",className:"className",styleText:"styleText"},Ng={$type:"Commit",id:"id",message:"message",tags:"tags",type:"type"};o(LLe,"isCommit");E(LLe,"isCommit");WA={$type:"Common",accDescr:"accDescr",accTitle:"accTitle",title:"title"},ug={$type:"Component",decorator:"decorator",evolution:"evolution",inertia:"inertia",label:"label",name:"name",visibility:"visibility"},qA={$type:"Curve",entries:"entries",label:"label",name:"name"},up={$type:"Cynefin",accDescr:"accDescr",accTitle:"accTitle",domains:"domains",title:"title",transitions:"transitions"};o(DLe,"isCynefin");E(DLe,"isCynefin");HA={$type:"Deaccelerator",name:"name",x:"x",y:"y"},cwe={$type:"Decorator",strategy:"strategy"},K1={$type:"Direction",accDescr:"accDescr",accTitle:"accTitle",dir:"dir",statements:"statements",title:"title"},UC={$type:"DomainBlock",domain:"domain",items:"items"};o(ILe,"isDomainBlock");E(ILe,"isDomainBlock");pR={$type:"DomainItem",label:"label"};o(MLe,"isDomainItem");E(MLe,"isDomainItem");_V={$type:"EbnfChoice",alternatives:"alternatives"},LV={$type:"EbnfExceptionPostfix",except:"except"},DV={$type:"EbnfGroup",element:"element"},IV={$type:"EbnfNonTerminal",name:"name"},MV={$type:"EbnfOneOrMorePostfix",operator:"operator"},NV={$type:"EbnfOptional",element:"element"},PV={$type:"EbnfOptionalPostfix",operator:"operator"},iv={$type:"EbnfPostfix"},rp={$type:"EbnfPrimary"},OV={$type:"EbnfRepetition",element:"element"},b6={$type:"EbnfRule",definition:"definition",name:"name"},BV={$type:"EbnfSequence",elements:"elements"},$V={$type:"EbnfSpecial",text:"text"},T6={$type:"EbnfTerm",base:"base",postfixes:"postfixes"},FV={$type:"EbnfTerminal",value:"value"},zV={$type:"EbnfZeroOrMorePostfix",operator:"operator"},Au={$type:"Edge",lhsDir:"lhsDir",lhsGroup:"lhsGroup",lhsId:"lhsId",lhsInto:"lhsInto",rhsDir:"rhsDir",rhsGroup:"rhsGroup",rhsId:"rhsId",rhsInto:"rhsInto",title:"title"},xg={$type:"EmDataEntity",dataBlockValue:"dataBlockValue",dataType:"dataType",name:"name"},np={$type:"EmFrame"},pC={$type:"EmGwt",givenStatements:"givenStatements",sourceFrame:"sourceFrame",thenStatements:"thenStatements",whenStatements:"whenStatements"},uwe={$type:"EmGwtStatement",entityIdentifier:"entityIdentifier"},pz={$type:"EmModelEntity",name:"name"};o(NLe,"isEmModelEntityType");E(NLe,"isEmModelEntityType");UA={$type:"EmNoteEntity",dataBlockValue:"dataBlockValue",dataType:"dataType",sourceFrame:"sourceFrame"},Vh={$type:"EmResetFrame",dataInlineValue:"dataInlineValue",dataReference:"dataReference",dataType:"dataType",entityIdentifier:"entityIdentifier",modelEntityType:"modelEntityType",name:"name",sourceFrames:"sourceFrames"};o(S_,"isEmResetFrame");E(S_,"isEmResetFrame");tp={$type:"EmTimeFrame",dataInlineValue:"dataInlineValue",dataReference:"dataReference",dataType:"dataType",entityIdentifier:"entityIdentifier",modelEntityType:"modelEntityType",name:"name",sourceFrames:"sourceFrames"},mz={$type:"Entry",axis:"axis",value:"value"},_u={$type:"EventModel",accDescr:"accDescr",accTitle:"accTitle",dataEntities:"dataEntities",frames:"frames",gwtEntities:"gwtEntities",modelEntities:"modelEntities",noteEntities:"noteEntities",title:"title"},hwe={$type:"Evolution",stages:"stages"},YA={$type:"EvolutionStage",boundary:"boundary",name:"name",secondName:"secondName"},gz={$type:"Evolve",component:"component",target:"target"},hp={$type:"GitGraph",accDescr:"accDescr",accTitle:"accTitle",statements:"statements",title:"title"};o(PLe,"isGitGraph");E(PLe,"isGitGraph");mC={$type:"Group",icon:"icon",id:"id",in:"in",title:"title"},mv={$type:"Info",accDescr:"accDescr",accTitle:"accTitle",title:"title"};o(OLe,"isInfo");E(OLe,"isInfo");gC={$type:"Item",classSelector:"classSelector",name:"name"},yz={$type:"Junction",id:"id",in:"in"},yC={$type:"Label",negX:"negX",negY:"negY",offsetX:"offsetX",offsetY:"offsetY"},jA={$type:"Leaf",classSelector:"classSelector",name:"name",value:"value"},hg={$type:"Link",arrow:"arrow",from:"from",fromPort:"fromPort",linkLabel:"linkLabel",to:"to",toPort:"toPort"},Pg={$type:"Merge",branch:"branch",id:"id",tags:"tags",type:"type"};o(BLe,"isMerge");E(BLe,"isMerge");XA={$type:"Note",evolution:"evolution",text:"text",visibility:"visibility"},vz={$type:"Option",name:"name",value:"value"},Og={$type:"Packet",accDescr:"accDescr",accTitle:"accTitle",blocks:"blocks",title:"title"};o($Le,"isPacket");E($Le,"isPacket");Bg={$type:"PacketBlock",bits:"bits",end:"end",label:"label",start:"start"};o(FLe,"isPacketBlock");E(FLe,"isPacketBlock");GV={$type:"PegAny",dot:"dot"},VV={$type:"PegGroup",element:"element"},WV={$type:"PegIdentifier",name:"name"},qV={$type:"PegLiteral",value:"value"},HV={$type:"PegOrderedChoice",alternatives:"alternatives"},C6={$type:"PegPrefix",operator:"operator",suffix:"suffix"},av={$type:"PegPrimary"},w6={$type:"PegRule",definition:"definition",name:"name"},UV={$type:"PegSequence",elements:"elements"},k6={$type:"PegSuffix",operator:"operator",primary:"primary"},dp={$type:"Pie",accDescr:"accDescr",accTitle:"accTitle",sections:"sections",showData:"showData",title:"title"};o(zLe,"isPie");E(zLe,"isPie");YC={$type:"PieSection",label:"label",value:"value"};o(GLe,"isPieSection");E(GLe,"isPieSection");xz={$type:"Pipeline",components:"components",parent:"parent"},KA={$type:"PipelineComponent",evolution:"evolution",label:"label",name:"name"},ip={$type:"Radar",accDescr:"accDescr",accTitle:"accTitle",axes:"axes",curves:"curves",options:"options",title:"title"},$g={$type:"Railroad",accDescr:"accDescr",accTitle:"accTitle",rules:"rules",title:"title"};o(VLe,"isRailroad");E(VLe,"isRailroad");Fg={$type:"RailroadAbnf",accDescr:"accDescr",accTitle:"accTitle",rules:"rules",title:"title"};o(WLe,"isRailroadAbnf");E(WLe,"isRailroadAbnf");YV={$type:"RailroadChoiceExpr",alternatives:"alternatives"},zg={$type:"RailroadEbnf",accDescr:"accDescr",accTitle:"accTitle",rules:"rules",title:"title"};o(qLe,"isRailroadEbnf");E(qLe,"isRailroadEbnf");Lu={$type:"RailroadExpression"},jV={$type:"RailroadNonTerminalExpr",name:"name"},XV={$type:"RailroadOneOrMoreExpr",element:"element"},KV={$type:"RailroadOptionalExpr",element:"element"},Gg={$type:"RailroadPeg",accDescr:"accDescr",accTitle:"accTitle",rules:"rules",title:"title"};o(HLe,"isRailroadPeg");E(HLe,"isRailroadPeg");S6={$type:"RailroadRule",definition:"definition",name:"name"},ZV={$type:"RailroadSequenceExpr",elements:"elements"},QV={$type:"RailroadSpecialExpr",text:"text"},JV={$type:"RailroadTerminalExpr",value:"value"},eW={$type:"RailroadZeroOrMoreExpr",element:"element"},bz={$type:"Section",classSelector:"classSelector",name:"name"},Z1={$type:"Service",icon:"icon",iconText:"iconText",id:"id",in:"in",title:"title"},Tz={$type:"Size",height:"height",width:"width"},bg={$type:"Statement"},gv={$type:"Transition",from:"from",label:"label",to:"to"};o(ULe,"isTransition");E(ULe,"isTransition");Vg={$type:"Treemap",accDescr:"accDescr",accTitle:"accTitle",title:"title",TreemapRows:"TreemapRows"};o(YLe,"isTreemap");E(YLe,"isTreemap");Cz={$type:"TreemapRow",indent:"indent",item:"item"},Tg={$type:"TreeNode",classAnnotation:"classAnnotation",descAnnotation:"descAnnotation",iconAnnotation:"iconAnnotation",indent:"indent",name:"name"},sv={$type:"TreeView",accDescr:"accDescr",accTitle:"accTitle",nodes:"nodes",title:"title"},La={$type:"Wardley",accDescr:"accDescr",accelerators:"accelerators",accTitle:"accTitle",anchors:"anchors",annotation:"annotation",annotations:"annotations",components:"components",deaccelerators:"deaccelerators",evolution:"evolution",evolves:"evolves",links:"links",notes:"notes",pipelines:"pipelines",size:"size",title:"title"};o(jLe,"isWardley");E(jLe,"isWardley");XLe=class extends oW{static{o(this,"MermaidAstReflection")}constructor(){super(...arguments),this.types={AbnfAlternation:{name:CV.$type,properties:{alternatives:{name:CV.alternatives,defaultValue:[]}},superTypes:[]},AbnfConcatenation:{name:wV.$type,properties:{elements:{name:wV.elements,defaultValue:[]}},superTypes:[]},AbnfElement:{name:v6.$type,properties:{primary:{name:v6.primary},repeat:{name:v6.repeat}},superTypes:[]},AbnfGroup:{name:kV.$type,properties:{element:{name:kV.element}},superTypes:[vg.$type]},AbnfNumVal:{name:SV.$type,properties:{value:{name:SV.value}},superTypes:[vg.$type]},AbnfOptionalGroup:{name:EV.$type,properties:{element:{name:EV.element}},superTypes:[vg.$type]},AbnfPrimary:{name:vg.$type,properties:{},superTypes:[]},AbnfRule:{name:x6.$type,properties:{definition:{name:x6.definition},name:{name:x6.name}},superTypes:[]},AbnfRuleName:{name:AV.$type,properties:{name:{name:AV.name}},superTypes:[vg.$type]},AbnfStringLiteral:{name:RV.$type,properties:{value:{name:RV.value}},superTypes:[vg.$type]},Accelerator:{name:FA.$type,properties:{name:{name:FA.name},x:{name:FA.x},y:{name:FA.y}},superTypes:[]},Alignment:{name:hz.$type,properties:{direction:{name:hz.direction},members:{name:hz.members,defaultValue:[]}},superTypes:[]},Anchor:{name:zA.$type,properties:{evolution:{name:zA.evolution},name:{name:zA.name},visibility:{name:zA.visibility}},superTypes:[]},Annotation:{name:fC.$type,properties:{number:{name:fC.number},text:{name:fC.text},x:{name:fC.x},y:{name:fC.y}},superTypes:[]},Annotations:{name:dz.$type,properties:{x:{name:dz.x},y:{name:dz.y}},superTypes:[]},Architecture:{name:bc.$type,properties:{accDescr:{name:bc.accDescr},accTitle:{name:bc.accTitle},alignments:{name:bc.alignments,defaultValue:[]},edges:{name:bc.edges,defaultValue:[]},groups:{name:bc.groups,defaultValue:[]},junctions:{name:bc.junctions,defaultValue:[]},services:{name:bc.services,defaultValue:[]},title:{name:bc.title}},superTypes:[]},Axis:{name:GA.$type,properties:{label:{name:GA.label},name:{name:GA.name}},superTypes:[]},Branch:{name:HC.$type,properties:{name:{name:HC.name},order:{name:HC.order}},superTypes:[bg.$type]},Checkout:{name:lwe.$type,properties:{branch:{name:lwe.branch}},superTypes:[bg.$type]},CherryPicking:{name:VA.$type,properties:{id:{name:VA.id},parent:{name:VA.parent},tags:{name:VA.tags,defaultValue:[]}},superTypes:[bg.$type]},ClassDefStatement:{name:fz.$type,properties:{className:{name:fz.className},styleText:{name:fz.styleText}},superTypes:[]},Commit:{name:Ng.$type,properties:{id:{name:Ng.id},message:{name:Ng.message},tags:{name:Ng.tags,defaultValue:[]},type:{name:Ng.type}},superTypes:[bg.$type]},Common:{name:WA.$type,properties:{accDescr:{name:WA.accDescr},accTitle:{name:WA.accTitle},title:{name:WA.title}},superTypes:[]},Component:{name:ug.$type,properties:{decorator:{name:ug.decorator},evolution:{name:ug.evolution},inertia:{name:ug.inertia,defaultValue:!1},label:{name:ug.label},name:{name:ug.name},visibility:{name:ug.visibility}},superTypes:[]},Curve:{name:qA.$type,properties:{entries:{name:qA.entries,defaultValue:[]},label:{name:qA.label},name:{name:qA.name}},superTypes:[]},Cynefin:{name:up.$type,properties:{accDescr:{name:up.accDescr},accTitle:{name:up.accTitle},domains:{name:up.domains,defaultValue:[]},title:{name:up.title},transitions:{name:up.transitions,defaultValue:[]}},superTypes:[]},Deaccelerator:{name:HA.$type,properties:{name:{name:HA.name},x:{name:HA.x},y:{name:HA.y}},superTypes:[]},Decorator:{name:cwe.$type,properties:{strategy:{name:cwe.strategy}},superTypes:[]},Direction:{name:K1.$type,properties:{accDescr:{name:K1.accDescr},accTitle:{name:K1.accTitle},dir:{name:K1.dir},statements:{name:K1.statements,defaultValue:[]},title:{name:K1.title}},superTypes:[hp.$type]},DomainBlock:{name:UC.$type,properties:{domain:{name:UC.domain},items:{name:UC.items,defaultValue:[]}},superTypes:[]},DomainItem:{name:pR.$type,properties:{label:{name:pR.label}},superTypes:[]},EbnfChoice:{name:_V.$type,properties:{alternatives:{name:_V.alternatives,defaultValue:[]}},superTypes:[]},EbnfExceptionPostfix:{name:LV.$type,properties:{except:{name:LV.except}},superTypes:[iv.$type]},EbnfGroup:{name:DV.$type,properties:{element:{name:DV.element}},superTypes:[rp.$type]},EbnfNonTerminal:{name:IV.$type,properties:{name:{name:IV.name}},superTypes:[rp.$type]},EbnfOneOrMorePostfix:{name:MV.$type,properties:{operator:{name:MV.operator}},superTypes:[iv.$type]},EbnfOptional:{name:NV.$type,properties:{element:{name:NV.element}},superTypes:[rp.$type]},EbnfOptionalPostfix:{name:PV.$type,properties:{operator:{name:PV.operator}},superTypes:[iv.$type]},EbnfPostfix:{name:iv.$type,properties:{},superTypes:[]},EbnfPrimary:{name:rp.$type,properties:{},superTypes:[]},EbnfRepetition:{name:OV.$type,properties:{element:{name:OV.element}},superTypes:[rp.$type]},EbnfRule:{name:b6.$type,properties:{definition:{name:b6.definition},name:{name:b6.name}},superTypes:[]},EbnfSequence:{name:BV.$type,properties:{elements:{name:BV.elements,defaultValue:[]}},superTypes:[]},EbnfSpecial:{name:$V.$type,properties:{text:{name:$V.text}},superTypes:[rp.$type]},EbnfTerm:{name:T6.$type,properties:{base:{name:T6.base},postfixes:{name:T6.postfixes,defaultValue:[]}},superTypes:[]},EbnfTerminal:{name:FV.$type,properties:{value:{name:FV.value}},superTypes:[rp.$type]},EbnfZeroOrMorePostfix:{name:zV.$type,properties:{operator:{name:zV.operator}},superTypes:[iv.$type]},Edge:{name:Au.$type,properties:{lhsDir:{name:Au.lhsDir},lhsGroup:{name:Au.lhsGroup,defaultValue:!1},lhsId:{name:Au.lhsId},lhsInto:{name:Au.lhsInto,defaultValue:!1},rhsDir:{name:Au.rhsDir},rhsGroup:{name:Au.rhsGroup,defaultValue:!1},rhsId:{name:Au.rhsId},rhsInto:{name:Au.rhsInto,defaultValue:!1},title:{name:Au.title}},superTypes:[]},EmDataEntity:{name:xg.$type,properties:{dataBlockValue:{name:xg.dataBlockValue},dataType:{name:xg.dataType},name:{name:xg.name}},superTypes:[]},EmFrame:{name:np.$type,properties:{},superTypes:[]},EmGwt:{name:pC.$type,properties:{givenStatements:{name:pC.givenStatements,defaultValue:[]},sourceFrame:{name:pC.sourceFrame,referenceType:np.$type},thenStatements:{name:pC.thenStatements,defaultValue:[]},whenStatements:{name:pC.whenStatements,defaultValue:[]}},superTypes:[]},EmGwtStatement:{name:uwe.$type,properties:{entityIdentifier:{name:uwe.entityIdentifier,referenceType:pz.$type}},superTypes:[]},EmModelEntity:{name:pz.$type,properties:{name:{name:pz.name}},superTypes:[]},EmNoteEntity:{name:UA.$type,properties:{dataBlockValue:{name:UA.dataBlockValue},dataType:{name:UA.dataType},sourceFrame:{name:UA.sourceFrame,referenceType:np.$type}},superTypes:[]},EmResetFrame:{name:Vh.$type,properties:{dataInlineValue:{name:Vh.dataInlineValue},dataReference:{name:Vh.dataReference,referenceType:xg.$type},dataType:{name:Vh.dataType},entityIdentifier:{name:Vh.entityIdentifier},modelEntityType:{name:Vh.modelEntityType},name:{name:Vh.name},sourceFrames:{name:Vh.sourceFrames,defaultValue:[],referenceType:np.$type}},superTypes:[np.$type]},EmTimeFrame:{name:tp.$type,properties:{dataInlineValue:{name:tp.dataInlineValue},dataReference:{name:tp.dataReference,referenceType:xg.$type},dataType:{name:tp.dataType},entityIdentifier:{name:tp.entityIdentifier},modelEntityType:{name:tp.modelEntityType},name:{name:tp.name},sourceFrames:{name:tp.sourceFrames,defaultValue:[],referenceType:np.$type}},superTypes:[np.$type]},Entry:{name:mz.$type,properties:{axis:{name:mz.axis,referenceType:GA.$type},value:{name:mz.value}},superTypes:[]},EventModel:{name:_u.$type,properties:{accDescr:{name:_u.accDescr},accTitle:{name:_u.accTitle},dataEntities:{name:_u.dataEntities,defaultValue:[]},frames:{name:_u.frames,defaultValue:[]},gwtEntities:{name:_u.gwtEntities,defaultValue:[]},modelEntities:{name:_u.modelEntities,defaultValue:[]},noteEntities:{name:_u.noteEntities,defaultValue:[]},title:{name:_u.title}},superTypes:[]},Evolution:{name:hwe.$type,properties:{stages:{name:hwe.stages,defaultValue:[]}},superTypes:[]},EvolutionStage:{name:YA.$type,properties:{boundary:{name:YA.boundary},name:{name:YA.name},secondName:{name:YA.secondName}},superTypes:[]},Evolve:{name:gz.$type,properties:{component:{name:gz.component},target:{name:gz.target}},superTypes:[]},GitGraph:{name:hp.$type,properties:{accDescr:{name:hp.accDescr},accTitle:{name:hp.accTitle},statements:{name:hp.statements,defaultValue:[]},title:{name:hp.title}},superTypes:[]},Group:{name:mC.$type,properties:{icon:{name:mC.icon},id:{name:mC.id},in:{name:mC.in},title:{name:mC.title}},superTypes:[]},Info:{name:mv.$type,properties:{accDescr:{name:mv.accDescr},accTitle:{name:mv.accTitle},title:{name:mv.title}},superTypes:[]},Item:{name:gC.$type,properties:{classSelector:{name:gC.classSelector},name:{name:gC.name}},superTypes:[]},Junction:{name:yz.$type,properties:{id:{name:yz.id},in:{name:yz.in}},superTypes:[]},Label:{name:yC.$type,properties:{negX:{name:yC.negX,defaultValue:!1},negY:{name:yC.negY,defaultValue:!1},offsetX:{name:yC.offsetX},offsetY:{name:yC.offsetY}},superTypes:[]},Leaf:{name:jA.$type,properties:{classSelector:{name:jA.classSelector},name:{name:jA.name},value:{name:jA.value}},superTypes:[gC.$type]},Link:{name:hg.$type,properties:{arrow:{name:hg.arrow},from:{name:hg.from},fromPort:{name:hg.fromPort},linkLabel:{name:hg.linkLabel},to:{name:hg.to},toPort:{name:hg.toPort}},superTypes:[]},Merge:{name:Pg.$type,properties:{branch:{name:Pg.branch},id:{name:Pg.id},tags:{name:Pg.tags,defaultValue:[]},type:{name:Pg.type}},superTypes:[bg.$type]},Note:{name:XA.$type,properties:{evolution:{name:XA.evolution},text:{name:XA.text},visibility:{name:XA.visibility}},superTypes:[]},Option:{name:vz.$type,properties:{name:{name:vz.name},value:{name:vz.value,defaultValue:!1}},superTypes:[]},Packet:{name:Og.$type,properties:{accDescr:{name:Og.accDescr},accTitle:{name:Og.accTitle},blocks:{name:Og.blocks,defaultValue:[]},title:{name:Og.title}},superTypes:[]},PacketBlock:{name:Bg.$type,properties:{bits:{name:Bg.bits},end:{name:Bg.end},label:{name:Bg.label},start:{name:Bg.start}},superTypes:[]},PegAny:{name:GV.$type,properties:{dot:{name:GV.dot}},superTypes:[av.$type]},PegGroup:{name:VV.$type,properties:{element:{name:VV.element}},superTypes:[av.$type]},PegIdentifier:{name:WV.$type,properties:{name:{name:WV.name}},superTypes:[av.$type]},PegLiteral:{name:qV.$type,properties:{value:{name:qV.value}},superTypes:[av.$type]},PegOrderedChoice:{name:HV.$type,properties:{alternatives:{name:HV.alternatives,defaultValue:[]}},superTypes:[]},PegPrefix:{name:C6.$type,properties:{operator:{name:C6.operator},suffix:{name:C6.suffix}},superTypes:[]},PegPrimary:{name:av.$type,properties:{},superTypes:[]},PegRule:{name:w6.$type,properties:{definition:{name:w6.definition},name:{name:w6.name}},superTypes:[]},PegSequence:{name:UV.$type,properties:{elements:{name:UV.elements,defaultValue:[]}},superTypes:[]},PegSuffix:{name:k6.$type,properties:{operator:{name:k6.operator},primary:{name:k6.primary}},superTypes:[]},Pie:{name:dp.$type,properties:{accDescr:{name:dp.accDescr},accTitle:{name:dp.accTitle},sections:{name:dp.sections,defaultValue:[]},showData:{name:dp.showData,defaultValue:!1},title:{name:dp.title}},superTypes:[]},PieSection:{name:YC.$type,properties:{label:{name:YC.label},value:{name:YC.value}},superTypes:[]},Pipeline:{name:xz.$type,properties:{components:{name:xz.components,defaultValue:[]},parent:{name:xz.parent}},superTypes:[]},PipelineComponent:{name:KA.$type,properties:{evolution:{name:KA.evolution},label:{name:KA.label},name:{name:KA.name}},superTypes:[]},Radar:{name:ip.$type,properties:{accDescr:{name:ip.accDescr},accTitle:{name:ip.accTitle},axes:{name:ip.axes,defaultValue:[]},curves:{name:ip.curves,defaultValue:[]},options:{name:ip.options,defaultValue:[]},title:{name:ip.title}},superTypes:[]},Railroad:{name:$g.$type,properties:{accDescr:{name:$g.accDescr},accTitle:{name:$g.accTitle},rules:{name:$g.rules,defaultValue:[]},title:{name:$g.title}},superTypes:[]},RailroadAbnf:{name:Fg.$type,properties:{accDescr:{name:Fg.accDescr},accTitle:{name:Fg.accTitle},rules:{name:Fg.rules,defaultValue:[]},title:{name:Fg.title}},superTypes:[]},RailroadChoiceExpr:{name:YV.$type,properties:{alternatives:{name:YV.alternatives,defaultValue:[]}},superTypes:[Lu.$type]},RailroadEbnf:{name:zg.$type,properties:{accDescr:{name:zg.accDescr},accTitle:{name:zg.accTitle},rules:{name:zg.rules,defaultValue:[]},title:{name:zg.title}},superTypes:[]},RailroadExpression:{name:Lu.$type,properties:{},superTypes:[]},RailroadNonTerminalExpr:{name:jV.$type,properties:{name:{name:jV.name}},superTypes:[Lu.$type]},RailroadOneOrMoreExpr:{name:XV.$type,properties:{element:{name:XV.element}},superTypes:[Lu.$type]},RailroadOptionalExpr:{name:KV.$type,properties:{element:{name:KV.element}},superTypes:[Lu.$type]},RailroadPeg:{name:Gg.$type,properties:{accDescr:{name:Gg.accDescr},accTitle:{name:Gg.accTitle},rules:{name:Gg.rules,defaultValue:[]},title:{name:Gg.title}},superTypes:[]},RailroadRule:{name:S6.$type,properties:{definition:{name:S6.definition},name:{name:S6.name}},superTypes:[]},RailroadSequenceExpr:{name:ZV.$type,properties:{elements:{name:ZV.elements,defaultValue:[]}},superTypes:[Lu.$type]},RailroadSpecialExpr:{name:QV.$type,properties:{text:{name:QV.text}},superTypes:[Lu.$type]},RailroadTerminalExpr:{name:JV.$type,properties:{value:{name:JV.value}},superTypes:[Lu.$type]},RailroadZeroOrMoreExpr:{name:eW.$type,properties:{element:{name:eW.element}},superTypes:[Lu.$type]},Section:{name:bz.$type,properties:{classSelector:{name:bz.classSelector},name:{name:bz.name}},superTypes:[gC.$type]},Service:{name:Z1.$type,properties:{icon:{name:Z1.icon},iconText:{name:Z1.iconText},id:{name:Z1.id},in:{name:Z1.in},title:{name:Z1.title}},superTypes:[]},Size:{name:Tz.$type,properties:{height:{name:Tz.height},width:{name:Tz.width}},superTypes:[]},Statement:{name:bg.$type,properties:{},superTypes:[]},Transition:{name:gv.$type,properties:{from:{name:gv.from},label:{name:gv.label},to:{name:gv.to}},superTypes:[]},TreeNode:{name:Tg.$type,properties:{classAnnotation:{name:Tg.classAnnotation},descAnnotation:{name:Tg.descAnnotation},iconAnnotation:{name:Tg.iconAnnotation},indent:{name:Tg.indent},name:{name:Tg.name}},superTypes:[]},TreeView:{name:sv.$type,properties:{accDescr:{name:sv.accDescr},accTitle:{name:sv.accTitle},nodes:{name:sv.nodes,defaultValue:[]},title:{name:sv.title}},superTypes:[]},Treemap:{name:Vg.$type,properties:{accDescr:{name:Vg.accDescr},accTitle:{name:Vg.accTitle},title:{name:Vg.title},TreemapRows:{name:Vg.TreemapRows,defaultValue:[]}},superTypes:[]},TreemapRow:{name:Cz.$type,properties:{indent:{name:Cz.indent},item:{name:Cz.item}},superTypes:[]},Wardley:{name:La.$type,properties:{accDescr:{name:La.accDescr},accelerators:{name:La.accelerators,defaultValue:[]},accTitle:{name:La.accTitle},anchors:{name:La.anchors,defaultValue:[]},annotation:{name:La.annotation,defaultValue:[]},annotations:{name:La.annotations,defaultValue:[]},components:{name:La.components,defaultValue:[]},deaccelerators:{name:La.deaccelerators,defaultValue:[]},evolution:{name:La.evolution},evolves:{name:La.evolves,defaultValue:[]},links:{name:La.links,defaultValue:[]},notes:{name:La.notes,defaultValue:[]},pipelines:{name:La.pipelines,defaultValue:[]},size:{name:La.size},title:{name:La.title}},superTypes:[]}}}static{E(this,"MermaidAstReflection")}},Fi=new XLe,zEt=E(()=>dwe??(dwe=Ma(`{"$type":"Grammar","isDeclared":true,"name":"ArchitectureGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"Architecture","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[],"cardinality":"*"},{"$type":"Keyword","value":"architecture-beta"},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"Statement","definition":{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"groups","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Assignment","feature":"services","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}},{"$type":"Assignment","feature":"junctions","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}},{"$type":"Assignment","feature":"edges","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}},{"$type":"Assignment","feature":"alignments","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}}]},"entry":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"LeftPort","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":":"},{"$type":"Assignment","feature":"lhsDir","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]}}]},"entry":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"RightPort","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"rhsDir","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]}},{"$type":"Keyword","value":":"}]},"entry":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"Arrow","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]},{"$type":"Assignment","feature":"lhsInto","operator":"?=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]},"cardinality":"?"},{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"--"},{"$type":"Group","elements":[{"$type":"Keyword","value":"-"},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@30"},"arguments":[]}},{"$type":"Keyword","value":"-"}]}]},{"$type":"Assignment","feature":"rhsInto","operator":"?=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]},"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}]},"entry":false,"parameters":[]},{"$type":"ParserRule","name":"Group","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"group"},{"$type":"Assignment","feature":"id","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"Assignment","feature":"icon","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@29"},"arguments":[]},"cardinality":"?"},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@30"},"arguments":[]},"cardinality":"?"},{"$type":"Group","elements":[{"$type":"Keyword","value":"in"},{"$type":"Assignment","feature":"in","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Service","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"service"},{"$type":"Assignment","feature":"id","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"iconText","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@22"},"arguments":[]}},{"$type":"Assignment","feature":"icon","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@29"},"arguments":[]}}],"cardinality":"?"},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@30"},"arguments":[]},"cardinality":"?"},{"$type":"Group","elements":[{"$type":"Keyword","value":"in"},{"$type":"Assignment","feature":"in","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Junction","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"junction"},{"$type":"Assignment","feature":"id","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"in"},{"$type":"Assignment","feature":"in","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Edge","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"lhsId","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"Assignment","feature":"lhsGroup","operator":"?=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]},"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]},{"$type":"Assignment","feature":"rhsId","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"Assignment","feature":"rhsGroup","operator":"?=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]},"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Alignment","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"align"},{"$type":"Assignment","feature":"direction","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"row"},{"$type":"Keyword","value":"column"}]}},{"$type":"Assignment","feature":"members","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"Assignment","feature":"members","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]},"cardinality":"+"},{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"ARROW_DIRECTION","definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"L"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"R"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"T"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"B"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ARROW_GROUP","definition":{"$type":"RegexToken","regex":"/\\\\{group\\\\}/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ARROW_INTO","definition":{"$type":"RegexToken","regex":"/<|>/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@19"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@20"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","name":"ARCH_ICON","definition":{"$type":"RegexToken","regex":"/\\\\([\\\\w-:]+\\\\)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ARCH_TITLE","definition":{"$type":"RegexToken","regex":"/\\\\[(?:\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'|[\\\\w ]+)\\\\]/","parenthesized":false},"fragment":false,"hidden":false}],"interfaces":[],"types":[]}`)),"ArchitectureGrammarGrammar"),GEt=E(()=>fwe??(fwe=Ma(`{"$type":"Grammar","isDeclared":true,"name":"CynefinGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"Cynefin","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[],"cardinality":"*"},{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"cynefin-beta"},{"$type":"Keyword","value":"cynefin-beta:"}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[],"cardinality":"*"},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]},{"$type":"Assignment","feature":"domains","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"transitions","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"DomainBlock","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"domain","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[],"cardinality":"*"},{"$type":"Group","elements":[{"$type":"Assignment","feature":"items","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[],"cardinality":"*"}],"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"DomainItem","definition":{"$type":"Assignment","feature":"label","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Transition","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"from","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"Keyword","value":"-->"},{"$type":"Assignment","feature":"to","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":":"},{"$type":"Assignment","feature":"label","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"DOMAIN_NAME","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"complex"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"complicated"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"clear"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"chaotic"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"confusion"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@11"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@12"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false}],"interfaces":[],"types":[]}`)),"CynefinGrammarGrammar"),VEt=E(()=>pwe??(pwe=Ma('{"$type":"Grammar","isDeclared":true,"name":"EventModeling","interfaces":[{"$type":"Interface","name":"Common","attributes":[{"$type":"TypeAttribute","name":"accDescr","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accTitle","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"title","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}}],"superTypes":[]}],"rules":[{"$type":"ParserRule","entry":true,"name":"EventModel","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"eventmodeling"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@21"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@22"},"arguments":[]}},{"$type":"Assignment","feature":"modelEntities","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}},{"$type":"Assignment","feature":"frames","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}},{"$type":"Assignment","feature":"dataEntities","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]}},{"$type":"Assignment","feature":"noteEntities","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]}},{"$type":"Assignment","feature":"gwtEntities","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmModelEntityType","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"rmo"},{"$type":"Keyword","value":"readmodel"},{"$type":"Keyword","value":"ui"},{"$type":"Keyword","value":"cmd"},{"$type":"Keyword","value":"command"},{"$type":"Keyword","value":"evt"},{"$type":"Keyword","value":"event"},{"$type":"Keyword","value":"pcr"},{"$type":"Keyword","value":"processor"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmDataType","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"json"},{"$type":"Keyword","value":"jsobj"},{"$type":"Keyword","value":"figma"},{"$type":"Keyword","value":"salt"},{"$type":"Keyword","value":"uri"},{"$type":"Keyword","value":"md"},{"$type":"Keyword","value":"html"},{"$type":"Keyword","value":"text"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"EmDataInline","definition":{"$type":"Group","elements":[{"$type":"Group","elements":[{"$type":"Keyword","value":"`"},{"$type":"Assignment","feature":"dataType","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}},{"$type":"Keyword","value":"`"}],"cardinality":"?"},{"$type":"Assignment","feature":"dataInlineValue","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]}}]},"entry":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"EmDataBlock","definition":{"$type":"Group","elements":[{"$type":"Group","elements":[{"$type":"Keyword","value":"`"},{"$type":"Assignment","feature":"dataType","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}},{"$type":"Keyword","value":"`"}],"cardinality":"?"},{"$type":"Assignment","feature":"dataBlockValue","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}}]},"entry":false,"parameters":[]},{"$type":"ParserRule","name":"QualifiedName","dataType":"string","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},{"$type":"Group","elements":[{"$type":"Keyword","value":"."},{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}],"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmTimeFrame","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"tf"},{"$type":"Keyword","value":"timeframe"}]},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}},{"$type":"Assignment","feature":"modelEntityType","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"entityIdentifier","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"->>"},{"$type":"Assignment","feature":"sourceFrames","operator":"+=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@8"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}}],"cardinality":"*"},{"$type":"Group","elements":[{"$type":"Keyword","value":"[["},{"$type":"Assignment","feature":"dataReference","operator":"=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@10"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}},{"$type":"Keyword","value":"]]"}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[],"cardinality":"?"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmResetFrame","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"rf"},{"$type":"Keyword","value":"resetframe"}]},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}},{"$type":"Assignment","feature":"modelEntityType","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"entityIdentifier","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"->>"},{"$type":"Assignment","feature":"sourceFrames","operator":"+=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@8"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}}],"cardinality":"*"},{"$type":"Group","elements":[{"$type":"Keyword","value":"[["},{"$type":"Assignment","feature":"dataReference","operator":"=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@10"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}},{"$type":"Keyword","value":"]]"}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[],"cardinality":"?"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmFrame","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmModelEntity","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"entity"},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmDataEntity","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"data"},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmNoteEntity","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"note"},{"$type":"Assignment","feature":"sourceFrame","operator":"=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@8"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}},{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmGwt","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"gwt"},{"$type":"Assignment","feature":"sourceFrame","operator":"=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@8"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}},{"$type":"Keyword","value":"given"},{"$type":"Assignment","feature":"givenStatements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]},"cardinality":"+"},{"$type":"Group","elements":[{"$type":"Keyword","value":"when"},{"$type":"Assignment","feature":"whenStatements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]},"cardinality":"+"}],"cardinality":"?"},{"$type":"Keyword","value":"then"},{"$type":"Assignment","feature":"thenStatements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]},"cardinality":"+"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EmGwtStatement","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]},{"$type":"Assignment","feature":"entityIdentifier","operator":"=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@9"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EM_EID","dataType":"string","definition":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EM_FI","dataType":"string","definition":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"EM_ID","definition":{"$type":"RegexToken","regex":"/[_a-zA-Z][\\\\w_]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EM_FID","definition":{"$type":"RegexToken","regex":"/\\\\d{1,3}/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EM_DATA_INLINE","definition":{"$type":"RegexToken","regex":"/\\\\{(.*)\\\\}|\\"(.*)\\"|\'(.*)\'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EM_DATA_BLOCK","definition":{"$type":"RegexToken","regex":"/\\\\{[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?\\\\}(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EM_ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EM_ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EM_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"EM_WS","definition":{"$type":"RegexToken","regex":"/\\\\s+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EM_YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EM_DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EM_SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EM_ML_COMMENT","definition":{"$type":"RegexToken","regex":"/\\\\/\\\\*[\\\\s\\\\S]*?\\\\*\\\\//","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EM_SL_COMMENT","definition":{"$type":"RegexToken","regex":"/\\\\/\\\\/[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false}],"imports":[],"types":[]}')),"EventModelingGrammar"),WEt=E(()=>mwe??(mwe=Ma(`{"$type":"Grammar","isDeclared":true,"name":"GitGraphGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"GitGraph","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[],"cardinality":"*"},{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"gitGraph"},{"$type":"Group","elements":[{"$type":"Keyword","value":"gitGraph"},{"$type":"Keyword","value":":"}]},{"$type":"Keyword","value":"gitGraph:"},{"$type":"Group","elements":[{"$type":"Keyword","value":"gitGraph"},{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]},{"$type":"Keyword","value":":"}]}]},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]},{"$type":"Assignment","feature":"statements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Statement","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Direction","definition":{"$type":"Assignment","feature":"dir","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"LR"},{"$type":"Keyword","value":"TB"},{"$type":"Keyword","value":"BT"}]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Commit","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"commit"},{"$type":"Alternatives","elements":[{"$type":"Group","elements":[{"$type":"Keyword","value":"id:"},{"$type":"Assignment","feature":"id","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"msg:","cardinality":"?"},{"$type":"Assignment","feature":"message","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"tag:"},{"$type":"Assignment","feature":"tags","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"type:"},{"$type":"Assignment","feature":"type","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"NORMAL"},{"$type":"Keyword","value":"REVERSE"},{"$type":"Keyword","value":"HIGHLIGHT"}]}}]}],"cardinality":"*"},{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Branch","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"branch"},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"order:"},{"$type":"Assignment","feature":"order","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Merge","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"merge"},{"$type":"Assignment","feature":"branch","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}]}},{"$type":"Alternatives","elements":[{"$type":"Group","elements":[{"$type":"Keyword","value":"id:"},{"$type":"Assignment","feature":"id","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"tag:"},{"$type":"Assignment","feature":"tags","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"type:"},{"$type":"Assignment","feature":"type","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"NORMAL"},{"$type":"Keyword","value":"REVERSE"},{"$type":"Keyword","value":"HIGHLIGHT"}]}}]}],"cardinality":"*"},{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Checkout","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"checkout"},{"$type":"Keyword","value":"switch"}]},{"$type":"Assignment","feature":"branch","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"CherryPicking","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"cherry-pick"},{"$type":"Alternatives","elements":[{"$type":"Group","elements":[{"$type":"Keyword","value":"id:"},{"$type":"Assignment","feature":"id","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"tag:"},{"$type":"Assignment","feature":"tags","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"parent:"},{"$type":"Assignment","feature":"parent","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]}],"cardinality":"*"},{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@14"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@15"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","name":"REFERENCE","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\\\w([-\\\\./\\\\w]*[-\\\\w])?/","parenthesized":false},"fragment":false,"hidden":false}],"interfaces":[],"types":[]}`)),"GitGraphGrammarGrammar"),qEt=E(()=>gwe??(gwe=Ma(`{"$type":"Grammar","isDeclared":true,"name":"InfoGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"Info","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[],"cardinality":"*"},{"$type":"Keyword","value":"info"},{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[],"cardinality":"*"},{"$type":"Group","elements":[{"$type":"Keyword","value":"showInfo"},{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[],"cardinality":"*"}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[],"cardinality":"?"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@7"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@8"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false}],"interfaces":[],"types":[]}`)),"InfoGrammarGrammar"),HEt=E(()=>ywe??(ywe=Ma(`{"$type":"Grammar","isDeclared":true,"name":"PacketGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"Packet","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[],"cardinality":"*"},{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"packet"},{"$type":"Keyword","value":"packet-beta"}]},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]},{"$type":"Assignment","feature":"blocks","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PacketBlock","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Group","elements":[{"$type":"Assignment","feature":"start","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"-"},{"$type":"Assignment","feature":"end","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}}],"cardinality":"?"}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"+"},{"$type":"Assignment","feature":"bits","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}}]}]},{"$type":"Keyword","value":":"},{"$type":"Assignment","feature":"label","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@8"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@9"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false}],"interfaces":[],"types":[]}`)),"PacketGrammarGrammar"),UEt=E(()=>vwe??(vwe=Ma(`{"$type":"Grammar","isDeclared":true,"name":"PieGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"Pie","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[],"cardinality":"*"},{"$type":"Keyword","value":"pie"},{"$type":"Assignment","feature":"showData","operator":"?=","terminal":{"$type":"Keyword","value":"showData"},"cardinality":"?"},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]},{"$type":"Assignment","feature":"sections","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PieSection","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"label","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},{"$type":"Keyword","value":":"},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"FLOAT_PIE","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/-?[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT_PIE","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/-?(0|[1-9][0-9]*)(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER_PIE","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@2"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@3"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@11"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@12"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false}],"interfaces":[],"types":[]}`)),"PieGrammarGrammar"),YEt=E(()=>xwe??(xwe=Ma(`{"$type":"Grammar","isDeclared":true,"name":"RadarGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"Radar","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"},{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"radar-beta"},{"$type":"Keyword","value":"radar-beta:"},{"$type":"Group","elements":[{"$type":"Keyword","value":"radar-beta"},{"$type":"Keyword","value":":"}]}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]},{"$type":"Group","elements":[{"$type":"Keyword","value":"axis"},{"$type":"Assignment","feature":"axes","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"axes","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}}],"cardinality":"*"}]},{"$type":"Group","elements":[{"$type":"Keyword","value":"curve"},{"$type":"Assignment","feature":"curves","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"curves","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}}],"cardinality":"*"}]},{"$type":"Group","elements":[{"$type":"Assignment","feature":"options","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"options","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}}],"cardinality":"*"}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"Label","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"label","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]}},{"$type":"Keyword","value":"]"}]},"entry":false,"parameters":[]},{"$type":"ParserRule","name":"Axis","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[],"cardinality":"?"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Curve","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[],"cardinality":"?"},{"$type":"Keyword","value":"{"},{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]},{"$type":"Keyword","value":"}"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"Entries","definition":{"$type":"Alternatives","elements":[{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"},{"$type":"Assignment","feature":"entries","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":","},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"},{"$type":"Assignment","feature":"entries","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}}],"cardinality":"*"},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"}]},{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"},{"$type":"Assignment","feature":"entries","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":","},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"},{"$type":"Assignment","feature":"entries","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}}],"cardinality":"*"},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"*"}]}]},"entry":false,"parameters":[]},{"$type":"ParserRule","name":"DetailedEntry","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"axis","operator":"=","terminal":{"$type":"CrossReference","type":{"$ref":"#/rules@2"},"terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},"deprecatedSyntax":false,"isMulti":false}},{"$type":"Keyword","value":":","cardinality":"?"},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"NumberEntry","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Option","definition":{"$type":"Alternatives","elements":[{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Keyword","value":"showLegend"}},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Keyword","value":"ticks"}},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Keyword","value":"max"}},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Keyword","value":"min"}},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Keyword","value":"graticule"}},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]}}]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"GRATICULE","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"circle"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"polygon"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@15"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@16"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false}],"interfaces":[{"$type":"Interface","name":"Entry","attributes":[{"$type":"TypeAttribute","name":"axis","isOptional":true,"type":{"$type":"ReferenceType","referenceType":{"$type":"SimpleType","typeRef":{"$ref":"#/rules@2"}},"isMulti":false}},{"$type":"TypeAttribute","name":"value","type":{"$type":"SimpleType","primitiveType":"number"},"isOptional":false}],"superTypes":[]}],"types":[]}`)),"RadarGrammarGrammar"),jEt=E(()=>bwe??(bwe=Ma('{"$type":"Grammar","isDeclared":true,"name":"RailroadAbnfGrammar","rules":[{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ABNF_RULENAME","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[A-Za-z][A-Za-z0-9-]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ABNF_STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"[^\\"]*\\"/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ABNF_NUMVAL","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/%[xXdDbB][0-9A-Fa-f]+(?:-[0-9A-Fa-f]+|\\\\.[0-9A-Fa-f]+)*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ABNF_REPEAT","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[0-9]*\\\\*[0-9]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ABNF_EXACT_REPEAT","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[0-9]+/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"ABNF_WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t \\\\r\\\\n]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"ABNF_YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"ABNF_DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"ABNF_SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"ABNF_COMMENT","definition":{"$type":"RegexToken","regex":"/;[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"ParserRule","entry":true,"name":"RailroadAbnf","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"railroad-abnf-beta"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@0"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}}],"cardinality":"*"},{"$type":"Assignment","feature":"rules","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfRule","returnType":{"$ref":"#/interfaces@1"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"Keyword","value":"="},{"$type":"Assignment","feature":"definition","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}},{"$type":"Keyword","value":";"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfAlternation","returnType":{"$ref":"#/interfaces@2"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"/"},{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}}],"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfConcatenation","returnType":{"$ref":"#/interfaces@3"},"definition":{"$type":"Assignment","feature":"elements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]},"cardinality":"+"},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfElement","returnType":{"$ref":"#/interfaces@4"},"definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"repeat","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}},{"$type":"Assignment","feature":"repeat","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}}],"cardinality":"?"},{"$type":"Assignment","feature":"primary","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]}}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfPrimary","returnType":{"$ref":"#/interfaces@5"},"definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@21"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@22"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfStringLiteral","returnType":{"$ref":"#/interfaces@6"},"definition":{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfNumVal","returnType":{"$ref":"#/interfaces@7"},"definition":{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfRuleName","returnType":{"$ref":"#/interfaces@8"},"definition":{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfGroup","returnType":{"$ref":"#/interfaces@9"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"AbnfOptionalGroup","returnType":{"$ref":"#/interfaces@10"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}},{"$type":"Keyword","value":"]"}]},"entry":false,"fragment":false,"parameters":[]}],"interfaces":[{"$type":"Interface","name":"RailroadAbnf","attributes":[{"$type":"TypeAttribute","name":"title","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accTitle","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accDescr","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"rules","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@1"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"AbnfRule","attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false},{"$type":"TypeAttribute","name":"definition","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"AbnfAlternation","attributes":[{"$type":"TypeAttribute","name":"alternatives","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@3"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"AbnfConcatenation","attributes":[{"$type":"TypeAttribute","name":"elements","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@4"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"AbnfElement","attributes":[{"$type":"TypeAttribute","name":"repeat","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"primary","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@5"}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"AbnfPrimary","attributes":[],"superTypes":[]},{"$type":"Interface","name":"AbnfStringLiteral","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"value","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"AbnfNumVal","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"value","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"AbnfRuleName","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"AbnfGroup","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"AbnfOptionalGroup","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]}],"imports":[],"types":[]}')),"RailroadAbnfGrammarGrammar"),XEt=E(()=>Twe??(Twe=Ma(`{"$type":"Grammar","isDeclared":true,"name":"RailroadEbnfGrammar","rules":[{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EBNF_ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[A-Z_a-z][\\\\w-]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EBNF_STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"EBNF_SPECIAL_SEQUENCE","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\\\?(?=[^?;]*[^?\\\\s;][^?;]*\\\\?)[^?;]*\\\\?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"EBNF_WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t \\\\r\\\\n]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EBNF_YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EBNF_DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EBNF_SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EBNF_BLOCK_COMMENT","definition":{"$type":"RegexToken","regex":"/\\\\/\\\\*[\\\\s\\\\S]*?\\\\*\\\\//","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"EBNF_ISO_COMMENT","definition":{"$type":"RegexToken","regex":"/\\\\(\\\\*[\\\\s\\\\S]*?\\\\*\\\\)/","parenthesized":false},"fragment":false},{"$type":"ParserRule","entry":true,"name":"RailroadEbnf","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"railroad-ebnf-beta"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@0"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}}],"cardinality":"*"},{"$type":"Assignment","feature":"rules","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]},"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfRule","returnType":{"$ref":"#/interfaces@1"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"Alternatives","elements":[{"$type":"Keyword","value":"="},{"$type":"Keyword","value":"::="}]},{"$type":"Assignment","feature":"definition","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},{"$type":"Keyword","value":";"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfChoice","returnType":{"$ref":"#/interfaces@2"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"|"},{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}}],"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfSequence","returnType":{"$ref":"#/interfaces@3"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"elements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":",","cardinality":"?"},{"$type":"Assignment","feature":"elements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}}],"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfTerm","returnType":{"$ref":"#/interfaces@4"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"base","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}},{"$type":"Assignment","feature":"postfixes","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[]},"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfPrimary","returnType":{"$ref":"#/interfaces@5"},"definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@21"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@22"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfTerminal","returnType":{"$ref":"#/interfaces@7"},"definition":{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfNonTerminal","returnType":{"$ref":"#/interfaces@8"},"definition":{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfSpecial","returnType":{"$ref":"#/interfaces@9"},"definition":{"$type":"Assignment","feature":"text","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfGroup","returnType":{"$ref":"#/interfaces@10"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfOptional","returnType":{"$ref":"#/interfaces@11"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},{"$type":"Keyword","value":"]"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfRepetition","returnType":{"$ref":"#/interfaces@12"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"{"},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},{"$type":"Keyword","value":"}"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfPostfix","returnType":{"$ref":"#/interfaces@6"},"definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@25"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@26"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@27"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@28"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfOptionalPostfix","returnType":{"$ref":"#/interfaces@13"},"definition":{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"?"}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfZeroOrMorePostfix","returnType":{"$ref":"#/interfaces@14"},"definition":{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"*"}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfOneOrMorePostfix","returnType":{"$ref":"#/interfaces@15"},"definition":{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"+"}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EbnfExceptionPostfix","returnType":{"$ref":"#/interfaces@16"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"-"},{"$type":"Assignment","feature":"except","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}}]},"entry":false,"fragment":false,"parameters":[]}],"interfaces":[{"$type":"Interface","name":"RailroadEbnf","attributes":[{"$type":"TypeAttribute","name":"title","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accTitle","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accDescr","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"rules","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@1"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"EbnfRule","attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false},{"$type":"TypeAttribute","name":"definition","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"EbnfChoice","attributes":[{"$type":"TypeAttribute","name":"alternatives","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@3"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"EbnfSequence","attributes":[{"$type":"TypeAttribute","name":"elements","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@4"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"EbnfTerm","attributes":[{"$type":"TypeAttribute","name":"base","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@5"}},"isOptional":false},{"$type":"TypeAttribute","name":"postfixes","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@6"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"EbnfPrimary","attributes":[],"superTypes":[]},{"$type":"Interface","name":"EbnfPostfix","attributes":[],"superTypes":[]},{"$type":"Interface","name":"EbnfTerminal","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"value","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"EbnfNonTerminal","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"EbnfSpecial","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"text","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"EbnfGroup","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"EbnfOptional","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"EbnfRepetition","superTypes":[{"$ref":"#/interfaces@5"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"EbnfOptionalPostfix","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"operator","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"EbnfZeroOrMorePostfix","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"operator","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"EbnfOneOrMorePostfix","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"operator","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"EbnfExceptionPostfix","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"except","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@5"}},"isOptional":false}]}],"imports":[],"types":[]}`)),"RailroadEbnfGrammarGrammar"),KEt=E(()=>Cwe??(Cwe=Ma(`{"$type":"Grammar","isDeclared":true,"name":"RailroadGrammar","rules":[{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"RR_ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[A-Z_a-z][\\\\w-]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"RR_STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"RR_WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t \\\\r\\\\n]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"RR_YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"RR_DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"RR_SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"RR_BLOCK_COMMENT","definition":{"$type":"RegexToken","regex":"/\\\\/\\\\*[\\\\s\\\\S]*?\\\\*\\\\//","parenthesized":false},"fragment":false},{"$type":"ParserRule","entry":true,"name":"Railroad","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"railroad-beta"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@0"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}}],"cardinality":"*"},{"$type":"Assignment","feature":"rules","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]},"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadRule","returnType":{"$ref":"#/interfaces@1"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"Keyword","value":"="},{"$type":"Assignment","feature":"definition","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Keyword","value":";"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadExpression","returnType":{"$ref":"#/interfaces@2"},"definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadSequenceExpr","returnType":{"$ref":"#/interfaces@3"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"sequence"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"elements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"elements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}}],"cardinality":"*"},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadChoiceExpr","returnType":{"$ref":"#/interfaces@4"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"choice"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}}],"cardinality":"*"},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadOptionalExpr","returnType":{"$ref":"#/interfaces@5"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"optional"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadOneOrMoreExpr","returnType":{"$ref":"#/interfaces@6"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"oneOrMore"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadZeroOrMoreExpr","returnType":{"$ref":"#/interfaces@7"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"zeroOrMore"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadTerminalExpr","returnType":{"$ref":"#/interfaces@8"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"terminal"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadNonTerminalExpr","returnType":{"$ref":"#/interfaces@9"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"nonterminal"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"RailroadSpecialExpr","returnType":{"$ref":"#/interfaces@10"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"special"},{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"text","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]}],"interfaces":[{"$type":"Interface","name":"Railroad","attributes":[{"$type":"TypeAttribute","name":"title","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accTitle","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accDescr","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"rules","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@1"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"RailroadRule","attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false},{"$type":"TypeAttribute","name":"definition","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"RailroadExpression","attributes":[],"superTypes":[]},{"$type":"Interface","name":"RailroadSequenceExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"elements","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}}},"isOptional":false}]},{"$type":"Interface","name":"RailroadChoiceExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"alternatives","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}}},"isOptional":false}]},{"$type":"Interface","name":"RailroadOptionalExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"RailroadOneOrMoreExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"RailroadZeroOrMoreExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"RailroadTerminalExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"value","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"RailroadNonTerminalExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"RailroadSpecialExpr","superTypes":[{"$ref":"#/interfaces@2"}],"attributes":[{"$type":"TypeAttribute","name":"text","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]}],"imports":[],"types":[]}`)),"RailroadGrammarGrammar"),ZEt=E(()=>wwe??(wwe=Ma(`{"$type":"Grammar","isDeclared":true,"name":"RailroadPegGrammar","rules":[{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"PEG_ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[A-Z_a-z][\\\\w-]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"PEG_STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"PEG_WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t \\\\r\\\\n]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"PEG_YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"PEG_DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"PEG_SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"PEG_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/#[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false},{"$type":"ParserRule","entry":true,"name":"RailroadPeg","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"railroad-peg-beta"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@0"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}}],"cardinality":"*"},{"$type":"Assignment","feature":"rules","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]},"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegRule","returnType":{"$ref":"#/interfaces@1"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"Keyword","value":"<-"},{"$type":"Assignment","feature":"definition","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Keyword","value":";"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegOrderedChoice","returnType":{"$ref":"#/interfaces@2"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"/"},{"$type":"Assignment","feature":"alternatives","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}}],"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegSequence","returnType":{"$ref":"#/interfaces@3"},"definition":{"$type":"Assignment","feature":"elements","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},"cardinality":"+"},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegPrefix","returnType":{"$ref":"#/interfaces@4"},"definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"&"}},{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"!"}}],"cardinality":"?"},{"$type":"Assignment","feature":"suffix","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegSuffix","returnType":{"$ref":"#/interfaces@5"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"primary","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"?"}},{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"*"}},{"$type":"Assignment","feature":"operator","operator":"=","terminal":{"$type":"Keyword","value":"+"}}],"cardinality":"?"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegPrimary","returnType":{"$ref":"#/interfaces@6"},"definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegLiteral","returnType":{"$ref":"#/interfaces@7"},"definition":{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegIdentifier","returnType":{"$ref":"#/interfaces@8"},"definition":{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegGroup","returnType":{"$ref":"#/interfaces@9"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"element","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PegAny","returnType":{"$ref":"#/interfaces@10"},"definition":{"$type":"Assignment","feature":"dot","operator":"=","terminal":{"$type":"Keyword","value":"."}},"entry":false,"fragment":false,"parameters":[]}],"interfaces":[{"$type":"Interface","name":"RailroadPeg","attributes":[{"$type":"TypeAttribute","name":"title","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accTitle","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accDescr","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"rules","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@1"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"PegRule","attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false},{"$type":"TypeAttribute","name":"definition","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"PegOrderedChoice","attributes":[{"$type":"TypeAttribute","name":"alternatives","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@3"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"PegSequence","attributes":[{"$type":"TypeAttribute","name":"elements","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@4"}}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"PegPrefix","attributes":[{"$type":"TypeAttribute","name":"operator","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"suffix","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@5"}},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"PegSuffix","attributes":[{"$type":"TypeAttribute","name":"primary","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@6"}},"isOptional":false},{"$type":"TypeAttribute","name":"operator","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}}],"superTypes":[]},{"$type":"Interface","name":"PegPrimary","attributes":[],"superTypes":[]},{"$type":"Interface","name":"PegLiteral","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"value","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"PegIdentifier","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]},{"$type":"Interface","name":"PegGroup","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"element","type":{"$type":"SimpleType","typeRef":{"$ref":"#/interfaces@2"}},"isOptional":false}]},{"$type":"Interface","name":"PegAny","superTypes":[{"$ref":"#/interfaces@6"}],"attributes":[{"$type":"TypeAttribute","name":"dot","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}]}],"imports":[],"types":[]}`)),"RailroadPegGrammarGrammar"),QEt=E(()=>kwe??(kwe=Ma(`{"$type":"Grammar","isDeclared":true,"name":"TreemapGrammar","rules":[{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","entry":true,"name":"Treemap","returnType":{"$ref":"#/interfaces@4"},"definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@0"},"arguments":[]},{"$type":"Assignment","feature":"TreemapRows","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"TREEMAP_KEYWORD","definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"treemap-beta"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"treemap"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"CLASS_DEF","definition":{"$type":"RegexToken","regex":"/classDef\\\\s+([a-zA-Z_][a-zA-Z0-9_]+)(?:\\\\s+([^;\\\\r\\\\n]*))?(?:;)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STYLE_SEPARATOR","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":":::"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"SEPARATOR","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":":"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"COMMA","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":","},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INDENTATION","definition":{"$type":"RegexToken","regex":"/[ \\\\t]{1,}/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WS","definition":{"$type":"RegexToken","regex":"/[ \\\\t]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"ML_COMMENT","definition":{"$type":"RegexToken","regex":"/\\\\%\\\\%[^\\\\n]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"NL","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false},{"$type":"ParserRule","name":"TreemapRow","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"indent","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]},"cardinality":"?"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"item","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"ClassDef","dataType":"string","definition":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Item","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Section","returnType":{"$ref":"#/interfaces@1"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]},{"$type":"Assignment","feature":"classSelector","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]}}],"cardinality":"?"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Leaf","returnType":{"$ref":"#/interfaces@2"},"definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[],"cardinality":"?"},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[],"cardinality":"?"},{"$type":"Assignment","feature":"value","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@22"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]},{"$type":"Assignment","feature":"classSelector","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]}}],"cardinality":"?"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"ID2","definition":{"$type":"RegexToken","regex":"/[a-zA-Z_][a-zA-Z0-9_]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER2","definition":{"$type":"RegexToken","regex":"/[0-9_\\\\.\\\\,]+/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","name":"MyNumber","dataType":"number","definition":{"$type":"RuleCall","rule":{"$ref":"#/rules@21"},"arguments":[]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"STRING2","definition":{"$type":"RegexToken","regex":"/\\"[^\\"]*\\"|'[^']*'/","parenthesized":false},"fragment":false,"hidden":false}],"interfaces":[{"$type":"Interface","name":"Item","attributes":[{"$type":"TypeAttribute","name":"name","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false},{"$type":"TypeAttribute","name":"classSelector","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}}],"superTypes":[]},{"$type":"Interface","name":"Section","superTypes":[{"$ref":"#/interfaces@0"}],"attributes":[]},{"$type":"Interface","name":"Leaf","superTypes":[{"$ref":"#/interfaces@0"}],"attributes":[{"$type":"TypeAttribute","name":"value","type":{"$type":"SimpleType","primitiveType":"number"},"isOptional":false}]},{"$type":"Interface","name":"ClassDefStatement","attributes":[{"$type":"TypeAttribute","name":"className","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false},{"$type":"TypeAttribute","name":"styleText","type":{"$type":"SimpleType","primitiveType":"string"},"isOptional":false}],"superTypes":[]},{"$type":"Interface","name":"Treemap","attributes":[{"$type":"TypeAttribute","name":"TreemapRows","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/rules@15"}}},"isOptional":false},{"$type":"TypeAttribute","name":"title","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accTitle","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accDescr","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}}],"superTypes":[]}],"imports":[],"types":[],"$comment":"/**\\n * Treemap grammar for Langium\\n * Converted from mindmap grammar\\n *\\n * The ML_COMMENT and NL hidden terminals handle whitespace, comments, and newlines\\n * before the treemap keyword, allowing for empty lines and comments before the\\n * treemap declaration.\\n */"}`)),"TreemapGrammarGrammar"),JEt=E(()=>Swe??(Swe=Ma(`{"$type":"Grammar","isDeclared":true,"name":"TreeViewGrammar","rules":[{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","entry":true,"name":"TreeView","returnType":{"$ref":"#/interfaces@0"},"definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"treeView-beta"},{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[],"cardinality":"?"},{"$type":"Assignment","feature":"nodes","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]},"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@0"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"CLASS_ANNOTATION","definition":{"$type":"RegexToken","regex":"/[ \\\\t]+:::[ \\\\t]*[A-Za-z_][\\\\w-]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ICON_ANNOTATION","definition":{"$type":"RegexToken","regex":"/[ \\\\t]+icon\\\\([\\\\w-]*(?::[\\\\w-]+)?\\\\)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"DESC_ANNOTATION","definition":{"$type":"RegexToken","regex":"/[ \\\\t]+##[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INDENTATION","definition":{"$type":"RegexToken","regex":"/[ \\\\t]{1,}/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"QUOTED_NAME","definition":{"$type":"RegexToken","regex":"/\\"[^\\"]*\\"|'[^']*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WS","definition":{"$type":"RegexToken","regex":"/[ \\\\t]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"ML_COMMENT","definition":{"$type":"RegexToken","regex":"/\\\\%\\\\%[^\\\\n]*/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"NL","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","name":"BARE_NAME","definition":{"$type":"RegexToken","regex":"/(?!:::|icon\\\\(|##)[^ \\\\t\\\\n\\\\r\\"'](?:(?![ \\\\t]+:::[ \\\\t]*[A-Za-z_]|[ \\\\t]+icon\\\\(|[ \\\\t]+##)[^\\\\n\\\\r])*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"ParserRule","name":"TreeNode","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"indent","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]},"cardinality":"?"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}}]},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"classAnnotation","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Assignment","feature":"iconAnnotation","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}},{"$type":"Assignment","feature":"descAnnotation","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]}}],"cardinality":"*"}]},"entry":false,"fragment":false,"parameters":[]}],"interfaces":[{"$type":"Interface","name":"TreeView","attributes":[{"$type":"TypeAttribute","name":"nodes","type":{"$type":"ArrayType","elementType":{"$type":"SimpleType","typeRef":{"$ref":"#/rules@14"}}},"isOptional":false},{"$type":"TypeAttribute","name":"title","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accTitle","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}},{"$type":"TypeAttribute","name":"accDescr","isOptional":true,"type":{"$type":"SimpleType","primitiveType":"string"}}],"superTypes":[]}],"imports":[],"types":[],"$comment":"/**\\n * TreeView grammar for Langium\\n *\\n * Supports both quoted labels (\\"my file\\") and bare labels (index.js).\\n * Annotations (:::class, icon(), ## description) are parsed directly into\\n * AST fields by the grammar. Value conversion for stripping quotes, extracting\\n * class names, icon names, and description text happens in valueConverter.ts.\\n *\\n * The ML_COMMENT and NL hidden terminals handle whitespace, comments, and newlines\\n * before the treeView keyword, allowing for empty lines and comments before the\\n * treeView declaration.\\n */"}`)),"TreeViewGrammarGrammar"),e4t=E(()=>Ewe??(Ewe=Ma(`{"$type":"Grammar","isDeclared":true,"name":"WardleyGrammar","imports":[],"rules":[{"$type":"ParserRule","entry":true,"name":"Wardley","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@52"},"arguments":[],"cardinality":"*"},{"$type":"RuleCall","rule":{"$ref":"#/rules@25"},"arguments":[]},{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@52"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@42"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@1"},"arguments":[]}],"cardinality":"*"}]},"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"Statement","definition":{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"size","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@2"},"arguments":[]}},{"$type":"Assignment","feature":"evolution","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@3"},"arguments":[]}},{"$type":"Assignment","feature":"anchors","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@5"},"arguments":[]}},{"$type":"Assignment","feature":"components","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@6"},"arguments":[]}},{"$type":"Assignment","feature":"links","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@9"},"arguments":[]}},{"$type":"Assignment","feature":"evolves","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@10"},"arguments":[]}},{"$type":"Assignment","feature":"pipelines","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@11"},"arguments":[]}},{"$type":"Assignment","feature":"notes","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@13"},"arguments":[]}},{"$type":"Assignment","feature":"annotations","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@14"},"arguments":[]}},{"$type":"Assignment","feature":"annotation","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@15"},"arguments":[]}},{"$type":"Assignment","feature":"accelerators","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@17"},"arguments":[]}},{"$type":"Assignment","feature":"deaccelerators","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@18"},"arguments":[]}}]},"entry":false,"parameters":[]},{"$type":"ParserRule","name":"Size","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@26"},"arguments":[]},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"width","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@48"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"height","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@48"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Evolution","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@27"},"arguments":[]},{"$type":"Assignment","feature":"stages","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]},{"$type":"Assignment","feature":"stages","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@4"},"arguments":[]}}],"cardinality":"+"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"EvolutionStage","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"@"},{"$type":"Assignment","feature":"boundary","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}}],"cardinality":"?"},{"$type":"Group","elements":[{"$type":"Keyword","value":"/"},{"$type":"Assignment","feature":"secondName","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}}],"cardinality":"?"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Anchor","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@28"},"arguments":[]},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"visibility","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"evolution","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Component","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@29"},"arguments":[]},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"visibility","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"evolution","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"Assignment","feature":"label","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]},"cardinality":"?"},{"$type":"Assignment","feature":"decorator","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@8"},"arguments":[]},"cardinality":"?"},{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"inertia","operator":"?=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@31"},"arguments":[]}},{"$type":"Group","elements":[{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"inertia","operator":"?=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@31"},"arguments":[]}},{"$type":"Keyword","value":")"}]}],"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Label","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@30"},"arguments":[]},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"negX","operator":"?=","terminal":{"$type":"Keyword","value":"-"},"cardinality":"?"},{"$type":"Assignment","feature":"offsetX","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@48"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"negY","operator":"?=","terminal":{"$type":"Keyword","value":"-"},"cardinality":"?"},{"$type":"Assignment","feature":"offsetY","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@48"},"arguments":[]}},{"$type":"Keyword","value":"]"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Decorator","definition":{"$type":"Group","elements":[{"$type":"Keyword","value":"("},{"$type":"Assignment","feature":"strategy","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@24"},"arguments":[]}},{"$type":"Keyword","value":")"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Link","definition":{"$type":"Group","elements":[{"$type":"Assignment","feature":"from","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Assignment","feature":"fromPort","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@21"},"arguments":[]},"cardinality":"?"},{"$type":"Assignment","feature":"arrow","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@22"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@20"},"arguments":[]}]},"cardinality":"?"},{"$type":"Assignment","feature":"to","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Assignment","feature":"toPort","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@21"},"arguments":[]},"cardinality":"?"},{"$type":"Assignment","feature":"linkLabel","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@23"},"arguments":[]},"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Evolve","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@32"},"arguments":[]},{"$type":"Assignment","feature":"component","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Assignment","feature":"target","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Pipeline","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@33"},"arguments":[]},{"$type":"Assignment","feature":"parent","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Keyword","value":"{"},{"$type":"RuleCall","rule":{"$ref":"#/rules@52"},"arguments":[],"cardinality":"+"},{"$type":"Assignment","feature":"components","operator":"+=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@12"},"arguments":[]},"cardinality":"+"},{"$type":"Keyword","value":"}"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"PipelineComponent","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@29"},"arguments":[]},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"evolution","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"Assignment","feature":"label","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@7"},"arguments":[]},"cardinality":"?"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Note","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@34"},"arguments":[]},{"$type":"Assignment","feature":"text","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]}},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"visibility","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"evolution","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Annotations","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@35"},"arguments":[]},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"x","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"y","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Annotation","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@36"},"arguments":[]},{"$type":"Assignment","feature":"number","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@48"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"x","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"y","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@16"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"Assignment","feature":"text","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]}},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"CoordinateValue","dataType":"number","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@48"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Accelerator","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@37"},"arguments":[]},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"x","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"y","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","name":"Deaccelerator","definition":{"$type":"Group","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@38"},"arguments":[]},{"$type":"Assignment","feature":"name","operator":"=","terminal":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@50"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@51"},"arguments":[]},{"$type":"RuleCall","rule":{"$ref":"#/rules@39"},"arguments":[]}]}},{"$type":"Keyword","value":"["},{"$type":"Assignment","feature":"x","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":","},{"$type":"Assignment","feature":"y","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@19"},"arguments":[]}},{"$type":"Keyword","value":"]"},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"TerminalRule","name":"WARDLEY_NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ARROW","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"->"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"LINK_PORT","definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"+<>"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"+>"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"+<"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"LINK_ARROW","definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"-->"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"-.->"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":">"},"parenthesized":false}],"parenthesized":false},{"$type":"RegexToken","regex":"/\\\\+'[^']*'<>/","parenthesized":false}],"parenthesized":false},{"$type":"RegexToken","regex":"/\\\\+'[^']*'/","parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"LINK_LABEL","definition":{"$type":"RegexToken","regex":"/;[^\\\\n\\\\r]+/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRATEGY","definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"build"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"buy"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"outsource"},"parenthesized":false}],"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"market"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_WARDLEY","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"wardley-beta"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_SIZE","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"size"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_EVOLUTION","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"evolution"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_ANCHOR","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"anchor"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_COMPONENT","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"component"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_LABEL","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"label"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_INERTIA","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"inertia"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_EVOLVE","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"evolve"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_PIPELINE","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"pipeline"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_NOTE","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"note"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_ANNOTATIONS","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"annotations"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_ANNOTATION","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"annotation"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_ACCELERATOR","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"accelerator"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"KW_DEACCELERATOR","definition":{"$type":"CharacterRange","left":{"$type":"Keyword","value":"deaccelerator"},"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NAME_WITH_SPACES","definition":{"$type":"RegexToken","regex":"/(?!title\\\\s|accTitle|accDescr)[A-Za-z](?:[A-Za-z0-9_()&]|-(?!>))*(?:[ \\\\t]+[A-Za-z(](?:[A-Za-z0-9_()&]|-(?!>))*)*/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WS","definition":{"$type":"RegexToken","regex":"/[ \\\\t]+/","parenthesized":false},"fragment":false},{"$type":"ParserRule","name":"EOL","dataType":"string","definition":{"$type":"Alternatives","elements":[{"$type":"RuleCall","rule":{"$ref":"#/rules@52"},"arguments":[],"cardinality":"+"},{"$type":"EndOfFile"}]},"entry":false,"fragment":false,"parameters":[]},{"$type":"ParserRule","fragment":true,"name":"TitleAndAccessibilities","definition":{"$type":"Group","elements":[{"$type":"Alternatives","elements":[{"$type":"Assignment","feature":"accDescr","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@44"},"arguments":[]}},{"$type":"Assignment","feature":"accTitle","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@45"},"arguments":[]}},{"$type":"Assignment","feature":"title","operator":"=","terminal":{"$type":"RuleCall","rule":{"$ref":"#/rules@46"},"arguments":[]}}]},{"$type":"RuleCall","rule":{"$ref":"#/rules@41"},"arguments":[]}],"cardinality":"+"},"entry":false,"parameters":[]},{"$type":"TerminalRule","name":"BOOLEAN","type":{"$type":"ReturnType","name":"boolean"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"CharacterRange","left":{"$type":"Keyword","value":"true"},"parenthesized":false},{"$type":"CharacterRange","left":{"$type":"Keyword","value":"false"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_DESCR","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accDescr(?:[\\\\t ]*:([^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)|\\\\s*{([^}]*)})/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ACC_TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*accTitle[\\\\t ]*:(?:[^\\\\n\\\\r]*?(?=%%)|[^\\\\n\\\\r]*)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"TITLE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*title(?:[\\\\t ][^\\\\n\\\\r]*?(?=%%)|[\\\\t ][^\\\\n\\\\r]*|)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"FLOAT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/[0-9]+\\\\.[0-9]+(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"INT","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"RegexToken","regex":"/0|[1-9][0-9]*(?!\\\\.)/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NUMBER","type":{"$type":"ReturnType","name":"number"},"definition":{"$type":"TerminalAlternatives","elements":[{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@47"},"parenthesized":false},{"$type":"TerminalRuleCall","rule":{"$ref":"#/rules@48"},"parenthesized":false}],"parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"STRING","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/\\"([^\\"\\\\\\\\]|\\\\\\\\.)*\\"|'([^'\\\\\\\\]|\\\\\\\\.)*'/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"ID","type":{"$type":"ReturnType","name":"string"},"definition":{"$type":"RegexToken","regex":"/[\\\\w]([-\\\\w]*\\\\w)?/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","name":"NEWLINE","definition":{"$type":"RegexToken","regex":"/\\\\r?\\\\n/","parenthesized":false},"fragment":false,"hidden":false},{"$type":"TerminalRule","hidden":true,"name":"WHITESPACE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]+/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"YAML","definition":{"$type":"RegexToken","regex":"/---[\\\\t ]*\\\\r?\\\\n(?:[\\\\S\\\\s]*?\\\\r?\\\\n)?---(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"DIRECTIVE","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%{[\\\\S\\\\s]*?}%%(?:\\\\r?\\\\n|(?!\\\\S))/","parenthesized":false},"fragment":false},{"$type":"TerminalRule","hidden":true,"name":"SINGLE_LINE_COMMENT","definition":{"$type":"RegexToken","regex":"/[\\\\t ]*%%[^\\\\n\\\\r]*/","parenthesized":false},"fragment":false}],"interfaces":[],"types":[]}`)),"WardleyGrammarGrammar"),t4t={languageId:"architecture",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},r4t={languageId:"cynefin",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},n4t={languageId:"eventmodeling",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},i4t={languageId:"gitGraph",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},a4t={languageId:"info",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},s4t={languageId:"packet",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},o4t={languageId:"pie",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},l4t={languageId:"radar",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},c4t={languageId:"railroadAbnf",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},u4t={languageId:"railroadEbnf",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},h4t={languageId:"railroad",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},d4t={languageId:"railroadPeg",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},f4t={languageId:"treemap",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},p4t={languageId:"treeView",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},m4t={languageId:"wardley",fileExtensions:[".mmd",".mermaid"],caseInsensitive:!1,mode:"production"},An={AstReflection:E(()=>new XLe,"AstReflection")},zH={Grammar:E(()=>zEt(),"Grammar"),LanguageMetaData:E(()=>t4t,"LanguageMetaData"),parser:{}},GH={Grammar:E(()=>GEt(),"Grammar"),LanguageMetaData:E(()=>r4t,"LanguageMetaData"),parser:{}},VH={Grammar:E(()=>VEt(),"Grammar"),LanguageMetaData:E(()=>n4t,"LanguageMetaData"),parser:{}},WH={Grammar:E(()=>WEt(),"Grammar"),LanguageMetaData:E(()=>i4t,"LanguageMetaData"),parser:{}},qH={Grammar:E(()=>qEt(),"Grammar"),LanguageMetaData:E(()=>a4t,"LanguageMetaData"),parser:{}},HH={Grammar:E(()=>HEt(),"Grammar"),LanguageMetaData:E(()=>s4t,"LanguageMetaData"),parser:{}},UH={Grammar:E(()=>UEt(),"Grammar"),LanguageMetaData:E(()=>o4t,"LanguageMetaData"),parser:{}},YH={Grammar:E(()=>YEt(),"Grammar"),LanguageMetaData:E(()=>l4t,"LanguageMetaData"),parser:{}},jH={Grammar:E(()=>jEt(),"Grammar"),LanguageMetaData:E(()=>c4t,"LanguageMetaData"),parser:{}},XH={Grammar:E(()=>XEt(),"Grammar"),LanguageMetaData:E(()=>u4t,"LanguageMetaData"),parser:{}},KH={Grammar:E(()=>KEt(),"Grammar"),LanguageMetaData:E(()=>h4t,"LanguageMetaData"),parser:{}},ZH={Grammar:E(()=>ZEt(),"Grammar"),LanguageMetaData:E(()=>d4t,"LanguageMetaData"),parser:{}},QH={Grammar:E(()=>QEt(),"Grammar"),LanguageMetaData:E(()=>f4t,"LanguageMetaData"),parser:{}},JH={Grammar:E(()=>JEt(),"Grammar"),LanguageMetaData:E(()=>p4t,"LanguageMetaData"),parser:{}},eU={Grammar:E(()=>e4t(),"Grammar"),LanguageMetaData:E(()=>m4t,"LanguageMetaData"),parser:{}},g4t=/accDescr(?:[\t ]*:([^\n\r]*)|\s*{([^}]*)})/,y4t=/accTitle[\t ]*:([^\n\r]*)/,v4t=/title([\t ][^\n\r]*|)/,x4t={ACC_DESCR:g4t,ACC_TITLE:y4t,TITLE:v4t},Qi=class extends vH{static{o(this,"AbstractMermaidValueConverter")}static{E(this,"AbstractMermaidValueConverter")}runConverter(e,t,r){let n=this.runCommonConverter(e,t,r);return n===void 0&&(n=this.runCustomConverter(e,t,r)),n===void 0?super.runConverter(e,t,r):n}runCommonConverter(e,t,r){let n=x4t[e.name];if(n===void 0)return;let i=n.exec(t);if(i!==null){if(i[1]!==void 0)return i[1].trim().replace(/[\t ]{2,}/gm," ");if(i[2]!==void 0)return i[2].replace(/^\s*/gm,"").replace(/\s+$/gm,"").replace(/[\t ]{2,}/gm," ").replace(/[\n\r]{2,}/gm,` +`)}}},Jo=class extends Qi{static{o(this,"CommonValueConverter")}static{E(this,"CommonValueConverter")}runCustomConverter(e,t,r){}},Mn=class extends g_{static{o(this,"AbstractMermaidTokenBuilder")}static{E(this,"AbstractMermaidTokenBuilder")}constructor(e){super(),this.keywords=new Set(e)}buildKeywordTokens(e,t,r){let n=super.buildKeywordTokens(e,t,r);return n.forEach(i=>{this.keywords.has(i.name)&&i.PATTERN!==void 0&&(i.PATTERN=new RegExp(i.PATTERN.toString()+"(?:(?=%%)|(?!\\S))"))}),n}},b4t=class extends Mn{static{o(this,"CommonTokenBuilder")}static{E(this,"CommonTokenBuilder")}};});function A_(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),YH,E_);return t.ServiceRegistry.register(r),{shared:t,Radar:r}}var T4t,E_,tU=F(()=>{"use strict";vn();T4t=class extends Mn{static{o(this,"RadarTokenBuilder")}static{E(this,"RadarTokenBuilder")}constructor(){super(["radar-beta"])}},E_={parser:{TokenBuilder:E(()=>new T4t,"TokenBuilder"),ValueConverter:E(()=>new Jo,"ValueConverter")}};o(A_,"createRadarServices");E(A_,"createRadarServices")});function Vv(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),KH,R_);return t.ServiceRegistry.register(r),{shared:t,Railroad:r}}var C4t,KLe,w4t,R_,rU=F(()=>{"use strict";vn();C4t=class extends Mn{static{o(this,"RailroadTokenBuilder")}static{E(this,"RailroadTokenBuilder")}constructor(){super(["railroad-beta"])}},KLe=E(e=>{let t=e.slice(1,-1),r="";for(let n=0;nnew C4t,"TokenBuilder"),ValueConverter:E(()=>new w4t,"ValueConverter")}};o(Vv,"createRailroadServices");E(Vv,"createRailroadServices")});function Wv(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),XH,__);return t.ServiceRegistry.register(r),{shared:t,RailroadEbnf:r}}var k4t,ZLe,S4t,__,nU=F(()=>{"use strict";vn();k4t=class extends Mn{static{o(this,"RailroadEbnfTokenBuilder")}static{E(this,"RailroadEbnfTokenBuilder")}constructor(){super(["railroad-ebnf-beta"])}},ZLe=E(e=>{let t=e.slice(1,-1),r="";for(let n=0;nnew k4t,"TokenBuilder"),ValueConverter:E(()=>new S4t,"ValueConverter")}};o(Wv,"createRailroadEbnfServices");E(Wv,"createRailroadEbnfServices")});function qv(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),jH,L_);return t.ServiceRegistry.register(r),{shared:t,RailroadAbnf:r}}var E4t,A4t,L_,iU=F(()=>{"use strict";vn();E4t=class extends Mn{static{o(this,"RailroadAbnfTokenBuilder")}static{E(this,"RailroadAbnfTokenBuilder")}constructor(){super(["railroad-abnf-beta"])}},A4t=class extends Qi{static{o(this,"RailroadAbnfValueConverter")}static{E(this,"RailroadAbnfValueConverter")}runConverter(e,t,r){let n=super.runConverter(e,t,r);if(e.name==="TITLE"&&typeof n=="string"){let i=n.trim();if(i.startsWith('"')&&i.endsWith('"')||i.startsWith("'")&&i.endsWith("'"))return i.slice(1,-1)}return n}runCustomConverter(e,t,r){if(e.name==="ABNF_STRING")return t.slice(1,-1)}},L_={parser:{TokenBuilder:E(()=>new E4t,"TokenBuilder"),ValueConverter:E(()=>new A4t,"ValueConverter")}};o(qv,"createRailroadAbnfServices");E(qv,"createRailroadAbnfServices")});function Hv(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),ZH,D_);return t.ServiceRegistry.register(r),{shared:t,RailroadPeg:r}}var R4t,QLe,_4t,D_,aU=F(()=>{"use strict";vn();R4t=class extends Mn{static{o(this,"RailroadPegTokenBuilder")}static{E(this,"RailroadPegTokenBuilder")}constructor(){super(["railroad-peg-beta"])}},QLe=E(e=>{let t=e.slice(1,-1),r="";for(let n=0;nnew R4t,"TokenBuilder"),ValueConverter:E(()=>new _4t,"ValueConverter")}};o(Hv,"createRailroadPegServices");E(Hv,"createRailroadPegServices")});function JLe(e){let t=e.validation.TreemapValidator,r=e.validation.ValidationRegistry;if(r){let n={Treemap:t.checkSingleRoot.bind(t)};r.register(n,t)}}function M_(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),QH,I_);return t.ServiceRegistry.register(r),JLe(r),{shared:t,Treemap:r}}var L4t,D4t,I4t,M4t,I_,sU=F(()=>{"use strict";vn();L4t=class extends Mn{static{o(this,"TreemapTokenBuilder")}static{E(this,"TreemapTokenBuilder")}constructor(){super(["treemap"])}},D4t=/classDef\s+([A-Z_a-z]\w+)(?:\s+([^\n\r;]*))?;?/,I4t=class extends Qi{static{o(this,"TreemapValueConverter")}static{E(this,"TreemapValueConverter")}runCustomConverter(e,t,r){if(e.name==="NUMBER2")return parseFloat(t.replace(/,/g,""));if(e.name==="SEPARATOR")return t.substring(1,t.length-1);if(e.name==="STRING2")return t.substring(1,t.length-1);if(e.name==="INDENTATION")return t.length;if(e.name==="ClassDef"){if(typeof t!="string")return t;let n=D4t.exec(t);if(n)return{$type:"ClassDefStatement",className:n[1],styleText:n[2]||void 0}}}};o(JLe,"registerValidationChecks");E(JLe,"registerValidationChecks");M4t=class{static{o(this,"TreemapValidator")}static{E(this,"TreemapValidator")}checkSingleRoot(e,t){let r;for(let n of e.TreemapRows)n.item&&(r===void 0&&n.indent===void 0?r=0:n.indent===void 0?t("error","Multiple root nodes are not allowed in a treemap.",{node:n,property:"item"}):r!==void 0&&r>=parseInt(n.indent,10)&&t("error","Multiple root nodes are not allowed in a treemap.",{node:n,property:"item"}))}},I_={parser:{TokenBuilder:E(()=>new L4t,"TokenBuilder"),ValueConverter:E(()=>new I4t,"ValueConverter")},validation:{TreemapValidator:E(()=>new M4t,"TreemapValidator")}};o(M_,"createTreemapServices");E(M_,"createTreemapServices")});function P_(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),eU,N_);return t.ServiceRegistry.register(r),{shared:t,Wardley:r}}var N4t,N_,oU=F(()=>{"use strict";vn();N4t=class extends Qi{static{o(this,"WardleyValueConverter")}static{E(this,"WardleyValueConverter")}runCustomConverter(e,t,r){switch(e.name.toUpperCase()){case"LINK_LABEL":return t.substring(1).trim();default:return}}},N_={parser:{ValueConverter:E(()=>new N4t,"ValueConverter")}};o(P_,"createWardleyServices");E(P_,"createWardleyServices")});function B_(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),GH,O_);return t.ServiceRegistry.register(r),{shared:t,Cynefin:r}}var P4t,O_,lU=F(()=>{"use strict";vn();P4t=class extends Mn{static{o(this,"CynefinTokenBuilder")}static{E(this,"CynefinTokenBuilder")}constructor(){super(["cynefin-beta"])}},O_={parser:{TokenBuilder:E(()=>new P4t,"TokenBuilder"),ValueConverter:E(()=>new Jo,"ValueConverter")}};o(B_,"createCynefinServices");E(B_,"createCynefinServices")});function F_(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),WH,$_);return t.ServiceRegistry.register(r),{shared:t,GitGraph:r}}var O4t,$_,cU=F(()=>{"use strict";vn();O4t=class extends Mn{static{o(this,"GitGraphTokenBuilder")}static{E(this,"GitGraphTokenBuilder")}constructor(){super(["gitGraph"])}},$_={parser:{TokenBuilder:E(()=>new O4t,"TokenBuilder"),ValueConverter:E(()=>new Jo,"ValueConverter")}};o(F_,"createGitGraphServices");E(F_,"createGitGraphServices")});function G_(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),qH,z_);return t.ServiceRegistry.register(r),{shared:t,Info:r}}var B4t,z_,uU=F(()=>{"use strict";vn();B4t=class extends Mn{static{o(this,"InfoTokenBuilder")}static{E(this,"InfoTokenBuilder")}constructor(){super(["info","showInfo"])}},z_={parser:{TokenBuilder:E(()=>new B4t,"TokenBuilder"),ValueConverter:E(()=>new Jo,"ValueConverter")}};o(G_,"createInfoServices");E(G_,"createInfoServices")});function W_(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),HH,V_);return t.ServiceRegistry.register(r),{shared:t,Packet:r}}var $4t,V_,hU=F(()=>{"use strict";vn();$4t=class extends Mn{static{o(this,"PacketTokenBuilder")}static{E(this,"PacketTokenBuilder")}constructor(){super(["packet"])}},V_={parser:{TokenBuilder:E(()=>new $4t,"TokenBuilder"),ValueConverter:E(()=>new Jo,"ValueConverter")}};o(W_,"createPacketServices");E(W_,"createPacketServices")});function H_(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),UH,q_);return t.ServiceRegistry.register(r),{shared:t,Pie:r}}var F4t,z4t,q_,dU=F(()=>{"use strict";vn();F4t=class extends Mn{static{o(this,"PieTokenBuilder")}static{E(this,"PieTokenBuilder")}constructor(){super(["pie","showData"])}},z4t=class extends Qi{static{o(this,"PieValueConverter")}static{E(this,"PieValueConverter")}runCustomConverter(e,t,r){if(e.name==="PIE_SECTION_LABEL")return t.replace(/"/g,"").trim()}},q_={parser:{TokenBuilder:E(()=>new F4t,"TokenBuilder"),ValueConverter:E(()=>new z4t,"ValueConverter")}};o(H_,"createPieServices");E(H_,"createPieServices")});function Y_(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),JH,U_);return t.ServiceRegistry.register(r),{shared:t,TreeView:r}}var G4t,V4t,U_,fU=F(()=>{"use strict";vn();G4t=class extends Qi{static{o(this,"TreeViewValueConverter")}static{E(this,"TreeViewValueConverter")}runCustomConverter(e,t,r){if(e.name==="INDENTATION")return t?.length||0;if(e.name==="QUOTED_NAME")return t.substring(1,t.length-1);if(e.name==="BARE_NAME")return t.replace(/[\t ]+$/,"");if(e.name==="CLASS_ANNOTATION")return t.trim().substring(3).trim();if(e.name==="ICON_ANNOTATION"){let n=t.trim();return n.substring(5,n.length-1)}if(e.name==="DESC_ANNOTATION")return t.trim().substring(2).trim()}},V4t=class extends Mn{static{o(this,"TreeViewTokenBuilder")}static{E(this,"TreeViewTokenBuilder")}constructor(){super(["treeView-beta"])}},U_={parser:{TokenBuilder:E(()=>new V4t,"TokenBuilder"),ValueConverter:E(()=>new G4t,"ValueConverter")}};o(Y_,"createTreeViewServices");E(Y_,"createTreeViewServices")});function X_(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),zH,j_);return t.ServiceRegistry.register(r),{shared:t,Architecture:r}}var W4t,q4t,j_,pU=F(()=>{"use strict";vn();W4t=class extends Mn{static{o(this,"ArchitectureTokenBuilder")}static{E(this,"ArchitectureTokenBuilder")}constructor(){super(["architecture"])}},q4t=class extends Qi{static{o(this,"ArchitectureValueConverter")}static{E(this,"ArchitectureValueConverter")}runCustomConverter(e,t,r){if(e.name==="ARCH_ICON")return t.replace(/[()]/g,"").trim();if(e.name==="ARCH_TEXT_ICON")return t.replace(/["()]/g,"");if(e.name==="ARCH_TITLE"){let n=t.replace(/^\[|]$/g,"").trim();return(n.startsWith('"')&&n.endsWith('"')||n.startsWith("'")&&n.endsWith("'"))&&(n=n.slice(1,-1),n=n.replace(/\\"/g,'"').replace(/\\'/g,"'")),n.trim()}}},j_={parser:{TokenBuilder:E(()=>new W4t,"TokenBuilder"),ValueConverter:E(()=>new q4t,"ValueConverter")}};o(X_,"createArchitectureServices");E(X_,"createArchitectureServices")});function iDe(e){let t=e.validation.EventModelingValidator,r=e.validation.ValidationRegistry;if(r){let n={EmTimeFrame:t.checkSourceFrameTypes.bind(t),EmResetFrame:t.checkSourceFrameTypes.bind(t)};r.register(n,t)}}function Z_(e=yn){let t=Mr(dn(e),An),r=Mr(hn({shared:t}),VH,K_);return t.ServiceRegistry.register(r),iDe(r),{shared:t,EventModel:r}}var H4t,eDe,tDe,mU,rDe,nDe,U4t,K_,gU=F(()=>{"use strict";vn();H4t=class extends Mn{static{o(this,"EventModelingTokenBuilder")}static{E(this,"EventModelingTokenBuilder")}constructor(){super(["eventmodeling"])}},eDe=new Set(["cmd","command"]),tDe=new Set(["evt","event"]),mU=new Set(["rmo","readmodel"]),rDe=new Set(["pcr","processor"]),nDe=new Set(["ui"]);o(iDe,"registerValidationChecks");E(iDe,"registerValidationChecks");U4t=class{static{o(this,"EventModelingValidator")}static{E(this,"EventModelingValidator")}checkSourceFrameTypes(e,t){e.sourceFrames.length!==0&&(eDe.has(e.modelEntityType)?this.validateSources(e,new Set([...nDe,...rDe]),"command","ui or processor",t):tDe.has(e.modelEntityType)?this.validateSources(e,eDe,"event","command",t):mU.has(e.modelEntityType)?this.validateSources(e,tDe,"read model","event",t):rDe.has(e.modelEntityType)?this.validateSources(e,mU,"processor","read model",t):nDe.has(e.modelEntityType)&&this.validateSources(e,mU,"ui","read model",t))}validateSources(e,t,r,n,i){for(let a of e.sourceFrames){let s=a.ref;s!==void 0&&!t.has(s.modelEntityType)&&i("error",`A ${r} can only receive input from a ${n}, not from '${s.modelEntityType}'.`,{node:e,property:"sourceFrames"})}}},K_={parser:{TokenBuilder:E(()=>new H4t,"TokenBuilder"),ValueConverter:E(()=>new Jo,"ValueConverter")},validation:{EventModelingValidator:E(()=>new U4t,"EventModelingValidator")}};o(Z_,"createEventModelingServices");E(Z_,"createEventModelingServices")});var aDe={};ir(aDe,{InfoModule:()=>z_,createInfoServices:()=>G_});var sDe=F(()=>{"use strict";uU();vn()});var oDe={};ir(oDe,{PacketModule:()=>V_,createPacketServices:()=>W_});var lDe=F(()=>{"use strict";hU();vn()});var cDe={};ir(cDe,{PieModule:()=>q_,createPieServices:()=>H_});var uDe=F(()=>{"use strict";dU();vn()});var hDe={};ir(hDe,{TreeViewModule:()=>U_,createTreeViewServices:()=>Y_});var dDe=F(()=>{"use strict";fU();vn()});var fDe={};ir(fDe,{ArchitectureModule:()=>j_,createArchitectureServices:()=>X_});var pDe=F(()=>{"use strict";pU();vn()});var mDe={};ir(mDe,{GitGraphModule:()=>$_,createGitGraphServices:()=>F_});var gDe=F(()=>{"use strict";cU();vn()});var yDe={};ir(yDe,{EventModelingModule:()=>K_,createEventModelingServices:()=>Z_});var vDe=F(()=>{"use strict";gU();vn()});var xDe={};ir(xDe,{RadarModule:()=>E_,createRadarServices:()=>A_});var bDe=F(()=>{"use strict";tU();vn()});var TDe={};ir(TDe,{RailroadModule:()=>R_,createRailroadServices:()=>Vv});var CDe=F(()=>{"use strict";rU();vn()});var wDe={};ir(wDe,{RailroadEbnfModule:()=>__,createRailroadEbnfServices:()=>Wv});var kDe=F(()=>{"use strict";nU();vn()});var SDe={};ir(SDe,{RailroadAbnfModule:()=>L_,createRailroadAbnfServices:()=>qv});var EDe=F(()=>{"use strict";iU();vn()});var ADe={};ir(ADe,{RailroadPegModule:()=>D_,createRailroadPegServices:()=>Hv});var RDe=F(()=>{"use strict";aU();vn()});var _De={};ir(_De,{TreemapModule:()=>I_,createTreemapServices:()=>M_});var LDe=F(()=>{"use strict";sU();vn()});var DDe={};ir(DDe,{WardleyModule:()=>N_,createWardleyServices:()=>P_});var IDe=F(()=>{"use strict";oU();vn()});var MDe={};ir(MDe,{CynefinModule:()=>O_,createCynefinServices:()=>B_});var NDe=F(()=>{"use strict";lU();vn()});async function Si(e,t){let r=Y4t[e];if(!r)throw new Error(`Unknown diagram type: ${e}`);ja[e]||await r();let i=ja[e].parse(t);if(i.lexerErrors.length>0||i.parserErrors.length>0)throw new sd(i);return i.value}var ja,Y4t,sd,Xa=F(()=>{"use strict";tU();rU();nU();iU();aU();sU();oU();lU();cU();uU();hU();dU();fU();pU();gU();vn();ja={},Y4t={info:E(async()=>{let{createInfoServices:e}=await Promise.resolve().then(()=>(sDe(),aDe)),t=e().Info.parser.LangiumParser;ja.info=t},"info"),packet:E(async()=>{let{createPacketServices:e}=await Promise.resolve().then(()=>(lDe(),oDe)),t=e().Packet.parser.LangiumParser;ja.packet=t},"packet"),pie:E(async()=>{let{createPieServices:e}=await Promise.resolve().then(()=>(uDe(),cDe)),t=e().Pie.parser.LangiumParser;ja.pie=t},"pie"),treeView:E(async()=>{let{createTreeViewServices:e}=await Promise.resolve().then(()=>(dDe(),hDe)),t=e().TreeView.parser.LangiumParser;ja.treeView=t},"treeView"),architecture:E(async()=>{let{createArchitectureServices:e}=await Promise.resolve().then(()=>(pDe(),fDe)),t=e().Architecture.parser.LangiumParser;ja.architecture=t},"architecture"),gitGraph:E(async()=>{let{createGitGraphServices:e}=await Promise.resolve().then(()=>(gDe(),mDe)),t=e().GitGraph.parser.LangiumParser;ja.gitGraph=t},"gitGraph"),eventmodeling:E(async()=>{let{createEventModelingServices:e}=await Promise.resolve().then(()=>(vDe(),yDe)),t=e().EventModel.parser.LangiumParser;ja.eventmodeling=t},"eventmodeling"),radar:E(async()=>{let{createRadarServices:e}=await Promise.resolve().then(()=>(bDe(),xDe)),t=e().Radar.parser.LangiumParser;ja.radar=t},"radar"),railroad:E(async()=>{let{createRailroadServices:e}=await Promise.resolve().then(()=>(CDe(),TDe)),t=e().Railroad.parser.LangiumParser;ja.railroad=t},"railroad"),railroadEbnf:E(async()=>{let{createRailroadEbnfServices:e}=await Promise.resolve().then(()=>(kDe(),wDe)),t=e().RailroadEbnf.parser.LangiumParser;ja.railroadEbnf=t},"railroadEbnf"),railroadAbnf:E(async()=>{let{createRailroadAbnfServices:e}=await Promise.resolve().then(()=>(EDe(),SDe)),t=e().RailroadAbnf.parser.LangiumParser;ja.railroadAbnf=t},"railroadAbnf"),railroadPeg:E(async()=>{let{createRailroadPegServices:e}=await Promise.resolve().then(()=>(RDe(),ADe)),t=e().RailroadPeg.parser.LangiumParser;ja.railroadPeg=t},"railroadPeg"),treemap:E(async()=>{let{createTreemapServices:e}=await Promise.resolve().then(()=>(LDe(),_De)),t=e().Treemap.parser.LangiumParser;ja.treemap=t},"treemap"),wardley:E(async()=>{let{createWardleyServices:e}=await Promise.resolve().then(()=>(IDe(),DDe)),t=e().Wardley.parser.LangiumParser;ja.wardley=t},"wardley"),cynefin:E(async()=>{let{createCynefinServices:e}=await Promise.resolve().then(()=>(NDe(),MDe)),t=e().Cynefin.parser.LangiumParser;ja.cynefin=t},"cynefin")};o(Si,"parse");E(Si,"parse");sd=class extends Error{static{o(this,"MermaidParseError")}constructor(e){let t=e.lexerErrors.map(n=>{let i=n.line!==void 0&&!isNaN(n.line)?n.line:"?",a=n.column!==void 0&&!isNaN(n.column)?n.column:"?";return`Lexer error on line ${i}, column ${a}: ${n.message}`}).join(` +`),r=e.parserErrors.map(n=>{let i=n.token.startLine!==void 0&&!isNaN(n.token.startLine)?n.token.startLine:"?",a=n.token.startColumn!==void 0&&!isNaN(n.token.startColumn)?n.token.startColumn:"?";return`Parse error on line ${i}, column ${a}: ${n.message}`}).join(` +`);super(`Parsing failed: ${t} ${r}`),this.result=e}static{E(this,"MermaidParseError")}}});function Gn(e,t){e.accDescr&&t.setAccDescription?.(e.accDescr),e.accTitle&&t.setAccTitle?.(e.accTitle),e.title&&t.setDiagramTitle?.(e.title)}var Hs=F(()=>{"use strict";o(Gn,"populateCommonDb")});var Rn,Q_=F(()=>{"use strict";Rn={NORMAL:0,REVERSE:1,HIGHLIGHT:2,MERGE:3,CHERRY_PICK:4}});var wp,J_=F(()=>{"use strict";wp=class{constructor(t){this.init=t;this.records=this.init()}static{o(this,"ImperativeState")}reset(){this.records=this.init()}}});function yU(){return pP({length:7})}function X4t(e,t){let r=Object.create(null);return e.reduce((n,i)=>{let a=t(i);return r[a]||(r[a]=!0,n.push(i)),n},[])}function PDe(e,t,r){let n=e.indexOf(t);n===-1?e.push(r):e.splice(n,1,r)}function BDe(e){let t=e.reduce((i,a)=>i.seq>a.seq?i:a,e[0]),r="";e.forEach(function(i){i===t?r+=" *":r+=" |"});let n=[r,t.id,t.seq];for(let i in Vt.records.branches)Vt.records.branches.get(i)===t.id&&n.push(i);if(Z.debug(n.join(" ")),t.parents&&t.parents.length==2&&t.parents[0]&&t.parents[1]){let i=Vt.records.commits.get(t.parents[0]);PDe(e,t,i),t.parents[1]&&e.push(Vt.records.commits.get(t.parents[1]))}else{if(t.parents.length==0)return;if(t.parents[0]){let i=Vt.records.commits.get(t.parents[0]);PDe(e,t,i)}}e=X4t(e,i=>i.id),BDe(e)}var j4t,b0,Vt,K4t,Z4t,Q4t,J4t,e3t,t3t,r3t,ODe,n3t,i3t,a3t,s3t,o3t,$De,l3t,c3t,u3t,eL,vU=F(()=>{"use strict";vt();Qt();ur();Vr();Nn();Q_();J_();Wi();j4t=cr.gitGraph,b0=o(()=>qr({...j4t,..._t().gitGraph}),"getConfig"),Vt=new wp(()=>{let e=b0(),t=e.mainBranchName,r=e.mainBranchOrder;return{mainBranchName:t,commits:new Map,head:null,branchConfig:new Map([[t,{name:t,order:r}]]),branches:new Map([[t,null]]),currBranch:t,direction:"LR",seq:0,options:{}}});o(yU,"getID");o(X4t,"uniqBy");K4t=o(function(e){Vt.records.direction=e},"setDirection"),Z4t=o(function(e){Z.debug("options str",e),e=e?.trim(),e=e||"{}";try{Vt.records.options=JSON.parse(e)}catch(t){Z.error("error while parsing gitGraph options",t.message)}},"setOptions"),Q4t=o(function(){return Vt.records.options},"getOptions"),J4t=o(function(e){let t=e.msg,r=e.id,n=e.type,i=e.tags;Z.info("commit",t,r,n,i),Z.debug("Entering commit:",t,r,n,i);let a=b0();r=xt.sanitizeText(r,a),t=xt.sanitizeText(t,a),i=i?.map(l=>xt.sanitizeText(l,a));let s={id:r||Vt.records.seq+"-"+yU(),message:t,seq:Vt.records.seq++,type:n??Rn.NORMAL,tags:i??[],parents:Vt.records.head==null?[]:[Vt.records.head.id],branch:Vt.records.currBranch};Vt.records.head=s,Z.info("main branch",a.mainBranchName),Vt.records.commits.has(s.id)&&Z.warn(`Commit ID ${s.id} already exists`),Vt.records.commits.set(s.id,s),Vt.records.branches.set(Vt.records.currBranch,s.id),Z.debug("in pushCommit "+s.id)},"commit"),e3t=o(function(e){let t=e.name,r=e.order;if(t=xt.sanitizeText(t,b0()),Vt.records.branches.has(t))throw new Error(`Trying to create an existing branch. (Help: Either use a new name if you want create a new branch or try using "checkout ${t}")`);Vt.records.branches.set(t,Vt.records.head!=null?Vt.records.head.id:null),Vt.records.branchConfig.set(t,{name:t,order:r}),ODe(t),Z.debug("in createBranch")},"branch"),t3t=o(e=>{let t=e.branch,r=e.id,n=e.type,i=e.tags,a=b0();t=xt.sanitizeText(t,a),r&&(r=xt.sanitizeText(r,a));let s=Vt.records.branches.get(Vt.records.currBranch),l=Vt.records.branches.get(t),u=s?Vt.records.commits.get(s):void 0,h=l?Vt.records.commits.get(l):void 0;if(u&&h&&u.branch===t)throw new Error(`Cannot merge branch '${t}' into itself.`);if(Vt.records.currBranch===t){let p=new Error('Incorrect usage of "merge". Cannot merge a branch to itself');throw p.hash={text:`merge ${t}`,token:`merge ${t}`,expected:["branch abc"]},p}if(u===void 0||!u){let p=new Error(`Incorrect usage of "merge". Current branch (${Vt.records.currBranch})has no commits`);throw p.hash={text:`merge ${t}`,token:`merge ${t}`,expected:["commit"]},p}if(!Vt.records.branches.has(t)){let p=new Error('Incorrect usage of "merge". Branch to be merged ('+t+") does not exist");throw p.hash={text:`merge ${t}`,token:`merge ${t}`,expected:[`branch ${t}`]},p}if(h===void 0||!h){let p=new Error('Incorrect usage of "merge". Branch to be merged ('+t+") has no commits");throw p.hash={text:`merge ${t}`,token:`merge ${t}`,expected:['"commit"']},p}if(u===h){let p=new Error('Incorrect usage of "merge". Both branches have same head');throw p.hash={text:`merge ${t}`,token:`merge ${t}`,expected:["branch abc"]},p}if(r&&Vt.records.commits.has(r)){let p=new Error('Incorrect usage of "merge". Commit with id:'+r+" already exists, use different custom id");throw p.hash={text:`merge ${t} ${r} ${n} ${i?.join(" ")}`,token:`merge ${t} ${r} ${n} ${i?.join(" ")}`,expected:[`merge ${t} ${r}_UNIQUE ${n} ${i?.join(" ")}`]},p}let d=l||"",f={id:r||`${Vt.records.seq}-${yU()}`,message:`merged branch ${t} into ${Vt.records.currBranch}`,seq:Vt.records.seq++,parents:Vt.records.head==null?[]:[Vt.records.head.id,d],branch:Vt.records.currBranch,type:Rn.MERGE,customType:n,customId:!!r,tags:i??[]};Vt.records.head=f,Vt.records.commits.set(f.id,f),Vt.records.branches.set(Vt.records.currBranch,f.id),Z.debug(Vt.records.branches),Z.debug("in mergeBranch")},"merge"),r3t=o(function(e){let t=e.id,r=e.targetId,n=e.tags,i=e.parent;Z.debug("Entering cherryPick:",t,r,n);let a=b0();if(t=xt.sanitizeText(t,a),r=xt.sanitizeText(r,a),n=n?.map(u=>xt.sanitizeText(u,a)),i=xt.sanitizeText(i,a),!t||!Vt.records.commits.has(t)){let u=new Error('Incorrect usage of "cherryPick". Source commit id should exist and provided');throw u.hash={text:`cherryPick ${t} ${r}`,token:`cherryPick ${t} ${r}`,expected:["cherry-pick abc"]},u}let s=Vt.records.commits.get(t);if(s===void 0||!s)throw new Error('Incorrect usage of "cherryPick". Source commit id should exist and provided');if(i&&!(Array.isArray(s.parents)&&s.parents.includes(i)))throw new Error("Invalid operation: The specified parent commit is not an immediate parent of the cherry-picked commit.");let l=s.branch;if(s.type===Rn.MERGE&&!i)throw new Error("Incorrect usage of cherry-pick: If the source commit is a merge commit, an immediate parent commit must be specified.");if(!r||!Vt.records.commits.has(r)){if(l===Vt.records.currBranch){let f=new Error('Incorrect usage of "cherryPick". Source commit is already on current branch');throw f.hash={text:`cherryPick ${t} ${r}`,token:`cherryPick ${t} ${r}`,expected:["cherry-pick abc"]},f}let u=Vt.records.branches.get(Vt.records.currBranch);if(u===void 0||!u){let f=new Error(`Incorrect usage of "cherry-pick". Current branch (${Vt.records.currBranch})has no commits`);throw f.hash={text:`cherryPick ${t} ${r}`,token:`cherryPick ${t} ${r}`,expected:["cherry-pick abc"]},f}let h=Vt.records.commits.get(u);if(h===void 0||!h){let f=new Error(`Incorrect usage of "cherry-pick". Current branch (${Vt.records.currBranch})has no commits`);throw f.hash={text:`cherryPick ${t} ${r}`,token:`cherryPick ${t} ${r}`,expected:["cherry-pick abc"]},f}let d={id:Vt.records.seq+"-"+yU(),message:`cherry-picked ${s?.message} into ${Vt.records.currBranch}`,seq:Vt.records.seq++,parents:Vt.records.head==null?[]:[Vt.records.head.id,s.id],branch:Vt.records.currBranch,type:Rn.CHERRY_PICK,tags:n?n.filter(Boolean):[`cherry-pick:${s.id}${s.type===Rn.MERGE?`|parent:${i}`:""}`]};Vt.records.head=d,Vt.records.commits.set(d.id,d),Vt.records.branches.set(Vt.records.currBranch,d.id),Z.debug(Vt.records.branches),Z.debug("in cherryPick")}},"cherryPick"),ODe=o(function(e){if(e=xt.sanitizeText(e,b0()),Vt.records.branches.has(e)){Vt.records.currBranch=e;let t=Vt.records.branches.get(Vt.records.currBranch);t===void 0||!t?Vt.records.head=null:Vt.records.head=Vt.records.commits.get(t)??null}else{let t=new Error(`Trying to checkout branch which is not yet created. (Help try using "branch ${e}")`);throw t.hash={text:`checkout ${e}`,token:`checkout ${e}`,expected:[`branch ${e}`]},t}},"checkout");o(PDe,"upsert");o(BDe,"prettyPrintCommitHistory");n3t=o(function(){Z.debug(Vt.records.commits);let e=$De()[0];BDe([e])},"prettyPrint"),i3t=o(function(){Vt.reset(),yr()},"clear"),a3t=o(function(){return[...Vt.records.branchConfig.values()].map((t,r)=>t.order!==null&&t.order!==void 0?t:{...t,order:parseFloat(`0.${r}`)}).sort((t,r)=>(t.order??0)-(r.order??0)).map(({name:t})=>({name:t}))},"getBranchesAsObjArray"),s3t=o(function(){return Vt.records.branches},"getBranches"),o3t=o(function(){return Vt.records.commits},"getCommits"),$De=o(function(){let e=[...Vt.records.commits.values()];return e.forEach(function(t){Z.debug(t.id)}),e.sort((t,r)=>t.seq-r.seq),e},"getCommitsArray"),l3t=o(function(){return Vt.records.currBranch},"getCurrentBranch"),c3t=o(function(){return Vt.records.direction},"getDirection"),u3t=o(function(){return Vt.records.head},"getHead"),eL={commitType:Rn,getConfig:b0,setDirection:K4t,setOptions:Z4t,getOptions:Q4t,commit:J4t,branch:e3t,merge:t3t,cherryPick:r3t,checkout:ODe,prettyPrint:n3t,clear:i3t,getBranchesAsObjArray:a3t,getBranches:s3t,getCommits:o3t,getCommitsArray:$De,getCurrentBranch:l3t,getDirection:c3t,getHead:u3t,setAccTitle:kr,getAccTitle:Ar,getAccDescription:_r,setAccDescription:Rr,setDiagramTitle:Or,getDiagramTitle:Lr}});var h3t,d3t,f3t,p3t,m3t,g3t,y3t,FDe,zDe=F(()=>{"use strict";Xa();vt();Hs();vU();Q_();h3t=o((e,t)=>{Gn(e,t),e.dir&&t.setDirection(e.dir);for(let r of e.statements)d3t(r,t)},"populate"),d3t=o((e,t)=>{let n={Commit:o(i=>t.commit(f3t(i)),"Commit"),Branch:o(i=>t.branch(p3t(i)),"Branch"),Merge:o(i=>t.merge(m3t(i)),"Merge"),Checkout:o(i=>t.checkout(g3t(i)),"Checkout"),CherryPicking:o(i=>t.cherryPick(y3t(i)),"CherryPicking")}[e.$type];n?n(e):Z.error(`Unknown statement type: ${e.$type}`)},"parseStatement"),f3t=o(e=>({id:e.id,msg:e.message??"",type:e.type!==void 0?Rn[e.type]:Rn.NORMAL,tags:e.tags??void 0}),"parseCommit"),p3t=o(e=>({name:e.name,order:e.order??0}),"parseBranch"),m3t=o(e=>({branch:e.branch,id:e.id??"",type:e.type!==void 0?Rn[e.type]:void 0,tags:e.tags??void 0}),"parseMerge"),g3t=o(e=>e.branch,"parseCheckout"),y3t=o(e=>({id:e.id,targetId:"",tags:e.tags?.length===0?void 0:e.tags,parent:e.parent}),"parseCherryPicking"),FDe={parse:o(async e=>{let t=await Si("gitGraph",e);Z.debug(t),h3t(t,eL)},"parse")}});var kp,Sp,Wu,od,T0,nL,xU,bU,v3t,C0,So,Eo,tL,Mw,rL,ld,rn,x3t,VDe,WDe,b3t,T3t,C3t,w3t,k3t,S3t,E3t,A3t,R3t,_3t,L3t,D3t,GDe,I3t,Nw,M3t,N3t,P3t,O3t,B3t,qDe,HDe=F(()=>{"use strict";$r();Xt();vt();Qt();Q_();kp=10,Sp=40,Wu=4,od=2,T0=8,nL=new Set(["redux","redux-dark","redux-color","redux-dark-color"]),xU=12,bU=new Set(["redux-color","redux-dark-color"]),v3t=new Set(["dark","redux-dark","redux-dark-color","neo-dark"]),C0=o((e,t,r=!1)=>r&&e>0?(e-1)%(t-1)+1:e%t,"calcColorIndex"),So=new Map,Eo=new Map,tL=30,Mw=new Map,rL=[],ld=0,rn="LR",x3t=o(()=>{So.clear(),Eo.clear(),Mw.clear(),ld=0,rL=[],rn="LR"},"clear"),VDe=o(e=>{let t=document.createElementNS("http://www.w3.org/2000/svg","text");return(typeof e=="string"?e.split(/\\n|\n|/gi):e).forEach(n=>{let i=document.createElementNS("http://www.w3.org/2000/svg","tspan");i.setAttributeNS("http://www.w3.org/XML/1998/namespace","xml:space","preserve"),i.setAttribute("dy","1em"),i.setAttribute("x","0"),i.setAttribute("class","row"),i.textContent=n.trim(),t.appendChild(i)}),t},"drawText"),WDe=o(e=>{let t,r,n;return rn==="BT"?(r=o((i,a)=>i<=a,"comparisonFunc"),n=1/0):(r=o((i,a)=>i>=a,"comparisonFunc"),n=0),e.forEach(i=>{let a=rn==="TB"||rn=="BT"?Eo.get(i)?.y:Eo.get(i)?.x;a!==void 0&&r(a,n)&&(t=i,n=a)}),t},"findClosestParent"),b3t=o(e=>{let t="",r=1/0;return e.forEach(n=>{let i=Eo.get(n).y;i<=r&&(t=n,r=i)}),t||void 0},"findClosestParentBT"),T3t=o((e,t,r)=>{let n=r,i=r,a=[];e.forEach(s=>{let l=t.get(s);if(!l)throw new Error(`Commit not found for key ${s}`);l.parents.length?(n=w3t(l),i=Math.max(n,i)):a.push(l),k3t(l,n)}),n=i,a.forEach(s=>{S3t(s,n,r)}),e.forEach(s=>{let l=t.get(s);if(l?.parents.length){let u=b3t(l.parents);n=Eo.get(u).y-Sp,n<=i&&(i=n);let h=So.get(l.branch).pos,d=n-kp;Eo.set(l.id,{x:h,y:d})}})},"setParallelBTPos"),C3t=o(e=>{let t=WDe(e.parents.filter(n=>n!==null));if(!t)throw new Error(`Closest parent not found for commit ${e.id}`);let r=Eo.get(t)?.y;if(r===void 0)throw new Error(`Closest parent position not found for commit ${e.id}`);return r},"findClosestParentPos"),w3t=o(e=>C3t(e)+Sp,"calculateCommitPosition"),k3t=o((e,t)=>{let r=So.get(e.branch);if(!r)throw new Error(`Branch not found for commit ${e.id}`);let n=r.pos,i=t+kp;return Eo.set(e.id,{x:n,y:i}),{x:n,y:i}},"setCommitPosition"),S3t=o((e,t,r)=>{let n=So.get(e.branch);if(!n)throw new Error(`Branch not found for commit ${e.id}`);let i=t+r,a=n.pos;Eo.set(e.id,{x:a,y:i})},"setRootPosition"),E3t=o((e,t,r,n,i,a)=>{let{theme:s}=Ae(),l=nL.has(s??""),u=bU.has(s??""),h=v3t.has(s??"");if(a===Rn.HIGHLIGHT)e.append("rect").attr("x",r.x-10+(l?3:0)).attr("y",r.y-10+(l?3:0)).attr("width",l?14:20).attr("height",l?14:20).attr("class",`commit ${t.id} commit-highlight${C0(i,T0,u)} ${n}-outer`),e.append("rect").attr("x",r.x-6+(l?2:0)).attr("y",r.y-6+(l?2:0)).attr("width",l?8:12).attr("height",l?8:12).attr("class",`commit ${t.id} commit${C0(i,T0,u)} ${n}-inner`);else if(a===Rn.CHERRY_PICK)e.append("circle").attr("cx",r.x).attr("cy",r.y).attr("r",l?7:10).attr("class",`commit ${t.id} ${n}`),e.append("circle").attr("cx",r.x-3).attr("cy",r.y+2).attr("r",l?2.5:2.75).attr("fill",h?"#000000":"#fff").attr("class",`commit ${t.id} ${n}`),e.append("circle").attr("cx",r.x+3).attr("cy",r.y+2).attr("r",l?2.5:2.75).attr("fill",h?"#000000":"#fff").attr("class",`commit ${t.id} ${n}`),e.append("line").attr("x1",r.x+3).attr("y1",r.y+1).attr("x2",r.x).attr("y2",r.y-5).attr("stroke",h?"#000000":"#fff").attr("class",`commit ${t.id} ${n}`),e.append("line").attr("x1",r.x-3).attr("y1",r.y+1).attr("x2",r.x).attr("y2",r.y-5).attr("stroke",h?"#000000":"#fff").attr("class",`commit ${t.id} ${n}`);else{let d=e.append("circle");if(d.attr("cx",r.x),d.attr("cy",r.y),d.attr("r",l?7:10),d.attr("class",`commit ${t.id} commit${C0(i,T0,u)}`),a===Rn.MERGE){let f=e.append("circle");f.attr("cx",r.x),f.attr("cy",r.y),f.attr("r",l?5:6),f.attr("class",`commit ${n} ${t.id} commit${C0(i,T0,u)}`)}if(a===Rn.REVERSE){let f=e.append("path"),p=l?4:5;f.attr("d",`M ${r.x-p},${r.y-p}L${r.x+p},${r.y+p}M${r.x-p},${r.y+p}L${r.x+p},${r.y-p}`).attr("class",`commit ${n} ${t.id} commit${C0(i,T0,u)}`)}}},"drawCommitBullet"),A3t=o((e,t,r,n,i)=>{if(t.type!==Rn.CHERRY_PICK&&(t.customId&&t.type===Rn.MERGE||t.type!==Rn.MERGE)&&i.showCommitLabel){let a=e.append("g"),s=a.insert("rect").attr("class","commit-label-bkg"),l=a.append("text").attr("x",n).attr("y",r.y+25).attr("class","commit-label").text(t.id),u=l.node()?.getBBox();if(u&&(s.attr("x",r.posWithOffset-u.width/2-od).attr("y",r.y+13.5).attr("width",u.width+2*od).attr("height",u.height+2*od),rn==="TB"||rn==="BT"?(s.attr("x",r.x-(u.width+4*Wu+5)).attr("y",r.y-12),l.attr("x",r.x-(u.width+4*Wu)).attr("y",r.y+u.height-12)):l.attr("x",r.posWithOffset-u.width/2),i.rotateCommitLabel))if(rn==="TB"||rn==="BT")l.attr("transform","rotate(-45, "+r.x+", "+r.y+")"),s.attr("transform","rotate(-45, "+r.x+", "+r.y+")");else{let h=-7.5-(u.width+10)/25*9.5,d=10+u.width/25*8.5;a.attr("transform","translate("+h+", "+d+") rotate(-45, "+n+", "+r.y+")")}}},"drawCommitLabel"),R3t=o((e,t,r,n)=>{if(t.tags.length>0){let i=0,a=0,s=0,l=[];for(let u of t.tags.reverse()){let h=e.insert("polygon"),d=e.append("circle"),f=e.append("text").attr("y",r.y-16-i).attr("class","tag-label").text(u),p=f.node()?.getBBox();if(!p)throw new Error("Tag bbox not found");a=Math.max(a,p.width),s=Math.max(s,p.height),f.attr("x",r.posWithOffset-p.width/2),l.push({tag:f,hole:d,rect:h,yOffset:i}),i+=20}for(let{tag:u,hole:h,rect:d,yOffset:f}of l){let p=s/2,m=r.y-19.2-f;if(d.attr("class","tag-label-bkg").attr("points",` + ${n-a/2-Wu/2},${m+od} + ${n-a/2-Wu/2},${m-od} + ${r.posWithOffset-a/2-Wu},${m-p-od} + ${r.posWithOffset+a/2+Wu},${m-p-od} + ${r.posWithOffset+a/2+Wu},${m+p+od} + ${r.posWithOffset-a/2-Wu},${m+p+od}`),h.attr("cy",m).attr("cx",n-a/2+Wu/2).attr("r",1.5).attr("class","tag-hole"),rn==="TB"||rn==="BT"){let g=n+f;d.attr("class","tag-label-bkg").attr("points",` + ${r.x},${g+2} + ${r.x},${g-2} + ${r.x+kp},${g-p-2} + ${r.x+kp+a+4},${g-p-2} + ${r.x+kp+a+4},${g+p+2} + ${r.x+kp},${g+p+2}`).attr("transform","translate(12,12) rotate(45, "+r.x+","+n+")"),h.attr("cx",r.x+Wu/2).attr("cy",g).attr("transform","translate(12,12) rotate(45, "+r.x+","+n+")"),u.attr("x",r.x+5).attr("y",g+3).attr("transform","translate(14,14) rotate(45, "+r.x+","+n+")")}}}},"drawCommitTags"),_3t=o(e=>{switch(e.customType??e.type){case Rn.NORMAL:return"commit-normal";case Rn.REVERSE:return"commit-reverse";case Rn.HIGHLIGHT:return"commit-highlight";case Rn.MERGE:return"commit-merge";case Rn.CHERRY_PICK:return"commit-cherry-pick";default:return"commit-normal"}},"getCommitClassType"),L3t=o((e,t,r,n)=>{let i={x:0,y:0};if(e.parents.length>0){let a=WDe(e.parents);if(a){let s=n.get(a)??i;return t==="TB"?s.y+Sp:t==="BT"?(n.get(e.id)??i).y-Sp:s.x+Sp}}else return t==="TB"?tL:t==="BT"?(n.get(e.id)??i).y-Sp:0;return 0},"calculatePosition"),D3t=o((e,t,r)=>{let n=rn==="BT"&&r?t:t+kp,i=So.get(e.branch)?.pos,a=rn==="TB"||rn==="BT"?So.get(e.branch)?.pos:n;if(a===void 0||i===void 0)throw new Error(`Position were undefined for commit ${e.id}`);let s=nL.has(Ae().theme??""),l=rn==="TB"||rn==="BT"?n:i+(s?xU/2+1:-2);return{x:a,y:l,posWithOffset:n}},"getCommitPosition"),GDe=o((e,t,r,n)=>{let i=e.append("g").attr("class","commit-bullets"),a=e.append("g").attr("class","commit-labels"),s=rn==="TB"||rn==="BT"?tL:0,l=[...t.keys()],u=n.parallelCommits??!1,h=o((f,p)=>{let m=t.get(f)?.seq,g=t.get(p)?.seq;return m!==void 0&&g!==void 0?m-g:0},"sortKeys"),d=l.sort(h);rn==="BT"&&(u&&T3t(d,t,s),d=d.reverse()),d.forEach(f=>{let p=t.get(f);if(!p)throw new Error(`Commit not found for key ${f}`);u&&(s=L3t(p,rn,s,Eo));let m=D3t(p,s,u);if(r){let g=_3t(p),y=p.customType??p.type,v=So.get(p.branch)?.index??0;E3t(i,p,m,g,v,y),A3t(a,p,m,s,n),R3t(a,p,m,s)}rn==="TB"||rn==="BT"?Eo.set(p.id,{x:m.x,y:m.posWithOffset}):Eo.set(p.id,{x:m.posWithOffset,y:m.y}),s=rn==="BT"&&u?s+Sp:s+Sp+kp,s>ld&&(ld=s)})},"drawCommits"),I3t=o((e,t,r,n,i)=>{let s=(rn==="TB"||rn==="BT"?r.xh.branch===s,"isOnBranchToGetCurve"),u=o(h=>h.seq>e.seq&&h.sequ(h)&&l(h))},"shouldRerouteArrow"),Nw=o((e,t,r=0)=>{let n=e+Math.abs(e-t)/2;if(r>5)return n;if(rL.every(s=>Math.abs(s-n)>=10))return rL.push(n),n;let a=Math.abs(e-t);return Nw(e,t-a/5,r+1)},"findLane"),M3t=o((e,t,r,n)=>{let{theme:i}=Ae(),a=bU.has(i??""),s=Eo.get(t.id),l=Eo.get(r.id);if(s===void 0||l===void 0)throw new Error(`Commit positions not found for commits ${t.id} and ${r.id}`);let u=I3t(t,r,s,l,n),h="",d="",f=0,p=0,m=So.get(r.branch)?.index;r.type===Rn.MERGE&&t.id!==r.parents[0]&&(m=So.get(t.branch)?.index);let g;if(u){h="A 10 10, 0, 0, 0,",d="A 10 10, 0, 0, 1,",f=10,p=10;let y=s.yl.x&&(h="A 20 20, 0, 0, 0,",d="A 20 20, 0, 0, 1,",f=20,p=20,r.type===Rn.MERGE&&t.id!==r.parents[0]?g=`M ${s.x} ${s.y} L ${s.x} ${l.y-f} ${d} ${s.x-p} ${l.y} L ${l.x} ${l.y}`:g=`M ${s.x} ${s.y} L ${l.x+f} ${s.y} ${h} ${l.x} ${s.y+p} L ${l.x} ${l.y}`),s.x===l.x&&(g=`M ${s.x} ${s.y} L ${l.x} ${l.y}`)):rn==="BT"?(s.xl.x&&(h="A 20 20, 0, 0, 0,",d="A 20 20, 0, 0, 1,",f=20,p=20,r.type===Rn.MERGE&&t.id!==r.parents[0]?g=`M ${s.x} ${s.y} L ${s.x} ${l.y+f} ${h} ${s.x-p} ${l.y} L ${l.x} ${l.y}`:g=`M ${s.x} ${s.y} L ${l.x+f} ${s.y} ${d} ${l.x} ${s.y-p} L ${l.x} ${l.y}`),s.x===l.x&&(g=`M ${s.x} ${s.y} L ${l.x} ${l.y}`)):(s.yl.y&&(r.type===Rn.MERGE&&t.id!==r.parents[0]?g=`M ${s.x} ${s.y} L ${l.x-f} ${s.y} ${h} ${l.x} ${s.y-p} L ${l.x} ${l.y}`:g=`M ${s.x} ${s.y} L ${s.x} ${l.y+f} ${d} ${s.x+p} ${l.y} L ${l.x} ${l.y}`),s.y===l.y&&(g=`M ${s.x} ${s.y} L ${l.x} ${l.y}`));if(g===void 0)throw new Error("Line definition not found");e.append("path").attr("d",g).attr("class","arrow arrow"+C0(m,T0,a))},"drawArrow"),N3t=o((e,t)=>{let r=e.append("g").attr("class","commit-arrows");[...t.keys()].forEach(n=>{let i=t.get(n);i.parents&&i.parents.length>0&&i.parents.forEach(a=>{M3t(r,t.get(a),i,t)})})},"drawArrows"),P3t=o((e,t,r,n)=>{let{look:i,theme:a,themeVariables:s}=Ae(),{dropShadow:l,THEME_COLOR_LIMIT:u}=s,h=nL.has(a??""),d=bU.has(a??""),f=e.append("g");t.forEach((p,m)=>{let g=C0(m,h?u:T0,d),y=So.get(p.name)?.pos;if(y===void 0)throw new Error(`Position not found for branch ${p.name}`);let v=rn==="TB"||rn==="BT"?y:h?y+xU/2+1:y-2,x=f.append("line");x.attr("x1",0),x.attr("y1",v),x.attr("x2",ld),x.attr("y2",v),x.attr("class","branch branch"+g),rn==="TB"?(x.attr("y1",tL),x.attr("x1",y),x.attr("y2",ld),x.attr("x2",y)):rn==="BT"&&(x.attr("y1",ld),x.attr("x1",y),x.attr("y2",tL),x.attr("x2",y)),rL.push(v);let b=p.name,T=VDe(b),k=f.insert("rect"),w=f.insert("g").attr("class","branchLabel").insert("g").attr("class","label branch-label"+g);w.node().appendChild(T);let S=T.getBBox(),R=h?0:4,L=h?16:0,N=h?xU:0;i==="neo"&&k.attr("data-look","neo"),k.attr("class","branchLabelBkg label"+g).attr("style",i==="neo"?`filter:${h?`url(#${n}-drop-shadow)`:l}`:"").attr("rx",R).attr("ry",R).attr("x",-S.width-4-(r.rotateCommitLabel===!0?30:0)).attr("y",-S.height/2+10).attr("width",S.width+18+L).attr("height",S.height+4+N),w.attr("transform","translate("+(-S.width-14-(r.rotateCommitLabel===!0?30:0)+L/2)+", "+(v-S.height/2-2)+")"),rn==="TB"?(k.attr("x",y-S.width/2-10).attr("y",0),w.attr("transform","translate("+(y-S.width/2-5)+", 0)"),h&&(k.attr("transform",`translate(${-L/2-3}, ${-N-10})`),w.attr("transform","translate("+(y-S.width/2-5)+", "+(-N*2+7)+")"))):rn==="BT"?(k.attr("x",y-S.width/2-10).attr("y",ld),w.attr("transform","translate("+(y-S.width/2-5)+", "+ld+")"),h&&(k.attr("transform",`translate(${-L/2-3}, ${N+10})`),w.attr("transform","translate("+(y-S.width/2-5)+", "+(ld+N*2+4)+")"))):k.attr("transform","translate(-19, "+(v-12-N/2)+")")})},"drawBranches"),O3t=o(function(e,t,r,n,i){return So.set(e,{pos:t,index:r}),t+=50+(i?40:0)+(rn==="TB"||rn==="BT"?n.width/2:0),t},"setBranchPosition"),B3t=o(function(e,t,r,n){x3t(),Z.debug("in gitgraph renderer",e+` +`,"id:",t,r);let i=n.db;if(!i.getConfig){Z.error("getConfig method is not available on db");return}let a=i.getConfig(),s=a.rotateCommitLabel??!1;Mw=i.getCommits();let l=i.getBranchesAsObjArray();rn=i.getDirection();let u=et(`[id="${t}"]`),{look:h,theme:d,themeVariables:f}=Ae(),{useGradient:p,gradientStart:m,gradientStop:g,filterColor:y}=f;if(p){let x=u.append("defs").append("linearGradient").attr("id",t+"-gradient").attr("gradientUnits","objectBoundingBox").attr("x1","0%").attr("y1","0%").attr("x2","100%").attr("y2","0%");x.append("stop").attr("offset","0%").attr("stop-color",m).attr("stop-opacity",1),x.append("stop").attr("offset","100%").attr("stop-color",g).attr("stop-opacity",1)}h==="neo"&&nL.has(d??"")&&u.append("defs").append("filter").attr("id",t+"-drop-shadow").attr("height","130%").attr("width","130%").append("feDropShadow").attr("dx","4").attr("dy","4").attr("stdDeviation",0).attr("flood-opacity","0.06").attr("flood-color",y);let v=0;l.forEach((x,b)=>{let T=VDe(x.name),k=u.append("g"),C=k.insert("g").attr("class","branchLabel"),w=C.insert("g").attr("class","label branch-label");w.node()?.appendChild(T);let S=T.getBBox();v=O3t(x.name,v,b,S,s),w.remove(),C.remove(),k.remove()}),GDe(u,Mw,!1,a),a.showBranches&&P3t(u,l,a,t),N3t(u,Mw),GDe(u,Mw,!0,a),Zt.insertTitle(u,"gitTitleText",a.titleTopMargin??0,i.getDiagramTitle()),db(void 0,u,a.diagramPadding,a.useMaxWidth)},"draw"),qDe={draw:B3t}});var UDe,YDe,$3t,F3t,z3t,G3t,V3t,W3t,q3t,H3t,jDe,XDe=F(()=>{"use strict";ur();UDe=8,YDe=new Set(["redux","redux-dark","redux-color","redux-dark-color"]),$3t=new Set(["redux-color","redux-dark-color"]),F3t=new Set(["neo","neo-dark"]),z3t=new Set(["dark","redux-dark","redux-dark-color","neo-dark"]),G3t=new Set(["redux","redux-dark","redux-color","redux-dark-color","neo","neo-dark"]),V3t=o(e=>{let{svgId:t}=e,r="";if(e.useGradient&&t)for(let n=0;n{let t=_t(),{theme:r,themeVariables:n}=t,{borderColorArray:i}=n,a=YDe.has(r);if(F3t.has(r)){let s="";for(let l=0;l`${Array.from({length:e.THEME_COLOR_LIMIT},(t,r)=>r).map(t=>{let r=t%UDe;return` + .branch-label${t} { fill: ${e["gitBranchLabel"+r]}; } + .commit${t} { stroke: ${e["git"+r]}; fill: ${e["git"+r]}; } + .commit-highlight${t} { stroke: ${e["gitInv"+r]}; fill: ${e["gitInv"+r]}; } + .label${t} { fill: ${e["git"+r]}; } + .arrow${t} { stroke: ${e["git"+r]}; } + `}).join(` +`)}`,"normalTheme"),H3t=o(e=>{let t=_t(),{theme:r}=t,n=G3t.has(r);return` + .commit-id, + .commit-msg, + .branch-label { + fill: lightgrey; + color: lightgrey; + font-family: 'trebuchet ms', verdana, arial, sans-serif; + font-family: var(--mermaid-font-family); + } + + ${n?W3t(e):q3t(e)} + + .branch { + stroke-width: ${e.strokeWidth}; + stroke: ${e.commitLineColor??e.lineColor}; + stroke-dasharray: ${n?"4 2":"2"}; + } + .commit-label { font-size: ${e.commitLabelFontSize}; fill: ${n?e.nodeBorder:e.commitLabelColor}; ${n?`font-weight:${e.noteFontWeight};`:""}} + .commit-label-bkg { font-size: ${e.commitLabelFontSize}; fill: ${n?"transparent":e.commitLabelBackground}; opacity: ${n?"":.5}; } + .tag-label { font-size: ${e.tagLabelFontSize}; fill: ${e.tagLabelColor};} + .tag-label-bkg { fill: ${n?e.mainBkg:e.tagLabelBackground}; stroke: ${n?e.nodeBorder:e.tagLabelBorder}; ${n?`filter:${e.dropShadow}`:""} } + .tag-hole { fill: ${e.textColor}; } + + .commit-merge { + stroke: ${n?e.mainBkg:e.primaryColor}; + fill: ${n?e.mainBkg:e.primaryColor}; + } + .commit-reverse { + stroke: ${n?e.mainBkg:e.primaryColor}; + fill: ${n?e.mainBkg:e.primaryColor}; + stroke-width: ${n?e.strokeWidth:3}; + } + .commit-highlight-outer { + } + .commit-highlight-inner { + stroke: ${n?e.mainBkg:e.primaryColor}; + fill: ${n?e.mainBkg:e.primaryColor}; + } + + .arrow { + /* Intentional: neo themes keep the bold 8px arrow (like classic themes); only redux-geometry themes use the thinner options.strokeWidth. */ + stroke-width: ${YDe.has(r)?e.strokeWidth:8}; + stroke-linecap: round; + fill: none + } + .gitTitleText { + text-anchor: middle; + font-size: 18px; + fill: ${e.textColor}; + } +`},"getStyles"),jDe=H3t});var KDe={};ir(KDe,{diagram:()=>U3t});var U3t,ZDe=F(()=>{"use strict";zDe();vU();HDe();XDe();U3t={parser:FDe,db:eL,renderer:qDe,styles:jDe}});var TU,e7e,t7e=F(()=>{"use strict";TU=(function(){var e=o(function(A,M,D,P){for(D=D||{},P=A.length;P--;D[A[P]]=M);return D},"o"),t=[6,8,10,12,13,14,15,16,17,18,20,21,22,23,24,25,26,27,28,29,30,31,33,35,36,38,40],r=[1,26],n=[1,27],i=[1,28],a=[1,29],s=[1,30],l=[1,31],u=[1,32],h=[1,33],d=[1,34],f=[1,9],p=[1,10],m=[1,11],g=[1,12],y=[1,13],v=[1,14],x=[1,15],b=[1,16],T=[1,19],k=[1,20],C=[1,21],w=[1,22],S=[1,23],R=[1,25],L=[1,35],N={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,gantt:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NL:10,weekday:11,weekday_monday:12,weekday_tuesday:13,weekday_wednesday:14,weekday_thursday:15,weekday_friday:16,weekday_saturday:17,weekday_sunday:18,weekend:19,weekend_friday:20,weekend_saturday:21,dateFormat:22,inclusiveEndDates:23,topAxis:24,axisFormat:25,tickInterval:26,excludes:27,includes:28,todayMarker:29,title:30,acc_title:31,acc_title_value:32,acc_descr:33,acc_descr_value:34,acc_descr_multiline_value:35,section:36,clickStatement:37,taskTxt:38,taskData:39,click:40,callbackname:41,callbackargs:42,href:43,clickStatementDebug:44,$accept:0,$end:1},terminals_:{2:"error",4:"gantt",6:"EOF",8:"SPACE",10:"NL",12:"weekday_monday",13:"weekday_tuesday",14:"weekday_wednesday",15:"weekday_thursday",16:"weekday_friday",17:"weekday_saturday",18:"weekday_sunday",20:"weekend_friday",21:"weekend_saturday",22:"dateFormat",23:"inclusiveEndDates",24:"topAxis",25:"axisFormat",26:"tickInterval",27:"excludes",28:"includes",29:"todayMarker",30:"title",31:"acc_title",32:"acc_title_value",33:"acc_descr",34:"acc_descr_value",35:"acc_descr_multiline_value",36:"section",38:"taskTxt",39:"taskData",40:"click",41:"callbackname",42:"callbackargs",43:"href"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[11,1],[11,1],[11,1],[11,1],[11,1],[11,1],[11,1],[19,1],[19,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,1],[9,2],[37,2],[37,3],[37,3],[37,4],[37,3],[37,4],[37,2],[44,2],[44,3],[44,3],[44,4],[44,3],[44,4],[44,2]],performAction:o(function(M,D,P,B,O,$,V){var G=$.length-1;switch(O){case 1:return $[G-1];case 2:this.$=[];break;case 3:$[G-1].push($[G]),this.$=$[G-1];break;case 4:case 5:this.$=$[G];break;case 6:case 7:this.$=[];break;case 8:B.setWeekday("monday");break;case 9:B.setWeekday("tuesday");break;case 10:B.setWeekday("wednesday");break;case 11:B.setWeekday("thursday");break;case 12:B.setWeekday("friday");break;case 13:B.setWeekday("saturday");break;case 14:B.setWeekday("sunday");break;case 15:B.setWeekend("friday");break;case 16:B.setWeekend("saturday");break;case 17:B.setDateFormat($[G].substr(11)),this.$=$[G].substr(11);break;case 18:B.enableInclusiveEndDates(),this.$=$[G].substr(18);break;case 19:B.TopAxis(),this.$=$[G].substr(8);break;case 20:B.setAxisFormat($[G].substr(11)),this.$=$[G].substr(11);break;case 21:B.setTickInterval($[G].substr(13)),this.$=$[G].substr(13);break;case 22:B.setExcludes($[G].substr(9)),this.$=$[G].substr(9);break;case 23:B.setIncludes($[G].substr(9)),this.$=$[G].substr(9);break;case 24:B.setTodayMarker($[G].substr(12)),this.$=$[G].substr(12);break;case 27:B.setDiagramTitle($[G].substr(6)),this.$=$[G].substr(6);break;case 28:this.$=$[G].trim(),B.setAccTitle(this.$);break;case 29:case 30:this.$=$[G].trim(),B.setAccDescription(this.$);break;case 31:B.addSection($[G].substr(8)),this.$=$[G].substr(8);break;case 33:B.addTask($[G-1],$[G]),this.$="task";break;case 34:this.$=$[G-1],B.setClickEvent($[G-1],$[G],null);break;case 35:this.$=$[G-2],B.setClickEvent($[G-2],$[G-1],$[G]);break;case 36:this.$=$[G-2],B.setClickEvent($[G-2],$[G-1],null),B.setLink($[G-2],$[G]);break;case 37:this.$=$[G-3],B.setClickEvent($[G-3],$[G-2],$[G-1]),B.setLink($[G-3],$[G]);break;case 38:this.$=$[G-2],B.setClickEvent($[G-2],$[G],null),B.setLink($[G-2],$[G-1]);break;case 39:this.$=$[G-3],B.setClickEvent($[G-3],$[G-1],$[G]),B.setLink($[G-3],$[G-2]);break;case 40:this.$=$[G-1],B.setLink($[G-1],$[G]);break;case 41:case 47:this.$=$[G-1]+" "+$[G];break;case 42:case 43:case 45:this.$=$[G-2]+" "+$[G-1]+" "+$[G];break;case 44:case 46:this.$=$[G-3]+" "+$[G-2]+" "+$[G-1]+" "+$[G];break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},e(t,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:17,12:r,13:n,14:i,15:a,16:s,17:l,18:u,19:18,20:h,21:d,22:f,23:p,24:m,25:g,26:y,27:v,28:x,29:b,30:T,31:k,33:C,35:w,36:S,37:24,38:R,40:L},e(t,[2,7],{1:[2,1]}),e(t,[2,3]),{9:36,11:17,12:r,13:n,14:i,15:a,16:s,17:l,18:u,19:18,20:h,21:d,22:f,23:p,24:m,25:g,26:y,27:v,28:x,29:b,30:T,31:k,33:C,35:w,36:S,37:24,38:R,40:L},e(t,[2,5]),e(t,[2,6]),e(t,[2,17]),e(t,[2,18]),e(t,[2,19]),e(t,[2,20]),e(t,[2,21]),e(t,[2,22]),e(t,[2,23]),e(t,[2,24]),e(t,[2,25]),e(t,[2,26]),e(t,[2,27]),{32:[1,37]},{34:[1,38]},e(t,[2,30]),e(t,[2,31]),e(t,[2,32]),{39:[1,39]},e(t,[2,8]),e(t,[2,9]),e(t,[2,10]),e(t,[2,11]),e(t,[2,12]),e(t,[2,13]),e(t,[2,14]),e(t,[2,15]),e(t,[2,16]),{41:[1,40],43:[1,41]},e(t,[2,4]),e(t,[2,28]),e(t,[2,29]),e(t,[2,33]),e(t,[2,34],{42:[1,42],43:[1,43]}),e(t,[2,40],{41:[1,44]}),e(t,[2,35],{43:[1,45]}),e(t,[2,36]),e(t,[2,38],{42:[1,46]}),e(t,[2,37]),e(t,[2,39])],defaultActions:{},parseError:o(function(M,D){if(D.recoverable)this.trace(M);else{var P=new Error(M);throw P.hash=D,P}},"parseError"),parse:o(function(M){var D=this,P=[0],B=[],O=[null],$=[],V=this.table,G="",z=0,W=0,H=0,j=2,Q=1,U=$.slice.call(arguments,1),oe=Object.create(this.lexer),te={yy:{}};for(var le in this.yy)Object.prototype.hasOwnProperty.call(this.yy,le)&&(te.yy[le]=this.yy[le]);oe.setInput(M,te.yy),te.yy.lexer=oe,te.yy.parser=this,typeof oe.yylloc>"u"&&(oe.yylloc={});var ie=oe.yylloc;$.push(ie);var ae=oe.options&&oe.options.ranges;typeof te.yy.parseError=="function"?this.parseError=te.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function Re(we){P.length=P.length-2*we,O.length=O.length-we,$.length=$.length-we}o(Re,"popStack");function be(){var we;return we=B.pop()||oe.lex()||Q,typeof we!="number"&&(we instanceof Array&&(B=we,we=B.pop()),we=D.symbols_[we]||we),we}o(be,"lex");for(var Pe,Ge,Oe,ue,ye,ke,ce={},re,J,se,ge;;){if(Oe=P[P.length-1],this.defaultActions[Oe]?ue=this.defaultActions[Oe]:((Pe===null||typeof Pe>"u")&&(Pe=be()),ue=V[Oe]&&V[Oe][Pe]),typeof ue>"u"||!ue.length||!ue[0]){var Te="";ge=[];for(re in V[Oe])this.terminals_[re]&&re>j&&ge.push("'"+this.terminals_[re]+"'");oe.showPosition?Te="Parse error on line "+(z+1)+`: +`+oe.showPosition()+` +Expecting `+ge.join(", ")+", got '"+(this.terminals_[Pe]||Pe)+"'":Te="Parse error on line "+(z+1)+": Unexpected "+(Pe==Q?"end of input":"'"+(this.terminals_[Pe]||Pe)+"'"),this.parseError(Te,{text:oe.match,token:this.terminals_[Pe]||Pe,line:oe.yylineno,loc:ie,expected:ge})}if(ue[0]instanceof Array&&ue.length>1)throw new Error("Parse Error: multiple actions possible at state: "+Oe+", token: "+Pe);switch(ue[0]){case 1:P.push(Pe),O.push(oe.yytext),$.push(oe.yylloc),P.push(ue[1]),Pe=null,Ge?(Pe=Ge,Ge=null):(W=oe.yyleng,G=oe.yytext,z=oe.yylineno,ie=oe.yylloc,H>0&&H--);break;case 2:if(J=this.productions_[ue[1]][1],ce.$=O[O.length-J],ce._$={first_line:$[$.length-(J||1)].first_line,last_line:$[$.length-1].last_line,first_column:$[$.length-(J||1)].first_column,last_column:$[$.length-1].last_column},ae&&(ce._$.range=[$[$.length-(J||1)].range[0],$[$.length-1].range[1]]),ke=this.performAction.apply(ce,[G,W,z,te.yy,ue[1],O,$].concat(U)),typeof ke<"u")return ke;J&&(P=P.slice(0,-1*J*2),O=O.slice(0,-1*J),$=$.slice(0,-1*J)),P.push(this.productions_[ue[1]][0]),O.push(ce.$),$.push(ce._$),se=V[P[P.length-2]][P[P.length-1]],P.push(se);break;case 3:return!0}}return!0},"parse")},I=(function(){var A={EOF:1,parseError:o(function(D,P){if(this.yy.parser)this.yy.parser.parseError(D,P);else throw new Error(D)},"parseError"),setInput:o(function(M,D){return this.yy=D||this.yy||{},this._input=M,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var M=this._input[0];this.yytext+=M,this.yyleng++,this.offset++,this.match+=M,this.matched+=M;var D=M.match(/(?:\r\n?|\n).*/g);return D?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),M},"input"),unput:o(function(M){var D=M.length,P=M.split(/(?:\r\n?|\n)/g);this._input=M+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-D),this.offset-=D;var B=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),P.length-1&&(this.yylineno-=P.length-1);var O=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:P?(P.length===B.length?this.yylloc.first_column:0)+B[B.length-P.length].length-P[0].length:this.yylloc.first_column-D},this.options.ranges&&(this.yylloc.range=[O[0],O[0]+this.yyleng-D]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(M){this.unput(this.match.slice(M))},"less"),pastInput:o(function(){var M=this.matched.substr(0,this.matched.length-this.match.length);return(M.length>20?"...":"")+M.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var M=this.match;return M.length<20&&(M+=this._input.substr(0,20-M.length)),(M.substr(0,20)+(M.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var M=this.pastInput(),D=new Array(M.length+1).join("-");return M+this.upcomingInput()+` +`+D+"^"},"showPosition"),test_match:o(function(M,D){var P,B,O;if(this.options.backtrack_lexer&&(O={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(O.yylloc.range=this.yylloc.range.slice(0))),B=M[0].match(/(?:\r\n?|\n).*/g),B&&(this.yylineno+=B.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:B?B[B.length-1].length-B[B.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+M[0].length},this.yytext+=M[0],this.match+=M[0],this.matches=M,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(M[0].length),this.matched+=M[0],P=this.performAction.call(this,this.yy,this,D,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),P)return P;if(this._backtrack){for(var $ in O)this[$]=O[$];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var M,D,P,B;this._more||(this.yytext="",this.match="");for(var O=this._currentRules(),$=0;$D[0].length)){if(D=P,B=$,this.options.backtrack_lexer){if(M=this.test_match(P,O[$]),M!==!1)return M;if(this._backtrack){D=!1;continue}else return!1}else if(!this.options.flex)break}return D?(M=this.test_match(D,O[B]),M!==!1?M:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var D=this.next();return D||this.lex()},"lex"),begin:o(function(D){this.conditionStack.push(D)},"begin"),popState:o(function(){var D=this.conditionStack.length-1;return D>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(D){return D=this.conditionStack.length-1-Math.abs(D||0),D>=0?this.conditionStack[D]:"INITIAL"},"topState"),pushState:o(function(D){this.begin(D)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:o(function(D,P,B,O){var $=O;switch(B){case 0:return this.begin("open_directive"),"open_directive";break;case 1:return this.begin("acc_title"),31;break;case 2:return this.popState(),"acc_title_value";break;case 3:return this.begin("acc_descr"),33;break;case 4:return this.popState(),"acc_descr_value";break;case 5:this.begin("acc_descr_multiline");break;case 6:this.popState();break;case 7:return"acc_descr_multiline_value";case 8:break;case 9:break;case 10:break;case 11:return 10;case 12:break;case 13:break;case 14:this.begin("href");break;case 15:this.popState();break;case 16:return 43;case 17:this.begin("callbackname");break;case 18:this.popState();break;case 19:this.popState(),this.begin("callbackargs");break;case 20:return 41;case 21:this.popState();break;case 22:return 42;case 23:this.begin("click");break;case 24:this.popState();break;case 25:return 40;case 26:return 4;case 27:return 22;case 28:return 23;case 29:return 24;case 30:return 25;case 31:return 26;case 32:return 28;case 33:return 27;case 34:return 29;case 35:return 12;case 36:return 13;case 37:return 14;case 38:return 15;case 39:return 16;case 40:return 17;case 41:return 18;case 42:return 20;case 43:return 21;case 44:return"date";case 45:return 30;case 46:return"accDescription";case 47:return 36;case 48:return 38;case 49:return 39;case 50:return":";case 51:return 6;case 52:return"INVALID"}},"anonymous"),rules:[/^(?:%%\{)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:%%(?!\{)*[^\n]*)/i,/^(?:[^\}]%%*[^\n]*)/i,/^(?:%%*[^\n]*[\n]*)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:%[^\n]*)/i,/^(?:href[\s]+["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:call[\s]+)/i,/^(?:\([\s]*\))/i,/^(?:\()/i,/^(?:[^(]*)/i,/^(?:\))/i,/^(?:[^)]*)/i,/^(?:click[\s]+)/i,/^(?:[\s\n])/i,/^(?:[^\s\n]*)/i,/^(?:gantt\b)/i,/^(?:dateFormat\s[^#\n;]+)/i,/^(?:inclusiveEndDates\b)/i,/^(?:topAxis\b)/i,/^(?:axisFormat\s[^#\n;]+)/i,/^(?:tickInterval\s[^#\n;]+)/i,/^(?:includes\s[^#\n;]+)/i,/^(?:excludes\s[^#\n;]+)/i,/^(?:todayMarker\s[^\n;]+)/i,/^(?:weekday\s+monday\b)/i,/^(?:weekday\s+tuesday\b)/i,/^(?:weekday\s+wednesday\b)/i,/^(?:weekday\s+thursday\b)/i,/^(?:weekday\s+friday\b)/i,/^(?:weekday\s+saturday\b)/i,/^(?:weekday\s+sunday\b)/i,/^(?:weekend\s+friday\b)/i,/^(?:weekend\s+saturday\b)/i,/^(?:\d\d\d\d-\d\d-\d\d\b)/i,/^(?:title\s[^\n]+)/i,/^(?:accDescription\s[^#\n;]+)/i,/^(?:section\s[^\n]+)/i,/^(?:[^:\n]+)/i,/^(?::[^#\n;]+)/i,/^(?::)/i,/^(?:$)/i,/^(?:.)/i],conditions:{acc_descr_multiline:{rules:[6,7],inclusive:!1},acc_descr:{rules:[4],inclusive:!1},acc_title:{rules:[2],inclusive:!1},callbackargs:{rules:[21,22],inclusive:!1},callbackname:{rules:[18,19,20],inclusive:!1},href:{rules:[15,16],inclusive:!1},click:{rules:[24,25],inclusive:!1},INITIAL:{rules:[0,1,3,5,8,9,10,11,12,13,14,17,23,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52],inclusive:!0}}};return A})();N.lexer=I;function _(){this.yy={}}return o(_,"Parser"),_.prototype=N,N.Parser=_,new _})();TU.parser=TU;e7e=TU});var r7e=Io((CU,wU)=>{"use strict";(function(e,t){typeof CU=="object"&&typeof wU<"u"?wU.exports=t():typeof define=="function"&&define.amd?define(t):(e=typeof globalThis<"u"?globalThis:e||self).dayjs_plugin_isoWeek=t()})(CU,(function(){"use strict";var e="day";return function(t,r,n){var i=o(function(l){return l.add(4-l.isoWeekday(),e)},"a"),a=r.prototype;a.isoWeekYear=function(){return i(this).year()},a.isoWeek=function(l){if(!this.$utils().u(l))return this.add(7*(l-this.isoWeek()),e);var u,h,d,f,p=i(this),m=(u=this.isoWeekYear(),h=this.$u,d=(h?n.utc:n)().year(u).startOf("year"),f=4-d.isoWeekday(),d.isoWeekday()>4&&(f+=7),d.add(f,e));return p.diff(m,"week")+1},a.isoWeekday=function(l){return this.$utils().u(l)?this.day()||7:this.day(this.day()%7?l:l-7)};var s=a.startOf;a.startOf=function(l,u){var h=this.$utils(),d=!!h.u(u)||u;return h.p(l)==="isoweek"?d?this.date(this.date()-(this.isoWeekday()-1)).startOf("day"):this.date(this.date()-1-(this.isoWeekday()-1)+7).endOf("day"):s.bind(this)(l,u)}}}))});var n7e=Io((kU,SU)=>{"use strict";(function(e,t){typeof kU=="object"&&typeof SU<"u"?SU.exports=t():typeof define=="function"&&define.amd?define(t):(e=typeof globalThis<"u"?globalThis:e||self).dayjs_plugin_customParseFormat=t()})(kU,(function(){"use strict";var e={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},t=/(\[[^[]*\])|([-_:/.,()\s]+)|(A|a|Q|YYYY|YY?|ww?|MM?M?M?|Do|DD?|hh?|HH?|mm?|ss?|S{1,3}|z|ZZ?)/g,r=/\d/,n=/\d\d/,i=/\d\d?/,a=/\d*[^-_:/,()\s\d]+/,s={},l=o(function(g){return(g=+g)+(g>68?1900:2e3)},"a"),u=o(function(g){return function(y){this[g]=+y}},"f"),h=[/[+-]\d\d:?(\d\d)?|Z/,function(g){(this.zone||(this.zone={})).offset=(function(y){if(!y||y==="Z")return 0;var v=y.match(/([+-]|\d\d)/g),x=60*v[1]+(+v[2]||0);return x===0?0:v[0]==="+"?-x:x})(g)}],d=o(function(g){var y=s[g];return y&&(y.indexOf?y:y.s.concat(y.f))},"u"),f=o(function(g,y){var v,x=s.meridiem;if(x){for(var b=1;b<=24;b+=1)if(g.indexOf(x(b,0,y))>-1){v=b>12;break}}else v=g===(y?"pm":"PM");return v},"d"),p={A:[a,function(g){this.afternoon=f(g,!1)}],a:[a,function(g){this.afternoon=f(g,!0)}],Q:[r,function(g){this.month=3*(g-1)+1}],S:[r,function(g){this.milliseconds=100*+g}],SS:[n,function(g){this.milliseconds=10*+g}],SSS:[/\d{3}/,function(g){this.milliseconds=+g}],s:[i,u("seconds")],ss:[i,u("seconds")],m:[i,u("minutes")],mm:[i,u("minutes")],H:[i,u("hours")],h:[i,u("hours")],HH:[i,u("hours")],hh:[i,u("hours")],D:[i,u("day")],DD:[n,u("day")],Do:[a,function(g){var y=s.ordinal,v=g.match(/\d+/);if(this.day=v[0],y)for(var x=1;x<=31;x+=1)y(x).replace(/\[|\]/g,"")===g&&(this.day=x)}],w:[i,u("week")],ww:[n,u("week")],M:[i,u("month")],MM:[n,u("month")],MMM:[a,function(g){var y=d("months"),v=(d("monthsShort")||y.map((function(x){return x.slice(0,3)}))).indexOf(g)+1;if(v<1)throw new Error;this.month=v%12||v}],MMMM:[a,function(g){var y=d("months").indexOf(g)+1;if(y<1)throw new Error;this.month=y%12||y}],Y:[/[+-]?\d+/,u("year")],YY:[n,function(g){this.year=l(g)}],YYYY:[/\d{4}/,u("year")],Z:h,ZZ:h};function m(g){var y,v;y=g,v=s&&s.formats;for(var x=(g=y.replace(/(\[[^\]]+])|(LTS?|l{1,4}|L{1,4})/g,(function(R,L,N){var I=N&&N.toUpperCase();return L||v[N]||e[N]||v[I].replace(/(\[[^\]]+])|(MMMM|MM|DD|dddd)/g,(function(_,A,M){return A||M.slice(1)}))}))).match(t),b=x.length,T=0;T-1)return new Date((P==="X"?1e3:1)*D);var $=m(P)(D),V=$.year,G=$.month,z=$.day,W=$.hours,H=$.minutes,j=$.seconds,Q=$.milliseconds,U=$.zone,oe=$.week,te=new Date,le=z||(V||G?1:te.getDate()),ie=V||te.getFullYear(),ae=0;V&&!G||(ae=G>0?G-1:te.getMonth());var Re,be=W||0,Pe=H||0,Ge=j||0,Oe=Q||0;return U?new Date(Date.UTC(ie,ae,le,be,Pe,Ge,Oe+60*U.offset*1e3)):B?new Date(Date.UTC(ie,ae,le,be,Pe,Ge,Oe)):(Re=new Date(ie,ae,le,be,Pe,Ge,Oe),oe&&(Re=O(Re).week(oe).toDate()),Re)}catch{return new Date("")}})(k,S,C,v),this.init(),I&&I!==!0&&(this.$L=this.locale(I).$L),N&&k!=this.format(S)&&(this.$d=new Date("")),s={}}else if(S instanceof Array)for(var _=S.length,A=1;A<=_;A+=1){w[1]=S[A-1];var M=v.apply(this,w);if(M.isValid()){this.$d=M.$d,this.$L=M.$L,this.init();break}A===_&&(this.$d=new Date(""))}else b.call(this,T)}}}))});var i7e=Io((EU,AU)=>{"use strict";(function(e,t){typeof EU=="object"&&typeof AU<"u"?AU.exports=t():typeof define=="function"&&define.amd?define(t):(e=typeof globalThis<"u"?globalThis:e||self).dayjs_plugin_advancedFormat=t()})(EU,(function(){"use strict";return function(e,t){var r=t.prototype,n=r.format;r.format=function(i){var a=this,s=this.$locale();if(!this.isValid())return n.bind(this)(i);var l=this.$utils(),u=(i||"YYYY-MM-DDTHH:mm:ssZ").replace(/\[([^\]]+)]|Q|wo|ww|w|WW|W|zzz|z|gggg|GGGG|Do|X|x|k{1,2}|S/g,(function(h){switch(h){case"Q":return Math.ceil((a.$M+1)/3);case"Do":return s.ordinal(a.$D);case"gggg":return a.weekYear();case"GGGG":return a.isoWeekYear();case"wo":return s.ordinal(a.week(),"W");case"w":case"ww":return l.s(a.week(),h==="w"?1:2,"0");case"W":case"WW":return l.s(a.isoWeek(),h==="W"?1:2,"0");case"k":case"kk":return l.s(String(a.$H===0?24:a.$H),h==="k"?1:2,"0");case"X":return Math.floor(a.$d.getTime()/1e3);case"x":return a.$d.getTime();case"z":return"["+a.offsetName()+"]";case"zzz":return"["+a.offsetName("long")+"]";default:return h}}));return n.bind(this)(u)}}}))});function T7e(e,t,r){let n=!0;for(;n;)n=!1,r.forEach(function(i){let a="^\\s*"+i+"\\s*$",s=new RegExp(a);e[0].match(s)&&(t[i]=!0,e.shift(1),n=!0)})}var o7e,el,l7e,c7e,u7e,a7e,qu,DU,IU,MU,jv,Xv,NU,PU,sL,Kv,OU,h7e,BU,Uv,Pw,$U,FU,oL,RU,K3t,Z3t,Q3t,J3t,e5t,t5t,r5t,n5t,i5t,a5t,s5t,o5t,l5t,c5t,u5t,h5t,d7e,d5t,f5t,p5t,m5t,g5t,y5t,v5t,x5t,f7e,b5t,T5t,C5t,p7e,w5t,_U,m7e,g7e,iL,Yv,k5t,S5t,LU,aL,fa,y7e,E5t,w0,A5t,s7e,R5t,v7e,_5t,x7e,L5t,D5t,b7e,C7e=F(()=>{"use strict";o7e=Xs(Ly(),1),el=Xs(xk(),1),l7e=Xs(r7e(),1),c7e=Xs(n7e(),1),u7e=Xs(i7e(),1);vt();Xt();Qt();Nn();el.default.extend(l7e.default);el.default.extend(c7e.default);el.default.extend(u7e.default);a7e={friday:5,saturday:6},qu="",DU="",MU="",jv=[],Xv=[],NU=new Map,PU=[],sL=[],Kv="",OU="",h7e=["active","done","crit","milestone","vert"],BU=[],Uv="",Pw=!1,$U=!1,FU="sunday",oL="saturday",RU=0,K3t=o(function(){PU=[],sL=[],Kv="",BU=[],iL=0,LU=void 0,aL=void 0,fa=[],qu="",DU="",OU="",IU=void 0,MU="",jv=[],Xv=[],Pw=!1,$U=!1,RU=0,NU=new Map,Uv="",yr(),FU="sunday",oL="saturday"},"clear"),Z3t=o(function(e){Uv=e},"setDiagramId"),Q3t=o(function(e){DU=e},"setAxisFormat"),J3t=o(function(){return DU},"getAxisFormat"),e5t=o(function(e){IU=e},"setTickInterval"),t5t=o(function(){return IU},"getTickInterval"),r5t=o(function(e){MU=e},"setTodayMarker"),n5t=o(function(){return MU},"getTodayMarker"),i5t=o(function(e){qu=e},"setDateFormat"),a5t=o(function(){Pw=!0},"enableInclusiveEndDates"),s5t=o(function(){return Pw},"endDatesAreInclusive"),o5t=o(function(){$U=!0},"enableTopAxis"),l5t=o(function(){return $U},"topAxisEnabled"),c5t=o(function(e){OU=e},"setDisplayMode"),u5t=o(function(){return OU},"getDisplayMode"),h5t=o(function(){return qu},"getDateFormat"),d7e=o((e,t)=>{let r=t.toLowerCase().split(/[\s,]+/).filter(n=>n!=="");return[...new Set([...e,...r])]},"mergeTokens"),d5t=o(function(e){jv=d7e(jv,e)},"setIncludes"),f5t=o(function(){return jv},"getIncludes"),p5t=o(function(e){Xv=d7e(Xv,e)},"setExcludes"),m5t=o(function(){return Xv},"getExcludes"),g5t=o(function(){return NU},"getLinks"),y5t=o(function(e){Kv=e,PU.push(e)},"addSection"),v5t=o(function(){return PU},"getSections"),x5t=o(function(){let e=s7e(),t=10,r=0;for(;!e&&rl))throw new Error("Failed to find a valid date that was not excluded by `excludes` after 10,000 iterations.");e=e.add(1,"d")}return[t,s]},"fixTaskDates"),_U=o(function(e,t,r){if(r=r.trim(),o(l=>{let u=l.trim();return u==="x"||u==="X"},"isTimestampFormat")(t)&&/^\d+$/.test(r))return new Date(Number(r));let a=/^after\s+(?[\d\w- ]+)/.exec(r);if(a!==null){let l=null;for(let h of a.groups.ids.split(" ")){let d=w0(h);d!==void 0&&(!l||d.endTime>l.endTime)&&(l=d)}if(l)return l.endTime;let u=new Date;return u.setHours(0,0,0,0),u}let s=(0,el.default)(r,t.trim(),!0);if(s.isValid())return s.toDate();{Z.debug("Invalid date:"+r),Z.debug("With date format:"+t.trim());let l=new Date(r);if(l===void 0||isNaN(l.getTime())||l.getFullYear()<-1e4||l.getFullYear()>1e4)throw new Error("Invalid date:"+r);return l}},"getStartDate"),m7e=o(function(e){let t=/^(\d+(?:\.\d+)?)([Mdhmswy]|ms)$/.exec(e.trim());return t!==null?[Number.parseFloat(t[1]),t[2]]:[NaN,"ms"]},"parseDuration"),g7e=o(function(e,t,r,n=!1){r=r.trim();let a=/^until\s+(?[\d\w- ]+)/.exec(r);if(a!==null){let d=null;for(let p of a.groups.ids.split(" ")){let m=w0(p);m!==void 0&&(!d||m.startTime{window.open(r,"_self")}),NU.set(n,r))}),v7e(e,"clickable")},"setLink"),v7e=o(function(e,t){e.split(",").forEach(function(r){let n=w0(r);n!==void 0&&n.classes.push(t)})},"setClass"),_5t=o(function(e,t,r){if(Ae().securityLevel!=="loose"||t===void 0)return;let n=[];if(typeof r=="string"){n=r.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);for(let a=0;a{Zt.runFunc(t,...n)})},"setClickFun"),x7e=o(function(e,t){BU.push(function(){let r=Uv?`${Uv}-${e}`:e,n=document.querySelector(`[id="${r}"]`);n!==null&&n.addEventListener("click",function(){t()})},function(){let r=Uv?`${Uv}-${e}`:e,n=document.querySelector(`[id="${r}-text"]`);n!==null&&n.addEventListener("click",function(){t()})})},"pushFun"),L5t=o(function(e,t,r){e.split(",").forEach(function(n){_5t(n,t,r)}),v7e(e,"clickable")},"setClickEvent"),D5t=o(function(e){BU.forEach(function(t){t(e)})},"bindFunctions"),b7e={getConfig:o(()=>Ae().gantt,"getConfig"),clear:K3t,setDateFormat:i5t,getDateFormat:h5t,enableInclusiveEndDates:a5t,endDatesAreInclusive:s5t,enableTopAxis:o5t,topAxisEnabled:l5t,setAxisFormat:Q3t,getAxisFormat:J3t,setTickInterval:e5t,getTickInterval:t5t,setTodayMarker:r5t,getTodayMarker:n5t,setAccTitle:kr,getAccTitle:Ar,setDiagramTitle:Or,getDiagramTitle:Lr,setDiagramId:Z3t,setDisplayMode:c5t,getDisplayMode:u5t,setAccDescription:Rr,getAccDescription:_r,addSection:y5t,getSections:v5t,getTasks:x5t,addTask:E5t,findTaskById:w0,addTaskOrg:A5t,setIncludes:d5t,getIncludes:f5t,setExcludes:p5t,getExcludes:m5t,setClickEvent:L5t,setLink:R5t,getLinks:g5t,bindFunctions:D5t,parseDuration:m7e,isInvalidDate:f7e,setWeekday:b5t,getWeekday:T5t,setWeekend:C5t};o(T7e,"getTaskTags")});var w7e=Io((zU,GU)=>{"use strict";(function(e,t){typeof zU=="object"&&typeof GU<"u"?GU.exports=t():typeof define=="function"&&define.amd?define(t):(e=typeof globalThis<"u"?globalThis:e||self).dayjs_plugin_duration=t()})(zU,(function(){"use strict";var e,t,r=1e3,n=6e4,i=36e5,a=864e5,s=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,l=31536e6,u=2628e6,h=/^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/,d={years:l,months:u,days:a,hours:i,minutes:n,seconds:r,milliseconds:1,weeks:6048e5},f=o(function(k){return k instanceof b},"c"),p=o(function(k,C,w){return new b(k,w,C.$l)},"f"),m=o(function(k){return t.p(k)+"s"},"m"),g=o(function(k){return k<0},"l"),y=o(function(k){return g(k)?Math.ceil(k):Math.floor(k)},"$"),v=o(function(k){return Math.abs(k)},"y"),x=o(function(k,C){return k?g(k)?{negative:!0,format:""+v(k)+C}:{negative:!1,format:""+k+C}:{negative:!1,format:""}},"v"),b=(function(){function k(w,S,R){var L=this;if(this.$d={},this.$l=R,w===void 0&&(this.$ms=0,this.parseFromMilliseconds()),S)return p(w*d[m(S)],this);if(typeof w=="number")return this.$ms=w,this.parseFromMilliseconds(),this;if(typeof w=="object")return Object.keys(w).forEach((function(_){L.$d[m(_)]=w[_]})),this.calMilliseconds(),this;if(typeof w=="string"){var N=w.match(h);if(N){var I=N.slice(2).map((function(_){return _!=null?Number(_):0}));return this.$d.years=I[0],this.$d.months=I[1],this.$d.weeks=I[2],this.$d.days=I[3],this.$d.hours=I[4],this.$d.minutes=I[5],this.$d.seconds=I[6],this.calMilliseconds(),this}}return this}o(k,"l");var C=k.prototype;return C.calMilliseconds=function(){var w=this;this.$ms=Object.keys(this.$d).reduce((function(S,R){return S+(w.$d[R]||0)*d[R]}),0)},C.parseFromMilliseconds=function(){var w=this.$ms;this.$d.years=y(w/l),w%=l,this.$d.months=y(w/u),w%=u,this.$d.days=y(w/a),w%=a,this.$d.hours=y(w/i),w%=i,this.$d.minutes=y(w/n),w%=n,this.$d.seconds=y(w/r),w%=r,this.$d.milliseconds=w},C.toISOString=function(){var w=x(this.$d.years,"Y"),S=x(this.$d.months,"M"),R=+this.$d.days||0;this.$d.weeks&&(R+=7*this.$d.weeks);var L=x(R,"D"),N=x(this.$d.hours,"H"),I=x(this.$d.minutes,"M"),_=this.$d.seconds||0;this.$d.milliseconds&&(_+=this.$d.milliseconds/1e3,_=Math.round(1e3*_)/1e3);var A=x(_,"S"),M=w.negative||S.negative||L.negative||N.negative||I.negative||A.negative,D=N.format||I.format||A.format?"T":"",P=(M?"-":"")+"P"+w.format+S.format+L.format+D+N.format+I.format+A.format;return P==="P"||P==="-P"?"P0D":P},C.toJSON=function(){return this.toISOString()},C.format=function(w){var S=w||"YYYY-MM-DDTHH:mm:ss",R={Y:this.$d.years,YY:t.s(this.$d.years,2,"0"),YYYY:t.s(this.$d.years,4,"0"),M:this.$d.months,MM:t.s(this.$d.months,2,"0"),D:this.$d.days,DD:t.s(this.$d.days,2,"0"),H:this.$d.hours,HH:t.s(this.$d.hours,2,"0"),m:this.$d.minutes,mm:t.s(this.$d.minutes,2,"0"),s:this.$d.seconds,ss:t.s(this.$d.seconds,2,"0"),SSS:t.s(this.$d.milliseconds,3,"0")};return S.replace(s,(function(L,N){return N||String(R[L])}))},C.as=function(w){return this.$ms/d[m(w)]},C.get=function(w){var S=this.$ms,R=m(w);return R==="milliseconds"?S%=1e3:S=R==="weeks"?y(S/d[R]):this.$d[R],S||0},C.add=function(w,S,R){var L;return L=S?w*d[m(S)]:f(w)?w.$ms:p(w,this).$ms,p(this.$ms+L*(R?-1:1),this)},C.subtract=function(w,S){return this.add(w,S,!0)},C.locale=function(w){var S=this.clone();return S.$l=w,S},C.clone=function(){return p(this.$ms,this)},C.humanize=function(w){return e().add(this.$ms,"ms").locale(this.$l).fromNow(!w)},C.valueOf=function(){return this.asMilliseconds()},C.milliseconds=function(){return this.get("milliseconds")},C.asMilliseconds=function(){return this.as("milliseconds")},C.seconds=function(){return this.get("seconds")},C.asSeconds=function(){return this.as("seconds")},C.minutes=function(){return this.get("minutes")},C.asMinutes=function(){return this.as("minutes")},C.hours=function(){return this.get("hours")},C.asHours=function(){return this.as("hours")},C.days=function(){return this.get("days")},C.asDays=function(){return this.as("days")},C.weeks=function(){return this.get("weeks")},C.asWeeks=function(){return this.as("weeks")},C.months=function(){return this.get("months")},C.asMonths=function(){return this.as("months")},C.years=function(){return this.get("years")},C.asYears=function(){return this.as("years")},k})(),T=o(function(k,C,w){return k.add(C.years()*w,"y").add(C.months()*w,"M").add(C.days()*w,"d").add(C.hours()*w,"h").add(C.minutes()*w,"m").add(C.seconds()*w,"s").add(C.milliseconds()*w,"ms")},"p");return function(k,C,w){e=w,t=w().$utils(),w.duration=function(L,N){var I=w.locale();return p(L,{$l:I},N)},w.isDuration=f;var S=C.prototype.add,R=C.prototype.subtract;C.prototype.add=function(L,N){return f(L)?T(this,L,1):S.bind(this)(L,N)},C.prototype.subtract=function(L,N){return f(L)?T(this,L,-1):R.bind(this)(L,N)}}}))});var Zv,S7e,I5t,k7e,M5t,cd,VU,N5t,E7e,A7e=F(()=>{"use strict";Zv=Xs(xk(),1),S7e=Xs(w7e(),1);vt();$r();Vr();Xt();$n();Zv.default.extend(S7e.default);I5t=o(function(){Z.debug("Something is calling, setConf, remove the call")},"setConf"),k7e={monday:Vd,tuesday:mE,wednesday:gE,thursday:Qc,friday:yE,saturday:vE,sunday:Zl},M5t=o((e,t)=>{let r=[...e].map(()=>-1/0),n=[...e].sort((a,s)=>a.startTime-s.startTime||a.order-s.order),i=0;for(let a of n)for(let s=0;s=r[s]){r[s]=a.endTime,a.order=s+t,s>i&&(i=s);break}return i},"getMaxIntersections"),VU=1e4,N5t=o(function(e,t,r,n){let i=Ae().gantt;n.db.setDiagramId(t);let a=Ae().securityLevel,s;a==="sandbox"&&(s=et("#i"+t));let l=a==="sandbox"?et(s.nodes()[0].contentDocument.body):et("body"),u=a==="sandbox"?s.nodes()[0].contentDocument:document,h=u.getElementById(t);cd=h.parentElement.offsetWidth,cd===void 0&&(cd=1200),i.useWidth!==void 0&&(cd=i.useWidth);let d=n.db.getTasks(),f=d.filter(N=>!N.vert),p=[];for(let N of f)p.push(N.type);p=L(p);let m={},g=2*i.topPadding;if(n.db.getDisplayMode()==="compact"||i.displayMode==="compact"){let N={};for(let _ of f)N[_.section]===void 0?N[_.section]=[_]:N[_.section].push(_);let I=0;for(let _ of Object.keys(N)){let A=M5t(N[_],I)+1;I+=A,g+=A*(i.barHeight+i.barGap),m[_]=A}}else{g+=f.length*(i.barHeight+i.barGap);for(let N of p)m[N]=f.filter(I=>I.type===N).length}h.setAttribute("viewBox","0 0 "+cd+" "+g);let y=l.select(`[id="${t}"]`),v=TE().domain([ES(d,function(N){return N.startTime}),SS(d,function(N){return N.endTime})]).rangeRound([0,cd-i.leftPadding-i.rightPadding]);function x(N,I){let _=N.startTime,A=I.startTime,M=0;return _>A?M=1:_W.vert===H.vert?0:W.vert?1:-1);let B=N.filter(W=>!W.vert),$=[...new Set(B.map(W=>W.order))].map(W=>B.find(H=>H.order===W));y.append("g").selectAll("rect").data($).enter().append("rect").attr("x",0).attr("y",function(W,H){return H=W.order,H*I+_-2}).attr("width",function(){return P-i.rightPadding/2}).attr("height",I).attr("class",function(W){for(let[H,j]of p.entries())if(W.type===j)return"section section"+H%i.numberSectionStyles;return"section section0"}).enter();let V=y.append("g").selectAll("rect").data(N).enter(),G=n.db.getLinks();if(V.append("rect").attr("id",function(W){return t+"-"+W.id}).attr("rx",3).attr("ry",3).attr("x",function(W){return W.milestone?v(W.startTime)+A+.5*(v(W.endTime)-v(W.startTime))-.5*M:v(W.startTime)+A}).attr("y",function(W,H){return H=W.order,W.vert?i.gridLineStartPadding:H*I+_}).attr("width",function(W){return W.milestone?M:W.vert?.08*M:v(W.renderEndTime||W.endTime)-v(W.startTime)}).attr("height",function(W){return W.vert?B.length*(i.barHeight+i.barGap)+i.barHeight*2:M}).attr("transform-origin",function(W,H){return H=W.order,(v(W.startTime)+A+.5*(v(W.endTime)-v(W.startTime))).toString()+"px "+(H*I+_+.5*M).toString()+"px"}).attr("class",function(W){let H="task",j="";W.classes.length>0&&(j=W.classes.join(" "));let Q=0;for(let[oe,te]of p.entries())W.type===te&&(Q=oe%i.numberSectionStyles);let U="";return W.active?W.crit?U+=" activeCrit":U=" active":W.done?W.crit?U=" doneCrit":U=" done":W.crit&&(U+=" crit"),U.length===0&&(U=" task"),W.milestone&&(U=" milestone "+U),W.vert&&(U=" vert "+U),U+=Q,U+=" "+j,H+U}),V.append("text").attr("id",function(W){return t+"-"+W.id+"-text"}).text(function(W){return W.task}).attr("font-size",i.fontSize).attr("x",function(W){let H=v(W.startTime),j=v(W.renderEndTime||W.endTime);if(W.milestone&&(H+=.5*(v(W.endTime)-v(W.startTime))-.5*M,j=H+M),W.vert)return v(W.startTime)+A;let Q=this.getBBox().width;return Q>j-H?j+Q+1.5*i.leftPadding>P?H+A-5:j+A+5:(j-H)/2+H+A}).attr("y",function(W,H){return W.vert?i.gridLineStartPadding+B.length*(i.barHeight+i.barGap)+60:(H=W.order,H*I+i.barHeight/2+(i.fontSize/2-2)+_)}).attr("text-height",M).attr("class",function(W){let H=v(W.startTime),j=v(W.endTime);W.milestone&&(j=H+M);let Q=this.getBBox().width,U="";W.classes.length>0&&(U=W.classes.join(" "));let oe=0;for(let[le,ie]of p.entries())W.type===ie&&(oe=le%i.numberSectionStyles);let te="";return W.active&&(W.crit?te="activeCritText"+oe:te="activeText"+oe),W.done?W.crit?te=te+" doneCritText"+oe:te=te+" doneText"+oe:W.crit&&(te=te+" critText"+oe),W.milestone&&(te+=" milestoneText"),W.vert&&(te+=" vertText"),Q>j-H?j+Q+1.5*i.leftPadding>P?U+" taskTextOutsideLeft taskTextOutside"+oe+" "+te:U+" taskTextOutsideRight taskTextOutside"+oe+" "+te+" width-"+Q:U+" taskText taskText"+oe+" "+te+" width-"+Q}),Ae().securityLevel==="sandbox"){let W;W=et("#i"+t);let H=W.nodes()[0].contentDocument;V.filter(function(j){return G.has(j.id)}).each(function(j){var Q=H.querySelector("#"+CSS.escape(t+"-"+j.id)),U=H.querySelector("#"+CSS.escape(t+"-"+j.id+"-text"));let oe=Q.parentNode;var te=H.createElement("a");te.setAttribute("xlink:href",G.get(j.id)),te.setAttribute("target","_top"),oe.appendChild(te),te.appendChild(Q),te.appendChild(U)})}}o(T,"drawRects");function k(N,I,_,A,M,D,P,B){if(P.length===0&&B.length===0)return;let O,$;for(let{startTime:j,endTime:Q}of D)(O===void 0||j$)&&($=Q);if(!O||!$)return;if((0,Zv.default)($).diff((0,Zv.default)(O),"year")>5){Z.warn("The difference between the min and max time is more than 5 years. This will cause performance issues. Skipping drawing exclude days.");return}let V=n.db.getDateFormat(),G=[],z=null,W=(0,Zv.default)(O);for(;W.valueOf()<=$;)n.db.isInvalidDate(W,V,P,B)?z?z.end=W:z={start:W,end:W}:z&&(G.push(z),z=null),W=W.add(1,"d");y.append("g").selectAll("rect").data(G).enter().append("rect").attr("id",j=>t+"-exclude-"+j.start.format("YYYY-MM-DD")).attr("x",j=>v(j.start.startOf("day"))+_).attr("y",i.gridLineStartPadding).attr("width",j=>v(j.end.endOf("day"))-v(j.start.startOf("day"))).attr("height",M-I-i.gridLineStartPadding).attr("transform-origin",function(j,Q){return(v(j.start)+_+.5*(v(j.end)-v(j.start))).toString()+"px "+(Q*N+.5*M).toString()+"px"}).attr("class","exclude-range")}o(k,"drawExcludeDays");function C(N,I,_,A){if(_<=0||N>I)return 1/0;let M=I-N,D=Zv.default.duration({[A??"day"]:_}).asMilliseconds();return D<=0?1/0:Math.ceil(M/D)}o(C,"getEstimatedTickCount");function w(N,I,_,A){let M=n.db.getDateFormat(),D=n.db.getAxisFormat(),P;D?P=D:M==="D"?P="%d":P=i.axisFormat??"%Y-%m-%d";let B=O8(v).tickSize(-A+I+i.gridLineStartPadding).tickFormat(hm(P)),$=/^([1-9]\d*)(millisecond|second|minute|hour|day|week|month)$/.exec(n.db.getTickInterval()||i.tickInterval);if($!==null){let V=parseInt($[1],10);if(isNaN(V)||V<=0)Z.warn(`Invalid tick interval value: "${$[1]}". Skipping custom tick interval.`);else{let G=$[2],z=n.db.getWeekday()||i.weekday,W=v.domain(),H=W[0],j=W[1],Q=C(H,j,V,G);if(Q>VU)Z.warn(`The tick interval "${V}${G}" would generate ${Q} ticks, which exceeds the maximum allowed (${VU}). This may indicate an invalid date or time range. Skipping custom tick interval.`);else switch(G){case"millisecond":B.ticks(Kc.every(V));break;case"second":B.ticks(Bo.every(V));break;case"minute":B.ticks(mh.every(V));break;case"hour":B.ticks(gh.every(V));break;case"day":B.ticks(hl.every(V));break;case"week":B.ticks(k7e[z].every(V));break;case"month":B.ticks(yh.every(V));break}}}if(y.append("g").attr("class","grid").attr("transform","translate("+N+", "+(A-50)+")").call(B).selectAll("text").style("text-anchor","middle").attr("fill","#000").attr("stroke","none").attr("font-size",10).attr("dy","1em"),n.db.topAxisEnabled()||i.topAxis){let V=P8(v).tickSize(-A+I+i.gridLineStartPadding).tickFormat(hm(P));if($!==null){let G=parseInt($[1],10);if(isNaN(G)||G<=0)Z.warn(`Invalid tick interval value: "${$[1]}". Skipping custom tick interval.`);else{let z=$[2],W=n.db.getWeekday()||i.weekday,H=v.domain(),j=H[0],Q=H[1];if(C(j,Q,G,z)<=VU)switch(z){case"millisecond":V.ticks(Kc.every(G));break;case"second":V.ticks(Bo.every(G));break;case"minute":V.ticks(mh.every(G));break;case"hour":V.ticks(gh.every(G));break;case"day":V.ticks(hl.every(G));break;case"week":V.ticks(k7e[W].every(G));break;case"month":V.ticks(yh.every(G));break}}}y.append("g").attr("class","grid").attr("transform","translate("+N+", "+I+")").call(V).selectAll("text").style("text-anchor","middle").attr("fill","#000").attr("stroke","none").attr("font-size",10)}}o(w,"makeGrid");function S(N,I){let _=0,A=Object.keys(m).map(M=>[M,m[M]]);y.append("g").selectAll("text").data(A).enter().append(function(M){let D=M[0].split(xt.lineBreakRegex),P=-(D.length-1)/2,B=u.createElementNS("http://www.w3.org/2000/svg","text");B.setAttribute("dy",P+"em");for(let[O,$]of D.entries()){let V=u.createElementNS("http://www.w3.org/2000/svg","tspan");V.setAttribute("alignment-baseline","central"),V.setAttribute("x","10"),O>0&&V.setAttribute("dy","1em"),V.textContent=$,B.appendChild(V)}return B}).attr("x",10).attr("y",function(M,D){if(D>0)for(let P=0;P{"use strict";P5t=o(e=>` + .mermaid-main-font { + font-family: ${e.fontFamily}; + } + + .exclude-range { + fill: ${e.excludeBkgColor}; + } + + .section { + stroke: none; + opacity: 0.2; + } + + .section0 { + fill: ${e.sectionBkgColor}; + } + + .section2 { + fill: ${e.sectionBkgColor2}; + } + + .section1, + .section3 { + fill: ${e.altSectionBkgColor}; + opacity: 0.2; + } + + .sectionTitle0 { + fill: ${e.titleColor}; + } + + .sectionTitle1 { + fill: ${e.titleColor}; + } + + .sectionTitle2 { + fill: ${e.titleColor}; + } + + .sectionTitle3 { + fill: ${e.titleColor}; + } + + .sectionTitle { + text-anchor: start; + font-family: ${e.fontFamily}; + } + + + /* Grid and axis */ + + .grid .tick { + stroke: ${e.gridColor}; + opacity: 0.8; + shape-rendering: crispEdges; + } + + .grid .tick text { + font-family: ${e.fontFamily}; + fill: ${e.textColor}; + } + + .grid path { + stroke-width: 0; + } + + + /* Today line */ + + .today { + fill: none; + stroke: ${e.todayLineColor}; + stroke-width: 2px; + } + + + /* Task styling */ + + /* Default task */ + + .task { + stroke-width: 2; + } + + .taskText { + text-anchor: middle; + font-family: ${e.fontFamily}; + } + + .taskTextOutsideRight { + fill: ${e.taskTextDarkColor}; + text-anchor: start; + font-family: ${e.fontFamily}; + } + + .taskTextOutsideLeft { + fill: ${e.taskTextDarkColor}; + text-anchor: end; + } + + + /* Special case clickable */ + + .task.clickable { + cursor: pointer; + } + + .taskText.clickable { + cursor: pointer; + fill: ${e.taskTextClickableColor} !important; + font-weight: bold; + } + + .taskTextOutsideLeft.clickable { + cursor: pointer; + fill: ${e.taskTextClickableColor} !important; + font-weight: bold; + } + + .taskTextOutsideRight.clickable { + cursor: pointer; + fill: ${e.taskTextClickableColor} !important; + font-weight: bold; + } + + + /* Specific task settings for the sections*/ + + .taskText0, + .taskText1, + .taskText2, + .taskText3 { + fill: ${e.taskTextColor}; + } + + .task0, + .task1, + .task2, + .task3 { + fill: ${e.taskBkgColor}; + stroke: ${e.taskBorderColor}; + } + + .taskTextOutside0, + .taskTextOutside2 + { + fill: ${e.taskTextOutsideColor}; + } + + .taskTextOutside1, + .taskTextOutside3 { + fill: ${e.taskTextOutsideColor}; + } + + + /* Active task */ + + .active0, + .active1, + .active2, + .active3 { + fill: ${e.activeTaskBkgColor}; + stroke: ${e.activeTaskBorderColor}; + } + + .activeText0, + .activeText1, + .activeText2, + .activeText3 { + fill: ${e.taskTextDarkColor} !important; + } + + + /* Completed task */ + + .done0, + .done1, + .done2, + .done3 { + stroke: ${e.doneTaskBorderColor}; + fill: ${e.doneTaskBkgColor}; + stroke-width: 2; + } + + .doneText0, + .doneText1, + .doneText2, + .doneText3 { + fill: ${e.taskTextDarkColor} !important; + } + + /* Done task text displayed outside the bar sits against the diagram background, + not against the done-task bar, so it must use the outside/contrast color. */ + .doneText0.taskTextOutsideLeft, + .doneText0.taskTextOutsideRight, + .doneText1.taskTextOutsideLeft, + .doneText1.taskTextOutsideRight, + .doneText2.taskTextOutsideLeft, + .doneText2.taskTextOutsideRight, + .doneText3.taskTextOutsideLeft, + .doneText3.taskTextOutsideRight { + fill: ${e.taskTextOutsideColor} !important; + } + + + /* Tasks on the critical line */ + + .crit0, + .crit1, + .crit2, + .crit3 { + stroke: ${e.critBorderColor}; + fill: ${e.critBkgColor}; + stroke-width: 2; + } + + .activeCrit0, + .activeCrit1, + .activeCrit2, + .activeCrit3 { + stroke: ${e.critBorderColor}; + fill: ${e.activeTaskBkgColor}; + stroke-width: 2; + } + + .doneCrit0, + .doneCrit1, + .doneCrit2, + .doneCrit3 { + stroke: ${e.critBorderColor}; + fill: ${e.doneTaskBkgColor}; + stroke-width: 2; + cursor: pointer; + shape-rendering: crispEdges; + } + + .milestone { + transform: rotate(45deg) scale(0.8,0.8); + } + + .milestoneText { + font-style: italic; + } + .doneCritText0, + .doneCritText1, + .doneCritText2, + .doneCritText3 { + fill: ${e.taskTextDarkColor} !important; + } + + /* Done-crit task text outside the bar \u2014 same reasoning as doneText above. */ + .doneCritText0.taskTextOutsideLeft, + .doneCritText0.taskTextOutsideRight, + .doneCritText1.taskTextOutsideLeft, + .doneCritText1.taskTextOutsideRight, + .doneCritText2.taskTextOutsideLeft, + .doneCritText2.taskTextOutsideRight, + .doneCritText3.taskTextOutsideLeft, + .doneCritText3.taskTextOutsideRight { + fill: ${e.taskTextOutsideColor} !important; + } + + .vert { + stroke: ${e.vertLineColor}; + } + + .vertText { + font-size: 15px; + text-anchor: middle; + fill: ${e.vertLineColor} !important; + } + + .activeCritText0, + .activeCritText1, + .activeCritText2, + .activeCritText3 { + fill: ${e.taskTextDarkColor} !important; + } + + .titleText { + text-anchor: middle; + font-size: 18px; + fill: ${e.titleColor||e.textColor}; + font-family: ${e.fontFamily}; + } +`,"getStyles"),R7e=P5t});var L7e={};ir(L7e,{diagram:()=>O5t});var O5t,D7e=F(()=>{"use strict";t7e();C7e();A7e();_7e();O5t={parser:e7e,db:b7e,renderer:E7e,styles:R7e}});var N7e,P7e=F(()=>{"use strict";Xa();vt();N7e={parse:o(async e=>{let t=await Si("info",e);Z.debug(t)},"parse")}});var z5t,G5t,O7e,B7e=F(()=>{"use strict";z5t={version:"11.16.1"},G5t=o(()=>z5t.version,"getVersion"),O7e={getVersion:G5t}});var xn,Ka=F(()=>{"use strict";$r();Xt();xn=o(e=>{let{securityLevel:t}=Ae(),r=et("body");if(t==="sandbox"){let a=et(`#i${e}`).node()?.contentDocument??document;r=et(a.body)}return r.select(`#${e}`)},"selectSvgElement")});var V5t,$7e,F7e=F(()=>{"use strict";vt();Ka();$n();V5t=o((e,t,r)=>{Z.debug(`rendering info diagram +`+e);let n=xn(t);Wr(n,100,400,!0),n.append("g").append("text").attr("x",100).attr("y",40).attr("class","version").attr("font-size",32).style("text-anchor","middle").text(`v${r}`)},"draw"),$7e={draw:V5t}});var z7e={};ir(z7e,{diagram:()=>W5t});var W5t,G7e=F(()=>{"use strict";P7e();B7e();F7e();W5t={parser:N7e,db:O7e,renderer:$7e}});var q7e,WU,lL,qU,U5t,Y5t,j5t,X5t,K5t,Z5t,Q5t,cL,HU=F(()=>{"use strict";vt();Nn();Wi();q7e=cr.pie,WU={sections:new Map,showData:!1,config:q7e},lL=WU.sections,qU=WU.showData,U5t=structuredClone(q7e),Y5t=o(()=>structuredClone(U5t),"getConfig"),j5t=o(()=>{lL=new Map,qU=WU.showData,yr()},"clear"),X5t=o(({label:e,value:t})=>{if(t<0)throw new Error(`"${e}" has invalid value: ${t}. Negative values are not allowed in pie charts. All slice values must be >= 0.`);lL.has(e)||(lL.set(e,t),Z.debug(`added new section: ${e}, with value: ${t}`))},"addSection"),K5t=o(()=>lL,"getSections"),Z5t=o(e=>{qU=e},"setShowData"),Q5t=o(()=>qU,"getShowData"),cL={getConfig:Y5t,clear:j5t,setDiagramTitle:Or,getDiagramTitle:Lr,setAccTitle:kr,getAccTitle:Ar,setAccDescription:Rr,getAccDescription:_r,addSection:X5t,getSections:K5t,setShowData:Z5t,getShowData:Q5t}});var J5t,H7e,U7e=F(()=>{"use strict";Xa();vt();Hs();HU();J5t=o((e,t)=>{Gn(e,t),t.setShowData(e.showData),e.sections.map(t.addSection)},"populateDb"),H7e={parse:o(async e=>{let t=await Si("pie",e);Z.debug(t),J5t(t,cL)},"parse")}});var eAt,Y7e,j7e=F(()=>{"use strict";eAt=o(e=>` + .pieCircle{ + stroke: ${e.pieStrokeColor}; + stroke-width : ${e.pieStrokeWidth}; + opacity : ${e.pieOpacity}; + } + .pieCircle.highlighted{ + scale: 1.05; + opacity: 1; + } + .pieCircle.highlightedOnHover:hover{ + transition-duration: 250ms; + scale: 1.05; + opacity: 1; + } + .pieOuterCircle{ + stroke: ${e.pieOuterStrokeColor}; + stroke-width: ${e.pieOuterStrokeWidth}; + fill: none; + } + .pieTitleText { + text-anchor: middle; + font-size: ${e.pieTitleTextSize}; + fill: ${e.pieTitleTextColor}; + font-family: ${e.fontFamily}; + } + .slice { + font-family: ${e.fontFamily}; + fill: ${e.pieSectionTextColor}; + font-size:${e.pieSectionTextSize}; + // fill: white; + } + .legend text { + fill: ${e.pieLegendTextColor}; + font-family: ${e.fontFamily}; + font-size: ${e.pieLegendTextSize}; + } +`,"getStyles"),Y7e=eAt});var tAt,rAt,X7e,K7e=F(()=>{"use strict";$r();Xt();vt();Ka();$n();Qt();tAt=o(e=>{let t=[...e.values()].reduce((i,a)=>i+a,0),r=[...e.entries()].map(([i,a])=>({label:i,value:a})).filter(i=>i.value/t*100>=1);return AE().value(i=>i.value).sort(null)(r)},"createPieArcs"),rAt=o((e,t,r,n)=>{Z.debug(`rendering pie chart +`+e);let i=n.db,a=Ae(),s=qr(i.getConfig(),a.pie),l=40,u=18,h=4,d=450,f=d,p=xn(t),m=p.append("g");m.attr("transform","translate("+f/2+","+d/2+")");let{themeVariables:g}=a,[y]=As(g.pieOuterStrokeWidth);y??=2;let v=s.legendPosition,x=s.textPosition,b=s.donutHole>0&&s.donutHole<=.9?s.donutHole:0,T=Math.min(f,d)/2-l,k=ec().innerRadius(b*T).outerRadius(T),C=ec().innerRadius(T*x).outerRadius(T*x),w=m.append("g");w.append("circle").attr("cx",0).attr("cy",0).attr("r",T+y/2).attr("class","pieOuterCircle");let S=i.getSections(),R=tAt(S),L=[g.pie1,g.pie2,g.pie3,g.pie4,g.pie5,g.pie6,g.pie7,g.pie8,g.pie9,g.pie10,g.pie11,g.pie12],N=0;S.forEach(U=>{N+=U});let I=R.filter(U=>(U.data.value/N*100).toFixed(0)!=="0"),_=Oo(L).domain([...S.keys()]);w.selectAll("mySlices").data(I).enter().append("path").attr("d",k).attr("fill",U=>_(U.data.label)).attr("class",U=>{let oe="pieCircle";return s.highlightSlice==="hover"?oe+=" highlightedOnHover":s.highlightSlice===U.data.label&&(oe+=" highlighted"),oe}),w.selectAll("mySlices").data(I).enter().append("text").text(U=>(U.data.value/N*100).toFixed(0)+"%").attr("transform",U=>"translate("+C.centroid(U)+")").style("text-anchor","middle").attr("class","slice");let A=m.append("text").text(i.getDiagramTitle()).attr("x",0).attr("y",-(d-50)/2).attr("class","pieTitleText"),M=[...S.entries()].map(([U,oe])=>({label:U,value:oe})),D=m.selectAll(".legend").data(M).enter().append("g").attr("class","legend");D.append("rect").attr("width",u).attr("height",u).style("fill",U=>_(U.label)).style("stroke",U=>_(U.label)),D.append("text").attr("x",u+h).attr("y",u-h).text(U=>i.getShowData()?`${U.label} [${U.value}]`:U.label);let P=Math.max(...D.selectAll("text").nodes().map(U=>U?.getBoundingClientRect().width??0)),B=d,O=f+l,$=u+h,V=M.length*$;switch(v){case"center":D.attr("transform",(U,oe)=>{let te=$*M.length/2,le=-P/2-(u+h),ie=oe*$-te;return"translate("+le+","+ie+")"});break;case"top":B+=V,D.attr("transform",(U,oe)=>{let te=T,le=-P/2-(u+h),ie=oe*$-te;return`translate(${le}, ${ie})`}),w.attr("transform",()=>`translate(0, ${V+$})`);break;case"bottom":B+=V,D.attr("transform",(U,oe)=>{let te=-T-$,le=-P/2-(u+h),ie=oe*$-te;return"translate("+le+","+ie+")"});break;case"left":O+=u+h+P,D.attr("transform",(U,oe)=>{let te=$*M.length/2,le=-T-(u+h),ie=oe*$-te;return"translate("+le+","+ie+")"}),w.attr("transform",()=>`translate(${P+u+h}, 0)`);break;case"right":default:O+=u+h+P,D.attr("transform",(U,oe)=>{let te=$*M.length/2,le=12*u,ie=oe*$-te;return"translate("+le+","+ie+")"});break}let G=A.node()?.getBoundingClientRect().width??0,z=f/2-G/2,W=f/2+G/2,H=Math.min(0,z),Q=Math.max(O,W)-H;p.attr("viewBox",`${H} 0 ${Q} ${B}`),Wr(p,B,Q,s.useMaxWidth)},"draw"),X7e={draw:rAt}});var Z7e={};ir(Z7e,{diagram:()=>nAt});var nAt,Q7e=F(()=>{"use strict";U7e();HU();j7e();K7e();nAt={parser:H7e,db:cL,renderer:X7e,styles:Y7e}});var UU,e8e,t8e=F(()=>{"use strict";UU=(function(){var e=o(function(X,fe,K,qe){for(K=K||{},qe=X.length;qe--;K[X[qe]]=fe);return K},"o"),t=[1,3],r=[1,4],n=[1,5],i=[1,6],a=[1,7],s=[1,4,5,10,12,13,14,15,18,25,35,37,39,41,42,48,50,51,52,53,54,55,56,57,60,61,63,64,65,66,67],l=[1,4,5,10,12,13,14,15,18,25,28,35,37,39,41,42,48,50,51,52,53,54,55,56,57,60,61,63,64,65,66,67],u=[55,56,57],h=[2,36],d=[1,37],f=[1,36],p=[1,38],m=[1,35],g=[1,43],y=[1,41],v=[1,45],x=[1,14],b=[1,23],T=[1,18],k=[1,19],C=[1,20],w=[1,21],S=[1,22],R=[1,24],L=[1,25],N=[1,26],I=[1,27],_=[1,28],A=[1,29],M=[1,32],D=[1,33],P=[1,34],B=[1,39],O=[1,40],$=[1,42],V=[1,44],G=[1,63],z=[1,62],W=[4,5,8,10,12,13,14,15,18,44,47,49,55,56,57,63,64,65,66,67],H=[1,66],j=[1,67],Q=[1,68],U=[1,69],oe=[1,70],te=[1,71],le=[1,72],ie=[1,73],ae=[1,74],Re=[1,75],be=[1,76],Pe=[1,77],Ge=[4,5,6,7,8,9,10,11,12,13,14,15,18],Oe=[1,91],ue=[1,92],ye=[1,93],ke=[1,100],ce=[1,94],re=[1,97],J=[1,95],se=[1,96],ge=[1,98],Te=[1,99],we=[1,103],Me=[10,55,56,57],ve=[4,5,6,8,10,11,13,17,18,19,20,55,56,57],ne={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,idStringToken:3,ALPHA:4,NUM:5,NODE_STRING:6,DOWN:7,MINUS:8,DEFAULT:9,COMMA:10,COLON:11,AMP:12,BRKT:13,MULT:14,UNICODE_TEXT:15,styleComponent:16,UNIT:17,SPACE:18,STYLE:19,PCT:20,idString:21,style:22,stylesOpt:23,classDefStatement:24,CLASSDEF:25,start:26,eol:27,QUADRANT:28,document:29,line:30,statement:31,axisDetails:32,quadrantDetails:33,points:34,title:35,title_value:36,acc_title:37,acc_title_value:38,acc_descr:39,acc_descr_value:40,acc_descr_multiline_value:41,section:42,text:43,point_start:44,point_x:45,point_y:46,class_name:47,"X-AXIS":48,"AXIS-TEXT-DELIMITER":49,"Y-AXIS":50,QUADRANT_1:51,QUADRANT_2:52,QUADRANT_3:53,QUADRANT_4:54,NEWLINE:55,SEMI:56,EOF:57,alphaNumToken:58,textNoTagsToken:59,STR:60,MD_STR:61,alphaNum:62,PUNCTUATION:63,PLUS:64,EQUALS:65,DOT:66,UNDERSCORE:67,$accept:0,$end:1},terminals_:{2:"error",4:"ALPHA",5:"NUM",6:"NODE_STRING",7:"DOWN",8:"MINUS",9:"DEFAULT",10:"COMMA",11:"COLON",12:"AMP",13:"BRKT",14:"MULT",15:"UNICODE_TEXT",17:"UNIT",18:"SPACE",19:"STYLE",20:"PCT",25:"CLASSDEF",28:"QUADRANT",35:"title",36:"title_value",37:"acc_title",38:"acc_title_value",39:"acc_descr",40:"acc_descr_value",41:"acc_descr_multiline_value",42:"section",44:"point_start",45:"point_x",46:"point_y",47:"class_name",48:"X-AXIS",49:"AXIS-TEXT-DELIMITER",50:"Y-AXIS",51:"QUADRANT_1",52:"QUADRANT_2",53:"QUADRANT_3",54:"QUADRANT_4",55:"NEWLINE",56:"SEMI",57:"EOF",60:"STR",61:"MD_STR",63:"PUNCTUATION",64:"PLUS",65:"EQUALS",66:"DOT",67:"UNDERSCORE"},productions_:[0,[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[3,1],[16,1],[16,1],[16,1],[16,1],[16,1],[16,1],[16,1],[16,1],[16,1],[16,1],[21,1],[21,2],[22,1],[22,2],[23,1],[23,3],[24,5],[26,2],[26,2],[26,2],[29,0],[29,2],[30,2],[31,0],[31,1],[31,2],[31,1],[31,1],[31,1],[31,2],[31,2],[31,2],[31,1],[31,1],[34,4],[34,5],[34,5],[34,6],[32,4],[32,3],[32,2],[32,4],[32,3],[32,2],[33,2],[33,2],[33,2],[33,2],[27,1],[27,1],[27,1],[43,1],[43,2],[43,1],[43,1],[62,1],[62,2],[58,1],[58,1],[58,1],[58,1],[58,1],[58,1],[58,1],[58,1],[58,1],[58,1],[58,1],[58,1],[59,1],[59,1],[59,1]],performAction:o(function(fe,K,qe,_e,Be,Ne,He){var $e=Ne.length-1;switch(Be){case 23:this.$=Ne[$e];break;case 24:this.$=Ne[$e-1]+""+Ne[$e];break;case 26:this.$=Ne[$e-1]+Ne[$e];break;case 27:this.$=[Ne[$e].trim()];break;case 28:Ne[$e-2].push(Ne[$e].trim()),this.$=Ne[$e-2];break;case 29:this.$=Ne[$e-4],_e.addClass(Ne[$e-2],Ne[$e]);break;case 37:this.$=[];break;case 42:this.$=Ne[$e].trim(),_e.setDiagramTitle(this.$);break;case 43:this.$=Ne[$e].trim(),_e.setAccTitle(this.$);break;case 44:case 45:this.$=Ne[$e].trim(),_e.setAccDescription(this.$);break;case 46:_e.addSection(Ne[$e].substr(8)),this.$=Ne[$e].substr(8);break;case 47:_e.addPoint(Ne[$e-3],"",Ne[$e-1],Ne[$e],[]);break;case 48:_e.addPoint(Ne[$e-4],Ne[$e-3],Ne[$e-1],Ne[$e],[]);break;case 49:_e.addPoint(Ne[$e-4],"",Ne[$e-2],Ne[$e-1],Ne[$e]);break;case 50:_e.addPoint(Ne[$e-5],Ne[$e-4],Ne[$e-2],Ne[$e-1],Ne[$e]);break;case 51:_e.setXAxisLeftText(Ne[$e-2]),_e.setXAxisRightText(Ne[$e]);break;case 52:Ne[$e-1].text+=" \u27F6 ",_e.setXAxisLeftText(Ne[$e-1]);break;case 53:_e.setXAxisLeftText(Ne[$e]);break;case 54:_e.setYAxisBottomText(Ne[$e-2]),_e.setYAxisTopText(Ne[$e]);break;case 55:Ne[$e-1].text+=" \u27F6 ",_e.setYAxisBottomText(Ne[$e-1]);break;case 56:_e.setYAxisBottomText(Ne[$e]);break;case 57:_e.setQuadrant1Text(Ne[$e]);break;case 58:_e.setQuadrant2Text(Ne[$e]);break;case 59:_e.setQuadrant3Text(Ne[$e]);break;case 60:_e.setQuadrant4Text(Ne[$e]);break;case 64:this.$={text:Ne[$e],type:"text"};break;case 65:this.$={text:Ne[$e-1].text+""+Ne[$e],type:Ne[$e-1].type};break;case 66:this.$={text:Ne[$e],type:"text"};break;case 67:this.$={text:Ne[$e],type:"markdown"};break;case 68:this.$=Ne[$e];break;case 69:this.$=Ne[$e-1]+""+Ne[$e];break}},"anonymous"),table:[{18:t,26:1,27:2,28:r,55:n,56:i,57:a},{1:[3]},{18:t,26:8,27:2,28:r,55:n,56:i,57:a},{18:t,26:9,27:2,28:r,55:n,56:i,57:a},e(s,[2,33],{29:10}),e(l,[2,61]),e(l,[2,62]),e(l,[2,63]),{1:[2,30]},{1:[2,31]},e(u,h,{30:11,31:12,24:13,32:15,33:16,34:17,43:30,58:31,1:[2,32],4:d,5:f,10:p,12:m,13:g,14:y,15:v,18:x,25:b,35:T,37:k,39:C,41:w,42:S,48:R,50:L,51:N,52:I,53:_,54:A,60:M,61:D,63:P,64:B,65:O,66:$,67:V}),e(s,[2,34]),{27:46,55:n,56:i,57:a},e(u,[2,37]),e(u,h,{24:13,32:15,33:16,34:17,43:30,58:31,31:47,4:d,5:f,10:p,12:m,13:g,14:y,15:v,18:x,25:b,35:T,37:k,39:C,41:w,42:S,48:R,50:L,51:N,52:I,53:_,54:A,60:M,61:D,63:P,64:B,65:O,66:$,67:V}),e(u,[2,39]),e(u,[2,40]),e(u,[2,41]),{36:[1,48]},{38:[1,49]},{40:[1,50]},e(u,[2,45]),e(u,[2,46]),{18:[1,51]},{4:d,5:f,10:p,12:m,13:g,14:y,15:v,43:52,58:31,60:M,61:D,63:P,64:B,65:O,66:$,67:V},{4:d,5:f,10:p,12:m,13:g,14:y,15:v,43:53,58:31,60:M,61:D,63:P,64:B,65:O,66:$,67:V},{4:d,5:f,10:p,12:m,13:g,14:y,15:v,43:54,58:31,60:M,61:D,63:P,64:B,65:O,66:$,67:V},{4:d,5:f,10:p,12:m,13:g,14:y,15:v,43:55,58:31,60:M,61:D,63:P,64:B,65:O,66:$,67:V},{4:d,5:f,10:p,12:m,13:g,14:y,15:v,43:56,58:31,60:M,61:D,63:P,64:B,65:O,66:$,67:V},{4:d,5:f,10:p,12:m,13:g,14:y,15:v,43:57,58:31,60:M,61:D,63:P,64:B,65:O,66:$,67:V},{4:d,5:f,8:G,10:p,12:m,13:g,14:y,15:v,18:z,44:[1,58],47:[1,59],58:61,59:60,63:P,64:B,65:O,66:$,67:V},e(W,[2,64]),e(W,[2,66]),e(W,[2,67]),e(W,[2,70]),e(W,[2,71]),e(W,[2,72]),e(W,[2,73]),e(W,[2,74]),e(W,[2,75]),e(W,[2,76]),e(W,[2,77]),e(W,[2,78]),e(W,[2,79]),e(W,[2,80]),e(W,[2,81]),e(s,[2,35]),e(u,[2,38]),e(u,[2,42]),e(u,[2,43]),e(u,[2,44]),{3:65,4:H,5:j,6:Q,7:U,8:oe,9:te,10:le,11:ie,12:ae,13:Re,14:be,15:Pe,21:64},e(u,[2,53],{59:60,58:61,4:d,5:f,8:G,10:p,12:m,13:g,14:y,15:v,18:z,49:[1,78],63:P,64:B,65:O,66:$,67:V}),e(u,[2,56],{59:60,58:61,4:d,5:f,8:G,10:p,12:m,13:g,14:y,15:v,18:z,49:[1,79],63:P,64:B,65:O,66:$,67:V}),e(u,[2,57],{59:60,58:61,4:d,5:f,8:G,10:p,12:m,13:g,14:y,15:v,18:z,63:P,64:B,65:O,66:$,67:V}),e(u,[2,58],{59:60,58:61,4:d,5:f,8:G,10:p,12:m,13:g,14:y,15:v,18:z,63:P,64:B,65:O,66:$,67:V}),e(u,[2,59],{59:60,58:61,4:d,5:f,8:G,10:p,12:m,13:g,14:y,15:v,18:z,63:P,64:B,65:O,66:$,67:V}),e(u,[2,60],{59:60,58:61,4:d,5:f,8:G,10:p,12:m,13:g,14:y,15:v,18:z,63:P,64:B,65:O,66:$,67:V}),{45:[1,80]},{44:[1,81]},e(W,[2,65]),e(W,[2,82]),e(W,[2,83]),e(W,[2,84]),{3:83,4:H,5:j,6:Q,7:U,8:oe,9:te,10:le,11:ie,12:ae,13:Re,14:be,15:Pe,18:[1,82]},e(Ge,[2,23]),e(Ge,[2,1]),e(Ge,[2,2]),e(Ge,[2,3]),e(Ge,[2,4]),e(Ge,[2,5]),e(Ge,[2,6]),e(Ge,[2,7]),e(Ge,[2,8]),e(Ge,[2,9]),e(Ge,[2,10]),e(Ge,[2,11]),e(Ge,[2,12]),e(u,[2,52],{58:31,43:84,4:d,5:f,10:p,12:m,13:g,14:y,15:v,60:M,61:D,63:P,64:B,65:O,66:$,67:V}),e(u,[2,55],{58:31,43:85,4:d,5:f,10:p,12:m,13:g,14:y,15:v,60:M,61:D,63:P,64:B,65:O,66:$,67:V}),{46:[1,86]},{45:[1,87]},{4:Oe,5:ue,6:ye,8:ke,11:ce,13:re,16:90,17:J,18:se,19:ge,20:Te,22:89,23:88},e(Ge,[2,24]),e(u,[2,51],{59:60,58:61,4:d,5:f,8:G,10:p,12:m,13:g,14:y,15:v,18:z,63:P,64:B,65:O,66:$,67:V}),e(u,[2,54],{59:60,58:61,4:d,5:f,8:G,10:p,12:m,13:g,14:y,15:v,18:z,63:P,64:B,65:O,66:$,67:V}),e(u,[2,47],{22:89,16:90,23:101,4:Oe,5:ue,6:ye,8:ke,11:ce,13:re,17:J,18:se,19:ge,20:Te}),{46:[1,102]},e(u,[2,29],{10:we}),e(Me,[2,27],{16:104,4:Oe,5:ue,6:ye,8:ke,11:ce,13:re,17:J,18:se,19:ge,20:Te}),e(ve,[2,25]),e(ve,[2,13]),e(ve,[2,14]),e(ve,[2,15]),e(ve,[2,16]),e(ve,[2,17]),e(ve,[2,18]),e(ve,[2,19]),e(ve,[2,20]),e(ve,[2,21]),e(ve,[2,22]),e(u,[2,49],{10:we}),e(u,[2,48],{22:89,16:90,23:105,4:Oe,5:ue,6:ye,8:ke,11:ce,13:re,17:J,18:se,19:ge,20:Te}),{4:Oe,5:ue,6:ye,8:ke,11:ce,13:re,16:90,17:J,18:se,19:ge,20:Te,22:106},e(ve,[2,26]),e(u,[2,50],{10:we}),e(Me,[2,28],{16:104,4:Oe,5:ue,6:ye,8:ke,11:ce,13:re,17:J,18:se,19:ge,20:Te})],defaultActions:{8:[2,30],9:[2,31]},parseError:o(function(fe,K){if(K.recoverable)this.trace(fe);else{var qe=new Error(fe);throw qe.hash=K,qe}},"parseError"),parse:o(function(fe){var K=this,qe=[0],_e=[],Be=[null],Ne=[],He=this.table,$e="",Xe=0,Fe=0,Ke=0,xe=2,mt=1,Le=Ne.slice.call(arguments,1),ft=Object.create(this.lexer),wt={yy:{}};for(var zt in this.yy)Object.prototype.hasOwnProperty.call(this.yy,zt)&&(wt.yy[zt]=this.yy[zt]);ft.setInput(fe,wt.yy),wt.yy.lexer=ft,wt.yy.parser=this,typeof ft.yylloc>"u"&&(ft.yylloc={});var St=ft.yylloc;Ne.push(St);var At=ft.options&&ft.options.ranges;typeof wt.yy.parseError=="function"?this.parseError=wt.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function bt(De){qe.length=qe.length-2*De,Be.length=Be.length-De,Ne.length=Ne.length-De}o(bt,"popStack");function me(){var De;return De=_e.pop()||ft.lex()||mt,typeof De!="number"&&(De instanceof Array&&(_e=De,De=_e.pop()),De=K.symbols_[De]||De),De}o(me,"lex");for(var lt,gt,Ze,Ee,tt,at,ot={},Wt,Bt,qt,vr;;){if(Ze=qe[qe.length-1],this.defaultActions[Ze]?Ee=this.defaultActions[Ze]:((lt===null||typeof lt>"u")&&(lt=me()),Ee=He[Ze]&&He[Ze][lt]),typeof Ee>"u"||!Ee.length||!Ee[0]){var Tt="";vr=[];for(Wt in He[Ze])this.terminals_[Wt]&&Wt>xe&&vr.push("'"+this.terminals_[Wt]+"'");ft.showPosition?Tt="Parse error on line "+(Xe+1)+`: +`+ft.showPosition()+` +Expecting `+vr.join(", ")+", got '"+(this.terminals_[lt]||lt)+"'":Tt="Parse error on line "+(Xe+1)+": Unexpected "+(lt==mt?"end of input":"'"+(this.terminals_[lt]||lt)+"'"),this.parseError(Tt,{text:ft.match,token:this.terminals_[lt]||lt,line:ft.yylineno,loc:St,expected:vr})}if(Ee[0]instanceof Array&&Ee.length>1)throw new Error("Parse Error: multiple actions possible at state: "+Ze+", token: "+lt);switch(Ee[0]){case 1:qe.push(lt),Be.push(ft.yytext),Ne.push(ft.yylloc),qe.push(Ee[1]),lt=null,gt?(lt=gt,gt=null):(Fe=ft.yyleng,$e=ft.yytext,Xe=ft.yylineno,St=ft.yylloc,Ke>0&&Ke--);break;case 2:if(Bt=this.productions_[Ee[1]][1],ot.$=Be[Be.length-Bt],ot._$={first_line:Ne[Ne.length-(Bt||1)].first_line,last_line:Ne[Ne.length-1].last_line,first_column:Ne[Ne.length-(Bt||1)].first_column,last_column:Ne[Ne.length-1].last_column},At&&(ot._$.range=[Ne[Ne.length-(Bt||1)].range[0],Ne[Ne.length-1].range[1]]),at=this.performAction.apply(ot,[$e,Fe,Xe,wt.yy,Ee[1],Be,Ne].concat(Le)),typeof at<"u")return at;Bt&&(qe=qe.slice(0,-1*Bt*2),Be=Be.slice(0,-1*Bt),Ne=Ne.slice(0,-1*Bt)),qe.push(this.productions_[Ee[1]][0]),Be.push(ot.$),Ne.push(ot._$),qt=He[qe[qe.length-2]][qe[qe.length-1]],qe.push(qt);break;case 3:return!0}}return!0},"parse")},q=(function(){var X={EOF:1,parseError:o(function(K,qe){if(this.yy.parser)this.yy.parser.parseError(K,qe);else throw new Error(K)},"parseError"),setInput:o(function(fe,K){return this.yy=K||this.yy||{},this._input=fe,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var fe=this._input[0];this.yytext+=fe,this.yyleng++,this.offset++,this.match+=fe,this.matched+=fe;var K=fe.match(/(?:\r\n?|\n).*/g);return K?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),fe},"input"),unput:o(function(fe){var K=fe.length,qe=fe.split(/(?:\r\n?|\n)/g);this._input=fe+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-K),this.offset-=K;var _e=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),qe.length-1&&(this.yylineno-=qe.length-1);var Be=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:qe?(qe.length===_e.length?this.yylloc.first_column:0)+_e[_e.length-qe.length].length-qe[0].length:this.yylloc.first_column-K},this.options.ranges&&(this.yylloc.range=[Be[0],Be[0]+this.yyleng-K]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(fe){this.unput(this.match.slice(fe))},"less"),pastInput:o(function(){var fe=this.matched.substr(0,this.matched.length-this.match.length);return(fe.length>20?"...":"")+fe.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var fe=this.match;return fe.length<20&&(fe+=this._input.substr(0,20-fe.length)),(fe.substr(0,20)+(fe.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var fe=this.pastInput(),K=new Array(fe.length+1).join("-");return fe+this.upcomingInput()+` +`+K+"^"},"showPosition"),test_match:o(function(fe,K){var qe,_e,Be;if(this.options.backtrack_lexer&&(Be={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(Be.yylloc.range=this.yylloc.range.slice(0))),_e=fe[0].match(/(?:\r\n?|\n).*/g),_e&&(this.yylineno+=_e.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:_e?_e[_e.length-1].length-_e[_e.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+fe[0].length},this.yytext+=fe[0],this.match+=fe[0],this.matches=fe,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(fe[0].length),this.matched+=fe[0],qe=this.performAction.call(this,this.yy,this,K,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),qe)return qe;if(this._backtrack){for(var Ne in Be)this[Ne]=Be[Ne];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var fe,K,qe,_e;this._more||(this.yytext="",this.match="");for(var Be=this._currentRules(),Ne=0;NeK[0].length)){if(K=qe,_e=Ne,this.options.backtrack_lexer){if(fe=this.test_match(qe,Be[Ne]),fe!==!1)return fe;if(this._backtrack){K=!1;continue}else return!1}else if(!this.options.flex)break}return K?(fe=this.test_match(K,Be[_e]),fe!==!1?fe:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var K=this.next();return K||this.lex()},"lex"),begin:o(function(K){this.conditionStack.push(K)},"begin"),popState:o(function(){var K=this.conditionStack.length-1;return K>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(K){return K=this.conditionStack.length-1-Math.abs(K||0),K>=0?this.conditionStack[K]:"INITIAL"},"topState"),pushState:o(function(K){this.begin(K)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:o(function(K,qe,_e,Be){var Ne=Be;switch(_e){case 0:break;case 1:break;case 2:return 55;case 3:break;case 4:return this.begin("title"),35;break;case 5:return this.popState(),"title_value";break;case 6:return this.begin("acc_title"),37;break;case 7:return this.popState(),"acc_title_value";break;case 8:return this.begin("acc_descr"),39;break;case 9:return this.popState(),"acc_descr_value";break;case 10:this.begin("acc_descr_multiline");break;case 11:this.popState();break;case 12:return"acc_descr_multiline_value";case 13:return 48;case 14:return 50;case 15:return 49;case 16:return 51;case 17:return 52;case 18:return 53;case 19:return 54;case 20:return 25;case 21:this.begin("md_string");break;case 22:return"MD_STR";case 23:this.popState();break;case 24:this.begin("string");break;case 25:this.popState();break;case 26:return"STR";case 27:this.begin("class_name");break;case 28:return this.popState(),47;break;case 29:return this.begin("point_start"),44;break;case 30:return this.begin("point_x"),45;break;case 31:this.popState();break;case 32:this.popState(),this.begin("point_y");break;case 33:return this.popState(),46;break;case 34:return 28;case 35:return 4;case 36:return 15;case 37:return 11;case 38:return 64;case 39:return 10;case 40:return 65;case 41:return 65;case 42:return 14;case 43:return 13;case 44:return 67;case 45:return 66;case 46:return 12;case 47:return 8;case 48:return 5;case 49:return 18;case 50:return 56;case 51:return 63;case 52:return 57}},"anonymous"),rules:[/^(?:%%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[\n\r]+)/i,/^(?:%%[^\n]*)/i,/^(?:title\b)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?: *x-axis *)/i,/^(?: *y-axis *)/i,/^(?: *--+> *)/i,/^(?: *quadrant-1 *)/i,/^(?: *quadrant-2 *)/i,/^(?: *quadrant-3 *)/i,/^(?: *quadrant-4 *)/i,/^(?:classDef\b)/i,/^(?:["][`])/i,/^(?:[^`"]+)/i,/^(?:[`]["])/i,/^(?:["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?::::)/i,/^(?:^\w+)/i,/^(?:\s*:\s*\[\s*)/i,/^(?:(1)|(0(.\d+)?))/i,/^(?:\s*\] *)/i,/^(?:\s*,\s*)/i,/^(?:(1)|(0(.\d+)?))/i,/^(?: *quadrantChart *)/i,/^(?:[A-Za-z]+)/i,/^(?:[^\x00-\x7F]+)/i,/^(?::)/i,/^(?:\+)/i,/^(?:,)/i,/^(?:=)/i,/^(?:=)/i,/^(?:\*)/i,/^(?:#)/i,/^(?:[\_])/i,/^(?:\.)/i,/^(?:&)/i,/^(?:-)/i,/^(?:[0-9]+)/i,/^(?:\s)/i,/^(?:;)/i,/^(?:[!"#$%&'*+,-.`?\\_/])/i,/^(?:$)/i],conditions:{class_name:{rules:[28],inclusive:!1},point_y:{rules:[33],inclusive:!1},point_x:{rules:[32],inclusive:!1},point_start:{rules:[30,31],inclusive:!1},acc_descr_multiline:{rules:[11,12],inclusive:!1},acc_descr:{rules:[9],inclusive:!1},acc_title:{rules:[7],inclusive:!1},title:{rules:[5],inclusive:!1},md_string:{rules:[22,23],inclusive:!1},string:{rules:[25,26],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,6,8,10,13,14,15,16,17,18,19,20,21,24,27,29,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52],inclusive:!0}}};return X})();ne.lexer=q;function he(){this.yy={}}return o(he,"Parser"),he.prototype=ne,ne.Parser=he,new he})();UU.parser=UU;e8e=UU});var Us,uL,r8e=F(()=>{"use strict";$r();Wi();vt();Pc();Us=ma(),uL=class{constructor(){this.classes=new Map;this.config=this.getDefaultConfig(),this.themeConfig=this.getDefaultThemeConfig(),this.data=this.getDefaultData()}static{o(this,"QuadrantBuilder")}getDefaultData(){return{titleText:"",quadrant1Text:"",quadrant2Text:"",quadrant3Text:"",quadrant4Text:"",xAxisLeftText:"",xAxisRightText:"",yAxisBottomText:"",yAxisTopText:"",points:[]}}getDefaultConfig(){return{showXAxis:!0,showYAxis:!0,showTitle:!0,chartHeight:cr.quadrantChart?.chartWidth||500,chartWidth:cr.quadrantChart?.chartHeight||500,titlePadding:cr.quadrantChart?.titlePadding||10,titleFontSize:cr.quadrantChart?.titleFontSize||20,quadrantPadding:cr.quadrantChart?.quadrantPadding||5,xAxisLabelPadding:cr.quadrantChart?.xAxisLabelPadding||5,yAxisLabelPadding:cr.quadrantChart?.yAxisLabelPadding||5,xAxisLabelFontSize:cr.quadrantChart?.xAxisLabelFontSize||16,yAxisLabelFontSize:cr.quadrantChart?.yAxisLabelFontSize||16,quadrantLabelFontSize:cr.quadrantChart?.quadrantLabelFontSize||16,quadrantTextTopPadding:cr.quadrantChart?.quadrantTextTopPadding||5,pointTextPadding:cr.quadrantChart?.pointTextPadding||5,pointLabelFontSize:cr.quadrantChart?.pointLabelFontSize||12,pointRadius:cr.quadrantChart?.pointRadius||5,xAxisPosition:cr.quadrantChart?.xAxisPosition||"top",yAxisPosition:cr.quadrantChart?.yAxisPosition||"left",quadrantInternalBorderStrokeWidth:cr.quadrantChart?.quadrantInternalBorderStrokeWidth||1,quadrantExternalBorderStrokeWidth:cr.quadrantChart?.quadrantExternalBorderStrokeWidth||2}}getDefaultThemeConfig(){return{quadrant1Fill:Us.quadrant1Fill,quadrant2Fill:Us.quadrant2Fill,quadrant3Fill:Us.quadrant3Fill,quadrant4Fill:Us.quadrant4Fill,quadrant1TextFill:Us.quadrant1TextFill,quadrant2TextFill:Us.quadrant2TextFill,quadrant3TextFill:Us.quadrant3TextFill,quadrant4TextFill:Us.quadrant4TextFill,quadrantPointFill:Us.quadrantPointFill,quadrantPointTextFill:Us.quadrantPointTextFill,quadrantXAxisTextFill:Us.quadrantXAxisTextFill,quadrantYAxisTextFill:Us.quadrantYAxisTextFill,quadrantTitleFill:Us.quadrantTitleFill,quadrantInternalBorderStrokeFill:Us.quadrantInternalBorderStrokeFill,quadrantExternalBorderStrokeFill:Us.quadrantExternalBorderStrokeFill}}clear(){this.config=this.getDefaultConfig(),this.themeConfig=this.getDefaultThemeConfig(),this.data=this.getDefaultData(),this.classes=new Map,Z.info("clear called")}setData(t){this.data={...this.data,...t}}addPoints(t){this.data.points=[...t,...this.data.points]}addClass(t,r){this.classes.set(t,r)}setConfig(t){Z.trace("setConfig called with: ",t),this.config={...this.config,...t}}setThemeConfig(t){Z.trace("setThemeConfig called with: ",t),this.themeConfig={...this.themeConfig,...t}}calculateSpace(t,r,n,i){let a=this.config.xAxisLabelPadding*2+this.config.xAxisLabelFontSize,s={top:t==="top"&&r?a:0,bottom:t==="bottom"&&r?a:0},l=this.config.yAxisLabelPadding*2+this.config.yAxisLabelFontSize,u={left:this.config.yAxisPosition==="left"&&n?l:0,right:this.config.yAxisPosition==="right"&&n?l:0},h=this.config.titleFontSize+this.config.titlePadding*2,d={top:i?h:0},f=this.config.quadrantPadding+u.left,p=this.config.quadrantPadding+s.top+d.top,m=this.config.chartWidth-this.config.quadrantPadding*2-u.left-u.right,g=this.config.chartHeight-this.config.quadrantPadding*2-s.top-s.bottom-d.top,y=m/2,v=g/2;return{xAxisSpace:s,yAxisSpace:u,titleSpace:d,quadrantSpace:{quadrantLeft:f,quadrantTop:p,quadrantWidth:m,quadrantHalfWidth:y,quadrantHeight:g,quadrantHalfHeight:v}}}getAxisLabels(t,r,n,i){let{quadrantSpace:a,titleSpace:s}=i,{quadrantHalfHeight:l,quadrantHeight:u,quadrantLeft:h,quadrantHalfWidth:d,quadrantTop:f,quadrantWidth:p}=a,m=!!this.data.xAxisRightText,g=!!this.data.yAxisTopText,y=[];return this.data.xAxisLeftText&&r&&y.push({text:this.data.xAxisLeftText,fill:this.themeConfig.quadrantXAxisTextFill,x:h+(m?d/2:0),y:t==="top"?this.config.xAxisLabelPadding+s.top:this.config.xAxisLabelPadding+f+u+this.config.quadrantPadding,fontSize:this.config.xAxisLabelFontSize,verticalPos:m?"center":"left",horizontalPos:"top",rotation:0}),this.data.xAxisRightText&&r&&y.push({text:this.data.xAxisRightText,fill:this.themeConfig.quadrantXAxisTextFill,x:h+d+(m?d/2:0),y:t==="top"?this.config.xAxisLabelPadding+s.top:this.config.xAxisLabelPadding+f+u+this.config.quadrantPadding,fontSize:this.config.xAxisLabelFontSize,verticalPos:m?"center":"left",horizontalPos:"top",rotation:0}),this.data.yAxisBottomText&&n&&y.push({text:this.data.yAxisBottomText,fill:this.themeConfig.quadrantYAxisTextFill,x:this.config.yAxisPosition==="left"?this.config.yAxisLabelPadding:this.config.yAxisLabelPadding+h+p+this.config.quadrantPadding,y:f+u-(g?l/2:0),fontSize:this.config.yAxisLabelFontSize,verticalPos:g?"center":"left",horizontalPos:"top",rotation:-90}),this.data.yAxisTopText&&n&&y.push({text:this.data.yAxisTopText,fill:this.themeConfig.quadrantYAxisTextFill,x:this.config.yAxisPosition==="left"?this.config.yAxisLabelPadding:this.config.yAxisLabelPadding+h+p+this.config.quadrantPadding,y:f+l-(g?l/2:0),fontSize:this.config.yAxisLabelFontSize,verticalPos:g?"center":"left",horizontalPos:"top",rotation:-90}),y}getQuadrants(t){let{quadrantSpace:r}=t,{quadrantHalfHeight:n,quadrantLeft:i,quadrantHalfWidth:a,quadrantTop:s}=r,l=[{text:{text:this.data.quadrant1Text,fill:this.themeConfig.quadrant1TextFill,x:0,y:0,fontSize:this.config.quadrantLabelFontSize,verticalPos:"center",horizontalPos:"middle",rotation:0},x:i+a,y:s,width:a,height:n,fill:this.themeConfig.quadrant1Fill},{text:{text:this.data.quadrant2Text,fill:this.themeConfig.quadrant2TextFill,x:0,y:0,fontSize:this.config.quadrantLabelFontSize,verticalPos:"center",horizontalPos:"middle",rotation:0},x:i,y:s,width:a,height:n,fill:this.themeConfig.quadrant2Fill},{text:{text:this.data.quadrant3Text,fill:this.themeConfig.quadrant3TextFill,x:0,y:0,fontSize:this.config.quadrantLabelFontSize,verticalPos:"center",horizontalPos:"middle",rotation:0},x:i,y:s+n,width:a,height:n,fill:this.themeConfig.quadrant3Fill},{text:{text:this.data.quadrant4Text,fill:this.themeConfig.quadrant4TextFill,x:0,y:0,fontSize:this.config.quadrantLabelFontSize,verticalPos:"center",horizontalPos:"middle",rotation:0},x:i+a,y:s+n,width:a,height:n,fill:this.themeConfig.quadrant4Fill}];for(let u of l)u.text.x=u.x+u.width/2,this.data.points.length===0?(u.text.y=u.y+u.height/2,u.text.horizontalPos="middle"):(u.text.y=u.y+this.config.quadrantTextTopPadding,u.text.horizontalPos="top");return l}getQuadrantPoints(t){let{quadrantSpace:r}=t,{quadrantHeight:n,quadrantLeft:i,quadrantTop:a,quadrantWidth:s}=r,l=Kl().domain([0,1]).range([i,s+i]),u=Kl().domain([0,1]).range([n+a,a]);return this.data.points.map(d=>{let f=this.classes.get(d.className);return f&&(d={...f,...d}),{x:l(d.x),y:u(d.y),fill:d.color??this.themeConfig.quadrantPointFill,radius:d.radius??this.config.pointRadius,text:{text:d.text,fill:this.themeConfig.quadrantPointTextFill,x:l(d.x),y:u(d.y)+this.config.pointTextPadding,verticalPos:"center",horizontalPos:"top",fontSize:this.config.pointLabelFontSize,rotation:0},strokeColor:d.strokeColor??this.themeConfig.quadrantPointFill,strokeWidth:d.strokeWidth??"0px"}})}getBorders(t){let r=this.config.quadrantExternalBorderStrokeWidth/2,{quadrantSpace:n}=t,{quadrantHalfHeight:i,quadrantHeight:a,quadrantLeft:s,quadrantHalfWidth:l,quadrantTop:u,quadrantWidth:h}=n;return[{strokeFill:this.themeConfig.quadrantExternalBorderStrokeFill,strokeWidth:this.config.quadrantExternalBorderStrokeWidth,x1:s-r,y1:u,x2:s+h+r,y2:u},{strokeFill:this.themeConfig.quadrantExternalBorderStrokeFill,strokeWidth:this.config.quadrantExternalBorderStrokeWidth,x1:s+h,y1:u+r,x2:s+h,y2:u+a-r},{strokeFill:this.themeConfig.quadrantExternalBorderStrokeFill,strokeWidth:this.config.quadrantExternalBorderStrokeWidth,x1:s-r,y1:u+a,x2:s+h+r,y2:u+a},{strokeFill:this.themeConfig.quadrantExternalBorderStrokeFill,strokeWidth:this.config.quadrantExternalBorderStrokeWidth,x1:s,y1:u+r,x2:s,y2:u+a-r},{strokeFill:this.themeConfig.quadrantInternalBorderStrokeFill,strokeWidth:this.config.quadrantInternalBorderStrokeWidth,x1:s+l,y1:u+r,x2:s+l,y2:u+a-r},{strokeFill:this.themeConfig.quadrantInternalBorderStrokeFill,strokeWidth:this.config.quadrantInternalBorderStrokeWidth,x1:s+r,y1:u+i,x2:s+h-r,y2:u+i}]}getTitle(t){if(t)return{text:this.data.titleText,fill:this.themeConfig.quadrantTitleFill,fontSize:this.config.titleFontSize,horizontalPos:"top",verticalPos:"center",rotation:0,y:this.config.titlePadding,x:this.config.chartWidth/2}}build(){let t=this.config.showXAxis&&!!(this.data.xAxisLeftText||this.data.xAxisRightText),r=this.config.showYAxis&&!!(this.data.yAxisTopText||this.data.yAxisBottomText),n=this.config.showTitle&&!!this.data.titleText,i=this.data.points.length>0?"bottom":this.config.xAxisPosition,a=this.calculateSpace(i,t,r,n);return{points:this.getQuadrantPoints(a),quadrants:this.getQuadrants(a),axisLabels:this.getAxisLabels(i,t,r,a),borderLines:this.getBorders(a),title:this.getTitle(n)}}}});function YU(e){return!/^#?([\dA-Fa-f]{6}|[\dA-Fa-f]{3})$/.test(e)}function n8e(e){return!/^\d+$/.test(e)}function i8e(e){return!/^\d+px$/.test(e)}var k0,a8e=F(()=>{"use strict";k0=class extends Error{static{o(this,"InvalidStyleError")}constructor(t,r,n){super(`value for ${t} ${r} is invalid, please use a valid ${n}`),this.name="InvalidStyleError"}};o(YU,"validateHexCode");o(n8e,"validateNumber");o(i8e,"validateSizeInPixels")});function ud(e){return mr(e.trim(),Ae())}function sAt(e){Za.setData({quadrant1Text:ud(e.text)})}function oAt(e){Za.setData({quadrant2Text:ud(e.text)})}function lAt(e){Za.setData({quadrant3Text:ud(e.text)})}function cAt(e){Za.setData({quadrant4Text:ud(e.text)})}function uAt(e){Za.setData({xAxisLeftText:ud(e.text)})}function hAt(e){Za.setData({xAxisRightText:ud(e.text)})}function dAt(e){Za.setData({yAxisTopText:ud(e.text)})}function fAt(e){Za.setData({yAxisBottomText:ud(e.text)})}function jU(e){let t={};for(let r of e){let[n,i]=r.trim().split(/\s*:\s*/);if(n==="radius"){if(n8e(i))throw new k0(n,i,"number");t.radius=parseInt(i)}else if(n==="color"){if(YU(i))throw new k0(n,i,"hex code");t.color=i}else if(n==="stroke-color"){if(YU(i))throw new k0(n,i,"hex code");t.strokeColor=i}else if(n==="stroke-width"){if(i8e(i))throw new k0(n,i,"number of pixels (eg. 10px)");t.strokeWidth=i}else throw new Error(`style named ${n} is not supported.`)}return t}function pAt(e,t,r,n,i){let a=jU(i);Za.addPoints([{x:r,y:n,text:ud(e.text),className:t,...a}])}function mAt(e,t){Za.addClass(e,jU(t))}function gAt(e){Za.setConfig({chartWidth:e})}function yAt(e){Za.setConfig({chartHeight:e})}function vAt(){let e=Ae(),{themeVariables:t,quadrantChart:r}=e;return r&&Za.setConfig(r),Za.setThemeConfig({quadrant1Fill:t.quadrant1Fill,quadrant2Fill:t.quadrant2Fill,quadrant3Fill:t.quadrant3Fill,quadrant4Fill:t.quadrant4Fill,quadrant1TextFill:t.quadrant1TextFill,quadrant2TextFill:t.quadrant2TextFill,quadrant3TextFill:t.quadrant3TextFill,quadrant4TextFill:t.quadrant4TextFill,quadrantPointFill:t.quadrantPointFill,quadrantPointTextFill:t.quadrantPointTextFill,quadrantXAxisTextFill:t.quadrantXAxisTextFill,quadrantYAxisTextFill:t.quadrantYAxisTextFill,quadrantExternalBorderStrokeFill:t.quadrantExternalBorderStrokeFill,quadrantInternalBorderStrokeFill:t.quadrantInternalBorderStrokeFill,quadrantTitleFill:t.quadrantTitleFill}),Za.setData({titleText:Lr()}),Za.build()}var Za,xAt,s8e,o8e=F(()=>{"use strict";Xt();Vr();Nn();r8e();a8e();o(ud,"textSanitizer");Za=new uL;o(sAt,"setQuadrant1Text");o(oAt,"setQuadrant2Text");o(lAt,"setQuadrant3Text");o(cAt,"setQuadrant4Text");o(uAt,"setXAxisLeftText");o(hAt,"setXAxisRightText");o(dAt,"setYAxisTopText");o(fAt,"setYAxisBottomText");o(jU,"parseStyles");o(pAt,"addPoint");o(mAt,"addClass");o(gAt,"setWidth");o(yAt,"setHeight");o(vAt,"getQuadrantData");xAt=o(function(){Za.clear(),yr()},"clear"),s8e={setWidth:gAt,setHeight:yAt,setQuadrant1Text:sAt,setQuadrant2Text:oAt,setQuadrant3Text:lAt,setQuadrant4Text:cAt,setXAxisLeftText:uAt,setXAxisRightText:hAt,setYAxisTopText:dAt,setYAxisBottomText:fAt,parseStyles:jU,addPoint:pAt,addClass:mAt,getQuadrantData:vAt,clear:xAt,setAccTitle:kr,getAccTitle:Ar,setDiagramTitle:Or,getDiagramTitle:Lr,getAccDescription:_r,setAccDescription:Rr}});var bAt,l8e,c8e=F(()=>{"use strict";$r();Xt();vt();$n();bAt=o((e,t,r,n)=>{function i(R){return R==="top"?"hanging":"middle"}o(i,"getDominantBaseLine");function a(R){return R==="left"?"start":"middle"}o(a,"getTextAnchor");function s(R){return`translate(${R.x}, ${R.y}) rotate(${R.rotation||0})`}o(s,"getTransformation");let l=Ae();Z.debug(`Rendering quadrant chart +`+e);let u=l.securityLevel,h;u==="sandbox"&&(h=et("#i"+t));let f=(u==="sandbox"?et(h.nodes()[0].contentDocument.body):et("body")).select(`[id="${t}"]`),p=f.append("g").attr("class","main"),m=l.quadrantChart?.chartWidth??500,g=l.quadrantChart?.chartHeight??500;Wr(f,g,m,l.quadrantChart?.useMaxWidth??!0),f.attr("viewBox","0 0 "+m+" "+g),n.db.setHeight(g),n.db.setWidth(m);let y=n.db.getQuadrantData(),v=p.append("g").attr("class","quadrants"),x=p.append("g").attr("class","border"),b=p.append("g").attr("class","data-points"),T=p.append("g").attr("class","labels"),k=p.append("g").attr("class","title");y.title&&k.append("text").attr("x",0).attr("y",0).attr("fill",y.title.fill).attr("font-size",y.title.fontSize).attr("dominant-baseline",i(y.title.horizontalPos)).attr("text-anchor",a(y.title.verticalPos)).attr("transform",s(y.title)).text(y.title.text),y.borderLines&&x.selectAll("line").data(y.borderLines).enter().append("line").attr("x1",R=>R.x1).attr("y1",R=>R.y1).attr("x2",R=>R.x2).attr("y2",R=>R.y2).style("stroke",R=>R.strokeFill).style("stroke-width",R=>R.strokeWidth);let C=v.selectAll("g.quadrant").data(y.quadrants).enter().append("g").attr("class","quadrant");C.append("rect").attr("x",R=>R.x).attr("y",R=>R.y).attr("width",R=>R.width).attr("height",R=>R.height).attr("fill",R=>R.fill),C.append("text").attr("x",0).attr("y",0).attr("fill",R=>R.text.fill).attr("font-size",R=>R.text.fontSize).attr("dominant-baseline",R=>i(R.text.horizontalPos)).attr("text-anchor",R=>a(R.text.verticalPos)).attr("transform",R=>s(R.text)).text(R=>R.text.text),T.selectAll("g.label").data(y.axisLabels).enter().append("g").attr("class","label").append("text").attr("x",0).attr("y",0).text(R=>R.text).attr("fill",R=>R.fill).attr("font-size",R=>R.fontSize).attr("dominant-baseline",R=>i(R.horizontalPos)).attr("text-anchor",R=>a(R.verticalPos)).attr("transform",R=>s(R));let S=b.selectAll("g.data-point").data(y.points).enter().append("g").attr("class","data-point");S.append("circle").attr("cx",R=>R.x).attr("cy",R=>R.y).attr("r",R=>R.radius).attr("fill",R=>R.fill).attr("stroke",R=>R.strokeColor).attr("stroke-width",R=>R.strokeWidth),S.append("text").attr("x",0).attr("y",0).text(R=>R.text.text).attr("fill",R=>R.text.fill).attr("font-size",R=>R.text.fontSize).attr("dominant-baseline",R=>i(R.text.horizontalPos)).attr("text-anchor",R=>a(R.text.verticalPos)).attr("transform",R=>s(R.text))},"draw"),l8e={draw:bAt}});var u8e={};ir(u8e,{diagram:()=>TAt});var TAt,h8e=F(()=>{"use strict";t8e();o8e();c8e();TAt={parser:e8e,db:s8e,renderer:l8e,styles:o(()=>"","styles")}});var XU,p8e,m8e=F(()=>{"use strict";XU=(function(){var e=o(function(P,B,O,$){for(O=O||{},$=P.length;$--;O[P[$]]=B);return O},"o"),t=[1,10,12,14,16,18,19,21,23],r=[2,6],n=[1,3],i=[1,5],a=[1,6],s=[1,7],l=[1,5,10,12,14,16,18,19,21,23,36,37,38],u=[1,25],h=[1,26],d=[1,28],f=[1,29],p=[1,30],m=[1,31],g=[1,32],y=[1,33],v=[1,34],x=[1,35],b=[1,36],T=[1,37],k=[1,43],C=[1,42],w=[1,47],S=[1,50],R=[1,10,12,14,16,18,19,21,23,36,37,38],L=[1,10,12,14,16,18,19,21,23,24,26,28,29,36,37,38],N=[1,10,12,14,16,18,19,21,23,24,26,28,29,36,37,38,42,43,44,45,46,47,48,49,50,51],I=[1,65],_=[26,28],A={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,eol:4,XYCHART:5,chartConfig:6,document:7,CHART_ORIENTATION:8,statement:9,title:10,text:11,X_AXIS:12,parseXAxis:13,Y_AXIS:14,parseYAxis:15,LINE:16,plotData:17,BAR:18,acc_title:19,acc_title_value:20,acc_descr:21,acc_descr_value:22,acc_descr_multiline_value:23,SQUARE_BRACES_START:24,dataPoints:25,SQUARE_BRACES_END:26,dataPoint:27,COMMA:28,NUMBER_WITH_DECIMAL:29,STR:30,xAxisData:31,bandData:32,ARROW_DELIMITER:33,commaSeparatedTexts:34,yAxisData:35,NEWLINE:36,SEMI:37,EOF:38,alphaNum:39,MD_STR:40,alphaNumToken:41,AMP:42,NUM:43,ALPHA:44,PLUS:45,EQUALS:46,MULT:47,DOT:48,BRKT:49,MINUS:50,UNDERSCORE:51,$accept:0,$end:1},terminals_:{2:"error",5:"XYCHART",8:"CHART_ORIENTATION",10:"title",12:"X_AXIS",14:"Y_AXIS",16:"LINE",18:"BAR",19:"acc_title",20:"acc_title_value",21:"acc_descr",22:"acc_descr_value",23:"acc_descr_multiline_value",24:"SQUARE_BRACES_START",26:"SQUARE_BRACES_END",28:"COMMA",29:"NUMBER_WITH_DECIMAL",30:"STR",33:"ARROW_DELIMITER",36:"NEWLINE",37:"SEMI",38:"EOF",40:"MD_STR",42:"AMP",43:"NUM",44:"ALPHA",45:"PLUS",46:"EQUALS",47:"MULT",48:"DOT",49:"BRKT",50:"MINUS",51:"UNDERSCORE"},productions_:[0,[3,2],[3,3],[3,2],[3,1],[6,1],[7,0],[7,2],[9,2],[9,2],[9,2],[9,2],[9,2],[9,3],[9,2],[9,3],[9,2],[9,2],[9,1],[17,3],[25,3],[25,1],[27,2],[27,1],[13,1],[13,2],[13,1],[31,1],[31,3],[32,3],[34,3],[34,1],[15,1],[15,2],[15,1],[35,3],[4,1],[4,1],[4,1],[11,1],[11,1],[11,1],[39,1],[39,2],[41,1],[41,1],[41,1],[41,1],[41,1],[41,1],[41,1],[41,1],[41,1],[41,1]],performAction:o(function(B,O,$,V,G,z,W){var H=z.length-1;switch(G){case 5:V.setOrientation(z[H]);break;case 9:V.setDiagramTitle(z[H].text.trim());break;case 12:V.setLineData({text:"",type:"text"},z[H]);break;case 13:V.setLineData(z[H-1],z[H]);break;case 14:V.setBarData({text:"",type:"text"},z[H]);break;case 15:V.setBarData(z[H-1],z[H]);break;case 16:this.$=z[H].trim(),V.setAccTitle(this.$);break;case 17:case 18:this.$=z[H].trim(),V.setAccDescription(this.$);break;case 19:this.$=z[H-1];break;case 20:case 30:this.$=[z[H-2],...z[H]];break;case 21:case 31:this.$=[z[H]];break;case 22:this.$={value:Number(z[H-1]),label:z[H]};break;case 23:this.$={value:Number(z[H]),label:""};break;case 24:V.setXAxisTitle(z[H]);break;case 25:V.setXAxisTitle(z[H-1]);break;case 26:V.setXAxisTitle({type:"text",text:""});break;case 27:V.setXAxisBand(z[H]);break;case 28:V.setXAxisRangeData(Number(z[H-2]),Number(z[H]));break;case 29:this.$=z[H-1];break;case 32:V.setYAxisTitle(z[H]);break;case 33:V.setYAxisTitle(z[H-1]);break;case 34:V.setYAxisTitle({type:"text",text:""});break;case 35:V.setYAxisRangeData(Number(z[H-2]),Number(z[H]));break;case 39:this.$={text:z[H],type:"text"};break;case 40:this.$={text:z[H],type:"text"};break;case 41:this.$={text:z[H],type:"markdown"};break;case 42:this.$=z[H];break;case 43:this.$=z[H-1]+""+z[H];break}},"anonymous"),table:[e(t,r,{3:1,4:2,7:4,5:n,36:i,37:a,38:s}),{1:[3]},e(t,r,{4:2,7:4,3:8,5:n,36:i,37:a,38:s}),e(t,r,{4:2,7:4,6:9,3:10,5:n,8:[1,11],36:i,37:a,38:s}),{1:[2,4],9:12,10:[1,13],12:[1,14],14:[1,15],16:[1,16],18:[1,17],19:[1,18],21:[1,19],23:[1,20]},e(l,[2,36]),e(l,[2,37]),e(l,[2,38]),{1:[2,1]},e(t,r,{4:2,7:4,3:21,5:n,36:i,37:a,38:s}),{1:[2,3]},e(l,[2,5]),e(t,[2,7],{4:22,36:i,37:a,38:s}),{11:23,30:u,39:24,40:h,41:27,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T},{11:39,13:38,24:k,29:C,30:u,31:40,32:41,39:24,40:h,41:27,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T},{11:45,15:44,29:w,30:u,35:46,39:24,40:h,41:27,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T},{11:49,17:48,24:S,30:u,39:24,40:h,41:27,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T},{11:52,17:51,24:S,30:u,39:24,40:h,41:27,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T},{20:[1,53]},{22:[1,54]},e(R,[2,18]),{1:[2,2]},e(R,[2,8]),e(R,[2,9]),e(L,[2,39],{41:55,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T}),e(L,[2,40]),e(L,[2,41]),e(N,[2,42]),e(N,[2,44]),e(N,[2,45]),e(N,[2,46]),e(N,[2,47]),e(N,[2,48]),e(N,[2,49]),e(N,[2,50]),e(N,[2,51]),e(N,[2,52]),e(N,[2,53]),e(R,[2,10]),e(R,[2,24],{32:41,31:56,24:k,29:C}),e(R,[2,26]),e(R,[2,27]),{33:[1,57]},{11:59,30:u,34:58,39:24,40:h,41:27,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T},e(R,[2,11]),e(R,[2,32],{35:60,29:w}),e(R,[2,34]),{33:[1,61]},e(R,[2,12]),{17:62,24:S},{25:63,27:64,29:I},e(R,[2,14]),{17:66,24:S},e(R,[2,16]),e(R,[2,17]),e(N,[2,43]),e(R,[2,25]),{29:[1,67]},{26:[1,68]},{26:[2,31],28:[1,69]},e(R,[2,33]),{29:[1,70]},e(R,[2,13]),{26:[1,71]},{26:[2,21],28:[1,72]},e(_,[2,23],{30:[1,73]}),e(R,[2,15]),e(R,[2,28]),e(R,[2,29]),{11:59,30:u,34:74,39:24,40:h,41:27,42:d,43:f,44:p,45:m,46:g,47:y,48:v,49:x,50:b,51:T},e(R,[2,35]),e(R,[2,19]),{25:75,27:64,29:I},e(_,[2,22]),{26:[2,30]},{26:[2,20]}],defaultActions:{8:[2,1],10:[2,3],21:[2,2],74:[2,30],75:[2,20]},parseError:o(function(B,O){if(O.recoverable)this.trace(B);else{var $=new Error(B);throw $.hash=O,$}},"parseError"),parse:o(function(B){var O=this,$=[0],V=[],G=[null],z=[],W=this.table,H="",j=0,Q=0,U=0,oe=2,te=1,le=z.slice.call(arguments,1),ie=Object.create(this.lexer),ae={yy:{}};for(var Re in this.yy)Object.prototype.hasOwnProperty.call(this.yy,Re)&&(ae.yy[Re]=this.yy[Re]);ie.setInput(B,ae.yy),ae.yy.lexer=ie,ae.yy.parser=this,typeof ie.yylloc>"u"&&(ie.yylloc={});var be=ie.yylloc;z.push(be);var Pe=ie.options&&ie.options.ranges;typeof ae.yy.parseError=="function"?this.parseError=ae.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function Ge(ne){$.length=$.length-2*ne,G.length=G.length-ne,z.length=z.length-ne}o(Ge,"popStack");function Oe(){var ne;return ne=V.pop()||ie.lex()||te,typeof ne!="number"&&(ne instanceof Array&&(V=ne,ne=V.pop()),ne=O.symbols_[ne]||ne),ne}o(Oe,"lex");for(var ue,ye,ke,ce,re,J,se={},ge,Te,we,Me;;){if(ke=$[$.length-1],this.defaultActions[ke]?ce=this.defaultActions[ke]:((ue===null||typeof ue>"u")&&(ue=Oe()),ce=W[ke]&&W[ke][ue]),typeof ce>"u"||!ce.length||!ce[0]){var ve="";Me=[];for(ge in W[ke])this.terminals_[ge]&&ge>oe&&Me.push("'"+this.terminals_[ge]+"'");ie.showPosition?ve="Parse error on line "+(j+1)+`: +`+ie.showPosition()+` +Expecting `+Me.join(", ")+", got '"+(this.terminals_[ue]||ue)+"'":ve="Parse error on line "+(j+1)+": Unexpected "+(ue==te?"end of input":"'"+(this.terminals_[ue]||ue)+"'"),this.parseError(ve,{text:ie.match,token:this.terminals_[ue]||ue,line:ie.yylineno,loc:be,expected:Me})}if(ce[0]instanceof Array&&ce.length>1)throw new Error("Parse Error: multiple actions possible at state: "+ke+", token: "+ue);switch(ce[0]){case 1:$.push(ue),G.push(ie.yytext),z.push(ie.yylloc),$.push(ce[1]),ue=null,ye?(ue=ye,ye=null):(Q=ie.yyleng,H=ie.yytext,j=ie.yylineno,be=ie.yylloc,U>0&&U--);break;case 2:if(Te=this.productions_[ce[1]][1],se.$=G[G.length-Te],se._$={first_line:z[z.length-(Te||1)].first_line,last_line:z[z.length-1].last_line,first_column:z[z.length-(Te||1)].first_column,last_column:z[z.length-1].last_column},Pe&&(se._$.range=[z[z.length-(Te||1)].range[0],z[z.length-1].range[1]]),J=this.performAction.apply(se,[H,Q,j,ae.yy,ce[1],G,z].concat(le)),typeof J<"u")return J;Te&&($=$.slice(0,-1*Te*2),G=G.slice(0,-1*Te),z=z.slice(0,-1*Te)),$.push(this.productions_[ce[1]][0]),G.push(se.$),z.push(se._$),we=W[$[$.length-2]][$[$.length-1]],$.push(we);break;case 3:return!0}}return!0},"parse")},M=(function(){var P={EOF:1,parseError:o(function(O,$){if(this.yy.parser)this.yy.parser.parseError(O,$);else throw new Error(O)},"parseError"),setInput:o(function(B,O){return this.yy=O||this.yy||{},this._input=B,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var B=this._input[0];this.yytext+=B,this.yyleng++,this.offset++,this.match+=B,this.matched+=B;var O=B.match(/(?:\r\n?|\n).*/g);return O?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),B},"input"),unput:o(function(B){var O=B.length,$=B.split(/(?:\r\n?|\n)/g);this._input=B+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-O),this.offset-=O;var V=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),$.length-1&&(this.yylineno-=$.length-1);var G=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:$?($.length===V.length?this.yylloc.first_column:0)+V[V.length-$.length].length-$[0].length:this.yylloc.first_column-O},this.options.ranges&&(this.yylloc.range=[G[0],G[0]+this.yyleng-O]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(B){this.unput(this.match.slice(B))},"less"),pastInput:o(function(){var B=this.matched.substr(0,this.matched.length-this.match.length);return(B.length>20?"...":"")+B.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var B=this.match;return B.length<20&&(B+=this._input.substr(0,20-B.length)),(B.substr(0,20)+(B.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var B=this.pastInput(),O=new Array(B.length+1).join("-");return B+this.upcomingInput()+` +`+O+"^"},"showPosition"),test_match:o(function(B,O){var $,V,G;if(this.options.backtrack_lexer&&(G={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(G.yylloc.range=this.yylloc.range.slice(0))),V=B[0].match(/(?:\r\n?|\n).*/g),V&&(this.yylineno+=V.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:V?V[V.length-1].length-V[V.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+B[0].length},this.yytext+=B[0],this.match+=B[0],this.matches=B,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(B[0].length),this.matched+=B[0],$=this.performAction.call(this,this.yy,this,O,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),$)return $;if(this._backtrack){for(var z in G)this[z]=G[z];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var B,O,$,V;this._more||(this.yytext="",this.match="");for(var G=this._currentRules(),z=0;zO[0].length)){if(O=$,V=z,this.options.backtrack_lexer){if(B=this.test_match($,G[z]),B!==!1)return B;if(this._backtrack){O=!1;continue}else return!1}else if(!this.options.flex)break}return O?(B=this.test_match(O,G[V]),B!==!1?B:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var O=this.next();return O||this.lex()},"lex"),begin:o(function(O){this.conditionStack.push(O)},"begin"),popState:o(function(){var O=this.conditionStack.length-1;return O>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(O){return O=this.conditionStack.length-1-Math.abs(O||0),O>=0?this.conditionStack[O]:"INITIAL"},"topState"),pushState:o(function(O){this.begin(O)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:o(function(O,$,V,G){var z=G;switch(V){case 0:break;case 1:break;case 2:return this.popState(),36;break;case 3:return this.popState(),36;break;case 4:return 36;case 5:break;case 6:return 10;case 7:return this.pushState("acc_title"),19;break;case 8:return this.popState(),"acc_title_value";break;case 9:return this.pushState("acc_descr"),21;break;case 10:return this.popState(),"acc_descr_value";break;case 11:this.pushState("acc_descr_multiline");break;case 12:this.popState();break;case 13:return"acc_descr_multiline_value";case 14:return 5;case 15:return 5;case 16:return 8;case 17:return this.pushState("axis_data"),"X_AXIS";break;case 18:return this.pushState("axis_data"),"Y_AXIS";break;case 19:return this.pushState("axis_band_data"),24;break;case 20:return 33;case 21:return this.pushState("data"),16;break;case 22:return this.pushState("data"),18;break;case 23:return this.pushState("data_inner"),24;break;case 24:return 29;case 25:return this.popState(),26;break;case 26:this.popState();break;case 27:this.pushState("string");break;case 28:this.popState();break;case 29:return"STR";case 30:return 24;case 31:return 26;case 32:return 44;case 33:return"COLON";case 34:return 45;case 35:return 28;case 36:return 46;case 37:return 47;case 38:return 49;case 39:return 51;case 40:return 48;case 41:return 42;case 42:return 50;case 43:return 43;case 44:break;case 45:return 37;case 46:return 38}},"anonymous"),rules:[/^(?:%%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:(\r?\n))/i,/^(?:(\r?\n))/i,/^(?:[\n\r]+)/i,/^(?:%%[^\n]*)/i,/^(?:title\b)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:\})/i,/^(?:[^\}]*)/i,/^(?:xychart-beta\b)/i,/^(?:xychart\b)/i,/^(?:(?:vertical|horizontal))/i,/^(?:x-axis\b)/i,/^(?:y-axis\b)/i,/^(?:\[)/i,/^(?:-->)/i,/^(?:line\b)/i,/^(?:bar\b)/i,/^(?:\[)/i,/^(?:[+-]?(?:\d+(?:\.\d+)?|\.\d+))/i,/^(?:\])/i,/^(?:(?:`\) \{ this\.pushState\(md_string\); \}\n\(\?:\(\?!`"\)\.\)\+ \{ return MD_STR; \}\n\(\?:`))/i,/^(?:["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:\[)/i,/^(?:\])/i,/^(?:[A-Za-z]+)/i,/^(?::)/i,/^(?:\+)/i,/^(?:,)/i,/^(?:=)/i,/^(?:\*)/i,/^(?:#)/i,/^(?:[\_])/i,/^(?:\.)/i,/^(?:&)/i,/^(?:-)/i,/^(?:[0-9]+)/i,/^(?:\s+)/i,/^(?:;)/i,/^(?:$)/i],conditions:{data_inner:{rules:[0,1,4,5,6,7,9,11,14,15,16,17,18,21,22,24,25,26,27,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46],inclusive:!0},data:{rules:[0,1,3,4,5,6,7,9,11,14,15,16,17,18,21,22,23,26,27,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46],inclusive:!0},axis_band_data:{rules:[0,1,4,5,6,7,9,11,14,15,16,17,18,21,22,25,26,27,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46],inclusive:!0},axis_data:{rules:[0,1,2,4,5,6,7,9,11,14,15,16,17,18,19,20,21,22,24,26,27,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46],inclusive:!0},acc_descr_multiline:{rules:[12,13],inclusive:!1},acc_descr:{rules:[10],inclusive:!1},acc_title:{rules:[8],inclusive:!1},title:{rules:[],inclusive:!1},md_string:{rules:[],inclusive:!1},string:{rules:[28,29],inclusive:!1},INITIAL:{rules:[0,1,4,5,6,7,9,11,14,15,16,17,18,21,22,26,27,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46],inclusive:!0}}};return P})();A.lexer=M;function D(){this.yy={}}return o(D,"Parser"),D.prototype=A,A.Parser=D,new D})();XU.parser=XU;p8e=XU});function KU(e){return e.type==="bar"}function Ow(e){return e.type==="band"}function Qv(e){return e.type==="linear"}var hL=F(()=>{"use strict";o(KU,"isBarPlot");o(Ow,"isBandAxisData");o(Qv,"isLinearAxisData")});var Jv,ZU=F(()=>{"use strict";Ls();Jv=class{constructor(t){this.parentGroup=t}static{o(this,"TextDimensionCalculatorWithFont")}getMaxDimension(t,r){if(!this.parentGroup)return{width:t.reduce((a,s)=>Math.max(s.length,a),0)*r,height:r};let n={width:0,height:0},i=this.parentGroup.append("g").attr("visibility","hidden").attr("font-size",r);for(let a of t){let s=Pse(i,1,a),l=s?s.width:a.length*r,u=s?s.height:r;n.width=Math.max(n.width,l),n.height=Math.max(n.height,u)}return i.remove(),n}}});var ex,QU=F(()=>{"use strict";ex=class{constructor(t,r,n,i){this.axisConfig=t;this.title=r;this.textDimensionCalculator=n;this.axisThemeConfig=i;this.boundingRect={x:0,y:0,width:0,height:0};this.axisPosition="left";this.showTitle=!1;this.showLabel=!1;this.showTick=!1;this.showAxisLine=!1;this.outerPadding=0;this.titleTextHeight=0;this.labelTextHeight=0;this.normalizedLabelRotationInRad=0;this.range=[0,10],this.boundingRect={x:0,y:0,width:0,height:0},this.axisPosition="left",this.normalizedLabelRotationInRad=this.axisConfig.labelRotation>=-90&&this.axisConfig.labelRotation<=90?this.axisConfig.labelRotation*Math.PI/180:0}static{o(this,"BaseAxis")}setRange(t){this.range=t,this.axisPosition==="left"||this.axisPosition==="right"?this.boundingRect.height=t[1]-t[0]:this.boundingRect.width=t[1]-t[0],this.recalculateScale()}getRange(){return[this.range[0]+this.outerPadding,this.range[1]-this.outerPadding]}setAxisPosition(t){this.axisPosition=t,this.setRange(this.range)}getTickDistance(){let t=this.getRange();return Math.abs(t[0]-t[1])/this.getTickValues().length}getAxisOuterPadding(){return this.outerPadding}getLabelDimension(){return this.textDimensionCalculator.getMaxDimension(this.getTickValues().map(t=>t.toString()),this.axisConfig.labelFontSize)}recalculateOuterPaddingToDrawBar(){.7*this.getTickDistance()>this.outerPadding*2&&(this.outerPadding=Math.floor(.7*this.getTickDistance()/2)),this.recalculateScale()}calculateSpaceIfDrawnHorizontally(t){let r=t.height;if(this.axisConfig.showAxisLine&&r>this.axisConfig.axisLineWidth&&(r-=this.axisConfig.axisLineWidth,this.showAxisLine=!0),this.axisConfig.showLabel){let n=this.getLabelDimension(),i=.2*t.width;this.outerPadding=Math.min(n.width/2,i);let a=n.height;this.axisPosition==="bottom"&&this.normalizedLabelRotationInRad!==0&&(a=Math.max(a,Math.abs(Math.sin(this.normalizedLabelRotationInRad)*n.width)+Math.abs(Math.cos(this.normalizedLabelRotationInRad)*n.height))),a+=this.axisConfig.labelPadding*2,this.labelTextHeight=n.height,a<=r&&(r-=a,this.showLabel=!0)}if(this.axisConfig.showTick&&r>=this.axisConfig.tickLength&&(this.showTick=!0,r-=this.axisConfig.tickLength),this.axisConfig.showTitle&&this.title){let n=this.textDimensionCalculator.getMaxDimension([this.title],this.axisConfig.titleFontSize),i=n.height+this.axisConfig.titlePadding*2;this.titleTextHeight=n.height,i<=r&&(r-=i,this.showTitle=!0)}this.boundingRect.width=t.width,this.boundingRect.height=t.height-r}calculateSpaceIfDrawnVertical(t){let r=t.width;if(this.axisConfig.showAxisLine&&r>this.axisConfig.axisLineWidth&&(r-=this.axisConfig.axisLineWidth,this.showAxisLine=!0),this.axisConfig.showLabel){let n=this.getLabelDimension(),i=.2*t.height;this.outerPadding=Math.min(n.height/2,i);let a=n.width+this.axisConfig.labelPadding*2;a<=r&&(r-=a,this.showLabel=!0)}if(this.axisConfig.showTick&&r>=this.axisConfig.tickLength&&(this.showTick=!0,r-=this.axisConfig.tickLength),this.axisConfig.showTitle&&this.title){let n=this.textDimensionCalculator.getMaxDimension([this.title],this.axisConfig.titleFontSize),i=n.height+this.axisConfig.titlePadding*2;this.titleTextHeight=n.height,i<=r&&(r-=i,this.showTitle=!0)}this.boundingRect.width=t.width-r,this.boundingRect.height=t.height}calculateSpace(t){return this.axisPosition==="left"||this.axisPosition==="right"?this.calculateSpaceIfDrawnVertical(t):this.calculateSpaceIfDrawnHorizontally(t),this.recalculateScale(),{width:this.boundingRect.width,height:this.boundingRect.height}}setBoundingBoxXY(t){this.boundingRect.x=t.x,this.boundingRect.y=t.y}calculateOffsetByRotation(t){let r=this.normalizedLabelRotationInRad;return r===0?0:Math.sin(r)*this.getLabelDimension()[t]/2}getDrawableElementsForLeftAxis(){let t=[];if(this.showAxisLine){let r=this.boundingRect.x+this.boundingRect.width-this.axisConfig.axisLineWidth/2;t.push({type:"path",groupTexts:["left-axis","axisl-line"],data:[{path:`M ${r},${this.boundingRect.y} L ${r},${this.boundingRect.y+this.boundingRect.height} `,strokeFill:this.axisThemeConfig.axisLineColor,strokeWidth:this.axisConfig.axisLineWidth}]})}if(this.showLabel&&t.push({type:"text",groupTexts:["left-axis","label"],data:this.getTickValues().map(r=>({text:r.toString(),x:this.boundingRect.x+this.boundingRect.width-(this.showLabel?this.axisConfig.labelPadding:0)-(this.showTick?this.axisConfig.tickLength:0)-(this.showAxisLine?this.axisConfig.axisLineWidth:0),y:this.getScaleValue(r),fill:this.axisThemeConfig.labelColor,fontSize:this.axisConfig.labelFontSize,rotation:0,verticalPos:"middle",horizontalPos:"right"}))}),this.showTick){let r=this.boundingRect.x+this.boundingRect.width-(this.showAxisLine?this.axisConfig.axisLineWidth:0);t.push({type:"path",groupTexts:["left-axis","ticks"],data:this.getTickValues().map(n=>({path:`M ${r},${this.getScaleValue(n)} L ${r-this.axisConfig.tickLength},${this.getScaleValue(n)}`,strokeFill:this.axisThemeConfig.tickColor,strokeWidth:this.axisConfig.tickWidth}))})}return this.showTitle&&t.push({type:"text",groupTexts:["left-axis","title"],data:[{text:this.title,x:this.boundingRect.x+this.axisConfig.titlePadding,y:this.boundingRect.y+this.boundingRect.height/2,fill:this.axisThemeConfig.titleColor,fontSize:this.axisConfig.titleFontSize,rotation:270,verticalPos:"top",horizontalPos:"center"}]}),t}getDrawableElementsForBottomAxis(){let t=[];if(this.showAxisLine){let r=this.boundingRect.y+this.axisConfig.axisLineWidth/2;t.push({type:"path",groupTexts:["bottom-axis","axis-line"],data:[{path:`M ${this.boundingRect.x},${r} L ${this.boundingRect.x+this.boundingRect.width},${r}`,strokeFill:this.axisThemeConfig.axisLineColor,strokeWidth:this.axisConfig.axisLineWidth}]})}if(this.showLabel&&t.push({type:"text",groupTexts:["bottom-axis","label"],data:this.getTickValues().map(r=>({text:r.toString(),x:this.getScaleValue(r)+this.calculateOffsetByRotation("height"),y:this.boundingRect.y+this.axisConfig.labelPadding+(this.showTick?this.axisConfig.tickLength:0)+(this.showAxisLine?this.axisConfig.axisLineWidth:0)+Math.abs(this.calculateOffsetByRotation("width")),fill:this.axisThemeConfig.labelColor,fontSize:this.axisConfig.labelFontSize,rotation:this.normalizedLabelRotationInRad*180/Math.PI,verticalPos:"top",horizontalPos:"center"}))}),this.showTick){let r=this.boundingRect.y+(this.showAxisLine?this.axisConfig.axisLineWidth:0);t.push({type:"path",groupTexts:["bottom-axis","ticks"],data:this.getTickValues().map(n=>({path:`M ${this.getScaleValue(n)},${r} L ${this.getScaleValue(n)},${r+this.axisConfig.tickLength}`,strokeFill:this.axisThemeConfig.tickColor,strokeWidth:this.axisConfig.tickWidth}))})}return this.showTitle&&t.push({type:"text",groupTexts:["bottom-axis","title"],data:[{text:this.title,x:this.range[0]+(this.range[1]-this.range[0])/2,y:this.boundingRect.y+this.boundingRect.height-this.axisConfig.titlePadding-this.titleTextHeight,fill:this.axisThemeConfig.titleColor,fontSize:this.axisConfig.titleFontSize,rotation:0,verticalPos:"top",horizontalPos:"center"}]}),t}getDrawableElementsForTopAxis(){let t=[];if(this.showAxisLine){let r=this.boundingRect.y+this.boundingRect.height-this.axisConfig.axisLineWidth/2;t.push({type:"path",groupTexts:["top-axis","axis-line"],data:[{path:`M ${this.boundingRect.x},${r} L ${this.boundingRect.x+this.boundingRect.width},${r}`,strokeFill:this.axisThemeConfig.axisLineColor,strokeWidth:this.axisConfig.axisLineWidth}]})}if(this.showLabel&&t.push({type:"text",groupTexts:["top-axis","label"],data:this.getTickValues().map(r=>({text:r.toString(),x:this.getScaleValue(r),y:this.boundingRect.y+(this.showTitle?this.titleTextHeight+this.axisConfig.titlePadding*2:0)+this.axisConfig.labelPadding,fill:this.axisThemeConfig.labelColor,fontSize:this.axisConfig.labelFontSize,rotation:0,verticalPos:"top",horizontalPos:"center"}))}),this.showTick){let r=this.boundingRect.y;t.push({type:"path",groupTexts:["top-axis","ticks"],data:this.getTickValues().map(n=>({path:`M ${this.getScaleValue(n)},${r+this.boundingRect.height-(this.showAxisLine?this.axisConfig.axisLineWidth:0)} L ${this.getScaleValue(n)},${r+this.boundingRect.height-this.axisConfig.tickLength-(this.showAxisLine?this.axisConfig.axisLineWidth:0)}`,strokeFill:this.axisThemeConfig.tickColor,strokeWidth:this.axisConfig.tickWidth}))})}return this.showTitle&&t.push({type:"text",groupTexts:["top-axis","title"],data:[{text:this.title,x:this.boundingRect.x+this.boundingRect.width/2,y:this.boundingRect.y+this.axisConfig.titlePadding,fill:this.axisThemeConfig.titleColor,fontSize:this.axisConfig.titleFontSize,rotation:0,verticalPos:"top",horizontalPos:"center"}]}),t}getDrawableElements(){if(this.axisPosition==="left")return this.getDrawableElementsForLeftAxis();if(this.axisPosition==="right")throw Error("Drawing of right axis is not implemented");return this.axisPosition==="bottom"?this.getDrawableElementsForBottomAxis():this.axisPosition==="top"?this.getDrawableElementsForTopAxis():[]}}});var dL,g8e=F(()=>{"use strict";$r();vt();QU();dL=class extends ex{static{o(this,"BandAxis")}constructor(t,r,n,i,a){super(t,i,a,r),this.categories=n,this.scale=by().domain(this.categories).range(this.getRange())}setRange(t){super.setRange(t)}recalculateScale(){this.scale=by().domain(this.categories).range(this.getRange()).paddingInner(1).paddingOuter(0).align(.5),Z.trace("BandAxis axis final categories, range: ",this.categories,this.getRange())}getTickValues(){return this.categories}getScaleValue(t){return this.scale(t)??this.getRange()[0]}}});var fL,y8e=F(()=>{"use strict";$r();QU();fL=class extends ex{static{o(this,"LinearAxis")}constructor(t,r,n,i,a){super(t,i,a,r),this.domain=n,this.scale=Kl().domain(this.domain).range(this.getRange())}getTickValues(){return this.scale.ticks()}recalculateScale(){let t=[...this.domain];this.axisPosition==="left"&&t.reverse(),this.scale=Kl().domain(t).range(this.getRange())}getScaleValue(t){return this.scale(t)}}});function JU(e,t,r,n){let i=new Jv(n);return Ow(e)?new dL(t,r,e.categories,e.title,i):new fL(t,r,[e.min,e.max],e.title,i)}var v8e=F(()=>{"use strict";hL();ZU();g8e();y8e();o(JU,"getAxis")});function x8e(e,t,r,n){let i=new Jv(n);return new eY(i,e,t,r)}var eY,b8e=F(()=>{"use strict";ZU();eY=class{constructor(t,r,n,i){this.textDimensionCalculator=t;this.chartConfig=r;this.chartData=n;this.chartThemeConfig=i;this.boundingRect={x:0,y:0,width:0,height:0},this.showChartTitle=!1}static{o(this,"ChartTitle")}setBoundingBoxXY(t){this.boundingRect.x=t.x,this.boundingRect.y=t.y}calculateSpace(t){let r=this.textDimensionCalculator.getMaxDimension([this.chartData.title],this.chartConfig.titleFontSize),n=Math.max(r.width,t.width),i=r.height+2*this.chartConfig.titlePadding;return r.width<=n&&r.height<=i&&this.chartConfig.showTitle&&this.chartData.title&&(this.boundingRect.width=n,this.boundingRect.height=i,this.showChartTitle=!0),{width:this.boundingRect.width,height:this.boundingRect.height}}getDrawableElements(){let t=[];return this.showChartTitle&&t.push({groupTexts:["chart-title"],type:"text",data:[{fontSize:this.chartConfig.titleFontSize,text:this.chartData.title,verticalPos:"middle",horizontalPos:"center",x:this.boundingRect.x+this.boundingRect.width/2,y:this.boundingRect.y+this.boundingRect.height/2,fill:this.chartThemeConfig.titleColor,rotation:0}]}),t}};o(x8e,"getChartTitleComponent")});var pL,T8e=F(()=>{"use strict";$r();pL=class{constructor(t,r,n,i,a){this.plotData=t;this.xAxis=r;this.yAxis=n;this.orientation=i;this.plotIndex=a}static{o(this,"LinePlot")}getDrawableElement(){let t=this.plotData.data.map(i=>[this.xAxis.getScaleValue(i[0]),this.yAxis.getScaleValue(i[1])]),r;if(this.orientation==="horizontal"?r=tc().y(i=>i[0]).x(i=>i[1])(t):r=tc().x(i=>i[0]).y(i=>i[1])(t),!r)return[];let n=[{groupTexts:["plot",`line-plot-${this.plotIndex}`],type:"path",data:[{path:r,strokeFill:this.plotData.strokeFill,strokeWidth:this.plotData.strokeWidth}]}];if(this.plotData.pointLabels&&this.plotData.pointLabels.length>0){let s=[];for(let[l,[u,h]]of t.entries()){let d=this.plotData.pointLabels[l];d&&(this.orientation==="horizontal"?s.push({x:h+10,y:u,text:d,fill:this.plotData.strokeFill,verticalPos:"middle",horizontalPos:"left",fontSize:12,rotation:0}):s.push({x:u,y:h-10,text:d,fill:this.plotData.strokeFill,verticalPos:"middle",horizontalPos:"center",fontSize:12,rotation:0}))}s.length>0&&n.push({groupTexts:["plot",`line-plot-${this.plotIndex}`,"labels"],type:"text",data:s})}return n}}});var mL,C8e=F(()=>{"use strict";mL=class{constructor(t,r,n,i,a,s){this.barData=t;this.boundingRect=r;this.xAxis=n;this.yAxis=i;this.orientation=a;this.plotIndex=s}static{o(this,"BarPlot")}getDrawableElement(){let t=this.barData.data.map(a=>[this.xAxis.getScaleValue(a[0]),this.yAxis.getScaleValue(a[1])]),n=Math.min(this.xAxis.getAxisOuterPadding()*2,this.xAxis.getTickDistance())*(1-.05),i=n/2;return this.orientation==="horizontal"?[{groupTexts:["plot",`bar-plot-${this.plotIndex}`],type:"rect",data:t.map(a=>({x:this.boundingRect.x,y:a[0]-i,height:n,width:a[1]-this.boundingRect.x,fill:this.barData.fill,strokeWidth:0,strokeFill:this.barData.fill}))}]:[{groupTexts:["plot",`bar-plot-${this.plotIndex}`],type:"rect",data:t.map(a=>({x:a[0]-i,y:a[1],width:n,height:this.boundingRect.y+this.boundingRect.height-a[1],fill:this.barData.fill,strokeWidth:0,strokeFill:this.barData.fill}))}]}}});function w8e(e,t,r){return new tY(e,t,r)}var tY,k8e=F(()=>{"use strict";T8e();C8e();tY=class{constructor(t,r,n){this.chartConfig=t;this.chartData=r;this.chartThemeConfig=n;this.boundingRect={x:0,y:0,width:0,height:0}}static{o(this,"BasePlot")}setAxes(t,r){this.xAxis=t,this.yAxis=r}setBoundingBoxXY(t){this.boundingRect.x=t.x,this.boundingRect.y=t.y}calculateSpace(t){return this.boundingRect.width=t.width,this.boundingRect.height=t.height,{width:this.boundingRect.width,height:this.boundingRect.height}}getDrawableElements(){if(!(this.xAxis&&this.yAxis))throw Error("Axes must be passed to render Plots");let t=[];for(let[r,n]of this.chartData.plots.entries())switch(n.type){case"line":{let i=new pL(n,this.xAxis,this.yAxis,this.chartConfig.chartOrientation,r);t.push(...i.getDrawableElement())}break;case"bar":{let i=new mL(n,this.boundingRect,this.xAxis,this.yAxis,this.chartConfig.chartOrientation,r);t.push(...i.getDrawableElement())}break}return t}};o(w8e,"getPlotComponent")});var gL,S8e=F(()=>{"use strict";v8e();b8e();k8e();hL();gL=class{constructor(t,r,n,i){this.chartConfig=t;this.chartData=r;this.componentStore={title:x8e(t,r,n,i),plot:w8e(t,r,n),xAxis:JU(r.xAxis,t.xAxis,{titleColor:n.xAxisTitleColor,labelColor:n.xAxisLabelColor,tickColor:n.xAxisTickColor,axisLineColor:n.xAxisLineColor},i),yAxis:JU(r.yAxis,t.yAxis,{titleColor:n.yAxisTitleColor,labelColor:n.yAxisLabelColor,tickColor:n.yAxisTickColor,axisLineColor:n.yAxisLineColor},i)}}static{o(this,"Orchestrator")}calculateVerticalSpace(){let t=this.chartConfig.width,r=this.chartConfig.height,n=0,i=0,a=Math.floor(t*this.chartConfig.plotReservedSpacePercent/100),s=Math.floor(r*this.chartConfig.plotReservedSpacePercent/100),l=this.componentStore.plot.calculateSpace({width:a,height:s});t-=l.width,r-=l.height,l=this.componentStore.title.calculateSpace({width:this.chartConfig.width,height:r}),i=l.height,r-=l.height,this.componentStore.xAxis.setAxisPosition("bottom"),l=this.componentStore.xAxis.calculateSpace({width:t,height:r}),r-=l.height,this.componentStore.yAxis.setAxisPosition("left"),l=this.componentStore.yAxis.calculateSpace({width:t,height:r}),n=l.width,t-=l.width,t>0&&(a+=t,t=0),r>0&&(s+=r,r=0),this.componentStore.plot.calculateSpace({width:a,height:s}),this.componentStore.plot.setBoundingBoxXY({x:n,y:i}),this.componentStore.xAxis.setRange([n,n+a]),this.componentStore.xAxis.setBoundingBoxXY({x:n,y:i+s}),this.componentStore.yAxis.setRange([i,i+s]),this.componentStore.yAxis.setBoundingBoxXY({x:0,y:i}),this.chartData.plots.some(u=>KU(u))&&this.componentStore.xAxis.recalculateOuterPaddingToDrawBar()}calculateHorizontalSpace(){let t=this.chartConfig.width,r=this.chartConfig.height,n=0,i=0,a=0,s=Math.floor(t*this.chartConfig.plotReservedSpacePercent/100),l=Math.floor(r*this.chartConfig.plotReservedSpacePercent/100),u=this.componentStore.plot.calculateSpace({width:s,height:l});t-=u.width,r-=u.height,u=this.componentStore.title.calculateSpace({width:this.chartConfig.width,height:r}),n=u.height,r-=u.height,this.componentStore.xAxis.setAxisPosition("left"),u=this.componentStore.xAxis.calculateSpace({width:t,height:r}),t-=u.width,i=u.width,this.componentStore.yAxis.setAxisPosition("top"),u=this.componentStore.yAxis.calculateSpace({width:t,height:r}),r-=u.height,a=n+u.height,t>0&&(s+=t,t=0),r>0&&(l+=r,r=0),this.componentStore.plot.calculateSpace({width:s,height:l}),this.componentStore.plot.setBoundingBoxXY({x:i,y:a}),this.componentStore.yAxis.setRange([i,i+s]),this.componentStore.yAxis.setBoundingBoxXY({x:i,y:n}),this.componentStore.xAxis.setRange([a,a+l]),this.componentStore.xAxis.setBoundingBoxXY({x:0,y:a}),this.chartData.plots.some(h=>KU(h))&&this.componentStore.xAxis.recalculateOuterPaddingToDrawBar()}calculateSpace(){this.chartConfig.chartOrientation==="horizontal"?this.calculateHorizontalSpace():this.calculateVerticalSpace()}getDrawableElement(){this.calculateSpace();let t=[];this.componentStore.plot.setAxes(this.componentStore.xAxis,this.componentStore.yAxis);for(let r of Object.values(this.componentStore))t.push(...r.getDrawableElements());return t}}});var yL,E8e=F(()=>{"use strict";S8e();yL=class{static{o(this,"XYChartBuilder")}static build(t,r,n,i){return new gL(t,r,n,i).getDrawableElement()}}});function R8e(){let e=ma(),t=_t();return qr(e.xyChart,t.themeVariables.xyChart)}function _8e(){let e=_t();return qr(cr.xyChart,e.xyChart)}function L8e(){return{yAxis:{type:"linear",title:"",min:1/0,max:-1/0},xAxis:{type:"band",title:"",categories:[]},title:"",plots:[]}}function xL(e){let t=_t();return mr(e.trim(),t)}function SAt(e){A8e=e}function EAt(e){e==="horizontal"?$w.chartOrientation="horizontal":$w.chartOrientation="vertical"}function AAt(e){fn.xAxis.title=xL(e.text)}function D8e(e,t){fn.xAxis={type:"linear",title:fn.xAxis.title,min:e,max:t},vL=!0}function RAt(e){fn.xAxis={type:"band",title:fn.xAxis.title,categories:e.map(t=>xL(t.text))},vL=!0}function _At(e){fn.yAxis.title=xL(e.text)}function LAt(e,t){fn.yAxis={type:"linear",title:fn.yAxis.title,min:e,max:t},nY=!0}function DAt(e){let t=Math.min(...e),r=Math.max(...e),n=Qv(fn.yAxis)?fn.yAxis.min:1/0,i=Qv(fn.yAxis)?fn.yAxis.max:-1/0;fn.yAxis={type:"linear",title:fn.yAxis.title,min:Math.min(n,t),max:Math.max(i,r)}}function I8e(e){let t=[];if(e.length===0)return t;if(!vL){let r=Qv(fn.xAxis)?fn.xAxis.min:1/0,n=Qv(fn.xAxis)?fn.xAxis.max:-1/0;D8e(Math.min(r,1),Math.max(n,e.length))}if(Ow(fn.xAxis)&&e.length>fn.xAxis.categories.length&&(e=e.slice(0,fn.xAxis.categories.length)),nY||DAt(e),Ow(fn.xAxis)&&(t=fn.xAxis.categories.map((r,n)=>[r,e[n]])),Qv(fn.xAxis)){let r=fn.xAxis.min,n=fn.xAxis.max;if(e.length===1)t=[[`${r}`,e[0]]];else{let i=(n-r)/(e.length-1);t=e.map((a,s)=>[`${r+s*i}`,a])}}return t}function M8e(e){return rY[e===0?0:e%rY.length]}function IAt(e,t){let r=t.map(s=>s.value),n=t.map(s=>s.label?xL(s.label):""),i=I8e(r),a=n.some(s=>s!=="");fn.plots.push({type:"line",strokeFill:M8e(Bw),strokeWidth:2,data:i,...a?{pointLabels:n}:{}}),Bw++}function MAt(e,t){let r=t.map(i=>i.value),n=I8e(r);fn.plots.push({type:"bar",fill:M8e(Bw),data:n}),Bw++}function NAt(){if(fn.plots.length===0)throw Error("No Plot to render, please provide a plot with some data");return fn.title=Lr(),yL.build($w,fn,Fw,A8e)}function PAt(){return Fw}function OAt(){return $w}function BAt(){return fn}var Bw,A8e,$w,Fw,fn,rY,vL,nY,$At,N8e,P8e=F(()=>{"use strict";ur();Wi();Pc();Qt();Vr();Nn();E8e();hL();Bw=0,$w=_8e(),Fw=R8e(),fn=L8e(),rY=Fw.plotColorPalette.split(",").map(e=>e.trim()),vL=!1,nY=!1;o(R8e,"getChartDefaultThemeConfig");o(_8e,"getChartDefaultConfig");o(L8e,"getChartDefaultData");o(xL,"textSanitizer");o(SAt,"setTmpSVGG");o(EAt,"setOrientation");o(AAt,"setXAxisTitle");o(D8e,"setXAxisRangeData");o(RAt,"setXAxisBand");o(_At,"setYAxisTitle");o(LAt,"setYAxisRangeData");o(DAt,"setYAxisRangeFromPlotData");o(I8e,"transformDataWithoutCategory");o(M8e,"getPlotColorFromPalette");o(IAt,"setLineData");o(MAt,"setBarData");o(NAt,"getDrawableElem");o(PAt,"getChartThemeConfig");o(OAt,"getChartConfig");o(BAt,"getXYChartData");$At=o(function(){yr(),Bw=0,$w=_8e(),fn=L8e(),Fw=R8e(),rY=Fw.plotColorPalette.split(",").map(e=>e.trim()),vL=!1,nY=!1},"clear"),N8e={getDrawableElem:NAt,clear:$At,setAccTitle:kr,getAccTitle:Ar,setDiagramTitle:Or,getDiagramTitle:Lr,getAccDescription:_r,setAccDescription:Rr,setOrientation:EAt,setXAxisTitle:AAt,setXAxisRangeData:D8e,setXAxisBand:RAt,setYAxisTitle:_At,setYAxisRangeData:LAt,setLineData:IAt,setBarData:MAt,setTmpSVGG:SAt,getChartThemeConfig:PAt,getChartConfig:OAt,getXYChartData:BAt}});var FAt,O8e,B8e=F(()=>{"use strict";vt();Ka();$n();FAt=o((e,t,r,n)=>{let i=n.db,a=i.getChartThemeConfig(),s=i.getChartConfig(),l=i.getXYChartData().plots[0].data.map(T=>T[1]);function u(T){return T==="top"?"text-before-edge":"middle"}o(u,"getDominantBaseLine");function h(T){return T==="left"?"start":T==="right"?"end":"middle"}o(h,"getTextAnchor");function d(T){return`translate(${T.x}, ${T.y}) rotate(${T.rotation||0})`}o(d,"getTextTransformation"),Z.debug(`Rendering xychart chart +`+e);let f=xn(t),p=f.append("g").attr("class","main"),m=p.append("rect").attr("width",s.width).attr("height",s.height).attr("class","background");Wr(f,s.height,s.width,!0),f.attr("viewBox",`0 0 ${s.width} ${s.height}`),m.attr("fill",a.backgroundColor),i.setTmpSVGG(f.append("g").attr("class","mermaid-tmp-group"));let g=i.getDrawableElem(),y={};function v(T){let k=p,C="";for(let[w]of T.entries()){let S=p;w>0&&y[C]&&(S=y[C]),C+=T[w],k=y[C],k||(k=y[C]=S.append("g").attr("class",T[w]))}return k}o(v,"getGroup");for(let T of g){if(T.data.length===0)continue;let k=v(T.groupTexts);switch(T.type){case"rect":if(k.selectAll("rect").data(T.data).enter().append("rect").attr("x",C=>C.x).attr("y",C=>C.y).attr("width",C=>C.width).attr("height",C=>C.height).attr("fill",C=>C.fill).attr("stroke",C=>C.strokeFill).attr("stroke-width",C=>C.strokeWidth),s.showDataLabel){let C=s.showDataLabelOutsideBar;if(s.chartOrientation==="horizontal"){let L=function(A,M){let{data:D,label:P}=A;return M*P.length*.7<=D.width-10};var x=L;o(L,"fitsHorizontally");let w=.7,S=10,R=T.data.map((A,M)=>({data:A,label:l[M].toString()})).filter(A=>A.data.width>0&&A.data.height>0),N=R.map(A=>{let{data:M}=A,D=M.height*.7;for(;!L(A,D)&&D>0;)D-=1;return D}),I=Math.floor(Math.min(...N)),_=o(A=>C?A.data.x+A.data.width+10:A.data.x+A.data.width-10,"determineLabelXPosition");k.selectAll("text").data(R).enter().append("text").attr("x",_).attr("y",A=>A.data.y+A.data.height/2).attr("text-anchor",C?"start":"end").attr("dominant-baseline","middle").attr("fill",a.dataLabelColor).attr("font-size",`${I}px`).text(A=>A.label)}else{let R=function(_,A,M){let{data:D,label:P}=_,O=A*P.length*.7,$=D.x+D.width/2,V=$-O/2,G=$+O/2,z=V>=D.x&&G<=D.x+D.width,W=D.y+M+A<=D.y+D.height;return z&&W};var b=R;o(R,"fitsInBar");let w=10,S=T.data.map((_,A)=>({data:_,label:l[A].toString()})).filter(_=>_.data.width>0&&_.data.height>0),L=S.map(_=>{let{data:A,label:M}=_,D=A.width/(M.length*.7);for(;!R(_,D,10)&&D>0;)D-=1;return D}),N=Math.floor(Math.min(...L)),I=o(_=>C?_.data.y-10:_.data.y+10,"determineLabelYPosition");k.selectAll("text").data(S).enter().append("text").attr("x",_=>_.data.x+_.data.width/2).attr("y",I).attr("text-anchor","middle").attr("dominant-baseline",C?"auto":"hanging").attr("fill",a.dataLabelColor).attr("font-size",`${N}px`).text(_=>_.label)}}break;case"text":k.selectAll("text").data(T.data).enter().append("text").attr("x",0).attr("y",0).attr("fill",C=>C.fill).attr("font-size",C=>C.fontSize).attr("dominant-baseline",C=>u(C.verticalPos)).attr("text-anchor",C=>h(C.horizontalPos)).attr("transform",C=>d(C)).text(C=>C.text);break;case"path":k.selectAll("path").data(T.data).enter().append("path").attr("d",C=>C.path).attr("fill",C=>C.fill?C.fill:"none").attr("stroke",C=>C.strokeFill).attr("stroke-width",C=>C.strokeWidth);break}}},"draw"),O8e={draw:FAt}});var $8e={};ir($8e,{diagram:()=>zAt});var zAt,F8e=F(()=>{"use strict";m8e();P8e();B8e();zAt={parser:p8e,db:N8e,renderer:O8e}});var iY,V8e,W8e=F(()=>{"use strict";iY=(function(){var e=o(function(ve,ne,q,he){for(q=q||{},he=ve.length;he--;q[ve[he]]=ne);return q},"o"),t=[1,3],r=[1,4],n=[1,5],i=[1,6],a=[5,6,8,9,11,13,21,22,23,24,41,42,43,44,45,46,54,72,74,77,89,90],s=[1,22],l=[2,7],u=[1,26],h=[1,27],d=[1,28],f=[1,29],p=[1,33],m=[1,34],g=[1,35],y=[1,36],v=[1,37],x=[1,38],b=[1,24],T=[1,31],k=[1,32],C=[1,30],w=[1,39],S=[1,40],R=[5,8,9,11,13,21,22,23,24,41,42,43,44,45,46,54,72,74,77,89,90],L=[1,61],N=[89,90],I=[5,8,9,11,13,21,22,23,24,27,29,41,42,43,44,45,46,54,61,63,72,74,75,76,77,80,81,82,83,84,85,86,87,88,89,90],_=[27,29],A=[1,70],M=[1,71],D=[1,72],P=[1,73],B=[1,74],O=[1,75],$=[1,76],V=[1,83],G=[1,80],z=[1,84],W=[1,85],H=[1,86],j=[1,87],Q=[1,88],U=[1,89],oe=[1,90],te=[1,91],le=[1,92],ie=[5,8,9,11,13,21,22,23,24,27,41,42,43,44,45,46,54,72,74,75,76,77,80,81,82,83,84,85,86,87,88,89,90],ae=[63,64],Re=[1,101],be=[5,8,9,11,13,21,22,23,24,41,42,43,44,45,46,54,72,74,76,77,89,90],Pe=[5,8,9,11,13,21,22,23,24,41,42,43,44,45,46,54,72,74,75,76,77,80,81,82,83,84,85,86,87,88,89,90],Ge=[1,110],Oe=[1,106],ue=[1,107],ye=[1,108],ke=[1,109],ce=[1,111],re=[1,116],J=[1,117],se=[1,114],ge=[1,115],Te={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,directive:4,NEWLINE:5,RD:6,diagram:7,EOF:8,acc_title:9,acc_title_value:10,acc_descr:11,acc_descr_value:12,acc_descr_multiline_value:13,requirementDef:14,elementDef:15,relationshipDef:16,direction:17,styleStatement:18,classDefStatement:19,classStatement:20,direction_tb:21,direction_bt:22,direction_rl:23,direction_lr:24,requirementType:25,requirementName:26,STRUCT_START:27,requirementBody:28,STYLE_SEPARATOR:29,idList:30,ID:31,COLONSEP:32,id:33,TEXT:34,text:35,RISK:36,riskLevel:37,VERIFYMTHD:38,verifyType:39,STRUCT_STOP:40,REQUIREMENT:41,FUNCTIONAL_REQUIREMENT:42,INTERFACE_REQUIREMENT:43,PERFORMANCE_REQUIREMENT:44,PHYSICAL_REQUIREMENT:45,DESIGN_CONSTRAINT:46,LOW_RISK:47,MED_RISK:48,HIGH_RISK:49,VERIFY_ANALYSIS:50,VERIFY_DEMONSTRATION:51,VERIFY_INSPECTION:52,VERIFY_TEST:53,ELEMENT:54,elementName:55,elementBody:56,TYPE:57,type:58,DOCREF:59,ref:60,END_ARROW_L:61,relationship:62,LINE:63,END_ARROW_R:64,CONTAINS:65,COPIES:66,DERIVES:67,SATISFIES:68,VERIFIES:69,REFINES:70,TRACES:71,CLASSDEF:72,stylesOpt:73,CLASS:74,ALPHA:75,COMMA:76,STYLE:77,style:78,styleComponent:79,NUM:80,COLON:81,UNIT:82,SPACE:83,BRKT:84,PCT:85,MINUS:86,LABEL:87,SEMICOLON:88,unqString:89,qString:90,$accept:0,$end:1},terminals_:{2:"error",5:"NEWLINE",6:"RD",8:"EOF",9:"acc_title",10:"acc_title_value",11:"acc_descr",12:"acc_descr_value",13:"acc_descr_multiline_value",21:"direction_tb",22:"direction_bt",23:"direction_rl",24:"direction_lr",27:"STRUCT_START",29:"STYLE_SEPARATOR",31:"ID",32:"COLONSEP",34:"TEXT",36:"RISK",38:"VERIFYMTHD",40:"STRUCT_STOP",41:"REQUIREMENT",42:"FUNCTIONAL_REQUIREMENT",43:"INTERFACE_REQUIREMENT",44:"PERFORMANCE_REQUIREMENT",45:"PHYSICAL_REQUIREMENT",46:"DESIGN_CONSTRAINT",47:"LOW_RISK",48:"MED_RISK",49:"HIGH_RISK",50:"VERIFY_ANALYSIS",51:"VERIFY_DEMONSTRATION",52:"VERIFY_INSPECTION",53:"VERIFY_TEST",54:"ELEMENT",57:"TYPE",59:"DOCREF",61:"END_ARROW_L",63:"LINE",64:"END_ARROW_R",65:"CONTAINS",66:"COPIES",67:"DERIVES",68:"SATISFIES",69:"VERIFIES",70:"REFINES",71:"TRACES",72:"CLASSDEF",74:"CLASS",75:"ALPHA",76:"COMMA",77:"STYLE",80:"NUM",81:"COLON",82:"UNIT",83:"SPACE",84:"BRKT",85:"PCT",86:"MINUS",87:"LABEL",88:"SEMICOLON",89:"unqString",90:"qString"},productions_:[0,[3,3],[3,2],[3,4],[4,2],[4,2],[4,1],[7,0],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2],[7,2],[17,1],[17,1],[17,1],[17,1],[14,5],[14,7],[28,5],[28,5],[28,5],[28,5],[28,2],[28,1],[25,1],[25,1],[25,1],[25,1],[25,1],[25,1],[37,1],[37,1],[37,1],[39,1],[39,1],[39,1],[39,1],[15,5],[15,7],[56,5],[56,5],[56,2],[56,1],[16,5],[16,5],[62,1],[62,1],[62,1],[62,1],[62,1],[62,1],[62,1],[19,3],[20,3],[20,3],[30,1],[30,3],[30,1],[30,3],[18,3],[73,1],[73,3],[78,1],[78,2],[79,1],[79,1],[79,1],[79,1],[79,1],[79,1],[79,1],[79,1],[79,1],[79,1],[26,1],[26,1],[33,1],[33,1],[35,1],[35,1],[55,1],[55,1],[58,1],[58,1],[60,1],[60,1]],performAction:o(function(ne,q,he,X,fe,K,qe){var _e=K.length-1;switch(fe){case 4:this.$=K[_e].trim(),X.setAccTitle(this.$);break;case 5:case 6:this.$=K[_e].trim(),X.setAccDescription(this.$);break;case 7:this.$=[];break;case 17:X.setDirection("TB");break;case 18:X.setDirection("BT");break;case 19:X.setDirection("RL");break;case 20:X.setDirection("LR");break;case 21:X.addRequirement(K[_e-3],K[_e-4]);break;case 22:X.addRequirement(K[_e-5],K[_e-6]),X.setClass([K[_e-5]],K[_e-3]);break;case 23:X.setNewReqId(K[_e-2]);break;case 24:X.setNewReqText(K[_e-2]);break;case 25:X.setNewReqRisk(K[_e-2]);break;case 26:X.setNewReqVerifyMethod(K[_e-2]);break;case 29:this.$=X.RequirementType.REQUIREMENT;break;case 30:this.$=X.RequirementType.FUNCTIONAL_REQUIREMENT;break;case 31:this.$=X.RequirementType.INTERFACE_REQUIREMENT;break;case 32:this.$=X.RequirementType.PERFORMANCE_REQUIREMENT;break;case 33:this.$=X.RequirementType.PHYSICAL_REQUIREMENT;break;case 34:this.$=X.RequirementType.DESIGN_CONSTRAINT;break;case 35:this.$=X.RiskLevel.LOW_RISK;break;case 36:this.$=X.RiskLevel.MED_RISK;break;case 37:this.$=X.RiskLevel.HIGH_RISK;break;case 38:this.$=X.VerifyType.VERIFY_ANALYSIS;break;case 39:this.$=X.VerifyType.VERIFY_DEMONSTRATION;break;case 40:this.$=X.VerifyType.VERIFY_INSPECTION;break;case 41:this.$=X.VerifyType.VERIFY_TEST;break;case 42:X.addElement(K[_e-3]);break;case 43:X.addElement(K[_e-5]),X.setClass([K[_e-5]],K[_e-3]);break;case 44:X.setNewElementType(K[_e-2]);break;case 45:X.setNewElementDocRef(K[_e-2]);break;case 48:X.addRelationship(K[_e-2],K[_e],K[_e-4]);break;case 49:X.addRelationship(K[_e-2],K[_e-4],K[_e]);break;case 50:this.$=X.Relationships.CONTAINS;break;case 51:this.$=X.Relationships.COPIES;break;case 52:this.$=X.Relationships.DERIVES;break;case 53:this.$=X.Relationships.SATISFIES;break;case 54:this.$=X.Relationships.VERIFIES;break;case 55:this.$=X.Relationships.REFINES;break;case 56:this.$=X.Relationships.TRACES;break;case 57:this.$=K[_e-2],X.defineClass(K[_e-1],K[_e]);break;case 58:X.setClass(K[_e-1],K[_e]);break;case 59:X.setClass([K[_e-2]],K[_e]);break;case 60:case 62:this.$=[K[_e]];break;case 61:case 63:this.$=K[_e-2].concat([K[_e]]);break;case 64:this.$=K[_e-2],X.setCssStyle(K[_e-1],K[_e]);break;case 65:this.$=[K[_e]];break;case 66:K[_e-2].push(K[_e]),this.$=K[_e-2];break;case 68:this.$=K[_e-1]+K[_e];break}},"anonymous"),table:[{3:1,4:2,6:t,9:r,11:n,13:i},{1:[3]},{3:8,4:2,5:[1,7],6:t,9:r,11:n,13:i},{5:[1,9]},{10:[1,10]},{12:[1,11]},e(a,[2,6]),{3:12,4:2,6:t,9:r,11:n,13:i},{1:[2,2]},{4:17,5:s,7:13,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:k,77:C,89:w,90:S},e(a,[2,4]),e(a,[2,5]),{1:[2,1]},{8:[1,41]},{4:17,5:s,7:42,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:k,77:C,89:w,90:S},{4:17,5:s,7:43,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:k,77:C,89:w,90:S},{4:17,5:s,7:44,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:k,77:C,89:w,90:S},{4:17,5:s,7:45,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:k,77:C,89:w,90:S},{4:17,5:s,7:46,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:k,77:C,89:w,90:S},{4:17,5:s,7:47,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:k,77:C,89:w,90:S},{4:17,5:s,7:48,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:k,77:C,89:w,90:S},{4:17,5:s,7:49,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:k,77:C,89:w,90:S},{4:17,5:s,7:50,8:l,9:r,11:n,13:i,14:14,15:15,16:16,17:18,18:19,19:20,20:21,21:u,22:h,23:d,24:f,25:23,33:25,41:p,42:m,43:g,44:y,45:v,46:x,54:b,72:T,74:k,77:C,89:w,90:S},{26:51,89:[1,52],90:[1,53]},{55:54,89:[1,55],90:[1,56]},{29:[1,59],61:[1,57],63:[1,58]},e(R,[2,17]),e(R,[2,18]),e(R,[2,19]),e(R,[2,20]),{30:60,33:62,75:L,89:w,90:S},{30:63,33:62,75:L,89:w,90:S},{30:64,33:62,75:L,89:w,90:S},e(N,[2,29]),e(N,[2,30]),e(N,[2,31]),e(N,[2,32]),e(N,[2,33]),e(N,[2,34]),e(I,[2,81]),e(I,[2,82]),{1:[2,3]},{8:[2,8]},{8:[2,9]},{8:[2,10]},{8:[2,11]},{8:[2,12]},{8:[2,13]},{8:[2,14]},{8:[2,15]},{8:[2,16]},{27:[1,65],29:[1,66]},e(_,[2,79]),e(_,[2,80]),{27:[1,67],29:[1,68]},e(_,[2,85]),e(_,[2,86]),{62:69,65:A,66:M,67:D,68:P,69:B,70:O,71:$},{62:77,65:A,66:M,67:D,68:P,69:B,70:O,71:$},{30:78,33:62,75:L,89:w,90:S},{73:79,75:V,76:G,78:81,79:82,80:z,81:W,82:H,83:j,84:Q,85:U,86:oe,87:te,88:le},e(ie,[2,60]),e(ie,[2,62]),{73:93,75:V,76:G,78:81,79:82,80:z,81:W,82:H,83:j,84:Q,85:U,86:oe,87:te,88:le},{30:94,33:62,75:L,76:G,89:w,90:S},{5:[1,95]},{30:96,33:62,75:L,89:w,90:S},{5:[1,97]},{30:98,33:62,75:L,89:w,90:S},{63:[1,99]},e(ae,[2,50]),e(ae,[2,51]),e(ae,[2,52]),e(ae,[2,53]),e(ae,[2,54]),e(ae,[2,55]),e(ae,[2,56]),{64:[1,100]},e(R,[2,59],{76:G}),e(R,[2,64],{76:Re}),{33:103,75:[1,102],89:w,90:S},e(be,[2,65],{79:104,75:V,80:z,81:W,82:H,83:j,84:Q,85:U,86:oe,87:te,88:le}),e(Pe,[2,67]),e(Pe,[2,69]),e(Pe,[2,70]),e(Pe,[2,71]),e(Pe,[2,72]),e(Pe,[2,73]),e(Pe,[2,74]),e(Pe,[2,75]),e(Pe,[2,76]),e(Pe,[2,77]),e(Pe,[2,78]),e(R,[2,57],{76:Re}),e(R,[2,58],{76:G}),{5:Ge,28:105,31:Oe,34:ue,36:ye,38:ke,40:ce},{27:[1,112],76:G},{5:re,40:J,56:113,57:se,59:ge},{27:[1,118],76:G},{33:119,89:w,90:S},{33:120,89:w,90:S},{75:V,78:121,79:82,80:z,81:W,82:H,83:j,84:Q,85:U,86:oe,87:te,88:le},e(ie,[2,61]),e(ie,[2,63]),e(Pe,[2,68]),e(R,[2,21]),{32:[1,122]},{32:[1,123]},{32:[1,124]},{32:[1,125]},{5:Ge,28:126,31:Oe,34:ue,36:ye,38:ke,40:ce},e(R,[2,28]),{5:[1,127]},e(R,[2,42]),{32:[1,128]},{32:[1,129]},{5:re,40:J,56:130,57:se,59:ge},e(R,[2,47]),{5:[1,131]},e(R,[2,48]),e(R,[2,49]),e(be,[2,66],{79:104,75:V,80:z,81:W,82:H,83:j,84:Q,85:U,86:oe,87:te,88:le}),{33:132,89:w,90:S},{35:133,89:[1,134],90:[1,135]},{37:136,47:[1,137],48:[1,138],49:[1,139]},{39:140,50:[1,141],51:[1,142],52:[1,143],53:[1,144]},e(R,[2,27]),{5:Ge,28:145,31:Oe,34:ue,36:ye,38:ke,40:ce},{58:146,89:[1,147],90:[1,148]},{60:149,89:[1,150],90:[1,151]},e(R,[2,46]),{5:re,40:J,56:152,57:se,59:ge},{5:[1,153]},{5:[1,154]},{5:[2,83]},{5:[2,84]},{5:[1,155]},{5:[2,35]},{5:[2,36]},{5:[2,37]},{5:[1,156]},{5:[2,38]},{5:[2,39]},{5:[2,40]},{5:[2,41]},e(R,[2,22]),{5:[1,157]},{5:[2,87]},{5:[2,88]},{5:[1,158]},{5:[2,89]},{5:[2,90]},e(R,[2,43]),{5:Ge,28:159,31:Oe,34:ue,36:ye,38:ke,40:ce},{5:Ge,28:160,31:Oe,34:ue,36:ye,38:ke,40:ce},{5:Ge,28:161,31:Oe,34:ue,36:ye,38:ke,40:ce},{5:Ge,28:162,31:Oe,34:ue,36:ye,38:ke,40:ce},{5:re,40:J,56:163,57:se,59:ge},{5:re,40:J,56:164,57:se,59:ge},e(R,[2,23]),e(R,[2,24]),e(R,[2,25]),e(R,[2,26]),e(R,[2,44]),e(R,[2,45])],defaultActions:{8:[2,2],12:[2,1],41:[2,3],42:[2,8],43:[2,9],44:[2,10],45:[2,11],46:[2,12],47:[2,13],48:[2,14],49:[2,15],50:[2,16],134:[2,83],135:[2,84],137:[2,35],138:[2,36],139:[2,37],141:[2,38],142:[2,39],143:[2,40],144:[2,41],147:[2,87],148:[2,88],150:[2,89],151:[2,90]},parseError:o(function(ne,q){if(q.recoverable)this.trace(ne);else{var he=new Error(ne);throw he.hash=q,he}},"parseError"),parse:o(function(ne){var q=this,he=[0],X=[],fe=[null],K=[],qe=this.table,_e="",Be=0,Ne=0,He=0,$e=2,Xe=1,Fe=K.slice.call(arguments,1),Ke=Object.create(this.lexer),xe={yy:{}};for(var mt in this.yy)Object.prototype.hasOwnProperty.call(this.yy,mt)&&(xe.yy[mt]=this.yy[mt]);Ke.setInput(ne,xe.yy),xe.yy.lexer=Ke,xe.yy.parser=this,typeof Ke.yylloc>"u"&&(Ke.yylloc={});var Le=Ke.yylloc;K.push(Le);var ft=Ke.options&&Ke.options.ranges;typeof xe.yy.parseError=="function"?this.parseError=xe.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function wt(Bt){he.length=he.length-2*Bt,fe.length=fe.length-Bt,K.length=K.length-Bt}o(wt,"popStack");function zt(){var Bt;return Bt=X.pop()||Ke.lex()||Xe,typeof Bt!="number"&&(Bt instanceof Array&&(X=Bt,Bt=X.pop()),Bt=q.symbols_[Bt]||Bt),Bt}o(zt,"lex");for(var St,At,bt,me,lt,gt,Ze={},Ee,tt,at,ot;;){if(bt=he[he.length-1],this.defaultActions[bt]?me=this.defaultActions[bt]:((St===null||typeof St>"u")&&(St=zt()),me=qe[bt]&&qe[bt][St]),typeof me>"u"||!me.length||!me[0]){var Wt="";ot=[];for(Ee in qe[bt])this.terminals_[Ee]&&Ee>$e&&ot.push("'"+this.terminals_[Ee]+"'");Ke.showPosition?Wt="Parse error on line "+(Be+1)+`: +`+Ke.showPosition()+` +Expecting `+ot.join(", ")+", got '"+(this.terminals_[St]||St)+"'":Wt="Parse error on line "+(Be+1)+": Unexpected "+(St==Xe?"end of input":"'"+(this.terminals_[St]||St)+"'"),this.parseError(Wt,{text:Ke.match,token:this.terminals_[St]||St,line:Ke.yylineno,loc:Le,expected:ot})}if(me[0]instanceof Array&&me.length>1)throw new Error("Parse Error: multiple actions possible at state: "+bt+", token: "+St);switch(me[0]){case 1:he.push(St),fe.push(Ke.yytext),K.push(Ke.yylloc),he.push(me[1]),St=null,At?(St=At,At=null):(Ne=Ke.yyleng,_e=Ke.yytext,Be=Ke.yylineno,Le=Ke.yylloc,He>0&&He--);break;case 2:if(tt=this.productions_[me[1]][1],Ze.$=fe[fe.length-tt],Ze._$={first_line:K[K.length-(tt||1)].first_line,last_line:K[K.length-1].last_line,first_column:K[K.length-(tt||1)].first_column,last_column:K[K.length-1].last_column},ft&&(Ze._$.range=[K[K.length-(tt||1)].range[0],K[K.length-1].range[1]]),gt=this.performAction.apply(Ze,[_e,Ne,Be,xe.yy,me[1],fe,K].concat(Fe)),typeof gt<"u")return gt;tt&&(he=he.slice(0,-1*tt*2),fe=fe.slice(0,-1*tt),K=K.slice(0,-1*tt)),he.push(this.productions_[me[1]][0]),fe.push(Ze.$),K.push(Ze._$),at=qe[he[he.length-2]][he[he.length-1]],he.push(at);break;case 3:return!0}}return!0},"parse")},we=(function(){var ve={EOF:1,parseError:o(function(q,he){if(this.yy.parser)this.yy.parser.parseError(q,he);else throw new Error(q)},"parseError"),setInput:o(function(ne,q){return this.yy=q||this.yy||{},this._input=ne,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var ne=this._input[0];this.yytext+=ne,this.yyleng++,this.offset++,this.match+=ne,this.matched+=ne;var q=ne.match(/(?:\r\n?|\n).*/g);return q?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),ne},"input"),unput:o(function(ne){var q=ne.length,he=ne.split(/(?:\r\n?|\n)/g);this._input=ne+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-q),this.offset-=q;var X=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),he.length-1&&(this.yylineno-=he.length-1);var fe=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:he?(he.length===X.length?this.yylloc.first_column:0)+X[X.length-he.length].length-he[0].length:this.yylloc.first_column-q},this.options.ranges&&(this.yylloc.range=[fe[0],fe[0]+this.yyleng-q]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(ne){this.unput(this.match.slice(ne))},"less"),pastInput:o(function(){var ne=this.matched.substr(0,this.matched.length-this.match.length);return(ne.length>20?"...":"")+ne.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var ne=this.match;return ne.length<20&&(ne+=this._input.substr(0,20-ne.length)),(ne.substr(0,20)+(ne.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var ne=this.pastInput(),q=new Array(ne.length+1).join("-");return ne+this.upcomingInput()+` +`+q+"^"},"showPosition"),test_match:o(function(ne,q){var he,X,fe;if(this.options.backtrack_lexer&&(fe={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(fe.yylloc.range=this.yylloc.range.slice(0))),X=ne[0].match(/(?:\r\n?|\n).*/g),X&&(this.yylineno+=X.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:X?X[X.length-1].length-X[X.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+ne[0].length},this.yytext+=ne[0],this.match+=ne[0],this.matches=ne,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(ne[0].length),this.matched+=ne[0],he=this.performAction.call(this,this.yy,this,q,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),he)return he;if(this._backtrack){for(var K in fe)this[K]=fe[K];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var ne,q,he,X;this._more||(this.yytext="",this.match="");for(var fe=this._currentRules(),K=0;Kq[0].length)){if(q=he,X=K,this.options.backtrack_lexer){if(ne=this.test_match(he,fe[K]),ne!==!1)return ne;if(this._backtrack){q=!1;continue}else return!1}else if(!this.options.flex)break}return q?(ne=this.test_match(q,fe[X]),ne!==!1?ne:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var q=this.next();return q||this.lex()},"lex"),begin:o(function(q){this.conditionStack.push(q)},"begin"),popState:o(function(){var q=this.conditionStack.length-1;return q>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(q){return q=this.conditionStack.length-1-Math.abs(q||0),q>=0?this.conditionStack[q]:"INITIAL"},"topState"),pushState:o(function(q){this.begin(q)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:o(function(q,he,X,fe){var K=fe;switch(X){case 0:return"title";case 1:return this.begin("acc_title"),9;break;case 2:return this.popState(),"acc_title_value";break;case 3:return this.begin("acc_descr"),11;break;case 4:return this.popState(),"acc_descr_value";break;case 5:this.begin("acc_descr_multiline");break;case 6:this.popState();break;case 7:return"acc_descr_multiline_value";case 8:return 21;case 9:return 22;case 10:return 23;case 11:return 24;case 12:return 5;case 13:break;case 14:break;case 15:break;case 16:return 8;case 17:return 6;case 18:return 27;case 19:return 40;case 20:return 29;case 21:return 32;case 22:return 31;case 23:return 34;case 24:return 36;case 25:return 38;case 26:return 41;case 27:return 42;case 28:return 43;case 29:return 44;case 30:return 45;case 31:return 46;case 32:return 47;case 33:return 48;case 34:return 49;case 35:return 50;case 36:return 51;case 37:return 52;case 38:return 53;case 39:return 54;case 40:return 65;case 41:return 66;case 42:return 67;case 43:return 68;case 44:return 69;case 45:return 70;case 46:return 71;case 47:return 57;case 48:return 59;case 49:return this.begin("style"),77;break;case 50:return 75;case 51:return 81;case 52:return 88;case 53:return"PERCENT";case 54:return 86;case 55:return 84;case 56:break;case 57:this.begin("string");break;case 58:this.popState();break;case 59:return this.begin("style"),72;break;case 60:return this.begin("style"),74;break;case 61:return 61;case 62:return 64;case 63:return 63;case 64:this.begin("string");break;case 65:this.popState();break;case 66:return"qString";case 67:return he.yytext=he.yytext.trim(),89;break;case 68:return 75;case 69:return 80;case 70:return 76}},"anonymous"),rules:[/^(?:title\s[^#\n;]+)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:.*direction\s+TB[^\n]*)/i,/^(?:.*direction\s+BT[^\n]*)/i,/^(?:.*direction\s+RL[^\n]*)/i,/^(?:.*direction\s+LR[^\n]*)/i,/^(?:(\r?\n)+)/i,/^(?:\s+)/i,/^(?:#[^\n]*)/i,/^(?:%[^\n]*)/i,/^(?:$)/i,/^(?:requirementDiagram\b)/i,/^(?:\{)/i,/^(?:\})/i,/^(?::{3})/i,/^(?::)/i,/^(?:id\b)/i,/^(?:text\b)/i,/^(?:risk\b)/i,/^(?:verifyMethod\b)/i,/^(?:requirement\b)/i,/^(?:functionalRequirement\b)/i,/^(?:interfaceRequirement\b)/i,/^(?:performanceRequirement\b)/i,/^(?:physicalRequirement\b)/i,/^(?:designConstraint\b)/i,/^(?:low\b)/i,/^(?:medium\b)/i,/^(?:high\b)/i,/^(?:analysis\b)/i,/^(?:demonstration\b)/i,/^(?:inspection\b)/i,/^(?:test\b)/i,/^(?:element\b)/i,/^(?:contains\b)/i,/^(?:copies\b)/i,/^(?:derives\b)/i,/^(?:satisfies\b)/i,/^(?:verifies\b)/i,/^(?:refines\b)/i,/^(?:traces\b)/i,/^(?:type\b)/i,/^(?:docref\b)/i,/^(?:style\b)/i,/^(?:\w+)/i,/^(?::)/i,/^(?:;)/i,/^(?:%)/i,/^(?:-)/i,/^(?:#)/i,/^(?: )/i,/^(?:["])/i,/^(?:\n)/i,/^(?:classDef\b)/i,/^(?:class\b)/i,/^(?:<-)/i,/^(?:->)/i,/^(?:-)/i,/^(?:["])/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:[\w][^:,\r\n\{\<\>\-\=]*)/i,/^(?:\w+)/i,/^(?:[0-9]+)/i,/^(?:,)/i],conditions:{acc_descr_multiline:{rules:[6,7,68,69,70],inclusive:!1},acc_descr:{rules:[4,68,69,70],inclusive:!1},acc_title:{rules:[2,68,69,70],inclusive:!1},style:{rules:[50,51,52,53,54,55,56,57,58,68,69,70],inclusive:!1},unqString:{rules:[68,69,70],inclusive:!1},token:{rules:[68,69,70],inclusive:!1},string:{rules:[65,66,68,69,70],inclusive:!1},INITIAL:{rules:[0,1,3,5,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,59,60,61,62,63,64,67,68,69,70],inclusive:!0}}};return ve})();Te.lexer=we;function Me(){this.yy={}}return o(Me,"Parser"),Me.prototype=Te,Te.Parser=Me,new Me})();iY.parser=iY;V8e=iY});var bL,q8e=F(()=>{"use strict";Xt();vt();Nn();bL=class{constructor(){this.relations=[];this.latestRequirement=this.getInitialRequirement();this.requirements=new Map;this.latestElement=this.getInitialElement();this.elements=new Map;this.classes=new Map;this.direction="TB";this.RequirementType={REQUIREMENT:"Requirement",FUNCTIONAL_REQUIREMENT:"Functional Requirement",INTERFACE_REQUIREMENT:"Interface Requirement",PERFORMANCE_REQUIREMENT:"Performance Requirement",PHYSICAL_REQUIREMENT:"Physical Requirement",DESIGN_CONSTRAINT:"Design Constraint"};this.RiskLevel={LOW_RISK:"Low",MED_RISK:"Medium",HIGH_RISK:"High"};this.VerifyType={VERIFY_ANALYSIS:"Analysis",VERIFY_DEMONSTRATION:"Demonstration",VERIFY_INSPECTION:"Inspection",VERIFY_TEST:"Test"};this.Relationships={CONTAINS:"contains",COPIES:"copies",DERIVES:"derives",SATISFIES:"satisfies",VERIFIES:"verifies",REFINES:"refines",TRACES:"traces"};this.setAccTitle=kr;this.getAccTitle=Ar;this.setAccDescription=Rr;this.getAccDescription=_r;this.setDiagramTitle=Or;this.getDiagramTitle=Lr;this.getConfig=o(()=>Ae().requirement,"getConfig");this.clear(),this.setDirection=this.setDirection.bind(this),this.addRequirement=this.addRequirement.bind(this),this.setNewReqId=this.setNewReqId.bind(this),this.setNewReqRisk=this.setNewReqRisk.bind(this),this.setNewReqText=this.setNewReqText.bind(this),this.setNewReqVerifyMethod=this.setNewReqVerifyMethod.bind(this),this.addElement=this.addElement.bind(this),this.setNewElementType=this.setNewElementType.bind(this),this.setNewElementDocRef=this.setNewElementDocRef.bind(this),this.addRelationship=this.addRelationship.bind(this),this.setCssStyle=this.setCssStyle.bind(this),this.setClass=this.setClass.bind(this),this.defineClass=this.defineClass.bind(this),this.setAccTitle=this.setAccTitle.bind(this),this.setAccDescription=this.setAccDescription.bind(this)}static{o(this,"RequirementDB")}getDirection(){return this.direction}setDirection(t){this.direction=t}resetLatestRequirement(){this.latestRequirement=this.getInitialRequirement()}resetLatestElement(){this.latestElement=this.getInitialElement()}getInitialRequirement(){return{requirementId:"",text:"",risk:"",verifyMethod:"",name:"",type:"",cssStyles:[],classes:["default"]}}getInitialElement(){return{name:"",type:"",docRef:"",cssStyles:[],classes:["default"]}}addRequirement(t,r){return this.requirements.has(t)||this.requirements.set(t,{name:t,type:r,requirementId:this.latestRequirement.requirementId,text:this.latestRequirement.text,risk:this.latestRequirement.risk,verifyMethod:this.latestRequirement.verifyMethod,cssStyles:[],classes:["default"]}),this.resetLatestRequirement(),this.requirements.get(t)}getRequirements(){return this.requirements}setNewReqId(t){this.latestRequirement!==void 0&&(this.latestRequirement.requirementId=t)}setNewReqText(t){this.latestRequirement!==void 0&&(this.latestRequirement.text=t)}setNewReqRisk(t){this.latestRequirement!==void 0&&(this.latestRequirement.risk=t)}setNewReqVerifyMethod(t){this.latestRequirement!==void 0&&(this.latestRequirement.verifyMethod=t)}addElement(t){return this.elements.has(t)||(this.elements.set(t,{name:t,type:this.latestElement.type,docRef:this.latestElement.docRef,cssStyles:[],classes:["default"]}),Z.info("Added new element: ",t)),this.resetLatestElement(),this.elements.get(t)}getElements(){return this.elements}setNewElementType(t){this.latestElement!==void 0&&(this.latestElement.type=t)}setNewElementDocRef(t){this.latestElement!==void 0&&(this.latestElement.docRef=t)}addRelationship(t,r,n){this.relations.push({type:t,src:r,dst:n})}getRelationships(){return this.relations}clear(){this.relations=[],this.resetLatestRequirement(),this.requirements=new Map,this.resetLatestElement(),this.elements=new Map,this.classes=new Map,yr()}setCssStyle(t,r){for(let n of t){let i=this.requirements.get(n)??this.elements.get(n);if(!r||!i)return;for(let a of r)a.includes(",")?i.cssStyles.push(...a.split(",")):i.cssStyles.push(a)}}setClass(t,r){for(let n of t){let i=this.requirements.get(n)??this.elements.get(n);if(i)for(let a of r){i.classes.push(a);let s=this.classes.get(a)?.styles;s&&i.cssStyles.push(...s)}}}defineClass(t,r){for(let n of t){let i=this.classes.get(n);i===void 0&&(i={id:n,styles:[],textStyles:[]},this.classes.set(n,i)),r&&r.forEach(function(a){if(/color/.exec(a)){let s=a.replace("fill","bgFill");i.textStyles.push(s)}i.styles.push(a)}),this.requirements.forEach(a=>{a.classes.includes(n)&&a.cssStyles.push(...r.flatMap(s=>s.split(",")))}),this.elements.forEach(a=>{a.classes.includes(n)&&a.cssStyles.push(...r.flatMap(s=>s.split(",")))})}}getClasses(){return this.classes}getData(){let t=Ae(),r=[],n=[];for(let i of this.requirements.values()){let a=i;a.id=i.name,a.cssStyles=i.cssStyles,a.cssClasses=i.classes.join(" "),a.shape="requirementBox",a.look=t.look,a.colorIndex=r.length,r.push(a)}for(let i of this.elements.values()){let a=i;a.shape="requirementBox",a.look=t.look,a.id=i.name,a.cssStyles=i.cssStyles,a.cssClasses=i.classes.join(" "),a.colorIndex=r.length,r.push(a)}for(let i of this.relations){let a=0,s=i.type===this.Relationships.CONTAINS,l={id:`${i.src}-${i.dst}-${a}`,start:this.requirements.get(i.src)?.name??this.elements.get(i.src)?.name,end:this.requirements.get(i.dst)?.name??this.elements.get(i.dst)?.name,label:`<<${i.type}>>`,classes:"relationshipLine",style:["fill:none",s?"":"stroke-dasharray: 10,7"],labelpos:"c",thickness:"normal",type:"normal",pattern:s?"normal":"dashed",arrowTypeStart:s?"requirement_contains":"",arrowTypeEnd:s?"":"requirement_arrow",look:t.look,labelType:"markdown"};n.push(l),a++}return{nodes:r,edges:n,other:{},config:t,direction:this.getDirection()}}}});var qAt,HAt,H8e,U8e=F(()=>{"use strict";ur();qAt=o(e=>{let t=_t(),{themeVariables:r,look:n}=t,{bkgColorArray:i,borderColorArray:a}=r;if(!a?.length)return"";let s="";for(let l=0;l{let t=_t(),{look:r,themeVariables:n}=t,{requirementEdgeLabelBackground:i}=n;return` + ${qAt(e)} + marker { + fill: ${e.relationColor}; + stroke: ${e.relationColor}; + } + + marker.cross { + stroke: ${e.lineColor}; + } + + svg { + font-family: ${e.fontFamily}; + font-size: ${e.fontSize}; + } + + .reqBox { + fill: ${e.requirementBackground}; + fill-opacity: 1.0; + stroke: ${e.requirementBorderColor}; + stroke-width: ${e.requirementBorderSize}; + } + + .reqTitle, .reqLabel{ + fill: ${e.requirementTextColor}; + } + .reqLabelBox { + fill: ${e.relationLabelBackground}; + fill-opacity: 1.0; + } + + .req-title-line { + stroke: ${e.requirementBorderColor}; + stroke-width: ${e.requirementBorderSize}; + } + .relationshipLine { + stroke: ${e.relationColor}; + stroke-width: ${r==="neo"?e.strokeWidth:"1px"}; + } + .relationshipLabel { + fill: ${e.relationLabelColor}; + } + .edgeLabel { + background-color: ${e.edgeLabelBackground}; + } + .edgeLabel .label rect { + fill: ${e.edgeLabelBackground}; + } + .edgeLabel .label text { + fill: ${e.relationLabelColor}; + } + .divider { + stroke: ${e.nodeBorder}; + stroke-width: 1; + } + .label { + font-family: ${e.fontFamily}; + color: ${e.nodeTextColor||e.textColor}; + } + .label text,span { + fill: ${e.nodeTextColor||e.textColor}; + color: ${e.nodeTextColor||e.textColor}; + } + .labelBkg { + background-color: ${i??e.edgeLabelBackground}; + } + +`},"getStyles"),H8e=HAt});var aY={};ir(aY,{draw:()=>UAt});var UAt,Y8e=F(()=>{"use strict";Xt();vt();Rm();Jf();ep();Qt();UAt=o(async function(e,t,r,n){Z.info("REF0:"),Z.info("Drawing requirement diagram (unified)",t);let{securityLevel:i,state:a,layout:s,look:l}=Ae(),u=n.db.getData(),h=pl(t,i);u.type=n.type,u.layoutAlgorithm=Su(s),u.nodeSpacing=a?.nodeSpacing??50,u.rankSpacing=a?.rankSpacing??50,u.markers=l==="neo"?["requirement_contains_neo","requirement_arrow_neo"]:["requirement_contains","requirement_arrow"],u.diagramId=t,await Al(u,h);let d=8;Zt.insertTitle(h,"requirementDiagramTitleText",a?.titleTopMargin??25,n.db.getDiagramTitle()),vo(h,d,"requirementDiagram",a?.useMaxWidth??!0)},"draw")});var j8e={};ir(j8e,{diagram:()=>YAt});var YAt,X8e=F(()=>{"use strict";W8e();q8e();U8e();Y8e();YAt={parser:V8e,get db(){return new bL},renderer:aY,styles:H8e}});var sY,Q8e,J8e=F(()=>{"use strict";sY=(function(){var e=o(function(Ne,He,$e,Xe){for($e=$e||{},Xe=Ne.length;Xe--;$e[Ne[Xe]]=He);return $e},"o"),t=[1,2],r=[1,3],n=[1,4],i=[2,4],a=[1,9],s=[1,11],l=[1,12],u=[1,14],h=[1,15],d=[1,17],f=[1,18],p=[1,19],m=[1,25],g=[1,26],y=[1,27],v=[1,28],x=[1,29],b=[1,30],T=[1,31],k=[1,32],C=[1,33],w=[1,34],S=[1,35],R=[1,36],L=[1,37],N=[1,38],I=[1,39],_=[1,40],A=[1,42],M=[1,43],D=[1,44],P=[1,45],B=[1,46],O=[1,47],$=[1,4,5,10,14,15,17,19,22,24,30,31,32,34,36,37,38,39,40,42,44,45,47,48,49,50,51,53,54,56,61,62,63,64,73],V=[1,74],G=[1,80],z=[1,81],W=[1,82],H=[1,83],j=[1,84],Q=[1,85],U=[1,86],oe=[1,87],te=[1,88],le=[1,89],ie=[1,90],ae=[1,91],Re=[1,92],be=[1,93],Pe=[1,94],Ge=[1,95],Oe=[1,96],ue=[1,97],ye=[1,98],ke=[1,99],ce=[1,100],re=[1,101],J=[1,102],se=[1,103],ge=[1,104],Te=[1,105],we=[2,78],Me=[4,5,17,51,53,54],ve=[4,5,10,14,15,17,19,22,24,30,31,32,34,36,37,38,39,40,42,44,45,47,51,53,54,56,61,62,63,64,73],ne=[4,5,10,14,15,17,19,22,24,30,31,32,34,36,37,38,39,40,42,44,45,47,50,51,53,54,56,61,62,63,64,73],q=[4,5,10,14,15,17,19,22,24,30,31,32,34,36,37,38,39,40,42,44,45,47,49,51,53,54,56,61,62,63,64,73],he=[4,5,10,14,15,17,19,22,24,30,31,32,34,36,37,38,39,40,42,44,45,47,48,51,53,54,56,61,62,63,64,73],X=[5,52],fe=[70,71,72,73],K=[1,151],qe={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,SPACE:4,NEWLINE:5,SD:6,document:7,line:8,statement:9,INVALID:10,box_section:11,box_line:12,participant_statement:13,create:14,box:15,restOfLine:16,end:17,signal:18,autonumber:19,NUM:20,off:21,activate:22,actor:23,deactivate:24,note_statement:25,links_statement:26,link_statement:27,properties_statement:28,details_statement:29,title:30,legacy_title:31,acc_title:32,acc_title_value:33,acc_descr:34,acc_descr_value:35,acc_descr_multiline_value:36,loop:37,rect:38,opt:39,alt:40,else_sections:41,par:42,par_sections:43,par_over:44,critical:45,option_sections:46,break:47,option:48,and:49,else:50,participant:51,AS:52,participant_actor:53,destroy:54,actor_with_config:55,note:56,placement:57,text2:58,over:59,actor_pair:60,links:61,link:62,properties:63,details:64,spaceList:65,",":66,left_of:67,right_of:68,signaltype:69,"+":70,"-":71,"()":72,ACTOR:73,config_object:74,CONFIG_START:75,CONFIG_CONTENT:76,CONFIG_END:77,SOLID_OPEN_ARROW:78,DOTTED_OPEN_ARROW:79,SOLID_ARROW:80,SOLID_ARROW_TOP:81,SOLID_ARROW_BOTTOM:82,STICK_ARROW_TOP:83,STICK_ARROW_BOTTOM:84,SOLID_ARROW_TOP_DOTTED:85,SOLID_ARROW_BOTTOM_DOTTED:86,STICK_ARROW_TOP_DOTTED:87,STICK_ARROW_BOTTOM_DOTTED:88,SOLID_ARROW_TOP_REVERSE:89,SOLID_ARROW_BOTTOM_REVERSE:90,STICK_ARROW_TOP_REVERSE:91,STICK_ARROW_BOTTOM_REVERSE:92,SOLID_ARROW_TOP_REVERSE_DOTTED:93,SOLID_ARROW_BOTTOM_REVERSE_DOTTED:94,STICK_ARROW_TOP_REVERSE_DOTTED:95,STICK_ARROW_BOTTOM_REVERSE_DOTTED:96,BIDIRECTIONAL_SOLID_ARROW:97,DOTTED_ARROW:98,BIDIRECTIONAL_DOTTED_ARROW:99,SOLID_CROSS:100,DOTTED_CROSS:101,SOLID_POINT:102,DOTTED_POINT:103,TXT:104,$accept:0,$end:1},terminals_:{2:"error",4:"SPACE",5:"NEWLINE",6:"SD",10:"INVALID",14:"create",15:"box",16:"restOfLine",17:"end",19:"autonumber",20:"NUM",21:"off",22:"activate",24:"deactivate",30:"title",31:"legacy_title",32:"acc_title",33:"acc_title_value",34:"acc_descr",35:"acc_descr_value",36:"acc_descr_multiline_value",37:"loop",38:"rect",39:"opt",40:"alt",42:"par",44:"par_over",45:"critical",47:"break",48:"option",49:"and",50:"else",51:"participant",52:"AS",53:"participant_actor",54:"destroy",56:"note",59:"over",61:"links",62:"link",63:"properties",64:"details",66:",",67:"left_of",68:"right_of",70:"+",71:"-",72:"()",73:"ACTOR",75:"CONFIG_START",76:"CONFIG_CONTENT",77:"CONFIG_END",78:"SOLID_OPEN_ARROW",79:"DOTTED_OPEN_ARROW",80:"SOLID_ARROW",81:"SOLID_ARROW_TOP",82:"SOLID_ARROW_BOTTOM",83:"STICK_ARROW_TOP",84:"STICK_ARROW_BOTTOM",85:"SOLID_ARROW_TOP_DOTTED",86:"SOLID_ARROW_BOTTOM_DOTTED",87:"STICK_ARROW_TOP_DOTTED",88:"STICK_ARROW_BOTTOM_DOTTED",89:"SOLID_ARROW_TOP_REVERSE",90:"SOLID_ARROW_BOTTOM_REVERSE",91:"STICK_ARROW_TOP_REVERSE",92:"STICK_ARROW_BOTTOM_REVERSE",93:"SOLID_ARROW_TOP_REVERSE_DOTTED",94:"SOLID_ARROW_BOTTOM_REVERSE_DOTTED",95:"STICK_ARROW_TOP_REVERSE_DOTTED",96:"STICK_ARROW_BOTTOM_REVERSE_DOTTED",97:"BIDIRECTIONAL_SOLID_ARROW",98:"DOTTED_ARROW",99:"BIDIRECTIONAL_DOTTED_ARROW",100:"SOLID_CROSS",101:"DOTTED_CROSS",102:"SOLID_POINT",103:"DOTTED_POINT",104:"TXT"},productions_:[0,[3,2],[3,2],[3,2],[7,0],[7,2],[8,2],[8,1],[8,1],[8,1],[11,0],[11,2],[12,2],[12,1],[12,1],[9,1],[9,2],[9,4],[9,2],[9,4],[9,3],[9,3],[9,2],[9,3],[9,3],[9,2],[9,2],[9,2],[9,2],[9,2],[9,1],[9,1],[9,2],[9,2],[9,1],[9,4],[9,4],[9,4],[9,4],[9,4],[9,4],[9,4],[9,4],[46,1],[46,4],[43,1],[43,4],[41,1],[41,4],[13,5],[13,3],[13,5],[13,3],[13,3],[13,5],[13,3],[13,5],[13,3],[25,4],[25,4],[26,3],[27,3],[28,3],[29,3],[65,2],[65,1],[60,3],[60,1],[57,1],[57,1],[18,5],[18,5],[18,5],[18,5],[18,6],[18,4],[55,2],[74,3],[23,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[69,1],[58,1]],performAction:o(function(He,$e,Xe,Fe,Ke,xe,mt){var Le=xe.length-1;switch(Ke){case 3:return Fe.apply(xe[Le]),xe[Le];break;case 4:case 10:this.$=[];break;case 5:case 11:xe[Le-1].push(xe[Le]),this.$=xe[Le-1];break;case 6:case 7:case 12:case 13:this.$=xe[Le];break;case 8:case 9:case 14:this.$=[];break;case 16:xe[Le].type="createParticipant",this.$=xe[Le];break;case 17:xe[Le-1].unshift({type:"boxStart",boxData:Fe.parseBoxData(xe[Le-2])}),xe[Le-1].push({type:"boxEnd",boxText:xe[Le-2]}),this.$=xe[Le-1];break;case 19:this.$={type:"sequenceIndex",sequenceIndex:Number(xe[Le-2]),sequenceIndexStep:Number(xe[Le-1]),sequenceVisible:!0,signalType:Fe.LINETYPE.AUTONUMBER};break;case 20:this.$={type:"sequenceIndex",sequenceIndex:Number(xe[Le-1]),sequenceIndexStep:1,sequenceVisible:!0,signalType:Fe.LINETYPE.AUTONUMBER};break;case 21:this.$={type:"sequenceIndex",sequenceVisible:!1,signalType:Fe.LINETYPE.AUTONUMBER};break;case 22:this.$={type:"sequenceIndex",sequenceVisible:!0,signalType:Fe.LINETYPE.AUTONUMBER};break;case 23:this.$={type:"activeStart",signalType:Fe.LINETYPE.ACTIVE_START,actor:xe[Le-1].actor};break;case 24:this.$={type:"activeEnd",signalType:Fe.LINETYPE.ACTIVE_END,actor:xe[Le-1].actor};break;case 30:Fe.setDiagramTitle(xe[Le].substring(6)),this.$=xe[Le].substring(6);break;case 31:Fe.setDiagramTitle(xe[Le].substring(7)),this.$=xe[Le].substring(7);break;case 32:this.$=xe[Le].trim(),Fe.setAccTitle(this.$);break;case 33:case 34:this.$=xe[Le].trim(),Fe.setAccDescription(this.$);break;case 35:xe[Le-1].unshift({type:"loopStart",loopText:Fe.parseMessage(xe[Le-2]),signalType:Fe.LINETYPE.LOOP_START}),xe[Le-1].push({type:"loopEnd",loopText:xe[Le-2],signalType:Fe.LINETYPE.LOOP_END}),this.$=xe[Le-1];break;case 36:xe[Le-1].unshift({type:"rectStart",color:Fe.parseMessage(xe[Le-2]),signalType:Fe.LINETYPE.RECT_START}),xe[Le-1].push({type:"rectEnd",color:Fe.parseMessage(xe[Le-2]),signalType:Fe.LINETYPE.RECT_END}),this.$=xe[Le-1];break;case 37:xe[Le-1].unshift({type:"optStart",optText:Fe.parseMessage(xe[Le-2]),signalType:Fe.LINETYPE.OPT_START}),xe[Le-1].push({type:"optEnd",optText:Fe.parseMessage(xe[Le-2]),signalType:Fe.LINETYPE.OPT_END}),this.$=xe[Le-1];break;case 38:xe[Le-1].unshift({type:"altStart",altText:Fe.parseMessage(xe[Le-2]),signalType:Fe.LINETYPE.ALT_START}),xe[Le-1].push({type:"altEnd",signalType:Fe.LINETYPE.ALT_END}),this.$=xe[Le-1];break;case 39:xe[Le-1].unshift({type:"parStart",parText:Fe.parseMessage(xe[Le-2]),signalType:Fe.LINETYPE.PAR_START}),xe[Le-1].push({type:"parEnd",signalType:Fe.LINETYPE.PAR_END}),this.$=xe[Le-1];break;case 40:xe[Le-1].unshift({type:"parStart",parText:Fe.parseMessage(xe[Le-2]),signalType:Fe.LINETYPE.PAR_OVER_START}),xe[Le-1].push({type:"parEnd",signalType:Fe.LINETYPE.PAR_END}),this.$=xe[Le-1];break;case 41:xe[Le-1].unshift({type:"criticalStart",criticalText:Fe.parseMessage(xe[Le-2]),signalType:Fe.LINETYPE.CRITICAL_START}),xe[Le-1].push({type:"criticalEnd",signalType:Fe.LINETYPE.CRITICAL_END}),this.$=xe[Le-1];break;case 42:xe[Le-1].unshift({type:"breakStart",breakText:Fe.parseMessage(xe[Le-2]),signalType:Fe.LINETYPE.BREAK_START}),xe[Le-1].push({type:"breakEnd",optText:Fe.parseMessage(xe[Le-2]),signalType:Fe.LINETYPE.BREAK_END}),this.$=xe[Le-1];break;case 44:this.$=xe[Le-3].concat([{type:"option",optionText:Fe.parseMessage(xe[Le-1]),signalType:Fe.LINETYPE.CRITICAL_OPTION},xe[Le]]);break;case 46:this.$=xe[Le-3].concat([{type:"and",parText:Fe.parseMessage(xe[Le-1]),signalType:Fe.LINETYPE.PAR_AND},xe[Le]]);break;case 48:this.$=xe[Le-3].concat([{type:"else",altText:Fe.parseMessage(xe[Le-1]),signalType:Fe.LINETYPE.ALT_ELSE},xe[Le]]);break;case 49:xe[Le-3].draw="participant",xe[Le-3].type="addParticipant",xe[Le-3].description=Fe.parseMessage(xe[Le-1]),this.$=xe[Le-3];break;case 50:xe[Le-1].draw="participant",xe[Le-1].type="addParticipant",this.$=xe[Le-1];break;case 51:xe[Le-3].draw="actor",xe[Le-3].type="addParticipant",xe[Le-3].description=Fe.parseMessage(xe[Le-1]),this.$=xe[Le-3];break;case 52:case 57:xe[Le-1].draw="actor",xe[Le-1].type="addParticipant",this.$=xe[Le-1];break;case 53:xe[Le-1].type="destroyParticipant",this.$=xe[Le-1];break;case 54:xe[Le-3].draw="participant",xe[Le-3].type="addParticipant",xe[Le-3].description=Fe.parseMessage(xe[Le-1]),this.$=xe[Le-3];break;case 55:xe[Le-1].draw="participant",xe[Le-1].type="addParticipant",this.$=xe[Le-1];break;case 56:xe[Le-3].draw="actor",xe[Le-3].type="addParticipant",xe[Le-3].description=Fe.parseMessage(xe[Le-1]),this.$=xe[Le-3];break;case 58:this.$=[xe[Le-1],{type:"addNote",placement:xe[Le-2],actor:xe[Le-1].actor,text:xe[Le]}];break;case 59:xe[Le-2]=[].concat(xe[Le-1],xe[Le-1]).slice(0,2),xe[Le-2][0]=xe[Le-2][0].actor,xe[Le-2][1]=xe[Le-2][1].actor,this.$=[xe[Le-1],{type:"addNote",placement:Fe.PLACEMENT.OVER,actor:xe[Le-2].slice(0,2),text:xe[Le]}];break;case 60:this.$=[xe[Le-1],{type:"addLinks",actor:xe[Le-1].actor,text:xe[Le]}];break;case 61:this.$=[xe[Le-1],{type:"addALink",actor:xe[Le-1].actor,text:xe[Le]}];break;case 62:this.$=[xe[Le-1],{type:"addProperties",actor:xe[Le-1].actor,text:xe[Le]}];break;case 63:this.$=[xe[Le-1],{type:"addDetails",actor:xe[Le-1].actor,text:xe[Le]}];break;case 66:this.$=[xe[Le-2],xe[Le]];break;case 67:this.$=xe[Le];break;case 68:this.$=Fe.PLACEMENT.LEFTOF;break;case 69:this.$=Fe.PLACEMENT.RIGHTOF;break;case 70:this.$=[xe[Le-4],xe[Le-1],{type:"addMessage",from:xe[Le-4].actor,to:xe[Le-1].actor,signalType:xe[Le-3],msg:xe[Le],activate:!0},{type:"activeStart",signalType:Fe.LINETYPE.ACTIVE_START,actor:xe[Le-1].actor}];break;case 71:this.$=[xe[Le-4],xe[Le-1],{type:"addMessage",from:xe[Le-4].actor,to:xe[Le-1].actor,signalType:xe[Le-3],msg:xe[Le]},{type:"activeEnd",signalType:Fe.LINETYPE.ACTIVE_END,actor:xe[Le-4].actor}];break;case 72:this.$=[xe[Le-4],xe[Le-1],{type:"addMessage",from:xe[Le-4].actor,to:xe[Le-1].actor,signalType:xe[Le-3],msg:xe[Le],activate:!0,centralConnection:Fe.LINETYPE.CENTRAL_CONNECTION},{type:"centralConnection",signalType:Fe.LINETYPE.CENTRAL_CONNECTION,actor:xe[Le-1].actor}];break;case 73:this.$=[xe[Le-4],xe[Le-1],{type:"addMessage",from:xe[Le-4].actor,to:xe[Le-1].actor,signalType:xe[Le-2],msg:xe[Le],activate:!1,centralConnection:Fe.LINETYPE.CENTRAL_CONNECTION_REVERSE},{type:"centralConnectionReverse",signalType:Fe.LINETYPE.CENTRAL_CONNECTION_REVERSE,actor:xe[Le-4].actor}];break;case 74:this.$=[xe[Le-5],xe[Le-1],{type:"addMessage",from:xe[Le-5].actor,to:xe[Le-1].actor,signalType:xe[Le-3],msg:xe[Le],activate:!0,centralConnection:Fe.LINETYPE.CENTRAL_CONNECTION_DUAL},{type:"centralConnection",signalType:Fe.LINETYPE.CENTRAL_CONNECTION,actor:xe[Le-1].actor},{type:"centralConnectionReverse",signalType:Fe.LINETYPE.CENTRAL_CONNECTION_REVERSE,actor:xe[Le-5].actor}];break;case 75:this.$=[xe[Le-3],xe[Le-1],{type:"addMessage",from:xe[Le-3].actor,to:xe[Le-1].actor,signalType:xe[Le-2],msg:xe[Le]}];break;case 76:this.$={type:"addParticipant",actor:xe[Le-1],config:xe[Le]};break;case 77:this.$=xe[Le-1].trim();break;case 78:this.$={type:"addParticipant",actor:xe[Le]};break;case 79:this.$=Fe.LINETYPE.SOLID_OPEN;break;case 80:this.$=Fe.LINETYPE.DOTTED_OPEN;break;case 81:this.$=Fe.LINETYPE.SOLID;break;case 82:this.$=Fe.LINETYPE.SOLID_TOP;break;case 83:this.$=Fe.LINETYPE.SOLID_BOTTOM;break;case 84:this.$=Fe.LINETYPE.STICK_TOP;break;case 85:this.$=Fe.LINETYPE.STICK_BOTTOM;break;case 86:this.$=Fe.LINETYPE.SOLID_TOP_DOTTED;break;case 87:this.$=Fe.LINETYPE.SOLID_BOTTOM_DOTTED;break;case 88:this.$=Fe.LINETYPE.STICK_TOP_DOTTED;break;case 89:this.$=Fe.LINETYPE.STICK_BOTTOM_DOTTED;break;case 90:this.$=Fe.LINETYPE.SOLID_ARROW_TOP_REVERSE;break;case 91:this.$=Fe.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE;break;case 92:this.$=Fe.LINETYPE.STICK_ARROW_TOP_REVERSE;break;case 93:this.$=Fe.LINETYPE.STICK_ARROW_BOTTOM_REVERSE;break;case 94:this.$=Fe.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED;break;case 95:this.$=Fe.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED;break;case 96:this.$=Fe.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED;break;case 97:this.$=Fe.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED;break;case 98:this.$=Fe.LINETYPE.BIDIRECTIONAL_SOLID;break;case 99:this.$=Fe.LINETYPE.DOTTED;break;case 100:this.$=Fe.LINETYPE.BIDIRECTIONAL_DOTTED;break;case 101:this.$=Fe.LINETYPE.SOLID_CROSS;break;case 102:this.$=Fe.LINETYPE.DOTTED_CROSS;break;case 103:this.$=Fe.LINETYPE.SOLID_POINT;break;case 104:this.$=Fe.LINETYPE.DOTTED_POINT;break;case 105:this.$=Fe.parseMessage(xe[Le].trim().substring(1));break}},"anonymous"),table:[{3:1,4:t,5:r,6:n},{1:[3]},{3:5,4:t,5:r,6:n},{3:6,4:t,5:r,6:n},e([1,4,5,10,14,15,19,22,24,30,31,32,34,36,37,38,39,40,42,44,45,47,51,53,54,56,61,62,63,64,73],i,{7:7}),{1:[2,1]},{1:[2,2]},{1:[2,3],4:a,5:s,8:8,9:10,10:l,13:13,14:u,15:h,18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:k,40:C,42:w,44:S,45:R,47:L,51:N,53:I,54:_,56:A,61:M,62:D,63:P,64:B,73:O},e($,[2,5]),{9:48,13:13,14:u,15:h,18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:k,40:C,42:w,44:S,45:R,47:L,51:N,53:I,54:_,56:A,61:M,62:D,63:P,64:B,73:O},e($,[2,7]),e($,[2,8]),e($,[2,9]),e($,[2,15]),{13:49,51:N,53:I,54:_},{16:[1,50]},{5:[1,51]},{5:[1,54],20:[1,52],21:[1,53]},{23:55,73:O},{23:56,73:O},{5:[1,57]},{5:[1,58]},{5:[1,59]},{5:[1,60]},{5:[1,61]},e($,[2,30]),e($,[2,31]),{33:[1,62]},{35:[1,63]},e($,[2,34]),{16:[1,64]},{16:[1,65]},{16:[1,66]},{16:[1,67]},{16:[1,68]},{16:[1,69]},{16:[1,70]},{16:[1,71]},{23:72,55:73,73:V},{23:75,55:76,73:V},{23:77,73:O},{69:78,72:[1,79],78:G,79:z,80:W,81:H,82:j,83:Q,84:U,85:oe,86:te,87:le,88:ie,89:ae,90:Re,91:be,92:Pe,93:Ge,94:Oe,95:ue,96:ye,97:ke,98:ce,99:re,100:J,101:se,102:ge,103:Te},{57:106,59:[1,107],67:[1,108],68:[1,109]},{23:110,73:O},{23:111,73:O},{23:112,73:O},{23:113,73:O},e([5,66,72,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104],we),e($,[2,6]),e($,[2,16]),e(Me,[2,10],{11:114}),e($,[2,18]),{5:[1,116],20:[1,115]},{5:[1,117]},e($,[2,22]),{5:[1,118]},{5:[1,119]},e($,[2,25]),e($,[2,26]),e($,[2,27]),e($,[2,28]),e($,[2,29]),e($,[2,32]),e($,[2,33]),e(ve,i,{7:120}),e(ve,i,{7:121}),e(ve,i,{7:122}),e(ne,i,{41:123,7:124}),e(q,i,{43:125,7:126}),e(q,i,{7:126,43:127}),e(he,i,{46:128,7:129}),e(ve,i,{7:130}),{5:[1,132],52:[1,131]},{5:[1,134],52:[1,133]},e(X,we,{74:135,75:[1,136]}),{5:[1,138],52:[1,137]},{5:[1,140],52:[1,139]},{5:[1,141]},{23:145,70:[1,142],71:[1,143],72:[1,144],73:O},{69:146,78:G,79:z,80:W,81:H,82:j,83:Q,84:U,85:oe,86:te,87:le,88:ie,89:ae,90:Re,91:be,92:Pe,93:Ge,94:Oe,95:ue,96:ye,97:ke,98:ce,99:re,100:J,101:se,102:ge,103:Te},e(fe,[2,79]),e(fe,[2,80]),e(fe,[2,81]),e(fe,[2,82]),e(fe,[2,83]),e(fe,[2,84]),e(fe,[2,85]),e(fe,[2,86]),e(fe,[2,87]),e(fe,[2,88]),e(fe,[2,89]),e(fe,[2,90]),e(fe,[2,91]),e(fe,[2,92]),e(fe,[2,93]),e(fe,[2,94]),e(fe,[2,95]),e(fe,[2,96]),e(fe,[2,97]),e(fe,[2,98]),e(fe,[2,99]),e(fe,[2,100]),e(fe,[2,101]),e(fe,[2,102]),e(fe,[2,103]),e(fe,[2,104]),{23:147,73:O},{23:149,60:148,73:O},{73:[2,68]},{73:[2,69]},{58:150,104:K},{58:152,104:K},{58:153,104:K},{58:154,104:K},{4:[1,157],5:[1,159],12:156,13:158,17:[1,155],51:N,53:I,54:_},{5:[1,160]},e($,[2,20]),e($,[2,21]),e($,[2,23]),e($,[2,24]),{4:a,5:s,8:8,9:10,10:l,13:13,14:u,15:h,17:[1,161],18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:k,40:C,42:w,44:S,45:R,47:L,51:N,53:I,54:_,56:A,61:M,62:D,63:P,64:B,73:O},{4:a,5:s,8:8,9:10,10:l,13:13,14:u,15:h,17:[1,162],18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:k,40:C,42:w,44:S,45:R,47:L,51:N,53:I,54:_,56:A,61:M,62:D,63:P,64:B,73:O},{4:a,5:s,8:8,9:10,10:l,13:13,14:u,15:h,17:[1,163],18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:k,40:C,42:w,44:S,45:R,47:L,51:N,53:I,54:_,56:A,61:M,62:D,63:P,64:B,73:O},{17:[1,164]},{4:a,5:s,8:8,9:10,10:l,13:13,14:u,15:h,17:[2,47],18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:k,40:C,42:w,44:S,45:R,47:L,50:[1,165],51:N,53:I,54:_,56:A,61:M,62:D,63:P,64:B,73:O},{17:[1,166]},{4:a,5:s,8:8,9:10,10:l,13:13,14:u,15:h,17:[2,45],18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:k,40:C,42:w,44:S,45:R,47:L,49:[1,167],51:N,53:I,54:_,56:A,61:M,62:D,63:P,64:B,73:O},{17:[1,168]},{17:[1,169]},{4:a,5:s,8:8,9:10,10:l,13:13,14:u,15:h,17:[2,43],18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:k,40:C,42:w,44:S,45:R,47:L,48:[1,170],51:N,53:I,54:_,56:A,61:M,62:D,63:P,64:B,73:O},{4:a,5:s,8:8,9:10,10:l,13:13,14:u,15:h,17:[1,171],18:16,19:d,22:f,23:41,24:p,25:20,26:21,27:22,28:23,29:24,30:m,31:g,32:y,34:v,36:x,37:b,38:T,39:k,40:C,42:w,44:S,45:R,47:L,51:N,53:I,54:_,56:A,61:M,62:D,63:P,64:B,73:O},{16:[1,172]},e($,[2,50]),{16:[1,173]},e($,[2,55]),e(X,[2,76]),{76:[1,174]},{16:[1,175]},e($,[2,52]),{16:[1,176]},e($,[2,57]),e($,[2,53]),{23:177,73:O},{23:178,73:O},{23:179,73:O},{58:180,104:K},{23:181,72:[1,182],73:O},{58:183,104:K},{58:184,104:K},{66:[1,185],104:[2,67]},{5:[2,60]},{5:[2,105]},{5:[2,61]},{5:[2,62]},{5:[2,63]},e($,[2,17]),e(Me,[2,11]),{13:186,51:N,53:I,54:_},e(Me,[2,13]),e(Me,[2,14]),e($,[2,19]),e($,[2,35]),e($,[2,36]),e($,[2,37]),e($,[2,38]),{16:[1,187]},e($,[2,39]),{16:[1,188]},e($,[2,40]),e($,[2,41]),{16:[1,189]},e($,[2,42]),{5:[1,190]},{5:[1,191]},{77:[1,192]},{5:[1,193]},{5:[1,194]},{58:195,104:K},{58:196,104:K},{58:197,104:K},{5:[2,75]},{58:198,104:K},{23:199,73:O},{5:[2,58]},{5:[2,59]},{23:200,73:O},e(Me,[2,12]),e(ne,i,{7:124,41:201}),e(q,i,{7:126,43:202}),e(he,i,{7:129,46:203}),e($,[2,49]),e($,[2,54]),e(X,[2,77]),e($,[2,51]),e($,[2,56]),{5:[2,70]},{5:[2,71]},{5:[2,72]},{5:[2,73]},{58:204,104:K},{104:[2,66]},{17:[2,48]},{17:[2,46]},{17:[2,44]},{5:[2,74]}],defaultActions:{5:[2,1],6:[2,2],108:[2,68],109:[2,69],150:[2,60],151:[2,105],152:[2,61],153:[2,62],154:[2,63],180:[2,75],183:[2,58],184:[2,59],195:[2,70],196:[2,71],197:[2,72],198:[2,73],200:[2,66],201:[2,48],202:[2,46],203:[2,44],204:[2,74]},parseError:o(function(He,$e){if($e.recoverable)this.trace(He);else{var Xe=new Error(He);throw Xe.hash=$e,Xe}},"parseError"),parse:o(function(He){var $e=this,Xe=[0],Fe=[],Ke=[null],xe=[],mt=this.table,Le="",ft=0,wt=0,zt=0,St=2,At=1,bt=xe.slice.call(arguments,1),me=Object.create(this.lexer),lt={yy:{}};for(var gt in this.yy)Object.prototype.hasOwnProperty.call(this.yy,gt)&&(lt.yy[gt]=this.yy[gt]);me.setInput(He,lt.yy),lt.yy.lexer=me,lt.yy.parser=this,typeof me.yylloc>"u"&&(me.yylloc={});var Ze=me.yylloc;xe.push(Ze);var Ee=me.options&&me.options.ranges;typeof lt.yy.parseError=="function"?this.parseError=lt.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function tt(Ht){Xe.length=Xe.length-2*Ht,Ke.length=Ke.length-Ht,xe.length=xe.length-Ht}o(tt,"popStack");function at(){var Ht;return Ht=Fe.pop()||me.lex()||At,typeof Ht!="number"&&(Ht instanceof Array&&(Fe=Ht,Ht=Fe.pop()),Ht=$e.symbols_[Ht]||Ht),Ht}o(at,"lex");for(var ot,Wt,Bt,qt,vr,Tt,De={},it,We,rt,yt;;){if(Bt=Xe[Xe.length-1],this.defaultActions[Bt]?qt=this.defaultActions[Bt]:((ot===null||typeof ot>"u")&&(ot=at()),qt=mt[Bt]&&mt[Bt][ot]),typeof qt>"u"||!qt.length||!qt[0]){var Yt="";yt=[];for(it in mt[Bt])this.terminals_[it]&&it>St&&yt.push("'"+this.terminals_[it]+"'");me.showPosition?Yt="Parse error on line "+(ft+1)+`: +`+me.showPosition()+` +Expecting `+yt.join(", ")+", got '"+(this.terminals_[ot]||ot)+"'":Yt="Parse error on line "+(ft+1)+": Unexpected "+(ot==At?"end of input":"'"+(this.terminals_[ot]||ot)+"'"),this.parseError(Yt,{text:me.match,token:this.terminals_[ot]||ot,line:me.yylineno,loc:Ze,expected:yt})}if(qt[0]instanceof Array&&qt.length>1)throw new Error("Parse Error: multiple actions possible at state: "+Bt+", token: "+ot);switch(qt[0]){case 1:Xe.push(ot),Ke.push(me.yytext),xe.push(me.yylloc),Xe.push(qt[1]),ot=null,Wt?(ot=Wt,Wt=null):(wt=me.yyleng,Le=me.yytext,ft=me.yylineno,Ze=me.yylloc,zt>0&&zt--);break;case 2:if(We=this.productions_[qt[1]][1],De.$=Ke[Ke.length-We],De._$={first_line:xe[xe.length-(We||1)].first_line,last_line:xe[xe.length-1].last_line,first_column:xe[xe.length-(We||1)].first_column,last_column:xe[xe.length-1].last_column},Ee&&(De._$.range=[xe[xe.length-(We||1)].range[0],xe[xe.length-1].range[1]]),Tt=this.performAction.apply(De,[Le,wt,ft,lt.yy,qt[1],Ke,xe].concat(bt)),typeof Tt<"u")return Tt;We&&(Xe=Xe.slice(0,-1*We*2),Ke=Ke.slice(0,-1*We),xe=xe.slice(0,-1*We)),Xe.push(this.productions_[qt[1]][0]),Ke.push(De.$),xe.push(De._$),rt=mt[Xe[Xe.length-2]][Xe[Xe.length-1]],Xe.push(rt);break;case 3:return!0}}return!0},"parse")},_e=(function(){var Ne={EOF:1,parseError:o(function($e,Xe){if(this.yy.parser)this.yy.parser.parseError($e,Xe);else throw new Error($e)},"parseError"),setInput:o(function(He,$e){return this.yy=$e||this.yy||{},this._input=He,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var He=this._input[0];this.yytext+=He,this.yyleng++,this.offset++,this.match+=He,this.matched+=He;var $e=He.match(/(?:\r\n?|\n).*/g);return $e?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),He},"input"),unput:o(function(He){var $e=He.length,Xe=He.split(/(?:\r\n?|\n)/g);this._input=He+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-$e),this.offset-=$e;var Fe=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),Xe.length-1&&(this.yylineno-=Xe.length-1);var Ke=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:Xe?(Xe.length===Fe.length?this.yylloc.first_column:0)+Fe[Fe.length-Xe.length].length-Xe[0].length:this.yylloc.first_column-$e},this.options.ranges&&(this.yylloc.range=[Ke[0],Ke[0]+this.yyleng-$e]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(He){this.unput(this.match.slice(He))},"less"),pastInput:o(function(){var He=this.matched.substr(0,this.matched.length-this.match.length);return(He.length>20?"...":"")+He.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var He=this.match;return He.length<20&&(He+=this._input.substr(0,20-He.length)),(He.substr(0,20)+(He.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var He=this.pastInput(),$e=new Array(He.length+1).join("-");return He+this.upcomingInput()+` +`+$e+"^"},"showPosition"),test_match:o(function(He,$e){var Xe,Fe,Ke;if(this.options.backtrack_lexer&&(Ke={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(Ke.yylloc.range=this.yylloc.range.slice(0))),Fe=He[0].match(/(?:\r\n?|\n).*/g),Fe&&(this.yylineno+=Fe.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:Fe?Fe[Fe.length-1].length-Fe[Fe.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+He[0].length},this.yytext+=He[0],this.match+=He[0],this.matches=He,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(He[0].length),this.matched+=He[0],Xe=this.performAction.call(this,this.yy,this,$e,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),Xe)return Xe;if(this._backtrack){for(var xe in Ke)this[xe]=Ke[xe];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var He,$e,Xe,Fe;this._more||(this.yytext="",this.match="");for(var Ke=this._currentRules(),xe=0;xe$e[0].length)){if($e=Xe,Fe=xe,this.options.backtrack_lexer){if(He=this.test_match(Xe,Ke[xe]),He!==!1)return He;if(this._backtrack){$e=!1;continue}else return!1}else if(!this.options.flex)break}return $e?(He=this.test_match($e,Ke[Fe]),He!==!1?He:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var $e=this.next();return $e||this.lex()},"lex"),begin:o(function($e){this.conditionStack.push($e)},"begin"),popState:o(function(){var $e=this.conditionStack.length-1;return $e>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function($e){return $e=this.conditionStack.length-1-Math.abs($e||0),$e>=0?this.conditionStack[$e]:"INITIAL"},"topState"),pushState:o(function($e){this.begin($e)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:o(function($e,Xe,Fe,Ke){var xe=Ke;switch(Fe){case 0:return 5;case 1:break;case 2:break;case 3:break;case 4:break;case 5:break;case 6:return 20;case 7:return this.begin("CONFIG"),75;break;case 8:return 76;case 9:return this.popState(),this.begin("ALIAS"),77;break;case 10:return this.popState(),this.popState(),77;break;case 11:return Xe.yytext=Xe.yytext.trim(),73;break;case 12:return Xe.yytext=Xe.yytext.trim(),this.begin("ALIAS"),73;break;case 13:return Xe.yytext=Xe.yytext.trim(),this.popState(),73;break;case 14:return this.popState(),10;break;case 15:return Xe.yytext=Xe.yytext.trim(),this.popState(),10;break;case 16:return this.begin("LINE"),15;break;case 17:return this.begin("ID"),51;break;case 18:return this.begin("ID"),53;break;case 19:return 14;case 20:return this.begin("ID"),54;break;case 21:return this.popState(),this.popState(),this.begin("LINE"),52;break;case 22:return this.popState(),this.popState(),5;break;case 23:return this.begin("LINE"),37;break;case 24:return this.begin("LINE"),38;break;case 25:return this.begin("LINE"),39;break;case 26:return this.begin("LINE"),40;break;case 27:return this.begin("LINE"),50;break;case 28:return this.begin("LINE"),42;break;case 29:return this.begin("LINE"),44;break;case 30:return this.begin("LINE"),49;break;case 31:return this.begin("LINE"),45;break;case 32:return this.begin("LINE"),48;break;case 33:return this.begin("LINE"),47;break;case 34:return this.popState(),16;break;case 35:return 17;case 36:return 67;case 37:return 68;case 38:return 61;case 39:return 62;case 40:return 63;case 41:return 64;case 42:return 59;case 43:return 56;case 44:return this.begin("ID"),22;break;case 45:return this.begin("ID"),24;break;case 46:return 30;case 47:return 31;case 48:return this.begin("acc_title"),32;break;case 49:return this.popState(),"acc_title_value";break;case 50:return this.begin("acc_descr"),34;break;case 51:return this.popState(),"acc_descr_value";break;case 52:this.begin("acc_descr_multiline");break;case 53:this.popState();break;case 54:return"acc_descr_multiline_value";case 55:return 6;case 56:return 19;case 57:return 21;case 58:return 66;case 59:return 5;case 60:return Xe.yytext=Xe.yytext.trim(),73;break;case 61:return 80;case 62:return 97;case 63:return 98;case 64:return 99;case 65:return 78;case 66:return 79;case 67:return 100;case 68:return 101;case 69:return 102;case 70:return 103;case 71:return 85;case 72:return 86;case 73:return 87;case 74:return 88;case 75:return 93;case 76:return 94;case 77:return 95;case 78:return 96;case 79:return 81;case 80:return 82;case 81:return 83;case 82:return 84;case 83:return 89;case 84:return 90;case 85:return 91;case 86:return 92;case 87:return 104;case 88:return 104;case 89:return 70;case 90:return 71;case 91:return 72;case 92:return 5;case 93:return 10}},"anonymous"),rules:[/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:((?!\n)\s)+)/i,/^(?:#[^\n]*)/i,/^(?:%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:([0-9]+(\.[0-9]{1,2})?|\.[0-9]{1,2})(?=[ \n]+))/i,/^(?:@\{)/i,/^(?:[^\}]+)/i,/^(?:\}(?=\s+as\s))/i,/^(?:\})/i,/^(?:[^\<->\->:\n,;@\s]+(?=@\{))/i,/^(?:[^<>:\n,;@\s]+(?=\s+as\s))/i,/^(?:[^<>:\n,;@]+(?=\s*[\n;#]|$))/i,/^(?:[^<>:\n,;@]*<[^\n]*)/i,/^(?:[^\n]+)/i,/^(?:box\b)/i,/^(?:participant\b)/i,/^(?:actor\b)/i,/^(?:create\b)/i,/^(?:destroy\b)/i,/^(?:as\b)/i,/^(?:(?:))/i,/^(?:loop\b)/i,/^(?:rect\b)/i,/^(?:opt\b)/i,/^(?:alt\b)/i,/^(?:else\b)/i,/^(?:par\b)/i,/^(?:par_over\b)/i,/^(?:and\b)/i,/^(?:critical\b)/i,/^(?:option\b)/i,/^(?:break\b)/i,/^(?:(?:[:]?(?:no)?wrap)?[^#\n;]*)/i,/^(?:end\b)/i,/^(?:left of\b)/i,/^(?:right of\b)/i,/^(?:links\b)/i,/^(?:link\b)/i,/^(?:properties\b)/i,/^(?:details\b)/i,/^(?:over\b)/i,/^(?:note\b)/i,/^(?:activate\b)/i,/^(?:deactivate\b)/i,/^(?:title\s[^#\n;]+)/i,/^(?:title:\s[^#\n;]+)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:sequenceDiagram\b)/i,/^(?:autonumber\b)/i,/^(?:off\b)/i,/^(?:,)/i,/^(?:;)/i,/^(?:[^\/\\\+\()\+<\->\->:\n,;]+((?!(-x|--x|-\)|--\)|-\|\\|-\\|-\/|-\/\/|-\|\/|\/\|-|\\\|-|\/\/-|\\\\-|\/\|-|--\|\\|--|\(\)))[\-]*[^\+<\->\->:\n,;]+)*)/i,/^(?:->>)/i,/^(?:<<->>)/i,/^(?:-->>)/i,/^(?:<<-->>)/i,/^(?:->)/i,/^(?:-->)/i,/^(?:-[x])/i,/^(?:--[x])/i,/^(?:-[\)])/i,/^(?:--[\)])/i,/^(?:--\|\\)/i,/^(?:--\|\/)/i,/^(?:--\\\\)/i,/^(?:--\/\/)/i,/^(?:\/\|--)/i,/^(?:\\\|--)/i,/^(?:\/\/--)/i,/^(?:\\\\--)/i,/^(?:-\|\\)/i,/^(?:-\|\/)/i,/^(?:-\\\\)/i,/^(?:-\/\/)/i,/^(?:\/\|-)/i,/^(?:\\\|-)/i,/^(?:\/\/-)/i,/^(?:\\\\-)/i,/^(?::(?:(?:no)?wrap)?[^#\n;]*)/i,/^(?::)/i,/^(?:\+)/i,/^(?:-)/i,/^(?:\(\))/i,/^(?:$)/i,/^(?:.)/i],conditions:{acc_descr_multiline:{rules:[53,54],inclusive:!1},acc_descr:{rules:[51],inclusive:!1},acc_title:{rules:[49],inclusive:!1},ID:{rules:[2,3,7,11,12,13,14,15],inclusive:!1},ALIAS:{rules:[2,3,21,22],inclusive:!1},LINE:{rules:[2,3,34],inclusive:!1},CONFIG:{rules:[8,9,10],inclusive:!1},CONFIG_DATA:{rules:[],inclusive:!1},INITIAL:{rules:[0,1,3,4,5,6,16,17,18,19,20,23,24,25,26,27,28,29,30,31,32,33,35,36,37,38,39,40,41,42,43,44,45,46,47,48,50,52,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93],inclusive:!0}}};return Ne})();qe.lexer=_e;function Be(){this.yy={}}return o(Be,"Parser"),Be.prototype=qe,qe.Parser=Be,new Be})();sY.parser=sY;Q8e=sY});var ZAt,QAt,JAt,zw,TL,oY=F(()=>{"use strict";Xt();R2();vt();J_();Vr();Nn();ZAt={SOLID:0,DOTTED:1,NOTE:2,SOLID_CROSS:3,DOTTED_CROSS:4,SOLID_OPEN:5,DOTTED_OPEN:6,LOOP_START:10,LOOP_END:11,ALT_START:12,ALT_ELSE:13,ALT_END:14,OPT_START:15,OPT_END:16,ACTIVE_START:17,ACTIVE_END:18,PAR_START:19,PAR_AND:20,PAR_END:21,RECT_START:22,RECT_END:23,SOLID_POINT:24,DOTTED_POINT:25,AUTONUMBER:26,CRITICAL_START:27,CRITICAL_OPTION:28,CRITICAL_END:29,BREAK_START:30,BREAK_END:31,PAR_OVER_START:32,BIDIRECTIONAL_SOLID:33,BIDIRECTIONAL_DOTTED:34,SOLID_TOP:41,SOLID_BOTTOM:42,STICK_TOP:43,STICK_BOTTOM:44,SOLID_ARROW_TOP_REVERSE:45,SOLID_ARROW_BOTTOM_REVERSE:46,STICK_ARROW_TOP_REVERSE:47,STICK_ARROW_BOTTOM_REVERSE:48,SOLID_TOP_DOTTED:51,SOLID_BOTTOM_DOTTED:52,STICK_TOP_DOTTED:53,STICK_BOTTOM_DOTTED:54,SOLID_ARROW_TOP_REVERSE_DOTTED:55,SOLID_ARROW_BOTTOM_REVERSE_DOTTED:56,STICK_ARROW_TOP_REVERSE_DOTTED:57,STICK_ARROW_BOTTOM_REVERSE_DOTTED:58,CENTRAL_CONNECTION:59,CENTRAL_CONNECTION_REVERSE:60,CENTRAL_CONNECTION_DUAL:61},QAt={FILLED:0,OPEN:1},JAt={LEFTOF:0,RIGHTOF:1,OVER:2},zw={ACTOR:"actor",BOUNDARY:"boundary",COLLECTIONS:"collections",CONTROL:"control",DATABASE:"database",ENTITY:"entity",PARTICIPANT:"participant",QUEUE:"queue"},TL=class{constructor(){this.state=new wp(()=>({prevActor:void 0,actors:new Map,createdActors:new Map,destroyedActors:new Map,boxes:[],messages:[],notes:[],sequenceNumbersEnabled:!1,wrapEnabled:void 0,currentBox:void 0,lastCreated:void 0,lastDestroyed:void 0}));this.setAccTitle=kr;this.setAccDescription=Rr;this.setDiagramTitle=Or;this.getAccTitle=Ar;this.getAccDescription=_r;this.getDiagramTitle=Lr;this.apply=this.apply.bind(this),this.parseBoxData=this.parseBoxData.bind(this),this.parseMessage=this.parseMessage.bind(this),this.clear(),this.setWrap(Ae().wrap),this.LINETYPE=ZAt,this.ARROWTYPE=QAt,this.PLACEMENT=JAt}static{o(this,"SequenceDB")}addBox(t){this.state.records.boxes.push({name:t.text,wrap:t.wrap??this.autoWrap(),fill:t.color,actorKeys:[]}),this.state.records.currentBox=this.state.records.boxes.slice(-1)[0]}addActor(t,r,n,i,a){let s=this.state.records.currentBox,l;if(a!==void 0){let h;a.includes(` +`)?h=a+` +`:h=`{ +`+a+` +}`,l=Jd(h,{schema:Qd})}i=l?.type??i,l?.alias&&(!n||n.text===r)&&(n={text:l.alias,wrap:n?.wrap,type:i});let u=this.state.records.actors.get(t);if(u){if(this.state.records.currentBox&&u.box&&this.state.records.currentBox!==u.box)throw new Error(`A same participant should only be defined in one Box: ${u.name} can't be in '${u.box.name}' and in '${this.state.records.currentBox.name}' at the same time.`);if(s=u.box?u.box:this.state.records.currentBox,u.box=s,u&&r===u.name&&n==null)return}if(n?.text==null&&(n={text:r,type:i}),(i==null||n.text==null)&&(n={text:r,type:i}),this.state.records.actors.set(t,{box:s,name:r,description:n.text,wrap:n.wrap??this.autoWrap(),prevActor:this.state.records.prevActor,links:{},properties:{},actorCnt:null,rectData:null,type:i??"participant"}),this.state.records.prevActor){let h=this.state.records.actors.get(this.state.records.prevActor);h&&(h.nextActor=t)}this.state.records.currentBox&&this.state.records.currentBox.actorKeys.push(t),this.state.records.prevActor=t}activationCount(t){let r,n=0;if(!t)return 0;for(r=0;r>-",token:"->>-",line:"1",loc:{first_line:1,last_line:1,first_column:1,last_column:1},expected:["'ACTIVE_PARTICIPANT'"]},u}return this.state.records.messages.push({id:this.state.records.messages.length.toString(),from:t,to:r,message:n?.text??"",wrap:n?.wrap??this.autoWrap(),type:i,activate:a,centralConnection:s??0}),!0}hasAtLeastOneBox(){return this.state.records.boxes.length>0}hasAtLeastOneBoxWithTitle(){return this.state.records.boxes.some(t=>t.name)}getMessages(){return this.state.records.messages}getBoxes(){return this.state.records.boxes}getActors(){return this.state.records.actors}getCreatedActors(){return this.state.records.createdActors}getDestroyedActors(){return this.state.records.destroyedActors}getActor(t){return this.state.records.actors.get(t)}getActorKeys(){return[...this.state.records.actors.keys()]}enableSequenceNumbers(){this.state.records.sequenceNumbersEnabled=!0}disableSequenceNumbers(){this.state.records.sequenceNumbersEnabled=!1}showSequenceNumbers(){return this.state.records.sequenceNumbersEnabled}setWrap(t){this.state.records.wrapEnabled=t}extractWrap(t){if(t===void 0)return{};t=t.trim();let r=/^:?wrap:/.exec(t)!==null?!0:/^:?nowrap:/.exec(t)!==null?!1:void 0;return{cleanedText:(r===void 0?t:t.replace(/^:?(?:no)?wrap:/,"")).trim(),wrap:r}}autoWrap(){return this.state.records.wrapEnabled!==void 0?this.state.records.wrapEnabled:Ae().sequence?.wrap??!1}clear(){this.state.reset(),yr()}parseMessage(t){let r=t.trim(),{wrap:n,cleanedText:i}=this.extractWrap(r),a={text:i,wrap:n};return Z.debug(`parseMessage: ${JSON.stringify(a)}`),a}parseBoxData(t){let r=/^((?:rgba?|hsla?)\s*\(.*\)|\w*)(.*)$/.exec(t),n=r?.[1]?r[1].trim():"transparent",i=r?.[2]?r[2].trim():void 0;if(window?.CSS)window.CSS.supports("color",n)||(n="transparent",i=t.trim());else{let l=new Option().style;l.color=n,l.color!==n&&(n="transparent",i=t.trim())}let{wrap:a,cleanedText:s}=this.extractWrap(i);return{text:s?mr(s,Ae()):void 0,color:n,wrap:a}}addNote(t,r,n){let i={actor:t,placement:r,message:n.text,wrap:n.wrap??this.autoWrap()},a=[].concat(t,t);this.state.records.notes.push(i),this.state.records.messages.push({id:this.state.records.messages.length.toString(),from:a[0],to:a[1],message:n.text,wrap:n.wrap??this.autoWrap(),type:this.LINETYPE.NOTE,placement:r})}addLinks(t,r){let n=this.getActor(t);try{let i=mr(r.text,Ae());i=i.replace(/=/g,"="),i=i.replace(/&/g,"&");let a=JSON.parse(i);this.insertLinks(n,a)}catch(i){Z.error("error while parsing actor link text",i)}}addALink(t,r){let n=this.getActor(t);try{let i={},a=mr(r.text,Ae()),s=a.indexOf("@");a=a.replace(/=/g,"="),a=a.replace(/&/g,"&");let l=a.slice(0,s-1).trim(),u=a.slice(s+1).trim();i[l]=u,this.insertLinks(n,i)}catch(i){Z.error("error while parsing actor link text",i)}}insertLinks(t,r){if(t.links==null)t.links=r;else for(let n in r)t.links[n]=r[n]}addProperties(t,r){let n=this.getActor(t);try{let i=mr(r.text,Ae()),a=JSON.parse(i);this.insertProperties(n,a)}catch(i){Z.error("error while parsing actor properties text",i)}}insertProperties(t,r){if(t.properties==null)t.properties=r;else for(let n in r)t.properties[n]=r[n]}boxEnd(){this.state.records.currentBox=void 0}addDetails(t,r){let n=this.getActor(t),i=document.getElementById(r.text);try{let a=i.innerHTML,s=JSON.parse(a);s.properties&&this.insertProperties(n,s.properties),s.links&&this.insertLinks(n,s.links)}catch(a){Z.error("error while parsing actor details text",a)}}getActorProperty(t,r){if(t?.properties!==void 0)return t.properties[r]}apply(t){if(Array.isArray(t))t.forEach(r=>{this.apply(r)});else switch(t.type){case"sequenceIndex":this.state.records.messages.push({id:this.state.records.messages.length.toString(),from:void 0,to:void 0,message:{start:t.sequenceIndex,step:t.sequenceIndexStep,visible:t.sequenceVisible},wrap:!1,type:t.signalType});break;case"addParticipant":this.addActor(t.actor,t.actor,t.description,t.draw,t.config);break;case"createParticipant":if(this.state.records.actors.has(t.actor))throw new Error("It is not possible to have actors with the same id, even if one is destroyed before the next is created. Use 'AS' aliases to simulate the behavior");this.state.records.lastCreated=t.actor,this.addActor(t.actor,t.actor,t.description,t.draw,t.config),this.state.records.createdActors.set(t.actor,this.state.records.messages.length);break;case"destroyParticipant":this.state.records.lastDestroyed=t.actor,this.state.records.destroyedActors.set(t.actor,this.state.records.messages.length);break;case"activeStart":this.addSignal(t.actor,void 0,void 0,t.signalType);break;case"centralConnection":this.addSignal(t.actor,void 0,void 0,t.signalType);break;case"centralConnectionReverse":this.addSignal(t.actor,void 0,void 0,t.signalType);break;case"activeEnd":this.addSignal(t.actor,void 0,void 0,t.signalType);break;case"addNote":this.addNote(t.actor,t.placement,t.text);break;case"addLinks":this.addLinks(t.actor,t.text);break;case"addALink":this.addALink(t.actor,t.text);break;case"addProperties":this.addProperties(t.actor,t.text);break;case"addDetails":this.addDetails(t.actor,t.text);break;case"addMessage":if(this.state.records.lastCreated){if(t.to!==this.state.records.lastCreated)throw new Error("The created participant "+this.state.records.lastCreated.name+" does not have an associated creating message after its declaration. Please check the sequence diagram.");this.state.records.lastCreated=void 0}else if(this.state.records.lastDestroyed){if(t.to!==this.state.records.lastDestroyed&&t.from!==this.state.records.lastDestroyed)throw new Error("The destroyed participant "+this.state.records.lastDestroyed.name+" does not have an associated destroying message after its declaration. Please check the sequence diagram.");this.state.records.lastDestroyed=void 0}this.addSignal(t.from,t.to,t.msg,t.signalType,t.activate,t.centralConnection);break;case"boxStart":this.addBox(t.boxData);break;case"boxEnd":this.boxEnd();break;case"loopStart":this.addSignal(void 0,void 0,t.loopText,t.signalType);break;case"loopEnd":this.addSignal(void 0,void 0,void 0,t.signalType);break;case"rectStart":this.addSignal(void 0,void 0,t.color,t.signalType);break;case"rectEnd":this.addSignal(void 0,void 0,void 0,t.signalType);break;case"optStart":this.addSignal(void 0,void 0,t.optText,t.signalType);break;case"optEnd":this.addSignal(void 0,void 0,void 0,t.signalType);break;case"altStart":this.addSignal(void 0,void 0,t.altText,t.signalType);break;case"else":this.addSignal(void 0,void 0,t.altText,t.signalType);break;case"altEnd":this.addSignal(void 0,void 0,void 0,t.signalType);break;case"setAccTitle":kr(t.text);break;case"parStart":this.addSignal(void 0,void 0,t.parText,t.signalType);break;case"and":this.addSignal(void 0,void 0,t.parText,t.signalType);break;case"parEnd":this.addSignal(void 0,void 0,void 0,t.signalType);break;case"criticalStart":this.addSignal(void 0,void 0,t.criticalText,t.signalType);break;case"option":this.addSignal(void 0,void 0,t.optionText,t.signalType);break;case"criticalEnd":this.addSignal(void 0,void 0,void 0,t.signalType);break;case"breakStart":this.addSignal(void 0,void 0,t.breakText,t.signalType);break;case"breakEnd":this.addSignal(void 0,void 0,void 0,t.signalType);break}}getConfig(){return Ae().sequence}}});var e6t,eIe,tIe=F(()=>{"use strict";Xt();e6t=o(e=>{let t=e.dropShadow??"none",{look:r}=Ae();return`.actor { + stroke: ${e.actorBorder}; + fill: ${e.actorBkg}; + stroke-width: ${e.strokeWidth??1}; + } + + rect.actor.outer-path[data-look="neo"] { + filter: ${t}; + } + + rect.note[data-look="neo"] { + stroke:${e.noteBorderColor}; + fill:${e.noteBkgColor}; + filter: ${t}; + } + + text.actor > tspan { + fill: ${e.actorTextColor}; + stroke: none; + } + + .actor-line { + stroke: ${e.actorLineColor}; + } + + .innerArc { + stroke-width: 1.5; + stroke-dasharray: none; + } + + .messageLine0 { + stroke-width: 1.5; + stroke-dasharray: none; + stroke: ${e.signalColor}; + } + + .messageLine1 { + stroke-width: 1.5; + stroke-dasharray: 2, 2; + stroke: ${e.signalColor}; + } + + [id$="-arrowhead"] path { + fill: ${e.signalColor}; + stroke: ${e.signalColor}; + } + + .sequenceNumber { + fill: ${e.sequenceNumberColor}; + } + + [id$="-sequencenumber"] { + fill: ${e.signalColor}; + } + + [id$="-crosshead"] path { + fill: ${e.signalColor}; + stroke: ${e.signalColor}; + } + + .messageText { + fill: ${e.signalTextColor}; + stroke: none; + } + + .labelBox { + stroke: ${e.labelBoxBorderColor}; + fill: ${e.labelBoxBkgColor}; + filter: ${r==="neo"?t:"none"}; + } + + .labelText, .labelText > tspan { + fill: ${e.labelTextColor}; + stroke: none; + } + + .loopText, .loopText > tspan { + fill: ${e.loopTextColor}; + stroke: none; + } + + .sectionTitle, .sectionTitle > tspan { + fill: ${e.loopTextColor}; + stroke: none; + } + + .loopLine { + stroke-width: 2px; + stroke-dasharray: 2, 2; + stroke: ${e.labelBoxBorderColor}; + fill: ${e.labelBoxBorderColor}; + } + + .note { + //stroke: #decc93; + stroke: ${e.noteBorderColor}; + fill: ${e.noteBkgColor}; + } + + .noteText, .noteText > tspan { + fill: ${e.noteTextColor}; + stroke: none; + ${e.noteFontWeight?`font-weight: ${e.noteFontWeight};`:""} + } + + .activation0 { + fill: ${e.activationBkgColor}; + stroke: ${e.activationBorderColor}; + } + + .activation1 { + fill: ${e.activationBkgColor}; + stroke: ${e.activationBorderColor}; + } + + .activation2 { + fill: ${e.activationBkgColor}; + stroke: ${e.activationBorderColor}; + } + + .actorPopupMenu { + position: absolute; + } + + .actorPopupMenuPanel { + position: absolute; + fill: ${e.actorBkg}; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + filter: drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4)); +} + .actor-man circle, line { + fill: ${e.actorBkg}; + stroke-width: 2px; + } + + g rect.rect { + filter: ${t}; + stroke: ${e.nodeBorder}; + } +`},"getStyles"),eIe=e6t});var lY,Ep,Ap,Rp,CL,S0,hd,Gw,t6t,wL,Vw,E0,rIe,nn,cY,r6t,n6t,i6t,a6t,s6t,o6t,l6t,c6t,u6t,h6t,d6t,f6t,p6t,nIe,m6t,g6t,y6t,v6t,x6t,b6t,T6t,C6t,iIe,w6t,dd,k6t,S6t,E6t,A6t,R6t,Un,aIe=F(()=>{"use strict";lY=Xs(Ly(),1);ur();Qt();Vr();Ud();Ep=36,Ap="actor-top",Rp="actor-bottom",CL="actor-box",S0="actor-man",hd=new Set(["redux-color","redux-dark-color"]),Gw=o(function(e,t){let r=fm(e,t);return _t().look==="neo"&&r.attr("data-look","neo"),r},"drawRect"),t6t=o(function(e,t,r,n,i){if(t.links===void 0||t.links===null||Object.keys(t.links).length===0)return{height:0,width:0};let a=t.links,s=t.actorCnt,l=t.rectData;var u="none";i&&(u="block !important");let h=e.append("g");h.attr("id","actor"+s+"_popup"),h.attr("class","actorPopupMenu"),h.attr("display",u);var d="";l.class!==void 0&&(d=" "+l.class);let f=l.width>r?l.width:r,p=h.append("rect");if(p.attr("class","actorPopupMenuPanel"+d),p.attr("x",l.x),p.attr("y",l.height),p.attr("fill",l.fill),p.attr("stroke",l.stroke),p.attr("width",f),p.attr("height",l.height),p.attr("rx",l.rx),p.attr("ry",l.ry),a!=null){var m=20;for(let v in a){var g=h.append("a"),y=(0,lY.sanitizeUrl)(a[v]);g.attr("xlink:href",y),g.attr("target","_blank"),k6t(n)(v,g,l.x+10,l.height+m,f,20,{class:"actor"},n),m+=30}}return p.attr("height",m),{height:l.height+m,width:f}},"drawPopup"),wL=o(function(e){return"var pu = document.getElementById('"+e+"'); if (pu != null) { pu.style.display = pu.style.display == 'block' ? 'none' : 'block'; }"},"popupMenuToggle"),Vw=o(async function(e,t,r=null){let n=e.append("foreignObject"),i=await ey(t.text,_t()),s=n.append("xhtml:div").attr("style","width: fit-content;").attr("xmlns","http://www.w3.org/1999/xhtml").html(i).node().getBoundingClientRect();if(n.attr("height",Math.round(s.height)).attr("width",Math.round(s.width)),t.class==="noteText"){let l=e.node().firstChild;l.setAttribute("height",s.height+2*t.textMargin);let u=l.getBBox();n.attr("x",Math.round(u.x+u.width/2-s.width/2)).attr("y",Math.round(u.y+u.height/2-s.height/2))}else if(r){let{startx:l,stopx:u,starty:h}=r;if(l>u){let d=l;l=u,u=d}n.attr("x",Math.round(l+Math.abs(l-u)/2-s.width/2)),t.class==="loopText"?n.attr("y",Math.round(h)):n.attr("y",Math.round(h-s.height))}return[n]},"drawKatex"),E0=o(function(e,t){let r=0,n=0,i=t.text.split(xt.lineBreakRegex),[a,s]=As(t.fontSize),l=[],u=0,h=o(()=>t.y,"yfunc");if(t.valign!==void 0&&t.textMargin!==void 0&&t.textMargin>0)switch(t.valign){case"top":case"start":h=o(()=>Math.round(t.y+t.textMargin),"yfunc");break;case"middle":case"center":h=o(()=>Math.round(t.y+(r+n+t.textMargin)/2),"yfunc");break;case"bottom":case"end":h=o(()=>Math.round(t.y+(r+n+2*t.textMargin)-t.textMargin),"yfunc");break}if(t.anchor!==void 0&&t.textMargin!==void 0&&t.width!==void 0)switch(t.anchor){case"left":case"start":t.x=Math.round(t.x+t.textMargin),t.anchor="start",t.dominantBaseline="middle",t.alignmentBaseline="middle";break;case"middle":case"center":t.x=Math.round(t.x+t.width/2),t.anchor="middle",t.dominantBaseline="middle",t.alignmentBaseline="middle";break;case"right":case"end":t.x=Math.round(t.x+t.width-t.textMargin),t.anchor="end",t.dominantBaseline="middle",t.alignmentBaseline="middle";break}for(let[d,f]of i.entries()){t.textMargin!==void 0&&t.textMargin===0&&a!==void 0&&(u=d*a);let p=e.append("text");p.attr("x",t.x),p.attr("y",h()),t.anchor!==void 0&&p.attr("text-anchor",t.anchor).attr("dominant-baseline",t.dominantBaseline).attr("alignment-baseline",t.alignmentBaseline),t.fontFamily!==void 0&&p.style("font-family",t.fontFamily),s!==void 0&&p.style("font-size",s),t.fontWeight!==void 0&&p.style("font-weight",t.fontWeight),t.fill!==void 0&&p.attr("fill",t.fill),t.class!==void 0&&p.attr("class",t.class),t.dy!==void 0?p.attr("dy",t.dy):u!==0&&p.attr("dy",u);let m=f||cP;if(t.tspan){let g=p.append("tspan");g.attr("x",t.x),t.fill!==void 0&&g.attr("fill",t.fill),g.text(m)}else p.text(m);t.valign!==void 0&&t.textMargin!==void 0&&t.textMargin>0&&(n+=(p._groups||p)[0][0].getBBox().height,r=n),l.push(p)}return l},"drawText"),rIe=o(function(e,t){function r(i,a,s,l,u){return i+","+a+" "+(i+s)+","+a+" "+(i+s)+","+(a+l-u)+" "+(i+s-u*1.2)+","+(a+l)+" "+i+","+(a+l)}o(r,"genPoints");let n=e.append("polygon");return n.attr("points",r(t.x,t.y,t.width,t.height,7)),n.attr("class","labelBox"),t.y=t.y+t.height/2,E0(e,t),n},"drawLabel"),nn=-1,cY=o((e,t,r,n)=>{e.select&&r.forEach(i=>{let a=t.get(i),s=e.select("#actor"+a.actorCnt);!n.mirrorActors&&a.stopy?s.attr("y2",a.stopy+a.height/2):n.mirrorActors&&s.attr("y2",a.stopy)})},"fixLifeLineHeights"),r6t=o(function(e,t,r,n,i){let a=n?t.stopy:t.starty,s=t.x+t.width/2,l=a+t.height,{look:u,theme:h,themeVariables:d}=r,{bkgColorArray:f,borderColorArray:p}=d,m=e.append("g").lower();var g=m;n||(nn++,Object.keys(t.links||{}).length&&!r.forceMenus&&g.attr("onclick",wL(`actor${nn}_popup`)).attr("cursor","pointer"),g.append("line").attr("id","actor"+nn).attr("x1",s).attr("y1",l).attr("x2",s).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),g=m.append("g"),t.actorCnt=nn,t.links!=null&&g.attr("id","root-"+nn),u==="neo"&&g.attr("data-look","neo"));let y=Fa();var v="actor";t.properties?.class?v=t.properties.class:y.fill="#eaeaea",n?v+=` ${Rp}`:v+=` ${Ap}`,y.x=t.x,y.y=a,y.width=t.width,y.height=t.height,y.class=v,y.rx=3,y.ry=3,y.name=t.name,u==="neo"&&(y.rx=6,y.ry=6);let x=Gw(g,y),b=i.get(t.name)??0;if(hd.has(h)&&(x.style("stroke",p[b%p.length]),x.style("fill",f[b%p.length])),u==="neo"&&x.attr("filter","url(#drop-shadow)"),t.rectData=y,t.properties?.icon){let k=t.properties.icon.trim();k.charAt(0)==="@"?zE(g,y.x+y.width-20,y.y+10,k.substr(1)):FE(g,y.x+y.width-20,y.y+10,k)}n||(g.attr("data-et","participant"),g.attr("data-type","participant"),g.attr("data-id",t.name)),dd(r,ni(t.description))(t.description,g,y.x,y.y,y.width,y.height,{class:`actor ${CL}`},r);let T=t.height;if(x.node){let k=x.node().getBBox();t.height=k.height,T=k.height}return T},"drawActorTypeParticipant"),n6t=o(function(e,t,r,n,i){let a=n?t.stopy:t.starty,s=t.x+t.width/2,l=a+t.height,{look:u,theme:h,themeVariables:d}=r,{bkgColorArray:f,borderColorArray:p}=d,m=e.append("g").lower();var g=m;n||(nn++,Object.keys(t.links||{}).length&&!r.forceMenus&&g.attr("onclick",wL(`actor${nn}_popup`)).attr("cursor","pointer"),g.append("line").attr("id","actor"+nn).attr("x1",s).attr("y1",l).attr("x2",s).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),g=m.append("g"),t.actorCnt=nn,t.links!=null&&g.attr("id","root-"+nn),u==="neo"&&g.attr("data-look","neo"));let y=Fa();var v="actor";t.properties?.class?v=t.properties.class:y.fill="#eaeaea",n?v+=` ${Rp}`:v+=` ${Ap}`,y.x=t.x,y.y=a,y.width=t.width,y.height=t.height,y.class=v,y.name=t.name;let x=6,b={...y,x:y.x+-x,y:y.y+ +x,class:"actor"},T=Gw(g,y),k=Gw(g,b);t.rectData=y,u==="neo"&&g.attr("filter","url(#drop-shadow)");let C=i.get(t.name)??0;if(hd.has(h)&&(T.style("stroke",p[C%p.length]),T.style("fill",f[C%p.length]),k.style("stroke",p[C%p.length]),k.style("fill",f[C%p.length])),t.properties?.icon){let S=t.properties.icon.trim();S.charAt(0)==="@"?zE(g,y.x+y.width-20,y.y+10,S.substr(1)):FE(g,y.x+y.width-20,y.y+10,S)}dd(r,ni(t.description))(t.description,g,y.x-x,y.y+x,y.width,y.height,{class:`actor ${CL}`},r);let w=t.height;if(T.node){let S=T.node().getBBox();t.height=S.height,w=S.height}return n||(g.attr("data-et","participant"),g.attr("data-type","collections"),g.attr("data-id",t.name)),w},"drawActorTypeCollections"),i6t=o(function(e,t,r,n,i){let a=n?t.stopy:t.starty,s=t.x+t.width/2,l=a+t.height,{look:u,theme:h,themeVariables:d}=r,{bkgColorArray:f,borderColorArray:p}=d,m=e.append("g").lower(),g=m;n||(nn++,Object.keys(t.links||{}).length&&!r.forceMenus&&g.attr("onclick",wL(`actor${nn}_popup`)).attr("cursor","pointer"),g.append("line").attr("id","actor"+nn).attr("x1",s).attr("y1",l).attr("x2",s).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),g=m.append("g"),t.actorCnt=nn,t.links!=null&&g.attr("id","root-"+nn),u==="neo"&&g.attr("data-look","neo"));let y=Fa(),v="actor";t.properties?.class?v=t.properties.class:y.fill="#eaeaea",n?v+=` ${Rp}`:v+=` ${Ap}`,g.attr("class",v),y.x=t.x,y.y=a,y.width=t.width,y.height=t.height,y.name=t.name;let x=y.height/2,b=x/(2.5+y.height/50),T=g.append("g"),k=g.append("g"),C=`M ${y.x},${y.y+x} + a ${b},${x} 0 0 0 0,${y.height} + h ${y.width-2*b} + a ${b},${x} 0 0 0 0,-${y.height} + Z + `;T.append("path").attr("d",C),k.append("path").attr("d",`M ${y.x},${y.y+x} + a ${b},${x} 0 0 0 0,${y.height}`),T.attr("transform",`translate(${b}, ${-(y.height/2)})`),k.attr("transform",`translate(${y.width-b}, ${-y.height/2})`),t.rectData=y,u==="neo"&&T.attr("filter","url(#drop-shadow)");let w=i.get(t.name)??0;if(hd.has(h)&&(T.style("stroke",p[w%p.length]),T.style("fill",f[w%p.length]),k.style("stroke",p[w%p.length]),k.style("fill",f[w%p.length])),t.properties?.icon){let L=t.properties.icon.trim(),N=y.x+y.width-20,I=y.y+10;L.charAt(0)==="@"?zE(g,N,I,L.substr(1)):FE(g,N,I,L)}dd(r,ni(t.description))(t.description,g,y.x,y.y,y.width,y.height,{class:`actor ${CL}`},r);let S=t.height,R=T.select("path:last-child");if(R.node()){let L=R.node().getBBox();t.height=L.height,S=L.height}return n||(g.attr("data-et","participant"),g.attr("data-type","queue"),g.attr("data-id",t.name)),S},"drawActorTypeQueue"),a6t=o(function(e,t,r,n,i,a){let s=n?t.stopy:t.starty,l=t.x+t.width/2,u=s+75,{look:h,theme:d,themeVariables:f}=r,{bkgColorArray:p,borderColorArray:m,actorBorder:g,actorBkg:y}=f,v=e.append("g").lower();n||(nn++,v.append("line").attr("id","actor"+nn).attr("x1",l).attr("y1",u).attr("x2",l).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),t.actorCnt=nn);let x=e.append("g"),b=S0;n?b+=` ${Rp}`:b+=` ${Ap}`,x.attr("class",b),x.attr("name",t.name);let T=Fa();T.x=t.x,T.y=s,T.fill="#eaeaea",T.width=t.width,T.height=t.height,T.class="actor";let k=t.x+t.width/2,C=s+32,w=22;x.append("defs").append("marker").attr("id",i+"-filled-head-control").attr("refX",11).attr("refY",5.8).attr("markerWidth",20).attr("markerHeight",28).attr("orient","172.5").attr("stroke-width",1.2).append("path").attr("d","M 14.4 5.6 L 7.2 10.4 L 8.8 5.6 L 7.2 0.8 Z"),x.append("circle").attr("cx",k).attr("cy",C).attr("r",w).attr("filter",`${h==="neo"?"url(#drop-shadow)":""}`),x.append("line").attr("marker-end","url(#"+i+"-filled-head-control)").attr("transform",`translate(${k}, ${C-w})`);let S=a.get(t.name)??0;hd.has(d)?(x.style("stroke",m[S%m.length]),x.style("fill",p[S%m.length])):(x.style("stroke",g),x.style("fill",y));let R=x.node().getBBox();return t.height=R.height+2*(r?.sequence?.labelBoxHeight??0),dd(r,ni(t.description))(t.description,x,T.x,T.y+w+(n?5:12),T.width,T.height,{class:`actor ${S0}`},r),n||(x.attr("data-et","participant"),x.attr("data-type","control"),x.attr("data-id",t.name)),t.height},"drawActorTypeControl"),s6t=o(function(e,t,r,n,i){let a=n?t.stopy:t.starty,s=t.x+t.width/2,l=a+75,{look:u,theme:h,themeVariables:d}=r,{bkgColorArray:f,borderColorArray:p}=d,m=e.append("g").lower(),g=e.append("g"),y="actor";n?y+=` ${Rp}`:y+=` ${Ap}`,g.attr("class",y),g.attr("name",t.name);let v=Fa();v.x=t.x,v.y=a,v.fill="#eaeaea",v.width=t.width,v.height=t.height,v.class="actor";let x=t.x+t.width/2,b=a+(n?10:25),T=22;g.append("circle").attr("cx",x).attr("cy",b).attr("r",T).attr("width",t.width).attr("height",t.height),g.append("line").attr("x1",x-T).attr("x2",x+T).attr("y1",b+T).attr("y2",b+T).attr("stroke-width",2),u==="neo"&&g.attr("filter","url(#drop-shadow)");let k=i.get(t.name)??0;hd.has(h)&&(g.style("stroke",p[k%p.length]),g.style("fill",f[k%p.length]));let C=g.node().getBBox();return t.height=C.height+(r?.sequence?.labelBoxHeight??0),n||(nn++,m.append("line").attr("id","actor"+nn).attr("x1",s).attr("y1",l).attr("x2",s).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),t.actorCnt=nn),dd(r,ni(t.description))(t.description,g,v.x,v.y+(n?15:30),v.width,v.height,{class:`actor ${S0}`},r),n?g.attr("transform",`translate(0, ${T})`):(g.attr("transform",`translate(0, ${T/2-5})`),g.attr("data-et","participant"),g.attr("data-type","entity"),g.attr("data-id",t.name)),t.height},"drawActorTypeEntity"),o6t=o(function(e,t,r,n,i){let a=n?t.stopy:t.starty,s=t.x+t.width/2,l=a+t.height+2*r.boxTextMargin,{theme:u,themeVariables:h,look:d}=r,{bkgColorArray:f,borderColorArray:p,actorBorder:m}=h,g=e.append("g").lower(),y=g;n||(nn++,Object.keys(t.links||{}).length&&!r.forceMenus&&y.attr("onclick",wL(`actor${nn}_popup`)).attr("cursor","pointer"),y.append("line").attr("id","actor"+nn).attr("x1",s).attr("y1",l).attr("x2",s).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),y=g.append("g"),t.actorCnt=nn,t.links!=null&&y.attr("id","root-"+nn),d==="neo"&&y.attr("data-look","neo"));let v=Fa(),x="actor";t.properties?.class?x=t.properties.class:v.fill="#eaeaea",n?x+=` ${Rp}`:x+=` ${Ap}`,v.x=t.x,v.y=a,v.width=t.width,v.height=t.height,v.class=x,v.name=t.name,v.x=t.x,v.y=a;let b=v.width/3,T=v.width/3,k=b/2,C=k/(2.5+b/50),w=y.append("g");w.attr("class",x);let S=` + M ${v.x},${v.y+C} + a ${k},${C} 0 0 0 ${b},0 + a ${k},${C} 0 0 0 -${b},0 + l 0,${T-2*C} + a ${k},${C} 0 0 0 ${b},0 + l 0,-${T-2*C} +`;w.append("path").attr("d",S),d==="neo"&&w.attr("filter","url(#drop-shadow)");let R=i.get(t.name)??0;hd.has(u)?(w.style("stroke",p[R%p.length]),w.style("fill",f[R%p.length])):w.style("stroke",m),w.attr("transform",`translate(${b}, ${C})`),t.rectData=v,dd(r,ni(t.description))(t.description,y,v.x,v.y+35,v.width,v.height,{class:`actor ${CL}`},r);let L=w.select("path:last-child");if(L.node()){let N=L.node().getBBox();t.height=N.height+(r.sequence.labelBoxHeight??0)}return n||(y.attr("data-et","participant"),y.attr("data-type","database"),y.attr("data-id",t.name)),t.height},"drawActorTypeDatabase"),l6t=o(function(e,t,r,n,i){let a=n?t.stopy:t.starty,s=t.x+t.width/2,l=a+80,u=22,h=e.append("g").lower(),{look:d,theme:f,themeVariables:p}=r,{bkgColorArray:m,borderColorArray:g,actorBorder:y}=p;n||(nn++,h.append("line").attr("id","actor"+nn).attr("x1",s).attr("y1",l).attr("x2",s).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),t.actorCnt=nn);let v=e.append("g"),x=S0;n?x+=` ${Rp}`:x+=` ${Ap}`,v.attr("class",x),v.attr("name",t.name);let b=Fa();b.x=t.x,b.y=a,b.fill="#eaeaea",b.width=t.width,b.height=t.height,b.class="actor",v.append("line").attr("id","actor-man-torso"+nn).attr("x1",t.x+t.width/2-u*2.5).attr("y1",a+12).attr("x2",t.x+t.width/2-15).attr("y2",a+12),v.append("line").attr("id","actor-man-arms"+nn).attr("x1",t.x+t.width/2-u*2.5).attr("y1",a+2).attr("x2",t.x+t.width/2-u*2.5).attr("y2",a+22),v.append("circle").attr("cx",t.x+t.width/2).attr("cy",a+12).attr("r",u),d==="neo"&&v.attr("filter","url(#drop-shadow)");let T=i.get(t.name)??0;hd.has(f)?(v.style("stroke",g[T%g.length]),v.style("fill",m[T%g.length])):v.style("stroke",y);let k=v.node().getBBox();return t.height=k.height+(r.sequence.labelBoxHeight??0),dd(r,ni(t.description))(t.description,v,b.x,b.y+15,b.width,b.height,{class:`actor ${S0}`},r),v.attr("transform",`translate(0,${u/2+10})`),n||(v.attr("data-et","participant"),v.attr("data-type","boundary"),v.attr("data-id",t.name)),t.height},"drawActorTypeBoundary"),c6t=o(function(e,t,r,n,i){let a=n?t.stopy:t.starty,s=t.x+t.width/2,l=a+80,{look:u,theme:h,themeVariables:d}=r,{bkgColorArray:f,borderColorArray:p,actorBorder:m}=d,g=e.append("g").lower();n||(nn++,g.append("line").attr("id","actor"+nn).attr("x1",s).attr("y1",l).attr("x2",s).attr("y2",2e3).attr("class","actor-line 200").attr("stroke-width","0.5px").attr("stroke","#999").attr("name",t.name).attr("data-et","life-line").attr("data-id",t.name),t.actorCnt=nn);let y=e.append("g"),v=S0;n?v+=` ${Rp}`:v+=` ${Ap}`,y.attr("class",v),y.attr("name",t.name),n||y.attr("data-et","participant").attr("data-type","actor").attr("data-id",t.name);let x=u==="neo"?.5:1,b=u==="neo"?a+(1-x)*30:a;y.append("line").attr("id","actor-man-torso"+nn).attr("x1",s).attr("y1",b+25*x).attr("x2",s).attr("y2",b+45*x),y.append("line").attr("id","actor-man-arms"+nn).attr("x1",s-Ep/2*x).attr("y1",b+33*x).attr("x2",s+Ep/2*x).attr("y2",b+33*x),y.append("line").attr("x1",s-Ep/2*x).attr("y1",b+60*x).attr("x2",s).attr("y2",b+45*x),y.append("line").attr("x1",s).attr("y1",b+45*x).attr("x2",s+(Ep/2-2)*x).attr("y2",b+60*x);let T=y.append("circle");T.attr("cx",t.x+t.width/2),T.attr("cy",b+10*x),T.attr("r",15*x),T.attr("width",t.width*x),T.attr("height",t.height*x);let k=y.node().getBBox();t.height=k.height;let C=Fa();C.x=t.x,C.y=b,C.fill="#eaeaea",C.width=t.width,C.height=t.height/x,C.class="actor",C.rx=3,C.ry=3;let w=i.get(t.name)??0;return hd.has(h)?(y.style("stroke",p[w%p.length]),y.style("fill",f[w%p.length])):y.style("stroke",m),dd(r,ni(t.description))(t.description,y,C.x,b+35*x-(u==="neo"?10:0),C.width,C.height,{class:`actor ${S0}`},r),t.height},"drawActorTypeActor"),u6t=o(async function(e,t,r,n,i,a,s){let l=s??new Map([...a.db.getActors().values()].map((u,h)=>[u.name,h]));switch(t.type){case"actor":return await c6t(e,t,r,n,l);case"participant":return await r6t(e,t,r,n,l);case"boundary":return await l6t(e,t,r,n,l);case"control":return await a6t(e,t,r,n,i,l);case"entity":return await s6t(e,t,r,n,l);case"database":return await o6t(e,t,r,n,l);case"collections":return await n6t(e,t,r,n,l);case"queue":return await i6t(e,t,r,n,l)}},"drawActor"),h6t=o(function(e,t,r){let i=e.append("g");nIe(i,t),t.name&&dd(r)(t.name,i,t.x,t.y+r.boxTextMargin+(t.textMaxHeight||0)/2,t.width,0,{class:"text"},r),i.lower()},"drawBox"),d6t=o(function(e){return e.append("g")},"anchorElement"),f6t=o(function(e,t,r,n,i,a,s){let{theme:l,themeVariables:u}=n,{bkgColorArray:h,borderColorArray:d,mainBkg:f}=u,p=Fa(),m=t.anchored,g=t.actor;p.x=t.startx,p.y=t.starty,p.class="activation"+i%3,p.width=t.stopx-t.startx,p.height=r-t.starty;let y=Gw(m,p),x=(s??new Map([...a.db.getActors().values()].map((b,T)=>[b.name,T]))).get(g)??0;hd.has(l)&&(y.style("stroke",d[x%d.length]),y.style("fill",h[x%d.length]??f))},"drawActivation"),p6t=o(async function(e,t,r,n,i){let{boxMargin:a,boxTextMargin:s,labelBoxHeight:l,labelBoxWidth:u,messageFontFamily:h,messageFontSize:d,messageFontWeight:f}=n,p=e.append("g").attr("data-et","control-structure").attr("data-id","i"+i.id),m=o(function(v,x,b,T){return p.append("line").attr("x1",v).attr("y1",x).attr("x2",b).attr("y2",T).attr("class","loopLine")},"drawLoopLine");m(t.startx,t.starty,t.stopx,t.starty),m(t.stopx,t.starty,t.stopx,t.stopy),m(t.startx,t.stopy,t.stopx,t.stopy),m(t.startx,t.starty,t.startx,t.stopy),t.sections!==void 0&&t.sections.forEach(function(v){m(t.startx,v.y,t.stopx,v.y).style("stroke-dasharray","3, 3")});let g=c2();g.text=r,g.x=t.startx,g.y=t.starty,g.fontFamily=h,g.fontSize=d,g.fontWeight=f,g.anchor="middle",g.valign="middle",g.tspan=!1,g.width=Math.max(u??0,50),g.height=l+(n.look==="neo"?15:0)||20,g.textMargin=s,g.class="labelText",rIe(p,g),g=iIe(),g.text=t.title,g.x=t.startx+u/2+(t.stopx-t.startx)/2,g.y=t.starty+a+s,g.anchor="middle",g.valign="middle",g.textMargin=s,g.class="loopText",g.fontFamily=h,g.fontSize=d,g.fontWeight=f,g.wrap=!0;let y=ni(g.text)?await Vw(p,g,t):E0(p,g);if(t.sectionTitles!==void 0){for(let[v,x]of Object.entries(t.sectionTitles))if(x.message){g.text=x.message,g.x=t.startx+(t.stopx-t.startx)/2,g.y=t.sections[v].y+a+s,g.class="sectionTitle",g.anchor="middle",g.valign="middle",g.tspan=!1,g.fontFamily=h,g.fontSize=d,g.fontWeight=f,g.wrap=t.wrap,ni(g.text)?(t.starty=t.sections[v].y,await Vw(p,g,t)):E0(p,g);let b=Math.round(y.map(T=>(T._groups||T)[0][0].getBBox().height).reduce((T,k)=>T+k));t.sections[v].height+=b-(a+s)}}return t.height=Math.round(t.stopy-t.starty),p},"drawLoop"),nIe=o(function(e,t){$E(e,t)},"drawBackgroundRect"),m6t=o(function(e,t){e.append("defs").append("symbol").attr("id",t+"-database").attr("fill-rule","evenodd").attr("clip-rule","evenodd").append("path").attr("transform","scale(.5)").attr("d","M12.258.001l.256.004.255.005.253.008.251.01.249.012.247.015.246.016.242.019.241.02.239.023.236.024.233.027.231.028.229.031.225.032.223.034.22.036.217.038.214.04.211.041.208.043.205.045.201.046.198.048.194.05.191.051.187.053.183.054.18.056.175.057.172.059.168.06.163.061.16.063.155.064.15.066.074.033.073.033.071.034.07.034.069.035.068.035.067.035.066.035.064.036.064.036.062.036.06.036.06.037.058.037.058.037.055.038.055.038.053.038.052.038.051.039.05.039.048.039.047.039.045.04.044.04.043.04.041.04.04.041.039.041.037.041.036.041.034.041.033.042.032.042.03.042.029.042.027.042.026.043.024.043.023.043.021.043.02.043.018.044.017.043.015.044.013.044.012.044.011.045.009.044.007.045.006.045.004.045.002.045.001.045v17l-.001.045-.002.045-.004.045-.006.045-.007.045-.009.044-.011.045-.012.044-.013.044-.015.044-.017.043-.018.044-.02.043-.021.043-.023.043-.024.043-.026.043-.027.042-.029.042-.03.042-.032.042-.033.042-.034.041-.036.041-.037.041-.039.041-.04.041-.041.04-.043.04-.044.04-.045.04-.047.039-.048.039-.05.039-.051.039-.052.038-.053.038-.055.038-.055.038-.058.037-.058.037-.06.037-.06.036-.062.036-.064.036-.064.036-.066.035-.067.035-.068.035-.069.035-.07.034-.071.034-.073.033-.074.033-.15.066-.155.064-.16.063-.163.061-.168.06-.172.059-.175.057-.18.056-.183.054-.187.053-.191.051-.194.05-.198.048-.201.046-.205.045-.208.043-.211.041-.214.04-.217.038-.22.036-.223.034-.225.032-.229.031-.231.028-.233.027-.236.024-.239.023-.241.02-.242.019-.246.016-.247.015-.249.012-.251.01-.253.008-.255.005-.256.004-.258.001-.258-.001-.256-.004-.255-.005-.253-.008-.251-.01-.249-.012-.247-.015-.245-.016-.243-.019-.241-.02-.238-.023-.236-.024-.234-.027-.231-.028-.228-.031-.226-.032-.223-.034-.22-.036-.217-.038-.214-.04-.211-.041-.208-.043-.204-.045-.201-.046-.198-.048-.195-.05-.19-.051-.187-.053-.184-.054-.179-.056-.176-.057-.172-.059-.167-.06-.164-.061-.159-.063-.155-.064-.151-.066-.074-.033-.072-.033-.072-.034-.07-.034-.069-.035-.068-.035-.067-.035-.066-.035-.064-.036-.063-.036-.062-.036-.061-.036-.06-.037-.058-.037-.057-.037-.056-.038-.055-.038-.053-.038-.052-.038-.051-.039-.049-.039-.049-.039-.046-.039-.046-.04-.044-.04-.043-.04-.041-.04-.04-.041-.039-.041-.037-.041-.036-.041-.034-.041-.033-.042-.032-.042-.03-.042-.029-.042-.027-.042-.026-.043-.024-.043-.023-.043-.021-.043-.02-.043-.018-.044-.017-.043-.015-.044-.013-.044-.012-.044-.011-.045-.009-.044-.007-.045-.006-.045-.004-.045-.002-.045-.001-.045v-17l.001-.045.002-.045.004-.045.006-.045.007-.045.009-.044.011-.045.012-.044.013-.044.015-.044.017-.043.018-.044.02-.043.021-.043.023-.043.024-.043.026-.043.027-.042.029-.042.03-.042.032-.042.033-.042.034-.041.036-.041.037-.041.039-.041.04-.041.041-.04.043-.04.044-.04.046-.04.046-.039.049-.039.049-.039.051-.039.052-.038.053-.038.055-.038.056-.038.057-.037.058-.037.06-.037.061-.036.062-.036.063-.036.064-.036.066-.035.067-.035.068-.035.069-.035.07-.034.072-.034.072-.033.074-.033.151-.066.155-.064.159-.063.164-.061.167-.06.172-.059.176-.057.179-.056.184-.054.187-.053.19-.051.195-.05.198-.048.201-.046.204-.045.208-.043.211-.041.214-.04.217-.038.22-.036.223-.034.226-.032.228-.031.231-.028.234-.027.236-.024.238-.023.241-.02.243-.019.245-.016.247-.015.249-.012.251-.01.253-.008.255-.005.256-.004.258-.001.258.001zm-9.258 20.499v.01l.001.021.003.021.004.022.005.021.006.022.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.023.018.024.019.024.021.024.022.025.023.024.024.025.052.049.056.05.061.051.066.051.07.051.075.051.079.052.084.052.088.052.092.052.097.052.102.051.105.052.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.048.144.049.147.047.152.047.155.047.16.045.163.045.167.043.171.043.176.041.178.041.183.039.187.039.19.037.194.035.197.035.202.033.204.031.209.03.212.029.216.027.219.025.222.024.226.021.23.02.233.018.236.016.24.015.243.012.246.01.249.008.253.005.256.004.259.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.021.224-.024.22-.026.216-.027.212-.028.21-.031.205-.031.202-.034.198-.034.194-.036.191-.037.187-.039.183-.04.179-.04.175-.042.172-.043.168-.044.163-.045.16-.046.155-.046.152-.047.148-.048.143-.049.139-.049.136-.05.131-.05.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.053.083-.051.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.05.023-.024.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.023.01-.022.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.127l-.077.055-.08.053-.083.054-.085.053-.087.052-.09.052-.093.051-.095.05-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.045-.118.044-.12.043-.122.042-.124.042-.126.041-.128.04-.13.04-.132.038-.134.038-.135.037-.138.037-.139.035-.142.035-.143.034-.144.033-.147.032-.148.031-.15.03-.151.03-.153.029-.154.027-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.01-.179.008-.179.008-.181.006-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.006-.179-.008-.179-.008-.178-.01-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.027-.153-.029-.151-.03-.15-.03-.148-.031-.146-.032-.145-.033-.143-.034-.141-.035-.14-.035-.137-.037-.136-.037-.134-.038-.132-.038-.13-.04-.128-.04-.126-.041-.124-.042-.122-.042-.12-.044-.117-.043-.116-.045-.113-.045-.112-.046-.109-.047-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.05-.093-.052-.09-.051-.087-.052-.085-.053-.083-.054-.08-.054-.077-.054v4.127zm0-5.654v.011l.001.021.003.021.004.021.005.022.006.022.007.022.009.022.01.022.011.023.012.023.013.023.015.024.016.023.017.024.018.024.019.024.021.024.022.024.023.025.024.024.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.052.11.051.114.051.119.052.123.05.127.051.131.05.135.049.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.044.171.042.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.022.23.02.233.018.236.016.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.012.241-.015.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.048.139-.05.136-.049.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.051.051-.049.023-.025.023-.024.021-.025.02-.024.019-.024.018-.024.017-.024.015-.023.014-.023.013-.024.012-.022.01-.023.01-.023.008-.022.006-.022.006-.022.004-.021.004-.022.001-.021.001-.021v-4.139l-.077.054-.08.054-.083.054-.085.052-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.049-.105.048-.106.047-.109.047-.111.046-.114.045-.115.044-.118.044-.12.044-.122.042-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.035-.143.033-.144.033-.147.033-.148.031-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.025-.161.024-.162.023-.163.022-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.011-.178.009-.179.009-.179.007-.181.007-.182.005-.182.004-.184.003-.184.002h-.37l-.184-.002-.184-.003-.182-.004-.182-.005-.181-.007-.179-.007-.179-.009-.178-.009-.176-.011-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.022-.162-.023-.161-.024-.159-.025-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.031-.146-.033-.145-.033-.143-.033-.141-.035-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.04-.126-.041-.124-.042-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.047-.105-.048-.102-.049-.1-.049-.097-.05-.095-.051-.093-.051-.09-.051-.087-.053-.085-.052-.083-.054-.08-.054-.077-.054v4.139zm0-5.666v.011l.001.02.003.022.004.021.005.022.006.021.007.022.009.023.01.022.011.023.012.023.013.023.015.023.016.024.017.024.018.023.019.024.021.025.022.024.023.024.024.025.052.05.056.05.061.05.066.051.07.051.075.052.079.051.084.052.088.052.092.052.097.052.102.052.105.051.11.052.114.051.119.051.123.051.127.05.131.05.135.05.139.049.144.048.147.048.152.047.155.046.16.045.163.045.167.043.171.043.176.042.178.04.183.04.187.038.19.037.194.036.197.034.202.033.204.032.209.03.212.028.216.027.219.025.222.024.226.021.23.02.233.018.236.017.24.014.243.012.246.01.249.008.253.006.256.003.259.001.26-.001.257-.003.254-.006.25-.008.247-.01.244-.013.241-.014.237-.016.233-.018.231-.02.226-.022.224-.024.22-.025.216-.027.212-.029.21-.03.205-.032.202-.033.198-.035.194-.036.191-.037.187-.039.183-.039.179-.041.175-.042.172-.043.168-.044.163-.045.16-.045.155-.047.152-.047.148-.048.143-.049.139-.049.136-.049.131-.051.126-.05.123-.051.118-.052.114-.051.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.052.07-.051.065-.051.06-.051.056-.05.051-.049.023-.025.023-.025.021-.024.02-.024.019-.024.018-.024.017-.024.015-.023.014-.024.013-.023.012-.023.01-.022.01-.023.008-.022.006-.022.006-.022.004-.022.004-.021.001-.021.001-.021v-4.153l-.077.054-.08.054-.083.053-.085.053-.087.053-.09.051-.093.051-.095.051-.097.05-.1.049-.102.048-.105.048-.106.048-.109.046-.111.046-.114.046-.115.044-.118.044-.12.043-.122.043-.124.042-.126.041-.128.04-.13.039-.132.039-.134.038-.135.037-.138.036-.139.036-.142.034-.143.034-.144.033-.147.032-.148.032-.15.03-.151.03-.153.028-.154.028-.156.027-.158.026-.159.024-.161.024-.162.023-.163.023-.165.021-.166.02-.167.019-.169.018-.169.017-.171.016-.173.015-.173.014-.175.013-.175.012-.177.01-.178.01-.179.009-.179.007-.181.006-.182.006-.182.004-.184.003-.184.001-.185.001-.185-.001-.184-.001-.184-.003-.182-.004-.182-.006-.181-.006-.179-.007-.179-.009-.178-.01-.176-.01-.176-.012-.175-.013-.173-.014-.172-.015-.171-.016-.17-.017-.169-.018-.167-.019-.166-.02-.165-.021-.163-.023-.162-.023-.161-.024-.159-.024-.157-.026-.156-.027-.155-.028-.153-.028-.151-.03-.15-.03-.148-.032-.146-.032-.145-.033-.143-.034-.141-.034-.14-.036-.137-.036-.136-.037-.134-.038-.132-.039-.13-.039-.128-.041-.126-.041-.124-.041-.122-.043-.12-.043-.117-.044-.116-.044-.113-.046-.112-.046-.109-.046-.106-.048-.105-.048-.102-.048-.1-.05-.097-.049-.095-.051-.093-.051-.09-.052-.087-.052-.085-.053-.083-.053-.08-.054-.077-.054v4.153zm8.74-8.179l-.257.004-.254.005-.25.008-.247.011-.244.012-.241.014-.237.016-.233.018-.231.021-.226.022-.224.023-.22.026-.216.027-.212.028-.21.031-.205.032-.202.033-.198.034-.194.036-.191.038-.187.038-.183.04-.179.041-.175.042-.172.043-.168.043-.163.045-.16.046-.155.046-.152.048-.148.048-.143.048-.139.049-.136.05-.131.05-.126.051-.123.051-.118.051-.114.052-.11.052-.106.052-.101.052-.096.052-.092.052-.088.052-.083.052-.079.052-.074.051-.07.052-.065.051-.06.05-.056.05-.051.05-.023.025-.023.024-.021.024-.02.025-.019.024-.018.024-.017.023-.015.024-.014.023-.013.023-.012.023-.01.023-.01.022-.008.022-.006.023-.006.021-.004.022-.004.021-.001.021-.001.021.001.021.001.021.004.021.004.022.006.021.006.023.008.022.01.022.01.023.012.023.013.023.014.023.015.024.017.023.018.024.019.024.02.025.021.024.023.024.023.025.051.05.056.05.06.05.065.051.07.052.074.051.079.052.083.052.088.052.092.052.096.052.101.052.106.052.11.052.114.052.118.051.123.051.126.051.131.05.136.05.139.049.143.048.148.048.152.048.155.046.16.046.163.045.168.043.172.043.175.042.179.041.183.04.187.038.191.038.194.036.198.034.202.033.205.032.21.031.212.028.216.027.22.026.224.023.226.022.231.021.233.018.237.016.241.014.244.012.247.011.25.008.254.005.257.004.26.001.26-.001.257-.004.254-.005.25-.008.247-.011.244-.012.241-.014.237-.016.233-.018.231-.021.226-.022.224-.023.22-.026.216-.027.212-.028.21-.031.205-.032.202-.033.198-.034.194-.036.191-.038.187-.038.183-.04.179-.041.175-.042.172-.043.168-.043.163-.045.16-.046.155-.046.152-.048.148-.048.143-.048.139-.049.136-.05.131-.05.126-.051.123-.051.118-.051.114-.052.11-.052.106-.052.101-.052.096-.052.092-.052.088-.052.083-.052.079-.052.074-.051.07-.052.065-.051.06-.05.056-.05.051-.05.023-.025.023-.024.021-.024.02-.025.019-.024.018-.024.017-.023.015-.024.014-.023.013-.023.012-.023.01-.023.01-.022.008-.022.006-.023.006-.021.004-.022.004-.021.001-.021.001-.021-.001-.021-.001-.021-.004-.021-.004-.022-.006-.021-.006-.023-.008-.022-.01-.022-.01-.023-.012-.023-.013-.023-.014-.023-.015-.024-.017-.023-.018-.024-.019-.024-.02-.025-.021-.024-.023-.024-.023-.025-.051-.05-.056-.05-.06-.05-.065-.051-.07-.052-.074-.051-.079-.052-.083-.052-.088-.052-.092-.052-.096-.052-.101-.052-.106-.052-.11-.052-.114-.052-.118-.051-.123-.051-.126-.051-.131-.05-.136-.05-.139-.049-.143-.048-.148-.048-.152-.048-.155-.046-.16-.046-.163-.045-.168-.043-.172-.043-.175-.042-.179-.041-.183-.04-.187-.038-.191-.038-.194-.036-.198-.034-.202-.033-.205-.032-.21-.031-.212-.028-.216-.027-.22-.026-.224-.023-.226-.022-.231-.021-.233-.018-.237-.016-.241-.014-.244-.012-.247-.011-.25-.008-.254-.005-.257-.004-.26-.001-.26.001z")},"insertDatabaseIcon"),g6t=o(function(e,t){e.append("defs").append("symbol").attr("id",t+"-computer").attr("width","24").attr("height","24").append("path").attr("transform","scale(.5)").attr("d","M2 2v13h20v-13h-20zm18 11h-16v-9h16v9zm-10.228 6l.466-1h3.524l.467 1h-4.457zm14.228 3h-24l2-6h2.104l-1.33 4h18.45l-1.297-4h2.073l2 6zm-5-10h-14v-7h14v7z")},"insertComputerIcon"),y6t=o(function(e,t){e.append("defs").append("symbol").attr("id",t+"-clock").attr("width","24").attr("height","24").append("path").attr("transform","scale(.5)").attr("d","M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm5.848 12.459c.202.038.202.333.001.372-1.907.361-6.045 1.111-6.547 1.111-.719 0-1.301-.582-1.301-1.301 0-.512.77-5.447 1.125-7.445.034-.192.312-.181.343.014l.985 6.238 5.394 1.011z")},"insertClockIcon"),v6t=o(function(e,t){e.append("defs").append("marker").attr("id",t+"-arrowhead").attr("refX",7.9).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",12).attr("markerHeight",12).attr("orient","auto-start-reverse").append("path").attr("d","M -1 0 L 10 5 L 0 10 z")},"insertArrowHead"),x6t=o(function(e,t){e.append("defs").append("marker").attr("id",t+"-filled-head").attr("refX",15.5).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").append("path").attr("d","M 18,7 L9,13 L14,7 L9,1 Z")},"insertArrowFilledHead"),b6t=o(function(e,t){e.append("defs").append("marker").attr("id",t+"-sequencenumber").attr("refX",15).attr("refY",15).attr("markerWidth",60).attr("markerHeight",40).attr("orient","auto").append("circle").attr("cx",15).attr("cy",15).attr("r",6)},"insertSequenceNumber"),T6t=o(function(e,t){e.append("defs").append("marker").attr("id",t+"-crosshead").attr("markerWidth",15).attr("markerHeight",8).attr("orient","auto").attr("refX",4).attr("refY",4.5).append("path").attr("fill","none").attr("stroke","#000000").style("stroke-dasharray","0, 0").attr("stroke-width","1pt").attr("d","M 1,2 L 6,7 M 6,2 L 1,7")},"insertArrowCrossHead"),C6t=o(function(e,t){let{theme:r}=t;e.append("defs").append("filter").attr("id","drop-shadow").attr("height","130%").attr("width","130%").append("feDropShadow").attr("dx","4").attr("dy","4").attr("stdDeviation",0).attr("flood-opacity","0.06").attr("flood-color",`${r==="redux"||r==="redux-color"?"#000000":"#FFFFFF"}`)},"insertDropShadow"),iIe=o(function(){return{x:0,y:0,fill:void 0,anchor:void 0,style:"#666",width:void 0,height:void 0,textMargin:0,rx:0,ry:0,tspan:!0,valign:void 0}},"getTextObj"),w6t=o(function(){return{x:0,y:0,fill:"#EDF2AE",stroke:"#666",width:100,anchor:"start",height:100,rx:0,ry:0}},"getNoteRect"),dd=(function(){function e(a,s,l,u,h,d,f){let p=s.append("text").attr("x",l+h/2).attr("y",u+d/2+5).style("text-anchor","middle").text(a);i(p,f)}o(e,"byText");function t(a,s,l,u,h,d,f,p){let{actorFontSize:m,actorFontFamily:g,actorFontWeight:y}=p,[v,x]=As(m),b=a.split(xt.lineBreakRegex);for(let T=0;T{let s=A0(je),l=a.actorKeys.reduce((f,p)=>f+=e.get(p).width+(e.get(p).margin||0),0),u=je.boxMargin*8;l+=u,l-=2*je.boxTextMargin,a.wrap&&(a.name=Zt.wrapLabel(a.name,l-2*je.wrapPadding,s));let h=Zt.calculateTextDimensions(a.name,s);i=xt.getMax(h.height,i);let d=xt.getMax(l,h.width+2*je.wrapPadding);if(a.margin=je.boxTextMargin,la.textMaxHeight=i),xt.getMax(n,je.height)}var je,Et,_6t,sIe,A0,tx,hY,D6t,I6t,dY,lIe,cIe,kL,oIe,N6t,O6t,$6t,F6t,z6t,uY,G6t,uIe,V6t,W6t,q6t,hIe,dIe=F(()=>{"use strict";$r();aIe();vt();Vr();Vr();Ud();Xt();G0();Qt();$n();oY();je={},Et={data:{startx:void 0,stopx:void 0,starty:void 0,stopy:void 0},verticalPos:0,sequenceItems:[],activations:[],models:{getHeight:o(function(){return Math.max.apply(null,this.actors.length===0?[0]:this.actors.map(e=>e.height||0))+(this.loops.length===0?0:this.loops.map(e=>e.height||0).reduce((e,t)=>e+t))+(this.messages.length===0?0:this.messages.map(e=>e.height||0).reduce((e,t)=>e+t))+(this.notes.length===0?0:this.notes.map(e=>e.height||0).reduce((e,t)=>e+t))},"getHeight"),clear:o(function(){this.actors=[],this.boxes=[],this.loops=[],this.messages=[],this.notes=[]},"clear"),addBox:o(function(e){this.boxes.push(e)},"addBox"),addActor:o(function(e){this.actors.push(e)},"addActor"),addLoop:o(function(e){this.loops.push(e)},"addLoop"),addMessage:o(function(e){this.messages.push(e)},"addMessage"),addNote:o(function(e){this.notes.push(e)},"addNote"),lastActor:o(function(){return this.actors[this.actors.length-1]},"lastActor"),lastLoop:o(function(){return this.loops[this.loops.length-1]},"lastLoop"),lastMessage:o(function(){return this.messages[this.messages.length-1]},"lastMessage"),lastNote:o(function(){return this.notes[this.notes.length-1]},"lastNote"),actors:[],boxes:[],loops:[],messages:[],notes:[]},init:o(function(){this.sequenceItems=[],this.activations=[],this.models.clear(),this.data={startx:void 0,stopx:void 0,starty:void 0,stopy:void 0},this.verticalPos=0,cIe(Ae())},"init"),updateVal:o(function(e,t,r,n){e[t]===void 0?e[t]=r:e[t]=n(r,e[t])},"updateVal"),updateBounds:o(function(e,t,r,n){let i=this,a=0;function s(l){return o(function(h){a++;let d=i.sequenceItems.length-a+1;i.updateVal(h,"starty",t-d*je.boxMargin,Math.min),i.updateVal(h,"stopy",n+d*je.boxMargin,Math.max),i.updateVal(Et.data,"startx",e-d*je.boxMargin,Math.min),i.updateVal(Et.data,"stopx",r+d*je.boxMargin,Math.max),l!=="activation"&&(i.updateVal(h,"startx",e-d*je.boxMargin,Math.min),i.updateVal(h,"stopx",r+d*je.boxMargin,Math.max),i.updateVal(Et.data,"starty",t-d*je.boxMargin,Math.min),i.updateVal(Et.data,"stopy",n+d*je.boxMargin,Math.max))},"updateItemBounds")}o(s,"updateFn"),this.sequenceItems.forEach(s()),this.activations.forEach(s("activation"))},"updateBounds"),insert:o(function(e,t,r,n){let i=xt.getMin(e,r),a=xt.getMax(e,r),s=xt.getMin(t,n),l=xt.getMax(t,n);this.updateVal(Et.data,"startx",i,Math.min),this.updateVal(Et.data,"starty",s,Math.min),this.updateVal(Et.data,"stopx",a,Math.max),this.updateVal(Et.data,"stopy",l,Math.max),this.updateBounds(i,s,a,l)},"insert"),newActivation:o(function(e,t,r){let n=r.get(e.from),i=kL(e.from).length||0,a=n.x+n.width/2+(i-1)*je.activationWidth/2;this.activations.push({startx:a,starty:this.verticalPos+2,stopx:a+je.activationWidth,stopy:void 0,actor:e.from,anchored:Un.anchorElement(t)})},"newActivation"),endActivation:o(function(e){let t=this.activations.map(function(r){return r.actor}).lastIndexOf(e.from);return this.activations.splice(t,1)[0]},"endActivation"),createLoop:o(function(e={message:void 0,wrap:!1,width:void 0},t){return{startx:void 0,starty:this.verticalPos,stopx:void 0,stopy:void 0,title:e.message,wrap:e.wrap,width:e.width,height:0,fill:t}},"createLoop"),newLoop:o(function(e={message:void 0,wrap:!1,width:void 0},t){this.sequenceItems.push(this.createLoop(e,t))},"newLoop"),endLoop:o(function(){return this.sequenceItems.pop()},"endLoop"),isLoopOverlap:o(function(){return this.sequenceItems.length?this.sequenceItems[this.sequenceItems.length-1].overlap:!1},"isLoopOverlap"),addSectionToLoop:o(function(e){let t=this.sequenceItems.pop();t.sections=t.sections||[],t.sectionTitles=t.sectionTitles||[],t.sections.push({y:Et.getVerticalPos(),height:0}),t.sectionTitles.push(e),this.sequenceItems.push(t)},"addSectionToLoop"),saveVerticalPos:o(function(){this.isLoopOverlap()&&(this.savedVerticalPos=this.verticalPos)},"saveVerticalPos"),resetVerticalPos:o(function(){this.isLoopOverlap()&&(this.verticalPos=this.savedVerticalPos)},"resetVerticalPos"),bumpVerticalPos:o(function(e){this.verticalPos=this.verticalPos+e,this.data.stopy=xt.getMax(this.data.stopy,this.verticalPos)},"bumpVerticalPos"),getVerticalPos:o(function(){return this.verticalPos},"getVerticalPos"),getBounds:o(function(){return{bounds:this.data,models:this.models}},"getBounds")},_6t=o(async function(e,t,r){Et.bumpVerticalPos(je.boxMargin),t.height=je.boxMargin,t.starty=Et.getVerticalPos();let n=Fa();n.x=t.startx,n.y=t.starty,n.width=t.width||je.width,n.class="note";let i=e.append("g");i.attr("data-et","note"),i.attr("data-id","i"+r);let a=Un.drawRect(i,n),s=c2();s.x=t.startx,s.y=t.starty,s.width=n.width,s.dy="1em",s.text=t.message,s.class="noteText",s.fontFamily=je.noteFontFamily,s.fontSize=je.noteFontSize,s.fontWeight=je.noteFontWeight,s.anchor=je.noteAlign,s.textMargin=je.noteMargin,s.valign="center";let l=ni(s.text)?await Vw(i,s):E0(i,s),u=Math.round(l.map(h=>(h._groups||h)[0][0].getBBox().height).reduce((h,d)=>h+d));a.attr("height",u+2*je.noteMargin),t.height+=u+2*je.noteMargin,Et.bumpVerticalPos(u+2*je.noteMargin),t.stopy=t.starty+u+2*je.noteMargin,t.stopx=t.startx+n.width,Et.insert(t.startx,t.starty,t.stopx,t.stopy),Et.models.addNote(t)},"drawNote"),sIe=o(function(e,t,r,n,i,a,s){let l=n.db.getActors(),u=l.get(t.from),h=l.get(t.to),d=r.sequenceVisible,f=u.x+u.width/2,p=h.x+h.width/2,m=f<=p,g=uIe(t,n),y=e.append("g"),v=16.5,x=o((w,S)=>{let R=w?v:-v;return S?-R:R},"getCircleOffset"),b=o(w=>{y.append("circle").attr("cx",w).attr("cy",s).attr("r",5).attr("width",10).attr("height",10)},"drawCircle"),{CENTRAL_CONNECTION:T,CENTRAL_CONNECTION_REVERSE:k,CENTRAL_CONNECTION_DUAL:C}=n.db.LINETYPE;if(d)switch(t.centralConnection){case T:g&&(p+=x(m,!0));break;case k:g||(f+=x(m,!1));break;case C:g?p+=x(m,!0):f+=x(m,!1);break}switch(t.centralConnection){case T:b(p);break;case k:b(f);break;case C:b(f),b(p);break}},"drawCentralConnection"),A0=o(e=>({fontFamily:e.messageFontFamily,fontSize:e.messageFontSize,fontWeight:e.messageFontWeight}),"messageFont"),tx=o(e=>({fontFamily:e.noteFontFamily,fontSize:e.noteFontSize,fontWeight:e.noteFontWeight}),"noteFont"),hY=o(e=>({fontFamily:e.actorFontFamily,fontSize:e.actorFontSize,fontWeight:e.actorFontWeight}),"actorFont");o(L6t,"boundMessage");D6t=o(async function(e,t,r,n,i,a){let{startx:s,stopx:l,starty:u,message:h,type:d,sequenceIndex:f,sequenceVisible:p}=t,m=Zt.calculateTextDimensions(h,A0(je)),g=c2();g.x=Math.min(s,l),g.y=u+10,g.width=Math.abs(l-s),g.class="messageText",g.dy="1em",g.text=h,g.fontFamily=je.messageFontFamily,g.fontSize=je.messageFontSize,g.fontWeight=je.messageFontWeight,g.anchor=je.messageAlign,g.valign="center",g.textMargin=je.wrapPadding,g.tspan=!1,ni(g.text)?await Vw(e,g,{startx:s,stopx:l,starty:r}):E0(e,g);let y=m.width,v;if(s===l){let b=p||je.showSequenceNumbers,T=uIe(i,n),k=V6t(i,n),C=s+(b&&(T||k)?10:0);je.rightAngles?v=e.append("path").attr("d",`M ${C},${r} H ${s+xt.getMax(je.width/2,y/2)} V ${r+25} H ${s}`):v=e.append("path").attr("d","M "+C+","+r+" C "+(C+60)+","+(r-10)+" "+(s+60)+","+(r+30)+" "+s+","+(r+20)),uY(i,n)&&sIe(e,i,t,n,s,l,r)}else v=e.append("line"),v.attr("x1",s),v.attr("y1",r),v.attr("x2",l),v.attr("y2",r),uY(i,n)&&sIe(e,i,t,n,s,l,r);d===n.db.LINETYPE.DOTTED||d===n.db.LINETYPE.DOTTED_CROSS||d===n.db.LINETYPE.DOTTED_POINT||d===n.db.LINETYPE.DOTTED_OPEN||d===n.db.LINETYPE.BIDIRECTIONAL_DOTTED||d===n.db.LINETYPE.SOLID_TOP_DOTTED||d===n.db.LINETYPE.SOLID_BOTTOM_DOTTED||d===n.db.LINETYPE.STICK_TOP_DOTTED||d===n.db.LINETYPE.STICK_BOTTOM_DOTTED||d===n.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED||d===n.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED||d===n.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED||d===n.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED?(v.style("stroke-dasharray","3, 3"),v.attr("class","messageLine1")):v.attr("class","messageLine0"),v.attr("data-et","message"),v.attr("data-id","i"+t.id),v.attr("data-from",t.from),v.attr("data-to",t.to);let x="";if(je.arrowMarkerAbsolute&&(x=qp(!0)),v.attr("stroke-width",2),v.attr("stroke","none"),v.style("fill","none"),(d===n.db.LINETYPE.SOLID_TOP||d===n.db.LINETYPE.SOLID_TOP_DOTTED)&&v.attr("marker-end","url("+x+"#"+a+"-solidTopArrowHead)"),(d===n.db.LINETYPE.SOLID_BOTTOM||d===n.db.LINETYPE.SOLID_BOTTOM_DOTTED)&&v.attr("marker-end","url("+x+"#"+a+"-solidBottomArrowHead)"),(d===n.db.LINETYPE.STICK_TOP||d===n.db.LINETYPE.STICK_TOP_DOTTED)&&v.attr("marker-end","url("+x+"#"+a+"-stickTopArrowHead)"),(d===n.db.LINETYPE.STICK_BOTTOM||d===n.db.LINETYPE.STICK_BOTTOM_DOTTED)&&v.attr("marker-end","url("+x+"#"+a+"-stickBottomArrowHead)"),(d===n.db.LINETYPE.SOLID_ARROW_TOP_REVERSE||d===n.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED)&&v.attr("marker-start","url("+x+"#"+a+"-solidBottomArrowHead)"),(d===n.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE||d===n.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED)&&v.attr("marker-start","url("+x+"#"+a+"-solidTopArrowHead)"),(d===n.db.LINETYPE.STICK_ARROW_TOP_REVERSE||d===n.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED)&&v.attr("marker-start","url("+x+"#"+a+"-stickBottomArrowHead)"),(d===n.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE||d===n.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED)&&v.attr("marker-start","url("+x+"#"+a+"-stickTopArrowHead)"),(d===n.db.LINETYPE.SOLID||d===n.db.LINETYPE.DOTTED)&&v.attr("marker-end","url("+x+"#"+a+"-arrowhead)"),(d===n.db.LINETYPE.BIDIRECTIONAL_SOLID||d===n.db.LINETYPE.BIDIRECTIONAL_DOTTED)&&(v.attr("marker-start","url("+x+"#"+a+"-arrowhead)"),v.attr("marker-end","url("+x+"#"+a+"-arrowhead)")),(d===n.db.LINETYPE.SOLID_POINT||d===n.db.LINETYPE.DOTTED_POINT)&&v.attr("marker-end","url("+x+"#"+a+"-filled-head)"),(d===n.db.LINETYPE.SOLID_CROSS||d===n.db.LINETYPE.DOTTED_CROSS)&&v.attr("marker-end","url("+x+"#"+a+"-crosshead)"),p||je.showSequenceNumbers){let b=d===n.db.LINETYPE.BIDIRECTIONAL_SOLID||d===n.db.LINETYPE.BIDIRECTIONAL_DOTTED,T=d===n.db.LINETYPE.SOLID_ARROW_TOP_REVERSE||d===n.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED||d===n.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE||d===n.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED||d===n.db.LINETYPE.STICK_ARROW_TOP_REVERSE||d===n.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED||d===n.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE||d===n.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED,k=6,C=uY(i,n),w=s,S=l;b?(ss?S=l-2*k:(S=l-k,w+=i?.centralConnection===n.db.LINETYPE.CENTRAL_CONNECTION_DUAL||i?.centralConnection===n.db.LINETYPE.CENTRAL_CONNECTION_REVERSE?-7.5:0),S+=C?15:0,v.attr("x2",S),v.attr("x1",w)):v.attr("x1",s+k);let R=0,L=s===l,N=s<=l;L?R=t.fromBounds+1:T?R=N?t.toBounds-1:t.fromBounds+1:R=N?t.fromBounds+1:t.toBounds-1;let I="12px",_=f.toString().length;_>5?I="7px":_>3&&(I="9px"),e.append("line").attr("x1",R).attr("y1",r).attr("x2",R).attr("y2",r).attr("stroke-width",0).attr("marker-start","url("+x+"#"+a+"-sequencenumber)"),e.append("text").attr("x",R).attr("y",r+4).attr("font-family","sans-serif").attr("font-size",I).attr("text-anchor","middle").attr("class","sequenceNumber").text(f)}},"drawMessage"),I6t=o(function(e,t,r,n,i,a,s){let l=0,u=0,h,d=0;for(let f of n){let p=t.get(f),m=p.box;h&&h!=m&&(s||Et.models.addBox(h),u+=je.boxMargin+h.margin),m&&m!=h&&(s||(m.x=l+u,m.y=i),u+=m.margin),p.width=xt.getMax(p.width||je.width,je.width),p.height=xt.getMax(p.height||je.height,je.height),p.margin=p.margin||je.actorMargin,d=xt.getMax(d,p.height),r.get(p.name)&&(u+=p.width/2),p.x=l+u,p.starty=Et.getVerticalPos(),Et.insert(p.x,i,p.x+p.width,p.height),l+=p.width+u,p.box&&(p.box.width=l+m.margin-p.box.x),u=p.margin,h=p.box,Et.models.addActor(p)}h&&!s&&Et.models.addBox(h),Et.bumpVerticalPos(d)},"addActorRenderingData"),dY=o(async function(e,t,r,n,i,a,s){if(n){let l=0;Et.bumpVerticalPos(je.boxMargin*2);for(let u of r){let h=t.get(u);h.stopy||(h.stopy=Et.getVerticalPos());let d=await Un.drawActor(e,h,je,!0,i,a,s);l=xt.getMax(l,d)}Et.bumpVerticalPos(l+je.boxMargin)}else for(let l of r){let u=t.get(l);await Un.drawActor(e,u,je,!1,i,a,s)}},"drawActors"),lIe=o(function(e,t,r,n){let i=0,a=0;for(let s of r){let l=t.get(s),u=O6t(l),h=Un.drawPopup(e,l,u,je,je.forceMenus,n);h.height>i&&(i=h.height),h.width+l.x>a&&(a=h.width+l.x)}return{maxHeight:i,maxWidth:a}},"drawActorsPopup"),cIe=o(function(e){ri(je,e),e.fontFamily&&(je.actorFontFamily=je.noteFontFamily=je.messageFontFamily=e.fontFamily),e.fontSize&&(je.actorFontSize=je.noteFontSize=je.messageFontSize=e.fontSize),e.fontWeight&&(je.actorFontWeight=je.noteFontWeight=je.messageFontWeight=e.fontWeight)},"setConf"),kL=o(function(e){return Et.activations.filter(function(t){return t.actor===e})},"actorActivations"),oIe=o(function(e,t){let r=t.get(e),n=kL(e),i=n.reduce(function(s,l){return xt.getMin(s,l.startx)},r.x+r.width/2-1),a=n.reduce(function(s,l){return xt.getMax(s,l.stopx)},r.x+r.width/2+1);return[i,a]},"activationBounds");o(Hu,"adjustLoopHeightForWrap");o(M6t,"adjustCreatedDestroyedData");N6t=o(async function(e,t,r,n){let{securityLevel:i,sequence:a,look:s,themeVariables:l}=Ae();je=a;let u;i==="sandbox"&&(u=et("#i"+t));let h=i==="sandbox"?et(u.nodes()[0].contentDocument.body):et("body"),d=i==="sandbox"?u.nodes()[0].contentDocument:document;Et.init(),Z.debug(n.db);let f=i==="sandbox"?h.select(`[id="${t}"]`):et(`[id="${t}"]`),p=n.db.getActors(),m=n.db.getCreatedActors(),g=n.db.getDestroyedActors(),y=n.db.getBoxes(),v=n.db.getActorKeys(),x=n.db.getMessages(),b=n.db.getDiagramTitle(),T=n.db.hasAtLeastOneBox(),k=n.db.hasAtLeastOneBoxWithTitle(),C=await P6t(p,x,n);if(je.height=await B6t(p,C,y),Un.insertComputerIcon(f,t),Un.insertDatabaseIcon(f,t),Un.insertClockIcon(f,t),T&&(Et.bumpVerticalPos(je.boxMargin),k&&Et.bumpVerticalPos(y[0].textMaxHeight)),je.hideUnusedParticipants===!0){let z=new Set;x.forEach(W=>{z.add(W.from),z.add(W.to)}),v=v.filter(W=>z.has(W))}let w=new Map(v.map((z,W)=>[p.get(z)?.name??z,W]));I6t(f,p,m,v,0,x,!1);let S=await q6t(x,p,C,n);Un.insertArrowHead(f,t),Un.insertArrowCrossHead(f,t),Un.insertArrowFilledHead(f,t),Un.insertSequenceNumber(f,t),Un.insertSolidTopArrowHead(f,t),Un.insertSolidBottomArrowHead(f,t),Un.insertStickTopArrowHead(f,t),Un.insertStickBottomArrowHead(f,t),s==="neo"&&Un.insertDropShadow(f,je);function R(z,W){let H=Et.endActivation(z);H.starty+18>W&&(H.starty=W-6,W+=12),Un.drawActivation(f,H,W,je,kL(z.from).length,n,w),Et.insert(H.startx,W-10,H.stopx,W)}o(R,"activeEnd");let L=1,N=1,I=[],_=[],A=0;for(let z of x){let W,H,j;switch(z.type){case n.db.LINETYPE.NOTE:Et.resetVerticalPos(),H=z.noteModel,await _6t(f,H,z.id);break;case n.db.LINETYPE.ACTIVE_START:Et.newActivation(z,f,p);break;case n.db.LINETYPE.CENTRAL_CONNECTION:Et.newActivation(z,f,p);break;case n.db.LINETYPE.CENTRAL_CONNECTION_REVERSE:Et.newActivation(z,f,p);break;case n.db.LINETYPE.ACTIVE_END:R(z,Et.getVerticalPos());break;case n.db.LINETYPE.LOOP_START:Hu(S,z,je.boxMargin,je.boxMargin+je.boxTextMargin,Q=>Et.newLoop(Q));break;case n.db.LINETYPE.LOOP_END:W=Et.endLoop(),await Un.drawLoop(f,W,"loop",je,z),Et.bumpVerticalPos(W.stopy-Et.getVerticalPos()),Et.models.addLoop(W);break;case n.db.LINETYPE.RECT_START:Hu(S,z,je.boxMargin,je.boxMargin,Q=>{let U=Q.message;U||(U=l?.rectBkgColor||l?.actorBkg||"rgba(128, 128, 128, 0.5)"),Et.newLoop(void 0,U)});break;case n.db.LINETYPE.RECT_END:W=Et.endLoop(),_.push(W),Et.models.addLoop(W),Et.bumpVerticalPos(W.stopy-Et.getVerticalPos());break;case n.db.LINETYPE.OPT_START:Hu(S,z,je.boxMargin,je.boxMargin+je.boxTextMargin,Q=>Et.newLoop(Q));break;case n.db.LINETYPE.OPT_END:W=Et.endLoop(),await Un.drawLoop(f,W,"opt",je,z),Et.bumpVerticalPos(W.stopy-Et.getVerticalPos()),Et.models.addLoop(W);break;case n.db.LINETYPE.ALT_START:Hu(S,z,je.boxMargin,je.boxMargin+je.boxTextMargin,Q=>Et.newLoop(Q));break;case n.db.LINETYPE.ALT_ELSE:Hu(S,z,je.boxMargin+je.boxTextMargin,je.boxMargin,Q=>Et.addSectionToLoop(Q));break;case n.db.LINETYPE.ALT_END:W=Et.endLoop(),await Un.drawLoop(f,W,"alt",je,z),Et.bumpVerticalPos(W.stopy-Et.getVerticalPos()),Et.models.addLoop(W);break;case n.db.LINETYPE.PAR_START:case n.db.LINETYPE.PAR_OVER_START:Hu(S,z,je.boxMargin,je.boxMargin+je.boxTextMargin,Q=>Et.newLoop(Q)),Et.saveVerticalPos();break;case n.db.LINETYPE.PAR_AND:Hu(S,z,je.boxMargin+je.boxTextMargin,je.boxMargin,Q=>Et.addSectionToLoop(Q));break;case n.db.LINETYPE.PAR_END:W=Et.endLoop(),await Un.drawLoop(f,W,"par",je,z),Et.bumpVerticalPos(W.stopy-Et.getVerticalPos()),Et.models.addLoop(W);break;case n.db.LINETYPE.AUTONUMBER:L=z.message.start||L,N=z.message.step||N,z.message.visible?n.db.enableSequenceNumbers():n.db.disableSequenceNumbers();break;case n.db.LINETYPE.CRITICAL_START:Hu(S,z,je.boxMargin,je.boxMargin+je.boxTextMargin,Q=>Et.newLoop(Q));break;case n.db.LINETYPE.CRITICAL_OPTION:Hu(S,z,je.boxMargin+je.boxTextMargin,je.boxMargin,Q=>Et.addSectionToLoop(Q));break;case n.db.LINETYPE.CRITICAL_END:W=Et.endLoop(),await Un.drawLoop(f,W,"critical",je,z),Et.bumpVerticalPos(W.stopy-Et.getVerticalPos()),Et.models.addLoop(W);break;case n.db.LINETYPE.BREAK_START:Hu(S,z,je.boxMargin,je.boxMargin+je.boxTextMargin,Q=>Et.newLoop(Q));break;case n.db.LINETYPE.BREAK_END:W=Et.endLoop(),await Un.drawLoop(f,W,"break",je,z),Et.bumpVerticalPos(W.stopy-Et.getVerticalPos()),Et.models.addLoop(W);break;default:try{j=z.msgModel,j.starty=Et.getVerticalPos(),j.sequenceIndex=L,j.sequenceVisible=n.db.showSequenceNumbers(),j.id=z.id,j.from=z.from,j.to=z.to;let Q=await L6t(f,j);M6t(z,j,Q,A,p,m,g),I.push({messageModel:j,lineStartY:Q,msg:z}),Et.models.addMessage(j)}catch(Q){Z.error("error while drawing message",Q)}}[n.db.LINETYPE.SOLID_OPEN,n.db.LINETYPE.DOTTED_OPEN,n.db.LINETYPE.SOLID,n.db.LINETYPE.SOLID_TOP,n.db.LINETYPE.SOLID_BOTTOM,n.db.LINETYPE.STICK_TOP,n.db.LINETYPE.STICK_BOTTOM,n.db.LINETYPE.SOLID_TOP_DOTTED,n.db.LINETYPE.SOLID_BOTTOM_DOTTED,n.db.LINETYPE.STICK_TOP_DOTTED,n.db.LINETYPE.STICK_BOTTOM_DOTTED,n.db.LINETYPE.SOLID_ARROW_TOP_REVERSE,n.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE,n.db.LINETYPE.STICK_ARROW_TOP_REVERSE,n.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE,n.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED,n.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED,n.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED,n.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED,n.db.LINETYPE.DOTTED,n.db.LINETYPE.SOLID_CROSS,n.db.LINETYPE.DOTTED_CROSS,n.db.LINETYPE.SOLID_POINT,n.db.LINETYPE.DOTTED_POINT,n.db.LINETYPE.BIDIRECTIONAL_SOLID,n.db.LINETYPE.BIDIRECTIONAL_DOTTED].includes(z.type)&&(L=Math.round((L+N)*100)/100),A++}Z.debug("createdActors",m),Z.debug("destroyedActors",g),await dY(f,p,v,!1,t,n,w);for(let z of I)await D6t(f,z.messageModel,z.lineStartY,n,z.msg,t);je.mirrorActors&&await dY(f,p,v,!0,t,n,w),_.forEach(z=>Un.drawBackgroundRect(f,z)),cY(f,p,v,je);for(let z of Et.models.boxes){z.height=Et.getVerticalPos()-z.y,Et.insert(z.x,z.y,z.x+z.width,z.height);let W=je.boxMargin*2;z.startx=z.x-W,z.starty=z.y-W*.25,z.stopx=z.startx+z.width+2*W,z.stopy=z.starty+z.height+W*.75,z.stroke="rgb(0,0,0, 0.5)",Un.drawBox(f,z,je)}T&&Et.bumpVerticalPos(je.boxMargin);let M=lIe(f,p,v,d),{bounds:D}=Et.getBounds();D.startx===void 0&&(D.startx=0),D.starty===void 0&&(D.starty=0),D.stopx===void 0&&(D.stopx=0),D.stopy===void 0&&(D.stopy=0);let P=D.stopy-D.starty;P2,p=o(v=>u?-v:v,"adjustValue");e.from===e.to?d=h:(e.activate&&!f&&(d+=p(je.activationWidth/2-1)),[r.db.LINETYPE.SOLID_OPEN,r.db.LINETYPE.DOTTED_OPEN,r.db.LINETYPE.STICK_TOP,r.db.LINETYPE.STICK_BOTTOM,r.db.LINETYPE.STICK_TOP_DOTTED,r.db.LINETYPE.STICK_BOTTOM_DOTTED,r.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED,r.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED,r.db.LINETYPE.STICK_ARROW_TOP_REVERSE,r.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE,r.db.LINETYPE.STICK_ARROW_TOP_REVERSE_DOTTED,r.db.LINETYPE.STICK_ARROW_BOTTOM_REVERSE_DOTTED,r.db.LINETYPE.SOLID_ARROW_TOP_REVERSE,r.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE].includes(e.type)||(d+=p(3)),[r.db.LINETYPE.BIDIRECTIONAL_SOLID,r.db.LINETYPE.BIDIRECTIONAL_DOTTED,r.db.LINETYPE.SOLID_ARROW_TOP_REVERSE_DOTTED,r.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE_DOTTED,r.db.LINETYPE.SOLID_ARROW_TOP_REVERSE,r.db.LINETYPE.SOLID_ARROW_BOTTOM_REVERSE].includes(e.type)&&(h-=p(3)));let m=[i,a,s,l],g=Math.abs(h-d);e.wrap&&e.message&&(e.message=Zt.wrapLabel(e.message,xt.getMax(g+2*je.wrapPadding,je.width),A0(je)));let y=Zt.calculateTextDimensions(e.message,A0(je));return{width:xt.getMax(e.wrap?0:y.width+2*je.wrapPadding,g+2*je.wrapPadding,je.width),height:0,startx:h,stopx:d,starty:0,stopy:0,message:e.message,type:e.type,wrap:e.wrap,fromBounds:Math.min.apply(null,m),toBounds:Math.max.apply(null,m)}},"buildMessageModel"),q6t=o(async function(e,t,r,n){let i={},a=[],s,l,u;for(let h of e){switch(h.type){case n.db.LINETYPE.LOOP_START:case n.db.LINETYPE.ALT_START:case n.db.LINETYPE.OPT_START:case n.db.LINETYPE.PAR_START:case n.db.LINETYPE.PAR_OVER_START:case n.db.LINETYPE.CRITICAL_START:case n.db.LINETYPE.BREAK_START:a.push({id:h.id,msg:h.message,from:Number.MAX_SAFE_INTEGER,to:Number.MIN_SAFE_INTEGER,width:0});break;case n.db.LINETYPE.ALT_ELSE:case n.db.LINETYPE.PAR_AND:case n.db.LINETYPE.CRITICAL_OPTION:h.message&&(s=a.pop(),i[s.id]=s,i[h.id]=s,a.push(s));break;case n.db.LINETYPE.LOOP_END:case n.db.LINETYPE.ALT_END:case n.db.LINETYPE.OPT_END:case n.db.LINETYPE.PAR_END:case n.db.LINETYPE.CRITICAL_END:case n.db.LINETYPE.BREAK_END:s=a.pop(),i[s.id]=s;break;case n.db.LINETYPE.ACTIVE_START:{let f=t.get(h.from?h.from:h.to.actor),p=kL(h.from?h.from:h.to.actor).length,m=f.x+f.width/2+(p-1)*je.activationWidth/2,g={startx:m,stopx:m+je.activationWidth,actor:h.from,enabled:!0};Et.activations.push(g)}break;case n.db.LINETYPE.ACTIVE_END:{let f=Et.activations.map(p=>p.actor).lastIndexOf(h.from);Et.activations.splice(f,1).splice(0,1)}break}h.placement!==void 0?(l=await $6t(h,t,n),h.noteModel=l,a.forEach(f=>{s=f,s.from=xt.getMin(s.from,l.startx),s.to=xt.getMax(s.to,l.startx+l.width),s.width=xt.getMax(s.width,Math.abs(s.from-s.to))-je.labelBoxWidth})):(u=W6t(h,t,n),h.msgModel=u,u.startx&&u.stopx&&a.length>0&&a.forEach(f=>{if(s=f,u.startx===u.stopx){let p=t.get(h.from),m=t.get(h.to);s.from=xt.getMin(p.x-u.width/2,p.x-p.width/2,s.from),s.to=xt.getMax(m.x+u.width/2,m.x+p.width/2,s.to),s.width=xt.getMax(s.width,Math.abs(s.to-s.from))-je.labelBoxWidth}else s.from=xt.getMin(u.startx,s.from),s.to=xt.getMax(u.stopx,s.to),s.width=xt.getMax(s.width,u.width)-je.labelBoxWidth}))}return Et.activations=[],Z.debug("Loop type widths:",i),i},"calculateLoopBounds"),hIe={bounds:Et,drawActors:dY,drawActorsPopup:lIe,setConf:cIe,draw:N6t}});var fIe={};ir(fIe,{diagram:()=>H6t});var H6t,pIe=F(()=>{"use strict";J8e();oY();tIe();Xt();dIe();H6t={parser:Q8e,get db(){return new TL},renderer:hIe,styles:eIe,init:o(e=>{e.sequence||(e.sequence={}),e.wrap&&(e.sequence.wrap=e.wrap,ub({sequence:{wrap:e.wrap}}))},"init")}});var fY,SL,pY=F(()=>{"use strict";fY=(function(){var e=o(function(we,Me,ve,ne){for(ve=ve||{},ne=we.length;ne--;ve[we[ne]]=Me);return ve},"o"),t=[1,18],r=[1,19],n=[1,20],i=[1,41],a=[1,26],s=[1,42],l=[1,24],u=[1,25],h=[1,32],d=[1,33],f=[1,34],p=[1,45],m=[1,35],g=[1,36],y=[1,37],v=[1,38],x=[1,27],b=[1,28],T=[1,29],k=[1,30],C=[1,31],w=[1,44],S=[1,46],R=[1,43],L=[1,47],N=[1,9],I=[1,8,9],_=[1,58],A=[1,59],M=[1,60],D=[1,61],P=[1,62],B=[1,63],O=[1,64],$=[1,8,9,41],V=[1,77],G=[1,8,9,12,13,22,39,41,44,46,68,69,70,71,72,73,74,79,81],z=[1,8,9,12,13,18,20,22,39,41,44,46,47,60,68,69,70,71,72,73,74,79,81,86,100,102,103],W=[13,60,86,100,102,103],H=[13,60,73,74,86,100,102,103],j=[13,60,68,69,70,71,72,86,100,102,103],Q=[1,103],U=[1,121],oe=[1,117],te=[1,113],le=[1,119],ie=[1,114],ae=[1,115],Re=[1,116],be=[1,118],Pe=[1,120],Ge=[22,50,60,61,82,86,87,88,89,90],Oe=[1,128],ue=[12,39],ye=[1,8,9,39,41,44,46],ke=[1,8,9,22],ce=[1,153],re=[1,8,9,61],J=[1,8,9,22,50,60,61,82,86,87,88,89,90],se={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mermaidDoc:4,statements:5,graphConfig:6,CLASS_DIAGRAM:7,NEWLINE:8,EOF:9,statement:10,classLabel:11,SQS:12,STR:13,SQE:14,namespaceName:15,alphaNumToken:16,classLiteralName:17,DOT:18,className:19,GENERICTYPE:20,relationStatement:21,LABEL:22,namespaceStatement:23,classStatement:24,memberStatement:25,annotationStatement:26,clickStatement:27,styleStatement:28,cssClassStatement:29,noteStatement:30,classDefStatement:31,direction:32,acc_title:33,acc_title_value:34,acc_descr:35,acc_descr_value:36,acc_descr_multiline_value:37,namespaceIdentifier:38,STRUCT_START:39,classStatements:40,STRUCT_STOP:41,NAMESPACE:42,classIdentifier:43,STYLE_SEPARATOR:44,members:45,ANNOTATION_START:46,ANNOTATION_END:47,CLASS:48,emptyBody:49,SPACE:50,MEMBER:51,SEPARATOR:52,relation:53,NOTE_FOR:54,noteText:55,NOTE:56,CLASSDEF:57,classList:58,stylesOpt:59,ALPHA:60,COMMA:61,direction_tb:62,direction_bt:63,direction_rl:64,direction_lr:65,relationType:66,lineType:67,AGGREGATION:68,EXTENSION:69,COMPOSITION:70,DEPENDENCY:71,LOLLIPOP:72,LINE:73,DOTTED_LINE:74,CALLBACK:75,LINK:76,LINK_TARGET:77,CLICK:78,CALLBACK_NAME:79,CALLBACK_ARGS:80,HREF:81,STYLE:82,CSSCLASS:83,style:84,styleComponent:85,NUM:86,COLON:87,UNIT:88,BRKT:89,PCT:90,commentToken:91,textToken:92,graphCodeTokens:93,textNoTagsToken:94,TAGSTART:95,TAGEND:96,"==":97,"--":98,DEFAULT:99,MINUS:100,keywords:101,UNICODE_TEXT:102,BQUOTE_STR:103,$accept:0,$end:1},terminals_:{2:"error",7:"CLASS_DIAGRAM",8:"NEWLINE",9:"EOF",12:"SQS",13:"STR",14:"SQE",18:"DOT",20:"GENERICTYPE",22:"LABEL",33:"acc_title",34:"acc_title_value",35:"acc_descr",36:"acc_descr_value",37:"acc_descr_multiline_value",39:"STRUCT_START",41:"STRUCT_STOP",42:"NAMESPACE",44:"STYLE_SEPARATOR",46:"ANNOTATION_START",47:"ANNOTATION_END",48:"CLASS",50:"SPACE",51:"MEMBER",52:"SEPARATOR",54:"NOTE_FOR",56:"NOTE",57:"CLASSDEF",60:"ALPHA",61:"COMMA",62:"direction_tb",63:"direction_bt",64:"direction_rl",65:"direction_lr",68:"AGGREGATION",69:"EXTENSION",70:"COMPOSITION",71:"DEPENDENCY",72:"LOLLIPOP",73:"LINE",74:"DOTTED_LINE",75:"CALLBACK",76:"LINK",77:"LINK_TARGET",78:"CLICK",79:"CALLBACK_NAME",80:"CALLBACK_ARGS",81:"HREF",82:"STYLE",83:"CSSCLASS",86:"NUM",87:"COLON",88:"UNIT",89:"BRKT",90:"PCT",93:"graphCodeTokens",95:"TAGSTART",96:"TAGEND",97:"==",98:"--",99:"DEFAULT",100:"MINUS",101:"keywords",102:"UNICODE_TEXT",103:"BQUOTE_STR"},productions_:[0,[3,1],[3,1],[4,1],[6,4],[5,1],[5,2],[5,3],[11,3],[15,1],[15,1],[15,3],[15,2],[19,1],[19,3],[19,1],[19,2],[19,2],[19,2],[10,1],[10,2],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,1],[10,2],[10,2],[10,1],[23,4],[23,5],[38,2],[38,3],[40,1],[40,2],[40,3],[40,1],[40,2],[40,3],[40,1],[40,2],[40,3],[24,1],[24,3],[24,4],[24,3],[24,6],[24,4],[24,7],[24,6],[43,2],[43,3],[49,0],[49,2],[49,2],[26,4],[45,1],[45,2],[25,1],[25,2],[25,1],[25,1],[21,3],[21,4],[21,4],[21,5],[30,3],[30,2],[31,3],[58,1],[58,3],[32,1],[32,1],[32,1],[32,1],[53,3],[53,2],[53,2],[53,1],[66,1],[66,1],[66,1],[66,1],[66,1],[67,1],[67,1],[27,3],[27,4],[27,3],[27,4],[27,4],[27,5],[27,3],[27,4],[27,4],[27,5],[27,4],[27,5],[27,5],[27,6],[28,3],[29,3],[59,1],[59,3],[84,1],[84,2],[85,1],[85,1],[85,1],[85,1],[85,1],[85,1],[85,1],[85,1],[85,1],[91,1],[91,1],[92,1],[92,1],[92,1],[92,1],[92,1],[92,1],[92,1],[94,1],[94,1],[94,1],[94,1],[16,1],[16,1],[16,1],[16,1],[17,1],[55,1]],performAction:o(function(Me,ve,ne,q,he,X,fe){var K=X.length-1;switch(he){case 8:this.$=X[K-1];break;case 9:case 10:case 13:case 15:this.$=X[K];break;case 11:case 14:this.$=X[K-2]+"."+X[K];break;case 12:case 16:this.$=X[K-1]+X[K];break;case 17:case 18:this.$=X[K-1]+"~"+X[K]+"~";break;case 19:q.addRelation(X[K]);break;case 20:X[K-1].title=q.cleanupLabel(X[K]),q.addRelation(X[K-1]);break;case 31:this.$=X[K].trim(),q.setAccTitle(this.$);break;case 32:case 33:this.$=X[K].trim(),q.setAccDescription(this.$);break;case 34:q.addClassesToNamespace(X[K-3],X[K-1][0],X[K-1][1]),q.popNamespace();break;case 35:q.addClassesToNamespace(X[K-4],X[K-1][0],X[K-1][1]),q.popNamespace();break;case 36:this.$=q.addNamespace(X[K]);break;case 37:this.$=q.addNamespace(X[K-1],X[K]);break;case 38:this.$=[[X[K]],[]];break;case 39:this.$=[[X[K-1]],[]];break;case 40:X[K][0].unshift(X[K-2]),this.$=X[K];break;case 41:this.$=[[],[X[K]]];break;case 42:this.$=[[],[X[K-1]]];break;case 43:X[K][1].unshift(X[K-2]),this.$=X[K];break;case 44:case 45:this.$=[[],[]];break;case 46:this.$=X[K];break;case 48:q.setCssClass(X[K-2],X[K]);break;case 49:q.addMembers(X[K-3],X[K-1]);break;case 51:q.setCssClass(X[K-5],X[K-3]),q.addMembers(X[K-5],X[K-1]);break;case 52:q.addAnnotation(X[K-3],X[K-1]);break;case 53:q.addAnnotation(X[K-6],X[K-4]),q.addMembers(X[K-6],X[K-1]);break;case 54:q.addAnnotation(X[K-5],X[K-3]);break;case 55:this.$=X[K],q.addClass(X[K]);break;case 56:this.$=X[K-1],q.addClass(X[K-1]),q.setClassLabel(X[K-1],X[K]);break;case 60:q.addAnnotation(X[K],X[K-2]);break;case 61:case 74:this.$=[X[K]];break;case 62:X[K].push(X[K-1]),this.$=X[K];break;case 63:break;case 64:q.addMember(X[K-1],q.cleanupLabel(X[K]));break;case 65:break;case 66:break;case 67:this.$={id1:X[K-2],id2:X[K],relation:X[K-1],relationTitle1:"none",relationTitle2:"none"};break;case 68:this.$={id1:X[K-3],id2:X[K],relation:X[K-1],relationTitle1:X[K-2],relationTitle2:"none"};break;case 69:this.$={id1:X[K-3],id2:X[K],relation:X[K-2],relationTitle1:"none",relationTitle2:X[K-1]};break;case 70:this.$={id1:X[K-4],id2:X[K],relation:X[K-2],relationTitle1:X[K-3],relationTitle2:X[K-1]};break;case 71:this.$=q.addNote(X[K],X[K-1]);break;case 72:this.$=q.addNote(X[K]);break;case 73:this.$=X[K-2],q.defineClass(X[K-1],X[K]);break;case 75:this.$=X[K-2].concat([X[K]]);break;case 76:q.setDirection("TB");break;case 77:q.setDirection("BT");break;case 78:q.setDirection("RL");break;case 79:q.setDirection("LR");break;case 80:this.$={type1:X[K-2],type2:X[K],lineType:X[K-1]};break;case 81:this.$={type1:"none",type2:X[K],lineType:X[K-1]};break;case 82:this.$={type1:X[K-1],type2:"none",lineType:X[K]};break;case 83:this.$={type1:"none",type2:"none",lineType:X[K]};break;case 84:this.$=q.relationType.AGGREGATION;break;case 85:this.$=q.relationType.EXTENSION;break;case 86:this.$=q.relationType.COMPOSITION;break;case 87:this.$=q.relationType.DEPENDENCY;break;case 88:this.$=q.relationType.LOLLIPOP;break;case 89:this.$=q.lineType.LINE;break;case 90:this.$=q.lineType.DOTTED_LINE;break;case 91:case 97:this.$=X[K-2],q.setClickEvent(X[K-1],X[K]);break;case 92:case 98:this.$=X[K-3],q.setClickEvent(X[K-2],X[K-1]),q.setTooltip(X[K-2],X[K]);break;case 93:this.$=X[K-2],q.setLink(X[K-1],X[K]);break;case 94:this.$=X[K-3],q.setLink(X[K-2],X[K-1],X[K]);break;case 95:this.$=X[K-3],q.setLink(X[K-2],X[K-1]),q.setTooltip(X[K-2],X[K]);break;case 96:this.$=X[K-4],q.setLink(X[K-3],X[K-2],X[K]),q.setTooltip(X[K-3],X[K-1]);break;case 99:this.$=X[K-3],q.setClickEvent(X[K-2],X[K-1],X[K]);break;case 100:this.$=X[K-4],q.setClickEvent(X[K-3],X[K-2],X[K-1]),q.setTooltip(X[K-3],X[K]);break;case 101:this.$=X[K-3],q.setLink(X[K-2],X[K]);break;case 102:this.$=X[K-4],q.setLink(X[K-3],X[K-1],X[K]);break;case 103:this.$=X[K-4],q.setLink(X[K-3],X[K-1]),q.setTooltip(X[K-3],X[K]);break;case 104:this.$=X[K-5],q.setLink(X[K-4],X[K-2],X[K]),q.setTooltip(X[K-4],X[K-1]);break;case 105:this.$=X[K-2],q.setCssStyle(X[K-1],X[K]);break;case 106:q.setCssClass(X[K-1],X[K]);break;case 107:this.$=[X[K]];break;case 108:X[K-2].push(X[K]),this.$=X[K-2];break;case 110:this.$=X[K-1]+X[K];break}},"anonymous"),table:[{3:1,4:2,5:3,6:4,7:[1,6],10:5,16:39,17:40,19:21,21:7,23:8,24:9,25:10,26:11,27:12,28:13,29:14,30:15,31:16,32:17,33:t,35:r,37:n,38:22,42:i,43:23,46:a,48:s,51:l,52:u,54:h,56:d,57:f,60:p,62:m,63:g,64:y,65:v,75:x,76:b,78:T,82:k,83:C,86:w,100:S,102:R,103:L},{1:[3]},{1:[2,1]},{1:[2,2]},{1:[2,3]},e(N,[2,5],{8:[1,48]}),{8:[1,49]},e(I,[2,19],{22:[1,50]}),e(I,[2,21]),e(I,[2,22]),e(I,[2,23]),e(I,[2,24]),e(I,[2,25]),e(I,[2,26]),e(I,[2,27]),e(I,[2,28]),e(I,[2,29]),e(I,[2,30]),{34:[1,51]},{36:[1,52]},e(I,[2,33]),e(I,[2,63],{53:53,66:56,67:57,13:[1,54],22:[1,55],68:_,69:A,70:M,71:D,72:P,73:B,74:O}),{39:[1,65]},e($,[2,47],{39:[1,67],44:[1,66],46:[1,68]}),e(I,[2,65]),e(I,[2,66]),{16:69,60:p,86:w,100:S,102:R},{16:39,17:40,19:70,60:p,86:w,100:S,102:R,103:L},{16:39,17:40,19:71,60:p,86:w,100:S,102:R,103:L},{16:39,17:40,19:72,60:p,86:w,100:S,102:R,103:L},{60:[1,73]},{13:[1,74]},{16:39,17:40,19:75,60:p,86:w,100:S,102:R,103:L},{13:V,55:76},{58:78,60:[1,79]},e(I,[2,76]),e(I,[2,77]),e(I,[2,78]),e(I,[2,79]),e(G,[2,13],{16:39,17:40,19:81,18:[1,80],20:[1,82],60:p,86:w,100:S,102:R,103:L}),e(G,[2,15],{20:[1,83]}),{15:84,16:85,17:86,60:p,86:w,100:S,102:R,103:L},{16:39,17:40,19:87,60:p,86:w,100:S,102:R,103:L},e(z,[2,133]),e(z,[2,134]),e(z,[2,135]),e(z,[2,136]),e([1,8,9,12,13,20,22,39,41,44,46,68,69,70,71,72,73,74,79,81],[2,137]),e(N,[2,6],{10:5,21:7,23:8,24:9,25:10,26:11,27:12,28:13,29:14,30:15,31:16,32:17,19:21,38:22,43:23,16:39,17:40,5:88,33:t,35:r,37:n,42:i,46:a,48:s,51:l,52:u,54:h,56:d,57:f,60:p,62:m,63:g,64:y,65:v,75:x,76:b,78:T,82:k,83:C,86:w,100:S,102:R,103:L}),{5:89,10:5,16:39,17:40,19:21,21:7,23:8,24:9,25:10,26:11,27:12,28:13,29:14,30:15,31:16,32:17,33:t,35:r,37:n,38:22,42:i,43:23,46:a,48:s,51:l,52:u,54:h,56:d,57:f,60:p,62:m,63:g,64:y,65:v,75:x,76:b,78:T,82:k,83:C,86:w,100:S,102:R,103:L},e(I,[2,20]),e(I,[2,31]),e(I,[2,32]),{13:[1,91],16:39,17:40,19:90,60:p,86:w,100:S,102:R,103:L},{53:92,66:56,67:57,68:_,69:A,70:M,71:D,72:P,73:B,74:O},e(I,[2,64]),{67:93,73:B,74:O},e(W,[2,83],{66:94,68:_,69:A,70:M,71:D,72:P}),e(H,[2,84]),e(H,[2,85]),e(H,[2,86]),e(H,[2,87]),e(H,[2,88]),e(j,[2,89]),e(j,[2,90]),{8:[1,96],23:99,24:97,30:98,38:22,40:95,42:i,43:23,48:s,54:h,56:d},{16:100,60:p,86:w,100:S,102:R},{41:[1,102],45:101,51:Q},{16:104,60:p,86:w,100:S,102:R},{47:[1,105]},{13:[1,106]},{13:[1,107]},{79:[1,108],81:[1,109]},{22:U,50:oe,59:110,60:te,82:le,84:111,85:112,86:ie,87:ae,88:Re,89:be,90:Pe},{60:[1,122]},{13:V,55:123},e($,[2,72]),e($,[2,138]),{22:U,50:oe,59:124,60:te,61:[1,125],82:le,84:111,85:112,86:ie,87:ae,88:Re,89:be,90:Pe},e(Ge,[2,74]),{16:39,17:40,19:126,60:p,86:w,100:S,102:R,103:L},e(G,[2,16]),e(G,[2,17]),e(G,[2,18]),{11:127,12:Oe,39:[2,36]},e(ue,[2,9],{16:85,17:86,15:130,18:[1,129],60:p,86:w,100:S,102:R,103:L}),e(ue,[2,10]),e(ye,[2,55],{11:131,12:Oe}),e(N,[2,7]),{9:[1,132]},e(ke,[2,67]),{16:39,17:40,19:133,60:p,86:w,100:S,102:R,103:L},{13:[1,135],16:39,17:40,19:134,60:p,86:w,100:S,102:R,103:L},e(W,[2,82],{66:136,68:_,69:A,70:M,71:D,72:P}),e(W,[2,81]),{41:[1,137]},{23:99,24:97,30:98,38:22,40:138,42:i,43:23,48:s,54:h,56:d},{8:[1,139],41:[2,38]},{8:[1,140],41:[2,41]},{8:[1,141],41:[2,44]},e($,[2,48],{39:[1,142]}),{41:[1,143]},e($,[2,50]),{41:[2,61],45:144,51:Q},{47:[1,145]},{16:39,17:40,19:146,60:p,86:w,100:S,102:R,103:L},e(I,[2,91],{13:[1,147]}),e(I,[2,93],{13:[1,149],77:[1,148]}),e(I,[2,97],{13:[1,150],80:[1,151]}),{13:[1,152]},e(I,[2,105],{61:ce}),e(re,[2,107],{85:154,22:U,50:oe,60:te,82:le,86:ie,87:ae,88:Re,89:be,90:Pe}),e(J,[2,109]),e(J,[2,111]),e(J,[2,112]),e(J,[2,113]),e(J,[2,114]),e(J,[2,115]),e(J,[2,116]),e(J,[2,117]),e(J,[2,118]),e(J,[2,119]),e(I,[2,106]),e($,[2,71]),e(I,[2,73],{61:ce}),{60:[1,155]},e(G,[2,14]),{39:[2,37]},{13:[1,156]},{15:157,16:85,17:86,60:p,86:w,100:S,102:R,103:L},e(ue,[2,12]),e(ye,[2,56]),{1:[2,4]},e(ke,[2,69]),e(ke,[2,68]),{16:39,17:40,19:158,60:p,86:w,100:S,102:R,103:L},e(W,[2,80]),e($,[2,34]),{41:[1,159]},{23:99,24:97,30:98,38:22,40:160,41:[2,39],42:i,43:23,48:s,54:h,56:d},{23:99,24:97,30:98,38:22,40:161,41:[2,42],42:i,43:23,48:s,54:h,56:d},{23:99,24:97,30:98,38:22,40:162,41:[2,45],42:i,43:23,48:s,54:h,56:d},{45:163,51:Q},e($,[2,49]),{41:[2,62]},e($,[2,52],{39:[1,164]}),e(I,[2,60]),e(I,[2,92]),e(I,[2,94]),e(I,[2,95],{77:[1,165]}),e(I,[2,98]),e(I,[2,99],{13:[1,166]}),e(I,[2,101],{13:[1,168],77:[1,167]}),{22:U,50:oe,60:te,82:le,84:169,85:112,86:ie,87:ae,88:Re,89:be,90:Pe},e(J,[2,110]),e(Ge,[2,75]),{14:[1,170]},e(ue,[2,11]),e(ke,[2,70]),e($,[2,35]),{41:[2,40]},{41:[2,43]},{41:[2,46]},{41:[1,171]},{41:[1,173],45:172,51:Q},e(I,[2,96]),e(I,[2,100]),e(I,[2,102]),e(I,[2,103],{77:[1,174]}),e(re,[2,108],{85:154,22:U,50:oe,60:te,82:le,86:ie,87:ae,88:Re,89:be,90:Pe}),e(ye,[2,8]),e($,[2,51]),{41:[1,175]},e($,[2,54]),e(I,[2,104]),e($,[2,53])],defaultActions:{2:[2,1],3:[2,2],4:[2,3],127:[2,37],132:[2,4],144:[2,62],160:[2,40],161:[2,43],162:[2,46]},parseError:o(function(Me,ve){if(ve.recoverable)this.trace(Me);else{var ne=new Error(Me);throw ne.hash=ve,ne}},"parseError"),parse:o(function(Me){var ve=this,ne=[0],q=[],he=[null],X=[],fe=this.table,K="",qe=0,_e=0,Be=0,Ne=2,He=1,$e=X.slice.call(arguments,1),Xe=Object.create(this.lexer),Fe={yy:{}};for(var Ke in this.yy)Object.prototype.hasOwnProperty.call(this.yy,Ke)&&(Fe.yy[Ke]=this.yy[Ke]);Xe.setInput(Me,Fe.yy),Fe.yy.lexer=Xe,Fe.yy.parser=this,typeof Xe.yylloc>"u"&&(Xe.yylloc={});var xe=Xe.yylloc;X.push(xe);var mt=Xe.options&&Xe.options.ranges;typeof Fe.yy.parseError=="function"?this.parseError=Fe.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function Le(ot){ne.length=ne.length-2*ot,he.length=he.length-ot,X.length=X.length-ot}o(Le,"popStack");function ft(){var ot;return ot=q.pop()||Xe.lex()||He,typeof ot!="number"&&(ot instanceof Array&&(q=ot,ot=q.pop()),ot=ve.symbols_[ot]||ot),ot}o(ft,"lex");for(var wt,zt,St,At,bt,me,lt={},gt,Ze,Ee,tt;;){if(St=ne[ne.length-1],this.defaultActions[St]?At=this.defaultActions[St]:((wt===null||typeof wt>"u")&&(wt=ft()),At=fe[St]&&fe[St][wt]),typeof At>"u"||!At.length||!At[0]){var at="";tt=[];for(gt in fe[St])this.terminals_[gt]&>>Ne&&tt.push("'"+this.terminals_[gt]+"'");Xe.showPosition?at="Parse error on line "+(qe+1)+`: +`+Xe.showPosition()+` +Expecting `+tt.join(", ")+", got '"+(this.terminals_[wt]||wt)+"'":at="Parse error on line "+(qe+1)+": Unexpected "+(wt==He?"end of input":"'"+(this.terminals_[wt]||wt)+"'"),this.parseError(at,{text:Xe.match,token:this.terminals_[wt]||wt,line:Xe.yylineno,loc:xe,expected:tt})}if(At[0]instanceof Array&&At.length>1)throw new Error("Parse Error: multiple actions possible at state: "+St+", token: "+wt);switch(At[0]){case 1:ne.push(wt),he.push(Xe.yytext),X.push(Xe.yylloc),ne.push(At[1]),wt=null,zt?(wt=zt,zt=null):(_e=Xe.yyleng,K=Xe.yytext,qe=Xe.yylineno,xe=Xe.yylloc,Be>0&&Be--);break;case 2:if(Ze=this.productions_[At[1]][1],lt.$=he[he.length-Ze],lt._$={first_line:X[X.length-(Ze||1)].first_line,last_line:X[X.length-1].last_line,first_column:X[X.length-(Ze||1)].first_column,last_column:X[X.length-1].last_column},mt&&(lt._$.range=[X[X.length-(Ze||1)].range[0],X[X.length-1].range[1]]),me=this.performAction.apply(lt,[K,_e,qe,Fe.yy,At[1],he,X].concat($e)),typeof me<"u")return me;Ze&&(ne=ne.slice(0,-1*Ze*2),he=he.slice(0,-1*Ze),X=X.slice(0,-1*Ze)),ne.push(this.productions_[At[1]][0]),he.push(lt.$),X.push(lt._$),Ee=fe[ne[ne.length-2]][ne[ne.length-1]],ne.push(Ee);break;case 3:return!0}}return!0},"parse")},ge=(function(){var we={EOF:1,parseError:o(function(ve,ne){if(this.yy.parser)this.yy.parser.parseError(ve,ne);else throw new Error(ve)},"parseError"),setInput:o(function(Me,ve){return this.yy=ve||this.yy||{},this._input=Me,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var Me=this._input[0];this.yytext+=Me,this.yyleng++,this.offset++,this.match+=Me,this.matched+=Me;var ve=Me.match(/(?:\r\n?|\n).*/g);return ve?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),Me},"input"),unput:o(function(Me){var ve=Me.length,ne=Me.split(/(?:\r\n?|\n)/g);this._input=Me+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-ve),this.offset-=ve;var q=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),ne.length-1&&(this.yylineno-=ne.length-1);var he=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:ne?(ne.length===q.length?this.yylloc.first_column:0)+q[q.length-ne.length].length-ne[0].length:this.yylloc.first_column-ve},this.options.ranges&&(this.yylloc.range=[he[0],he[0]+this.yyleng-ve]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(Me){this.unput(this.match.slice(Me))},"less"),pastInput:o(function(){var Me=this.matched.substr(0,this.matched.length-this.match.length);return(Me.length>20?"...":"")+Me.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var Me=this.match;return Me.length<20&&(Me+=this._input.substr(0,20-Me.length)),(Me.substr(0,20)+(Me.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var Me=this.pastInput(),ve=new Array(Me.length+1).join("-");return Me+this.upcomingInput()+` +`+ve+"^"},"showPosition"),test_match:o(function(Me,ve){var ne,q,he;if(this.options.backtrack_lexer&&(he={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(he.yylloc.range=this.yylloc.range.slice(0))),q=Me[0].match(/(?:\r\n?|\n).*/g),q&&(this.yylineno+=q.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:q?q[q.length-1].length-q[q.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+Me[0].length},this.yytext+=Me[0],this.match+=Me[0],this.matches=Me,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(Me[0].length),this.matched+=Me[0],ne=this.performAction.call(this,this.yy,this,ve,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),ne)return ne;if(this._backtrack){for(var X in he)this[X]=he[X];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var Me,ve,ne,q;this._more||(this.yytext="",this.match="");for(var he=this._currentRules(),X=0;Xve[0].length)){if(ve=ne,q=X,this.options.backtrack_lexer){if(Me=this.test_match(ne,he[X]),Me!==!1)return Me;if(this._backtrack){ve=!1;continue}else return!1}else if(!this.options.flex)break}return ve?(Me=this.test_match(ve,he[q]),Me!==!1?Me:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var ve=this.next();return ve||this.lex()},"lex"),begin:o(function(ve){this.conditionStack.push(ve)},"begin"),popState:o(function(){var ve=this.conditionStack.length-1;return ve>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(ve){return ve=this.conditionStack.length-1-Math.abs(ve||0),ve>=0?this.conditionStack[ve]:"INITIAL"},"topState"),pushState:o(function(ve){this.begin(ve)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{},performAction:o(function(ve,ne,q,he){var X=he;switch(q){case 0:return 62;case 1:return 63;case 2:return 64;case 3:return 65;case 4:break;case 5:break;case 6:return this.begin("acc_title"),33;break;case 7:return this.popState(),"acc_title_value";break;case 8:return this.begin("acc_descr"),35;break;case 9:return this.popState(),"acc_descr_value";break;case 10:this.begin("acc_descr_multiline");break;case 11:this.popState();break;case 12:return"acc_descr_multiline_value";case 13:return 8;case 14:break;case 15:return 7;case 16:return 7;case 17:return"EDGE_STATE";case 18:this.begin("callback_name");break;case 19:this.popState();break;case 20:this.popState(),this.begin("callback_args");break;case 21:return 79;case 22:this.popState();break;case 23:return 80;case 24:this.popState();break;case 25:return"STR";case 26:this.begin("string");break;case 27:return 82;case 28:return 57;case 29:return this.begin("namespace"),42;break;case 30:return this.popState(),8;break;case 31:break;case 32:return this.begin("namespace-body"),39;break;case 33:this.popState(),this.less(0);break;case 34:return this.popState(),41;break;case 35:return"EOF_IN_STRUCT";case 36:return 8;case 37:break;case 38:return"EDGE_STATE";case 39:return this.begin("class"),48;break;case 40:return this.popState(),8;break;case 41:break;case 42:return this.popState(),this.popState(),41;break;case 43:return this.begin("class-body"),39;break;case 44:return this.popState(),41;break;case 45:return"EOF_IN_STRUCT";case 46:return"EDGE_STATE";case 47:return"OPEN_IN_STRUCT";case 48:break;case 49:return"MEMBER";case 50:return 83;case 51:return 75;case 52:return 76;case 53:return 78;case 54:return 54;case 55:return 56;case 56:return 46;case 57:return 47;case 58:return 81;case 59:this.popState();break;case 60:return"GENERICTYPE";case 61:this.begin("generic");break;case 62:this.popState();break;case 63:return"BQUOTE_STR";case 64:this.begin("bqstring");break;case 65:return 77;case 66:return 77;case 67:return 77;case 68:return 77;case 69:return 69;case 70:return 69;case 71:return 71;case 72:return 71;case 73:return 70;case 74:return 68;case 75:return 72;case 76:return 73;case 77:return 74;case 78:return 22;case 79:return 44;case 80:return 100;case 81:return 18;case 82:return"PLUS";case 83:return 87;case 84:return 61;case 85:return 89;case 86:return 89;case 87:return 90;case 88:return"EQUALS";case 89:return"EQUALS";case 90:return 60;case 91:return 12;case 92:return 14;case 93:return"PUNCTUATION";case 94:return 86;case 95:return 102;case 96:return 50;case 97:return 50;case 98:return 9}},"anonymous"),rules:[/^(?:.*direction\s+TB[^\n]*)/,/^(?:.*direction\s+BT[^\n]*)/,/^(?:.*direction\s+RL[^\n]*)/,/^(?:.*direction\s+LR[^\n]*)/,/^(?:%%(?!\{)*[^\n]*(\r?\n?)+)/,/^(?:%%[^\n]*(\r?\n)*)/,/^(?:accTitle\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*\{\s*)/,/^(?:[\}])/,/^(?:[^\}]*)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:classDiagram-v2\b)/,/^(?:classDiagram\b)/,/^(?:\[\*\])/,/^(?:call[\s]+)/,/^(?:\([\s]*\))/,/^(?:\()/,/^(?:[^(]*)/,/^(?:\))/,/^(?:[^)]*)/,/^(?:["])/,/^(?:[^"]*)/,/^(?:["])/,/^(?:style\b)/,/^(?:classDef\b)/,/^(?:namespace\b)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:[{])/,/^(?:[}])/,/^(?:[}])/,/^(?:$)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:\[\*\])/,/^(?:class\b)/,/^(?:\s*(\r?\n)+)/,/^(?:\s+)/,/^(?:[}])/,/^(?:[{])/,/^(?:[}])/,/^(?:$)/,/^(?:\[\*\])/,/^(?:[{])/,/^(?:[\n])/,/^(?:[^{}\n]*)/,/^(?:cssClass\b)/,/^(?:callback\b)/,/^(?:link\b)/,/^(?:click\b)/,/^(?:note for\b)/,/^(?:note\b)/,/^(?:<<)/,/^(?:>>)/,/^(?:href\b)/,/^(?:[~])/,/^(?:[^~]*)/,/^(?:~)/,/^(?:[`])/,/^(?:[^`]+)/,/^(?:[`])/,/^(?:_self\b)/,/^(?:_blank\b)/,/^(?:_parent\b)/,/^(?:_top\b)/,/^(?:\s*<\|)/,/^(?:\s*\|>)/,/^(?:\s*>)/,/^(?:\s*<)/,/^(?:\s*\*)/,/^(?:\s*o\b)/,/^(?:\s*\(\))/,/^(?:--)/,/^(?:\.\.)/,/^(?::{1}[^:\n;]+)/,/^(?::{3})/,/^(?:-)/,/^(?:\.)/,/^(?:\+)/,/^(?::)/,/^(?:,)/,/^(?:#)/,/^(?:#)/,/^(?:%)/,/^(?:=)/,/^(?:=)/,/^(?:\w+)/,/^(?:\[)/,/^(?:\])/,/^(?:[!"#$%&'*+,-.`?\\/])/,/^(?:[0-9]+)/,/^(?:[\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6]|[\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377]|[\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5]|[\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA]|[\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE]|[\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA]|[\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0]|[\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977]|[\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2]|[\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A]|[\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39]|[\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8]|[\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C]|[\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C]|[\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99]|[\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0]|[\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D]|[\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3]|[\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10]|[\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1]|[\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81]|[\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3]|[\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6]|[\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A]|[\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081]|[\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D]|[\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0]|[\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310]|[\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C]|[\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711]|[\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7]|[\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C]|[\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16]|[\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF]|[\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC]|[\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D]|[\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D]|[\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3]|[\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F]|[\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128]|[\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184]|[\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3]|[\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6]|[\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE]|[\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C]|[\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D]|[\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC]|[\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B]|[\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788]|[\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805]|[\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB]|[\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28]|[\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5]|[\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4]|[\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E]|[\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D]|[\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36]|[\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D]|[\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC]|[\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF]|[\uFFD2-\uFFD7\uFFDA-\uFFDC])/,/^(?:\s)/,/^(?:\s)/,/^(?:$)/],conditions:{"namespace-body":{rules:[26,29,34,35,36,37,38,39,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},namespace:{rules:[26,29,30,31,32,33,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},"class-body":{rules:[26,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},class:{rules:[26,40,41,42,43,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},acc_descr_multiline:{rules:[11,12,26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},acc_descr:{rules:[9,26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},acc_title:{rules:[7,26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},callback_args:{rules:[22,23,26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},callback_name:{rules:[19,20,21,26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},href:{rules:[26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},struct:{rules:[26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},generic:{rules:[26,50,51,52,53,54,55,56,57,58,59,60,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},bqstring:{rules:[26,50,51,52,53,54,55,56,57,58,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},string:{rules:[24,25,26,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,87,88,89,90,91,92,93,94,95,96,98],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,8,10,13,14,15,16,17,18,26,27,28,29,39,50,51,52,53,54,55,56,57,58,61,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98],inclusive:!0}}};return we})();se.lexer=ge;function Te(){this.yy={}}return o(Te,"Parser"),Te.prototype=se,se.Parser=Te,new Te})();fY.parser=fY;SL=fY});var yIe,Ww,vIe=F(()=>{"use strict";Xt();Vr();yIe=["#","+","~","-",""],Ww=class{static{o(this,"ClassMember")}constructor(t,r){this.memberType=r,this.visibility="",this.classifier="",this.text="";let n=mr(t,Ae());this.parseMember(n)}getDisplayDetails(){let t=this.visibility+qc(this.id);this.memberType==="method"&&(t+=`(${qc(this.parameters.trim())})`,this.returnType&&(t+=" : "+qc(this.returnType))),t=t.trim();let r=this.parseClassifier();return{displayText:t,cssStyle:r}}parseMember(t){let r="";if(this.memberType==="method"){let a=/([#+~-])?(.+)\((.*)\)([\s$*])?(.*)([$*])?/.exec(t);if(a){let s=a[1]?a[1].trim():"";if(yIe.includes(s)&&(this.visibility=s),this.id=a[2],this.parameters=a[3]?a[3].trim():"",r=a[4]?a[4].trim():"",this.returnType=a[5]?a[5].trim():"",r===""){let l=this.returnType.substring(this.returnType.length-1);/[$*]/.exec(l)&&(r=l,this.returnType=this.returnType.substring(0,this.returnType.length-1))}}}else{let i=t.length,a=t.substring(0,1),s=t.substring(i-1);yIe.includes(a)&&(this.visibility=a),/[$*]/.exec(s)&&(r=s),this.id=t.substring(this.visibility===""?0:1,r===""?i:i-1)}this.classifier=r,this.id=this.id.startsWith(" ")?" "+this.id.trim():this.id.trim();let n=`${this.visibility?"\\"+this.visibility:""}${qc(this.id)}${this.memberType==="method"?`(${qc(this.parameters)})${this.returnType?" : "+qc(this.returnType):""}`:""}`;this.text=n.replaceAll("<","<").replaceAll(">",">"),this.text.startsWith("\\<")&&(this.text=this.text.replace("\\<","~"))}parseClassifier(){switch(this.classifier){case"*":return"font-style:italic;";case"$":return"text-decoration:underline;";default:return""}}}});var EL,xIe,R0,rx,mY=F(()=>{"use strict";$r();vt();Xt();Vr();Qt();Nn();Ud();vIe();H0();EL="classId-",xIe=0,R0=o(e=>xt.sanitizeText(e,Ae()),"sanitizeText"),rx=class e{constructor(){this.relations=[];this.classes=new Map;this.styleClasses=new Map;this.notes=new Map;this.interfaces=[];this.namespaces=new Map;this.namespaceCounter=0;this.namespaceStack=[];this.diagramId="";this.functions=[];this.lineType={LINE:0,DOTTED_LINE:1};this.relationType={AGGREGATION:0,EXTENSION:1,COMPOSITION:2,DEPENDENCY:3,LOLLIPOP:4};this.setupToolTips=o(t=>{let r=Dy();et(t).select("svg").selectAll("g").filter(function(){return et(this).attr("title")!==null}).on("mouseover",a=>{let s=et(a.currentTarget),l=s.attr("title");if(!l)return;let u=a.currentTarget.getBoundingClientRect();r.transition().duration(200).style("opacity",".9"),r.html(Zs.sanitize(l)).style("left",`${window.scrollX+u.left+u.width/2}px`).style("top",`${window.scrollY+u.bottom+4}px`),s.classed("hover",!0)}).on("mouseout",a=>{r.transition().duration(500).style("opacity",0),et(a.currentTarget).classed("hover",!1)})},"setupToolTips");this.direction="TB";this.setAccTitle=kr;this.getAccTitle=Ar;this.setAccDescription=Rr;this.getAccDescription=_r;this.setDiagramTitle=Or;this.getDiagramTitle=Lr;this.getConfig=o(()=>Ae().class,"getConfig");this.functions.push(this.setupToolTips.bind(this)),this.clear(),this.addRelation=this.addRelation.bind(this),this.addClassesToNamespace=this.addClassesToNamespace.bind(this),this.addNamespace=this.addNamespace.bind(this),this.popNamespace=this.popNamespace.bind(this),this.setCssClass=this.setCssClass.bind(this),this.addMembers=this.addMembers.bind(this),this.addClass=this.addClass.bind(this),this.setClassLabel=this.setClassLabel.bind(this),this.addAnnotation=this.addAnnotation.bind(this),this.addMember=this.addMember.bind(this),this.cleanupLabel=this.cleanupLabel.bind(this),this.addNote=this.addNote.bind(this),this.defineClass=this.defineClass.bind(this),this.setDirection=this.setDirection.bind(this),this.setLink=this.setLink.bind(this),this.bindFunctions=this.bindFunctions.bind(this),this.clear=this.clear.bind(this),this.setTooltip=this.setTooltip.bind(this),this.setClickEvent=this.setClickEvent.bind(this),this.setCssStyle=this.setCssStyle.bind(this)}static{o(this,"ClassDB")}splitClassNameAndType(t){let r=xt.sanitizeText(t,Ae()),n="",i=r;if(r.indexOf("~")>0){let a=r.split("~");i=R0(a[0]),n=R0(a[1])}return{className:i,type:n}}setClassLabel(t,r){let n=xt.sanitizeText(t,Ae());r&&(r=R0(r));let{className:i}=this.splitClassNameAndType(n);this.classes.get(i).label=r,this.classes.get(i).text=`${r}${this.classes.get(i).type?`<${this.classes.get(i).type}>`:""}`}addClass(t){let r=xt.sanitizeText(t,Ae()),{className:n,type:i}=this.splitClassNameAndType(r);if(this.classes.has(n))return;let a=xt.sanitizeText(n,Ae());this.classes.set(a,{id:a,type:i,label:a,text:`${a}${i?`<${i}>`:""}`,shape:"classBox",cssClasses:"default",methods:[],members:[],annotations:[],styles:[],domId:EL+a+"-"+xIe}),xIe++}addInterface(t,r){let n={id:`interface${this.interfaces.length}`,label:t,classId:r};this.interfaces.push(n)}setDiagramId(t){this.diagramId=t}lookUpDomId(t){let r=xt.sanitizeText(t,Ae());if(this.classes.has(r)){let n=this.classes.get(r).domId;return this.diagramId?`${this.diagramId}-${n}`:n}throw new Error("Class not found: "+r)}clear(){this.relations=[],this.classes=new Map,this.notes=new Map,this.interfaces=[],this.functions=[],this.functions.push(this.setupToolTips.bind(this)),this.namespaces=new Map,this.namespaceCounter=0,this.namespaceStack=[],this.diagramId="",this.direction="TB",yr()}getClass(t){return this.classes.get(t)}getClasses(){return this.classes}getRelations(){return this.relations}getNote(t){let r=typeof t=="number"?`note${t}`:t;return this.notes.get(r)}getNotes(){return this.notes}addRelation(t){Z.debug("Adding relation: "+JSON.stringify(t));let r=[this.relationType.LOLLIPOP,this.relationType.AGGREGATION,this.relationType.COMPOSITION,this.relationType.DEPENDENCY,this.relationType.EXTENSION];t.relation.type1===this.relationType.LOLLIPOP&&!r.includes(t.relation.type2)?(this.addClass(t.id2),this.addInterface(t.id1,t.id2),t.id1=`interface${this.interfaces.length-1}`):t.relation.type2===this.relationType.LOLLIPOP&&!r.includes(t.relation.type1)?(this.addClass(t.id1),this.addInterface(t.id2,t.id1),t.id2=`interface${this.interfaces.length-1}`):(this.addClass(t.id1),this.addClass(t.id2)),t.id1=this.splitClassNameAndType(t.id1).className,t.id2=this.splitClassNameAndType(t.id2).className,t.relationTitle1=xt.sanitizeText(t.relationTitle1.trim(),Ae()),t.relationTitle2=xt.sanitizeText(t.relationTitle2.trim(),Ae()),this.relations.push(t)}addAnnotation(t,r){let n=this.splitClassNameAndType(t).className;this.classes.get(n).annotations.push(r)}addMember(t,r){this.addClass(t);let n=this.splitClassNameAndType(t).className,i=this.classes.get(n);if(typeof r=="string"){let a=r.trim();a.startsWith("<<")&&a.endsWith(">>")?i.annotations.push(R0(a.substring(2,a.length-2))):a.indexOf(")")>0?i.methods.push(new Ww(a,"method")):a&&i.members.push(new Ww(a,"attribute"))}}addMembers(t,r){Array.isArray(r)&&(r.reverse(),r.forEach(n=>this.addMember(t,n)))}addNote(t,r){let n=this.notes.size,i={id:`note${n}`,class:r,text:t,index:n};return this.notes.set(i.id,i),i.id}cleanupLabel(t){return t.startsWith(":")&&(t=t.substring(1)),R0(t.trim())}setCssClass(t,r){t.split(",").forEach(n=>{let i=n;/\d/.exec(n[0])&&(i=EL+i),i=this.splitClassNameAndType(i).className;let a=this.classes.get(i);a&&(a.cssClasses+=" "+r)})}defineClass(t,r){for(let n of t){let i=this.styleClasses.get(n);i===void 0&&(i={id:n,styles:[],textStyles:[]},this.styleClasses.set(n,i)),r&&r.forEach(a=>{if(/color/.exec(a)){let s=a.replace("fill","bgFill");i.textStyles.push(s)}i.styles.push(a)}),this.classes.forEach(a=>{a.cssClasses.includes(n)&&a.styles.push(...r.flatMap(s=>s.split(",")))})}}setTooltip(t,r){t.split(",").forEach(n=>{if(r!==void 0){let i=this.splitClassNameAndType(n).className,a=this.classes.get(i);a&&(a.tooltip=R0(r))}})}getTooltip(t,r){return r&&this.namespaces.has(r)?this.namespaces.get(r).classes.get(t).tooltip:this.classes.get(t).tooltip}setLink(t,r,n){let i=Ae();t.split(",").forEach(a=>{let s=a;/\d/.exec(a[0])&&(s=EL+s),s=this.splitClassNameAndType(s).className;let l=this.classes.get(s);l&&(l.link=Zt.formatUrl(r,i),i.securityLevel==="sandbox"?l.linkTarget="_top":typeof n=="string"?l.linkTarget=R0(n):l.linkTarget="_blank")}),this.setCssClass(t,"clickable")}setClickEvent(t,r,n){t.split(",").forEach(i=>{this.setClickFunc(i,r,n);let a=this.splitClassNameAndType(i).className,s=this.classes.get(a);s&&(s.haveCallback=!0)}),this.setCssClass(t,"clickable")}setClickFunc(t,r,n){let i=xt.sanitizeText(t,Ae());if(Ae().securityLevel!=="loose"||r===void 0)return;let s=this.splitClassNameAndType(i).className;if(this.classes.has(s)){let l=[];if(typeof n=="string"){l=n.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);for(let u=0;u{let u=this.lookUpDomId(s),h=document.querySelector(`[id="${u}"]`);h!==null&&h.addEventListener("click",()=>{Zt.runFunc(r,...l)},!1)})}}bindFunctions(t){this.functions.forEach(r=>{r(t)})}escapeHtml(t){return t.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}getDirection(){return this.direction}setDirection(t){this.direction=t}static resolveQualifiedId(t,r){let n=r.at(-1);return n?`${n}.${t}`:t}static getAncestorIds(t){let r=t.split("."),n=new Array(r.length);n[0]=r[0];for(let i=1;i0?a[s-1]:void 0,h=s===a.length-1,d=h&&r?r:i[s];this.namespaces.has(l)?h&&(this.namespaces.get(l).explicit=!0):this.namespaces.set(l,this.createNamespaceNode(l,d,u,h)),u&&this.linkParentChild(u,l)}return n}popNamespace(){this.namespaceStack.pop()}getNamespace(t){return this.namespaces.get(t)}getNamespaces(){return this.namespaces}addClassesToNamespace(t,r,n){if(this.namespaces.has(t)){for(let i of r){let{className:a}=this.splitClassNameAndType(i),s=this.getClass(a);s.parent=t,this.namespaces.get(t).classes.set(a,s)}for(let i of n){let a=this.getNote(i);a.parent=t,this.namespaces.get(t).notes.set(i,a)}}}setCssStyle(t,r){let n=this.classes.get(t);if(!(!r||!n))for(let i of r)i.includes(",")?n.styles.push(...i.split(",")):n.styles.push(i)}getArrowMarker(t){let r;switch(t){case 0:r="aggregation";break;case 1:r="extension";break;case 2:r="composition";break;case 3:r="dependency";break;case 4:r="lollipop";break;default:r="none"}return r}resolveExplicitAncestor(t){let r=t;for(;r;){let n=this.namespaces.get(r);if(!n)return;if(n.explicit)return r;r=n.parent}}getData(){let t=[],r=[],n=Ae(),i=n.class?.hierarchicalNamespaces??!0;for(let s of this.namespaces.values()){if(!i&&!s.explicit)continue;let l={id:s.id,label:i?s.label:s.id,isGroup:!0,padding:n.class.padding??16,shape:"rect",cssStyles:[],look:n.look,parentId:i?s.parent:void 0};t.push(l)}for(let s of this.classes.values()){let l=i?s.parent:this.resolveExplicitAncestor(s.parent),u={...s,type:void 0,isGroup:!1,parentId:l,look:n.look};t.push(u)}for(let s of this.notes.values()){let l=i?s.parent:this.resolveExplicitAncestor(s.parent),u={id:s.id,label:s.text,isGroup:!1,shape:"note",padding:n.class.padding??6,cssStyles:["text-align: left","white-space: nowrap",`fill: ${n.themeVariables.noteBkgColor}`,`stroke: ${n.themeVariables.noteBorderColor}`],look:n.look,parentId:l,labelType:"markdown"};t.push(u);let h=this.classes.get(s.class)?.id;if(h){let d={id:`edgeNote${s.index}`,start:s.id,end:h,type:"normal",thickness:"normal",classes:"relation",arrowTypeStart:"none",arrowTypeEnd:"none",arrowheadStyle:"",labelStyle:[""],style:["fill: none"],pattern:"dotted",look:n.look};r.push(d)}}for(let s of this.interfaces){let l={id:s.id,label:s.label,isGroup:!1,shape:"rect",cssStyles:["opacity: 0;"],look:n.look};t.push(l)}let a=0;for(let s of this.relations){a++;let l={id:eu(s.id1,s.id2,{prefix:"id",counter:a}),start:s.id1,end:s.id2,type:"normal",label:s.title,labelpos:"c",thickness:"normal",classes:"relation",arrowTypeStart:this.getArrowMarker(s.relation.type1),arrowTypeEnd:this.getArrowMarker(s.relation.type2),startLabelRight:s.relationTitle1==="none"?"":s.relationTitle1,endLabelLeft:s.relationTitle2==="none"?"":s.relationTitle2,arrowheadStyle:"",labelStyle:["display: inline-block"],style:s.style||"",pattern:s.relation.lineType==1?"dashed":"solid",look:n.look,labelType:"markdown"};r.push(l)}return{nodes:t,edges:r,other:{},config:n,direction:this.getDirection()}}}});var X6t,AL,gY=F(()=>{"use strict";X1();X6t=o(e=>`g.classGroup text { + fill: ${e.nodeBorder||e.classText}; + stroke: none; + font-family: ${e.fontFamily}; + font-size: 10px; + + .title { + font-weight: bolder; + } + +} + + .cluster-label text { + fill: ${e.titleColor}; + } + .cluster-label span { + color: ${e.titleColor}; + } + .cluster-label span p { + background-color: transparent; + } + + .cluster rect { + fill: ${e.clusterBkg}; + stroke: ${e.clusterBorder}; + stroke-width: 1px; + } + + .cluster text { + fill: ${e.titleColor}; + } + + .cluster span { + color: ${e.titleColor}; + } + +.nodeLabel, .edgeLabel { + color: ${e.classText}; +} + +.noteLabel .nodeLabel, .noteLabel .edgeLabel { + color: ${e.noteTextColor}; +} +.edgeLabel .label rect { + fill: ${e.mainBkg}; +} +.label text { + fill: ${e.classText}; +} + +.labelBkg { + background: ${e.mainBkg}; +} +.edgeLabel .label span { + background: ${e.mainBkg}; +} + +.classTitle { + font-weight: bolder; +} +.node rect, + .node circle, + .node ellipse, + .node polygon, + .node path { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; + stroke-width: ${e.strokeWidth}; + } + + +.divider { + stroke: ${e.nodeBorder}; + stroke-width: 1; +} + +g.clickable { + cursor: pointer; +} + +g.classGroup rect { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; +} + +g.classGroup line { + stroke: ${e.nodeBorder}; + stroke-width: 1; +} + +.classLabel .box { + stroke: none; + stroke-width: 0; + fill: ${e.mainBkg}; + opacity: 0.5; +} + +.classLabel .label { + fill: ${e.nodeBorder}; + font-size: 10px; +} + +.relation { + stroke: ${e.lineColor}; + stroke-width: ${e.strokeWidth}; + fill: none; +} + +.dashed-line{ + stroke-dasharray: 3; +} + +.dotted-line{ + stroke-dasharray: 1 2; +} + +[id$="-compositionStart"], .composition { + fill: ${e.lineColor} !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-compositionEnd"], .composition { + fill: ${e.lineColor} !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-dependencyStart"], .dependency { + fill: ${e.lineColor} !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-dependencyEnd"], .dependency { + fill: ${e.lineColor} !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-extensionStart"], .extension { + fill: transparent !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-extensionEnd"], .extension { + fill: transparent !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-aggregationStart"], .aggregation { + fill: transparent !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-aggregationEnd"], .aggregation { + fill: transparent !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-lollipopStart"], .lollipop { + fill: ${e.mainBkg} !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +[id$="-lollipopEnd"], .lollipop { + fill: ${e.mainBkg} !important; + stroke: ${e.lineColor} !important; + stroke-width: 1; +} + +.edgeTerminals { + font-size: 11px; + line-height: initial; +} + +.classTitleText { + text-anchor: middle; + font-size: 18px; + fill: ${e.textColor}; +} + +.edgeLabel[data-look="neo"] { + background-color: ${e.edgeLabelBackground}; + p { + background-color: ${e.edgeLabelBackground}; + } + rect { + opacity: 0.5; + background-color: ${e.edgeLabelBackground}; + fill: ${e.edgeLabelBackground}; + } + text-align: center; +} + ${Eu()} +`,"getStyles"),AL=X6t});var K6t,Z6t,Q6t,RL,yY=F(()=>{"use strict";Xt();vt();Rm();Jf();ep();Qt();K6t=o((e,t="TB")=>{if(!e.doc)return t;let r=t;for(let n of e.doc)n.stmt==="dir"&&(r=n.value);return r},"getDir"),Z6t=o(function(e,t){return t.db.getClasses()},"getClasses"),Q6t=o(async function(e,t,r,n){Z.info("REF0:"),Z.info("Drawing class diagram (v3)",t);let{securityLevel:i,state:a,layout:s}=Ae();n.db.setDiagramId(t);let l=n.db.getData(),u=pl(t,i);l.type=n.type,l.layoutAlgorithm=Su(s),l.nodeSpacing=a?.nodeSpacing||50,l.rankSpacing=a?.rankSpacing||50,l.markers=["aggregation","extension","composition","dependency","lollipop"],l.diagramId=t,await Al(l,u);let h=8;Zt.insertTitle(u,"classDiagramTitleText",a?.titleTopMargin??25,n.db.getDiagramTitle()),vo(u,h,"classDiagram",a?.useMaxWidth??!0)},"draw"),RL={getClasses:Z6t,draw:Q6t,getDir:K6t}});var bIe={};ir(bIe,{diagram:()=>J6t});var J6t,TIe=F(()=>{"use strict";pY();mY();gY();yY();J6t={parser:SL,get db(){return new rx},renderer:RL,styles:AL,init:o(e=>{e.class||(e.class={}),e.class.arrowMarkerAbsolute=e.arrowMarkerAbsolute},"init")}});var kIe={};ir(kIe,{diagram:()=>nRt});var nRt,SIe=F(()=>{"use strict";pY();mY();gY();yY();nRt={parser:SL,get db(){return new rx},renderer:RL,styles:AL,init:o(e=>{e.class||(e.class={}),e.class.arrowMarkerAbsolute=e.arrowMarkerAbsolute},"init")}});var vY,_L,xY=F(()=>{"use strict";vY=(function(){var e=o(function($,V,G,z){for(G=G||{},z=$.length;z--;G[$[z]]=V);return G},"o"),t=[1,2],r=[1,3],n=[1,4],i=[2,4],a=[1,9],s=[1,11],l=[1,16],u=[1,17],h=[1,18],d=[1,19],f=[1,33],p=[1,20],m=[1,21],g=[1,22],y=[1,23],v=[1,24],x=[1,26],b=[1,27],T=[1,28],k=[1,29],C=[1,30],w=[1,31],S=[1,32],R=[1,35],L=[1,36],N=[1,37],I=[1,38],_=[1,34],A=[1,4,5,16,17,19,21,22,24,25,26,27,28,29,33,35,37,38,41,45,48,51,52,53,54,57],M=[1,4,5,14,15,16,17,19,21,22,24,25,26,27,28,29,33,35,37,38,39,40,41,45,48,51,52,53,54,57],D=[4,5,16,17,19,21,22,24,25,26,27,28,29,33,35,37,38,41,45,48,51,52,53,54,57],P={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,SPACE:4,NL:5,SD:6,document:7,line:8,statement:9,classDefStatement:10,styleStatement:11,cssClassStatement:12,idStatement:13,DESCR:14,"-->":15,HIDE_EMPTY:16,scale:17,WIDTH:18,COMPOSIT_STATE:19,STRUCT_START:20,STRUCT_STOP:21,STATE_DESCR:22,AS:23,ID:24,FORK:25,JOIN:26,CHOICE:27,CONCURRENT:28,note:29,notePosition:30,NOTE_TEXT:31,direction:32,acc_title:33,acc_title_value:34,acc_descr:35,acc_descr_value:36,acc_descr_multiline_value:37,CLICK:38,STRING:39,HREF:40,classDef:41,CLASSDEF_ID:42,CLASSDEF_STYLEOPTS:43,DEFAULT:44,style:45,STYLE_IDS:46,STYLEDEF_STYLEOPTS:47,class:48,CLASSENTITY_IDS:49,STYLECLASS:50,direction_tb:51,direction_bt:52,direction_rl:53,direction_lr:54,eol:55,";":56,EDGE_STATE:57,STYLE_SEPARATOR:58,left_of:59,right_of:60,$accept:0,$end:1},terminals_:{2:"error",4:"SPACE",5:"NL",6:"SD",14:"DESCR",15:"-->",16:"HIDE_EMPTY",17:"scale",18:"WIDTH",19:"COMPOSIT_STATE",20:"STRUCT_START",21:"STRUCT_STOP",22:"STATE_DESCR",23:"AS",24:"ID",25:"FORK",26:"JOIN",27:"CHOICE",28:"CONCURRENT",29:"note",31:"NOTE_TEXT",33:"acc_title",34:"acc_title_value",35:"acc_descr",36:"acc_descr_value",37:"acc_descr_multiline_value",38:"CLICK",39:"STRING",40:"HREF",41:"classDef",42:"CLASSDEF_ID",43:"CLASSDEF_STYLEOPTS",44:"DEFAULT",45:"style",46:"STYLE_IDS",47:"STYLEDEF_STYLEOPTS",48:"class",49:"CLASSENTITY_IDS",50:"STYLECLASS",51:"direction_tb",52:"direction_bt",53:"direction_rl",54:"direction_lr",56:";",57:"EDGE_STATE",58:"STYLE_SEPARATOR",59:"left_of",60:"right_of"},productions_:[0,[3,2],[3,2],[3,2],[7,0],[7,2],[8,2],[8,1],[8,1],[9,1],[9,1],[9,1],[9,1],[9,2],[9,3],[9,4],[9,1],[9,2],[9,1],[9,4],[9,3],[9,6],[9,1],[9,1],[9,1],[9,1],[9,4],[9,4],[9,1],[9,2],[9,2],[9,1],[9,5],[9,5],[10,3],[10,3],[11,3],[12,3],[32,1],[32,1],[32,1],[32,1],[55,1],[55,1],[13,1],[13,1],[13,3],[13,3],[30,1],[30,1]],performAction:o(function(V,G,z,W,H,j,Q){var U=j.length-1;switch(H){case 3:return W.setRootDoc(j[U]),j[U];break;case 4:this.$=[];break;case 5:j[U]!="nl"&&(j[U-1].push(j[U]),this.$=j[U-1]);break;case 6:case 7:this.$=j[U];break;case 8:this.$="nl";break;case 12:this.$=j[U];break;case 13:let ie=j[U-1];ie.description=W.trimColon(j[U]),this.$=ie;break;case 14:this.$={stmt:"relation",state1:j[U-2],state2:j[U]};break;case 15:let ae=W.trimColon(j[U]);this.$={stmt:"relation",state1:j[U-3],state2:j[U-1],description:ae};break;case 19:this.$={stmt:"state",id:j[U-3],type:"default",description:"",doc:j[U-1]};break;case 20:var oe=j[U],te=j[U-2].trim();if(j[U].match(":")){var le=j[U].split(":");oe=le[0],te=[te,le[1]]}this.$={stmt:"state",id:oe,type:"default",description:te};break;case 21:this.$={stmt:"state",id:j[U-3],type:"default",description:j[U-5],doc:j[U-1]};break;case 22:this.$={stmt:"state",id:j[U],type:"fork"};break;case 23:this.$={stmt:"state",id:j[U],type:"join"};break;case 24:this.$={stmt:"state",id:j[U],type:"choice"};break;case 25:this.$={stmt:"state",id:W.getDividerId(),type:"divider"};break;case 26:this.$={stmt:"state",id:j[U-1].trim(),note:{position:j[U-2].trim(),text:j[U].trim()}};break;case 29:this.$=j[U].trim(),W.setAccTitle(this.$);break;case 30:case 31:this.$=j[U].trim(),W.setAccDescription(this.$);break;case 32:this.$={stmt:"click",id:j[U-3],url:j[U-2],tooltip:j[U-1]};break;case 33:this.$={stmt:"click",id:j[U-3],url:j[U-1],tooltip:""};break;case 34:case 35:this.$={stmt:"classDef",id:j[U-1].trim(),classes:j[U].trim()};break;case 36:this.$={stmt:"style",id:j[U-1].trim(),styleClass:j[U].trim()};break;case 37:this.$={stmt:"applyClass",id:j[U-1].trim(),styleClass:j[U].trim()};break;case 38:W.setDirection("TB"),this.$={stmt:"dir",value:"TB"};break;case 39:W.setDirection("BT"),this.$={stmt:"dir",value:"BT"};break;case 40:W.setDirection("RL"),this.$={stmt:"dir",value:"RL"};break;case 41:W.setDirection("LR"),this.$={stmt:"dir",value:"LR"};break;case 44:case 45:this.$={stmt:"state",id:j[U].trim(),type:"default",description:""};break;case 46:this.$={stmt:"state",id:j[U-2].trim(),classes:[j[U].trim()],type:"default",description:""};break;case 47:this.$={stmt:"state",id:j[U-2].trim(),classes:[j[U].trim()],type:"default",description:""};break}},"anonymous"),table:[{3:1,4:t,5:r,6:n},{1:[3]},{3:5,4:t,5:r,6:n},{3:6,4:t,5:r,6:n},e([1,4,5,16,17,19,22,24,25,26,27,28,29,33,35,37,38,41,45,48,51,52,53,54,57],i,{7:7}),{1:[2,1]},{1:[2,2]},{1:[2,3],4:a,5:s,8:8,9:10,10:12,11:13,12:14,13:15,16:l,17:u,19:h,22:d,24:f,25:p,26:m,27:g,28:y,29:v,32:25,33:x,35:b,37:T,38:k,41:C,45:w,48:S,51:R,52:L,53:N,54:I,57:_},e(A,[2,5]),{9:39,10:12,11:13,12:14,13:15,16:l,17:u,19:h,22:d,24:f,25:p,26:m,27:g,28:y,29:v,32:25,33:x,35:b,37:T,38:k,41:C,45:w,48:S,51:R,52:L,53:N,54:I,57:_},e(A,[2,7]),e(A,[2,8]),e(A,[2,9]),e(A,[2,10]),e(A,[2,11]),e(A,[2,12],{14:[1,40],15:[1,41]}),e(A,[2,16]),{18:[1,42]},e(A,[2,18],{20:[1,43]}),{23:[1,44]},e(A,[2,22]),e(A,[2,23]),e(A,[2,24]),e(A,[2,25]),{30:45,31:[1,46],59:[1,47],60:[1,48]},e(A,[2,28]),{34:[1,49]},{36:[1,50]},e(A,[2,31]),{13:51,24:f,57:_},{42:[1,52],44:[1,53]},{46:[1,54]},{49:[1,55]},e(M,[2,44],{58:[1,56]}),e(M,[2,45],{58:[1,57]}),e(A,[2,38]),e(A,[2,39]),e(A,[2,40]),e(A,[2,41]),e(A,[2,6]),e(A,[2,13]),{13:58,24:f,57:_},e(A,[2,17]),e(D,i,{7:59}),{24:[1,60]},{24:[1,61]},{23:[1,62]},{24:[2,48]},{24:[2,49]},e(A,[2,29]),e(A,[2,30]),{39:[1,63],40:[1,64]},{43:[1,65]},{43:[1,66]},{47:[1,67]},{50:[1,68]},{24:[1,69]},{24:[1,70]},e(A,[2,14],{14:[1,71]}),{4:a,5:s,8:8,9:10,10:12,11:13,12:14,13:15,16:l,17:u,19:h,21:[1,72],22:d,24:f,25:p,26:m,27:g,28:y,29:v,32:25,33:x,35:b,37:T,38:k,41:C,45:w,48:S,51:R,52:L,53:N,54:I,57:_},e(A,[2,20],{20:[1,73]}),{31:[1,74]},{24:[1,75]},{39:[1,76]},{39:[1,77]},e(A,[2,34]),e(A,[2,35]),e(A,[2,36]),e(A,[2,37]),e(M,[2,46]),e(M,[2,47]),e(A,[2,15]),e(A,[2,19]),e(D,i,{7:78}),e(A,[2,26]),e(A,[2,27]),{5:[1,79]},{5:[1,80]},{4:a,5:s,8:8,9:10,10:12,11:13,12:14,13:15,16:l,17:u,19:h,21:[1,81],22:d,24:f,25:p,26:m,27:g,28:y,29:v,32:25,33:x,35:b,37:T,38:k,41:C,45:w,48:S,51:R,52:L,53:N,54:I,57:_},e(A,[2,32]),e(A,[2,33]),e(A,[2,21])],defaultActions:{5:[2,1],6:[2,2],47:[2,48],48:[2,49]},parseError:o(function(V,G){if(G.recoverable)this.trace(V);else{var z=new Error(V);throw z.hash=G,z}},"parseError"),parse:o(function(V){var G=this,z=[0],W=[],H=[null],j=[],Q=this.table,U="",oe=0,te=0,le=0,ie=2,ae=1,Re=j.slice.call(arguments,1),be=Object.create(this.lexer),Pe={yy:{}};for(var Ge in this.yy)Object.prototype.hasOwnProperty.call(this.yy,Ge)&&(Pe.yy[Ge]=this.yy[Ge]);be.setInput(V,Pe.yy),Pe.yy.lexer=be,Pe.yy.parser=this,typeof be.yylloc>"u"&&(be.yylloc={});var Oe=be.yylloc;j.push(Oe);var ue=be.options&&be.options.ranges;typeof Pe.yy.parseError=="function"?this.parseError=Pe.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function ye(X){z.length=z.length-2*X,H.length=H.length-X,j.length=j.length-X}o(ye,"popStack");function ke(){var X;return X=W.pop()||be.lex()||ae,typeof X!="number"&&(X instanceof Array&&(W=X,X=W.pop()),X=G.symbols_[X]||X),X}o(ke,"lex");for(var ce,re,J,se,ge,Te,we={},Me,ve,ne,q;;){if(J=z[z.length-1],this.defaultActions[J]?se=this.defaultActions[J]:((ce===null||typeof ce>"u")&&(ce=ke()),se=Q[J]&&Q[J][ce]),typeof se>"u"||!se.length||!se[0]){var he="";q=[];for(Me in Q[J])this.terminals_[Me]&&Me>ie&&q.push("'"+this.terminals_[Me]+"'");be.showPosition?he="Parse error on line "+(oe+1)+`: +`+be.showPosition()+` +Expecting `+q.join(", ")+", got '"+(this.terminals_[ce]||ce)+"'":he="Parse error on line "+(oe+1)+": Unexpected "+(ce==ae?"end of input":"'"+(this.terminals_[ce]||ce)+"'"),this.parseError(he,{text:be.match,token:this.terminals_[ce]||ce,line:be.yylineno,loc:Oe,expected:q})}if(se[0]instanceof Array&&se.length>1)throw new Error("Parse Error: multiple actions possible at state: "+J+", token: "+ce);switch(se[0]){case 1:z.push(ce),H.push(be.yytext),j.push(be.yylloc),z.push(se[1]),ce=null,re?(ce=re,re=null):(te=be.yyleng,U=be.yytext,oe=be.yylineno,Oe=be.yylloc,le>0&&le--);break;case 2:if(ve=this.productions_[se[1]][1],we.$=H[H.length-ve],we._$={first_line:j[j.length-(ve||1)].first_line,last_line:j[j.length-1].last_line,first_column:j[j.length-(ve||1)].first_column,last_column:j[j.length-1].last_column},ue&&(we._$.range=[j[j.length-(ve||1)].range[0],j[j.length-1].range[1]]),Te=this.performAction.apply(we,[U,te,oe,Pe.yy,se[1],H,j].concat(Re)),typeof Te<"u")return Te;ve&&(z=z.slice(0,-1*ve*2),H=H.slice(0,-1*ve),j=j.slice(0,-1*ve)),z.push(this.productions_[se[1]][0]),H.push(we.$),j.push(we._$),ne=Q[z[z.length-2]][z[z.length-1]],z.push(ne);break;case 3:return!0}}return!0},"parse")},B=(function(){var $={EOF:1,parseError:o(function(G,z){if(this.yy.parser)this.yy.parser.parseError(G,z);else throw new Error(G)},"parseError"),setInput:o(function(V,G){return this.yy=G||this.yy||{},this._input=V,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var V=this._input[0];this.yytext+=V,this.yyleng++,this.offset++,this.match+=V,this.matched+=V;var G=V.match(/(?:\r\n?|\n).*/g);return G?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),V},"input"),unput:o(function(V){var G=V.length,z=V.split(/(?:\r\n?|\n)/g);this._input=V+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-G),this.offset-=G;var W=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),z.length-1&&(this.yylineno-=z.length-1);var H=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:z?(z.length===W.length?this.yylloc.first_column:0)+W[W.length-z.length].length-z[0].length:this.yylloc.first_column-G},this.options.ranges&&(this.yylloc.range=[H[0],H[0]+this.yyleng-G]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(V){this.unput(this.match.slice(V))},"less"),pastInput:o(function(){var V=this.matched.substr(0,this.matched.length-this.match.length);return(V.length>20?"...":"")+V.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var V=this.match;return V.length<20&&(V+=this._input.substr(0,20-V.length)),(V.substr(0,20)+(V.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var V=this.pastInput(),G=new Array(V.length+1).join("-");return V+this.upcomingInput()+` +`+G+"^"},"showPosition"),test_match:o(function(V,G){var z,W,H;if(this.options.backtrack_lexer&&(H={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(H.yylloc.range=this.yylloc.range.slice(0))),W=V[0].match(/(?:\r\n?|\n).*/g),W&&(this.yylineno+=W.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:W?W[W.length-1].length-W[W.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+V[0].length},this.yytext+=V[0],this.match+=V[0],this.matches=V,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(V[0].length),this.matched+=V[0],z=this.performAction.call(this,this.yy,this,G,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),z)return z;if(this._backtrack){for(var j in H)this[j]=H[j];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var V,G,z,W;this._more||(this.yytext="",this.match="");for(var H=this._currentRules(),j=0;jG[0].length)){if(G=z,W=j,this.options.backtrack_lexer){if(V=this.test_match(z,H[j]),V!==!1)return V;if(this._backtrack){G=!1;continue}else return!1}else if(!this.options.flex)break}return G?(V=this.test_match(G,H[W]),V!==!1?V:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var G=this.next();return G||this.lex()},"lex"),begin:o(function(G){this.conditionStack.push(G)},"begin"),popState:o(function(){var G=this.conditionStack.length-1;return G>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(G){return G=this.conditionStack.length-1-Math.abs(G||0),G>=0?this.conditionStack[G]:"INITIAL"},"topState"),pushState:o(function(G){this.begin(G)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:o(function(G,z,W,H){function j(){let U=z.yytext.indexOf("%%");if(U===0)return!1;if(U>0){let oe=z.yytext.slice(0,U),te=z.yytext.slice(U);te&&G.lexer.unput(te),z.yytext=oe}return!0}o(j,"processId");var Q=H;switch(W){case 0:return 38;case 1:return 40;case 2:return 39;case 3:return 44;case 4:return 51;case 5:return 52;case 6:return 53;case 7:return 54;case 8:return 5;case 9:break;case 10:break;case 11:break;case 12:break;case 13:return this.pushState("SCALE"),17;break;case 14:return 18;case 15:this.popState();break;case 16:return this.begin("acc_title"),33;break;case 17:return this.popState(),"acc_title_value";break;case 18:return this.begin("acc_descr"),35;break;case 19:return this.popState(),"acc_descr_value";break;case 20:this.begin("acc_descr_multiline");break;case 21:this.popState();break;case 22:return"acc_descr_multiline_value";case 23:return this.pushState("CLASSDEF"),41;break;case 24:return this.popState(),this.pushState("CLASSDEFID"),"DEFAULT_CLASSDEF_ID";break;case 25:return this.popState(),this.pushState("CLASSDEFID"),42;break;case 26:return this.popState(),43;break;case 27:return this.pushState("CLASS"),48;break;case 28:return this.popState(),this.pushState("CLASS_STYLE"),49;break;case 29:return this.popState(),50;break;case 30:return this.pushState("STYLE"),45;break;case 31:return this.popState(),this.pushState("STYLEDEF_STYLES"),46;break;case 32:return this.popState(),47;break;case 33:return this.pushState("SCALE"),17;break;case 34:return 18;case 35:this.popState();break;case 36:this.pushState("STATE");break;case 37:return this.popState(),z.yytext=z.yytext.slice(0,-8).trim(),25;break;case 38:return this.popState(),z.yytext=z.yytext.slice(0,-8).trim(),26;break;case 39:return this.popState(),z.yytext=z.yytext.slice(0,-10).trim(),27;break;case 40:return this.popState(),z.yytext=z.yytext.slice(0,-8).trim(),25;break;case 41:return this.popState(),z.yytext=z.yytext.slice(0,-8).trim(),26;break;case 42:return this.popState(),z.yytext=z.yytext.slice(0,-10).trim(),27;break;case 43:return 51;case 44:return 52;case 45:return 53;case 46:return 54;case 47:this.pushState("STATE_STRING");break;case 48:return this.pushState("STATE_ID"),"AS";break;case 49:if(!j())return;return this.popState(),"ID";break;case 50:this.popState();break;case 51:return"STATE_DESCR";case 52:throw new Error('Error: State name must be a single word. Found: "'+z.yytext.trim()+'"');case 53:return 19;case 54:this.popState();break;case 55:return this.popState(),this.pushState("struct"),20;break;case 56:return this.popState(),21;break;case 57:break;case 58:return this.begin("NOTE"),29;break;case 59:return this.popState(),this.pushState("NOTE_ID"),59;break;case 60:return this.popState(),this.pushState("NOTE_ID"),60;break;case 61:this.popState(),this.pushState("FLOATING_NOTE");break;case 62:return this.popState(),this.pushState("FLOATING_NOTE_ID"),"AS";break;case 63:break;case 64:return"NOTE_TEXT";case 65:if(!j())return;return this.popState(),"ID";break;case 66:if(!j())return;return this.popState(),this.pushState("NOTE_TEXT"),24;break;case 67:return this.popState(),z.yytext=z.yytext.substr(2).trim(),31;break;case 68:return this.popState(),z.yytext=z.yytext.slice(0,-8).trim(),31;break;case 69:return 6;case 70:return 6;case 71:return 16;case 72:return 57;case 73:return j()?24:void 0;case 74:return z.yytext=z.yytext.trim(),14;break;case 75:return 15;case 76:return 28;case 77:return 58;case 78:return 5;case 79:return"INVALID"}},"anonymous"),rules:[/^(?:click\b)/i,/^(?:href\b)/i,/^(?:"[^"]*")/i,/^(?:default\b)/i,/^(?:.*direction\s+TB[^\n]*)/i,/^(?:.*direction\s+BT[^\n]*)/i,/^(?:.*direction\s+RL[^\n]*)/i,/^(?:.*direction\s+LR[^\n]*)/i,/^(?:[\n]+)/i,/^(?:[\s]+)/i,/^(?:((?!\n)\s)+)/i,/^(?:#[^\n]*)/i,/^(?:%%(?!\{)[^\n]*)/i,/^(?:scale\s+)/i,/^(?:\d+)/i,/^(?:\s+width\b)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:classDef\s+)/i,/^(?:DEFAULT\s+)/i,/^(?:\w+\s+)/i,/^(?:[^\n]*)/i,/^(?:class\s+)/i,/^(?:(\w+)+((,\s*\w+)*))/i,/^(?:[^\n]*)/i,/^(?:style\s+)/i,/^(?:[\w,]+\s+)/i,/^(?:[^\n]*)/i,/^(?:scale\s+)/i,/^(?:\d+)/i,/^(?:\s+width\b)/i,/^(?:state\s+)/i,/^(?:.*<>)/i,/^(?:.*<>)/i,/^(?:.*<>)/i,/^(?:.*\[\[fork\]\])/i,/^(?:.*\[\[join\]\])/i,/^(?:.*\[\[choice\]\])/i,/^(?:.*direction\s+TB[^\n]*)/i,/^(?:.*direction\s+BT[^\n]*)/i,/^(?:.*direction\s+RL[^\n]*)/i,/^(?:.*direction\s+LR[^\n]*)/i,/^(?:["])/i,/^(?:\s*as\s+)/i,/^(?:[^\n\{]*)/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:\w+\s+\w+.*?\{)/i,/^(?:[^\n\s\{]+)/i,/^(?:\n)/i,/^(?:\{)/i,/^(?:\})/i,/^(?:[\n])/i,/^(?:note\s+)/i,/^(?:left of\b)/i,/^(?:right of\b)/i,/^(?:")/i,/^(?:\s*as\s*)/i,/^(?:["])/i,/^(?:[^"]*)/i,/^(?:[^\n]*)/i,/^(?:\s*[^:\n\s\-]+)/i,/^(?:\s*:[^:\n;]+)/i,/^(?:[\s\S]*?\n\s*end note\b)/i,/^(?:stateDiagram\s+)/i,/^(?:stateDiagram-v2\s+)/i,/^(?:hide empty description\b)/i,/^(?:\[\*\])/i,/^(?:[^:\n\s\-\{]+)/i,/^(?:\s*:(?:[^:\n;]|:[^:\n;])+)/i,/^(?:-->)/i,/^(?:--)/i,/^(?::::)/i,/^(?:$)/i,/^(?:.)/i],conditions:{LINE:{rules:[10,11,12],inclusive:!1},struct:{rules:[10,11,12,23,27,30,36,43,44,45,46,56,57,58,72,73,74,75,76,77],inclusive:!1},FLOATING_NOTE_ID:{rules:[65],inclusive:!1},FLOATING_NOTE:{rules:[62,63,64],inclusive:!1},NOTE_TEXT:{rules:[67,68],inclusive:!1},NOTE_ID:{rules:[66],inclusive:!1},NOTE:{rules:[59,60,61],inclusive:!1},STYLEDEF_STYLEOPTS:{rules:[],inclusive:!1},STYLEDEF_STYLES:{rules:[32],inclusive:!1},STYLE_IDS:{rules:[],inclusive:!1},STYLE:{rules:[31],inclusive:!1},CLASS_STYLE:{rules:[29],inclusive:!1},CLASS:{rules:[28],inclusive:!1},CLASSDEFID:{rules:[26],inclusive:!1},CLASSDEF:{rules:[24,25],inclusive:!1},acc_descr_multiline:{rules:[21,22],inclusive:!1},acc_descr:{rules:[19],inclusive:!1},acc_title:{rules:[17],inclusive:!1},SCALE:{rules:[14,15,34,35],inclusive:!1},ALIAS:{rules:[],inclusive:!1},STATE_ID:{rules:[49],inclusive:!1},STATE_STRING:{rules:[50,51],inclusive:!1},FORK_STATE:{rules:[],inclusive:!1},STATE:{rules:[10,11,12,37,38,39,40,41,42,47,48,52,53,54,55],inclusive:!1},ID:{rules:[10,11,12],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,7,8,9,11,12,13,16,18,20,23,27,30,33,36,55,58,69,70,71,72,73,74,75,77,78,79],inclusive:!0}}};return $})();P.lexer=B;function O(){this.yy={}}return o(O,"Parser"),O.prototype=P,P.Parser=O,new O})();vY.parser=vY;_L=vY});var _p,_0,qw,RIe,_Ie,LIe,L0,LL,bY,TY,CY,wY,DL,IL,DIe,IIe,kY,SY,MIe,NIe,nx,oRt,PIe,EY,lRt,cRt,OIe,BIe,uRt,$Ie,hRt,FIe,AY,RY,zIe,ML,GIe,_Y,NL=F(()=>{"use strict";_p="state",_0="root",qw="relation",RIe="classDef",_Ie="style",LIe="applyClass",L0="default",LL="divider",bY="fill:none",TY="fill: #333",CY="markdown",wY="normal",DL="rect",IL="rectWithTitle",DIe="stateStart",IIe="stateEnd",kY="divider",SY="roundedWithTitle",MIe="note",NIe="noteGroup",nx="statediagram",oRt="state",PIe=`${nx}-${oRt}`,EY="transition",lRt="note",cRt="note-edge",OIe=`${EY} ${cRt}`,BIe=`${nx}-${lRt}`,uRt="cluster",$Ie=`${nx}-${uRt}`,hRt="cluster-alt",FIe=`${nx}-${hRt}`,AY="parent",RY="note",zIe="state",ML="----",GIe=`${ML}${RY}`,_Y=`${ML}${AY}`});function LY(e="",t=0,r="",n=ML){let i=r!==null&&r.length>0?`${n}${r}`:"";return`${zIe}-${e}${i}-${t}`}function PL(e,t,r){if(!t.id||t.id===""||t.id==="")return;t.cssClasses&&(Array.isArray(t.cssCompiledStyles)||(t.cssCompiledStyles=[]),t.cssClasses.split(" ").forEach(i=>{let a=r.get(i);a&&(t.cssCompiledStyles=[...t.cssCompiledStyles??[],...a.styles])}));let n=e.find(i=>i.id===t.id);n?Object.assign(n,t):e.push(t)}function fRt(e){return e?.classes?.join(" ")??""}function pRt(e){return e?.styles??[]}var OL,Lp,dRt,VIe,ix,qIe,HIe=F(()=>{"use strict";Xt();vt();Vr();NL();OL=new Map,Lp=0;o(LY,"stateDomId");dRt=o((e,t,r,n,i,a,s,l)=>{Z.trace("items",t),t.forEach(u=>{switch(u.stmt){case _p:ix(e,u,r,n,i,a,s,l);break;case L0:ix(e,u,r,n,i,a,s,l);break;case qw:{ix(e,u.state1,r,n,i,a,s,l),ix(e,u.state2,r,n,i,a,s,l);let h=s==="neo",d={id:"edge"+Lp,start:u.state1.id,end:u.state2.id,arrowhead:"normal",arrowTypeEnd:h?"arrow_barb_neo":"arrow_barb",style:bY,labelStyle:"",label:xt.sanitizeText(u.description??"",Ae()),arrowheadStyle:TY,labelpos:"c",labelType:CY,thickness:wY,classes:EY,look:s};i.push(d),Lp++}break}})},"setupDoc"),VIe=o((e,t="TB")=>{let r=t;if(e.doc)for(let n of e.doc)n.stmt==="dir"&&(r=n.value);return r},"getDir");o(PL,"insertOrUpdateNode");o(fRt,"getClassesFromDbInfo");o(pRt,"getStylesFromDbInfo");ix=o((e,t,r,n,i,a,s,l)=>{let u=t.id,h=r.get(u),d=fRt(h),f=pRt(h),p=Ae();if(Z.info("dataFetcher parsedItem",t,h,f),u!=="root"){let m=DL;t.start===!0?m=DIe:t.start===!1&&(m=IIe),t.type!==L0&&(m=t.type),OL.get(u)||OL.set(u,{id:u,shape:m,description:xt.sanitizeText(u,p),cssClasses:`${d} ${PIe}`,cssStyles:f});let g=OL.get(u);t.description&&(Array.isArray(g.description)?(g.shape=IL,g.description.push(t.description)):g.description?.length&&g.description.length>0?(g.shape=IL,g.description===u?g.description=[t.description]:g.description=[g.description,t.description]):(g.shape=DL,g.description=t.description),g.description=xt.sanitizeTextOrArray(g.description,p)),g.description?.length===1&&g.shape===IL&&(g.type==="group"?g.shape=SY:g.shape=DL),!g.type&&t.doc&&(Z.info("Setting cluster for XCX",u,VIe(t)),g.type="group",g.isGroup=!0,g.dir=VIe(t),g.explicitDir=t.doc.some(v=>v.stmt==="dir"),g.shape=t.type===LL?kY:SY,g.cssClasses=`${g.cssClasses} ${$Ie} ${a?FIe:""}`);let y={labelStyle:"",shape:g.shape,label:g.description,cssClasses:g.cssClasses,cssCompiledStyles:[],cssStyles:g.cssStyles,id:u,dir:g.dir,domId:LY(u,Lp),type:g.type,isGroup:g.type==="group",padding:8,rx:10,ry:10,look:s,labelType:"markdown"};if(y.shape===kY&&(y.label=""),e&&e.id!=="root"&&(Z.trace("Setting node ",u," to be child of its parent ",e.id),y.parentId=e.id),y.centerLabel=!0,t.note){let v={labelStyle:"",shape:MIe,label:t.note.text,labelType:"markdown",cssClasses:BIe,cssStyles:[],cssCompiledStyles:[],id:u+GIe+"-"+Lp,domId:LY(u,Lp,RY),type:g.type,isGroup:g.type==="group",padding:p.flowchart?.padding,look:s,position:t.note.position},x=u+_Y,b={labelStyle:"",shape:NIe,label:t.note.text,cssClasses:g.cssClasses,cssStyles:[],id:u+_Y,domId:LY(u,Lp,AY),type:"group",isGroup:!0,padding:16,look:s,position:t.note.position};Lp++,b.id=x,v.parentId=x,PL(n,b,l),PL(n,v,l),PL(n,y,l);let T=u,k=v.id;t.note.position==="left of"&&(T=v.id,k=u),i.push({id:T+"-"+k,start:T,end:k,arrowhead:"none",arrowTypeEnd:"",style:bY,labelStyle:"",classes:OIe,arrowheadStyle:TY,labelpos:"c",labelType:CY,thickness:wY,look:s})}else PL(n,y,l)}t.doc&&(Z.trace("Adding nodes children "),dRt(t,t.doc,r,n,i,!a,s,l))},"dataFetcher"),qIe=o(()=>{OL.clear(),Lp=0},"reset")});var IY,mRt,gRt,UIe,MY=F(()=>{"use strict";Xt();vt();Rm();Jf();ep();Qt();NL();IY=o((e,t="TB")=>{if(!e.doc)return t;let r=t;for(let n of e.doc)n.stmt==="dir"&&(r=n.value);return r},"getDir"),mRt=o(function(e,t){return t.db.getClasses()},"getClasses"),gRt=o(async function(e,t,r,n){Z.info("REF0:"),Z.info("Drawing state diagram (v2)",t);let{securityLevel:i,state:a,layout:s}=Ae();n.db.extract(n.db.getRootDocV2());let l=n.db.getData(),u=pl(t,i);l.type=n.type,l.layoutAlgorithm=s,l.nodeSpacing=a?.nodeSpacing||50,l.rankSpacing=a?.rankSpacing||50,Ae().look==="neo"?l.markers=["barbNeo"]:l.markers=["barb"],l.diagramId=t,await Al(l,u);let d=8;try{(typeof n.db.getLinks=="function"?n.db.getLinks():new Map).forEach((p,m)=>{let g=typeof m=="string"?m:typeof m?.id=="string"?m.id:"",y=l.nodes.find(C=>C.id===g);if(!g){Z.warn("\u26A0\uFE0F Invalid or missing stateId from key:",JSON.stringify(m));return}let v=u.node()?.querySelectorAll("g.node, g.rough-node"),x;if(v?.forEach(C=>{let w=C.textContent?.trim();(C.id===y?.domId||w===g)&&(x=C)}),!x){Z.warn("\u26A0\uFE0F Could not find node matching text:",g);return}let b=x.parentNode;if(!b){Z.warn("\u26A0\uFE0F Node has no parent, cannot wrap:",g);return}let T=document.createElementNS("http://www.w3.org/2000/svg","a"),k=p.url.replace(/^"+|"+$/g,"");if(T.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",k),T.setAttribute("target","_blank"),p.tooltip){let C=p.tooltip.replace(/^"+|"+$/g,"");T.setAttribute("title",C),x.setAttribute("title",C)}b.replaceChild(T,x),T.appendChild(x),Z.info("\u{1F517} Wrapped node in
tag for:",g,p.url)})}catch(f){Z.error("\u274C Error injecting clickable links:",f)}Zt.insertTitle(u,"statediagramTitleText",a?.titleTopMargin??25,n.db.getDiagramTitle()),vo(u,d,nx,a?.useMaxWidth??!0)},"draw"),UIe={getClasses:mRt,draw:gRt,getDir:IY}});var Ys,jIe,XIe,BL,Nl,$L=F(()=>{"use strict";$r();H0();Xt();vt();Qt();Vr();Nn();Ud();HIe();MY();NL();Ys={START_NODE:"[*]",START_TYPE:"start",END_NODE:"[*]",END_TYPE:"end",COLOR_KEYWORD:"color",FILL_KEYWORD:"fill",BG_FILL:"bgFill",STYLECLASS_SEP:","},jIe=o(()=>new Map,"newClassesList"),XIe=o(()=>({relations:[],states:new Map,documents:{}}),"newDoc"),BL=o(e=>JSON.parse(JSON.stringify(e)),"clone"),Nl=class{constructor(t){this.version=t;this.nodes=[];this.edges=[];this.rootDoc=[];this.classes=jIe();this.documents={root:XIe()};this.currentDocument=this.documents.root;this.startEndCount=0;this.dividerCnt=0;this.links=new Map;this.funs=[];this.getAccTitle=Ar;this.setAccTitle=kr;this.getAccDescription=_r;this.setAccDescription=Rr;this.setDiagramTitle=Or;this.getDiagramTitle=Lr;this.clear(),this.setRootDoc=this.setRootDoc.bind(this),this.getDividerId=this.getDividerId.bind(this),this.setDirection=this.setDirection.bind(this),this.trimColon=this.trimColon.bind(this),this.bindFunctions=this.bindFunctions.bind(this)}static{o(this,"StateDB")}static{this.relationType={AGGREGATION:0,EXTENSION:1,COMPOSITION:2,DEPENDENCY:3}}extract(t){this.clear(!0);for(let i of Array.isArray(t)?t:t.doc)switch(i.stmt){case _p:this.addState(i.id.trim(),i.type,i.doc,i.description,i.note);break;case qw:this.addRelation(i.state1,i.state2,i.description);break;case RIe:this.addStyleClass(i.id.trim(),i.classes);break;case _Ie:this.handleStyleDef(i);break;case LIe:this.setCssClass(i.id.trim(),i.styleClass);break;case"click":this.addLink(i.id,i.url,i.tooltip);break}let r=this.getStates(),n=Ae();qIe(),ix(void 0,this.getRootDocV2(),r,this.nodes,this.edges,!0,n.look,this.classes);for(let i of this.nodes)if(Array.isArray(i.label)){if(i.description=i.label.slice(1),i.isGroup&&i.description.length>0)throw new Error(`Group nodes can only have label. Remove the additional description for node [${i.id}]`);i.label=i.label[0]}}handleStyleDef(t){let r=t.id.trim().split(","),n=t.styleClass.split(",");for(let i of r){let a=this.getState(i);if(!a){let s=i.trim();this.addState(s),a=this.getState(s)}a&&(a.styles=n.map(s=>s.replace(/;/g,"")?.trim()))}}setRootDoc(t){Z.info("Setting root doc",t),this.rootDoc=t,this.version===1?this.extract(t):this.extract(this.getRootDocV2())}docTranslator(t,r,n){if(r.stmt===qw){this.docTranslator(t,r.state1,!0),this.docTranslator(t,r.state2,!1);return}if(r.stmt===_p&&(r.id===Ys.START_NODE?(r.id=t.id+(n?"_start":"_end"),r.start=n):r.id=r.id.trim()),r.stmt!==_0&&r.stmt!==_p||!r.doc)return;let i=[],a=[];for(let s of r.doc)if(s.type===LL){let l=BL(s);l.doc=BL(a),i.push(l),a=[]}else a.push(s);if(i.length>0&&a.length>0){let s={stmt:_p,id:fP(),type:"divider",doc:BL(a)};i.push(BL(s)),r.doc=i}r.doc.forEach(s=>this.docTranslator(r,s,!0))}getRootDocV2(){return this.docTranslator({id:_0,stmt:_0},{id:_0,stmt:_0,doc:this.rootDoc},!0),{id:_0,doc:this.rootDoc}}addState(t,r=L0,n=void 0,i=void 0,a=void 0,s=void 0,l=void 0,u=void 0){let h=t?.trim();if(!this.currentDocument.states.has(h))Z.info("Adding state ",h,i),this.currentDocument.states.set(h,{stmt:_p,id:h,descriptions:[],type:r,doc:n,note:a,classes:[],styles:[],textStyles:[]});else{let d=this.currentDocument.states.get(h);if(!d)throw new Error(`State not found: ${h}`);d.doc||(d.doc=n),d.type||(d.type=r)}if(i&&(Z.info("Setting state description",h,i),(Array.isArray(i)?i:[i]).forEach(f=>this.addDescription(h,f.trim()))),a){let d=this.currentDocument.states.get(h);if(!d)throw new Error(`State not found: ${h}`);d.note=a,d.note.text=xt.sanitizeText(d.note.text,Ae())}s&&(Z.info("Setting state classes",h,s),(Array.isArray(s)?s:[s]).forEach(f=>this.setCssClass(h,f.trim()))),l&&(Z.info("Setting state styles",h,l),(Array.isArray(l)?l:[l]).forEach(f=>this.setStyle(h,f.trim()))),u&&(Z.info("Setting state styles",h,l),(Array.isArray(u)?u:[u]).forEach(f=>this.setTextStyle(h,f.trim())))}clear(t){this.nodes=[],this.edges=[],this.funs=[this.setupToolTips.bind(this)],this.documents={root:XIe()},this.currentDocument=this.documents.root,this.startEndCount=0,this.classes=jIe(),t||(this.links=new Map,yr())}getState(t){return this.currentDocument.states.get(t)}getStates(){return this.currentDocument.states}logDocuments(){Z.info("Documents = ",this.documents)}getRelations(){return this.currentDocument.relations}addLink(t,r,n){this.links.set(t,{url:r,tooltip:n}),Z.warn("Adding link",t,r,n)}getLinks(){return this.links}startIdIfNeeded(t=""){return t===Ys.START_NODE?(this.startEndCount++,`${Ys.START_TYPE}${this.startEndCount}`):t}startTypeIfNeeded(t="",r=L0){return t===Ys.START_NODE?Ys.START_TYPE:r}endIdIfNeeded(t=""){return t===Ys.END_NODE?(this.startEndCount++,`${Ys.END_TYPE}${this.startEndCount}`):t}endTypeIfNeeded(t="",r=L0){return t===Ys.END_NODE?Ys.END_TYPE:r}addRelationObjs(t,r,n=""){let i=this.startIdIfNeeded(t.id.trim()),a=this.startTypeIfNeeded(t.id.trim(),t.type),s=this.startIdIfNeeded(r.id.trim()),l=this.startTypeIfNeeded(r.id.trim(),r.type);this.addState(i,a,t.doc,t.description,t.note,t.classes,t.styles,t.textStyles),this.addState(s,l,r.doc,r.description,r.note,r.classes,r.styles,r.textStyles),this.currentDocument.relations.push({id1:i,id2:s,relationTitle:xt.sanitizeText(n,Ae())})}addRelation(t,r,n){if(typeof t=="object"&&typeof r=="object")this.addRelationObjs(t,r,n);else if(typeof t=="string"&&typeof r=="string"){let i=this.startIdIfNeeded(t.trim()),a=this.startTypeIfNeeded(t),s=this.endIdIfNeeded(r.trim()),l=this.endTypeIfNeeded(r);this.addState(i,a),this.addState(s,l),this.currentDocument.relations.push({id1:i,id2:s,relationTitle:n?xt.sanitizeText(n,Ae()):void 0})}}addDescription(t,r){let n=this.currentDocument.states.get(t),i=r.startsWith(":")?r.replace(":","").trim():r;n?.descriptions?.push(xt.sanitizeText(i,Ae()))}cleanupLabel(t){return t.startsWith(":")?t.slice(2).trim():t.trim()}getDividerId(){return this.dividerCnt++,`divider-id-${this.dividerCnt}`}addStyleClass(t,r=""){this.classes.has(t)||this.classes.set(t,{id:t,styles:[],textStyles:[]});let n=this.classes.get(t);r&&n&&r.split(Ys.STYLECLASS_SEP).forEach(i=>{let a=i.replace(/([^;]*);/,"$1").trim();if(RegExp(Ys.COLOR_KEYWORD).exec(i)){let l=a.replace(Ys.FILL_KEYWORD,Ys.BG_FILL).replace(Ys.COLOR_KEYWORD,Ys.FILL_KEYWORD);n.textStyles.push(l)}n.styles.push(a)})}getClasses(){return this.classes}setupToolTips(t){let r=Dy();et(t).select("svg").selectAll("g.node, g.rough-node").on("mouseover",a=>{let s=et(a.currentTarget),l=s.attr("title");if(l===null)return;let u=a.currentTarget?.getBoundingClientRect();r.transition().duration(200).style("opacity",".9"),r.style("left",window.scrollX+u.left+(u.right-u.left)/2+"px").style("top",window.scrollY+u.bottom+"px"),r.html(Zs.sanitize(l)),s.classed("hover",!0)}).on("mouseout",a=>{r.transition().duration(500).style("opacity",0),et(a.currentTarget).classed("hover",!1)})}setCssClass(t,r){t.split(",").forEach(n=>{let i=this.getState(n);if(!i){let a=n.trim();this.addState(a),i=this.getState(a)}i?.classes?.push(r)})}setStyle(t,r){this.getState(t)?.styles?.push(r)}setTextStyle(t,r){this.getState(t)?.textStyles?.push(r)}bindFunctions(t){this.funs.forEach(r=>{r(t)})}getDirectionStatement(){return this.rootDoc.find(t=>t.stmt==="dir")}getDirection(){return this.getDirectionStatement()?.value??"TB"}setDirection(t){let r=this.getDirectionStatement();r?r.value=t:this.rootDoc.unshift({stmt:"dir",value:t})}trimColon(t){return t.startsWith(":")?t.slice(1).trim():t.trim()}getData(){let t=Ae();return{nodes:this.nodes,edges:this.edges,other:{},config:t,direction:IY(this.getRootDocV2())}}getConfig(){return Ae().state}}});var vRt,FL,NY=F(()=>{"use strict";vRt=o(e=>` +defs [id$="-barbEnd"] { + fill: ${e.transitionColor}; + stroke: ${e.transitionColor}; + } +g.stateGroup text { + fill: ${e.nodeBorder}; + stroke: none; + font-size: 10px; +} +g.stateGroup text { + fill: ${e.textColor}; + stroke: none; + font-size: 10px; + +} +g.stateGroup .state-title { + font-weight: bolder; + fill: ${e.stateLabelColor}; +} + +g.stateGroup rect { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; +} + +g.stateGroup line { + stroke: ${e.lineColor}; + stroke-width: ${e.strokeWidth||1}; +} + +.transition { + stroke: ${e.transitionColor}; + stroke-width: ${e.strokeWidth||1}; + fill: none; +} + +.stateGroup .composit { + fill: ${e.background}; + border-bottom: 1px +} + +.stateGroup .alt-composit { + fill: #e0e0e0; + border-bottom: 1px +} + +.state-note { + stroke: ${e.noteBorderColor}; + fill: ${e.noteBkgColor}; + + text { + fill: ${e.noteTextColor}; + stroke: none; + font-size: 10px; + } +} + +.stateLabel .box { + stroke: none; + stroke-width: 0; + fill: ${e.mainBkg}; + opacity: 0.5; +} + +.edgeLabel .label rect { + fill: ${e.labelBackgroundColor}; + opacity: 0.5; +} +.edgeLabel { + background-color: ${e.edgeLabelBackground}; + p { + background-color: ${e.edgeLabelBackground}; + } + rect { + opacity: 0.5; + background-color: ${e.edgeLabelBackground}; + fill: ${e.edgeLabelBackground}; + } + text-align: center; +} +.edgeLabel .label text { + fill: ${e.transitionLabelColor||e.tertiaryTextColor}; +} +.label div .edgeLabel { + color: ${e.transitionLabelColor||e.tertiaryTextColor}; +} + +.stateLabel text { + fill: ${e.stateLabelColor}; + font-size: 10px; + font-weight: bold; +} + +.node circle.state-start { + fill: ${e.specialStateColor}; + stroke: ${e.specialStateColor}; +} + +.node .fork-join { + fill: ${e.specialStateColor}; + stroke: ${e.specialStateColor}; +} + +.node circle.state-end { + fill: ${e.innerEndBackground}; + stroke: ${e.background}; + stroke-width: 1.5 +} +.end-state-inner { + fill: ${e.compositeBackground||e.background}; + // stroke: ${e.background}; + stroke-width: 1.5 +} + +.node rect { + fill: ${e.stateBkg||e.mainBkg}; + stroke: ${e.stateBorder||e.nodeBorder}; + stroke-width: ${e.strokeWidth||1}px; +} +.node polygon { + fill: ${e.mainBkg}; + stroke: ${e.stateBorder||e.nodeBorder};; + stroke-width: ${e.strokeWidth||1}px; +} +[id$="-barbEnd"] { + fill: ${e.lineColor}; +} + +.statediagram-cluster rect { + fill: ${e.compositeTitleBackground}; + stroke: ${e.stateBorder||e.nodeBorder}; + stroke-width: ${e.strokeWidth||1}px; +} + +.cluster-label, .nodeLabel { + color: ${e.stateLabelColor}; + // line-height: 1; +} + +.statediagram-cluster rect.outer { + rx: 5px; + ry: 5px; +} +.statediagram-state .divider { + stroke: ${e.stateBorder||e.nodeBorder}; +} + +.statediagram-state .title-state { + rx: 5px; + ry: 5px; +} +.statediagram-cluster.statediagram-cluster .inner { + fill: ${e.compositeBackground||e.background}; +} +.statediagram-cluster.statediagram-cluster-alt .inner { + fill: ${e.altBackground?e.altBackground:"#efefef"}; +} + +.statediagram-cluster .inner { + rx:0; + ry:0; +} + +.statediagram-state rect.basic { + rx: 5px; + ry: 5px; +} +.statediagram-state rect.divider { + stroke-dasharray: 10,10; + fill: ${e.altBackground?e.altBackground:"#efefef"}; +} + +.note-edge { + stroke-dasharray: 5; +} + +.statediagram-note rect { + fill: ${e.noteBkgColor}; + stroke: ${e.noteBorderColor}; + stroke-width: 1px; + rx: 0; + ry: 0; +} +.statediagram-note rect { + fill: ${e.noteBkgColor}; + stroke: ${e.noteBorderColor}; + stroke-width: 1px; + rx: 0; + ry: 0; +} + +.statediagram-note text { + fill: ${e.noteTextColor}; +} + +.statediagram-note .nodeLabel { + color: ${e.noteTextColor}; +} +.statediagram .edgeLabel { + color: red; // ${e.noteTextColor}; +} + +[id$="-dependencyStart"], [id$="-dependencyEnd"] { + fill: ${e.lineColor}; + stroke: ${e.lineColor}; + stroke-width: ${e.strokeWidth||1}; +} + +.statediagramTitleText { + text-anchor: middle; + font-size: 18px; + fill: ${e.textColor}; +} + +[data-look="neo"].statediagram-cluster rect { + fill: ${e.mainBkg}; + stroke: ${e.useGradient?"url("+e.svgId+"-gradient)":e.stateBorder||e.nodeBorder}; + stroke-width: ${e.strokeWidth??1}; +} +[data-look="neo"].statediagram-cluster rect.outer { + rx: ${e.radius}px; + ry: ${e.radius}px; + filter: ${e.dropShadow?e.dropShadow.replace("url(#drop-shadow)",`url(${e.svgId}-drop-shadow)`):"none"} +} +`,"getStyles"),FL=vRt});var xRt,bRt,TRt,CRt,ZIe,wRt,kRt,SRt,ERt,PY,KIe,QIe,JIe=F(()=>{"use strict";$r();$L();Qt();Vr();Xt();vt();xRt=o(e=>e.append("circle").attr("class","start-state").attr("r",Ae().state.sizeUnit).attr("cx",Ae().state.padding+Ae().state.sizeUnit).attr("cy",Ae().state.padding+Ae().state.sizeUnit),"drawStartState"),bRt=o(e=>e.append("line").style("stroke","grey").style("stroke-dasharray","3").attr("x1",Ae().state.textHeight).attr("class","divider").attr("x2",Ae().state.textHeight*2).attr("y1",0).attr("y2",0),"drawDivider"),TRt=o((e,t)=>{let r=e.append("text").attr("x",2*Ae().state.padding).attr("y",Ae().state.textHeight+2*Ae().state.padding).attr("font-size",Ae().state.fontSize).attr("class","state-title").text(t.id),n=r.node().getBBox();return e.insert("rect",":first-child").attr("x",Ae().state.padding).attr("y",Ae().state.padding).attr("width",n.width+2*Ae().state.padding).attr("height",n.height+2*Ae().state.padding).attr("rx",Ae().state.radius),r},"drawSimpleState"),CRt=o((e,t)=>{let r=o(function(p,m,g){let y=p.append("tspan").attr("x",2*Ae().state.padding).text(m);g||y.attr("dy",Ae().state.textHeight)},"addTspan"),i=e.append("text").attr("x",2*Ae().state.padding).attr("y",Ae().state.textHeight+1.3*Ae().state.padding).attr("font-size",Ae().state.fontSize).attr("class","state-title").text(t.descriptions[0]).node().getBBox(),a=i.height,s=e.append("text").attr("x",Ae().state.padding).attr("y",a+Ae().state.padding*.4+Ae().state.dividerMargin+Ae().state.textHeight).attr("class","state-description"),l=!0,u=!0;t.descriptions.forEach(function(p){l||(r(s,p,u),u=!1),l=!1});let h=e.append("line").attr("x1",Ae().state.padding).attr("y1",Ae().state.padding+a+Ae().state.dividerMargin/2).attr("y2",Ae().state.padding+a+Ae().state.dividerMargin/2).attr("class","descr-divider"),d=s.node().getBBox(),f=Math.max(d.width,i.width);return h.attr("x2",f+3*Ae().state.padding),e.insert("rect",":first-child").attr("x",Ae().state.padding).attr("y",Ae().state.padding).attr("width",f+2*Ae().state.padding).attr("height",d.height+a+2*Ae().state.padding).attr("rx",Ae().state.radius),e},"drawDescrState"),ZIe=o((e,t,r)=>{let n=Ae().state.padding,i=2*Ae().state.padding,a=e.node().getBBox(),s=a.width,l=a.x,u=e.append("text").attr("x",0).attr("y",Ae().state.titleShift).attr("font-size",Ae().state.fontSize).attr("class","state-title").text(t.id),d=u.node().getBBox().width+i,f=Math.max(d,s);f===s&&(f=f+i);let p,m=e.node().getBBox();t.doc,p=l-n,d>s&&(p=(s-f)/2+n),Math.abs(l-m.x)s&&(p=l-(d-s)/2);let g=1-Ae().state.textHeight;return e.insert("rect",":first-child").attr("x",p).attr("y",g).attr("class",r?"alt-composit":"composit").attr("width",f).attr("height",m.height+Ae().state.textHeight+Ae().state.titleShift+1).attr("rx","0"),u.attr("x",p+n),d<=s&&u.attr("x",l+(f-i)/2-d/2+n),e.insert("rect",":first-child").attr("x",p).attr("y",Ae().state.titleShift-Ae().state.textHeight-Ae().state.padding).attr("width",f).attr("height",Ae().state.textHeight*3).attr("rx",Ae().state.radius),e.insert("rect",":first-child").attr("x",p).attr("y",Ae().state.titleShift-Ae().state.textHeight-Ae().state.padding).attr("width",f).attr("height",m.height+3+2*Ae().state.textHeight).attr("rx",Ae().state.radius),e},"addTitleAndBox"),wRt=o(e=>(e.append("circle").attr("class","end-state-outer").attr("r",Ae().state.sizeUnit+Ae().state.miniPadding).attr("cx",Ae().state.padding+Ae().state.sizeUnit+Ae().state.miniPadding).attr("cy",Ae().state.padding+Ae().state.sizeUnit+Ae().state.miniPadding),e.append("circle").attr("class","end-state-inner").attr("r",Ae().state.sizeUnit).attr("cx",Ae().state.padding+Ae().state.sizeUnit+2).attr("cy",Ae().state.padding+Ae().state.sizeUnit+2)),"drawEndState"),kRt=o((e,t)=>{let r=Ae().state.forkWidth,n=Ae().state.forkHeight;if(t.parentId){let i=r;r=n,n=i}return e.append("rect").style("stroke","black").style("fill","black").attr("width",r).attr("height",n).attr("x",Ae().state.padding).attr("y",Ae().state.padding)},"drawForkJoinState"),SRt=o((e,t,r,n)=>{let i=0,a=n.append("text");a.style("text-anchor","start"),a.attr("class","noteText");let s=e.replace(/\r\n/g,"
");s=s.replace(/\n/g,"
");let l=s.split(xt.lineBreakRegex),u=1.25*Ae().state.noteMargin;for(let h of l){let d=h.trim();if(d.length>0){let f=a.append("tspan");if(f.text(d),u===0){let p=f.node().getBBox();u+=p.height}i+=u,f.attr("x",t+Ae().state.noteMargin),f.attr("y",r+i+1.25*Ae().state.noteMargin)}}return{textWidth:a.node().getBBox().width,textHeight:i}},"_drawLongText"),ERt=o((e,t)=>{t.attr("class","state-note");let r=t.append("rect").attr("x",0).attr("y",Ae().state.padding),n=t.append("g"),{textWidth:i,textHeight:a}=SRt(e,0,0,n);return r.attr("height",a+2*Ae().state.noteMargin),r.attr("width",i+Ae().state.noteMargin*2),r},"drawNote"),PY=o(function(e,t){let r=t.id,n={id:r,label:t.id,width:0,height:0},i=e.append("g").attr("id",r).attr("class","stateGroup");t.type==="start"&&xRt(i),t.type==="end"&&wRt(i),(t.type==="fork"||t.type==="join")&&kRt(i,t),t.type==="note"&&ERt(t.note.text,i),t.type==="divider"&&bRt(i),t.type==="default"&&t.descriptions.length===0&&TRt(i,t),t.type==="default"&&t.descriptions.length>0&&CRt(i,t);let a=i.node().getBBox();return n.width=a.width+2*Ae().state.padding,n.height=a.height+2*Ae().state.padding,n},"drawState"),KIe=0,QIe=o(function(e,t,r){let n=o(function(u){switch(u){case Nl.relationType.AGGREGATION:return"aggregation";case Nl.relationType.EXTENSION:return"extension";case Nl.relationType.COMPOSITION:return"composition";case Nl.relationType.DEPENDENCY:return"dependency"}},"getRelationType");t.points=t.points.filter(u=>!Number.isNaN(u.y));let i=t.points,a=tc().x(function(u){return u.x}).y(function(u){return u.y}).curve(rc),s=e.append("path").attr("d",a(i)).attr("id","edge"+KIe).attr("class","transition"),l="";if(Ae().state.arrowMarkerAbsolute&&(l=qp(!0)),s.attr("marker-end","url("+l+"#"+n(Nl.relationType.DEPENDENCY)+"End)"),r.title!==void 0){let u=e.append("g").attr("class","stateLabel"),{x:h,y:d}=Zt.calcLabelPosition(t.points),f=xt.getRows(r.title),p=0,m=[],g=0,y=0;for(let b=0;b<=f.length;b++){let T=u.append("text").attr("text-anchor","middle").text(f[b]).attr("x",h).attr("y",d+p),k=T.node().getBBox();g=Math.max(g,k.width),y=Math.min(y,k.x),Z.info(k.x,h,d+p),p===0&&(p=T.node().getBBox().height,Z.info("Title height",p,d)),m.push(T)}let v=p*f.length;if(f.length>1){let b=(f.length-1)*p*.5;m.forEach((T,k)=>T.attr("y",d+k*p-b)),v=p*f.length}let x=u.node().getBBox();u.insert("rect",":first-child").attr("class","box").attr("x",h-g/2-Ae().state.padding/2).attr("y",d-v/2-Ae().state.padding/2-3.5).attr("width",g+Ae().state.padding).attr("height",v+Ae().state.padding),Z.info(x)}KIe++},"drawEdge")});var tl,OY,ARt,RRt,_Rt,LRt,eMe,tMe,rMe=F(()=>{"use strict";$r();U9();qo();vt();Vr();JIe();Xt();$n();OY={},ARt=o(function(){},"setConf"),RRt=o(function(e){e.append("defs").append("marker").attr("id","dependencyEnd").attr("refX",19).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").append("path").attr("d","M 19,7 L9,13 L14,7 L9,1 Z")},"insertMarkers"),_Rt=o(function(e,t,r,n){tl=Ae().state;let i=Ae().securityLevel,a;i==="sandbox"&&(a=et("#i"+t));let s=i==="sandbox"?et(a.nodes()[0].contentDocument.body):et("body"),l=i==="sandbox"?a.nodes()[0].contentDocument:document;Z.debug("Rendering diagram "+e);let u=s.select(`[id='${t}']`);RRt(u);let h=n.db.getRootDoc(),d=u.append("g").attr("id",t+"-root");eMe(h,d,void 0,!1,s,l,n);let f=tl.padding,p=u.node().getBBox(),m=p.width+f*2,g=p.height+f*2,y=m*1.75;Wr(u,g,y,tl.useMaxWidth),u.attr("viewBox",`${p.x-tl.padding} ${p.y-tl.padding} `+m+" "+g)},"draw"),LRt=o(e=>e?e.length*tl.fontSizeFactor:1,"getLabelWidth"),eMe=o((e,t,r,n,i,a,s)=>{let l=new on({compound:!0,multigraph:!0}),u,h=!0;for(u=0;u{let C=k.parentElement,w=0,S=0;C&&(C.parentElement&&(w=C.parentElement.getBBox().width),S=parseInt(C.getAttribute("data-x-shift"),10),Number.isNaN(S)&&(S=0)),k.setAttribute("x1",0-S+8),k.setAttribute("x2",w-S-8)})):Z.debug("No Node "+b+": "+JSON.stringify(l.node(b)))});let v=y.getBBox();l.edges().forEach(function(b){b!==void 0&&l.edge(b)!==void 0&&(Z.debug("Edge "+b.v+" -> "+b.w+": "+JSON.stringify(l.edge(b))),QIe(t,l.edge(b),l.edge(b).relation))}),v=y.getBBox();let x={id:r||"root",label:r||"root",width:0,height:0};return x.width=v.width+2*tl.padding,x.height=v.height+2*tl.padding,Z.debug("Doc rendered",x,l),x},"renderDoc"),tMe={setConf:ARt,draw:_Rt}});var nMe={};ir(nMe,{diagram:()=>DRt});var DRt,iMe=F(()=>{"use strict";xY();$L();NY();rMe();DRt={parser:_L,get db(){return new Nl(1)},renderer:tMe,styles:FL,init:o(e=>{e.state||(e.state={}),e.state.arrowMarkerAbsolute=e.arrowMarkerAbsolute},"init")}});var oMe={};ir(oMe,{diagram:()=>PRt});var PRt,lMe=F(()=>{"use strict";xY();$L();NY();MY();PRt={parser:_L,get db(){return new Nl(2)},renderer:UIe,styles:FL,init:o(e=>{e.state||(e.state={}),e.state.arrowMarkerAbsolute=e.arrowMarkerAbsolute},"init")}});var BY,hMe,dMe=F(()=>{"use strict";BY=(function(){var e=o(function(f,p,m,g){for(m=m||{},g=f.length;g--;m[f[g]]=p);return m},"o"),t=[6,8,10,11,12,14,16,17,18],r=[1,9],n=[1,10],i=[1,11],a=[1,12],s=[1,13],l=[1,14],u={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,journey:4,document:5,EOF:6,line:7,SPACE:8,statement:9,NEWLINE:10,title:11,acc_title:12,acc_title_value:13,acc_descr:14,acc_descr_value:15,acc_descr_multiline_value:16,section:17,taskName:18,taskData:19,$accept:0,$end:1},terminals_:{2:"error",4:"journey",6:"EOF",8:"SPACE",10:"NEWLINE",11:"title",12:"acc_title",13:"acc_title_value",14:"acc_descr",15:"acc_descr_value",16:"acc_descr_multiline_value",17:"section",18:"taskName",19:"taskData"},productions_:[0,[3,3],[5,0],[5,2],[7,2],[7,1],[7,1],[7,1],[9,1],[9,2],[9,2],[9,1],[9,1],[9,2]],performAction:o(function(p,m,g,y,v,x,b){var T=x.length-1;switch(v){case 1:return x[T-1];case 2:this.$=[];break;case 3:x[T-1].push(x[T]),this.$=x[T-1];break;case 4:case 5:this.$=x[T];break;case 6:case 7:this.$=[];break;case 8:y.setDiagramTitle(x[T].substr(6)),this.$=x[T].substr(6);break;case 9:this.$=x[T].trim(),y.setAccTitle(this.$);break;case 10:case 11:this.$=x[T].trim(),y.setAccDescription(this.$);break;case 12:y.addSection(x[T].substr(8)),this.$=x[T].substr(8);break;case 13:y.addTask(x[T-1],x[T]),this.$="task";break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},e(t,[2,2],{5:3}),{6:[1,4],7:5,8:[1,6],9:7,10:[1,8],11:r,12:n,14:i,16:a,17:s,18:l},e(t,[2,7],{1:[2,1]}),e(t,[2,3]),{9:15,11:r,12:n,14:i,16:a,17:s,18:l},e(t,[2,5]),e(t,[2,6]),e(t,[2,8]),{13:[1,16]},{15:[1,17]},e(t,[2,11]),e(t,[2,12]),{19:[1,18]},e(t,[2,4]),e(t,[2,9]),e(t,[2,10]),e(t,[2,13])],defaultActions:{},parseError:o(function(p,m){if(m.recoverable)this.trace(p);else{var g=new Error(p);throw g.hash=m,g}},"parseError"),parse:o(function(p){var m=this,g=[0],y=[],v=[null],x=[],b=this.table,T="",k=0,C=0,w=0,S=2,R=1,L=x.slice.call(arguments,1),N=Object.create(this.lexer),I={yy:{}};for(var _ in this.yy)Object.prototype.hasOwnProperty.call(this.yy,_)&&(I.yy[_]=this.yy[_]);N.setInput(p,I.yy),I.yy.lexer=N,I.yy.parser=this,typeof N.yylloc>"u"&&(N.yylloc={});var A=N.yylloc;x.push(A);var M=N.options&&N.options.ranges;typeof I.yy.parseError=="function"?this.parseError=I.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function D(te){g.length=g.length-2*te,v.length=v.length-te,x.length=x.length-te}o(D,"popStack");function P(){var te;return te=y.pop()||N.lex()||R,typeof te!="number"&&(te instanceof Array&&(y=te,te=y.pop()),te=m.symbols_[te]||te),te}o(P,"lex");for(var B,O,$,V,G,z,W={},H,j,Q,U;;){if($=g[g.length-1],this.defaultActions[$]?V=this.defaultActions[$]:((B===null||typeof B>"u")&&(B=P()),V=b[$]&&b[$][B]),typeof V>"u"||!V.length||!V[0]){var oe="";U=[];for(H in b[$])this.terminals_[H]&&H>S&&U.push("'"+this.terminals_[H]+"'");N.showPosition?oe="Parse error on line "+(k+1)+`: +`+N.showPosition()+` +Expecting `+U.join(", ")+", got '"+(this.terminals_[B]||B)+"'":oe="Parse error on line "+(k+1)+": Unexpected "+(B==R?"end of input":"'"+(this.terminals_[B]||B)+"'"),this.parseError(oe,{text:N.match,token:this.terminals_[B]||B,line:N.yylineno,loc:A,expected:U})}if(V[0]instanceof Array&&V.length>1)throw new Error("Parse Error: multiple actions possible at state: "+$+", token: "+B);switch(V[0]){case 1:g.push(B),v.push(N.yytext),x.push(N.yylloc),g.push(V[1]),B=null,O?(B=O,O=null):(C=N.yyleng,T=N.yytext,k=N.yylineno,A=N.yylloc,w>0&&w--);break;case 2:if(j=this.productions_[V[1]][1],W.$=v[v.length-j],W._$={first_line:x[x.length-(j||1)].first_line,last_line:x[x.length-1].last_line,first_column:x[x.length-(j||1)].first_column,last_column:x[x.length-1].last_column},M&&(W._$.range=[x[x.length-(j||1)].range[0],x[x.length-1].range[1]]),z=this.performAction.apply(W,[T,C,k,I.yy,V[1],v,x].concat(L)),typeof z<"u")return z;j&&(g=g.slice(0,-1*j*2),v=v.slice(0,-1*j),x=x.slice(0,-1*j)),g.push(this.productions_[V[1]][0]),v.push(W.$),x.push(W._$),Q=b[g[g.length-2]][g[g.length-1]],g.push(Q);break;case 3:return!0}}return!0},"parse")},h=(function(){var f={EOF:1,parseError:o(function(m,g){if(this.yy.parser)this.yy.parser.parseError(m,g);else throw new Error(m)},"parseError"),setInput:o(function(p,m){return this.yy=m||this.yy||{},this._input=p,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var p=this._input[0];this.yytext+=p,this.yyleng++,this.offset++,this.match+=p,this.matched+=p;var m=p.match(/(?:\r\n?|\n).*/g);return m?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),p},"input"),unput:o(function(p){var m=p.length,g=p.split(/(?:\r\n?|\n)/g);this._input=p+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-m),this.offset-=m;var y=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),g.length-1&&(this.yylineno-=g.length-1);var v=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:g?(g.length===y.length?this.yylloc.first_column:0)+y[y.length-g.length].length-g[0].length:this.yylloc.first_column-m},this.options.ranges&&(this.yylloc.range=[v[0],v[0]+this.yyleng-m]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(p){this.unput(this.match.slice(p))},"less"),pastInput:o(function(){var p=this.matched.substr(0,this.matched.length-this.match.length);return(p.length>20?"...":"")+p.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var p=this.match;return p.length<20&&(p+=this._input.substr(0,20-p.length)),(p.substr(0,20)+(p.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var p=this.pastInput(),m=new Array(p.length+1).join("-");return p+this.upcomingInput()+` +`+m+"^"},"showPosition"),test_match:o(function(p,m){var g,y,v;if(this.options.backtrack_lexer&&(v={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(v.yylloc.range=this.yylloc.range.slice(0))),y=p[0].match(/(?:\r\n?|\n).*/g),y&&(this.yylineno+=y.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:y?y[y.length-1].length-y[y.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+p[0].length},this.yytext+=p[0],this.match+=p[0],this.matches=p,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(p[0].length),this.matched+=p[0],g=this.performAction.call(this,this.yy,this,m,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),g)return g;if(this._backtrack){for(var x in v)this[x]=v[x];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var p,m,g,y;this._more||(this.yytext="",this.match="");for(var v=this._currentRules(),x=0;xm[0].length)){if(m=g,y=x,this.options.backtrack_lexer){if(p=this.test_match(g,v[x]),p!==!1)return p;if(this._backtrack){m=!1;continue}else return!1}else if(!this.options.flex)break}return m?(p=this.test_match(m,v[y]),p!==!1?p:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var m=this.next();return m||this.lex()},"lex"),begin:o(function(m){this.conditionStack.push(m)},"begin"),popState:o(function(){var m=this.conditionStack.length-1;return m>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(m){return m=this.conditionStack.length-1-Math.abs(m||0),m>=0?this.conditionStack[m]:"INITIAL"},"topState"),pushState:o(function(m){this.begin(m)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:o(function(m,g,y,v){var x=v;switch(y){case 0:break;case 1:break;case 2:return 10;case 3:break;case 4:break;case 5:return 4;case 6:return 11;case 7:return this.begin("acc_title"),12;break;case 8:return this.popState(),"acc_title_value";break;case 9:return this.begin("acc_descr"),14;break;case 10:return this.popState(),"acc_descr_value";break;case 11:this.begin("acc_descr_multiline");break;case 12:this.popState();break;case 13:return"acc_descr_multiline_value";case 14:return 17;case 15:return 18;case 16:return 19;case 17:return":";case 18:return 6;case 19:return"INVALID"}},"anonymous"),rules:[/^(?:%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:#[^\n]*)/i,/^(?:journey\b)/i,/^(?:title\s[^#\n;]+)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:section\s[^#:\n;]+)/i,/^(?:[^#:\n;]+)/i,/^(?::[^#\n;]+)/i,/^(?::)/i,/^(?:$)/i,/^(?:.)/i],conditions:{acc_descr_multiline:{rules:[12,13],inclusive:!1},acc_descr:{rules:[10],inclusive:!1},acc_title:{rules:[8],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,7,9,11,14,15,16,17,18,19],inclusive:!0}}};return f})();u.lexer=h;function d(){this.yy={}}return o(d,"Parser"),d.prototype=u,u.Parser=d,new d})();BY.parser=BY;hMe=BY});var ax,$Y,Hw,Uw,FRt,zRt,GRt,VRt,WRt,qRt,HRt,fMe,URt,FY,pMe=F(()=>{"use strict";Xt();Nn();ax="",$Y=[],Hw=[],Uw=[],FRt=o(function(){$Y.length=0,Hw.length=0,ax="",Uw.length=0,yr()},"clear"),zRt=o(function(e){ax=e,$Y.push(e)},"addSection"),GRt=o(function(){return $Y},"getSections"),VRt=o(function(){let e=fMe(),t=100,r=0;for(;!e&&r{r.people&&e.push(...r.people)}),[...new Set(e)].sort()},"updateActors"),qRt=o(function(e,t){let r=t.substr(1).split(":"),n=0,i=[];r.length===1?(n=Number(r[0]),i=[]):(n=Number(r[0]),i=r[1].split(","));let a=i.map(l=>l.trim()),s={section:ax,type:ax,people:a,task:e,score:n};Uw.push(s)},"addTask"),HRt=o(function(e){let t={section:ax,type:ax,description:e,task:e,classes:[]};Hw.push(t)},"addTaskOrg"),fMe=o(function(){let e=o(function(r){return Uw[r].processed},"compileTask"),t=!0;for(let[r,n]of Uw.entries())e(r),t=t&&n.processed;return t},"compileTasks"),URt=o(function(){return WRt()},"getActors"),FY={getConfig:o(()=>Ae().journey,"getConfig"),clear:FRt,setDiagramTitle:Or,getDiagramTitle:Lr,setAccTitle:kr,getAccTitle:Ar,setAccDescription:Rr,getAccDescription:_r,addSection:zRt,getSections:GRt,getTasks:VRt,addTask:qRt,addTaskOrg:HRt,getActors:URt}});var YRt,mMe,gMe=F(()=>{"use strict";X1();YRt=o(e=>`.label { + font-family: ${e.fontFamily}; + color: ${e.textColor}; + } + .mouth { + stroke: #666; + } + + line { + stroke: ${e.textColor} + } + + .legend { + fill: ${e.textColor}; + font-family: ${e.fontFamily}; + } + + .label text { + fill: #333; + } + .label { + color: ${e.textColor} + } + + .face { + ${e.faceColor?`fill: ${e.faceColor}`:"fill: #FFF8DC"}; + stroke: #999; + } + + .node rect, + .node circle, + .node ellipse, + .node polygon, + .node path { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; + stroke-width: 1px; + } + + .node .label { + text-align: center; + } + .node.clickable { + cursor: pointer; + } + + .arrowheadPath { + fill: ${e.arrowheadColor}; + } + + .edgePath .path { + stroke: ${e.lineColor}; + stroke-width: 1.5px; + } + + .flowchart-link { + stroke: ${e.lineColor}; + fill: none; + } + + .edgeLabel { + background-color: ${e.edgeLabelBackground}; + rect { + opacity: 0.5; + } + text-align: center; + } + + .cluster rect { + } + + .cluster text { + fill: ${e.titleColor}; + } + + div.mermaidTooltip { + position: absolute; + text-align: center; + max-width: 200px; + padding: 2px; + font-family: ${e.fontFamily}; + font-size: 12px; + background: ${e.tertiaryColor}; + border: 1px solid ${e.border2}; + border-radius: 2px; + pointer-events: none; + z-index: 100; + } + + .task-type-0, .section-type-0 { + ${e.fillType0?`fill: ${e.fillType0}`:""}; + } + .task-type-1, .section-type-1 { + ${e.fillType0?`fill: ${e.fillType1}`:""}; + } + .task-type-2, .section-type-2 { + ${e.fillType0?`fill: ${e.fillType2}`:""}; + } + .task-type-3, .section-type-3 { + ${e.fillType0?`fill: ${e.fillType3}`:""}; + } + .task-type-4, .section-type-4 { + ${e.fillType0?`fill: ${e.fillType4}`:""}; + } + .task-type-5, .section-type-5 { + ${e.fillType0?`fill: ${e.fillType5}`:""}; + } + .task-type-6, .section-type-6 { + ${e.fillType0?`fill: ${e.fillType6}`:""}; + } + .task-type-7, .section-type-7 { + ${e.fillType0?`fill: ${e.fillType7}`:""}; + } + + .actor-0 { + ${e.actor0?`fill: ${e.actor0}`:""}; + } + .actor-1 { + ${e.actor1?`fill: ${e.actor1}`:""}; + } + .actor-2 { + ${e.actor2?`fill: ${e.actor2}`:""}; + } + .actor-3 { + ${e.actor3?`fill: ${e.actor3}`:""}; + } + .actor-4 { + ${e.actor4?`fill: ${e.actor4}`:""}; + } + .actor-5 { + ${e.actor5?`fill: ${e.actor5}`:""}; + } + ${Eu()} +`,"getStyles"),mMe=YRt});var GY,jRt,yMe,vMe,XRt,KRt,zY,ZRt,QRt,xMe,JRt,sx,bMe=F(()=>{"use strict";$r();Ud();GY=o(function(e,t){return fm(e,t)},"drawRect"),jRt=o(function(e,t){let n=e.append("circle").attr("cx",t.cx).attr("cy",t.cy).attr("class","face").attr("r",15).attr("stroke-width",2).attr("overflow","visible"),i=e.append("g");i.append("circle").attr("cx",t.cx-15/3).attr("cy",t.cy-15/3).attr("r",1.5).attr("stroke-width",2).attr("fill","#666").attr("stroke","#666"),i.append("circle").attr("cx",t.cx+15/3).attr("cy",t.cy-15/3).attr("r",1.5).attr("stroke-width",2).attr("fill","#666").attr("stroke","#666");function a(u){let h=ec().startAngle(Math.PI/2).endAngle(3*(Math.PI/2)).innerRadius(7.5).outerRadius(6.8181818181818175);u.append("path").attr("class","mouth").attr("d",h).attr("transform","translate("+t.cx+","+(t.cy+2)+")")}o(a,"smile");function s(u){let h=ec().startAngle(3*Math.PI/2).endAngle(5*(Math.PI/2)).innerRadius(7.5).outerRadius(6.8181818181818175);u.append("path").attr("class","mouth").attr("d",h).attr("transform","translate("+t.cx+","+(t.cy+7)+")")}o(s,"sad");function l(u){u.append("line").attr("class","mouth").attr("stroke",2).attr("x1",t.cx-5).attr("y1",t.cy+7).attr("x2",t.cx+5).attr("y2",t.cy+7).attr("class","mouth").attr("stroke-width","1px").attr("stroke","#666")}return o(l,"ambivalent"),t.score>3?a(i):t.score<3?s(i):l(i),n},"drawFace"),yMe=o(function(e,t){let r=e.append("circle");return r.attr("cx",t.cx),r.attr("cy",t.cy),r.attr("class","actor-"+t.pos),r.attr("fill",t.fill),r.attr("stroke",t.stroke),r.attr("r",t.r),r.class!==void 0&&r.attr("class",r.class),t.title!==void 0&&r.append("title").text(t.title),r},"drawCircle"),vMe=o(function(e,t){return Sie(e,t)},"drawText"),XRt=o(function(e,t){function r(i,a,s,l,u){return i+","+a+" "+(i+s)+","+a+" "+(i+s)+","+(a+l-u)+" "+(i+s-u*1.2)+","+(a+l)+" "+i+","+(a+l)}o(r,"genPoints");let n=e.append("polygon");n.attr("points",r(t.x,t.y,50,20,7)),n.attr("class","labelBox"),t.y=t.y+t.labelMargin,t.x=t.x+.5*t.labelMargin,vMe(e,t)},"drawLabel"),KRt=o(function(e,t,r){let n=e.append("g"),i=Fa();i.x=t.x,i.y=t.y,i.fill=t.fill,i.width=r.width*t.taskCount+r.diagramMarginX*(t.taskCount-1),i.height=r.height,i.class="journey-section section-type-"+t.num,i.rx=3,i.ry=3,GY(n,i),xMe(r)(t.text,n,i.x,i.y,i.width,i.height,{class:"journey-section section-type-"+t.num},r,t.colour)},"drawSection"),zY=-1,ZRt=o(function(e,t,r,n){let i=t.x+r.width/2,a=e.append("g");zY++,a.append("line").attr("id",n+"-task"+zY).attr("x1",i).attr("y1",t.y).attr("x2",i).attr("y2",450).attr("class","task-line").attr("stroke-width","1px").attr("stroke-dasharray","4 2").attr("stroke","#666"),jRt(a,{cx:i,cy:300+(5-t.score)*30,score:t.score});let l=Fa();l.x=t.x,l.y=t.y,l.fill=t.fill,l.width=r.width,l.height=r.height,l.class="task task-type-"+t.num,l.rx=3,l.ry=3,GY(a,l);let u=t.x+14;t.people.forEach(h=>{let d=t.actors[h].color,f={cx:u,cy:t.y,r:7,fill:d,stroke:"#000",title:h,pos:t.actors[h].position};yMe(a,f),u+=10}),xMe(r)(t.task,a,l.x,l.y,l.width,l.height,{class:"task"},r,t.colour)},"drawTask"),QRt=o(function(e,t){$E(e,t)},"drawBackgroundRect"),xMe=(function(){function e(i,a,s,l,u,h,d,f){let p=a.append("text").attr("x",s+u/2).attr("y",l+h/2+5).style("font-color",f).style("text-anchor","middle").text(i);n(p,d)}o(e,"byText");function t(i,a,s,l,u,h,d,f,p){let{taskFontSize:m,taskFontFamily:g}=f,y=i.split(//gi);for(let v=0;v{let a=fd[i].color,s={cx:20,cy:n,r:7,fill:a,stroke:"#000",pos:fd[i].position};sx.drawCircle(e,s);let l=e.append("text").attr("visibility","hidden").text(i),u=l.node().getBoundingClientRect().width;l.remove();let h=[];if(u<=r)h=[i];else{let d=i.split(" "),f="";l=e.append("text").attr("visibility","hidden"),d.forEach(p=>{let m=f?`${f} ${p}`:p;if(l.text(m),l.node().getBoundingClientRect().width>r){if(f&&h.push(f),f=p,l.text(p),l.node().getBoundingClientRect().width>r){let y="";for(let v of p)y+=v,l.text(y+"-"),l.node().getBoundingClientRect().width>r&&(h.push(y.slice(0,-1)+"-"),y=v);f=y}}else f=m}),f&&h.push(f),l.remove()}h.forEach((d,f)=>{let p={x:40,y:n+7+f*20,fill:"#666",text:d,textMargin:t.boxTextMargin??5},g=sx.drawText(e,p).node().getBoundingClientRect().width;g>zL&&g>t.leftMargin-g&&(zL=g)}),n+=Math.max(20,h.length*20)})}var e_t,fd,zL,Lc,Dp,r_t,Pl,VY,TMe,n_t,WY,CMe=F(()=>{"use strict";$r();bMe();Xt();$n();e_t=o(function(e){Object.keys(e).forEach(function(r){Lc[r]=e[r]})},"setConf"),fd={},zL=0;o(t_t,"drawActorLegend");Lc=Ae().journey,Dp=0,r_t=o(function(e,t,r,n){let i=Ae(),a=i.journey.titleColor,s=i.journey.titleFontSize,l=i.journey.titleFontFamily,u=i.securityLevel,h;u==="sandbox"&&(h=et("#i"+t));let d=u==="sandbox"?et(h.nodes()[0].contentDocument.body):et("body");Pl.init();let f=d.select("#"+t);sx.initGraphics(f,t);let p=n.db.getTasks(),m=n.db.getDiagramTitle(),g=n.db.getActors();for(let k in fd)delete fd[k];let y=0;g.forEach(k=>{fd[k]={color:Lc.actorColours[y%Lc.actorColours.length],position:y},y++}),t_t(f),Dp=Lc.leftMargin+zL,Pl.insert(0,0,Dp,Object.keys(fd).length*50),n_t(f,p,0,t);let v=Pl.getBounds();m&&f.append("text").text(m).attr("x",Dp).attr("font-size",s).attr("font-weight","bold").attr("y",25).attr("fill",a).attr("font-family",l);let x=v.stopy-v.starty+2*Lc.diagramMarginY,b=Dp+v.stopx+2*Lc.diagramMarginX;Wr(f,x,b,Lc.useMaxWidth),f.append("line").attr("x1",Dp).attr("y1",Lc.height*4).attr("x2",b-Dp-4).attr("y2",Lc.height*4).attr("stroke-width",4).attr("stroke","black").attr("marker-end","url(#"+t+"-arrowhead)");let T=m?70:0;f.attr("viewBox",`${v.startx} -25 ${b} ${x+T}`),f.attr("preserveAspectRatio","xMinYMin meet"),f.attr("height",x+T+25)},"draw"),Pl={data:{startx:void 0,stopx:void 0,starty:void 0,stopy:void 0},verticalPos:0,sequenceItems:[],init:o(function(){this.sequenceItems=[],this.data={startx:void 0,stopx:void 0,starty:void 0,stopy:void 0},this.verticalPos=0},"init"),updateVal:o(function(e,t,r,n){e[t]===void 0?e[t]=r:e[t]=n(r,e[t])},"updateVal"),updateBounds:o(function(e,t,r,n){let i=Ae().journey,a=this,s=0;function l(u){return o(function(d){s++;let f=a.sequenceItems.length-s+1;a.updateVal(d,"starty",t-f*i.boxMargin,Math.min),a.updateVal(d,"stopy",n+f*i.boxMargin,Math.max),a.updateVal(Pl.data,"startx",e-f*i.boxMargin,Math.min),a.updateVal(Pl.data,"stopx",r+f*i.boxMargin,Math.max),u!=="activation"&&(a.updateVal(d,"startx",e-f*i.boxMargin,Math.min),a.updateVal(d,"stopx",r+f*i.boxMargin,Math.max),a.updateVal(Pl.data,"starty",t-f*i.boxMargin,Math.min),a.updateVal(Pl.data,"stopy",n+f*i.boxMargin,Math.max))},"updateItemBounds")}o(l,"updateFn"),this.sequenceItems.forEach(l())},"updateBounds"),insert:o(function(e,t,r,n){let i=Math.min(e,r),a=Math.max(e,r),s=Math.min(t,n),l=Math.max(t,n);this.updateVal(Pl.data,"startx",i,Math.min),this.updateVal(Pl.data,"starty",s,Math.min),this.updateVal(Pl.data,"stopx",a,Math.max),this.updateVal(Pl.data,"stopy",l,Math.max),this.updateBounds(i,s,a,l)},"insert"),bumpVerticalPos:o(function(e){this.verticalPos=this.verticalPos+e,this.data.stopy=this.verticalPos},"bumpVerticalPos"),getVerticalPos:o(function(){return this.verticalPos},"getVerticalPos"),getBounds:o(function(){return this.data},"getBounds")},VY=Lc.sectionFills,TMe=Lc.sectionColours,n_t=o(function(e,t,r,n){let i=Ae().journey,a="",s=i.height*2+i.diagramMarginY,l=r+s,u=0,h="#CCC",d="black",f=0;for(let[p,m]of t.entries()){if(a!==m.section){h=VY[u%VY.length],f=u%VY.length,d=TMe[u%TMe.length];let y=0,v=m.section;for(let b=p;b(fd[v]&&(y[v]=fd[v]),y),{});m.x=p*i.taskMargin+p*i.width+Dp,m.y=l,m.width=i.diagramMarginX,m.height=i.diagramMarginY,m.colour=d,m.fill=h,m.num=f,m.actors=g,sx.drawTask(e,m,i,n),Pl.insert(m.x,m.y,m.x+m.width+i.taskMargin,450)}},"drawTasks"),WY={setConf:e_t,draw:r_t}});var wMe={};ir(wMe,{diagram:()=>i_t});var i_t,kMe=F(()=>{"use strict";dMe();pMe();gMe();CMe();i_t={parser:hMe,db:FY,renderer:WY,styles:mMe,init:o(e=>{WY.setConf(e.journey),FY.clear()},"init")}});var HY,DMe,IMe=F(()=>{"use strict";HY=(function(){var e=o(function(p,m,g,y){for(g=g||{},y=p.length;y--;g[p[y]]=m);return g},"o"),t=[6,11,13,14,15,17,19,20,23,24],r=[1,12],n=[1,13],i=[1,14],a=[1,15],s=[1,16],l=[1,19],u=[1,20],h={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,timeline_header:4,document:5,EOF:6,timeline:7,timeline_lr:8,timeline_td:9,line:10,SPACE:11,statement:12,NEWLINE:13,title:14,acc_title:15,acc_title_value:16,acc_descr:17,acc_descr_value:18,acc_descr_multiline_value:19,section:20,period_statement:21,event_statement:22,period:23,event:24,$accept:0,$end:1},terminals_:{2:"error",6:"EOF",7:"timeline",8:"timeline_lr",9:"timeline_td",11:"SPACE",13:"NEWLINE",14:"title",15:"acc_title",16:"acc_title_value",17:"acc_descr",18:"acc_descr_value",19:"acc_descr_multiline_value",20:"section",23:"period",24:"event"},productions_:[0,[3,3],[4,1],[4,1],[4,1],[5,0],[5,2],[10,2],[10,1],[10,1],[10,1],[12,1],[12,2],[12,2],[12,1],[12,1],[12,1],[12,1],[21,1],[22,1]],performAction:o(function(m,g,y,v,x,b,T){var k=b.length-1;switch(x){case 1:return b[k-1];case 3:v.setDirection("LR");break;case 4:v.setDirection("TD");break;case 5:this.$=[];break;case 6:b[k-1].push(b[k]),this.$=b[k-1];break;case 7:case 8:this.$=b[k];break;case 9:case 10:this.$=[];break;case 11:v.getCommonDb().setDiagramTitle(b[k].substr(6)),this.$=b[k].substr(6);break;case 12:this.$=b[k].trim(),v.getCommonDb().setAccTitle(this.$);break;case 13:case 14:this.$=b[k].trim(),v.getCommonDb().setAccDescription(this.$);break;case 15:v.addSection(b[k].substr(8)),this.$=b[k].substr(8);break;case 18:v.addTask(b[k],0,""),this.$=b[k];break;case 19:v.addEvent(b[k].substr(2)),this.$=b[k];break}},"anonymous"),table:[{3:1,4:2,7:[1,3],8:[1,4],9:[1,5]},{1:[3]},e(t,[2,5],{5:6}),e(t,[2,2]),e(t,[2,3]),e(t,[2,4]),{6:[1,7],10:8,11:[1,9],12:10,13:[1,11],14:r,15:n,17:i,19:a,20:s,21:17,22:18,23:l,24:u},e(t,[2,10],{1:[2,1]}),e(t,[2,6]),{12:21,14:r,15:n,17:i,19:a,20:s,21:17,22:18,23:l,24:u},e(t,[2,8]),e(t,[2,9]),e(t,[2,11]),{16:[1,22]},{18:[1,23]},e(t,[2,14]),e(t,[2,15]),e(t,[2,16]),e(t,[2,17]),e(t,[2,18]),e(t,[2,19]),e(t,[2,7]),e(t,[2,12]),e(t,[2,13])],defaultActions:{},parseError:o(function(m,g){if(g.recoverable)this.trace(m);else{var y=new Error(m);throw y.hash=g,y}},"parseError"),parse:o(function(m){var g=this,y=[0],v=[],x=[null],b=[],T=this.table,k="",C=0,w=0,S=0,R=2,L=1,N=b.slice.call(arguments,1),I=Object.create(this.lexer),_={yy:{}};for(var A in this.yy)Object.prototype.hasOwnProperty.call(this.yy,A)&&(_.yy[A]=this.yy[A]);I.setInput(m,_.yy),_.yy.lexer=I,_.yy.parser=this,typeof I.yylloc>"u"&&(I.yylloc={});var M=I.yylloc;b.push(M);var D=I.options&&I.options.ranges;typeof _.yy.parseError=="function"?this.parseError=_.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function P(le){y.length=y.length-2*le,x.length=x.length-le,b.length=b.length-le}o(P,"popStack");function B(){var le;return le=v.pop()||I.lex()||L,typeof le!="number"&&(le instanceof Array&&(v=le,le=v.pop()),le=g.symbols_[le]||le),le}o(B,"lex");for(var O,$,V,G,z,W,H={},j,Q,U,oe;;){if(V=y[y.length-1],this.defaultActions[V]?G=this.defaultActions[V]:((O===null||typeof O>"u")&&(O=B()),G=T[V]&&T[V][O]),typeof G>"u"||!G.length||!G[0]){var te="";oe=[];for(j in T[V])this.terminals_[j]&&j>R&&oe.push("'"+this.terminals_[j]+"'");I.showPosition?te="Parse error on line "+(C+1)+`: +`+I.showPosition()+` +Expecting `+oe.join(", ")+", got '"+(this.terminals_[O]||O)+"'":te="Parse error on line "+(C+1)+": Unexpected "+(O==L?"end of input":"'"+(this.terminals_[O]||O)+"'"),this.parseError(te,{text:I.match,token:this.terminals_[O]||O,line:I.yylineno,loc:M,expected:oe})}if(G[0]instanceof Array&&G.length>1)throw new Error("Parse Error: multiple actions possible at state: "+V+", token: "+O);switch(G[0]){case 1:y.push(O),x.push(I.yytext),b.push(I.yylloc),y.push(G[1]),O=null,$?(O=$,$=null):(w=I.yyleng,k=I.yytext,C=I.yylineno,M=I.yylloc,S>0&&S--);break;case 2:if(Q=this.productions_[G[1]][1],H.$=x[x.length-Q],H._$={first_line:b[b.length-(Q||1)].first_line,last_line:b[b.length-1].last_line,first_column:b[b.length-(Q||1)].first_column,last_column:b[b.length-1].last_column},D&&(H._$.range=[b[b.length-(Q||1)].range[0],b[b.length-1].range[1]]),W=this.performAction.apply(H,[k,w,C,_.yy,G[1],x,b].concat(N)),typeof W<"u")return W;Q&&(y=y.slice(0,-1*Q*2),x=x.slice(0,-1*Q),b=b.slice(0,-1*Q)),y.push(this.productions_[G[1]][0]),x.push(H.$),b.push(H._$),U=T[y[y.length-2]][y[y.length-1]],y.push(U);break;case 3:return!0}}return!0},"parse")},d=(function(){var p={EOF:1,parseError:o(function(g,y){if(this.yy.parser)this.yy.parser.parseError(g,y);else throw new Error(g)},"parseError"),setInput:o(function(m,g){return this.yy=g||this.yy||{},this._input=m,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var m=this._input[0];this.yytext+=m,this.yyleng++,this.offset++,this.match+=m,this.matched+=m;var g=m.match(/(?:\r\n?|\n).*/g);return g?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),m},"input"),unput:o(function(m){var g=m.length,y=m.split(/(?:\r\n?|\n)/g);this._input=m+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-g),this.offset-=g;var v=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),y.length-1&&(this.yylineno-=y.length-1);var x=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:y?(y.length===v.length?this.yylloc.first_column:0)+v[v.length-y.length].length-y[0].length:this.yylloc.first_column-g},this.options.ranges&&(this.yylloc.range=[x[0],x[0]+this.yyleng-g]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(m){this.unput(this.match.slice(m))},"less"),pastInput:o(function(){var m=this.matched.substr(0,this.matched.length-this.match.length);return(m.length>20?"...":"")+m.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var m=this.match;return m.length<20&&(m+=this._input.substr(0,20-m.length)),(m.substr(0,20)+(m.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var m=this.pastInput(),g=new Array(m.length+1).join("-");return m+this.upcomingInput()+` +`+g+"^"},"showPosition"),test_match:o(function(m,g){var y,v,x;if(this.options.backtrack_lexer&&(x={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(x.yylloc.range=this.yylloc.range.slice(0))),v=m[0].match(/(?:\r\n?|\n).*/g),v&&(this.yylineno+=v.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:v?v[v.length-1].length-v[v.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+m[0].length},this.yytext+=m[0],this.match+=m[0],this.matches=m,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(m[0].length),this.matched+=m[0],y=this.performAction.call(this,this.yy,this,g,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),y)return y;if(this._backtrack){for(var b in x)this[b]=x[b];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var m,g,y,v;this._more||(this.yytext="",this.match="");for(var x=this._currentRules(),b=0;bg[0].length)){if(g=y,v=b,this.options.backtrack_lexer){if(m=this.test_match(y,x[b]),m!==!1)return m;if(this._backtrack){g=!1;continue}else return!1}else if(!this.options.flex)break}return g?(m=this.test_match(g,x[v]),m!==!1?m:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var g=this.next();return g||this.lex()},"lex"),begin:o(function(g){this.conditionStack.push(g)},"begin"),popState:o(function(){var g=this.conditionStack.length-1;return g>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(g){return g=this.conditionStack.length-1-Math.abs(g||0),g>=0?this.conditionStack[g]:"INITIAL"},"topState"),pushState:o(function(g){this.begin(g)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:o(function(g,y,v,x){var b=x;switch(v){case 0:break;case 1:break;case 2:return 13;case 3:break;case 4:break;case 5:return 8;case 6:return 9;case 7:return 7;case 8:return 14;case 9:return this.begin("acc_title"),15;break;case 10:return this.popState(),"acc_title_value";break;case 11:return this.begin("acc_descr"),17;break;case 12:return this.popState(),"acc_descr_value";break;case 13:this.begin("acc_descr_multiline");break;case 14:this.popState();break;case 15:return"acc_descr_multiline_value";case 16:return 20;case 17:return 24;case 18:return 23;case 19:return 6;case 20:return"INVALID"}},"anonymous"),rules:[/^(?:%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[\n]+)/i,/^(?:\s+)/i,/^(?:#[^\n]*)/i,/^(?:timeline[ \t]+LR\b)/i,/^(?:timeline[ \t]+TD\b)/i,/^(?:timeline\b)/i,/^(?:title\s[^\n]+)/i,/^(?:accTitle\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*:\s*)/i,/^(?:(?!\n||)*[^\n]*)/i,/^(?:accDescr\s*\{\s*)/i,/^(?:[\}])/i,/^(?:[^\}]*)/i,/^(?:section\s[^:\n]+)/i,/^(?::\s(?:[^:\n]|:(?!\s))+)/i,/^(?:[^#:\n]+)/i,/^(?:$)/i,/^(?:.)/i],conditions:{acc_descr_multiline:{rules:[14,15],inclusive:!1},acc_descr:{rules:[12],inclusive:!1},acc_title:{rules:[10],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,7,8,9,11,13,16,17,18,19,20],inclusive:!0}}};return p})();h.lexer=d;function f(){this.yy={}}return o(f,"Parser"),f.prototype=h,h.Parser=f,new f})();HY.parser=HY;DMe=HY});var jY={};ir(jY,{addEvent:()=>WMe,addSection:()=>FMe,addTask:()=>VMe,addTaskOrg:()=>qMe,clear:()=>OMe,default:()=>f_t,getCommonDb:()=>PMe,getDirection:()=>$Me,getSections:()=>zMe,getTasks:()=>GMe,setDirection:()=>BMe});var ox,NMe,UY,YY,GL,lx,PMe,OMe,BMe,$Me,FMe,zMe,GMe,VMe,WMe,qMe,MMe,f_t,HMe=F(()=>{"use strict";Nn();ox="",NMe=0,UY="LR",YY=[],GL=[],lx=[],PMe=o(()=>cb,"getCommonDb"),OMe=o(function(){YY.length=0,GL.length=0,ox="",lx.length=0,UY="LR",yr()},"clear"),BMe=o(function(e){UY=e},"setDirection"),$Me=o(function(){return UY},"getDirection"),FMe=o(function(e){ox=e,YY.push(e)},"addSection"),zMe=o(function(){return YY},"getSections"),GMe=o(function(){let e=MMe(),t=100,r=0;for(;!e&&rr.id===NMe-1).events.push(e)},"addEvent"),qMe=o(function(e){let t={section:ox,type:ox,description:e,task:e,classes:[]};GL.push(t)},"addTaskOrg"),MMe=o(function(){let e=o(function(r){return lx[r].processed},"compileTask"),t=!0;for(let[r,n]of lx.entries())e(r),t=t&&n.processed;return t},"compileTasks"),f_t={clear:OMe,getCommonDb:PMe,getDirection:$Me,setDirection:BMe,addSection:FMe,getSections:zMe,getTasks:GMe,addTask:VMe,addTaskOrg:qMe,addEvent:WMe}});function XMe(e,t){e.each(function(){var r=et(this),n=r.text().split(/(\s+|
)/).reverse(),i,a=[],s=1.1,l=r.attr("y"),u=parseFloat(r.attr("dy")),h=r.text(null).append("tspan").attr("x",0).attr("y",l).attr("dy",u+"em");for(let d=0;dt||i==="
")&&(a.pop(),h.text(a.join(" ").trim()),i==="
"?a=[""]:a=[i],h=r.append("tspan").attr("x",0).attr("y",l).attr("dy",s+"em").text(i))})}var UMe,VL,p_t,m_t,YMe,g_t,y_t,XY,v_t,x_t,b_t,KY,jMe,T_t,C_t,w_t,k_t,fs,ZY=F(()=>{"use strict";$r();UMe=0,VL=o(function(e,t){let r=e.append("rect");return r.attr("x",t.x),r.attr("y",t.y),r.attr("fill",t.fill),r.attr("stroke",t.stroke),r.attr("width",t.width),r.attr("height",t.height),r.attr("rx",t.rx),r.attr("ry",t.ry),t.class!==void 0&&r.attr("class",t.class),r},"drawRect"),p_t=o(function(e,t){let n=e.append("circle").attr("cx",t.cx).attr("cy",t.cy).attr("class","face").attr("r",15).attr("stroke-width",2).attr("overflow","visible"),i=e.append("g");i.append("circle").attr("cx",t.cx-15/3).attr("cy",t.cy-15/3).attr("r",1.5).attr("stroke-width",2).attr("fill","#666").attr("stroke","#666"),i.append("circle").attr("cx",t.cx+15/3).attr("cy",t.cy-15/3).attr("r",1.5).attr("stroke-width",2).attr("fill","#666").attr("stroke","#666");function a(u){let h=ec().startAngle(Math.PI/2).endAngle(3*(Math.PI/2)).innerRadius(7.5).outerRadius(6.8181818181818175);u.append("path").attr("class","mouth").attr("d",h).attr("transform","translate("+t.cx+","+(t.cy+2)+")")}o(a,"smile");function s(u){let h=ec().startAngle(3*Math.PI/2).endAngle(5*(Math.PI/2)).innerRadius(7.5).outerRadius(6.8181818181818175);u.append("path").attr("class","mouth").attr("d",h).attr("transform","translate("+t.cx+","+(t.cy+7)+")")}o(s,"sad");function l(u){u.append("line").attr("class","mouth").attr("stroke",2).attr("x1",t.cx-5).attr("y1",t.cy+7).attr("x2",t.cx+5).attr("y2",t.cy+7).attr("class","mouth").attr("stroke-width","1px").attr("stroke","#666")}return o(l,"ambivalent"),t.score>3?a(i):t.score<3?s(i):l(i),n},"drawFace"),m_t=o(function(e,t){let r=e.append("circle");return r.attr("cx",t.cx),r.attr("cy",t.cy),r.attr("class","actor-"+t.pos),r.attr("fill",t.fill),r.attr("stroke",t.stroke),r.attr("r",t.r),r.class!==void 0&&r.attr("class",r.class),t.title!==void 0&&r.append("title").text(t.title),r},"drawCircle"),YMe=o(function(e,t){let r=t.text.replace(//gi," "),n=e.append("text");n.attr("x",t.x),n.attr("y",t.y),n.attr("class","legend"),n.style("text-anchor",t.anchor),t.class!==void 0&&n.attr("class",t.class);let i=n.append("tspan");return i.attr("x",t.x+t.textMargin*2),i.text(r),n},"drawText"),g_t=o(function(e,t){function r(i,a,s,l,u){return i+","+a+" "+(i+s)+","+a+" "+(i+s)+","+(a+l-u)+" "+(i+s-u*1.2)+","+(a+l)+" "+i+","+(a+l)}o(r,"genPoints");let n=e.append("polygon");n.attr("points",r(t.x,t.y,50,20,7)),n.attr("class","labelBox"),t.y=t.y+t.labelMargin,t.x=t.x+.5*t.labelMargin,YMe(e,t)},"drawLabel"),y_t=o(function(e,t,r){let n=e.append("g"),i=KY();i.x=t.x,i.y=t.y,i.fill=t.fill,i.width=r.width,i.height=r.height,i.class="journey-section section-type-"+t.num,i.rx=3,i.ry=3,VL(n,i),jMe(r)(t.text,n,i.x,i.y,i.width,i.height,{class:"journey-section section-type-"+t.num},r,t.colour)},"drawSection"),XY=-1,v_t=o(function(e,t,r,n){let i=t.x+r.width/2,a=e.append("g");XY++,a.append("line").attr("id",n+"-task"+XY).attr("x1",i).attr("y1",t.y).attr("x2",i).attr("y2",450).attr("class","task-line").attr("stroke-width","1px").attr("stroke-dasharray","4 2").attr("stroke","#666"),p_t(a,{cx:i,cy:300+(5-t.score)*30,score:t.score});let l=KY();l.x=t.x,l.y=t.y,l.fill=t.fill,l.width=r.width,l.height=r.height,l.class="task task-type-"+t.num,l.rx=3,l.ry=3,VL(a,l),jMe(r)(t.task,a,l.x,l.y,l.width,l.height,{class:"task"},r,t.colour)},"drawTask"),x_t=o(function(e,t){VL(e,{x:t.startx,y:t.starty,width:t.stopx-t.startx,height:t.stopy-t.starty,fill:t.fill,class:"rect"}).lower()},"drawBackgroundRect"),b_t=o(function(){return{x:0,y:0,fill:void 0,"text-anchor":"start",width:100,height:100,textMargin:0,rx:0,ry:0}},"getTextObj"),KY=o(function(){return{x:0,y:0,width:100,anchor:"start",height:100,rx:0,ry:0}},"getNoteRect"),jMe=(function(){function e(i,a,s,l,u,h,d,f){let p=a.append("text").attr("x",s+u/2).attr("y",l+h/2+5).style("font-color",f).style("text-anchor","middle").text(i);n(p,d)}o(e,"byText");function t(i,a,s,l,u,h,d,f,p){let{taskFontSize:m,taskFontFamily:g}=f,y=i.split(//gi);for(let v=0;v0?`M0 ${t.height-l} v${-t.height+2*l} q0,-${s},${s},-${s} h${t.width-2*l} q${s},0,${s},${s} v${t.height-l} H0 Z`:`M0 ${t.height-l} v${-(t.height-l)} h${t.width} v${t.height} H0 Z`;e.append("path").attr("id",n+"-node-"+UMe++).attr("class","node-bkg node-"+t.type).attr("d",u),a?.includes("redux")||e.append("line").attr("class","node-line-"+r).attr("x1",0).attr("y1",t.height).attr("x2",t.width).attr("y2",t.height)},"defaultBkg"),fs={drawRect:VL,drawCircle:m_t,drawSection:y_t,drawText:YMe,drawLabel:g_t,drawTask:v_t,drawBackgroundRect:x_t,getTextObj:b_t,getNoteRect:KY,initGraphics:T_t,drawNode:C_t,getVirtualNodeHeight:w_t}});var S_t,KMe,E_t,ZMe,QMe=F(()=>{"use strict";$r();ZY();vt();Xt();$n();S_t=o(function(e,t,r,n){let i=Ae(),{look:a,theme:s,themeVariables:l}=i,{useGradient:u,gradientStart:h,gradientStop:d}=l,f=i.timeline?.leftMargin??50;Z.debug("timeline",n.db);let p=i.securityLevel,m;p==="sandbox"&&(m=et("#i"+t));let y=(p==="sandbox"?et(m.nodes()[0].contentDocument.body):et("body")).select("#"+t);y.append("g");let v=n.db.getTasks(),x=n.db.getCommonDb().getDiagramTitle();Z.debug("task",v),fs.initGraphics(y,t);let b=n.db.getSections();Z.debug("sections",b);let T=0,k=0,C=0,w=0,S=50+f,R=50;w=50;let L=0,N=!0;b.forEach(function(D){let P={number:L,descr:D,section:L,width:150,padding:20,maxHeight:T},B=fs.getVirtualNodeHeight(y,P,i);Z.debug("sectionHeight before draw",B),T=Math.max(T,B+20)});let I=0,_=0;Z.debug("tasks.length",v.length);for(let[D,P]of v.entries()){let B={number:D,descr:P,section:P.section,width:150,padding:20,maxHeight:k},O=fs.getVirtualNodeHeight(y,B,i);Z.debug("taskHeight before draw",O),k=Math.max(k,O+20),I=Math.max(I,P.events.length);let $=0;for(let V of P.events){let G={descr:V,section:P.section,number:P.section,width:150,padding:20,maxHeight:50};$+=fs.getVirtualNodeHeight(y,G,i)}P.events.length>0&&($+=(P.events.length-1)*10),_=Math.max(_,$)}Z.debug("maxSectionHeight before draw",T),Z.debug("maxTaskHeight before draw",k),b&&b.length>0?b.forEach(D=>{let P=v.filter(V=>V.section===D),B={number:L,descr:D,section:L,width:200*Math.max(P.length,1)-50,padding:20,maxHeight:T};Z.debug("sectionNode",B);let O=y.append("g"),$=fs.drawNode(O,B,L,i,t);Z.debug("sectionNode output",$),O.attr("transform",`translate(${S}, ${w})`),R+=T+50,P.length>0&&KMe(y,P,L,S,R,k,i,I,_,T,!1,t),S+=200*Math.max(P.length,1),R=w,L++}):(N=!1,KMe(y,v,L,S,R,k,i,I,_,T,!0,t));let A=y.node().getBBox();if(Z.debug("bounds",A),x&&y.append("text").text(x).attr("x",a==="neo"?A.x*2+f:A.width/2-f).attr("font-size","4ex").attr("font-weight","bold").attr("y",20),C=N?T+k+150:k+100,y.append("g").attr("class","lineWrapper").append("line").attr("x1",f).attr("y1",C).attr("x2",A.width+3*f).attr("y2",C).attr("stroke-width",4).attr("stroke","black").attr("marker-end",`url(#${t}-arrowhead)`),a==="neo"&&u&&s!=="neutral"){let D=y.select("defs"),B=(D.empty()?y.append("defs"):D).append("linearGradient").attr("id",y.attr("id")+"-gradient").attr("gradientUnits","objectBoundingBox").attr("x1","0%").attr("y1","0%").attr("x2","100%").attr("y2","0%");B.append("stop").attr("offset","0%").attr("stop-color",h).attr("stop-opacity",1),B.append("stop").attr("offset","100%").attr("stop-color",d).attr("stop-opacity",1)}ul(void 0,y,i.timeline?.padding??50,i.timeline?.useMaxWidth??!1)},"draw"),KMe=o(function(e,t,r,n,i,a,s,l,u,h,d,f){for(let p of t){let m={descr:p.task,section:r,number:r,width:150,padding:20,maxHeight:a};Z.debug("taskNode",m);let g=e.append("g").attr("class","taskWrapper"),v=fs.drawNode(g,m,r,s,f).height;if(Z.debug("taskHeight after draw",v),g.attr("transform",`translate(${n}, ${i})`),a=Math.max(a,v),p.events){let x=e.append("g").attr("class","lineWrapper"),b=a;i+=100,b=b+E_t(e,p.events,r,n,i,s,f),i-=100,x.append("line").attr("x1",n+190/2).attr("y1",i+a).attr("x2",n+190/2).attr("y2",i+a+100+u+100).attr("stroke-width",2).attr("stroke","black").attr("marker-end",`url(#${f}-arrowhead)`).attr("stroke-dasharray","5,5")}n=n+200,d&&!s.timeline?.disableMulticolor&&r++}i=i-10},"drawTasks"),E_t=o(function(e,t,r,n,i,a,s){let l=0,u=i;i=i+100;for(let h of t){let d={descr:h,section:r,number:r,width:150,padding:20,maxHeight:50};Z.debug("eventNode",d);let f=e.append("g").attr("class","eventWrapper"),m=fs.drawNode(f,d,r,a,s,!0).height;l=l+m,f.attr("transform",`translate(${n}, ${i})`),i=i+10+m}return i=u,l},"drawEvents"),ZMe={setConf:o(()=>{},"setConf"),draw:S_t}});var WL,pd,A_t,QY,R_t,rNe,__t,JMe,nNe,eNe,iNe,L_t,tNe,D_t,aNe,sNe=F(()=>{"use strict";ZY();vt();Xt();$n();Ka();Qt();WL=200,pd=5,A_t=WL+pd*2,QY=WL+100,R_t=QY+pd*2,rNe=10,__t=0,JMe=20,nNe=20,eNe=30,iNe=50,L_t=o(function(e,t,r,n){let i=Ae(),a=i.timeline?.leftMargin??50;Z.debug("timeline",n.db);let s=xn(t);s.append("g");let l=n.db.getTasks(),u=n.db.getCommonDb().getDiagramTitle();Z.debug("task",l),fs.initGraphics(s);let h=n.db.getSections();Z.debug("sections",h);let d=0,f=0,p=50+a,m=50,g=m,y=p,v=A_t+nNe,x=R_t+iNe,b=y+v,T=0,k=h&&h.length>0,C=k?b:p+v,w=Math.max(50,v+x-pd*2);h.forEach(function(D){let P={number:T,descr:D,section:T,width:w,padding:pd,maxHeight:d},B=fs.getVirtualNodeHeight(s,P,i);Z.debug("sectionHeight before draw",B),d=Math.max(d,B)});let S=0;Z.debug("tasks.length",l.length);for(let[D,P]of l.entries()){let B={number:D,descr:P,section:P.section,width:WL,padding:pd,maxHeight:f},O=fs.getVirtualNodeHeight(s,B,i);Z.debug("taskHeight before draw",O),f=Math.max(f,O);let $=0;for(let V of P.events){let G={descr:V,section:P.section,number:P.section,width:QY,padding:pd,maxHeight:50};$+=fs.getVirtualNodeHeight(s,G,i)}P.events.length>0&&($+=(P.events.length-1)*rNe),S=Math.max(S,$)+__t}Z.debug("maxSectionHeight before draw",d),Z.debug("maxTaskHeight before draw",f);let L=Math.max(f,S)+eNe;k?h.forEach(D=>{let P=l.filter(H=>H.section===D),B={number:T,descr:D,section:T,width:w,padding:pd,maxHeight:d};Z.debug("sectionNode",B);let O=s.append("g"),$=fs.drawNode(O,B,T,i);Z.debug("sectionNode output",$);let V=C-v;O.attr("transform",`translate(${V}, ${m})`);let G=m+$.height+JMe;P.length>0&&tNe(s,P,T,C,G,f,i,L,!1);let z=P.length,W=$.height+JMe+L*Math.max(z,1)-(z>0?eNe*2:0);m+=W,T++}):tNe(s,l,T,C,m,f,i,L,!0);let N=s.node()?.getBBox();if(!N)throw new Error("bbox not found");if(Z.debug("bounds",N),u){if(s.append("text").text(u).attr("x",N.width/2-a).attr("font-size","4ex").attr("font-weight","bold").attr("y",20),N=s.node()?.getBBox(),!N)throw new Error("bbox not found");Z.debug("bounds after title",N)}let[I]=As(i.fontSize),_=(I??16)*2,A=(I??16)*.5+20,M=s.append("g").attr("class","lineWrapper");M.append("line").attr("x1",C).attr("y1",g-_).attr("x2",C).attr("y2",N.y+N.height+A).attr("stroke-width",4).attr("stroke","black").attr("marker-end","url(#arrowhead)"),M.lower(),ul(void 0,s,i.timeline?.padding??50,i.timeline?.useMaxWidth??!1)},"draw"),tNe=o(function(e,t,r,n,i,a,s,l,u){for(let h of t){let d={descr:h.task,section:r,number:r,width:WL,padding:pd,maxHeight:a};Z.debug("taskNode",d);let f=e.append("g").attr("class","taskWrapper"),p=fs.drawNode(f,d,r,s),m=p.height;Z.debug("taskHeight after draw",m);let g=n-nNe-p.width;if(f.attr("transform",`translate(${g}, ${i})`),a=Math.max(a,m),h.events&&h.events.length>0){let y=i,v=n+iNe;D_t(e,h.events,r,n,v,y,s)}i=i+l,u&&!s.timeline?.disableMulticolor&&r++}},"drawTasks"),D_t=o(function(e,t,r,n,i,a,s){let l=a;for(let u of t){let h={descr:u,section:r,number:r,width:QY,padding:pd,maxHeight:0};Z.debug("eventNode",h);let d=e.append("g").attr("class","eventWrapper"),p=fs.drawNode(d,h,r,s).height;d.attr("transform",`translate(${i}, ${l})`);let m=e.append("g").attr("class","lineWrapper"),g=l+p/2;m.append("line").attr("x1",n).attr("y1",g).attr("x2",i).attr("y2",g).attr("stroke-width",2).attr("stroke","black").attr("marker-end","url(#arrowhead)").attr("stroke-dasharray","5,5"),l=l+p+rNe}return l-a},"drawEvents"),aNe={setConf:o(()=>{},"setConf"),draw:L_t}});var I_t,M_t,N_t,oNe,lNe=F(()=>{"use strict";zi();ur();I_t=o(e=>{let{theme:t}=_t(),r=t?.includes("dark"),n=t?.includes("color"),i=e.svgId?.replace(/^#/,"")??"",a=i?`url(#${i}-drop-shadow)`:e.dropShadow??"none",s="";for(let l=0;l{let t="";for(let r=0;r{let{theme:t}=_t(),r=t?.includes("redux"),n=t==="neutral",i=e.svgId?.replace(/^#/,"")??"",a="";if(e.useGradient&&i&&e.THEME_COLOR_LIMIT&&!n)for(let s=0;sO_t});var P_t,O_t,uNe=F(()=>{"use strict";IMe();HMe();QMe();sNe();lNe();P_t={setConf:o(()=>{},"setConf"),draw:o((e,t,r,n)=>(n?.db?.getDirection?.()??"LR")==="TD"?aNe.draw(e,t,r,n):ZMe.draw(e,t,r,n),"draw")},O_t={db:jY,renderer:P_t,parser:DMe,styles:oNe}});var JY,fNe,pNe=F(()=>{"use strict";JY=(function(){var e=o(function(k,C,w,S){for(w=w||{},S=k.length;S--;w[k[S]]=C);return w},"o"),t=[1,4],r=[1,13],n=[1,12],i=[1,15],a=[1,16],s=[1,20],l=[1,19],u=[6,7,8],h=[1,26],d=[1,24],f=[1,25],p=[6,7,11],m=[1,6,13,15,16,19,22],g=[1,33],y=[1,34],v=[1,6,7,11,13,15,16,19,22],x={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mindMap:4,spaceLines:5,SPACELINE:6,NL:7,MINDMAP:8,document:9,stop:10,EOF:11,statement:12,SPACELIST:13,node:14,ICON:15,CLASS:16,nodeWithId:17,nodeWithoutId:18,NODE_DSTART:19,NODE_DESCR:20,NODE_DEND:21,NODE_ID:22,$accept:0,$end:1},terminals_:{2:"error",6:"SPACELINE",7:"NL",8:"MINDMAP",11:"EOF",13:"SPACELIST",15:"ICON",16:"CLASS",19:"NODE_DSTART",20:"NODE_DESCR",21:"NODE_DEND",22:"NODE_ID"},productions_:[0,[3,1],[3,2],[5,1],[5,2],[5,2],[4,2],[4,3],[10,1],[10,1],[10,1],[10,2],[10,2],[9,3],[9,2],[12,2],[12,2],[12,2],[12,1],[12,1],[12,1],[12,1],[12,1],[14,1],[14,1],[18,3],[17,1],[17,4]],performAction:o(function(C,w,S,R,L,N,I){var _=N.length-1;switch(L){case 6:case 7:return R;case 8:R.getLogger().trace("Stop NL ");break;case 9:R.getLogger().trace("Stop EOF ");break;case 11:R.getLogger().trace("Stop NL2 ");break;case 12:R.getLogger().trace("Stop EOF2 ");break;case 15:R.getLogger().info("Node: ",N[_].id),R.addNode(N[_-1].length,N[_].id,N[_].descr,N[_].type);break;case 16:R.getLogger().trace("Icon: ",N[_]),R.decorateNode({icon:N[_]});break;case 17:case 21:R.decorateNode({class:N[_]});break;case 18:R.getLogger().trace("SPACELIST");break;case 19:R.getLogger().trace("Node: ",N[_].id),R.addNode(0,N[_].id,N[_].descr,N[_].type);break;case 20:R.decorateNode({icon:N[_]});break;case 25:R.getLogger().trace("node found ..",N[_-2]),this.$={id:N[_-1],descr:N[_-1],type:R.getType(N[_-2],N[_])};break;case 26:this.$={id:N[_],descr:N[_],type:R.nodeType.DEFAULT};break;case 27:R.getLogger().trace("node found ..",N[_-3]),this.$={id:N[_-3],descr:N[_-1],type:R.getType(N[_-2],N[_])};break}},"anonymous"),table:[{3:1,4:2,5:3,6:[1,5],8:t},{1:[3]},{1:[2,1]},{4:6,6:[1,7],7:[1,8],8:t},{6:r,7:[1,10],9:9,12:11,13:n,14:14,15:i,16:a,17:17,18:18,19:s,22:l},e(u,[2,3]),{1:[2,2]},e(u,[2,4]),e(u,[2,5]),{1:[2,6],6:r,12:21,13:n,14:14,15:i,16:a,17:17,18:18,19:s,22:l},{6:r,9:22,12:11,13:n,14:14,15:i,16:a,17:17,18:18,19:s,22:l},{6:h,7:d,10:23,11:f},e(p,[2,22],{17:17,18:18,14:27,15:[1,28],16:[1,29],19:s,22:l}),e(p,[2,18]),e(p,[2,19]),e(p,[2,20]),e(p,[2,21]),e(p,[2,23]),e(p,[2,24]),e(p,[2,26],{19:[1,30]}),{20:[1,31]},{6:h,7:d,10:32,11:f},{1:[2,7],6:r,12:21,13:n,14:14,15:i,16:a,17:17,18:18,19:s,22:l},e(m,[2,14],{7:g,11:y}),e(v,[2,8]),e(v,[2,9]),e(v,[2,10]),e(p,[2,15]),e(p,[2,16]),e(p,[2,17]),{20:[1,35]},{21:[1,36]},e(m,[2,13],{7:g,11:y}),e(v,[2,11]),e(v,[2,12]),{21:[1,37]},e(p,[2,25]),e(p,[2,27])],defaultActions:{2:[2,1],6:[2,2]},parseError:o(function(C,w){if(w.recoverable)this.trace(C);else{var S=new Error(C);throw S.hash=w,S}},"parseError"),parse:o(function(C){var w=this,S=[0],R=[],L=[null],N=[],I=this.table,_="",A=0,M=0,D=0,P=2,B=1,O=N.slice.call(arguments,1),$=Object.create(this.lexer),V={yy:{}};for(var G in this.yy)Object.prototype.hasOwnProperty.call(this.yy,G)&&(V.yy[G]=this.yy[G]);$.setInput(C,V.yy),V.yy.lexer=$,V.yy.parser=this,typeof $.yylloc>"u"&&($.yylloc={});var z=$.yylloc;N.push(z);var W=$.options&&$.options.ranges;typeof V.yy.parseError=="function"?this.parseError=V.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function H(ue){S.length=S.length-2*ue,L.length=L.length-ue,N.length=N.length-ue}o(H,"popStack");function j(){var ue;return ue=R.pop()||$.lex()||B,typeof ue!="number"&&(ue instanceof Array&&(R=ue,ue=R.pop()),ue=w.symbols_[ue]||ue),ue}o(j,"lex");for(var Q,U,oe,te,le,ie,ae={},Re,be,Pe,Ge;;){if(oe=S[S.length-1],this.defaultActions[oe]?te=this.defaultActions[oe]:((Q===null||typeof Q>"u")&&(Q=j()),te=I[oe]&&I[oe][Q]),typeof te>"u"||!te.length||!te[0]){var Oe="";Ge=[];for(Re in I[oe])this.terminals_[Re]&&Re>P&&Ge.push("'"+this.terminals_[Re]+"'");$.showPosition?Oe="Parse error on line "+(A+1)+`: +`+$.showPosition()+` +Expecting `+Ge.join(", ")+", got '"+(this.terminals_[Q]||Q)+"'":Oe="Parse error on line "+(A+1)+": Unexpected "+(Q==B?"end of input":"'"+(this.terminals_[Q]||Q)+"'"),this.parseError(Oe,{text:$.match,token:this.terminals_[Q]||Q,line:$.yylineno,loc:z,expected:Ge})}if(te[0]instanceof Array&&te.length>1)throw new Error("Parse Error: multiple actions possible at state: "+oe+", token: "+Q);switch(te[0]){case 1:S.push(Q),L.push($.yytext),N.push($.yylloc),S.push(te[1]),Q=null,U?(Q=U,U=null):(M=$.yyleng,_=$.yytext,A=$.yylineno,z=$.yylloc,D>0&&D--);break;case 2:if(be=this.productions_[te[1]][1],ae.$=L[L.length-be],ae._$={first_line:N[N.length-(be||1)].first_line,last_line:N[N.length-1].last_line,first_column:N[N.length-(be||1)].first_column,last_column:N[N.length-1].last_column},W&&(ae._$.range=[N[N.length-(be||1)].range[0],N[N.length-1].range[1]]),ie=this.performAction.apply(ae,[_,M,A,V.yy,te[1],L,N].concat(O)),typeof ie<"u")return ie;be&&(S=S.slice(0,-1*be*2),L=L.slice(0,-1*be),N=N.slice(0,-1*be)),S.push(this.productions_[te[1]][0]),L.push(ae.$),N.push(ae._$),Pe=I[S[S.length-2]][S[S.length-1]],S.push(Pe);break;case 3:return!0}}return!0},"parse")},b=(function(){var k={EOF:1,parseError:o(function(w,S){if(this.yy.parser)this.yy.parser.parseError(w,S);else throw new Error(w)},"parseError"),setInput:o(function(C,w){return this.yy=w||this.yy||{},this._input=C,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var C=this._input[0];this.yytext+=C,this.yyleng++,this.offset++,this.match+=C,this.matched+=C;var w=C.match(/(?:\r\n?|\n).*/g);return w?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),C},"input"),unput:o(function(C){var w=C.length,S=C.split(/(?:\r\n?|\n)/g);this._input=C+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-w),this.offset-=w;var R=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),S.length-1&&(this.yylineno-=S.length-1);var L=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:S?(S.length===R.length?this.yylloc.first_column:0)+R[R.length-S.length].length-S[0].length:this.yylloc.first_column-w},this.options.ranges&&(this.yylloc.range=[L[0],L[0]+this.yyleng-w]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(C){this.unput(this.match.slice(C))},"less"),pastInput:o(function(){var C=this.matched.substr(0,this.matched.length-this.match.length);return(C.length>20?"...":"")+C.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var C=this.match;return C.length<20&&(C+=this._input.substr(0,20-C.length)),(C.substr(0,20)+(C.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var C=this.pastInput(),w=new Array(C.length+1).join("-");return C+this.upcomingInput()+` +`+w+"^"},"showPosition"),test_match:o(function(C,w){var S,R,L;if(this.options.backtrack_lexer&&(L={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(L.yylloc.range=this.yylloc.range.slice(0))),R=C[0].match(/(?:\r\n?|\n).*/g),R&&(this.yylineno+=R.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:R?R[R.length-1].length-R[R.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+C[0].length},this.yytext+=C[0],this.match+=C[0],this.matches=C,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(C[0].length),this.matched+=C[0],S=this.performAction.call(this,this.yy,this,w,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),S)return S;if(this._backtrack){for(var N in L)this[N]=L[N];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var C,w,S,R;this._more||(this.yytext="",this.match="");for(var L=this._currentRules(),N=0;Nw[0].length)){if(w=S,R=N,this.options.backtrack_lexer){if(C=this.test_match(S,L[N]),C!==!1)return C;if(this._backtrack){w=!1;continue}else return!1}else if(!this.options.flex)break}return w?(C=this.test_match(w,L[R]),C!==!1?C:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var w=this.next();return w||this.lex()},"lex"),begin:o(function(w){this.conditionStack.push(w)},"begin"),popState:o(function(){var w=this.conditionStack.length-1;return w>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(w){return w=this.conditionStack.length-1-Math.abs(w||0),w>=0?this.conditionStack[w]:"INITIAL"},"topState"),pushState:o(function(w){this.begin(w)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:o(function(w,S,R,L){var N=L;switch(R){case 0:return w.getLogger().trace("Found comment",S.yytext),6;break;case 1:return 8;case 2:this.begin("CLASS");break;case 3:return this.popState(),16;break;case 4:this.popState();break;case 5:w.getLogger().trace("Begin icon"),this.begin("ICON");break;case 6:return w.getLogger().trace("SPACELINE"),6;break;case 7:return 7;case 8:return 15;case 9:w.getLogger().trace("end icon"),this.popState();break;case 10:return w.getLogger().trace("Exploding node"),this.begin("NODE"),19;break;case 11:return w.getLogger().trace("Cloud"),this.begin("NODE"),19;break;case 12:return w.getLogger().trace("Explosion Bang"),this.begin("NODE"),19;break;case 13:return w.getLogger().trace("Cloud Bang"),this.begin("NODE"),19;break;case 14:return this.begin("NODE"),19;break;case 15:return this.begin("NODE"),19;break;case 16:return this.begin("NODE"),19;break;case 17:return this.begin("NODE"),19;break;case 18:return 13;case 19:return 22;case 20:return 11;case 21:this.begin("NSTR2");break;case 22:return"NODE_DESCR";case 23:this.popState();break;case 24:w.getLogger().trace("Starting NSTR"),this.begin("NSTR");break;case 25:return w.getLogger().trace("description:",S.yytext),"NODE_DESCR";break;case 26:this.popState();break;case 27:return this.popState(),w.getLogger().trace("node end ))"),"NODE_DEND";break;case 28:return this.popState(),w.getLogger().trace("node end )"),"NODE_DEND";break;case 29:return this.popState(),w.getLogger().trace("node end ...",S.yytext),"NODE_DEND";break;case 30:return this.popState(),w.getLogger().trace("node end (("),"NODE_DEND";break;case 31:return this.popState(),w.getLogger().trace("node end (-"),"NODE_DEND";break;case 32:return this.popState(),w.getLogger().trace("node end (-"),"NODE_DEND";break;case 33:return this.popState(),w.getLogger().trace("node end (("),"NODE_DEND";break;case 34:return this.popState(),w.getLogger().trace("node end (("),"NODE_DEND";break;case 35:return w.getLogger().trace("Long description:",S.yytext),20;break;case 36:return w.getLogger().trace("Long description:",S.yytext),20;break}},"anonymous"),rules:[/^(?:\s*%%.*)/i,/^(?:mindmap\b)/i,/^(?::::)/i,/^(?:.+)/i,/^(?:\n)/i,/^(?:::icon\()/i,/^(?:[\s]+[\n])/i,/^(?:[\n]+)/i,/^(?:[^\)]+)/i,/^(?:\))/i,/^(?:-\))/i,/^(?:\(-)/i,/^(?:\)\))/i,/^(?:\))/i,/^(?:\(\()/i,/^(?:\{\{)/i,/^(?:\()/i,/^(?:\[)/i,/^(?:[\s]+)/i,/^(?:[^\(\[\n\)\{\}]+)/i,/^(?:$)/i,/^(?:["][`])/i,/^(?:[^`"]+)/i,/^(?:[`]["])/i,/^(?:["])/i,/^(?:[^"]+)/i,/^(?:["])/i,/^(?:[\)]\))/i,/^(?:[\)])/i,/^(?:[\]])/i,/^(?:\}\})/i,/^(?:\(-)/i,/^(?:-\))/i,/^(?:\(\()/i,/^(?:\()/i,/^(?:[^\)\]\(\}]+)/i,/^(?:.+(?!\(\())/i],conditions:{CLASS:{rules:[3,4],inclusive:!1},ICON:{rules:[8,9],inclusive:!1},NSTR2:{rules:[22,23],inclusive:!1},NSTR:{rules:[25,26],inclusive:!1},NODE:{rules:[21,24,27,28,29,30,31,32,33,34,35,36],inclusive:!1},INITIAL:{rules:[0,1,2,5,6,7,10,11,12,13,14,15,16,17,18,19,20],inclusive:!0}}};return k})();x.lexer=b;function T(){this.yy={}}return o(T,"Parser"),T.prototype=x,x.Parser=T,new T})();JY.parser=JY;fNe=JY});function mNe(e,t=0){return(Qa[e[t+0]]+Qa[e[t+1]]+Qa[e[t+2]]+Qa[e[t+3]]+"-"+Qa[e[t+4]]+Qa[e[t+5]]+"-"+Qa[e[t+6]]+Qa[e[t+7]]+"-"+Qa[e[t+8]]+Qa[e[t+9]]+"-"+Qa[e[t+10]]+Qa[e[t+11]]+Qa[e[t+12]]+Qa[e[t+13]]+Qa[e[t+14]]+Qa[e[t+15]]).toLowerCase()}var Qa,gNe=F(()=>{"use strict";Qa=[];for(let e=0;e<256;++e)Qa.push((e+256).toString(16).slice(1));o(mNe,"unsafeStringify")});function ej(){return crypto.getRandomValues(z_t)}var z_t,yNe=F(()=>{"use strict";z_t=new Uint8Array(16);o(ej,"rng")});function G_t(e,t,r){return!t&&!e&&crypto.randomUUID?crypto.randomUUID():V_t(e,t,r)}function V_t(e,t,r){e=e||{};let n=e.random??e.rng?.()??ej();if(n.length<16)throw new Error("Random bytes length must be >= 16");if(n[6]=n[6]&15|64,n[8]=n[8]&63|128,t){if(r=r||0,r<0||r+16>t.length)throw new RangeError(`UUID byte range ${r}:${r+15} is out of buffer bounds`);for(let i=0;i<16;++i)t[r+i]=n[i];return t}return mNe(n)}var tj,vNe=F(()=>{"use strict";yNe();gNe();o(G_t,"v4");o(V_t,"_v4");tj=G_t});var xNe=F(()=>{"use strict";vNe()});var bNe,TNe=F(()=>{"use strict";Ls();Qt();bNe=12});var md,qL,CNe=F(()=>{"use strict";Xt();xNe();Vr();vt();Wi();ur();TNe();md={DEFAULT:0,NO_BORDER:0,ROUNDED_RECT:1,RECT:2,CIRCLE:3,CLOUD:4,BANG:5,HEXAGON:6},qL=class{constructor(){this.nodes=[];this.count=0;this.elements={};this.getLogger=this.getLogger.bind(this),this.nodeType=md,this.clear(),this.getType=this.getType.bind(this),this.getElementById=this.getElementById.bind(this),this.getParent=this.getParent.bind(this),this.getMindmap=this.getMindmap.bind(this),this.addNode=this.addNode.bind(this),this.decorateNode=this.decorateNode.bind(this)}static{o(this,"MindmapDB")}clear(){this.nodes=[],this.count=0,this.elements={},this.baseLevel=void 0}getParent(t){for(let r=this.nodes.length-1;r>=0;r--)if(this.nodes[r].level0?this.nodes[0]:null}addNode(t,r,n,i){Z.info("addNode",t,r,n,i);let a=!1;this.nodes.length===0?(this.baseLevel=t,t=0,a=!0):this.baseLevel!==void 0&&(t=t-this.baseLevel,a=!1);let s=Ae(),l=s.mindmap?.padding??cr.mindmap.padding;switch(i){case this.nodeType.ROUNDED_RECT:case this.nodeType.RECT:case this.nodeType.HEXAGON:l*=2;break}let u={id:this.count++,nodeId:mr(r,s),level:t,descr:mr(n,s),type:i,children:[],width:s.mindmap?.maxNodeWidth??cr.mindmap.maxNodeWidth,padding:l,isRoot:a},h=this.getParent(t);if(h)h.children.push(u),this.nodes.push(u);else if(a)this.nodes.push(u);else throw new Error(`There can be only one root. No parent could be found for ("${u.descr}")`)}getType(t,r){switch(Z.debug("In get type",t,r),t){case"[":return this.nodeType.RECT;case"(":return r===")"?this.nodeType.ROUNDED_RECT:this.nodeType.CLOUD;case"((":return this.nodeType.CIRCLE;case")":return this.nodeType.CLOUD;case"))":return this.nodeType.BANG;case"{{":return this.nodeType.HEXAGON;default:return this.nodeType.DEFAULT}}setElementForId(t,r){this.elements[t]=r}getElementById(t){return this.elements[t]}decorateNode(t){if(!t)return;let r=Ae(),n=this.nodes[this.nodes.length-1];t.icon&&(n.icon=mr(t.icon,r)),t.class&&(n.class=mr(t.class,r))}type2Str(t){switch(t){case this.nodeType.DEFAULT:return"no-border";case this.nodeType.RECT:return"rect";case this.nodeType.ROUNDED_RECT:return"rounded-rect";case this.nodeType.CIRCLE:return"circle";case this.nodeType.CLOUD:return"cloud";case this.nodeType.BANG:return"bang";case this.nodeType.HEXAGON:return"hexgon";default:return"no-border"}}assignSections(t,r){if(t.level===0?t.section=void 0:t.section=r,t.children)for(let[n,i]of t.children.entries()){let a=t.level===0?n%(bNe-1):r;this.assignSections(i,a)}}flattenNodes(t,r){let n=Ae(),i=["mindmap-node"];t.isRoot===!0?i.push("section-root","section--1"):t.section!==void 0&&i.push(`section-${t.section}`),t.class&&i.push(t.class);let a=i.join(" "),s=o(u=>{let d=(n.theme?.toLowerCase()??"").includes("redux");switch(u){case md.CIRCLE:return"mindmapCircle";case md.RECT:return"rect";case md.ROUNDED_RECT:return"rounded";case md.CLOUD:return"cloud";case md.BANG:return"bang";case md.HEXAGON:return"hexagon";case md.DEFAULT:return d?"rounded":"defaultMindmapNode";case md.NO_BORDER:default:return"rect"}},"getShapeFromType"),l={id:t.id.toString(),domId:"node_"+t.id.toString(),label:t.descr,labelType:"markdown",isGroup:!1,shape:s(t.type),width:t.width,height:t.height??0,padding:t.padding,cssClasses:a,cssStyles:[],look:n.look,icon:t.icon,x:t.x,y:t.y,level:t.level,nodeId:t.nodeId,type:t.type,section:t.section};if(r.push(l),t.children)for(let u of t.children)this.flattenNodes(u,r)}generateEdges(t,r){if(!t.children)return;let n=Ae();for(let i of t.children){let a="edge";i.section!==void 0&&(a+=` section-edge-${i.section}`);let s=t.level+1;a+=` edge-depth-${s}`;let l={id:`edge_${t.id}_${i.id}`,start:t.id.toString(),end:i.id.toString(),type:"normal",curve:"basis",thickness:"normal",look:n.look,classes:a,depth:t.level,section:i.section};r.push(l),this.generateEdges(i,r)}}getData(){let t=this.getMindmap(),r=Ae(),i=Lk().layout!==void 0,a=r;if(i||(a.layout="cose-bilkent"),!t)return{nodes:[],edges:[],config:a};Z.debug("getData: mindmapRoot",t,r),this.assignSections(t);let s=[],l=[];this.flattenNodes(t,s),this.generateEdges(t,l),Z.debug(`getData: processed ${s.length} nodes and ${l.length} edges`);let u=new Map;for(let h of s)u.set(h.id,{shape:h.shape,width:h.width,height:h.height,padding:h.padding});return{nodes:s,edges:l,config:a,rootNode:t,markers:["point"],direction:"TB",nodeSpacing:50,rankSpacing:50,shapes:Object.fromEntries(u),type:"mindmap",diagramId:"mindmap-"+tj()}}getLogger(){return Z}}});var W_t,wNe,kNe=F(()=>{"use strict";vt();Rm();Jf();ep();Wi();ur();W_t=o(async(e,t,r,n)=>{Z.debug(`Rendering mindmap diagram +`+e);let i=n.db,a=i.getData(),s=pl(t,a.config.securityLevel);if(a.type=n.type,a.layoutAlgorithm=Su(a.config.layout,{fallback:"cose-bilkent"}),a.diagramId=t,!i.getMindmap())return;a.nodes.forEach(p=>{p.shape==="rounded"?(p.radius=15,p.taper=15,p.stroke="none",p.width=0,p.padding=15):p.shape==="circle"?p.padding=10:p.shape==="rect"?(p.width=0,p.padding=10):p.shape==="hexagon"&&(p.width=0,p.height=0)}),await Al(a,s);let{themeVariables:u}=_t(),{useGradient:h,gradientStart:d,gradientStop:f}=u;if(h&&d&&f){let p=s.attr("id"),m=s.append("defs").append("linearGradient").attr("id",`${p}-gradient`).attr("gradientUnits","objectBoundingBox").attr("x1","0%").attr("y1","0%").attr("x2","100%").attr("y2","0%");m.append("stop").attr("offset","0%").attr("stop-color",d).attr("stop-opacity",1),m.append("stop").attr("offset","100%").attr("stop-color",f).attr("stop-opacity",1)}vo(s,a.config.mindmap?.padding??cr.mindmap.padding,"mindmapDiagram",a.config.mindmap?.useMaxWidth??cr.mindmap.useMaxWidth)},"draw"),wNe={draw:W_t}});var q_t,H_t,U_t,SNe,ENe=F(()=>{"use strict";zi();q_t=o(e=>{let{theme:t,look:r}=e,n="";for(let i=0;i{let n="";for(let i=0;i{let{theme:t}=e,r=e.svgId,n=e.dropShadow?e.dropShadow.replace("url(#drop-shadow)",`url(${r}-drop-shadow)`):"none";return` + .edge { + stroke-width: 3; + } + ${q_t(e)} + .section-root rect, .section-root path, .section-root circle, .section-root polygon { + fill: ${e.git0}; + } + .section-root text { + fill: ${e.gitBranchLabel0}; + } + .section-root span { + color: ${t?.includes("redux")?e.nodeBorder:e.gitBranchLabel0}; + } + .icon-container { + height:100%; + display: flex; + justify-content: center; + align-items: center; + } + .edge { + fill: none; + } + .mindmap-node-label { + dy: 1em; + alignment-baseline: middle; + text-anchor: middle; + dominant-baseline: middle; + text-align: center; + } + [data-look="neo"].mindmap-node { + filter: ${n}; + } + [data-look="neo"].mindmap-node.section-root rect, [data-look="neo"].mindmap-node.section-root path, [data-look="neo"].mindmap-node.section-root circle, [data-look="neo"].mindmap-node.section-root polygon { + fill: ${t?.includes("redux")?e.mainBkg:e.git0}; + } + [data-look="neo"].mindmap-node.section-root .text-inner-tspan { + fill: ${t?.includes("redux")?e.nodeBorder:e["cScaleLabel"+(t==="neutral"?1:0)]}; + } + ${e.useGradient&&r&&e.mainBkg?H_t(e.THEME_COLOR_LIMIT,r,e.mainBkg):""} +`},"getStyles"),SNe=U_t});var ANe={};ir(ANe,{diagram:()=>Y_t});var Y_t,RNe=F(()=>{"use strict";pNe();CNe();kNe();ENe();Y_t={get db(){return new qL},renderer:wNe,parser:fNe,styles:SNe}});var rj,DNe,INe=F(()=>{"use strict";rj=(function(){var e=o(function(S,R,L,N){for(L=L||{},N=S.length;N--;L[S[N]]=R);return L},"o"),t=[1,4],r=[1,13],n=[1,12],i=[1,15],a=[1,16],s=[1,20],l=[1,19],u=[6,7,8],h=[1,26],d=[1,24],f=[1,25],p=[6,7,11],m=[1,31],g=[6,7,11,24],y=[1,6,13,16,17,20,23],v=[1,35],x=[1,36],b=[1,6,7,11,13,16,17,20,23],T=[1,38],k={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,mindMap:4,spaceLines:5,SPACELINE:6,NL:7,KANBAN:8,document:9,stop:10,EOF:11,statement:12,SPACELIST:13,node:14,shapeData:15,ICON:16,CLASS:17,nodeWithId:18,nodeWithoutId:19,NODE_DSTART:20,NODE_DESCR:21,NODE_DEND:22,NODE_ID:23,SHAPE_DATA:24,$accept:0,$end:1},terminals_:{2:"error",6:"SPACELINE",7:"NL",8:"KANBAN",11:"EOF",13:"SPACELIST",16:"ICON",17:"CLASS",20:"NODE_DSTART",21:"NODE_DESCR",22:"NODE_DEND",23:"NODE_ID",24:"SHAPE_DATA"},productions_:[0,[3,1],[3,2],[5,1],[5,2],[5,2],[4,2],[4,3],[10,1],[10,1],[10,1],[10,2],[10,2],[9,3],[9,2],[12,3],[12,2],[12,2],[12,2],[12,1],[12,2],[12,1],[12,1],[12,1],[12,1],[14,1],[14,1],[19,3],[18,1],[18,4],[15,2],[15,1]],performAction:o(function(R,L,N,I,_,A,M){var D=A.length-1;switch(_){case 6:case 7:return I;case 8:I.getLogger().trace("Stop NL ");break;case 9:I.getLogger().trace("Stop EOF ");break;case 11:I.getLogger().trace("Stop NL2 ");break;case 12:I.getLogger().trace("Stop EOF2 ");break;case 15:I.getLogger().info("Node: ",A[D-1].id),I.addNode(A[D-2].length,A[D-1].id,A[D-1].descr,A[D-1].type,A[D]);break;case 16:I.getLogger().info("Node: ",A[D].id),I.addNode(A[D-1].length,A[D].id,A[D].descr,A[D].type);break;case 17:I.getLogger().trace("Icon: ",A[D]),I.decorateNode({icon:A[D]});break;case 18:case 23:I.decorateNode({class:A[D]});break;case 19:I.getLogger().trace("SPACELIST");break;case 20:I.getLogger().trace("Node: ",A[D-1].id),I.addNode(0,A[D-1].id,A[D-1].descr,A[D-1].type,A[D]);break;case 21:I.getLogger().trace("Node: ",A[D].id),I.addNode(0,A[D].id,A[D].descr,A[D].type);break;case 22:I.decorateNode({icon:A[D]});break;case 27:I.getLogger().trace("node found ..",A[D-2]),this.$={id:A[D-1],descr:A[D-1],type:I.getType(A[D-2],A[D])};break;case 28:this.$={id:A[D],descr:A[D],type:0};break;case 29:I.getLogger().trace("node found ..",A[D-3]),this.$={id:A[D-3],descr:A[D-1],type:I.getType(A[D-2],A[D])};break;case 30:this.$=A[D-1]+A[D];break;case 31:this.$=A[D];break}},"anonymous"),table:[{3:1,4:2,5:3,6:[1,5],8:t},{1:[3]},{1:[2,1]},{4:6,6:[1,7],7:[1,8],8:t},{6:r,7:[1,10],9:9,12:11,13:n,14:14,16:i,17:a,18:17,19:18,20:s,23:l},e(u,[2,3]),{1:[2,2]},e(u,[2,4]),e(u,[2,5]),{1:[2,6],6:r,12:21,13:n,14:14,16:i,17:a,18:17,19:18,20:s,23:l},{6:r,9:22,12:11,13:n,14:14,16:i,17:a,18:17,19:18,20:s,23:l},{6:h,7:d,10:23,11:f},e(p,[2,24],{18:17,19:18,14:27,16:[1,28],17:[1,29],20:s,23:l}),e(p,[2,19]),e(p,[2,21],{15:30,24:m}),e(p,[2,22]),e(p,[2,23]),e(g,[2,25]),e(g,[2,26]),e(g,[2,28],{20:[1,32]}),{21:[1,33]},{6:h,7:d,10:34,11:f},{1:[2,7],6:r,12:21,13:n,14:14,16:i,17:a,18:17,19:18,20:s,23:l},e(y,[2,14],{7:v,11:x}),e(b,[2,8]),e(b,[2,9]),e(b,[2,10]),e(p,[2,16],{15:37,24:m}),e(p,[2,17]),e(p,[2,18]),e(p,[2,20],{24:T}),e(g,[2,31]),{21:[1,39]},{22:[1,40]},e(y,[2,13],{7:v,11:x}),e(b,[2,11]),e(b,[2,12]),e(p,[2,15],{24:T}),e(g,[2,30]),{22:[1,41]},e(g,[2,27]),e(g,[2,29])],defaultActions:{2:[2,1],6:[2,2]},parseError:o(function(R,L){if(L.recoverable)this.trace(R);else{var N=new Error(R);throw N.hash=L,N}},"parseError"),parse:o(function(R){var L=this,N=[0],I=[],_=[null],A=[],M=this.table,D="",P=0,B=0,O=0,$=2,V=1,G=A.slice.call(arguments,1),z=Object.create(this.lexer),W={yy:{}};for(var H in this.yy)Object.prototype.hasOwnProperty.call(this.yy,H)&&(W.yy[H]=this.yy[H]);z.setInput(R,W.yy),W.yy.lexer=z,W.yy.parser=this,typeof z.yylloc>"u"&&(z.yylloc={});var j=z.yylloc;A.push(j);var Q=z.options&&z.options.ranges;typeof W.yy.parseError=="function"?this.parseError=W.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function U(ce){N.length=N.length-2*ce,_.length=_.length-ce,A.length=A.length-ce}o(U,"popStack");function oe(){var ce;return ce=I.pop()||z.lex()||V,typeof ce!="number"&&(ce instanceof Array&&(I=ce,ce=I.pop()),ce=L.symbols_[ce]||ce),ce}o(oe,"lex");for(var te,le,ie,ae,Re,be,Pe={},Ge,Oe,ue,ye;;){if(ie=N[N.length-1],this.defaultActions[ie]?ae=this.defaultActions[ie]:((te===null||typeof te>"u")&&(te=oe()),ae=M[ie]&&M[ie][te]),typeof ae>"u"||!ae.length||!ae[0]){var ke="";ye=[];for(Ge in M[ie])this.terminals_[Ge]&&Ge>$&&ye.push("'"+this.terminals_[Ge]+"'");z.showPosition?ke="Parse error on line "+(P+1)+`: +`+z.showPosition()+` +Expecting `+ye.join(", ")+", got '"+(this.terminals_[te]||te)+"'":ke="Parse error on line "+(P+1)+": Unexpected "+(te==V?"end of input":"'"+(this.terminals_[te]||te)+"'"),this.parseError(ke,{text:z.match,token:this.terminals_[te]||te,line:z.yylineno,loc:j,expected:ye})}if(ae[0]instanceof Array&&ae.length>1)throw new Error("Parse Error: multiple actions possible at state: "+ie+", token: "+te);switch(ae[0]){case 1:N.push(te),_.push(z.yytext),A.push(z.yylloc),N.push(ae[1]),te=null,le?(te=le,le=null):(B=z.yyleng,D=z.yytext,P=z.yylineno,j=z.yylloc,O>0&&O--);break;case 2:if(Oe=this.productions_[ae[1]][1],Pe.$=_[_.length-Oe],Pe._$={first_line:A[A.length-(Oe||1)].first_line,last_line:A[A.length-1].last_line,first_column:A[A.length-(Oe||1)].first_column,last_column:A[A.length-1].last_column},Q&&(Pe._$.range=[A[A.length-(Oe||1)].range[0],A[A.length-1].range[1]]),be=this.performAction.apply(Pe,[D,B,P,W.yy,ae[1],_,A].concat(G)),typeof be<"u")return be;Oe&&(N=N.slice(0,-1*Oe*2),_=_.slice(0,-1*Oe),A=A.slice(0,-1*Oe)),N.push(this.productions_[ae[1]][0]),_.push(Pe.$),A.push(Pe._$),ue=M[N[N.length-2]][N[N.length-1]],N.push(ue);break;case 3:return!0}}return!0},"parse")},C=(function(){var S={EOF:1,parseError:o(function(L,N){if(this.yy.parser)this.yy.parser.parseError(L,N);else throw new Error(L)},"parseError"),setInput:o(function(R,L){return this.yy=L||this.yy||{},this._input=R,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var R=this._input[0];this.yytext+=R,this.yyleng++,this.offset++,this.match+=R,this.matched+=R;var L=R.match(/(?:\r\n?|\n).*/g);return L?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),R},"input"),unput:o(function(R){var L=R.length,N=R.split(/(?:\r\n?|\n)/g);this._input=R+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-L),this.offset-=L;var I=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),N.length-1&&(this.yylineno-=N.length-1);var _=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:N?(N.length===I.length?this.yylloc.first_column:0)+I[I.length-N.length].length-N[0].length:this.yylloc.first_column-L},this.options.ranges&&(this.yylloc.range=[_[0],_[0]+this.yyleng-L]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(R){this.unput(this.match.slice(R))},"less"),pastInput:o(function(){var R=this.matched.substr(0,this.matched.length-this.match.length);return(R.length>20?"...":"")+R.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var R=this.match;return R.length<20&&(R+=this._input.substr(0,20-R.length)),(R.substr(0,20)+(R.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var R=this.pastInput(),L=new Array(R.length+1).join("-");return R+this.upcomingInput()+` +`+L+"^"},"showPosition"),test_match:o(function(R,L){var N,I,_;if(this.options.backtrack_lexer&&(_={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(_.yylloc.range=this.yylloc.range.slice(0))),I=R[0].match(/(?:\r\n?|\n).*/g),I&&(this.yylineno+=I.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:I?I[I.length-1].length-I[I.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+R[0].length},this.yytext+=R[0],this.match+=R[0],this.matches=R,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(R[0].length),this.matched+=R[0],N=this.performAction.call(this,this.yy,this,L,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),N)return N;if(this._backtrack){for(var A in _)this[A]=_[A];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var R,L,N,I;this._more||(this.yytext="",this.match="");for(var _=this._currentRules(),A=0;A<_.length;A++)if(N=this._input.match(this.rules[_[A]]),N&&(!L||N[0].length>L[0].length)){if(L=N,I=A,this.options.backtrack_lexer){if(R=this.test_match(N,_[A]),R!==!1)return R;if(this._backtrack){L=!1;continue}else return!1}else if(!this.options.flex)break}return L?(R=this.test_match(L,_[I]),R!==!1?R:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var L=this.next();return L||this.lex()},"lex"),begin:o(function(L){this.conditionStack.push(L)},"begin"),popState:o(function(){var L=this.conditionStack.length-1;return L>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(L){return L=this.conditionStack.length-1-Math.abs(L||0),L>=0?this.conditionStack[L]:"INITIAL"},"topState"),pushState:o(function(L){this.begin(L)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:o(function(L,N,I,_){var A=_;switch(I){case 0:return this.pushState("shapeData"),N.yytext="",24;break;case 1:return this.pushState("shapeDataStr"),24;break;case 2:return this.popState(),24;break;case 3:let M=/\n\s*/g;return N.yytext=N.yytext.replace(M,"
"),24;break;case 4:return 24;case 5:this.popState();break;case 6:return L.getLogger().trace("Found comment",N.yytext),6;break;case 7:return 8;case 8:this.begin("CLASS");break;case 9:return this.popState(),17;break;case 10:this.popState();break;case 11:L.getLogger().trace("Begin icon"),this.begin("ICON");break;case 12:return L.getLogger().trace("SPACELINE"),6;break;case 13:return 7;case 14:return 16;case 15:L.getLogger().trace("end icon"),this.popState();break;case 16:return L.getLogger().trace("Exploding node"),this.begin("NODE"),20;break;case 17:return L.getLogger().trace("Cloud"),this.begin("NODE"),20;break;case 18:return L.getLogger().trace("Explosion Bang"),this.begin("NODE"),20;break;case 19:return L.getLogger().trace("Cloud Bang"),this.begin("NODE"),20;break;case 20:return this.begin("NODE"),20;break;case 21:return this.begin("NODE"),20;break;case 22:return this.begin("NODE"),20;break;case 23:return this.begin("NODE"),20;break;case 24:return 13;case 25:return 23;case 26:return 11;case 27:this.begin("NSTR2");break;case 28:return"NODE_DESCR";case 29:this.popState();break;case 30:L.getLogger().trace("Starting NSTR"),this.begin("NSTR");break;case 31:return L.getLogger().trace("description:",N.yytext),"NODE_DESCR";break;case 32:this.popState();break;case 33:return this.popState(),L.getLogger().trace("node end ))"),"NODE_DEND";break;case 34:return this.popState(),L.getLogger().trace("node end )"),"NODE_DEND";break;case 35:return this.popState(),L.getLogger().trace("node end ...",N.yytext),"NODE_DEND";break;case 36:return this.popState(),L.getLogger().trace("node end (("),"NODE_DEND";break;case 37:return this.popState(),L.getLogger().trace("node end (-"),"NODE_DEND";break;case 38:return this.popState(),L.getLogger().trace("node end (-"),"NODE_DEND";break;case 39:return this.popState(),L.getLogger().trace("node end (("),"NODE_DEND";break;case 40:return this.popState(),L.getLogger().trace("node end (("),"NODE_DEND";break;case 41:return L.getLogger().trace("Long description:",N.yytext),21;break;case 42:return L.getLogger().trace("Long description:",N.yytext),21;break}},"anonymous"),rules:[/^(?:@\{)/i,/^(?:["])/i,/^(?:["])/i,/^(?:[^\"]+)/i,/^(?:[^}^"]+)/i,/^(?:\})/i,/^(?:\s*%%.*)/i,/^(?:kanban\b)/i,/^(?::::)/i,/^(?:.+)/i,/^(?:\n)/i,/^(?:::icon\()/i,/^(?:[\s]+[\n])/i,/^(?:[\n]+)/i,/^(?:[^\)]+)/i,/^(?:\))/i,/^(?:-\))/i,/^(?:\(-)/i,/^(?:\)\))/i,/^(?:\))/i,/^(?:\(\()/i,/^(?:\{\{)/i,/^(?:\()/i,/^(?:\[)/i,/^(?:[\s]+)/i,/^(?:[^\(\[\n\)\{\}@]+)/i,/^(?:$)/i,/^(?:["][`])/i,/^(?:[^`"]+)/i,/^(?:[`]["])/i,/^(?:["])/i,/^(?:[^"]+)/i,/^(?:["])/i,/^(?:[\)]\))/i,/^(?:[\)])/i,/^(?:[\]])/i,/^(?:\}\})/i,/^(?:\(-)/i,/^(?:-\))/i,/^(?:\(\()/i,/^(?:\()/i,/^(?:[^\)\]\(\}]+)/i,/^(?:.+(?!\(\())/i],conditions:{shapeDataEndBracket:{rules:[],inclusive:!1},shapeDataStr:{rules:[2,3],inclusive:!1},shapeData:{rules:[1,4,5],inclusive:!1},CLASS:{rules:[9,10],inclusive:!1},ICON:{rules:[14,15],inclusive:!1},NSTR2:{rules:[28,29],inclusive:!1},NSTR:{rules:[31,32],inclusive:!1},NODE:{rules:[27,30,33,34,35,36,37,38,39,40,41,42],inclusive:!1},INITIAL:{rules:[0,6,7,8,11,12,13,16,17,18,19,20,21,22,23,24,25,26],inclusive:!0}}};return S})();k.lexer=C;function w(){this.yy={}}return o(w,"Parser"),w.prototype=k,k.Parser=w,new w})();rj.parser=rj;DNe=rj});var Ol,ij,nj,aj,Z_t,Q_t,MNe,J_t,eLt,pa,tLt,rLt,nLt,iLt,aLt,sLt,oLt,NNe,PNe=F(()=>{"use strict";Xt();Vr();vt();Wi();R2();Ol=[],ij=[],nj=0,aj={},Z_t=o(()=>{Ol=[],ij=[],nj=0,aj={}},"clear"),Q_t=o(e=>{if(Ol.length===0)return null;let t=Ol[0].level,r=null;for(let n=Ol.length-1;n>=0;n--)if(Ol[n].level===t&&!r&&(r=Ol[n]),Ol[n].levell.parentId===i.id);for(let l of s){let u={id:l.id,parentId:i.id,label:mr(l.label??"",n),labelType:"markdown",isGroup:!1,ticket:l?.ticket,priority:l?.priority,assigned:l?.assigned,icon:l?.icon,shape:"kanbanItem",level:l.level,rx:5,ry:5,cssStyles:["text-align: left"]};t.push(u)}}return{nodes:t,edges:e,other:{},config:Ae()}},"getData"),eLt=o((e,t,r,n,i)=>{let a=Ae(),s=a.mindmap?.padding??cr.mindmap.padding;switch(n){case pa.ROUNDED_RECT:case pa.RECT:case pa.HEXAGON:s*=2}let l={id:mr(t,a)||"kbn"+nj++,level:e,label:mr(r,a),width:a.mindmap?.maxNodeWidth??cr.mindmap.maxNodeWidth,padding:s,isGroup:!1};if(i!==void 0){let h;i.includes(` +`)?h=i+` +`:h=`{ +`+i+` +}`;let d=Jd(h,{schema:Qd});if(d.shape&&(d.shape!==d.shape.toLowerCase()||d.shape.includes("_")))throw new Error(`No such shape: ${d.shape}. Shape names should be lowercase.`);d?.shape&&d.shape==="kanbanItem"&&(l.shape=d?.shape),d?.label&&(l.label=d?.label),d?.icon&&(l.icon=d?.icon.toString()),d?.assigned&&(l.assigned=d?.assigned.toString()),d?.ticket&&(l.ticket=d?.ticket.toString()),d?.priority&&(l.priority=d?.priority)}let u=Q_t(e);u?l.parentId=u.id||"kbn"+nj++:ij.push(l),Ol.push(l)},"addNode"),pa={DEFAULT:0,NO_BORDER:0,ROUNDED_RECT:1,RECT:2,CIRCLE:3,CLOUD:4,BANG:5,HEXAGON:6},tLt=o((e,t)=>{switch(Z.debug("In get type",e,t),e){case"[":return pa.RECT;case"(":return t===")"?pa.ROUNDED_RECT:pa.CLOUD;case"((":return pa.CIRCLE;case")":return pa.CLOUD;case"))":return pa.BANG;case"{{":return pa.HEXAGON;default:return pa.DEFAULT}},"getType"),rLt=o((e,t)=>{aj[e]=t},"setElementForId"),nLt=o(e=>{if(!e)return;let t=Ae(),r=Ol[Ol.length-1];e.icon&&(r.icon=mr(e.icon,t)),e.class&&(r.cssClasses=mr(e.class,t))},"decorateNode"),iLt=o(e=>{switch(e){case pa.DEFAULT:return"no-border";case pa.RECT:return"rect";case pa.ROUNDED_RECT:return"rounded-rect";case pa.CIRCLE:return"circle";case pa.CLOUD:return"cloud";case pa.BANG:return"bang";case pa.HEXAGON:return"hexgon";default:return"no-border"}},"type2Str"),aLt=o(()=>Z,"getLogger"),sLt=o(e=>aj[e],"getElementById"),oLt={clear:Z_t,addNode:eLt,getSections:MNe,getData:J_t,nodeType:pa,getType:tLt,setElementForId:rLt,decorateNode:nLt,type2Str:iLt,getLogger:aLt,getElementById:sLt},NNe=oLt});var lLt,ONe,BNe=F(()=>{"use strict";Xt();vt();Ka();$n();Wi();Wy();Dm();lLt=o(async(e,t,r,n)=>{Z.debug(`Rendering kanban diagram +`+e);let a=n.db.getData(),s=Ae();s.htmlLabels=!1;let l=xn(t);for(let v of a.nodes)v.domId=`${t}-${v.id}`;let u=l.append("g");u.attr("class","sections");let h=l.append("g");h.attr("class","items");let d=a.nodes.filter(v=>v.isGroup),f=0,p=10,m=[],g=25;for(let v of d){let x=s?.kanban?.sectionWidth||200;f=f+1,v.x=x*f+(f-1)*p/2,v.width=x,v.y=0,v.height=x*3,v.rx=5,v.ry=5,v.cssClasses=v.cssClasses+" section-"+f;let b=await nf(u,v);g=Math.max(g,b?.labelBBox?.height),m.push(b)}let y=0;for(let v of d){let x=m[y];y=y+1;let b=s?.kanban?.sectionWidth||200,T=-b*3/2+g,k=T,C=a.nodes.filter(R=>R.parentId===v.id);for(let R of C){if(R.isGroup)throw new Error("Groups within groups are not allowed in Kanban diagrams");R.x=v.x,R.width=b-1.5*p;let N=(await af(h,R,{config:s})).node().getBBox();R.y=k+N.height/2,await Lm(R),k=R.y+N.height/2+p/2}let w=x.cluster.select("rect"),S=Math.max(k-T+3*p,50)+(g-25);w.attr("height",S)}ul(void 0,l,s.mindmap?.padding??cr.kanban.padding,s.mindmap?.useMaxWidth??cr.kanban.useMaxWidth)},"draw"),ONe={draw:lLt}});var cLt,uLt,$Ne,FNe=F(()=>{"use strict";zi();X1();cLt=o(e=>{let t="";for(let n=0;ne.darkMode?Je(n,i):Qe(n,i),"adjuster");for(let n=0;n` + .edge { + stroke-width: 3; + } + ${cLt(e)} + .section-root rect, .section-root path, .section-root circle, .section-root polygon { + fill: ${e.git0}; + } + .section-root text { + fill: ${e.gitBranchLabel0}; + } + .icon-container { + height:100%; + display: flex; + justify-content: center; + align-items: center; + } + .edge { + fill: none; + } + .cluster-label, .label { + color: ${e.textColor}; + fill: ${e.textColor}; + } + .kanban-label { + dy: 1em; + alignment-baseline: middle; + text-anchor: middle; + dominant-baseline: middle; + text-align: center; + } + ${Eu()} +`,"getStyles"),$Ne=uLt});var zNe={};ir(zNe,{diagram:()=>hLt});var hLt,GNe=F(()=>{"use strict";INe();PNe();BNe();FNe();hLt={db:NNe,renderer:ONe,parser:DNe,styles:$Ne}});var sj,Yw,qNe=F(()=>{"use strict";sj=(function(){var e=o(function(l,u,h,d){for(h=h||{},d=l.length;d--;h[l[d]]=u);return h},"o"),t=[1,9],r=[1,10],n=[1,5,10,12],i={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,SANKEY:4,NEWLINE:5,csv:6,opt_eof:7,record:8,csv_tail:9,EOF:10,"field[source]":11,COMMA:12,"field[target]":13,"field[value]":14,field:15,escaped:16,non_escaped:17,DQUOTE:18,ESCAPED_TEXT:19,NON_ESCAPED_TEXT:20,$accept:0,$end:1},terminals_:{2:"error",4:"SANKEY",5:"NEWLINE",10:"EOF",11:"field[source]",12:"COMMA",13:"field[target]",14:"field[value]",18:"DQUOTE",19:"ESCAPED_TEXT",20:"NON_ESCAPED_TEXT"},productions_:[0,[3,4],[6,2],[9,2],[9,0],[7,1],[7,0],[8,5],[15,1],[15,1],[16,3],[17,1]],performAction:o(function(u,h,d,f,p,m,g){var y=m.length-1;switch(p){case 7:let v=f.findOrCreateNode(m[y-4].trim().replaceAll('""','"')),x=f.findOrCreateNode(m[y-2].trim().replaceAll('""','"')),b=parseFloat(m[y].trim());f.addLink(v,x,b);break;case 8:case 9:case 11:this.$=m[y];break;case 10:this.$=m[y-1];break}},"anonymous"),table:[{3:1,4:[1,2]},{1:[3]},{5:[1,3]},{6:4,8:5,15:6,16:7,17:8,18:t,20:r},{1:[2,6],7:11,10:[1,12]},e(r,[2,4],{9:13,5:[1,14]}),{12:[1,15]},e(n,[2,8]),e(n,[2,9]),{19:[1,16]},e(n,[2,11]),{1:[2,1]},{1:[2,5]},e(r,[2,2]),{6:17,8:5,15:6,16:7,17:8,18:t,20:r},{15:18,16:7,17:8,18:t,20:r},{18:[1,19]},e(r,[2,3]),{12:[1,20]},e(n,[2,10]),{15:21,16:7,17:8,18:t,20:r},e([1,5,10],[2,7])],defaultActions:{11:[2,1],12:[2,5]},parseError:o(function(u,h){if(h.recoverable)this.trace(u);else{var d=new Error(u);throw d.hash=h,d}},"parseError"),parse:o(function(u){var h=this,d=[0],f=[],p=[null],m=[],g=this.table,y="",v=0,x=0,b=0,T=2,k=1,C=m.slice.call(arguments,1),w=Object.create(this.lexer),S={yy:{}};for(var R in this.yy)Object.prototype.hasOwnProperty.call(this.yy,R)&&(S.yy[R]=this.yy[R]);w.setInput(u,S.yy),S.yy.lexer=w,S.yy.parser=this,typeof w.yylloc>"u"&&(w.yylloc={});var L=w.yylloc;m.push(L);var N=w.options&&w.options.ranges;typeof S.yy.parseError=="function"?this.parseError=S.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function I(j){d.length=d.length-2*j,p.length=p.length-j,m.length=m.length-j}o(I,"popStack");function _(){var j;return j=f.pop()||w.lex()||k,typeof j!="number"&&(j instanceof Array&&(f=j,j=f.pop()),j=h.symbols_[j]||j),j}o(_,"lex");for(var A,M,D,P,B,O,$={},V,G,z,W;;){if(D=d[d.length-1],this.defaultActions[D]?P=this.defaultActions[D]:((A===null||typeof A>"u")&&(A=_()),P=g[D]&&g[D][A]),typeof P>"u"||!P.length||!P[0]){var H="";W=[];for(V in g[D])this.terminals_[V]&&V>T&&W.push("'"+this.terminals_[V]+"'");w.showPosition?H="Parse error on line "+(v+1)+`: +`+w.showPosition()+` +Expecting `+W.join(", ")+", got '"+(this.terminals_[A]||A)+"'":H="Parse error on line "+(v+1)+": Unexpected "+(A==k?"end of input":"'"+(this.terminals_[A]||A)+"'"),this.parseError(H,{text:w.match,token:this.terminals_[A]||A,line:w.yylineno,loc:L,expected:W})}if(P[0]instanceof Array&&P.length>1)throw new Error("Parse Error: multiple actions possible at state: "+D+", token: "+A);switch(P[0]){case 1:d.push(A),p.push(w.yytext),m.push(w.yylloc),d.push(P[1]),A=null,M?(A=M,M=null):(x=w.yyleng,y=w.yytext,v=w.yylineno,L=w.yylloc,b>0&&b--);break;case 2:if(G=this.productions_[P[1]][1],$.$=p[p.length-G],$._$={first_line:m[m.length-(G||1)].first_line,last_line:m[m.length-1].last_line,first_column:m[m.length-(G||1)].first_column,last_column:m[m.length-1].last_column},N&&($._$.range=[m[m.length-(G||1)].range[0],m[m.length-1].range[1]]),O=this.performAction.apply($,[y,x,v,S.yy,P[1],p,m].concat(C)),typeof O<"u")return O;G&&(d=d.slice(0,-1*G*2),p=p.slice(0,-1*G),m=m.slice(0,-1*G)),d.push(this.productions_[P[1]][0]),p.push($.$),m.push($._$),z=g[d[d.length-2]][d[d.length-1]],d.push(z);break;case 3:return!0}}return!0},"parse")},a=(function(){var l={EOF:1,parseError:o(function(h,d){if(this.yy.parser)this.yy.parser.parseError(h,d);else throw new Error(h)},"parseError"),setInput:o(function(u,h){return this.yy=h||this.yy||{},this._input=u,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var u=this._input[0];this.yytext+=u,this.yyleng++,this.offset++,this.match+=u,this.matched+=u;var h=u.match(/(?:\r\n?|\n).*/g);return h?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),u},"input"),unput:o(function(u){var h=u.length,d=u.split(/(?:\r\n?|\n)/g);this._input=u+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-h),this.offset-=h;var f=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),d.length-1&&(this.yylineno-=d.length-1);var p=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:d?(d.length===f.length?this.yylloc.first_column:0)+f[f.length-d.length].length-d[0].length:this.yylloc.first_column-h},this.options.ranges&&(this.yylloc.range=[p[0],p[0]+this.yyleng-h]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(u){this.unput(this.match.slice(u))},"less"),pastInput:o(function(){var u=this.matched.substr(0,this.matched.length-this.match.length);return(u.length>20?"...":"")+u.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var u=this.match;return u.length<20&&(u+=this._input.substr(0,20-u.length)),(u.substr(0,20)+(u.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var u=this.pastInput(),h=new Array(u.length+1).join("-");return u+this.upcomingInput()+` +`+h+"^"},"showPosition"),test_match:o(function(u,h){var d,f,p;if(this.options.backtrack_lexer&&(p={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(p.yylloc.range=this.yylloc.range.slice(0))),f=u[0].match(/(?:\r\n?|\n).*/g),f&&(this.yylineno+=f.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:f?f[f.length-1].length-f[f.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+u[0].length},this.yytext+=u[0],this.match+=u[0],this.matches=u,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(u[0].length),this.matched+=u[0],d=this.performAction.call(this,this.yy,this,h,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),d)return d;if(this._backtrack){for(var m in p)this[m]=p[m];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var u,h,d,f;this._more||(this.yytext="",this.match="");for(var p=this._currentRules(),m=0;mh[0].length)){if(h=d,f=m,this.options.backtrack_lexer){if(u=this.test_match(d,p[m]),u!==!1)return u;if(this._backtrack){h=!1;continue}else return!1}else if(!this.options.flex)break}return h?(u=this.test_match(h,p[f]),u!==!1?u:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var h=this.next();return h||this.lex()},"lex"),begin:o(function(h){this.conditionStack.push(h)},"begin"),popState:o(function(){var h=this.conditionStack.length-1;return h>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(h){return h=this.conditionStack.length-1-Math.abs(h||0),h>=0?this.conditionStack[h]:"INITIAL"},"topState"),pushState:o(function(h){this.begin(h)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:o(function(h,d,f,p){var m=p;switch(f){case 0:return this.pushState("csv"),4;break;case 1:return this.pushState("csv"),4;break;case 2:return 10;case 3:return 5;case 4:return 12;case 5:return this.pushState("escaped_text"),18;break;case 6:return 20;case 7:return this.popState("escaped_text"),18;break;case 8:return 19}},"anonymous"),rules:[/^(?:sankey-beta\b)/i,/^(?:sankey\b)/i,/^(?:$)/i,/^(?:((\u000D\u000A)|(\u000A)))/i,/^(?:(\u002C))/i,/^(?:(\u0022))/i,/^(?:([\u0020-\u0021\u0023-\u002B\u002D-\u007E])*)/i,/^(?:(\u0022)(?!(\u0022)))/i,/^(?:(([\u0020-\u0021\u0023-\u002B\u002D-\u007E])|(\u002C)|(\u000D)|(\u000A)|(\u0022)(\u0022))*)/i],conditions:{csv:{rules:[2,3,4,5,6,7,8],inclusive:!1},escaped_text:{rules:[7,8],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,7,8],inclusive:!0}}};return l})();i.lexer=a;function s(){this.yy={}}return o(s,"Parser"),s.prototype=i,i.Parser=s,new s})();sj.parser=sj;Yw=sj});var UL,YL,HL,mLt,oj,gLt,lj,yLt,vLt,xLt,bLt,HNe,UNe=F(()=>{"use strict";Xt();Vr();Nn();UL=[],YL=[],HL=new Map,mLt=o(()=>{UL=[],YL=[],HL=new Map,yr()},"clear"),oj=class{constructor(t,r,n=0){this.source=t;this.target=r;this.value=n}static{o(this,"SankeyLink")}},gLt=o((e,t,r)=>{UL.push(new oj(e,t,r))},"addLink"),lj=class{constructor(t){this.ID=t}static{o(this,"SankeyNode")}},yLt=o(e=>{e=xt.sanitizeText(e,Ae());let t=HL.get(e);return t===void 0&&(t=new lj(e),HL.set(e,t),YL.push(t)),t},"findOrCreateNode"),vLt=o(()=>YL,"getNodes"),xLt=o(()=>UL,"getLinks"),bLt=o(()=>({nodes:YL.map(e=>({id:e.ID})),links:UL.map(e=>({source:e.source.ID,target:e.target.ID,value:e.value}))}),"getGraph"),HNe={nodesMap:HL,getConfig:o(()=>Ae().sankey,"getConfig"),getNodes:vLt,getLinks:xLt,getGraph:bLt,addLink:gLt,findOrCreateNode:yLt,getAccTitle:Ar,setAccTitle:kr,getAccDescription:_r,setAccDescription:Rr,getDiagramTitle:Lr,setDiagramTitle:Or,clear:mLt}});function jw(e,t){let r;if(t===void 0)for(let n of e)n!=null&&(r=n)&&(r=n);else{let n=-1;for(let i of e)(i=t(i,++n,e))!=null&&(r=i)&&(r=i)}return r}var YNe=F(()=>{"use strict";o(jw,"max")});function cx(e,t){let r;if(t===void 0)for(let n of e)n!=null&&(r>n||r===void 0&&n>=n)&&(r=n);else{let n=-1;for(let i of e)(i=t(i,++n,e))!=null&&(r>i||r===void 0&&i>=i)&&(r=i)}return r}var jNe=F(()=>{"use strict";o(cx,"min")});function ux(e,t){let r=0;if(t===void 0)for(let n of e)(n=+n)&&(r+=n);else{let n=-1;for(let i of e)(i=+t(i,++n,e))&&(r+=i)}return r}var XNe=F(()=>{"use strict";o(ux,"sum")});var cj=F(()=>{"use strict";YNe();jNe();XNe()});function TLt(e){return e.target.depth}function uj(e){return e.depth}function hj(e,t){return t-1-e.height}function Xw(e,t){return e.sourceLinks.length?e.depth:t-1}function dj(e){return e.targetLinks.length?e.depth:e.sourceLinks.length?cx(e.sourceLinks,TLt)-1:0}var fj=F(()=>{"use strict";cj();o(TLt,"targetDepth");o(uj,"left");o(hj,"right");o(Xw,"justify");o(dj,"center")});function hx(e){return function(){return e}}var KNe=F(()=>{"use strict";o(hx,"constant")});function ZNe(e,t){return jL(e.source,t.source)||e.index-t.index}function QNe(e,t){return jL(e.target,t.target)||e.index-t.index}function jL(e,t){return e.y0-t.y0}function pj(e){return e.value}function CLt(e){return e.index}function wLt(e){return e.nodes}function kLt(e){return e.links}function JNe(e,t){let r=e.get(t);if(!r)throw new Error("missing: "+t);return r}function ePe({nodes:e}){for(let t of e){let r=t.y0,n=r;for(let i of t.sourceLinks)i.y0=r+i.width/2,r+=i.width;for(let i of t.targetLinks)i.y1=n+i.width/2,n+=i.width}}function XL(){let e=0,t=0,r=1,n=1,i=24,a=8,s,l=CLt,u=Xw,h,d,f=wLt,p=kLt,m=6;function g(){let D={nodes:f.apply(null,arguments),links:p.apply(null,arguments)};return y(D),v(D),x(D),b(D),C(D),ePe(D),D}o(g,"sankey"),g.update=function(D){return ePe(D),D},g.nodeId=function(D){return arguments.length?(l=typeof D=="function"?D:hx(D),g):l},g.nodeAlign=function(D){return arguments.length?(u=typeof D=="function"?D:hx(D),g):u},g.nodeSort=function(D){return arguments.length?(h=D,g):h},g.nodeWidth=function(D){return arguments.length?(i=+D,g):i},g.nodePadding=function(D){return arguments.length?(a=s=+D,g):a},g.nodes=function(D){return arguments.length?(f=typeof D=="function"?D:hx(D),g):f},g.links=function(D){return arguments.length?(p=typeof D=="function"?D:hx(D),g):p},g.linkSort=function(D){return arguments.length?(d=D,g):d},g.size=function(D){return arguments.length?(e=t=0,r=+D[0],n=+D[1],g):[r-e,n-t]},g.extent=function(D){return arguments.length?(e=+D[0][0],r=+D[1][0],t=+D[0][1],n=+D[1][1],g):[[e,t],[r,n]]},g.iterations=function(D){return arguments.length?(m=+D,g):m};function y({nodes:D,links:P}){for(let[O,$]of D.entries())$.index=O,$.sourceLinks=[],$.targetLinks=[];let B=new Map(D.map((O,$)=>[l(O,$,D),O]));for(let[O,$]of P.entries()){$.index=O;let{source:V,target:G}=$;typeof V!="object"&&(V=$.source=JNe(B,V)),typeof G!="object"&&(G=$.target=JNe(B,G)),V.sourceLinks.push($),G.targetLinks.push($)}if(d!=null)for(let{sourceLinks:O,targetLinks:$}of D)O.sort(d),$.sort(d)}o(y,"computeNodeLinks");function v({nodes:D}){for(let P of D)P.value=P.fixedValue===void 0?Math.max(ux(P.sourceLinks,pj),ux(P.targetLinks,pj)):P.fixedValue}o(v,"computeNodeValues");function x({nodes:D}){let P=D.length,B=new Set(D),O=new Set,$=0;for(;B.size;){for(let V of B){V.depth=$;for(let{target:G}of V.sourceLinks)O.add(G)}if(++$>P)throw new Error("circular link");B=O,O=new Set}}o(x,"computeNodeDepths");function b({nodes:D}){let P=D.length,B=new Set(D),O=new Set,$=0;for(;B.size;){for(let V of B){V.height=$;for(let{source:G}of V.targetLinks)O.add(G)}if(++$>P)throw new Error("circular link");B=O,O=new Set}}o(b,"computeNodeHeights");function T({nodes:D}){let P=jw(D,$=>$.depth)+1,B=(r-e-i)/(P-1),O=new Array(P);for(let $ of D){let V=Math.max(0,Math.min(P-1,Math.floor(u.call(null,$,P))));$.layer=V,$.x0=e+V*B,$.x1=$.x0+i,O[V]?O[V].push($):O[V]=[$]}if(h)for(let $ of O)$.sort(h);return O}o(T,"computeNodeLayers");function k(D){let P=cx(D,B=>(n-t-(B.length-1)*s)/ux(B,pj));for(let B of D){let O=t;for(let $ of B){$.y0=O,$.y1=O+$.value*P,O=$.y1+s;for(let V of $.sourceLinks)V.width=V.value*P}O=(n-O+s)/(B.length+1);for(let $=0;$B.length)-1)),k(P);for(let B=0;B0))continue;let H=(z/W-G.y0)*P;G.y0+=H,G.y1+=H,I(G)}h===void 0&&V.sort(jL),R(V,B)}}o(w,"relaxLeftToRight");function S(D,P,B){for(let O=D.length,$=O-2;$>=0;--$){let V=D[$];for(let G of V){let z=0,W=0;for(let{target:j,value:Q}of G.sourceLinks){let U=Q*(j.layer-G.layer);z+=M(G,j)*U,W+=U}if(!(W>0))continue;let H=(z/W-G.y0)*P;G.y0+=H,G.y1+=H,I(G)}h===void 0&&V.sort(jL),R(V,B)}}o(S,"relaxRightToLeft");function R(D,P){let B=D.length>>1,O=D[B];N(D,O.y0-s,B-1,P),L(D,O.y1+s,B+1,P),N(D,n,D.length-1,P),L(D,t,0,P)}o(R,"resolveCollisions");function L(D,P,B,O){for(;B1e-6&&($.y0+=V,$.y1+=V),P=$.y1+s}}o(L,"resolveCollisionsTopToBottom");function N(D,P,B,O){for(;B>=0;--B){let $=D[B],V=($.y1-P)*O;V>1e-6&&($.y0-=V,$.y1-=V),P=$.y0-s}}o(N,"resolveCollisionsBottomToTop");function I({sourceLinks:D,targetLinks:P}){if(d===void 0){for(let{source:{sourceLinks:B}}of P)B.sort(QNe);for(let{target:{targetLinks:B}}of D)B.sort(ZNe)}}o(I,"reorderNodeLinks");function _(D){if(d===void 0)for(let{sourceLinks:P,targetLinks:B}of D)P.sort(QNe),B.sort(ZNe)}o(_,"reorderLinks");function A(D,P){let B=D.y0-(D.sourceLinks.length-1)*s/2;for(let{target:O,width:$}of D.sourceLinks){if(O===P)break;B+=$+s}for(let{source:O,width:$}of P.targetLinks){if(O===D)break;B-=$}return B}o(A,"targetTop");function M(D,P){let B=P.y0-(P.targetLinks.length-1)*s/2;for(let{source:O,width:$}of P.targetLinks){if(O===D)break;B+=$+s}for(let{target:O,width:$}of D.sourceLinks){if(O===P)break;B-=$}return B}return o(M,"sourceTop"),g}var tPe=F(()=>{"use strict";cj();fj();KNe();o(ZNe,"ascendingSourceBreadth");o(QNe,"ascendingTargetBreadth");o(jL,"ascendingBreadth");o(pj,"value");o(CLt,"defaultId");o(wLt,"defaultNodes");o(kLt,"defaultLinks");o(JNe,"find");o(ePe,"computeLinkBreadths");o(XL,"Sankey")});function yj(){this._x0=this._y0=this._x1=this._y1=null,this._=""}function rPe(){return new yj}var mj,gj,D0,SLt,vj,nPe=F(()=>{"use strict";mj=Math.PI,gj=2*mj,D0=1e-6,SLt=gj-D0;o(yj,"Path");o(rPe,"path");yj.prototype=rPe.prototype={constructor:yj,moveTo:o(function(e,t){this._+="M"+(this._x0=this._x1=+e)+","+(this._y0=this._y1=+t)},"moveTo"),closePath:o(function(){this._x1!==null&&(this._x1=this._x0,this._y1=this._y0,this._+="Z")},"closePath"),lineTo:o(function(e,t){this._+="L"+(this._x1=+e)+","+(this._y1=+t)},"lineTo"),quadraticCurveTo:o(function(e,t,r,n){this._+="Q"+ +e+","+ +t+","+(this._x1=+r)+","+(this._y1=+n)},"quadraticCurveTo"),bezierCurveTo:o(function(e,t,r,n,i,a){this._+="C"+ +e+","+ +t+","+ +r+","+ +n+","+(this._x1=+i)+","+(this._y1=+a)},"bezierCurveTo"),arcTo:o(function(e,t,r,n,i){e=+e,t=+t,r=+r,n=+n,i=+i;var a=this._x1,s=this._y1,l=r-e,u=n-t,h=a-e,d=s-t,f=h*h+d*d;if(i<0)throw new Error("negative radius: "+i);if(this._x1===null)this._+="M"+(this._x1=e)+","+(this._y1=t);else if(f>D0)if(!(Math.abs(d*l-u*h)>D0)||!i)this._+="L"+(this._x1=e)+","+(this._y1=t);else{var p=r-a,m=n-s,g=l*l+u*u,y=p*p+m*m,v=Math.sqrt(g),x=Math.sqrt(f),b=i*Math.tan((mj-Math.acos((g+f-y)/(2*v*x)))/2),T=b/x,k=b/v;Math.abs(T-1)>D0&&(this._+="L"+(e+T*h)+","+(t+T*d)),this._+="A"+i+","+i+",0,0,"+ +(d*p>h*m)+","+(this._x1=e+k*l)+","+(this._y1=t+k*u)}},"arcTo"),arc:o(function(e,t,r,n,i,a){e=+e,t=+t,r=+r,a=!!a;var s=r*Math.cos(n),l=r*Math.sin(n),u=e+s,h=t+l,d=1^a,f=a?n-i:i-n;if(r<0)throw new Error("negative radius: "+r);this._x1===null?this._+="M"+u+","+h:(Math.abs(this._x1-u)>D0||Math.abs(this._y1-h)>D0)&&(this._+="L"+u+","+h),r&&(f<0&&(f=f%gj+gj),f>SLt?this._+="A"+r+","+r+",0,1,"+d+","+(e-s)+","+(t-l)+"A"+r+","+r+",0,1,"+d+","+(this._x1=u)+","+(this._y1=h):f>D0&&(this._+="A"+r+","+r+",0,"+ +(f>=mj)+","+d+","+(this._x1=e+r*Math.cos(i))+","+(this._y1=t+r*Math.sin(i))))},"arc"),rect:o(function(e,t,r,n){this._+="M"+(this._x0=this._x1=+e)+","+(this._y0=this._y1=+t)+"h"+ +r+"v"+ +n+"h"+-r+"Z"},"rect"),toString:o(function(){return this._},"toString")};vj=rPe});var iPe=F(()=>{"use strict";nPe()});function KL(e){return o(function(){return e},"constant")}var aPe=F(()=>{"use strict";o(KL,"default")});function sPe(e){return e[0]}function oPe(e){return e[1]}var lPe=F(()=>{"use strict";o(sPe,"x");o(oPe,"y")});var cPe,uPe=F(()=>{"use strict";cPe=Array.prototype.slice});function ELt(e){return e.source}function ALt(e){return e.target}function RLt(e){var t=ELt,r=ALt,n=sPe,i=oPe,a=null;function s(){var l,u=cPe.call(arguments),h=t.apply(this,u),d=r.apply(this,u);if(a||(a=l=vj()),e(a,+n.apply(this,(u[0]=h,u)),+i.apply(this,u),+n.apply(this,(u[0]=d,u)),+i.apply(this,u)),l)return a=null,l+""||null}return o(s,"link"),s.source=function(l){return arguments.length?(t=l,s):t},s.target=function(l){return arguments.length?(r=l,s):r},s.x=function(l){return arguments.length?(n=typeof l=="function"?l:KL(+l),s):n},s.y=function(l){return arguments.length?(i=typeof l=="function"?l:KL(+l),s):i},s.context=function(l){return arguments.length?(a=l??null,s):a},s}function _Lt(e,t,r,n,i){e.moveTo(t,r),e.bezierCurveTo(t=(t+n)/2,r,t,i,n,i)}function xj(){return RLt(_Lt)}var hPe=F(()=>{"use strict";iPe();uPe();aPe();lPe();o(ELt,"linkSource");o(ALt,"linkTarget");o(RLt,"link");o(_Lt,"curveHorizontal");o(xj,"linkHorizontal")});var dPe=F(()=>{"use strict";hPe()});function LLt(e){return[e.source.x1,e.y0]}function DLt(e){return[e.target.x0,e.y1]}function ZL(){return xj().source(LLt).target(DLt)}var fPe=F(()=>{"use strict";dPe();o(LLt,"horizontalSource");o(DLt,"horizontalTarget");o(ZL,"default")});var pPe=F(()=>{"use strict";tPe();fj();fPe()});var Kw,mPe=F(()=>{"use strict";Kw=class e{static{o(this,"Uid")}static{this.count=0}static next(t){return new e(t+ ++e.count)}constructor(t){this.id=t,this.href=`#${t}`}toString(){return"url("+this.href+")"}}});var ILt,MLt,NLt,gPe,yPe=F(()=>{"use strict";Xt();$r();pPe();$n();mPe();ILt={left:uj,right:hj,center:dj,justify:Xw},MLt=o(e=>{let t=0,r=0;for(let n of e){let i=n.value??0;i>t&&(t=i,r=n.layer??0)}return r},"findCentralNodeLayer"),NLt=o(function(e,t,r,n){let{securityLevel:i,sankey:a}=Ae(),s=bS.sankey,l;i==="sandbox"&&(l=et("#i"+t));let u=i==="sandbox"?et(l.nodes()[0].contentDocument.body):et("body"),h=i==="sandbox"?u.select(`[id="${t}"]`):et(`[id="${t}"]`),d=a?.width??s.width,f=a?.height??s.width,p=a?.useMaxWidth??s.useMaxWidth,m=a?.nodeAlignment??s.nodeAlignment,g=a?.prefix??s.prefix,y=a?.suffix??s.suffix,v=a?.showValues??s.showValues,x=a?.nodeWidth??s.nodeWidth??10,b=a?.nodePadding??s.nodePadding??12,T=a?.labelStyle??s.labelStyle??"legacy",k=a?.nodeColors??{},C=n.db.getGraph(),w=ILt[m];XL().nodeId(O=>O.id).nodeWidth(x).nodePadding(b+(v?15:0)).nodeAlign(w).extent([[0,0],[d,f]])(C);let R=MLt(C.nodes),L=Oo(IN),N=o(O=>k[O]??L(O),"getNodeColor");h.append("g").attr("class","nodes").selectAll(".node").data(C.nodes).join("g").attr("class","node").attr("id",O=>(O.uid=Kw.next("node-")).id).attr("transform",function(O){return"translate("+O.x0+","+O.y0+")"}).attr("x",O=>O.x0).attr("y",O=>O.y0).append("rect").attr("height",O=>O.y1-O.y0).attr("width",O=>O.x1-O.x0).attr("fill",O=>N(O.id));let I=o(({id:O,value:$})=>v?`${O} +${g}${Math.round($*100)/100}${y}`:O,"getText"),_=o(O=>T==="outlined"?(O.layer??0)A.selectAll(O?`.${O}`:"text").data(C.nodes).join("text").attr("class",O??null).attr("x",$=>_($).x).attr("y",$=>($.y1+$.y0)/2).attr("dy",`${v?"0":"0.35"}em`).attr("text-anchor",$=>_($).anchor).text(I),"appendLabel");T==="outlined"?(M("sankey-label-bg"),M("sankey-label-fg")):M();let D=h.append("g").attr("class","links").attr("fill","none").attr("stroke-opacity",.5).selectAll(".link").data(C.links).join("g").attr("class","link").style("mix-blend-mode","multiply"),P=a?.linkColor??"gradient";if(P==="gradient"){let O=D.append("linearGradient").attr("id",$=>($.uid=Kw.next("linearGradient-")).id).attr("gradientUnits","userSpaceOnUse").attr("x1",$=>$.source.x1).attr("x2",$=>$.target.x0);O.append("stop").attr("offset","0%").attr("stop-color",$=>N($.source.id)),O.append("stop").attr("offset","100%").attr("stop-color",$=>N($.target.id))}let B;switch(P){case"gradient":B=o(O=>O.uid,"coloring");break;case"source":B=o(O=>N(O.source.id),"coloring");break;case"target":B=o(O=>N(O.target.id),"coloring");break;default:B=P}D.append("path").attr("d",ZL()).attr("stroke",B).attr("stroke-width",O=>Math.max(1,O.width)),ul(void 0,h,0,p)},"draw"),gPe={draw:NLt}});var vPe,xPe=F(()=>{"use strict";vPe=o(e=>e.replaceAll(/^[^\S\n\r]+|[^\S\n\r]+$/g,"").replaceAll(/([\n\r])+/g,` +`).trim(),"prepareTextForParsing")});var PLt,bPe,TPe=F(()=>{"use strict";PLt=o(e=>`.label { + font-family: ${e.fontFamily}; + } + + .node-labels { + font-family: ${e.fontFamily}; + } + + /* Outlined label style - background stroke for better readability */ + .sankey-label-bg { + stroke: ${e.mainBkg||e.background||"#fff"}; + stroke-width: 4px; + stroke-linejoin: round; + paint-order: stroke; + } + + /* Foreground label text */ + .sankey-label-fg { + fill: ${e.textColor}; + } + + /* Node styling */ + .node rect { + shape-rendering: crispEdges; + } + + /* Link styling */ + .link { + fill: none; + stroke-opacity: 0.5; + mix-blend-mode: multiply; + } +`,"getStyles"),bPe=PLt});var CPe={};ir(CPe,{diagram:()=>BLt});var OLt,BLt,wPe=F(()=>{"use strict";qNe();UNe();yPe();xPe();TPe();OLt=Yw.parse.bind(Yw);Yw.parse=e=>OLt(vPe(e));BLt={styles:bPe,parser:Yw,db:HNe,renderer:gPe}});var GLt,dx,bj=F(()=>{"use strict";ur();Wi();Qt();Nn();GLt=cr.packet,dx=class{constructor(){this.packet=[];this.setAccTitle=kr;this.getAccTitle=Ar;this.setDiagramTitle=Or;this.getDiagramTitle=Lr;this.getAccDescription=_r;this.setAccDescription=Rr}static{o(this,"PacketDB")}getConfig(){let t=qr({...GLt,..._t().packet});return t.showBits&&(t.paddingY+=10),t}getPacket(){return this.packet}pushWord(t){t.length>0&&this.packet.push(t)}clear(){yr(),this.packet=[]}}});var VLt,WLt,qLt,Tj,EPe=F(()=>{"use strict";Xa();vt();Hs();bj();VLt=1e4,WLt=o((e,t)=>{Gn(e,t);let r=-1,n=[],i=1,{bitsPerRow:a}=t.getConfig();for(let{start:s,end:l,bits:u,label:h}of e.blocks){if(s!==void 0&&l!==void 0&&l{if(e.start===void 0)throw new Error("start should have been set during first phase");if(e.end===void 0)throw new Error("end should have been set during first phase");if(e.start>e.end)throw new Error(`Block start ${e.start} is greater than block end ${e.end}.`);if(e.end+1<=t*r)return[e,void 0];let n=t*r-1,i=t*r;return[{start:e.start,end:n,label:e.label,bits:n-e.start},{start:i,end:e.end,label:e.label,bits:e.end-i}]},"getNextFittingBlock"),Tj={parser:{yy:void 0},parse:o(async e=>{let t=await Si("packet",e),r=Tj.parser?.yy;if(!(r instanceof dx))throw new Error("parser.parser?.yy was not a PacketDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.");Z.debug(t),WLt(t,r)},"parse")}});var HLt,ULt,APe,RPe=F(()=>{"use strict";Ka();$n();HLt=o((e,t,r,n)=>{let i=n.db,a=i.getConfig(),{rowHeight:s,paddingY:l,bitWidth:u,bitsPerRow:h}=a,d=i.getPacket(),f=i.getDiagramTitle(),p=s+l,m=p*(d.length+1)-(f?0:s),g=u*h+2,y=xn(t);y.attr("viewBox",`0 0 ${g} ${m}`),Wr(y,m,g,a.useMaxWidth);for(let[v,x]of d.entries())ULt(y,x,v,a);y.append("text").text(f).attr("x",g/2).attr("y",m-p/2).attr("dominant-baseline","middle").attr("text-anchor","middle").attr("class","packetTitle")},"draw"),ULt=o((e,t,r,{rowHeight:n,paddingX:i,paddingY:a,bitWidth:s,bitsPerRow:l,showBits:u})=>{let h=e.append("g"),d=r*(n+a)+a;for(let f of t){let p=f.start%l*s+1,m=(f.end-f.start+1)*s-i;if(h.append("rect").attr("x",p).attr("y",d).attr("width",m).attr("height",n).attr("class","packetBlock"),h.append("text").attr("x",p+m/2).attr("y",d+n/2).attr("class","packetLabel").attr("dominant-baseline","middle").attr("text-anchor","middle").text(f.label),!u)continue;let g=f.end===f.start,y=d-2;h.append("text").attr("x",p+(g?m/2:0)).attr("y",y).attr("class","packetByte start").attr("dominant-baseline","auto").attr("text-anchor",g?"middle":"start").text(f.start),g||h.append("text").attr("x",p+m).attr("y",y).attr("class","packetByte end").attr("dominant-baseline","auto").attr("text-anchor","end").text(f.end)}},"drawWord"),APe={draw:HLt}});var YLt,_Pe,LPe=F(()=>{"use strict";Qt();YLt={byteFontSize:"10px",startByteColor:"black",endByteColor:"black",labelColor:"black",labelFontSize:"12px",titleColor:"black",titleFontSize:"14px",blockStrokeColor:"black",blockStrokeWidth:"1",blockFillColor:"#efefef"},_Pe=o(({packet:e}={})=>{let t=qr(YLt,e);return` + .packetByte { + font-size: ${t.byteFontSize}; + } + .packetByte.start { + fill: ${t.startByteColor}; + } + .packetByte.end { + fill: ${t.endByteColor}; + } + .packetLabel { + fill: ${t.labelColor}; + font-size: ${t.labelFontSize}; + } + .packetTitle { + fill: ${t.titleColor}; + font-size: ${t.titleFontSize}; + } + .packetBlock { + stroke: ${t.blockStrokeColor}; + stroke-width: ${t.blockStrokeWidth}; + fill: ${t.blockFillColor}; + } + `},"styles")});var DPe={};ir(DPe,{diagram:()=>jLt});var jLt,IPe=F(()=>{"use strict";bj();EPe();RPe();LPe();jLt={parser:Tj,get db(){return new dx},renderer:APe,styles:_Pe}});var fx,QL,PPe,Uu,ZLt,QLt,OPe,JLt,eDt,tDt,rDt,nDt,iDt,aDt,I0,Cj=F(()=>{"use strict";ur();Wi();Qt();Nn();vt();fx={showLegend:!0,ticks:5,max:null,min:0,graticule:"circle"},QL=32,PPe={axes:[],curves:[],options:fx},Uu=structuredClone(PPe),ZLt=cr.radar,QLt=o(()=>qr({...ZLt,..._t().radar}),"getConfig"),OPe=o(()=>Uu.axes,"getAxes"),JLt=o(()=>Uu.curves,"getCurves"),eDt=o(()=>Uu.options,"getOptions"),tDt=o(e=>{Uu.axes=e.map(t=>({name:t.name,label:t.label??t.name}))},"setAxes"),rDt=o(e=>{Uu.curves=e.map(t=>({name:t.name,label:t.label??t.name,entries:nDt(t.entries)}))},"setCurves"),nDt=o(e=>{if(e[0].axis==null)return e.map(r=>r.value);let t=OPe();if(t.length===0)throw new Error("Axes must be populated before curves for reference entries");return t.map(r=>{let n=e.find(i=>i.axis?.$refText===r.name);if(n===void 0)throw new Error("Missing entry for axis "+r.label);return n.value})},"computeCurveEntries"),iDt=o(e=>{let t=e.reduce((r,n)=>(r[n.name]=n,r),{});Uu.options={showLegend:t.showLegend?.value??fx.showLegend,ticks:t.ticks?.value??fx.ticks,max:t.max?.value??fx.max,min:t.min?.value??fx.min,graticule:t.graticule?.value??fx.graticule},Uu.options.ticks>QL&&(Z.warn(`Radar diagram ticks (${Uu.options.ticks}) exceeds maximum allowed (${QL}). Using ${QL} instead.`),Uu.options.ticks=QL)},"setOptions"),aDt=o(()=>{yr(),Uu=structuredClone(PPe)},"clear"),I0={getAxes:OPe,getCurves:JLt,getOptions:eDt,setAxes:tDt,setCurves:rDt,setOptions:iDt,getConfig:QLt,clear:aDt,setAccTitle:kr,getAccTitle:Ar,setDiagramTitle:Or,getDiagramTitle:Lr,getAccDescription:_r,setAccDescription:Rr}});var sDt,BPe,$Pe=F(()=>{"use strict";Xa();vt();Hs();Cj();sDt=o(e=>{Gn(e,I0);let{axes:t,curves:r,options:n}=e;I0.setAxes(t),I0.setCurves(r),I0.setOptions(n)},"populate"),BPe={parse:o(async e=>{let t=await Si("radar",e);Z.debug(t),sDt(t)},"parse")}});function hDt(e,t,r,n,i,a,s){let l=t.length,u=Math.min(s.width,s.height)/2;r.forEach((h,d)=>{if(h.entries.length!==l)return;let f=h.entries.map((p,m)=>{let g=2*Math.PI*m/l-Math.PI/2,y=dDt(p,n,i,u),v=y*Math.cos(g),x=y*Math.sin(g);return{x:v,y:x}});a==="circle"?e.append("path").attr("d",fDt(f,s.curveTension)).attr("class",`radarCurve-${d}`):a==="polygon"&&e.append("polygon").attr("points",f.map(p=>`${p.x},${p.y}`).join(" ")).attr("class",`radarCurve-${d}`)})}function dDt(e,t,r,n){let i=Math.min(Math.max(e,t),r);return n*(i-t)/(r-t)}function fDt(e,t){let r=e.length,n=`M${e[0].x},${e[0].y}`;for(let i=0;i{let h=e.append("g").attr("transform",`translate(${i}, ${a+u*s})`);h.append("rect").attr("width",12).attr("height",12).attr("class",`radarLegendBox-${u}`),h.append("text").attr("x",16).attr("y",0).attr("class","radarLegendText").text(l.label)})}var oDt,lDt,cDt,uDt,FPe,zPe=F(()=>{"use strict";Ka();$n();oDt=o((e,t,r,n)=>{let i=n.db,a=i.getAxes(),s=i.getCurves(),l=i.getOptions(),u=i.getConfig(),h=i.getDiagramTitle(),d=xn(t),f=lDt(d,u),p=l.max??Math.max(...s.map(y=>Math.max(...y.entries))),m=l.min,g=Math.min(u.width,u.height)/2;cDt(f,a,g,l.ticks,l.graticule),uDt(f,a,g,u),hDt(f,a,s,m,p,l.graticule,u),pDt(f,s,l.showLegend,u),f.append("text").attr("class","radarTitle").text(h).attr("x",0).attr("y",-u.height/2-u.marginTop)},"draw"),lDt=o((e,t)=>{let r=t.width+t.marginLeft+t.marginRight,n=t.height+t.marginTop+t.marginBottom,i={x:t.marginLeft+t.width/2,y:t.marginTop+t.height/2};return Wr(e,n,r,t.useMaxWidth??!0),e.attr("viewBox",`0 0 ${r} ${n}`).attr("overflow","visible"),e.append("g").attr("transform",`translate(${i.x}, ${i.y})`)},"drawFrame"),cDt=o((e,t,r,n,i)=>{if(i==="circle")for(let a=0;a{let f=2*d*Math.PI/a-Math.PI/2,p=l*Math.cos(f),m=l*Math.sin(f);return`${p},${m}`}).join(" ");e.append("polygon").attr("points",u).attr("class","radarGraticule")}}},"drawGraticule"),uDt=o((e,t,r,n)=>{let i=t.length;for(let a=0;a.01?"start":u<-.01?"end":"middle",f=h>.01?"hanging":h<-.01?"auto":"central",p=4;e.append("text").text(s).attr("x",r*n.axisLabelFactor*u+p*u).attr("y",r*n.axisLabelFactor*h+p*h).attr("text-anchor",d).attr("dominant-baseline",f).attr("class","radarAxisLabel")}},"drawAxes");o(hDt,"drawCurves");o(dDt,"relativeRadius");o(fDt,"closedRoundCurve");o(pDt,"drawLegend");FPe={draw:oDt}});var mDt,gDt,GPe,VPe=F(()=>{"use strict";Qt();Pc();ur();mDt=o((e,t)=>{let r="";for(let n=0;n{let t=ma(),r=_t(),n=qr(t,r.themeVariables),i=qr(n.radar,e);return{themeVariables:n,radarOptions:i}},"buildRadarStyleOptions"),GPe=o(({radar:e}={})=>{let{themeVariables:t,radarOptions:r}=gDt(e);return` + .radarTitle { + font-size: ${t.fontSize}; + color: ${t.titleColor}; + dominant-baseline: hanging; + text-anchor: middle; + } + .radarAxisLine { + stroke: ${r.axisColor}; + stroke-width: ${r.axisStrokeWidth}; + } + .radarAxisLabel { + font-size: ${r.axisLabelFontSize}px; + color: ${r.axisColor}; + } + .radarGraticule { + fill: ${r.graticuleColor}; + fill-opacity: ${r.graticuleOpacity}; + stroke: ${r.graticuleColor}; + stroke-width: ${r.graticuleStrokeWidth}; + } + .radarLegendText { + text-anchor: start; + font-size: ${r.legendFontSize}px; + dominant-baseline: hanging; + } + ${mDt(t,r)} + `},"styles")});var WPe={};ir(WPe,{diagram:()=>yDt});var yDt,qPe=F(()=>{"use strict";Cj();$Pe();zPe();VPe();yDt={parser:BPe,db:I0,renderer:FPe,styles:GPe}});var wj,YPe,jPe=F(()=>{"use strict";wj=(function(){var e=o(function(T,k,C,w){for(C=C||{},w=T.length;w--;C[T[w]]=k);return C},"o"),t=[1,15],r=[1,7],n=[1,13],i=[1,14],a=[1,19],s=[1,16],l=[1,17],u=[1,18],h=[8,30],d=[8,10,21,28,29,30,31,39,43,46],f=[1,23],p=[1,24],m=[8,10,15,16,21,28,29,30,31,39,43,46],g=[8,10,15,16,21,27,28,29,30,31,39,43,46],y=[1,49],v={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,spaceLines:3,SPACELINE:4,NL:5,separator:6,SPACE:7,EOF:8,start:9,BLOCK_DIAGRAM_KEY:10,document:11,stop:12,statement:13,link:14,LINK:15,START_LINK:16,LINK_LABEL:17,STR:18,nodeStatement:19,columnsStatement:20,SPACE_BLOCK:21,blockStatement:22,classDefStatement:23,cssClassStatement:24,styleStatement:25,node:26,SIZE:27,COLUMNS:28,"id-block":29,end:30,NODE_ID:31,nodeShapeNLabel:32,dirList:33,DIR:34,NODE_DSTART:35,NODE_DEND:36,BLOCK_ARROW_START:37,BLOCK_ARROW_END:38,classDef:39,CLASSDEF_ID:40,CLASSDEF_STYLEOPTS:41,DEFAULT:42,class:43,CLASSENTITY_IDS:44,STYLECLASS:45,style:46,STYLE_ENTITY_IDS:47,STYLE_DEFINITION_DATA:48,$accept:0,$end:1},terminals_:{2:"error",4:"SPACELINE",5:"NL",7:"SPACE",8:"EOF",10:"BLOCK_DIAGRAM_KEY",15:"LINK",16:"START_LINK",17:"LINK_LABEL",18:"STR",21:"SPACE_BLOCK",27:"SIZE",28:"COLUMNS",29:"id-block",30:"end",31:"NODE_ID",34:"DIR",35:"NODE_DSTART",36:"NODE_DEND",37:"BLOCK_ARROW_START",38:"BLOCK_ARROW_END",39:"classDef",40:"CLASSDEF_ID",41:"CLASSDEF_STYLEOPTS",42:"DEFAULT",43:"class",44:"CLASSENTITY_IDS",45:"STYLECLASS",46:"style",47:"STYLE_ENTITY_IDS",48:"STYLE_DEFINITION_DATA"},productions_:[0,[3,1],[3,2],[3,2],[6,1],[6,1],[6,1],[9,3],[12,1],[12,1],[12,2],[12,2],[11,1],[11,2],[14,1],[14,4],[13,1],[13,1],[13,1],[13,1],[13,1],[13,1],[13,1],[19,3],[19,2],[19,1],[20,1],[22,4],[22,3],[26,1],[26,2],[33,1],[33,2],[32,3],[32,4],[23,3],[23,3],[24,3],[25,3]],performAction:o(function(k,C,w,S,R,L,N){var I=L.length-1;switch(R){case 4:S.getLogger().debug("Rule: separator (NL) ");break;case 5:S.getLogger().debug("Rule: separator (Space) ");break;case 6:S.getLogger().debug("Rule: separator (EOF) ");break;case 7:S.getLogger().debug("Rule: hierarchy: ",L[I-1]),S.setHierarchy(L[I-1]);break;case 8:S.getLogger().debug("Stop NL ");break;case 9:S.getLogger().debug("Stop EOF ");break;case 10:S.getLogger().debug("Stop NL2 ");break;case 11:S.getLogger().debug("Stop EOF2 ");break;case 12:S.getLogger().debug("Rule: statement: ",L[I]),typeof L[I].length=="number"?this.$=L[I]:this.$=[L[I]];break;case 13:S.getLogger().debug("Rule: statement #2: ",L[I-1]),this.$=[L[I-1]].concat(L[I]);break;case 14:S.getLogger().debug("Rule: link: ",L[I],k),this.$={edgeTypeStr:L[I],label:""};break;case 15:S.getLogger().debug("Rule: LABEL link: ",L[I-3],L[I-1],L[I]),this.$={edgeTypeStr:L[I],label:L[I-1]};break;case 18:let _=parseInt(L[I]),A=S.generateId();this.$={id:A,type:"space",label:"",width:_,children:[]};break;case 23:S.getLogger().debug("Rule: (nodeStatement link node) ",L[I-2],L[I-1],L[I]," typestr: ",L[I-1].edgeTypeStr);let M=S.edgeStrToEdgeData(L[I-1].edgeTypeStr),D=S.edgeStrToEdgeStartData(L[I-1].edgeTypeStr),P=S.edgeStrToThickness(L[I-1].edgeTypeStr),B=S.edgeStrToPattern(L[I-1].edgeTypeStr);this.$=[{id:L[I-2].id,label:L[I-2].label,type:L[I-2].type,directions:L[I-2].directions},{id:L[I-2].id+"-"+L[I].id,start:L[I-2].id,end:L[I].id,label:L[I-1].label,type:"edge",thickness:P,pattern:B,directions:L[I].directions,arrowTypeEnd:M,arrowTypeStart:D},{id:L[I].id,label:L[I].label,type:S.typeStr2Type(L[I].typeStr),directions:L[I].directions}];break;case 24:S.getLogger().debug("Rule: nodeStatement (abc88 node size) ",L[I-1],L[I]),this.$={id:L[I-1].id,label:L[I-1].label,type:S.typeStr2Type(L[I-1].typeStr),directions:L[I-1].directions,widthInColumns:parseInt(L[I],10)};break;case 25:S.getLogger().debug("Rule: nodeStatement (node) ",L[I]),this.$={id:L[I].id,label:L[I].label,type:S.typeStr2Type(L[I].typeStr),directions:L[I].directions,widthInColumns:1};break;case 26:S.getLogger().debug("APA123",this?this:"na"),S.getLogger().debug("COLUMNS: ",L[I]),this.$={type:"column-setting",columns:L[I]==="auto"?-1:parseInt(L[I])};break;case 27:S.getLogger().debug("Rule: id-block statement : ",L[I-2],L[I-1]);let O=S.generateId();this.$={...L[I-2],type:"composite",children:L[I-1]};break;case 28:S.getLogger().debug("Rule: blockStatement : ",L[I-2],L[I-1],L[I]);let $=S.generateId();this.$={id:$,type:"composite",label:"",children:L[I-1]};break;case 29:S.getLogger().debug("Rule: node (NODE_ID separator): ",L[I]),this.$={id:L[I]};break;case 30:S.getLogger().debug("Rule: node (NODE_ID nodeShapeNLabel separator): ",L[I-1],L[I]),this.$={id:L[I-1],label:L[I].label,typeStr:L[I].typeStr,directions:L[I].directions};break;case 31:S.getLogger().debug("Rule: dirList: ",L[I]),this.$=[L[I]];break;case 32:S.getLogger().debug("Rule: dirList: ",L[I-1],L[I]),this.$=[L[I-1]].concat(L[I]);break;case 33:S.getLogger().debug("Rule: nodeShapeNLabel: ",L[I-2],L[I-1],L[I]),this.$={typeStr:L[I-2]+L[I],label:L[I-1]};break;case 34:S.getLogger().debug("Rule: BLOCK_ARROW nodeShapeNLabel: ",L[I-3],L[I-2]," #3:",L[I-1],L[I]),this.$={typeStr:L[I-3]+L[I],label:L[I-2],directions:L[I-1]};break;case 35:case 36:this.$={type:"classDef",id:L[I-1].trim(),css:L[I].trim()};break;case 37:this.$={type:"applyClass",id:L[I-1].trim(),styleClass:L[I].trim()};break;case 38:this.$={type:"applyStyles",id:L[I-1].trim(),stylesStr:L[I].trim()};break}},"anonymous"),table:[{9:1,10:[1,2]},{1:[3]},{10:t,11:3,13:4,19:5,20:6,21:r,22:8,23:9,24:10,25:11,26:12,28:n,29:i,31:a,39:s,43:l,46:u},{8:[1,20]},e(h,[2,12],{13:4,19:5,20:6,22:8,23:9,24:10,25:11,26:12,11:21,10:t,21:r,28:n,29:i,31:a,39:s,43:l,46:u}),e(d,[2,16],{14:22,15:f,16:p}),e(d,[2,17]),e(d,[2,18]),e(d,[2,19]),e(d,[2,20]),e(d,[2,21]),e(d,[2,22]),e(m,[2,25],{27:[1,25]}),e(d,[2,26]),{19:26,26:12,31:a},{10:t,11:27,13:4,19:5,20:6,21:r,22:8,23:9,24:10,25:11,26:12,28:n,29:i,31:a,39:s,43:l,46:u},{40:[1,28],42:[1,29]},{44:[1,30]},{47:[1,31]},e(g,[2,29],{32:32,35:[1,33],37:[1,34]}),{1:[2,7]},e(h,[2,13]),{26:35,31:a},{31:[2,14]},{17:[1,36]},e(m,[2,24]),{10:t,11:37,13:4,14:22,15:f,16:p,19:5,20:6,21:r,22:8,23:9,24:10,25:11,26:12,28:n,29:i,31:a,39:s,43:l,46:u},{30:[1,38]},{41:[1,39]},{41:[1,40]},{45:[1,41]},{48:[1,42]},e(g,[2,30]),{18:[1,43]},{18:[1,44]},e(m,[2,23]),{18:[1,45]},{30:[1,46]},e(d,[2,28]),e(d,[2,35]),e(d,[2,36]),e(d,[2,37]),e(d,[2,38]),{36:[1,47]},{33:48,34:y},{15:[1,50]},e(d,[2,27]),e(g,[2,33]),{38:[1,51]},{33:52,34:y,38:[2,31]},{31:[2,15]},e(g,[2,34]),{38:[2,32]}],defaultActions:{20:[2,7],23:[2,14],50:[2,15],52:[2,32]},parseError:o(function(k,C){if(C.recoverable)this.trace(k);else{var w=new Error(k);throw w.hash=C,w}},"parseError"),parse:o(function(k){var C=this,w=[0],S=[],R=[null],L=[],N=this.table,I="",_=0,A=0,M=0,D=2,P=1,B=L.slice.call(arguments,1),O=Object.create(this.lexer),$={yy:{}};for(var V in this.yy)Object.prototype.hasOwnProperty.call(this.yy,V)&&($.yy[V]=this.yy[V]);O.setInput(k,$.yy),$.yy.lexer=O,$.yy.parser=this,typeof O.yylloc>"u"&&(O.yylloc={});var G=O.yylloc;L.push(G);var z=O.options&&O.options.ranges;typeof $.yy.parseError=="function"?this.parseError=$.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function W(Oe){w.length=w.length-2*Oe,R.length=R.length-Oe,L.length=L.length-Oe}o(W,"popStack");function H(){var Oe;return Oe=S.pop()||O.lex()||P,typeof Oe!="number"&&(Oe instanceof Array&&(S=Oe,Oe=S.pop()),Oe=C.symbols_[Oe]||Oe),Oe}o(H,"lex");for(var j,Q,U,oe,te,le,ie={},ae,Re,be,Pe;;){if(U=w[w.length-1],this.defaultActions[U]?oe=this.defaultActions[U]:((j===null||typeof j>"u")&&(j=H()),oe=N[U]&&N[U][j]),typeof oe>"u"||!oe.length||!oe[0]){var Ge="";Pe=[];for(ae in N[U])this.terminals_[ae]&&ae>D&&Pe.push("'"+this.terminals_[ae]+"'");O.showPosition?Ge="Parse error on line "+(_+1)+`: +`+O.showPosition()+` +Expecting `+Pe.join(", ")+", got '"+(this.terminals_[j]||j)+"'":Ge="Parse error on line "+(_+1)+": Unexpected "+(j==P?"end of input":"'"+(this.terminals_[j]||j)+"'"),this.parseError(Ge,{text:O.match,token:this.terminals_[j]||j,line:O.yylineno,loc:G,expected:Pe})}if(oe[0]instanceof Array&&oe.length>1)throw new Error("Parse Error: multiple actions possible at state: "+U+", token: "+j);switch(oe[0]){case 1:w.push(j),R.push(O.yytext),L.push(O.yylloc),w.push(oe[1]),j=null,Q?(j=Q,Q=null):(A=O.yyleng,I=O.yytext,_=O.yylineno,G=O.yylloc,M>0&&M--);break;case 2:if(Re=this.productions_[oe[1]][1],ie.$=R[R.length-Re],ie._$={first_line:L[L.length-(Re||1)].first_line,last_line:L[L.length-1].last_line,first_column:L[L.length-(Re||1)].first_column,last_column:L[L.length-1].last_column},z&&(ie._$.range=[L[L.length-(Re||1)].range[0],L[L.length-1].range[1]]),le=this.performAction.apply(ie,[I,A,_,$.yy,oe[1],R,L].concat(B)),typeof le<"u")return le;Re&&(w=w.slice(0,-1*Re*2),R=R.slice(0,-1*Re),L=L.slice(0,-1*Re)),w.push(this.productions_[oe[1]][0]),R.push(ie.$),L.push(ie._$),be=N[w[w.length-2]][w[w.length-1]],w.push(be);break;case 3:return!0}}return!0},"parse")},x=(function(){var T={EOF:1,parseError:o(function(C,w){if(this.yy.parser)this.yy.parser.parseError(C,w);else throw new Error(C)},"parseError"),setInput:o(function(k,C){return this.yy=C||this.yy||{},this._input=k,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var k=this._input[0];this.yytext+=k,this.yyleng++,this.offset++,this.match+=k,this.matched+=k;var C=k.match(/(?:\r\n?|\n).*/g);return C?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),k},"input"),unput:o(function(k){var C=k.length,w=k.split(/(?:\r\n?|\n)/g);this._input=k+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-C),this.offset-=C;var S=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),w.length-1&&(this.yylineno-=w.length-1);var R=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:w?(w.length===S.length?this.yylloc.first_column:0)+S[S.length-w.length].length-w[0].length:this.yylloc.first_column-C},this.options.ranges&&(this.yylloc.range=[R[0],R[0]+this.yyleng-C]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(k){this.unput(this.match.slice(k))},"less"),pastInput:o(function(){var k=this.matched.substr(0,this.matched.length-this.match.length);return(k.length>20?"...":"")+k.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var k=this.match;return k.length<20&&(k+=this._input.substr(0,20-k.length)),(k.substr(0,20)+(k.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var k=this.pastInput(),C=new Array(k.length+1).join("-");return k+this.upcomingInput()+` +`+C+"^"},"showPosition"),test_match:o(function(k,C){var w,S,R;if(this.options.backtrack_lexer&&(R={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(R.yylloc.range=this.yylloc.range.slice(0))),S=k[0].match(/(?:\r\n?|\n).*/g),S&&(this.yylineno+=S.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:S?S[S.length-1].length-S[S.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+k[0].length},this.yytext+=k[0],this.match+=k[0],this.matches=k,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(k[0].length),this.matched+=k[0],w=this.performAction.call(this,this.yy,this,C,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),w)return w;if(this._backtrack){for(var L in R)this[L]=R[L];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var k,C,w,S;this._more||(this.yytext="",this.match="");for(var R=this._currentRules(),L=0;LC[0].length)){if(C=w,S=L,this.options.backtrack_lexer){if(k=this.test_match(w,R[L]),k!==!1)return k;if(this._backtrack){C=!1;continue}else return!1}else if(!this.options.flex)break}return C?(k=this.test_match(C,R[S]),k!==!1?k:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var C=this.next();return C||this.lex()},"lex"),begin:o(function(C){this.conditionStack.push(C)},"begin"),popState:o(function(){var C=this.conditionStack.length-1;return C>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(C){return C=this.conditionStack.length-1-Math.abs(C||0),C>=0?this.conditionStack[C]:"INITIAL"},"topState"),pushState:o(function(C){this.begin(C)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{},performAction:o(function(C,w,S,R){var L=R;switch(S){case 0:return C.getLogger().debug("Found block-beta"),10;break;case 1:return C.getLogger().debug("Found id-block"),29;break;case 2:return C.getLogger().debug("Found block"),10;break;case 3:C.getLogger().debug(".",w.yytext);break;case 4:C.getLogger().debug("_",w.yytext);break;case 5:return 5;case 6:return w.yytext=-1,28;break;case 7:return w.yytext=w.yytext.replace(/columns\s+/,""),C.getLogger().debug("COLUMNS (LEX)",w.yytext),28;break;case 8:this.pushState("md_string");break;case 9:return"MD_STR";case 10:this.popState();break;case 11:this.pushState("string");break;case 12:C.getLogger().debug("LEX: POPPING STR:",w.yytext),this.popState();break;case 13:return C.getLogger().debug("LEX: STR end:",w.yytext),"STR";break;case 14:return w.yytext=w.yytext.replace(/space\:/,""),C.getLogger().debug("SPACE NUM (LEX)",w.yytext),21;break;case 15:return w.yytext="1",C.getLogger().debug("COLUMNS (LEX)",w.yytext),21;break;case 16:return 42;case 17:return"LINKSTYLE";case 18:return"INTERPOLATE";case 19:return this.pushState("CLASSDEF"),39;break;case 20:return this.popState(),this.pushState("CLASSDEFID"),"DEFAULT_CLASSDEF_ID";break;case 21:return this.popState(),this.pushState("CLASSDEFID"),40;break;case 22:return this.popState(),41;break;case 23:return this.pushState("CLASS"),43;break;case 24:return this.popState(),this.pushState("CLASS_STYLE"),44;break;case 25:return this.popState(),45;break;case 26:return this.pushState("STYLE_STMNT"),46;break;case 27:return this.popState(),this.pushState("STYLE_DEFINITION"),47;break;case 28:return this.popState(),48;break;case 29:return this.pushState("acc_title"),"acc_title";break;case 30:return this.popState(),"acc_title_value";break;case 31:return this.pushState("acc_descr"),"acc_descr";break;case 32:return this.popState(),"acc_descr_value";break;case 33:this.pushState("acc_descr_multiline");break;case 34:this.popState();break;case 35:return"acc_descr_multiline_value";case 36:return 30;case 37:return this.popState(),C.getLogger().debug("Lex: (("),"NODE_DEND";break;case 38:return this.popState(),C.getLogger().debug("Lex: (("),"NODE_DEND";break;case 39:return this.popState(),C.getLogger().debug("Lex: ))"),"NODE_DEND";break;case 40:return this.popState(),C.getLogger().debug("Lex: (("),"NODE_DEND";break;case 41:return this.popState(),C.getLogger().debug("Lex: (("),"NODE_DEND";break;case 42:return this.popState(),C.getLogger().debug("Lex: (-"),"NODE_DEND";break;case 43:return this.popState(),C.getLogger().debug("Lex: -)"),"NODE_DEND";break;case 44:return this.popState(),C.getLogger().debug("Lex: (("),"NODE_DEND";break;case 45:return this.popState(),C.getLogger().debug("Lex: ]]"),"NODE_DEND";break;case 46:return this.popState(),C.getLogger().debug("Lex: ("),"NODE_DEND";break;case 47:return this.popState(),C.getLogger().debug("Lex: ])"),"NODE_DEND";break;case 48:return this.popState(),C.getLogger().debug("Lex: /]"),"NODE_DEND";break;case 49:return this.popState(),C.getLogger().debug("Lex: /]"),"NODE_DEND";break;case 50:return this.popState(),C.getLogger().debug("Lex: )]"),"NODE_DEND";break;case 51:return this.popState(),C.getLogger().debug("Lex: )"),"NODE_DEND";break;case 52:return this.popState(),C.getLogger().debug("Lex: ]>"),"NODE_DEND";break;case 53:return this.popState(),C.getLogger().debug("Lex: ]"),"NODE_DEND";break;case 54:return C.getLogger().debug("Lexa: -)"),this.pushState("NODE"),35;break;case 55:return C.getLogger().debug("Lexa: (-"),this.pushState("NODE"),35;break;case 56:return C.getLogger().debug("Lexa: ))"),this.pushState("NODE"),35;break;case 57:return C.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;break;case 58:return C.getLogger().debug("Lex: ((("),this.pushState("NODE"),35;break;case 59:return C.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;break;case 60:return C.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;break;case 61:return C.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;break;case 62:return C.getLogger().debug("Lexc: >"),this.pushState("NODE"),35;break;case 63:return C.getLogger().debug("Lexa: (["),this.pushState("NODE"),35;break;case 64:return C.getLogger().debug("Lexa: )"),this.pushState("NODE"),35;break;case 65:return this.pushState("NODE"),35;break;case 66:return this.pushState("NODE"),35;break;case 67:return this.pushState("NODE"),35;break;case 68:return this.pushState("NODE"),35;break;case 69:return this.pushState("NODE"),35;break;case 70:return this.pushState("NODE"),35;break;case 71:return this.pushState("NODE"),35;break;case 72:return C.getLogger().debug("Lexa: ["),this.pushState("NODE"),35;break;case 73:return this.pushState("BLOCK_ARROW"),C.getLogger().debug("LEX ARR START"),37;break;case 74:return C.getLogger().debug("Lex: NODE_ID",w.yytext),31;break;case 75:return C.getLogger().debug("Lex: EOF",w.yytext),8;break;case 76:this.pushState("md_string");break;case 77:this.pushState("md_string");break;case 78:return"NODE_DESCR";case 79:this.popState();break;case 80:C.getLogger().debug("Lex: Starting string"),this.pushState("string");break;case 81:C.getLogger().debug("LEX ARR: Starting string"),this.pushState("string");break;case 82:return C.getLogger().debug("LEX: NODE_DESCR:",w.yytext),"NODE_DESCR";break;case 83:C.getLogger().debug("LEX POPPING"),this.popState();break;case 84:C.getLogger().debug("Lex: =>BAE"),this.pushState("ARROW_DIR");break;case 85:return w.yytext=w.yytext.replace(/^,\s*/,""),C.getLogger().debug("Lex (right): dir:",w.yytext),"DIR";break;case 86:return w.yytext=w.yytext.replace(/^,\s*/,""),C.getLogger().debug("Lex (left):",w.yytext),"DIR";break;case 87:return w.yytext=w.yytext.replace(/^,\s*/,""),C.getLogger().debug("Lex (x):",w.yytext),"DIR";break;case 88:return w.yytext=w.yytext.replace(/^,\s*/,""),C.getLogger().debug("Lex (y):",w.yytext),"DIR";break;case 89:return w.yytext=w.yytext.replace(/^,\s*/,""),C.getLogger().debug("Lex (up):",w.yytext),"DIR";break;case 90:return w.yytext=w.yytext.replace(/^,\s*/,""),C.getLogger().debug("Lex (down):",w.yytext),"DIR";break;case 91:return w.yytext="]>",C.getLogger().debug("Lex (ARROW_DIR end):",w.yytext),this.popState(),this.popState(),"BLOCK_ARROW_END";break;case 92:return C.getLogger().debug("Lex: LINK","#"+w.yytext+"#"),15;break;case 93:return C.getLogger().debug("Lex: LINK",w.yytext),15;break;case 94:return C.getLogger().debug("Lex: LINK",w.yytext),15;break;case 95:return C.getLogger().debug("Lex: LINK",w.yytext),15;break;case 96:return C.getLogger().debug("Lex: START_LINK",w.yytext),this.pushState("LLABEL"),16;break;case 97:return C.getLogger().debug("Lex: START_LINK",w.yytext),this.pushState("LLABEL"),16;break;case 98:return C.getLogger().debug("Lex: START_LINK",w.yytext),this.pushState("LLABEL"),16;break;case 99:this.pushState("md_string");break;case 100:return C.getLogger().debug("Lex: Starting string"),this.pushState("string"),"LINK_LABEL";break;case 101:return this.popState(),C.getLogger().debug("Lex: LINK","#"+w.yytext+"#"),15;break;case 102:return this.popState(),C.getLogger().debug("Lex: LINK",w.yytext),15;break;case 103:return this.popState(),C.getLogger().debug("Lex: LINK",w.yytext),15;break;case 104:return C.getLogger().debug("Lex: COLON",w.yytext),w.yytext=w.yytext.slice(1),27;break}},"anonymous"),rules:[/^(?:block-beta\b)/,/^(?:block:)/,/^(?:block\b)/,/^(?:[\s]+)/,/^(?:[\n]+)/,/^(?:((\u000D\u000A)|(\u000A)))/,/^(?:columns\s+auto\b)/,/^(?:columns\s+[\d]+)/,/^(?:["][`])/,/^(?:[^`"]+)/,/^(?:[`]["])/,/^(?:["])/,/^(?:["])/,/^(?:[^"]*)/,/^(?:space[:]\d+)/,/^(?:space\b)/,/^(?:default\b)/,/^(?:linkStyle\b)/,/^(?:interpolate\b)/,/^(?:classDef\s+)/,/^(?:DEFAULT\s+)/,/^(?:\w+\s+)/,/^(?:[^\n]*)/,/^(?:class\s+)/,/^(?:(\w+)+((,\s*\w+)*))/,/^(?:[^\n]*)/,/^(?:style\s+)/,/^(?:(\w+)+((,\s*\w+)*))/,/^(?:[^\n]*)/,/^(?:accTitle\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*:\s*)/,/^(?:(?!\n||)*[^\n]*)/,/^(?:accDescr\s*\{\s*)/,/^(?:[\}])/,/^(?:[^\}]*)/,/^(?:end\b\s*)/,/^(?:\(\(\()/,/^(?:\)\)\))/,/^(?:[\)]\))/,/^(?:\}\})/,/^(?:\})/,/^(?:\(-)/,/^(?:-\))/,/^(?:\(\()/,/^(?:\]\])/,/^(?:\()/,/^(?:\]\))/,/^(?:\\\])/,/^(?:\/\])/,/^(?:\)\])/,/^(?:[\)])/,/^(?:\]>)/,/^(?:[\]])/,/^(?:-\))/,/^(?:\(-)/,/^(?:\)\))/,/^(?:\))/,/^(?:\(\(\()/,/^(?:\(\()/,/^(?:\{\{)/,/^(?:\{)/,/^(?:>)/,/^(?:\(\[)/,/^(?:\()/,/^(?:\[\[)/,/^(?:\[\|)/,/^(?:\[\()/,/^(?:\)\)\))/,/^(?:\[\\)/,/^(?:\[\/)/,/^(?:\[\\)/,/^(?:\[)/,/^(?:<\[)/,/^(?:[^\(\[\n\-\)\{\}\s\<\>:=]+)/,/^(?:$)/,/^(?:["][`])/,/^(?:["][`])/,/^(?:[^`"]+)/,/^(?:[`]["])/,/^(?:["])/,/^(?:["])/,/^(?:[^"]+)/,/^(?:["])/,/^(?:\]>\s*\()/,/^(?:,?\s*right\s*)/,/^(?:,?\s*left\s*)/,/^(?:,?\s*x\s*)/,/^(?:,?\s*y\s*)/,/^(?:,?\s*up\s*)/,/^(?:,?\s*down\s*)/,/^(?:\)\s*)/,/^(?:\s*[xo<]?--+[-xo>]\s*)/,/^(?:\s*[xo<]?==+[=xo>]\s*)/,/^(?:\s*[xo<]?-?\.+-[xo>]?\s*)/,/^(?:\s*~~[\~]+\s*)/,/^(?:\s*[xo<]?--\s*)/,/^(?:\s*[xo<]?==\s*)/,/^(?:\s*[xo<]?-\.\s*)/,/^(?:["][`])/,/^(?:["])/,/^(?:\s*[xo<]?--+[-xo>]\s*)/,/^(?:\s*[xo<]?==+[=xo>]\s*)/,/^(?:\s*[xo<]?-?\.+-[xo>]?\s*)/,/^(?::\d+)/],conditions:{STYLE_DEFINITION:{rules:[28],inclusive:!1},STYLE_STMNT:{rules:[27],inclusive:!1},CLASSDEFID:{rules:[22],inclusive:!1},CLASSDEF:{rules:[20,21],inclusive:!1},CLASS_STYLE:{rules:[25],inclusive:!1},CLASS:{rules:[24],inclusive:!1},LLABEL:{rules:[99,100,101,102,103],inclusive:!1},ARROW_DIR:{rules:[85,86,87,88,89,90,91],inclusive:!1},BLOCK_ARROW:{rules:[76,81,84],inclusive:!1},NODE:{rules:[37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,77,80],inclusive:!1},md_string:{rules:[9,10,78,79],inclusive:!1},space:{rules:[],inclusive:!1},string:{rules:[12,13,82,83],inclusive:!1},acc_descr_multiline:{rules:[34,35],inclusive:!1},acc_descr:{rules:[32],inclusive:!1},acc_title:{rules:[30],inclusive:!1},INITIAL:{rules:[0,1,2,3,4,5,6,7,8,11,14,15,16,17,18,19,23,26,29,31,33,36,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,92,93,94,95,96,97,98,104],inclusive:!0}}};return T})();v.lexer=x;function b(){this.yy={}}return o(b,"Parser"),b.prototype=v,v.Parser=b,new b})();wj.parser=wj;YPe=wj});function EDt(e){switch(Z.debug("typeStr2Type",e),e){case"[]":return"square";case"()":return Z.debug("we have a round"),"round";case"(())":return"circle";case">]":return"rect_left_inv_arrow";case"{}":return"diamond";case"{{}}":return"hexagon";case"([])":return"stadium";case"[[]]":return"subroutine";case"[()]":return"cylinder";case"((()))":return"doublecircle";case"[//]":return"lean_right";case"[\\\\]":return"lean_left";case"[/\\]":return"trapezoid";case"[\\/]":return"inv_trapezoid";case"<[]>":return"block_arrow";default:return"na"}}function ADt(e){switch(Z.debug("typeStr2Type",e),e){case"==":return"thick";default:return"normal"}}function RDt(e){switch(e.trim().slice(-1)){case"x":return"arrow_cross";case"o":return"arrow_circle";case">":return"arrow_point";default:return""}}function _Dt(e){switch(e.trim().charAt(0)){case"x":return"arrow_cross";case"o":return"arrow_circle";case"<":return"arrow_point";default:return"arrow_open"}}function LDt(e){return e.includes("==")?"thick":"normal"}function DDt(e){return e.includes(".-")?"dotted":"solid"}var Dc,Sj,kj,XPe,KPe,bDt,QPe,JL,Ej,TDt,CDt,wDt,kDt,JPe,Aj,Zw,SDt,ZPe,IDt,MDt,NDt,PDt,ODt,BDt,$Dt,FDt,zDt,GDt,VDt,WDt,qDt,eOe,tOe=F(()=>{"use strict";s4();ur();Xt();vt();Vr();Nn();Dc=new Map,Sj=[],kj=new Map,XPe="color",KPe="fill",bDt="bgFill",QPe=",",JL=new Map,Ej="",TDt=o(e=>xt.sanitizeText(e,Ae()),"sanitizeText"),CDt=o(function(e,t=""){let r=JL.get(e);r||(r={id:e,styles:[],textStyles:[]},JL.set(e,r)),t?.split(QPe).forEach(n=>{let i=n.replace(/([^;]*);/,"$1").trim();if(RegExp(XPe).exec(n)){let s=i.replace(KPe,bDt).replace(XPe,KPe);r.textStyles.push(s)}r.styles.push(i)})},"addStyleClass"),wDt=o(function(e,t=""){let r=Dc.get(e);t!=null&&(r.styles=t.split(QPe))},"addStyle2Node"),kDt=o(function(e,t){e.split(",").forEach(function(r){let n=Dc.get(r);if(n===void 0){let i=r.trim();n={id:i,type:"na",children:[]},Dc.set(i,n)}n.classes||(n.classes=[]),n.classes.push(t)})},"setCssClass"),JPe=o((e,t)=>{let r=e.flat(),n=[],a=r.find(s=>s?.type==="column-setting")?.columns??-1;for(let s of r){if(typeof a=="number"&&a>0&&s.type!=="column-setting"&&typeof s.widthInColumns=="number"&&s.widthInColumns>a&&Z.warn(`Block ${s.id} width ${s.widthInColumns} exceeds configured column width ${a}`),s.label&&(s.label=TDt(s.label)),s.type==="classDef"){CDt(s.id,s.css);continue}if(s.type==="applyClass"){kDt(s.id,s?.styleClass??"");continue}if(s.type==="applyStyles"){s?.stylesStr&&wDt(s.id,s?.stylesStr);continue}if(s.type==="column-setting")t.columns=s.columns??-1;else if(s.type==="edge"){let l=(kj.get(s.id)??0)+1;kj.set(s.id,l),s.id=l+"-"+s.id,Sj.push(s)}else{s.label||(s.type==="composite"?s.label="":s.label=s.id);let l=Dc.get(s.id);if(l===void 0?Dc.set(s.id,s):(s.type!=="na"&&(l.type=s.type),s.label!==s.id&&(l.label=s.label)),s.children&&JPe(s.children,s),s.type==="space"){let u=s.width??1;for(let h=0;h{Z.debug("Clear called"),yr(),Zw={id:"root",type:"composite",children:[],columns:-1},Dc=new Map([["root",Zw]]),Aj=[],JL=new Map,Sj=[],kj=new Map,Ej=""},"clear");o(EDt,"typeStr2Type");o(ADt,"edgeTypeStr2Type");o(RDt,"edgeStrToEdgeData");o(_Dt,"edgeStrToEdgeStartData");o(LDt,"edgeStrToThickness");o(DDt,"edgeStrToPattern");ZPe=0,IDt=o(()=>(ZPe++,"id-"+Math.random().toString(36).substr(2,12)+"-"+ZPe),"generateId"),MDt=o(e=>{Zw.children=e,JPe(e,Zw),Aj=Zw.children},"setHierarchy"),NDt=o(e=>{let t=Dc.get(e);return t?t.columns?t.columns:t.children?t.children.length:-1:-1},"getColumns"),PDt=o(()=>[...Dc.values()],"getBlocksFlat"),ODt=o(()=>Aj||[],"getBlocks"),BDt=o(()=>Sj,"getEdges"),$Dt=o(e=>Dc.get(e),"getBlock"),FDt=o(e=>{Dc.set(e.id,e)},"setBlock"),zDt=o(e=>{Ej=e},"setDiagramId"),GDt=o(()=>Ej,"getDiagramId"),VDt=o(()=>Z,"getLogger"),WDt=o(function(){return JL},"getClasses"),qDt={getConfig:o(()=>_t().block,"getConfig"),typeStr2Type:EDt,edgeTypeStr2Type:ADt,edgeStrToEdgeData:RDt,edgeStrToEdgeStartData:_Dt,edgeStrToThickness:LDt,edgeStrToPattern:DDt,getLogger:VDt,getBlocksFlat:PDt,getBlocks:ODt,getEdges:BDt,setHierarchy:MDt,getBlock:$Dt,setBlock:FDt,getColumns:NDt,getClasses:WDt,clear:SDt,generateId:IDt,setDiagramId:zDt,getDiagramId:GDt},eOe=qDt});var Rj,HDt,rOe,nOe=F(()=>{"use strict";zi();X1();Rj=o((e,t)=>{let r=Fp,n=r(e,"r"),i=r(e,"g"),a=r(e,"b");return Oi(n,i,a,t)},"fade"),HDt=o(e=>`.label { + font-family: ${e.fontFamily}; + color: ${e.nodeTextColor||e.textColor}; + } + .cluster-label text { + fill: ${e.titleColor}; + } + .cluster-label span,p { + color: ${e.titleColor}; + } + + + + .label text,span,p { + fill: ${e.nodeTextColor||e.textColor}; + color: ${e.nodeTextColor||e.textColor}; + } + + .node rect, + .node circle, + .node ellipse, + .node polygon, + .node path { + fill: ${e.mainBkg}; + stroke: ${e.nodeBorder}; + stroke-width: 1px; + } + .flowchart-label text { + text-anchor: middle; + } + // .flowchart-label .text-outer-tspan { + // text-anchor: middle; + // } + // .flowchart-label .text-inner-tspan { + // text-anchor: start; + // } + + .node .label { + text-align: center; + } + .node.clickable { + cursor: pointer; + } + + .arrowheadPath { + fill: ${e.arrowheadColor}; + } + + .edgePath .path { + stroke: ${e.lineColor}; + stroke-width: 2.0px; + } + + .flowchart-link { + stroke: ${e.lineColor}; + fill: none; + } + + .edgeLabel { + background-color: ${e.edgeLabelBackground}; + /* + * This is for backward compatibility with existing code that didn't + * add a \`

\` around edge labels. + * + * TODO: We should probably remove this in a future release. + */ + p { + margin: 0; + padding: 0; + display: inline; + } + rect { + opacity: 0.5; + background-color: ${e.edgeLabelBackground}; + fill: ${e.edgeLabelBackground}; + } + text-align: center; + } + + /* For html labels only */ + .labelBkg { + background-color: ${e.edgeLabelBackground}; + } + + .node .cluster { + // fill: ${Rj(e.mainBkg,.5)}; + fill: ${Rj(e.clusterBkg,.5)}; + stroke: ${Rj(e.clusterBorder,.2)}; + box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px; + stroke-width: 1px; + } + + .cluster text { + fill: ${e.titleColor}; + } + + .cluster span,p { + color: ${e.titleColor}; + } + /* .cluster div { + color: ${e.titleColor}; + } */ + + div.mermaidTooltip { + position: absolute; + text-align: center; + max-width: 200px; + padding: 2px; + font-family: ${e.fontFamily}; + font-size: 12px; + background: ${e.tertiaryColor}; + border: 1px solid ${e.border2}; + border-radius: 2px; + pointer-events: none; + z-index: 100; + } + + .flowchartTitleText { + text-anchor: middle; + font-size: 18px; + fill: ${e.textColor}; + } + ${Eu()} +`,"getStyles"),rOe=HDt});var UDt,YDt,jDt,XDt,KDt,ZDt,QDt,JDt,e7t,t7t,r7t,iOe,aOe=F(()=>{"use strict";vt();UDt=o((e,t,r,n)=>{t.forEach(i=>{r7t[i](e,r,n)})},"insertMarkers"),YDt=o((e,t,r)=>{Z.trace("Making markers for ",r),e.append("defs").append("marker").attr("id",r+"_"+t+"-extensionStart").attr("class","marker extension "+t).attr("refX",18).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").append("path").attr("d","M 1,7 L18,13 V 1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-extensionEnd").attr("class","marker extension "+t).attr("refX",1).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").append("path").attr("d","M 1,1 V 13 L18,7 Z")},"extension"),jDt=o((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-compositionStart").attr("class","marker composition "+t).attr("refX",18).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").append("path").attr("d","M 18,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-compositionEnd").attr("class","marker composition "+t).attr("refX",1).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").append("path").attr("d","M 18,7 L9,13 L1,7 L9,1 Z")},"composition"),XDt=o((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-aggregationStart").attr("class","marker aggregation "+t).attr("refX",18).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").append("path").attr("d","M 18,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-aggregationEnd").attr("class","marker aggregation "+t).attr("refX",1).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").append("path").attr("d","M 18,7 L9,13 L1,7 L9,1 Z")},"aggregation"),KDt=o((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-dependencyStart").attr("class","marker dependency "+t).attr("refX",6).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").append("path").attr("d","M 5,7 L9,13 L1,7 L9,1 Z"),e.append("defs").append("marker").attr("id",r+"_"+t+"-dependencyEnd").attr("class","marker dependency "+t).attr("refX",13).attr("refY",7).attr("markerWidth",20).attr("markerHeight",28).attr("orient","auto").append("path").attr("d","M 18,7 L9,13 L14,7 L9,1 Z")},"dependency"),ZDt=o((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-lollipopStart").attr("class","marker lollipop "+t).attr("refX",13).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").append("circle").attr("stroke","black").attr("fill","transparent").attr("cx",7).attr("cy",7).attr("r",6),e.append("defs").append("marker").attr("id",r+"_"+t+"-lollipopEnd").attr("class","marker lollipop "+t).attr("refX",1).attr("refY",7).attr("markerWidth",190).attr("markerHeight",240).attr("orient","auto").append("circle").attr("stroke","black").attr("fill","transparent").attr("cx",7).attr("cy",7).attr("r",6)},"lollipop"),QDt=o((e,t,r)=>{e.append("marker").attr("id",r+"_"+t+"-pointEnd").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refX",6).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",12).attr("markerHeight",12).attr("orient","auto").append("path").attr("d","M 0 0 L 10 5 L 0 10 z").attr("class","arrowMarkerPath").style("stroke-width",1).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-pointStart").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refX",4.5).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",12).attr("markerHeight",12).attr("orient","auto").append("path").attr("d","M 0 5 L 10 10 L 10 0 z").attr("class","arrowMarkerPath").style("stroke-width",1).style("stroke-dasharray","1,0")},"point"),JDt=o((e,t,r)=>{e.append("marker").attr("id",r+"_"+t+"-circleEnd").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refX",11).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",11).attr("markerHeight",11).attr("orient","auto").append("circle").attr("cx","5").attr("cy","5").attr("r","5").attr("class","arrowMarkerPath").style("stroke-width",1).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-circleStart").attr("class","marker "+t).attr("viewBox","0 0 10 10").attr("refX",-1).attr("refY",5).attr("markerUnits","userSpaceOnUse").attr("markerWidth",11).attr("markerHeight",11).attr("orient","auto").append("circle").attr("cx","5").attr("cy","5").attr("r","5").attr("class","arrowMarkerPath").style("stroke-width",1).style("stroke-dasharray","1,0")},"circle"),e7t=o((e,t,r)=>{e.append("marker").attr("id",r+"_"+t+"-crossEnd").attr("class","marker cross "+t).attr("viewBox","0 0 11 11").attr("refX",12).attr("refY",5.2).attr("markerUnits","userSpaceOnUse").attr("markerWidth",11).attr("markerHeight",11).attr("orient","auto").append("path").attr("d","M 1,1 l 9,9 M 10,1 l -9,9").attr("class","arrowMarkerPath").style("stroke-width",2).style("stroke-dasharray","1,0"),e.append("marker").attr("id",r+"_"+t+"-crossStart").attr("class","marker cross "+t).attr("viewBox","0 0 11 11").attr("refX",-1).attr("refY",5.2).attr("markerUnits","userSpaceOnUse").attr("markerWidth",11).attr("markerHeight",11).attr("orient","auto").append("path").attr("d","M 1,1 l 9,9 M 10,1 l -9,9").attr("class","arrowMarkerPath").style("stroke-width",2).style("stroke-dasharray","1,0")},"cross"),t7t=o((e,t,r)=>{e.append("defs").append("marker").attr("id",r+"_"+t+"-barbEnd").attr("refX",19).attr("refY",7).attr("markerWidth",20).attr("markerHeight",14).attr("markerUnits","strokeWidth").attr("orient","auto").append("path").attr("d","M 19,7 L9,13 L14,7 L9,1 Z")},"barb"),r7t={extension:YDt,composition:jDt,aggregation:XDt,dependency:KDt,lollipop:ZDt,point:QDt,circle:JDt,cross:e7t,barb:t7t},iOe=UDt});function sOe(e,t){if(e===0||!Number.isInteger(e))throw new Error("Columns must be an integer !== 0.");if(t<0||!Number.isInteger(t))throw new Error("Position must be a non-negative integer."+t);if(e<0)return{px:t,py:0};if(e===1)return{px:0,py:t};let r=t%e,n=Math.floor(t/e);return{px:r,py:n}}function _j(e,t,r=0,n=0,i=8){Z.debug("setBlockSizes abc95 (start)",e.id,e?.size?.x,"block width =",e?.size,"siblingWidth",r),e?.size?.width||(e.size={width:r,height:n,x:0,y:0});let a=0,s=0;if(e.children?.length>0){for(let g of e.children)_j(g,t,0,0,i);let l=n7t(e);a=l.width,s=l.height,Z.debug("setBlockSizes abc95 maxWidth of",e.id,":s children is ",a,s);for(let g of e.children)g.size&&(Z.debug(`abc95 Setting size of children of ${e.id} id=${g.id} ${a} ${s} ${JSON.stringify(g.size)}`),g.size.width=a*(g.widthInColumns??1)+i*((g.widthInColumns??1)-1),g.size.height=s,g.size.x=0,g.size.y=0,Z.debug(`abc95 updating size of ${e.id} children child:${g.id} maxWidth:${a} maxHeight:${s}`));for(let g of e.children)_j(g,t,a,s,i);let u=e.columns??-1,h=0;for(let g of e.children)h+=g.widthInColumns??1;let d=e.children.length;u>0&&u0?Math.min(e.children.length,u):e.children.length;if(g>0){let y=(p-g*i-i)/g;Z.debug("abc95 (growing to fit) width",e.id,p,e.size?.width,y);for(let v of e.children)v.size&&(v.size.width=y)}}e.size={width:p,height:m,x:0,y:0}}Z.debug("setBlockSizes abc94 (done)",e.id,e?.size?.x,e?.size?.width,e?.size?.y,e?.size?.height)}function oOe(e,t,r=8){Z.debug(`abc85 layout blocks (=>layoutBlocks) ${e.id} x: ${e?.size?.x} y: ${e?.size?.y} width: ${e?.size?.width}`);let n=e.columns??-1;if(Z.debug("layoutBlocks columns abc95",e.id,"=>",n,e),e.children&&e.children.length>0){let i=e?.children[0]?.size?.width??0,a=e.children.length*i+(e.children.length-1)*r;Z.debug("widthOfChildren 88",a,"posX");let s=new Map;{let f=0;for(let p of e.children){if(!p.size)continue;let{py:m}=sOe(n,f),g=s.get(m)??0;p.size.height>g&&s.set(m,p.size.height);let y=p?.widthInColumns??1;n>0&&(y=Math.min(y,n-f%n)),f+=y}}let l=new Map;{let f=0,p=[...s.keys()].sort((m,g)=>m-g);for(let m of p)l.set(m,f),f+=(s.get(m)??0)+r}let u=0;Z.debug("abc91 block?.size?.x",e.id,e?.size?.x);let h=e?.size?.x?e?.size?.x+(-e?.size?.width/2||0):-r,d=0;for(let f of e.children){let p=e;if(!f.size)continue;let{width:m,height:g}=f.size,{px:y,py:v}=sOe(n,u);if(v!=d&&(d=v,h=e?.size?.x?e?.size?.x+(-e?.size?.width/2||0):-r,Z.debug("New row in layout for block",e.id," and child ",f.id,d)),Z.debug(`abc89 layout blocks (child) id: ${f.id} Pos: ${u} (px, py) ${y},${v} (${p?.size?.x},${p?.size?.y}) parent: ${p.id} width: ${m}${r}`),p.size){let b=m/2;f.size.x=h+r+b,Z.debug(`abc91 layout blocks (calc) px, pyid:${f.id} startingPos=X${h} new startingPosX${f.size.x} ${b} padding=${r} width=${m} halfWidth=${b} => x:${f.size.x} y:${f.size.y} ${f.widthInColumns} (width * (child?.w || 1)) / 2 ${m*(f?.widthInColumns??1)/2}`),h=f.size.x+b;let T=l.get(v)??0,k=s.get(v)??g;f.size.y=p.size.y-p.size.height/2+T+k/2+r,Z.debug(`abc88 layout blocks (calc) px, pyid:${f.id}startingPosX${h}${r}${b}=>x:${f.size.x}y:${f.size.y}${f.widthInColumns}(width * (child?.w || 1)) / 2${m*(f?.widthInColumns??1)/2}`)}f.children&&oOe(f,t,r);let x=f?.widthInColumns??1;n>0&&(x=Math.min(x,n-u%n)),u+=x,Z.debug("abc88 columnsPos",f,u)}}Z.debug(`layout blocks (<==layoutBlocks) ${e.id} x: ${e?.size?.x} y: ${e?.size?.y} width: ${e?.size?.width}`)}function lOe(e,{minX:t,minY:r,maxX:n,maxY:i}={minX:0,minY:0,maxX:0,maxY:0}){if(e.size&&e.id!=="root"){let{x:a,y:s,width:l,height:u}=e.size;a-l/2n&&(n=a+l/2),s+u/2>i&&(i=s+u/2)}if(e.children)for(let a of e.children)({minX:t,minY:r,maxX:n,maxY:i}=lOe(a,{minX:t,minY:r,maxX:n,maxY:i}));return{minX:t,minY:r,maxX:n,maxY:i}}function cOe(e){let t=e.getBlock("root");if(!t)return;let r=Ae()?.block?.padding??8;_j(t,e,0,0,r),oOe(t,e,r),Z.debug("getBlocks",JSON.stringify(t,null,2));let{minX:n,minY:i,maxX:a,maxY:s}=lOe(t),l=s-i,u=a-n;return{x:n,y:i,width:u,height:l}}var n7t,uOe=F(()=>{"use strict";vt();Xt();o(sOe,"calculateBlockPosition");n7t=o(e=>{let t=0,r=0;for(let n of e.children){let{width:i,height:a,x:s,y:l}=n.size??{width:0,height:0,x:0,y:0};if(Z.debug("getMaxChildSize abc95 child:",n.id,"width:",i,"height:",a,"x:",s,"y:",l,n.type),n.type==="space")continue;let u=i/(n.widthInColumns??1);u>t&&(t=u),a>r&&(r=a)}return{width:t,height:r}},"getMaxChildSize");o(_j,"setBlockSizes");o(oOe,"layoutBlocks");o(lOe,"findBounds");o(cOe,"layout")});var i7t,Ao,eD=F(()=>{"use strict";ur();Xt();Ls();i7t=o(async(e,t,r,n=!1,i=!1)=>{let a=t||"";typeof a=="object"&&(a=a[0]);let s=Ae(),l=Gr(s);return await Pn(e,a,{style:r,isTitle:n,useHtmlLabels:l,markdown:!1,isNode:i,width:Number.POSITIVE_INFINITY},s)},"createLabel"),Ao=i7t});var dOe,a7t,hOe,fOe=F(()=>{"use strict";vt();dOe=o((e,t,r,n,i)=>{t.arrowTypeStart&&hOe(e,"start",t.arrowTypeStart,r,n,i),t.arrowTypeEnd&&hOe(e,"end",t.arrowTypeEnd,r,n,i)},"addEdgeMarkers"),a7t={arrow_cross:"cross",arrow_point:"point",arrow_barb:"barb",arrow_circle:"circle",aggregation:"aggregation",extension:"extension",composition:"composition",dependency:"dependency",lollipop:"lollipop"},hOe=o((e,t,r,n,i,a)=>{let s=a7t[r];if(!s){Z.warn(`Unknown arrow type: ${r}`);return}let l=t==="start"?"Start":"End";e.attr(`marker-${t}`,`url(${n}#${i}_${a}-${s}${l})`)},"addEdgeMarker")});function tD(e,t){Gr(Ae())&&e&&(e.style.width=t.length*9+"px",e.style.height="12px")}var Lj,ps,mOe,gOe,s7t,o7t,pOe,yOe,vOe=F(()=>{"use strict";vt();eD();Ls();vO();$r();Xt();ur();Qt();Vr();Y4();Vy();fOe();Lj={},ps={},mOe=o(async(e,t)=>{let r=Ae(),n=Gr(r),i=e.insert("g").attr("class","edgeLabel"),a=i.insert("g").attr("class","label"),s=t.labelType==="markdown",l=await Pn(e,t.label,{style:t.labelStyle,useHtmlLabels:n,addSvgBackground:s,isNode:!1,markdown:s,width:s?void 0:Number.POSITIVE_INFINITY},r);a.node().appendChild(l);let u=l.getBBox(),h=u;if(n){let f=l.children[0],p=et(l);u=f.getBoundingClientRect(),h=u,p.attr("width",u.width),p.attr("height",u.height)}else{let f=et(l).select("text").node();f&&typeof f.getBBox=="function"&&(h=f.getBBox())}a.attr("transform",ml(h,n)),Lj[t.id]=i,t.width=u.width,t.height=u.height;let d;if(t.startLabelLeft){let f=e.insert("g").attr("class","edgeTerminals"),p=f.insert("g").attr("class","inner"),m=await Ao(p,t.startLabelLeft,t.labelStyle);d=m;let g=m.getBBox();if(n){let y=m.children[0],v=et(m);g=y.getBoundingClientRect(),v.attr("width",g.width),v.attr("height",g.height)}p.attr("transform",ml(g,n)),ps[t.id]||(ps[t.id]={}),ps[t.id].startLeft=f,tD(d,t.startLabelLeft)}if(t.startLabelRight){let f=e.insert("g").attr("class","edgeTerminals"),p=f.insert("g").attr("class","inner"),m=await Ao(p,t.startLabelRight,t.labelStyle);d=m;let g=m.getBBox();if(n){let y=m.children[0],v=et(m);g=y.getBoundingClientRect(),v.attr("width",g.width),v.attr("height",g.height)}p.attr("transform",ml(g,n)),ps[t.id]||(ps[t.id]={}),ps[t.id].startRight=f,tD(d,t.startLabelRight)}if(t.endLabelLeft){let f=e.insert("g").attr("class","edgeTerminals"),p=f.insert("g").attr("class","inner"),m=await Ao(f,t.endLabelLeft,t.labelStyle);d=m;let g=m.getBBox();if(n){let y=m.children[0],v=et(m);g=y.getBoundingClientRect(),v.attr("width",g.width),v.attr("height",g.height)}p.attr("transform",ml(g,n)),ps[t.id]||(ps[t.id]={}),ps[t.id].endLeft=f,tD(d,t.endLabelLeft)}if(t.endLabelRight){let f=e.insert("g").attr("class","edgeTerminals"),p=f.insert("g").attr("class","inner"),m=await Ao(f,t.endLabelRight,t.labelStyle);d=m;let g=m.getBBox();if(n){let y=m.children[0],v=et(m);g=y.getBoundingClientRect(),v.attr("width",g.width),v.attr("height",g.height)}p.attr("transform",ml(g,n)),ps[t.id]||(ps[t.id]={}),ps[t.id].endRight=f,tD(d,t.endLabelRight)}return l},"insertEdgeLabel");o(tD,"setTerminalWidth");gOe=o((e,t)=>{Z.debug("Moving label abc88 ",e.id,e.label,Lj[e.id],t);let r=t.updatedPath?t.updatedPath:t.originalPath,n=Ae(),{subGraphTitleTotalMargin:i}=oc(n);if(e.label){let a=Lj[e.id],s=e.x,l=e.y;if(r){let u=Zt.calcLabelPosition(r);Z.debug("Moving label "+e.label+" from (",s,",",l,") to (",u.x,",",u.y,") abc88"),t.updatedPath&&(s=u.x,l=u.y)}a.attr("transform",`translate(${s}, ${l+i/2})`)}if(e.startLabelLeft){let a=ps[e.id].startLeft,s=e.x,l=e.y;if(r){let u=Zt.calcTerminalLabelPosition(e.arrowTypeStart?10:0,"start_left",r);s=u.x,l=u.y}a.attr("transform",`translate(${s}, ${l})`)}if(e.startLabelRight){let a=ps[e.id].startRight,s=e.x,l=e.y;if(r){let u=Zt.calcTerminalLabelPosition(e.arrowTypeStart?10:0,"start_right",r);s=u.x,l=u.y}a.attr("transform",`translate(${s}, ${l})`)}if(e.endLabelLeft){let a=ps[e.id].endLeft,s=e.x,l=e.y;if(r){let u=Zt.calcTerminalLabelPosition(e.arrowTypeEnd?10:0,"end_left",r);s=u.x,l=u.y}a.attr("transform",`translate(${s}, ${l})`)}if(e.endLabelRight){let a=ps[e.id].endRight,s=e.x,l=e.y;if(r){let u=Zt.calcTerminalLabelPosition(e.arrowTypeEnd?10:0,"end_right",r);s=u.x,l=u.y}a.attr("transform",`translate(${s}, ${l})`)}},"positionEdgeLabel"),s7t=o((e,t)=>{let r=e.x,n=e.y,i=Math.abs(t.x-r),a=Math.abs(t.y-n),s=e.width/2,l=e.height/2;return i>=s||a>=l},"outsideNode"),o7t=o((e,t,r)=>{Z.debug(`intersection calc abc89: + outsidePoint: ${JSON.stringify(t)} + insidePoint : ${JSON.stringify(r)} + node : x:${e.x} y:${e.y} w:${e.width} h:${e.height}`);let n=e.x,i=e.y,a=Math.abs(n-r.x),s=e.width/2,l=r.xMath.abs(n-t.x)*u){let f=r.y{Z.debug("abc88 cutPathAtIntersect",e,t);let r=[],n=e[0],i=!1;return e.forEach(a=>{if(!s7t(t,a)&&!i){let s=o7t(t,n,a),l=!1;r.forEach(u=>{l=l||u.x===s.x&&u.y===s.y}),r.some(u=>u.x===s.x&&u.y===s.y)||r.push(s),i=!0}else n=a,i||r.push(a)}),r},"cutPathAtIntersect"),yOe=o(function(e,t,r,n,i,a,s){let l=r.points;Z.debug("abc88 InsertEdge: edge=",r,"e=",t);let u=!1,h=a.node(t.v);var d=a.node(t.w);d?.intersect&&h?.intersect&&(l=l.slice(1,r.points.length-1),l.unshift(h.intersect(l[0])),l.push(d.intersect(l[l.length-1]))),r.toCluster&&(Z.debug("to cluster abc88",n[r.toCluster]),l=pOe(r.points,n[r.toCluster].node),u=!0),r.fromCluster&&(Z.debug("from cluster abc88",n[r.fromCluster]),l=pOe(l.reverse(),n[r.fromCluster].node).reverse(),u=!0);let f=l.filter(k=>!Number.isNaN(k.y)),p=rc;r.curve&&(i==="graph"||i==="flowchart")&&(p=r.curve);let{x:m,y:g}=U4(r),y=tc().x(m).y(g).curve(p),v;switch(r.thickness){case"normal":v="edge-thickness-normal";break;case"thick":v="edge-thickness-thick";break;case"invisible":v="edge-thickness-thick";break;default:v=""}switch(r.pattern){case"solid":v+=" edge-pattern-solid";break;case"dotted":v+=" edge-pattern-dotted";break;case"dashed":v+=" edge-pattern-dashed";break}let x=e.append("path").attr("d",y(f)).attr("id",r.id).attr("class"," "+v+(r.classes?" "+r.classes:"")).attr("style",r.style),b="";(Ae().flowchart.arrowMarkerAbsolute||Ae().state.arrowMarkerAbsolute)&&(b=qp(!0)),dOe(x,r,b,s,i);let T={};return u&&(T.updatedPath=l),T.originalPath=r.points,T},"insertEdge")});var l7t,xOe,bOe=F(()=>{"use strict";l7t=o(e=>{let t=new Set;for(let r of e)switch(r){case"x":t.add("right"),t.add("left");break;case"y":t.add("up"),t.add("down");break;default:t.add(r);break}return t},"expandAndDeduplicateDirections"),xOe=o((e,t,r,n)=>{let i=l7t(e),a=2,s=t.height+2*r.padding,l=s/a,u=n??t.width+2*l+r.padding,h=r.padding/2;return i.has("right")&&i.has("left")&&i.has("up")&&i.has("down")?[{x:0,y:0},{x:l,y:0},{x:u/2,y:2*h},{x:u-l,y:0},{x:u,y:0},{x:u,y:-s/3},{x:u+2*h,y:-s/2},{x:u,y:-2*s/3},{x:u,y:-s},{x:u-l,y:-s},{x:u/2,y:-s-2*h},{x:l,y:-s},{x:0,y:-s},{x:0,y:-2*s/3},{x:-2*h,y:-s/2},{x:0,y:-s/3}]:i.has("right")&&i.has("left")&&i.has("up")?[{x:l,y:0},{x:u-l,y:0},{x:u,y:-s/2},{x:u-l,y:-s},{x:l,y:-s},{x:0,y:-s/2}]:i.has("right")&&i.has("left")&&i.has("down")?[{x:0,y:0},{x:l,y:-s},{x:u-l,y:-s},{x:u,y:0}]:i.has("right")&&i.has("up")&&i.has("down")?[{x:0,y:0},{x:u,y:-l},{x:u,y:-s+l},{x:0,y:-s}]:i.has("left")&&i.has("up")&&i.has("down")?[{x:u,y:0},{x:0,y:-l},{x:0,y:-s+l},{x:u,y:-s}]:i.has("right")&&i.has("left")?[{x:l,y:0},{x:l,y:-h},{x:u-l,y:-h},{x:u-l,y:0},{x:u,y:-s/2},{x:u-l,y:-s},{x:u-l,y:-s+h},{x:l,y:-s+h},{x:l,y:-s},{x:0,y:-s/2}]:i.has("up")&&i.has("down")?[{x:u/2,y:0},{x:0,y:-h},{x:l,y:-h},{x:l,y:-s+h},{x:0,y:-s+h},{x:u/2,y:-s},{x:u,y:-s+h},{x:u-l,y:-s+h},{x:u-l,y:-h},{x:u,y:-h}]:i.has("right")&&i.has("up")?[{x:0,y:0},{x:u,y:-l},{x:0,y:-s}]:i.has("right")&&i.has("down")?[{x:0,y:0},{x:u,y:0},{x:0,y:-s}]:i.has("left")&&i.has("up")?[{x:u,y:0},{x:0,y:-l},{x:u,y:-s}]:i.has("left")&&i.has("down")?[{x:u,y:0},{x:0,y:0},{x:u,y:-s}]:i.has("right")?[{x:l,y:-h},{x:l,y:-h},{x:u-l,y:-h},{x:u-l,y:0},{x:u,y:-s/2},{x:u-l,y:-s},{x:u-l,y:-s+h},{x:l,y:-s+h},{x:l,y:-s+h}]:i.has("left")?[{x:l,y:0},{x:l,y:-h},{x:u-l,y:-h},{x:u-l,y:-s+h},{x:l,y:-s+h},{x:l,y:-s},{x:0,y:-s/2}]:i.has("up")?[{x:l,y:-h},{x:l,y:-s+h},{x:0,y:-s+h},{x:u/2,y:-s},{x:u,y:-s+h},{x:u-l,y:-s+h},{x:u-l,y:-h}]:i.has("down")?[{x:u/2,y:0},{x:0,y:-h},{x:l,y:-h},{x:l,y:-s+h},{x:u-l,y:-s+h},{x:u-l,y:-h},{x:u,y:-h}]:[{x:0,y:0}]},"getArrowPoints")});function c7t(e,t){return e.intersect(t)}var TOe,COe=F(()=>{"use strict";o(c7t,"intersectNode");TOe=c7t});function u7t(e,t,r,n){var i=e.x,a=e.y,s=i-n.x,l=a-n.y,u=Math.sqrt(t*t*l*l+r*r*s*s),h=Math.abs(t*r*s/u);n.x{"use strict";o(u7t,"intersectEllipse");rD=u7t});function h7t(e,t,r){return rD(e,t,t,r)}var wOe,kOe=F(()=>{"use strict";Dj();o(h7t,"intersectCircle");wOe=h7t});function d7t(e,t,r,n){var i,a,s,l,u,h,d,f,p,m,g,y,v,x,b;if(i=t.y-e.y,s=e.x-t.x,u=t.x*e.y-e.x*t.y,p=i*r.x+s*r.y+u,m=i*n.x+s*n.y+u,!(p!==0&&m!==0&&SOe(p,m))&&(a=n.y-r.y,l=r.x-n.x,h=n.x*r.y-r.x*n.y,d=a*e.x+l*e.y+h,f=a*t.x+l*t.y+h,!(d!==0&&f!==0&&SOe(d,f))&&(g=i*l-a*s,g!==0)))return y=Math.abs(g/2),v=s*h-l*u,x=v<0?(v-y)/g:(v+y)/g,v=a*u-i*h,b=v<0?(v-y)/g:(v+y)/g,{x,y:b}}function SOe(e,t){return e*t>0}var EOe,AOe=F(()=>{"use strict";o(d7t,"intersectLine");o(SOe,"sameSign");EOe=d7t});function f7t(e,t,r){var n=e.x,i=e.y,a=[],s=Number.POSITIVE_INFINITY,l=Number.POSITIVE_INFINITY;typeof t.forEach=="function"?t.forEach(function(g){s=Math.min(s,g.x),l=Math.min(l,g.y)}):(s=Math.min(s,t.x),l=Math.min(l,t.y));for(var u=n-e.width/2-s,h=i-e.height/2-l,d=0;d1&&a.sort(function(g,y){var v=g.x-r.x,x=g.y-r.y,b=Math.sqrt(v*v+x*x),T=y.x-r.x,k=y.y-r.y,C=Math.sqrt(T*T+k*k);return b{"use strict";AOe();ROe=f7t;o(f7t,"intersectPolygon")});var p7t,LOe,DOe=F(()=>{"use strict";p7t=o((e,t)=>{var r=e.x,n=e.y,i=t.x-r,a=t.y-n,s=e.width/2,l=e.height/2,u,h;return Math.abs(a)*s>Math.abs(i)*l?(a<0&&(l=-l),u=a===0?0:l*i/a,h=l):(i<0&&(s=-s),u=s,h=i===0?0:s*a/i),{x:r+u,y:n+h}},"intersectRect"),LOe=p7t});var ei,Ij=F(()=>{"use strict";COe();kOe();Dj();_Oe();DOe();ei={node:TOe,circle:wOe,ellipse:rD,polygon:ROe,rect:LOe}});function Ic(e,t,r,n){return e.insert("polygon",":first-child").attr("points",n.map(function(i){return i.x+","+i.y}).join(" ")).attr("class","label-container").attr("transform","translate("+-t/2+","+r/2+")")}var Ji,mi,Mj=F(()=>{"use strict";eD();Ls();Xt();ur();$r();Vr();Qt();YP();Ji=o(async(e,t,r,n)=>{let i=Ae(),a,s=t.useHtmlLabels||Gr(i);r?a=r:a="node default";let l=e.insert("g").attr("class",a).attr("id",t.domId||t.id),u=l.insert("g").attr("class","label").attr("style",t.labelStyle),h;t.labelText===void 0?h="":h=typeof t.labelText=="string"?t.labelText:t.labelText[0];let d;t.labelType==="markdown"?d=Pn(u,mr(Rs(h),i),{useHtmlLabels:s,width:t.width||i.flowchart.wrappingWidth,classes:"markdown-node-label"},i):d=await Ao(u,mr(Rs(h),i),t.labelStyle,!1,n);let f=d.getBBox(),p=t.padding/2;if(Gr(i)){let m=d.children[0],g=et(d);await E4(m,h),f=m.getBoundingClientRect(),g.attr("width",f.width),g.attr("height",f.height)}return s?u.attr("transform","translate("+-f.width/2+", "+-f.height/2+")"):u.attr("transform","translate(0, "+-f.height/2+")"),t.centerLabel&&u.attr("transform","translate("+-f.width/2+", "+-f.height/2+")"),u.insert("rect",":first-child"),{shapeSvg:l,bbox:f,halfPadding:p,label:u}},"labelHelper"),mi=o((e,t)=>{let r=t.node().getBBox();e.width=r.width,e.height=r.height},"updateNodeBounds");o(Ic,"insertPolygonShape")});var m7t,IOe,MOe=F(()=>{"use strict";Mj();vt();Xt();ur();Ij();m7t=o(async(e,t)=>{t.useHtmlLabels||Gr(Ae())||(t.centerLabel=!0);let{shapeSvg:n,bbox:i,halfPadding:a}=await Ji(e,t,"node "+t.classes,!0);Z.info("Classes = ",t.classes);let s=n.insert("rect",":first-child");return s.attr("rx",t.rx).attr("ry",t.ry).attr("x",-i.width/2-a).attr("y",-i.height/2-a).attr("width",i.width+t.padding).attr("height",i.height+t.padding),mi(t,s),t.intersect=function(l){return ei.rect(t,l)},n},"note"),IOe=m7t});function Nj(e,t,r,n){let i=[],a=o(l=>{i.push(l,0)},"addBorder"),s=o(l=>{i.push(0,l)},"skipBorder");t.includes("t")?(Z.debug("add top border"),a(r)):s(r),t.includes("r")?(Z.debug("add right border"),a(n)):s(n),t.includes("b")?(Z.debug("add bottom border"),a(r)):s(r),t.includes("l")?(Z.debug("add left border"),a(n)):s(n),e.attr("stroke-dasharray",i.join(" "))}var NOe,rl,POe,g7t,y7t,v7t,x7t,b7t,T7t,C7t,w7t,k7t,S7t,E7t,A7t,R7t,_7t,L7t,D7t,I7t,M7t,N7t,OOe,P7t,O7t,BOe,nD,Pj,$Oe,FOe=F(()=>{"use strict";$r();Xt();ur();vt();bOe();eD();Ij();MOe();Mj();NOe=o(e=>e?" "+e:"","formatClass"),rl=o((e,t)=>`${t||"node default"}${NOe(e.classes)} ${NOe(e.class)}`,"getClassesFromNode"),POe=o(async(e,t)=>{let{shapeSvg:r,bbox:n}=await Ji(e,t,rl(t,void 0),!0),i=n.width+t.padding,a=n.height+t.padding,s=i+a,l=[{x:s/2,y:0},{x:s,y:-s/2},{x:s/2,y:-s},{x:0,y:-s/2}];Z.info("Question main (Circle)");let u=Ic(r,s,s,l);return u.attr("style",t.style),mi(t,u),t.intersect=function(h){return Z.warn("Intersect called"),ei.polygon(t,l,h)},r},"question"),g7t=o((e,t)=>{let r=e.insert("g").attr("class","node default").attr("id",t.domId||t.id),n=28,i=[{x:0,y:n/2},{x:n/2,y:0},{x:0,y:-n/2},{x:-n/2,y:0}];return r.insert("polygon",":first-child").attr("points",i.map(function(s){return s.x+","+s.y}).join(" ")).attr("class","state-start").attr("r",7).attr("width",28).attr("height",28),t.width=28,t.height=28,t.intersect=function(s){return ei.circle(t,14,s)},r},"choice"),y7t=o(async(e,t)=>{let{shapeSvg:r,bbox:n}=await Ji(e,t,rl(t,void 0),!0),i=4,a=t.positioned?t.height:n.height+t.padding,s=a/i,l=t.positioned?t.width:n.width+2*s+t.padding,u=[{x:s,y:0},{x:l-s,y:0},{x:l,y:-a/2},{x:l-s,y:-a},{x:s,y:-a},{x:0,y:-a/2}],h=Ic(r,l,a,u);return h.attr("style",t.style),mi(t,h),t.intersect=function(d){return ei.polygon(t,u,d)},r},"hexagon"),v7t=o(async(e,t)=>{let{shapeSvg:r,bbox:n}=await Ji(e,t,void 0,!0),i=2,a=n.height+2*t.padding,s=a/i,l=n.width+2*s+t.padding,h=t.positioned&&(t.widthInColumns??1)>1&&t.width>l?t.width:l,d=xOe(t.directions,n,t,h),f=Ic(r,h,a,d);return f.attr("style",t.style),mi(t,f),t.intersect=function(p){return ei.polygon(t,d,p)},r},"block_arrow"),x7t=o(async(e,t)=>{let{shapeSvg:r,bbox:n}=await Ji(e,t,rl(t,void 0),!0),i=n.width+t.padding,a=n.height+t.padding,s=[{x:-a/2,y:0},{x:i,y:0},{x:i,y:-a},{x:-a/2,y:-a},{x:0,y:-a/2}];return Ic(r,i,a,s).attr("style",t.style),t.width=i+a,t.height=a,t.intersect=function(u){return ei.polygon(t,s,u)},r},"rect_left_inv_arrow"),b7t=o(async(e,t)=>{let{shapeSvg:r,bbox:n}=await Ji(e,t,rl(t),!0),i=n.width+t.padding,a=n.height+t.padding,s=[{x:-2*a/6,y:0},{x:i-a/6,y:0},{x:i+2*a/6,y:-a},{x:a/6,y:-a}],l=Ic(r,i,a,s);return l.attr("style",t.style),mi(t,l),t.intersect=function(u){return ei.polygon(t,s,u)},r},"lean_right"),T7t=o(async(e,t)=>{let{shapeSvg:r,bbox:n}=await Ji(e,t,rl(t,void 0),!0),i=n.width+t.padding,a=n.height+t.padding,s=[{x:2*a/6,y:0},{x:i+a/6,y:0},{x:i-2*a/6,y:-a},{x:-a/6,y:-a}],l=Ic(r,i,a,s);return l.attr("style",t.style),mi(t,l),t.intersect=function(u){return ei.polygon(t,s,u)},r},"lean_left"),C7t=o(async(e,t)=>{let{shapeSvg:r,bbox:n}=await Ji(e,t,rl(t,void 0),!0),i=n.width+t.padding,a=n.height+t.padding,s=[{x:-2*a/6,y:0},{x:i+2*a/6,y:0},{x:i-a/6,y:-a},{x:a/6,y:-a}],l=Ic(r,i,a,s);return l.attr("style",t.style),mi(t,l),t.intersect=function(u){return ei.polygon(t,s,u)},r},"trapezoid"),w7t=o(async(e,t)=>{let{shapeSvg:r,bbox:n}=await Ji(e,t,rl(t,void 0),!0),i=n.width+t.padding,a=n.height+t.padding,s=[{x:a/6,y:0},{x:i-a/6,y:0},{x:i+2*a/6,y:-a},{x:-2*a/6,y:-a}],l=Ic(r,i,a,s);return l.attr("style",t.style),mi(t,l),t.intersect=function(u){return ei.polygon(t,s,u)},r},"inv_trapezoid"),k7t=o(async(e,t)=>{let{shapeSvg:r,bbox:n}=await Ji(e,t,rl(t,void 0),!0),i=n.width+t.padding,a=n.height+t.padding,s=[{x:0,y:0},{x:i+a/2,y:0},{x:i,y:-a/2},{x:i+a/2,y:-a},{x:0,y:-a}],l=Ic(r,i,a,s);return l.attr("style",t.style),mi(t,l),t.intersect=function(u){return ei.polygon(t,s,u)},r},"rect_right_inv_arrow"),S7t=o(async(e,t)=>{let{shapeSvg:r,bbox:n}=await Ji(e,t,rl(t,void 0),!0),i=n.width+t.padding,a=i/2,s=a/(2.5+i/50),l=n.height+s+t.padding,u="M 0,"+s+" a "+a+","+s+" 0,0,0 "+i+" 0 a "+a+","+s+" 0,0,0 "+-i+" 0 l 0,"+l+" a "+a+","+s+" 0,0,0 "+i+" 0 l 0,"+-l,h=r.attr("label-offset-y",s).insert("path",":first-child").attr("style",t.style).attr("d",u).attr("transform","translate("+-i/2+","+-(l/2+s)+")");return mi(t,h),t.intersect=function(d){let f=ei.rect(t,d),p=f.x-t.x;if(a!=0&&(Math.abs(p)t.height/2-s)){let m=s*s*(1-p*p/(a*a));m!=0&&(m=Math.sqrt(m)),m=s-m,d.y-t.y>0&&(m=-m),f.y+=m}return f},r},"cylinder"),E7t=o(async(e,t)=>{let{shapeSvg:r,bbox:n,halfPadding:i}=await Ji(e,t,"node "+t.classes+" "+t.class,!0),a=r.insert("rect",":first-child"),s=t.positioned?t.width:n.width+t.padding,l=t.positioned?t.height:n.height+t.padding,u=t.positioned?-s/2:-n.width/2-i,h=t.positioned?-l/2:-n.height/2-i;if(a.attr("class","basic label-container").attr("style",t.style).attr("rx",t.rx).attr("ry",t.ry).attr("x",u).attr("y",h).attr("width",s).attr("height",l),t.props){let d=new Set(Object.keys(t.props));t.props.borders&&(Nj(a,t.props.borders,s,l),d.delete("borders")),d.forEach(f=>{Z.warn(`Unknown node property ${f}`)})}return mi(t,a),t.intersect=function(d){return ei.rect(t,d)},r},"rect"),A7t=o(async(e,t)=>{let{shapeSvg:r,bbox:n,halfPadding:i}=await Ji(e,t,"node "+t.classes,!0),a=r.insert("rect",":first-child"),s=t.positioned?t.width:n.width+t.padding,l=t.positioned?t.height:n.height+t.padding,u=t.positioned?-s/2:-n.width/2-i,h=t.positioned?-l/2:-n.height/2-i;if(a.attr("class","basic cluster composite label-container").attr("style",t.style).attr("rx",t.rx).attr("ry",t.ry).attr("x",u).attr("y",h).attr("width",s).attr("height",l),t.props){let d=new Set(Object.keys(t.props));t.props.borders&&(Nj(a,t.props.borders,s,l),d.delete("borders")),d.forEach(f=>{Z.warn(`Unknown node property ${f}`)})}return mi(t,a),t.intersect=function(d){return ei.rect(t,d)},r},"composite"),R7t=o(async(e,t)=>{let{shapeSvg:r}=await Ji(e,t,"label",!0);Z.trace("Classes = ",t.class);let n=r.insert("rect",":first-child"),i=0,a=0;if(n.attr("width",i).attr("height",a),r.attr("class","label edgeLabel"),t.props){let s=new Set(Object.keys(t.props));t.props.borders&&(Nj(n,t.props.borders,i,a),s.delete("borders")),s.forEach(l=>{Z.warn(`Unknown node property ${l}`)})}return mi(t,n),t.intersect=function(s){return ei.rect(t,s)},r},"labelRect");o(Nj,"applyNodePropertyBorders");_7t=o(async(e,t)=>{let r;t.classes?r="node "+t.classes:r="node default";let n=e.insert("g").attr("class",r).attr("id",t.domId||t.id),i=n.insert("rect",":first-child"),a=n.insert("line"),s=n.insert("g").attr("class","label"),l=t.labelText.flat?t.labelText.flat():t.labelText,u="";typeof l=="object"?u=l[0]:u=l,Z.info("Label text abc79",u,l,typeof l=="object");let h=await Ao(s,u,t.labelStyle,!0,!0),d={width:0,height:0};if(Gr(Ae())){let y=h.children[0],v=et(h);d=y.getBoundingClientRect(),v.attr("width",d.width),v.attr("height",d.height)}Z.info("Text 2",l);let f=l.slice(1,l.length),p=h.getBBox(),m=await Ao(s,f.join?f.join("
"):f,t.labelStyle,!0,!0);if(Gr(Ae())){let y=m.children[0],v=et(m);d=y.getBoundingClientRect(),v.attr("width",d.width),v.attr("height",d.height)}let g=t.padding/2;return et(m).attr("transform","translate( "+(d.width>p.width?0:(p.width-d.width)/2)+", "+(p.height+g+5)+")"),et(h).attr("transform","translate( "+(d.width{let{shapeSvg:r,bbox:n}=await Ji(e,t,rl(t,void 0),!0),i=n.height+t.padding,a=n.width+i/4+t.padding,s=r.insert("rect",":first-child").attr("style",t.style).attr("rx",i/2).attr("ry",i/2).attr("x",-a/2).attr("y",-i/2).attr("width",a).attr("height",i);return mi(t,s),t.intersect=function(l){return ei.rect(t,l)},r},"stadium"),D7t=o(async(e,t)=>{let{shapeSvg:r,bbox:n,halfPadding:i}=await Ji(e,t,rl(t,void 0),!0),a=r.insert("circle",":first-child");return a.attr("style",t.style).attr("rx",t.rx).attr("ry",t.ry).attr("r",n.width/2+i).attr("width",n.width+t.padding).attr("height",n.height+t.padding),Z.info("Circle main"),mi(t,a),t.intersect=function(s){return Z.info("Circle intersect",t,n.width/2+i,s),ei.circle(t,n.width/2+i,s)},r},"circle"),I7t=o(async(e,t)=>{let{shapeSvg:r,bbox:n,halfPadding:i}=await Ji(e,t,rl(t,void 0),!0),a=5,s=r.insert("g",":first-child"),l=s.insert("circle"),u=s.insert("circle");return s.attr("class",t.class),l.attr("style",t.style).attr("rx",t.rx).attr("ry",t.ry).attr("r",n.width/2+i+a).attr("width",n.width+t.padding+a*2).attr("height",n.height+t.padding+a*2),u.attr("style",t.style).attr("rx",t.rx).attr("ry",t.ry).attr("r",n.width/2+i).attr("width",n.width+t.padding).attr("height",n.height+t.padding),Z.info("DoubleCircle main"),mi(t,l),t.intersect=function(h){return Z.info("DoubleCircle intersect",t,n.width/2+i+a,h),ei.circle(t,n.width/2+i+a,h)},r},"doublecircle"),M7t=o(async(e,t)=>{let{shapeSvg:r,bbox:n}=await Ji(e,t,rl(t,void 0),!0),i=n.width+t.padding,a=n.height+t.padding,s=[{x:0,y:0},{x:i,y:0},{x:i,y:-a},{x:0,y:-a},{x:0,y:0},{x:-8,y:0},{x:i+8,y:0},{x:i+8,y:-a},{x:-8,y:-a},{x:-8,y:0}],l=Ic(r,i,a,s);return l.attr("style",t.style),mi(t,l),t.intersect=function(u){return ei.polygon(t,s,u)},r},"subroutine"),N7t=o((e,t)=>{let r=e.insert("g").attr("class","node default").attr("id",t.domId||t.id),n=r.insert("circle",":first-child");return n.attr("class","state-start").attr("r",7).attr("width",14).attr("height",14),mi(t,n),t.intersect=function(i){return ei.circle(t,7,i)},r},"start"),OOe=o((e,t,r)=>{let n=e.insert("g").attr("class","node default").attr("id",t.domId||t.id),i=70,a=10;r==="LR"&&(i=10,a=70);let s=n.append("rect").attr("x",-1*i/2).attr("y",-1*a/2).attr("width",i).attr("height",a).attr("class","fork-join");return mi(t,s),t.height=t.height+t.padding/2,t.width=t.width+t.padding/2,t.intersect=function(l){return ei.rect(t,l)},n},"forkJoin"),P7t=o((e,t)=>{let r=e.insert("g").attr("class","node default").attr("id",t.domId||t.id),n=r.insert("circle",":first-child"),i=r.insert("circle",":first-child");return i.attr("class","state-start").attr("r",7).attr("width",14).attr("height",14),n.attr("class","state-end").attr("r",5).attr("width",10).attr("height",10),mi(t,i),t.intersect=function(a){return ei.circle(t,7,a)},r},"end"),O7t=o(async(e,t)=>{let r=t.padding/2,n=4,i=8,a;t.classes?a="node "+t.classes:a="node default";let s=e.insert("g").attr("class",a).attr("id",t.domId||t.id),l=s.insert("rect",":first-child"),u=s.insert("line"),h=s.insert("line"),d=0,f=n,p=s.insert("g").attr("class","label"),m=0,g=t.classData.annotations?.[0],y=t.classData.annotations[0]?"\xAB"+t.classData.annotations[0]+"\xBB":"",v=await Ao(p,y,t.labelStyle,!0,!0),x=v.getBBox();if(Gr(Ae())){let R=v.children[0],L=et(v);x=R.getBoundingClientRect(),L.attr("width",x.width),L.attr("height",x.height)}t.classData.annotations[0]&&(f+=x.height+n,d+=x.width);let b=t.classData.label;t.classData.type!==void 0&&t.classData.type!==""&&(Gr(Ae())?b+="<"+t.classData.type+">":b+="<"+t.classData.type+">");let T=await Ao(p,b,t.labelStyle,!0,!0);et(T).attr("class","classTitle");let k=T.getBBox();if(Gr(Ae())){let R=T.children[0],L=et(T);k=R.getBoundingClientRect(),L.attr("width",k.width),L.attr("height",k.height)}f+=k.height+n,k.width>d&&(d=k.width);let C=[];t.classData.members.forEach(async R=>{let L=R.getDisplayDetails(),N=L.displayText;Gr(Ae())&&(N=N.replace(//g,">"));let I=await Ao(p,N,L.cssStyle?L.cssStyle:t.labelStyle,!0,!0),_=I.getBBox();if(Gr(Ae())){let A=I.children[0],M=et(I);_=A.getBoundingClientRect(),M.attr("width",_.width),M.attr("height",_.height)}_.width>d&&(d=_.width),f+=_.height+n,C.push(I)}),f+=i;let w=[];if(t.classData.methods.forEach(async R=>{let L=R.getDisplayDetails(),N=L.displayText;Gr(Ae())&&(N=N.replace(//g,">"));let I=await Ao(p,N,L.cssStyle?L.cssStyle:t.labelStyle,!0,!0),_=I.getBBox();if(Gr(Ae())){let A=I.children[0],M=et(I);_=A.getBoundingClientRect(),M.attr("width",_.width),M.attr("height",_.height)}_.width>d&&(d=_.width),f+=_.height+n,w.push(I)}),f+=i,g){let R=(d-x.width)/2;et(v).attr("transform","translate( "+(-1*d/2+R)+", "+-1*f/2+")"),m=x.height+n}let S=(d-k.width)/2;return et(T).attr("transform","translate( "+(-1*d/2+S)+", "+(-1*f/2+m)+")"),m+=k.height+n,u.attr("class","divider").attr("x1",-d/2-r).attr("x2",d/2+r).attr("y1",-f/2-r+i+m).attr("y2",-f/2-r+i+m),m+=i,C.forEach(R=>{et(R).attr("transform","translate( "+-d/2+", "+(-1*f/2+m+i/2)+")");let L=R?.getBBox();m+=(L?.height??0)+n}),m+=i,h.attr("class","divider").attr("x1",-d/2-r).attr("x2",d/2+r).attr("y1",-f/2-r+i+m).attr("y2",-f/2-r+i+m),m+=i,w.forEach(R=>{et(R).attr("transform","translate( "+-d/2+", "+(-1*f/2+m)+")");let L=R?.getBBox();m+=(L?.height??0)+n}),l.attr("style",t.style).attr("class","outer title-state").attr("x",-d/2-r).attr("y",-(f/2)-r).attr("width",d+t.padding).attr("height",f+t.padding),mi(t,l),t.intersect=function(R){return ei.rect(t,R)},s},"class_box"),BOe={rhombus:POe,composite:A7t,question:POe,rect:E7t,labelRect:R7t,rectWithTitle:_7t,choice:g7t,circle:D7t,doublecircle:I7t,stadium:L7t,hexagon:y7t,block_arrow:v7t,rect_left_inv_arrow:x7t,lean_right:b7t,lean_left:T7t,trapezoid:C7t,inv_trapezoid:w7t,rect_right_inv_arrow:k7t,cylinder:S7t,start:N7t,end:P7t,note:IOe,subroutine:M7t,fork:OOe,join:OOe,class_box:O7t},nD={},Pj=o(async(e,t,r)=>{let n,i;if(t.link){let a;Ae().securityLevel==="sandbox"?a="_top":t.linkTarget&&(a=t.linkTarget||"_blank"),n=e.insert("svg:a").attr("xlink:href",t.link).attr("target",a),i=await BOe[t.shape](n,t,r)}else i=await BOe[t.shape](e,t,r),n=i;return t.tooltip&&i.attr("title",t.tooltip),t.class&&i.attr("class","node default "+t.class),nD[t.id]=n,t.haveCallback&&nD[t.id].attr("class",nD[t.id].attr("class")+" clickable"),n},"insertNode"),$Oe=o(e=>{let t=nD[e.id];Z.trace("Transforming node",e.diff,e,"translate("+(e.x-e.width/2-5)+", "+e.width/2+")");let r=8,n=e.diff||0;return e.clusterNode?t.attr("transform","translate("+(e.x+n-e.width/2)+", "+(e.y-e.height/2-r)+")"):t.attr("transform","translate("+e.x+", "+e.y+")"),n},"positionNode")});function zOe(e,t,r=!1){let n=e,i="default";(n?.classes?.length||0)>0&&(i=(n?.classes??[]).join(" ")),i=i+" flowchart-label";let a=0,s="",l;switch(n.type){case"round":a=5,s="rect";break;case"composite":a=0,s="composite",l=0;break;case"square":s="rect";break;case"diamond":s="question";break;case"hexagon":s="hexagon";break;case"block_arrow":s="block_arrow";break;case"odd":s="rect_left_inv_arrow";break;case"lean_right":s="lean_right";break;case"lean_left":s="lean_left";break;case"trapezoid":s="trapezoid";break;case"inv_trapezoid":s="inv_trapezoid";break;case"rect_left_inv_arrow":s="rect_left_inv_arrow";break;case"circle":s="circle";break;case"ellipse":s="ellipse";break;case"stadium":s="stadium";break;case"subroutine":s="subroutine";break;case"cylinder":s="cylinder";break;case"group":s="rect";break;case"doublecircle":s="doublecircle";break;default:s="rect"}let u=dP(n?.styles??[]),h=n.label,d=n.size??{width:0,height:0,x:0,y:0},f=t.getDiagramId();return{labelStyle:u.labelStyle,shape:s,labelText:h,rx:a,ry:a,class:i,style:u.style,id:n.id,domId:f?`${f}-${n.id}`:n.id,directions:n.directions,width:d.width,height:d.height,x:d.x,y:d.y,positioned:r,intersect:void 0,type:n.type,padding:l??_t()?.block?.padding??0,widthInColumns:n.widthInColumns??1}}async function B7t(e,t,r){let n=zOe(t,r,!1);if(n.type==="group")return;let i=_t(),a=await Pj(e,n,{config:i}),s=a.node().getBBox(),l=r.getBlock(n.id);l.size={width:s.width,height:s.height,x:0,y:0,node:a},r.setBlock(l),a.remove()}async function $7t(e,t,r){let n=zOe(t,r,!0);if(r.getBlock(n.id).type!=="space"){let a=_t();await Pj(e,n,{config:a}),t.intersect=n?.intersect,$Oe(n)}}async function Oj(e,t,r,n){for(let i of t)await n(e,i,r),i.children&&await Oj(e,i.children,r,n)}async function GOe(e,t,r){await Oj(e,t,r,B7t)}async function VOe(e,t,r){await Oj(e,t,r,$7t)}async function WOe(e,t,r,n,i){let a=new on({multigraph:!0,compound:!0});a.setGraph({rankdir:"TB",nodesep:10,ranksep:10,marginx:8,marginy:8});for(let s of r)s.size&&a.setNode(s.id,{width:s.size.width,height:s.size.height,intersect:s.intersect});for(let s of t)if(s.start&&s.end){let l=n.getBlock(s.start),u=n.getBlock(s.end);if(l?.size&&u?.size){let h=l.size,d=u.size,f=[{x:h.x,y:h.y},{x:h.x+(d.x-h.x)/2,y:h.y+(d.y-h.y)/2},{x:d.x,y:d.y}],p=i?`${i}-${s.id}`:s.id,m=s.thickness==="thick"?"edge-thickness-thick":"edge-thickness-normal",g=s.pattern==="dotted"?"edge-pattern-dotted":"edge-pattern-solid",y=`${m} ${g} flowchart-link LS-a1 LE-b1`;yOe(e,{v:s.start,w:s.end,name:p},{...s,id:p,arrowTypeEnd:s.arrowTypeEnd,arrowTypeStart:s.arrowTypeStart,points:f,classes:y},void 0,"block",a,i),s.label&&(await mOe(e,{...s,label:s.label,labelStyle:"stroke: #333; stroke-width: 1.5px;fill:none;",arrowTypeEnd:s.arrowTypeEnd,arrowTypeStart:s.arrowTypeStart,points:f,classes:y}),gOe({...s,x:f[1].x,y:f[1].y},{originalPath:f}))}}}var qOe=F(()=>{"use strict";qo();ur();vOe();FOe();Qt();o(zOe,"getNodeFromBlock");o(B7t,"calculateBlockSize");o($7t,"insertBlockPositioned");o(Oj,"performOperations");o(GOe,"calculateBlockSizes");o(VOe,"insertBlocks");o(WOe,"insertEdges")});var F7t,z7t,HOe,UOe=F(()=>{"use strict";$r();ur();aOe();vt();$n();uOe();qOe();F7t=o(function(e,t){return t.db.getClasses()},"getClasses"),z7t=o(async function(e,t,r,n){let{securityLevel:i,block:a}=_t(),s=n.db;s.setDiagramId(t);let l;i==="sandbox"&&(l=et("#i"+t));let u=i==="sandbox"?et(l.nodes()[0].contentDocument.body):et("body"),h=i==="sandbox"?u.select(`[id="${t}"]`):et(`[id="${t}"]`);iOe(h,["point","circle","cross"],n.type,t);let f=s.getBlocks(),p=s.getBlocksFlat(),m=s.getEdges(),g=h.insert("g").attr("class","block");await GOe(g,f,s);let y=cOe(s);if(await VOe(g,f,s),await WOe(g,m,p,s,t),y){let v=y,x=Math.max(1,Math.round(.125*(v.width/v.height))),b=v.height+x+10,T=v.width+10,{useMaxWidth:k}=a;Wr(h,b,T,!!k),Z.debug("Here Bounds",y,v),h.attr("viewBox",`${v.x-5} ${v.y-5} ${v.width+10} ${v.height+10}`)}},"draw"),HOe={draw:z7t,getClasses:F7t}});var YOe={};ir(YOe,{diagram:()=>G7t});var G7t,jOe=F(()=>{"use strict";jPe();tOe();nOe();UOe();G7t={parser:YPe,db:eOe,renderer:HOe,styles:rOe}});function Y7t(e){return e.some(t=>e9e.test(t))}function j7t(e){for(let t of e){let r=t9e.exec(t);if(r?.index&&r.index>0)return r.index}return 4}function r9e(e,t){return e.replace(/\bline\s+(\d+)\b/gi,(r,n)=>{let i=parseInt(n,10),a=t.get(i);return a?`line ${a}`:r})}function n9e(e){let t=e.split(` +`),r=new Map,n=-1;for(let[u,h]of t.entries())if(h.trim()==="treeView-beta"){n=u;break}if(n===-1)return{text:e,lineMap:r};let i=[];for(let u=n+1;u{"use strict";e9e=/[─━│┃└┗├┣]/,t9e=/[└┗├┣]/,H7t=/[─━]/,ZOe=/^[\s│┃]+$/,QOe=/^\s*(title[\t ]|accTitle[\t ]*:|accDescr[\t ]*[:{])/,JOe=/^\s*%%/,U7t=" ";o(Y7t,"isBoxDrawingFormat");o(j7t,"inferSegmentWidth");o(r9e,"remapErrorLines");o(n9e,"preprocessBoxDrawing")});var Yu,X7t,K7t,Z7t,Q7t,J7t,e8t,t8t,Qw,Bj=F(()=>{"use strict";ur();Wi();Qt();J_();Nn();Yu=new wp(()=>({cnt:1,stack:[{id:0,level:-1,name:"/",nodeType:"directory",children:[]}]})),X7t=o(()=>{Yu.reset(),yr()},"clear"),K7t=o(()=>Yu.records.stack[0],"getRoot"),Z7t=o(()=>Yu.records.cnt,"getCount"),Q7t=cr.treeView,J7t=o(()=>qr(Q7t,_t().treeView),"getConfig"),e8t=o((e,t,r,n,i,a)=>{for(;e<=Yu.records.stack[Yu.records.stack.length-1].level;)Yu.records.stack.pop();let s={id:Yu.records.cnt++,level:e,name:t,nodeType:r,icon:i,cssClass:n,description:a,children:[]};Yu.records.stack[Yu.records.stack.length-1].children.push(s),Yu.records.stack.push(s)},"addNode"),t8t={clear:X7t,addNode:e8t,getRoot:K7t,getCount:Z7t,getConfig:J7t,getAccTitle:Ar,getAccDescription:_r,getDiagramTitle:Lr,setAccDescription:Rr,setAccTitle:kr,setDiagramTitle:Or},Qw=t8t});var r8t,a9e,s9e=F(()=>{"use strict";Xa();ur();vt();Vr();Hs();i9e();Bj();r8t=o(e=>{Gn(e,Qw);for(let t of e.nodes){let r=typeof t.indent=="number"?t.indent:0,n=t.name,i=n.endsWith("/");i&&(n=n.slice(0,-1));let a=i?"directory":"file",s=t.classAnnotation||void 0,l=t.iconAnnotation,u=l!==void 0?l||"none":void 0,h=t.descAnnotation||void 0,d=h?mr(h,_t()):void 0;Qw.addNode(r,n,a,s,u,d)}},"populate"),a9e={parse:o(async e=>{let{text:t,lineMap:r}=n9e(e);try{let n=await Si("treeView",t);Z.debug(n),r8t(n)}catch(n){throw r.size>0&&n instanceof Error&&(n.message=r9e(n.message,r)),n}},"parse")}});function n8t(e,t){let r=t?.filenameIcons?.[e];if(r)return r;let n=e.lastIndexOf(".");if(n>0){let i=e.substring(n).toLowerCase(),a=t?.extensionIcons;return a?.[i]??a?.[i.slice(1)]}}function o9e(e,t){return e.includes(":")?e:e in px.icons||!t?`${px.prefix}:${e}`:`${t}:${e}`}function $j(e,t){if(e.icon!=="none"){if(e.icon)return o9e(e.icon,t.defaultIconPack);if(t.showIcons){if(e.nodeType==="file"){let r=n8t(e.name,t);if(r==="none")return;if(r)return o9e(r,t.defaultIconPack)}return`${px.prefix}:${e.nodeType==="directory"?"folder":"file"}`}}}var px,l9e=F(()=>{"use strict";px={prefix:"mermaid-treeview",height:24,width:24,icons:{folder:{body:''},file:{body:''}}};o(n8t,"detectIcon");o(o9e,"qualifyIcon");o($j,"getNodeIcon")});var Fj,i8t,a8t,u9e,s8t,o8t,c9e,l8t,c8t,u8t,h9e,d9e=F(()=>{"use strict";vt();Vl();Ka();$n();l9e();ty([{name:px.prefix,icons:px}]);Fj=14,i8t=4,a8t=16,u9e=o((e,t)=>`tv-icon-${e}-${t.replace(/[^\w-]/g,"-")}`,"iconSymbolId"),s8t=o(async(e,t,r,n)=>{let i=new Set,a=o(u=>{let h=$j(u,r);h&&i.add(h),u.children.forEach(a)},"collect");if(a(t),i.size===0)return;let s=await Promise.all([...i].map(async u=>({icon:u,svg:await ts(u,{height:Fj,width:Fj})}))),l=e.append("defs");for(let{icon:u,svg:h}of s)l.append("g").attr("id",u9e(n,u)).html(h)},"injectIconDefs"),o8t=o((e,t,r,n,i,a)=>{let s=n.append("g"),l="treeView-node-label";r.nodeType==="directory"&&(l+=" treeView-node-dir"),r.cssClass&&(l+=` ${r.cssClass}`);let u=Fj+i8t,h=$j(r,i),d=h!==void 0;h&&s.append("use").attr("xlink:href",`#${u9e(a,h)}`).attr("x",e+i.paddingX).attr("y",t+i.paddingY).attr("class","treeView-node-icon");let f=s.append("text").text(r.name).attr("dominant-baseline","middle").attr("class",l),{height:p,width:m}=f.node().getBBox(),g=p+i.paddingY*2,y=e+i.paddingX+(d?u:0);f.attr("x",y),f.attr("y",t+g/2);let v=y+m,x=m+i.paddingX*2+(d?u:0);return r.BBox={x:e,y:t,width:x,height:g},r.cssClass?.split(/\s+/).includes("highlight")&&s.insert("rect",":first-child").attr("x",e).attr("y",t+1).attr("width",0).attr("height",g-2).attr("rx",3).attr("class","treeView-highlight-bg"),{node:r,nodeGroup:s,labelRightEdge:v,centerY:t+g/2}},"positionLabel"),c9e=o((e,t,r,n,i,a)=>e.append("line").attr("x1",t).attr("y1",r).attr("x2",n).attr("y2",i).attr("stroke-width",a).attr("class","treeView-node-line"),"positionLine"),l8t=o((e,t,r,n)=>{let i=0,a=0,s=[],l=o((d,f,p,m)=>{let g=m*(p.rowIndent+p.paddingX),y=o8t(g,i,f,d,p,n);s.push(y);let{height:v,width:x}=f.BBox;c9e(d,g-p.rowIndent,i+v/2,g,i+v/2,p.lineThickness),a=Math.max(a,g+x),i+=v},"drawNode"),u=o((d,f=0)=>{l(e,d,r,f),d.children.forEach(y=>{u(y,f+1)});let{x:p,y:m,height:g}=d.BBox;if(d.children.length){let{y,height:v}=d.children[d.children.length-1].BBox;c9e(e,p+r.paddingX,m+g,p+r.paddingX,y+v/2+r.lineThickness/2,r.lineThickness)}},"processNode");u(t);let h=s.filter(d=>d.node.description);if(h.length>0){let f=Math.max(...s.map(p=>p.labelRightEdge))+a8t;for(let p of h){let g=p.nodeGroup.append("text").text(p.node.description).attr("dominant-baseline","middle").attr("class","treeView-node-description").attr("x",f).attr("y",p.centerY).node().getBBox();a=Math.max(a,f+g.width+r.paddingX)}}for(let d of s)if(d.node.cssClass?.split(/\s+/).includes("highlight")){let f=d.nodeGroup.select(".treeView-highlight-bg");if(!f.empty()){let p=a-d.node.BBox.x+8;f.attr("width",p),a=Math.max(a,d.node.BBox.x+p+2)}}return{totalHeight:i,totalWidth:a}},"drawTree"),c8t=o(async(e,t,r,n)=>{Z.debug(`Rendering treeView diagram +`+e);let i=n.db,a=i.getRoot(),s=i.getConfig(),l=xn(t);await s8t(l,a,s,t);let u=l.append("g");u.attr("class","tree-view");let{totalHeight:h,totalWidth:d}=l8t(u,a,s,t);l.attr("viewBox",`-${s.lineThickness/2} 0 ${d} ${h}`),Wr(l,h,d,s.useMaxWidth)},"draw"),u8t={draw:c8t},h9e=u8t});var h8t,d8t,f9e,p9e=F(()=>{"use strict";Qt();h8t={labelFontSize:"16px",labelColor:"black",lineColor:"black",iconColor:"#546e7a",descriptionColor:"#6a9955",highlightBg:"rgba(255, 193, 7, 0.15)",highlightStroke:"#ffc107"},d8t=o(({treeView:e})=>{let{labelFontSize:t,labelColor:r,lineColor:n,iconColor:i,descriptionColor:a,highlightBg:s,highlightStroke:l}=qr(h8t,e);return` + .treeView-node-label { + font-size: ${t}; + fill: ${r}; + white-space: pre; + } + .treeView-node-dir { + font-weight: bold; + } + .treeView-node-line { + stroke: ${n}; + } + .treeView-node-icon { + color: ${i}; + } + .treeView-node-description { + font-size: ${t}; + fill: ${a}; + font-style: italic; + white-space: pre; + } + .treeView-highlight-bg { + fill: ${s}; + stroke: ${l}; + stroke-width: 1; + } + `},"styles"),f9e=d8t});var m9e={};ir(m9e,{diagram:()=>f8t});var f8t,g9e=F(()=>{"use strict";s9e();Bj();d9e();p9e();f8t={db:Qw,renderer:h9e,parser:a9e,styles:f9e}});var zj,Gj,Jw,x9e,Vj,ms,ju,ek,b9e,y8t,tk,T9e,C9e,w9e,k9e,S9e,iD,aD,Ip,sD=F(()=>{"use strict";zj={L:"left",R:"right",T:"top",B:"bottom"},Gj={L:o(e=>`${e},${e/2} 0,${e} 0,0`,"L"),R:o(e=>`0,${e/2} ${e},0 ${e},${e}`,"R"),T:o(e=>`0,0 ${e},0 ${e/2},${e}`,"T"),B:o(e=>`${e/2},0 ${e},${e} 0,${e}`,"B")},Jw={L:o((e,t)=>e-t+2,"L"),R:o((e,t)=>e-2,"R"),T:o((e,t)=>e-t+2,"T"),B:o((e,t)=>e-2,"B")},x9e=o(function(e){return ms(e)?e==="L"?"R":"L":e==="T"?"B":"T"},"getOppositeArchitectureDirection"),Vj=o(function(e){let t=e;return t==="L"||t==="R"||t==="T"||t==="B"},"isArchitectureDirection"),ms=o(function(e){let t=e;return t==="L"||t==="R"},"isArchitectureDirectionX"),ju=o(function(e){let t=e;return t==="T"||t==="B"},"isArchitectureDirectionY"),ek=o(function(e,t){let r=ms(e)&&ju(t),n=ju(e)&&ms(t);return r||n},"isArchitectureDirectionXY"),b9e=o(function(e){let t=e[0],r=e[1],n=ms(t)&&ju(r),i=ju(t)&&ms(r);return n||i},"isArchitecturePairXY"),y8t=o(function(e){return e!=="LL"&&e!=="RR"&&e!=="TT"&&e!=="BB"},"isValidArchitectureDirectionPair"),tk=o(function(e,t){let r=`${e}${t}`;return y8t(r)?r:void 0},"getArchitectureDirectionPair"),T9e=o(function([e,t],r){let n=r[0],i=r[1];return ms(n)?ju(i)?[e+(n==="L"?-1:1),t+(i==="T"?1:-1)]:[e+(n==="L"?-1:1),t]:ms(i)?[e+(i==="L"?1:-1),t+(n==="T"?1:-1)]:[e,t+(n==="T"?1:-1)]},"shiftPositionByArchitectureDirectionPair"),C9e=o(function(e){return e==="LT"||e==="TL"?[1,1]:e==="BL"||e==="LB"?[1,-1]:e==="BR"||e==="RB"?[-1,-1]:[-1,1]},"getArchitectureDirectionXYFactors"),w9e=o(function(e,t){return ek(e,t)?"bend":ms(e)?"horizontal":"vertical"},"getArchitectureDirectionAlignment"),k9e=o(function(e){return e.type==="service"},"isArchitectureService"),S9e=o(function(e){return e.type==="junction"},"isArchitectureJunction"),iD=o((e,t)=>{let[r,n]=[e,t].sort();return`${JSON.stringify(r)}-${JSON.stringify(n)}`},"architectureGroupAlignmentKey"),aD=o(e=>e.data(),"edgeData"),Ip=o(e=>e.data(),"nodeData")});var v8t,mx,Wj=F(()=>{"use strict";ur();Wi();Qt();Nn();sD();v8t=cr.architecture,mx=class{constructor(){this.nodes=new Map;this.groups=new Map;this.edges=[];this.layoutHints=[];this.registeredIds=new Map;this.elements=new Map;this.diagramId="";this.setAccTitle=kr;this.getAccTitle=Ar;this.setDiagramTitle=Or;this.getDiagramTitle=Lr;this.getAccDescription=_r;this.setAccDescription=Rr;this.clear()}static{o(this,"ArchitectureDB")}setDiagramId(t){this.diagramId=t}getDiagramId(){return this.diagramId}clear(){this.nodes=new Map,this.groups=new Map,this.edges=[],this.layoutHints=[],this.registeredIds=new Map,this.dataStructures=void 0,this.elements=new Map,this.diagramId="",yr()}addService({id:t,icon:r,in:n,title:i,iconText:a}){if(this.registeredIds.has(t))throw new Error(`The service id [${t}] is already in use by another ${this.registeredIds.get(t)}`);if(n!==void 0){if(t===n)throw new Error(`The service [${t}] cannot be placed within itself`);if(!this.registeredIds.has(n))throw new Error(`The service [${t}]'s parent does not exist. Please make sure the parent is created before this service`);if(this.registeredIds.get(n)==="node")throw new Error(`The service [${t}]'s parent is not a group`)}this.registeredIds.set(t,"node"),this.nodes.set(t,{id:t,type:"service",icon:r,iconText:a,title:i,edges:[],in:n})}getServices(){return[...this.nodes.values()].filter(k9e)}addJunction({id:t,in:r}){if(this.registeredIds.has(t))throw new Error(`The junction id [${t}] is already in use by another ${this.registeredIds.get(t)}`);if(r!==void 0){if(t===r)throw new Error(`The junction [${t}] cannot be placed within itself`);if(!this.registeredIds.has(r))throw new Error(`The junction [${t}]'s parent does not exist. Please make sure the parent is created before this junction`);if(this.registeredIds.get(r)==="node")throw new Error(`The junction [${t}]'s parent is not a group`)}this.registeredIds.set(t,"node"),this.nodes.set(t,{id:t,type:"junction",edges:[],in:r})}getJunctions(){return[...this.nodes.values()].filter(S9e)}getNodes(){return[...this.nodes.values()]}getNode(t){return this.nodes.get(t)??null}addGroup({id:t,icon:r,in:n,title:i}){if(this.registeredIds.has(t))throw new Error(`The group id [${t}] is already in use by another ${this.registeredIds.get(t)}`);if(n!==void 0){if(t===n)throw new Error(`The group [${t}] cannot be placed within itself`);if(!this.registeredIds.has(n))throw new Error(`The group [${t}]'s parent does not exist. Please make sure the parent is created before this group`);if(this.registeredIds.get(n)==="node")throw new Error(`The group [${t}]'s parent is not a group`)}this.registeredIds.set(t,"group"),this.groups.set(t,{id:t,icon:r,title:i,in:n})}getGroups(){return[...this.groups.values()]}addEdge({lhsId:t,rhsId:r,lhsDir:n,rhsDir:i,lhsInto:a,rhsInto:s,lhsGroup:l,rhsGroup:u,title:h}){if(!Vj(n))throw new Error(`Invalid direction given for left hand side of edge ${t}--${r}. Expected (L,R,T,B) got ${String(n)}`);if(!Vj(i))throw new Error(`Invalid direction given for right hand side of edge ${t}--${r}. Expected (L,R,T,B) got ${String(i)}`);if(!this.nodes.has(t)&&!this.groups.has(t))throw new Error(`The left-hand id [${t}] does not yet exist. Please create the service/group before declaring an edge to it.`);if(!this.nodes.has(r)&&!this.groups.has(r))throw new Error(`The right-hand id [${r}] does not yet exist. Please create the service/group before declaring an edge to it.`);let d=this.nodes.get(t).in,f=this.nodes.get(r).in;if(l&&d&&f&&d==f)throw new Error(`The left-hand id [${t}] is modified to traverse the group boundary, but the edge does not pass through two groups.`);if(u&&d&&f&&d==f)throw new Error(`The right-hand id [${r}] is modified to traverse the group boundary, but the edge does not pass through two groups.`);let p={lhsId:t,lhsDir:n,lhsInto:a,lhsGroup:l,rhsId:r,rhsDir:i,rhsInto:s,rhsGroup:u,title:h};this.edges.push(p);let m=this.nodes.get(t),g=this.nodes.get(r);m&&g&&(m.edges.push(this.edges[this.edges.length-1]),g.edges.push(this.edges[this.edges.length-1]))}getEdges(){return this.edges}addLayoutHint(t){if(t.members.length<2)throw new Error(`An align directive requires at least two members; got ${t.members.length}`);let r=new Set;t.members.forEach(n=>{if(this.registeredIds.get(n)!=="node")throw new Error(`align ${t.direction} references [${n}], which is not a service or junction`);if(r.has(n))throw new Error(`align ${t.direction} lists [${n}] more than once`);r.add(n)}),this.layoutHints.push(t)}getLayoutHints(){return this.layoutHints}getDataStructures(){if(this.dataStructures===void 0){let t=new Map,r=new Map;for(let[l,u]of this.nodes.entries()){let h=new Map;for(let d of u.edges){let f=this.getNode(d.lhsId)?.in,p=this.getNode(d.rhsId)?.in;if(f&&p&&f!==p){let m=w9e(d.lhsDir,d.rhsDir);m!=="bend"&&t.set(iD(f,p),m)}if(d.lhsId===l){let m=tk(d.lhsDir,d.rhsDir);m&&h.set(m,d.rhsId)}else{let m=tk(d.rhsDir,d.lhsDir);m&&h.set(m,d.lhsId)}}r.set(l,h)}let n=new Set,i=new Set(r.keys()),a=o(l=>{let u=new Map([[l,[0,0]]]),h=[l];for(;h.length>0;){let d=h.shift();if(d){n.add(d),i.delete(d);let f=r.get(d);if(!f)throw new Error(`BFS error: adjacency list for id ${d} not found. Please report this as a bug.`);let p=u.get(d);if(!p)throw new Error(`BFS error: position for id ${d} not found in spatial map. Please report this as a bug.`);let[m,g]=p;f.forEach((y,v)=>{n.has(y)||(u.set(y,T9e([m,g],v)),h.push(y))})}}return u},"BFS"),s=[];for(;i.size>0;){let l=i.values().next().value;s.push(a(l))}this.dataStructures={adjList:r,spatialMaps:s,groupAlignments:t}}return this.dataStructures}setElementForId(t,r){this.elements.set(t,r)}getElementById(t){return this.elements.get(t)}getConfig(){return qr({...v8t,..._t().architecture})}getConfigField(t){return this.getConfig()[t]}}});var x8t,qj,E9e=F(()=>{"use strict";Xa();vt();Hs();Wj();x8t=o((e,t)=>{Gn(e,t),e.groups.map(r=>t.addGroup(r)),e.services.map(r=>t.addService({...r,type:"service"})),e.junctions.map(r=>t.addJunction({...r,type:"junction"})),e.edges.map(r=>t.addEdge(r)),e.alignments?.map(r=>t.addLayoutHint({direction:r.direction,members:[...r.members]}))},"populateDb"),qj={parser:{yy:void 0},parse:o(async e=>{let t=await Si("architecture",e);Z.debug(t);let r=qj.parser?.yy;if(!(r instanceof mx))throw new Error("parser.parser?.yy was not a ArchitectureDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.");x8t(t,r)},"parse")}});var b8t,A9e,R9e=F(()=>{"use strict";b8t=o(e=>` + .edge { + stroke-width: ${e.archEdgeWidth}; + stroke: ${e.archEdgeColor}; + fill: none; + } + + .arrow { + fill: ${e.archEdgeArrowColor}; + } + + .node-bkg { + fill: none; + stroke: ${e.archGroupBorderColor}; + stroke-width: ${e.archGroupBorderWidth}; + stroke-dasharray: 8; + } + .node-icon-text { + display: flex; + align-items: center; + } + + .node-icon-text > div { + color: #fff; + margin: 1px; + height: fit-content; + text-align: center; + overflow: hidden; + display: -webkit-box; + -webkit-box-orient: vertical; + } +`,"getStyles"),A9e=b8t});var Uj=Io((rk,Hj)=>{"use strict";o((function(t,r){typeof rk=="object"&&typeof Hj=="object"?Hj.exports=r():typeof define=="function"&&define.amd?define([],r):typeof rk=="object"?rk.layoutBase=r():t.layoutBase=r()}),"webpackUniversalModuleDefinition")(rk,function(){return(function(e){var t={};function r(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}return o(r,"__webpack_require__"),r.m=e,r.c=t,r.i=function(n){return n},r.d=function(n,i,a){r.o(n,i)||Object.defineProperty(n,i,{configurable:!1,enumerable:!0,get:a})},r.n=function(n){var i=n&&n.__esModule?o(function(){return n.default},"getDefault"):o(function(){return n},"getModuleExports");return r.d(i,"a",i),i},r.o=function(n,i){return Object.prototype.hasOwnProperty.call(n,i)},r.p="",r(r.s=28)})([(function(e,t,r){"use strict";function n(){}o(n,"LayoutConstants"),n.QUALITY=1,n.DEFAULT_CREATE_BENDS_AS_NEEDED=!1,n.DEFAULT_INCREMENTAL=!1,n.DEFAULT_ANIMATION_ON_LAYOUT=!0,n.DEFAULT_ANIMATION_DURING_LAYOUT=!1,n.DEFAULT_ANIMATION_PERIOD=50,n.DEFAULT_UNIFORM_LEAF_NODE_SIZES=!1,n.DEFAULT_GRAPH_MARGIN=15,n.NODE_DIMENSIONS_INCLUDE_LABELS=!1,n.SIMPLE_NODE_SIZE=40,n.SIMPLE_NODE_HALF_SIZE=n.SIMPLE_NODE_SIZE/2,n.EMPTY_COMPOUND_NODE_SIZE=40,n.MIN_EDGE_LENGTH=1,n.WORLD_BOUNDARY=1e6,n.INITIAL_WORLD_BOUNDARY=n.WORLD_BOUNDARY/1e3,n.WORLD_CENTER_X=1200,n.WORLD_CENTER_Y=900,e.exports=n}),(function(e,t,r){"use strict";var n=r(2),i=r(8),a=r(9);function s(u,h,d){n.call(this,d),this.isOverlapingSourceAndTarget=!1,this.vGraphObject=d,this.bendpoints=[],this.source=u,this.target=h}o(s,"LEdge"),s.prototype=Object.create(n.prototype);for(var l in n)s[l]=n[l];s.prototype.getSource=function(){return this.source},s.prototype.getTarget=function(){return this.target},s.prototype.isInterGraph=function(){return this.isInterGraph},s.prototype.getLength=function(){return this.length},s.prototype.isOverlapingSourceAndTarget=function(){return this.isOverlapingSourceAndTarget},s.prototype.getBendpoints=function(){return this.bendpoints},s.prototype.getLca=function(){return this.lca},s.prototype.getSourceInLca=function(){return this.sourceInLca},s.prototype.getTargetInLca=function(){return this.targetInLca},s.prototype.getOtherEnd=function(u){if(this.source===u)return this.target;if(this.target===u)return this.source;throw"Node is not incident with this edge"},s.prototype.getOtherEndInGraph=function(u,h){for(var d=this.getOtherEnd(u),f=h.getGraphManager().getRoot();;){if(d.getOwner()==h)return d;if(d.getOwner()==f)break;d=d.getOwner().getParent()}return null},s.prototype.updateLength=function(){var u=new Array(4);this.isOverlapingSourceAndTarget=i.getIntersection(this.target.getRect(),this.source.getRect(),u),this.isOverlapingSourceAndTarget||(this.lengthX=u[0]-u[2],this.lengthY=u[1]-u[3],Math.abs(this.lengthX)<1&&(this.lengthX=a.sign(this.lengthX)),Math.abs(this.lengthY)<1&&(this.lengthY=a.sign(this.lengthY)),this.length=Math.sqrt(this.lengthX*this.lengthX+this.lengthY*this.lengthY))},s.prototype.updateLengthSimple=function(){this.lengthX=this.target.getCenterX()-this.source.getCenterX(),this.lengthY=this.target.getCenterY()-this.source.getCenterY(),Math.abs(this.lengthX)<1&&(this.lengthX=a.sign(this.lengthX)),Math.abs(this.lengthY)<1&&(this.lengthY=a.sign(this.lengthY)),this.length=Math.sqrt(this.lengthX*this.lengthX+this.lengthY*this.lengthY)},e.exports=s}),(function(e,t,r){"use strict";function n(i){this.vGraphObject=i}o(n,"LGraphObject"),e.exports=n}),(function(e,t,r){"use strict";var n=r(2),i=r(10),a=r(13),s=r(0),l=r(16),u=r(5);function h(f,p,m,g){m==null&&g==null&&(g=p),n.call(this,g),f.graphManager!=null&&(f=f.graphManager),this.estimatedSize=i.MIN_VALUE,this.inclusionTreeDepth=i.MAX_VALUE,this.vGraphObject=g,this.edges=[],this.graphManager=f,m!=null&&p!=null?this.rect=new a(p.x,p.y,m.width,m.height):this.rect=new a}o(h,"LNode"),h.prototype=Object.create(n.prototype);for(var d in n)h[d]=n[d];h.prototype.getEdges=function(){return this.edges},h.prototype.getChild=function(){return this.child},h.prototype.getOwner=function(){return this.owner},h.prototype.getWidth=function(){return this.rect.width},h.prototype.setWidth=function(f){this.rect.width=f},h.prototype.getHeight=function(){return this.rect.height},h.prototype.setHeight=function(f){this.rect.height=f},h.prototype.getCenterX=function(){return this.rect.x+this.rect.width/2},h.prototype.getCenterY=function(){return this.rect.y+this.rect.height/2},h.prototype.getCenter=function(){return new u(this.rect.x+this.rect.width/2,this.rect.y+this.rect.height/2)},h.prototype.getLocation=function(){return new u(this.rect.x,this.rect.y)},h.prototype.getRect=function(){return this.rect},h.prototype.getDiagonal=function(){return Math.sqrt(this.rect.width*this.rect.width+this.rect.height*this.rect.height)},h.prototype.getHalfTheDiagonal=function(){return Math.sqrt(this.rect.height*this.rect.height+this.rect.width*this.rect.width)/2},h.prototype.setRect=function(f,p){this.rect.x=f.x,this.rect.y=f.y,this.rect.width=p.width,this.rect.height=p.height},h.prototype.setCenter=function(f,p){this.rect.x=f-this.rect.width/2,this.rect.y=p-this.rect.height/2},h.prototype.setLocation=function(f,p){this.rect.x=f,this.rect.y=p},h.prototype.moveBy=function(f,p){this.rect.x+=f,this.rect.y+=p},h.prototype.getEdgeListToNode=function(f){var p=[],m,g=this;return g.edges.forEach(function(y){if(y.target==f){if(y.source!=g)throw"Incorrect edge source!";p.push(y)}}),p},h.prototype.getEdgesBetween=function(f){var p=[],m,g=this;return g.edges.forEach(function(y){if(!(y.source==g||y.target==g))throw"Incorrect edge source and/or target";(y.target==f||y.source==f)&&p.push(y)}),p},h.prototype.getNeighborsList=function(){var f=new Set,p=this;return p.edges.forEach(function(m){if(m.source==p)f.add(m.target);else{if(m.target!=p)throw"Incorrect incidency!";f.add(m.source)}}),f},h.prototype.withChildren=function(){var f=new Set,p,m;if(f.add(this),this.child!=null)for(var g=this.child.getNodes(),y=0;yp?(this.rect.x-=(this.labelWidth-p)/2,this.setWidth(this.labelWidth)):this.labelPosHorizontal=="right"&&this.setWidth(p+this.labelWidth)),this.labelHeight&&(this.labelPosVertical=="top"?(this.rect.y-=this.labelHeight,this.setHeight(m+this.labelHeight)):this.labelPosVertical=="center"&&this.labelHeight>m?(this.rect.y-=(this.labelHeight-m)/2,this.setHeight(this.labelHeight)):this.labelPosVertical=="bottom"&&this.setHeight(m+this.labelHeight))}}},h.prototype.getInclusionTreeDepth=function(){if(this.inclusionTreeDepth==i.MAX_VALUE)throw"assert failed";return this.inclusionTreeDepth},h.prototype.transform=function(f){var p=this.rect.x;p>s.WORLD_BOUNDARY?p=s.WORLD_BOUNDARY:p<-s.WORLD_BOUNDARY&&(p=-s.WORLD_BOUNDARY);var m=this.rect.y;m>s.WORLD_BOUNDARY?m=s.WORLD_BOUNDARY:m<-s.WORLD_BOUNDARY&&(m=-s.WORLD_BOUNDARY);var g=new u(p,m),y=f.inverseTransformPoint(g);this.setLocation(y.x,y.y)},h.prototype.getLeft=function(){return this.rect.x},h.prototype.getRight=function(){return this.rect.x+this.rect.width},h.prototype.getTop=function(){return this.rect.y},h.prototype.getBottom=function(){return this.rect.y+this.rect.height},h.prototype.getParent=function(){return this.owner==null?null:this.owner.getParent()},e.exports=h}),(function(e,t,r){"use strict";var n=r(0);function i(){}o(i,"FDLayoutConstants");for(var a in n)i[a]=n[a];i.MAX_ITERATIONS=2500,i.DEFAULT_EDGE_LENGTH=50,i.DEFAULT_SPRING_STRENGTH=.45,i.DEFAULT_REPULSION_STRENGTH=4500,i.DEFAULT_GRAVITY_STRENGTH=.4,i.DEFAULT_COMPOUND_GRAVITY_STRENGTH=1,i.DEFAULT_GRAVITY_RANGE_FACTOR=3.8,i.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR=1.5,i.DEFAULT_USE_SMART_IDEAL_EDGE_LENGTH_CALCULATION=!0,i.DEFAULT_USE_SMART_REPULSION_RANGE_CALCULATION=!0,i.DEFAULT_COOLING_FACTOR_INCREMENTAL=.3,i.COOLING_ADAPTATION_FACTOR=.33,i.ADAPTATION_LOWER_NODE_LIMIT=1e3,i.ADAPTATION_UPPER_NODE_LIMIT=5e3,i.MAX_NODE_DISPLACEMENT_INCREMENTAL=100,i.MAX_NODE_DISPLACEMENT=i.MAX_NODE_DISPLACEMENT_INCREMENTAL*3,i.MIN_REPULSION_DIST=i.DEFAULT_EDGE_LENGTH/10,i.CONVERGENCE_CHECK_PERIOD=100,i.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR=.1,i.MIN_EDGE_LENGTH=1,i.GRID_CALCULATION_CHECK_PERIOD=10,e.exports=i}),(function(e,t,r){"use strict";function n(i,a){i==null&&a==null?(this.x=0,this.y=0):(this.x=i,this.y=a)}o(n,"PointD"),n.prototype.getX=function(){return this.x},n.prototype.getY=function(){return this.y},n.prototype.setX=function(i){this.x=i},n.prototype.setY=function(i){this.y=i},n.prototype.getDifference=function(i){return new DimensionD(this.x-i.x,this.y-i.y)},n.prototype.getCopy=function(){return new n(this.x,this.y)},n.prototype.translate=function(i){return this.x+=i.width,this.y+=i.height,this},e.exports=n}),(function(e,t,r){"use strict";var n=r(2),i=r(10),a=r(0),s=r(7),l=r(3),u=r(1),h=r(13),d=r(12),f=r(11);function p(g,y,v){n.call(this,v),this.estimatedSize=i.MIN_VALUE,this.margin=a.DEFAULT_GRAPH_MARGIN,this.edges=[],this.nodes=[],this.isConnected=!1,this.parent=g,y!=null&&y instanceof s?this.graphManager=y:y!=null&&y instanceof Layout&&(this.graphManager=y.graphManager)}o(p,"LGraph"),p.prototype=Object.create(n.prototype);for(var m in n)p[m]=n[m];p.prototype.getNodes=function(){return this.nodes},p.prototype.getEdges=function(){return this.edges},p.prototype.getGraphManager=function(){return this.graphManager},p.prototype.getParent=function(){return this.parent},p.prototype.getLeft=function(){return this.left},p.prototype.getRight=function(){return this.right},p.prototype.getTop=function(){return this.top},p.prototype.getBottom=function(){return this.bottom},p.prototype.isConnected=function(){return this.isConnected},p.prototype.add=function(g,y,v){if(y==null&&v==null){var x=g;if(this.graphManager==null)throw"Graph has no graph mgr!";if(this.getNodes().indexOf(x)>-1)throw"Node already in graph!";return x.owner=this,this.getNodes().push(x),x}else{var b=g;if(!(this.getNodes().indexOf(y)>-1&&this.getNodes().indexOf(v)>-1))throw"Source or target not in graph!";if(!(y.owner==v.owner&&y.owner==this))throw"Both owners must be this graph!";return y.owner!=v.owner?null:(b.source=y,b.target=v,b.isInterGraph=!1,this.getEdges().push(b),y.edges.push(b),v!=y&&v.edges.push(b),b)}},p.prototype.remove=function(g){var y=g;if(g instanceof l){if(y==null)throw"Node is null!";if(!(y.owner!=null&&y.owner==this))throw"Owner graph is invalid!";if(this.graphManager==null)throw"Owner graph manager is invalid!";for(var v=y.edges.slice(),x,b=v.length,T=0;T-1&&w>-1))throw"Source and/or target doesn't know this edge!";x.source.edges.splice(C,1),x.target!=x.source&&x.target.edges.splice(w,1);var k=x.source.owner.getEdges().indexOf(x);if(k==-1)throw"Not in owner's edge list!";x.source.owner.getEdges().splice(k,1)}},p.prototype.updateLeftTop=function(){for(var g=i.MAX_VALUE,y=i.MAX_VALUE,v,x,b,T=this.getNodes(),k=T.length,C=0;Cv&&(g=v),y>x&&(y=x)}return g==i.MAX_VALUE?null:(T[0].getParent().paddingLeft!=null?b=T[0].getParent().paddingLeft:b=this.margin,this.left=y-b,this.top=g-b,new d(this.left,this.top))},p.prototype.updateBounds=function(g){for(var y=i.MAX_VALUE,v=-i.MAX_VALUE,x=i.MAX_VALUE,b=-i.MAX_VALUE,T,k,C,w,S,R=this.nodes,L=R.length,N=0;NT&&(y=T),vC&&(x=C),bT&&(y=T),vC&&(x=C),b=this.nodes.length){var L=0;v.forEach(function(N){N.owner==g&&L++}),L==this.nodes.length&&(this.isConnected=!0)}},e.exports=p}),(function(e,t,r){"use strict";var n,i=r(1);function a(s){n=r(6),this.layout=s,this.graphs=[],this.edges=[]}o(a,"LGraphManager"),a.prototype.addRoot=function(){var s=this.layout.newGraph(),l=this.layout.newNode(null),u=this.add(s,l);return this.setRootGraph(u),this.rootGraph},a.prototype.add=function(s,l,u,h,d){if(u==null&&h==null&&d==null){if(s==null)throw"Graph is null!";if(l==null)throw"Parent node is null!";if(this.graphs.indexOf(s)>-1)throw"Graph already in this graph mgr!";if(this.graphs.push(s),s.parent!=null)throw"Already has a parent!";if(l.child!=null)throw"Already has a child!";return s.parent=l,l.child=s,s}else{d=u,h=l,u=s;var f=h.getOwner(),p=d.getOwner();if(!(f!=null&&f.getGraphManager()==this))throw"Source not in this graph mgr!";if(!(p!=null&&p.getGraphManager()==this))throw"Target not in this graph mgr!";if(f==p)return u.isInterGraph=!1,f.add(u,h,d);if(u.isInterGraph=!0,u.source=h,u.target=d,this.edges.indexOf(u)>-1)throw"Edge already in inter-graph edge list!";if(this.edges.push(u),!(u.source!=null&&u.target!=null))throw"Edge source and/or target is null!";if(!(u.source.edges.indexOf(u)==-1&&u.target.edges.indexOf(u)==-1))throw"Edge already in source and/or target incidency list!";return u.source.edges.push(u),u.target.edges.push(u),u}},a.prototype.remove=function(s){if(s instanceof n){var l=s;if(l.getGraphManager()!=this)throw"Graph not in this graph mgr";if(!(l==this.rootGraph||l.parent!=null&&l.parent.graphManager==this))throw"Invalid parent node!";var u=[];u=u.concat(l.getEdges());for(var h,d=u.length,f=0;f=s.getRight()?l[0]+=Math.min(s.getX()-a.getX(),a.getRight()-s.getRight()):s.getX()<=a.getX()&&s.getRight()>=a.getRight()&&(l[0]+=Math.min(a.getX()-s.getX(),s.getRight()-a.getRight())),a.getY()<=s.getY()&&a.getBottom()>=s.getBottom()?l[1]+=Math.min(s.getY()-a.getY(),a.getBottom()-s.getBottom()):s.getY()<=a.getY()&&s.getBottom()>=a.getBottom()&&(l[1]+=Math.min(a.getY()-s.getY(),s.getBottom()-a.getBottom()));var d=Math.abs((s.getCenterY()-a.getCenterY())/(s.getCenterX()-a.getCenterX()));s.getCenterY()===a.getCenterY()&&s.getCenterX()===a.getCenterX()&&(d=1);var f=d*l[0],p=l[1]/d;l[0]f)return l[0]=u,l[1]=m,l[2]=d,l[3]=R,!1;if(hd)return l[0]=p,l[1]=h,l[2]=w,l[3]=f,!1;if(ud?(l[0]=y,l[1]=v,_=!0):(l[0]=g,l[1]=m,_=!0):M===P&&(u>d?(l[0]=p,l[1]=m,_=!0):(l[0]=x,l[1]=v,_=!0)),-D===P?d>u?(l[2]=S,l[3]=R,A=!0):(l[2]=w,l[3]=C,A=!0):D===P&&(d>u?(l[2]=k,l[3]=C,A=!0):(l[2]=L,l[3]=R,A=!0)),_&&A)return!1;if(u>d?h>f?(B=this.getCardinalDirection(M,P,4),O=this.getCardinalDirection(D,P,2)):(B=this.getCardinalDirection(-M,P,3),O=this.getCardinalDirection(-D,P,1)):h>f?(B=this.getCardinalDirection(-M,P,1),O=this.getCardinalDirection(-D,P,3)):(B=this.getCardinalDirection(M,P,2),O=this.getCardinalDirection(D,P,4)),!_)switch(B){case 1:V=m,$=u+-T/P,l[0]=$,l[1]=V;break;case 2:$=x,V=h+b*P,l[0]=$,l[1]=V;break;case 3:V=v,$=u+T/P,l[0]=$,l[1]=V;break;case 4:$=y,V=h+-b*P,l[0]=$,l[1]=V;break}if(!A)switch(O){case 1:z=C,G=d+-I/P,l[2]=G,l[3]=z;break;case 2:G=L,z=f+N*P,l[2]=G,l[3]=z;break;case 3:z=R,G=d+I/P,l[2]=G,l[3]=z;break;case 4:G=S,z=f+-N*P,l[2]=G,l[3]=z;break}}return!1},i.getCardinalDirection=function(a,s,l){return a>s?l:1+l%4},i.getIntersection=function(a,s,l,u){if(u==null)return this.getIntersection2(a,s,l);var h=a.x,d=a.y,f=s.x,p=s.y,m=l.x,g=l.y,y=u.x,v=u.y,x=void 0,b=void 0,T=void 0,k=void 0,C=void 0,w=void 0,S=void 0,R=void 0,L=void 0;return T=p-d,C=h-f,S=f*d-h*p,k=v-g,w=m-y,R=y*g-m*v,L=T*w-k*C,L===0?null:(x=(C*R-w*S)/L,b=(k*S-T*R)/L,new n(x,b))},i.angleOfVector=function(a,s,l,u){var h=void 0;return a!==l?(h=Math.atan((u-s)/(l-a)),l=0){var v=(-m+Math.sqrt(m*m-4*p*g))/(2*p),x=(-m-Math.sqrt(m*m-4*p*g))/(2*p),b=null;return v>=0&&v<=1?[v]:x>=0&&x<=1?[x]:b}else return null},i.HALF_PI=.5*Math.PI,i.ONE_AND_HALF_PI=1.5*Math.PI,i.TWO_PI=2*Math.PI,i.THREE_PI=3*Math.PI,e.exports=i}),(function(e,t,r){"use strict";function n(){}o(n,"IMath"),n.sign=function(i){return i>0?1:i<0?-1:0},n.floor=function(i){return i<0?Math.ceil(i):Math.floor(i)},n.ceil=function(i){return i<0?Math.floor(i):Math.ceil(i)},e.exports=n}),(function(e,t,r){"use strict";function n(){}o(n,"Integer"),n.MAX_VALUE=2147483647,n.MIN_VALUE=-2147483648,e.exports=n}),(function(e,t,r){"use strict";var n=(function(){function h(d,f){for(var p=0;p"u"?"undefined":n(a);return a==null||s!="object"&&s!="function"},e.exports=i}),(function(e,t,r){"use strict";function n(m){if(Array.isArray(m)){for(var g=0,y=Array(m.length);g0&&g;){for(T.push(C[0]);T.length>0&&g;){var w=T[0];T.splice(0,1),b.add(w);for(var S=w.getEdges(),x=0;x-1&&C.splice(I,1)}b=new Set,k=new Map}}return m},p.prototype.createDummyNodesForBendpoints=function(m){for(var g=[],y=m.source,v=this.graphManager.calcLowestCommonAncestor(m.source,m.target),x=0;x0){for(var v=this.edgeToDummyNodes.get(y),x=0;x=0&&g.splice(R,1);var L=k.getNeighborsList();L.forEach(function(_){if(y.indexOf(_)<0){var A=v.get(_),M=A-1;M==1&&w.push(_),v.set(_,M)}})}y=y.concat(w),(g.length==1||g.length==2)&&(x=!0,b=g[0])}return b},p.prototype.setGraphManager=function(m){this.graphManager=m},e.exports=p}),(function(e,t,r){"use strict";function n(){}o(n,"RandomSeed"),n.seed=1,n.x=0,n.nextDouble=function(){return n.x=Math.sin(n.seed++)*1e4,n.x-Math.floor(n.x)},e.exports=n}),(function(e,t,r){"use strict";var n=r(5);function i(a,s){this.lworldOrgX=0,this.lworldOrgY=0,this.ldeviceOrgX=0,this.ldeviceOrgY=0,this.lworldExtX=1,this.lworldExtY=1,this.ldeviceExtX=1,this.ldeviceExtY=1}o(i,"Transform"),i.prototype.getWorldOrgX=function(){return this.lworldOrgX},i.prototype.setWorldOrgX=function(a){this.lworldOrgX=a},i.prototype.getWorldOrgY=function(){return this.lworldOrgY},i.prototype.setWorldOrgY=function(a){this.lworldOrgY=a},i.prototype.getWorldExtX=function(){return this.lworldExtX},i.prototype.setWorldExtX=function(a){this.lworldExtX=a},i.prototype.getWorldExtY=function(){return this.lworldExtY},i.prototype.setWorldExtY=function(a){this.lworldExtY=a},i.prototype.getDeviceOrgX=function(){return this.ldeviceOrgX},i.prototype.setDeviceOrgX=function(a){this.ldeviceOrgX=a},i.prototype.getDeviceOrgY=function(){return this.ldeviceOrgY},i.prototype.setDeviceOrgY=function(a){this.ldeviceOrgY=a},i.prototype.getDeviceExtX=function(){return this.ldeviceExtX},i.prototype.setDeviceExtX=function(a){this.ldeviceExtX=a},i.prototype.getDeviceExtY=function(){return this.ldeviceExtY},i.prototype.setDeviceExtY=function(a){this.ldeviceExtY=a},i.prototype.transformX=function(a){var s=0,l=this.lworldExtX;return l!=0&&(s=this.ldeviceOrgX+(a-this.lworldOrgX)*this.ldeviceExtX/l),s},i.prototype.transformY=function(a){var s=0,l=this.lworldExtY;return l!=0&&(s=this.ldeviceOrgY+(a-this.lworldOrgY)*this.ldeviceExtY/l),s},i.prototype.inverseTransformX=function(a){var s=0,l=this.ldeviceExtX;return l!=0&&(s=this.lworldOrgX+(a-this.ldeviceOrgX)*this.lworldExtX/l),s},i.prototype.inverseTransformY=function(a){var s=0,l=this.ldeviceExtY;return l!=0&&(s=this.lworldOrgY+(a-this.ldeviceOrgY)*this.lworldExtY/l),s},i.prototype.inverseTransformPoint=function(a){var s=new n(this.inverseTransformX(a.x),this.inverseTransformY(a.y));return s},e.exports=i}),(function(e,t,r){"use strict";function n(f){if(Array.isArray(f)){for(var p=0,m=Array(f.length);pa.ADAPTATION_LOWER_NODE_LIMIT&&(this.coolingFactor=Math.max(this.coolingFactor*a.COOLING_ADAPTATION_FACTOR,this.coolingFactor-(f-a.ADAPTATION_LOWER_NODE_LIMIT)/(a.ADAPTATION_UPPER_NODE_LIMIT-a.ADAPTATION_LOWER_NODE_LIMIT)*this.coolingFactor*(1-a.COOLING_ADAPTATION_FACTOR))),this.maxNodeDisplacement=a.MAX_NODE_DISPLACEMENT_INCREMENTAL):(f>a.ADAPTATION_LOWER_NODE_LIMIT?this.coolingFactor=Math.max(a.COOLING_ADAPTATION_FACTOR,1-(f-a.ADAPTATION_LOWER_NODE_LIMIT)/(a.ADAPTATION_UPPER_NODE_LIMIT-a.ADAPTATION_LOWER_NODE_LIMIT)*(1-a.COOLING_ADAPTATION_FACTOR)):this.coolingFactor=1,this.initialCoolingFactor=this.coolingFactor,this.maxNodeDisplacement=a.MAX_NODE_DISPLACEMENT),this.maxIterations=Math.max(this.getAllNodes().length*5,this.maxIterations),this.displacementThresholdPerNode=3*a.DEFAULT_EDGE_LENGTH/100,this.totalDisplacementThreshold=this.displacementThresholdPerNode*this.getAllNodes().length,this.repulsionRange=this.calcRepulsionRange()},h.prototype.calcSpringForces=function(){for(var f=this.getAllEdges(),p,m=0;m0&&arguments[0]!==void 0?arguments[0]:!0,p=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1,m,g,y,v,x=this.getAllNodes(),b;if(this.useFRGridVariant)for(this.totalIterations%a.GRID_CALCULATION_CHECK_PERIOD==1&&f&&this.updateGrid(),b=new Set,m=0;mT||b>T)&&(f.gravitationForceX=-this.gravityConstant*y,f.gravitationForceY=-this.gravityConstant*v)):(T=p.getEstimatedSize()*this.compoundGravityRangeFactor,(x>T||b>T)&&(f.gravitationForceX=-this.gravityConstant*y*this.compoundGravityConstant,f.gravitationForceY=-this.gravityConstant*v*this.compoundGravityConstant))},h.prototype.isConverged=function(){var f,p=!1;return this.totalIterations>this.maxIterations/3&&(p=Math.abs(this.totalDisplacement-this.oldTotalDisplacement)<2),f=this.totalDisplacement=x.length||T>=x[0].length)){for(var k=0;kh},"_defaultCompareFunction")}]),l})();e.exports=s}),(function(e,t,r){"use strict";function n(){}o(n,"SVD"),n.svd=function(i){this.U=null,this.V=null,this.s=null,this.m=0,this.n=0,this.m=i.length,this.n=i[0].length;var a=Math.min(this.m,this.n);this.s=(function(At){for(var bt=[];At-- >0;)bt.push(0);return bt})(Math.min(this.m+1,this.n)),this.U=(function(At){var bt=o(function me(lt){if(lt.length==0)return 0;for(var gt=[],Ze=0;Ze0;)bt.push(0);return bt})(this.n),l=(function(At){for(var bt=[];At-- >0;)bt.push(0);return bt})(this.m),u=!0,h=!0,d=Math.min(this.m-1,this.n),f=Math.max(0,Math.min(this.n-2,this.m)),p=0;p=0;P--)if(this.s[P]!==0){for(var B=P+1;B=0;H--){if((function(At,bt){return At&&bt})(H0;){var be=void 0,Pe=void 0;for(be=A-2;be>=-1&&be!==-1;be--)if(Math.abs(s[be])<=Re+ae*(Math.abs(this.s[be])+Math.abs(this.s[be+1]))){s[be]=0;break}if(be===A-2)Pe=4;else{var Ge=void 0;for(Ge=A-1;Ge>=be&&Ge!==be;Ge--){var Oe=(Ge!==A?Math.abs(s[Ge]):0)+(Ge!==be+1?Math.abs(s[Ge-1]):0);if(Math.abs(this.s[Ge])<=Re+ae*Oe){this.s[Ge]=0;break}}Ge===be?Pe=3:Ge===A-1?Pe=1:(Pe=2,be=Ge)}switch(be++,Pe){case 1:{var ue=s[A-2];s[A-2]=0;for(var ye=A-2;ye>=be;ye--){var ke=n.hypot(this.s[ye],ue),ce=this.s[ye]/ke,re=ue/ke;if(this.s[ye]=ke,ye!==be&&(ue=-re*s[ye-1],s[ye-1]=ce*s[ye-1]),h)for(var J=0;J=this.s[be+1]);){var ft=this.s[be];if(this.s[be]=this.s[be+1],this.s[be+1]=ft,h&&beMath.abs(a)?(s=a/i,s=Math.abs(i)*Math.sqrt(1+s*s)):a!=0?(s=i/a,s=Math.abs(a)*Math.sqrt(1+s*s)):s=0,s},e.exports=n}),(function(e,t,r){"use strict";var n=(function(){function s(l,u){for(var h=0;h2&&arguments[2]!==void 0?arguments[2]:1,d=arguments.length>3&&arguments[3]!==void 0?arguments[3]:-1,f=arguments.length>4&&arguments[4]!==void 0?arguments[4]:-1;i(this,s),this.sequence1=l,this.sequence2=u,this.match_score=h,this.mismatch_penalty=d,this.gap_penalty=f,this.iMax=l.length+1,this.jMax=u.length+1,this.grid=new Array(this.iMax);for(var p=0;p=0;l--){var u=this.listeners[l];u.event===a&&u.callback===s&&this.listeners.splice(l,1)}},i.emit=function(a,s){for(var l=0;l{"use strict";o((function(t,r){typeof nk=="object"&&typeof Yj=="object"?Yj.exports=r(Uj()):typeof define=="function"&&define.amd?define(["layout-base"],r):typeof nk=="object"?nk.coseBase=r(Uj()):t.coseBase=r(t.layoutBase)}),"webpackUniversalModuleDefinition")(nk,function(e){return(()=>{"use strict";var t={45:((a,s,l)=>{var u={};u.layoutBase=l(551),u.CoSEConstants=l(806),u.CoSEEdge=l(767),u.CoSEGraph=l(880),u.CoSEGraphManager=l(578),u.CoSELayout=l(765),u.CoSENode=l(991),u.ConstraintHandler=l(902),a.exports=u}),806:((a,s,l)=>{var u=l(551).FDLayoutConstants;function h(){}o(h,"CoSEConstants");for(var d in u)h[d]=u[d];h.DEFAULT_USE_MULTI_LEVEL_SCALING=!1,h.DEFAULT_RADIAL_SEPARATION=u.DEFAULT_EDGE_LENGTH,h.DEFAULT_COMPONENT_SEPERATION=60,h.TILE=!0,h.TILING_PADDING_VERTICAL=10,h.TILING_PADDING_HORIZONTAL=10,h.TRANSFORM_ON_CONSTRAINT_HANDLING=!0,h.ENFORCE_CONSTRAINTS=!0,h.APPLY_LAYOUT=!0,h.RELAX_MOVEMENT_ON_CONSTRAINTS=!0,h.TREE_REDUCTION_ON_INCREMENTAL=!0,h.PURE_INCREMENTAL=h.DEFAULT_INCREMENTAL,a.exports=h}),767:((a,s,l)=>{var u=l(551).FDLayoutEdge;function h(f,p,m){u.call(this,f,p,m)}o(h,"CoSEEdge"),h.prototype=Object.create(u.prototype);for(var d in u)h[d]=u[d];a.exports=h}),880:((a,s,l)=>{var u=l(551).LGraph;function h(f,p,m){u.call(this,f,p,m)}o(h,"CoSEGraph"),h.prototype=Object.create(u.prototype);for(var d in u)h[d]=u[d];a.exports=h}),578:((a,s,l)=>{var u=l(551).LGraphManager;function h(f){u.call(this,f)}o(h,"CoSEGraphManager"),h.prototype=Object.create(u.prototype);for(var d in u)h[d]=u[d];a.exports=h}),765:((a,s,l)=>{var u=l(551).FDLayout,h=l(578),d=l(880),f=l(991),p=l(767),m=l(806),g=l(902),y=l(551).FDLayoutConstants,v=l(551).LayoutConstants,x=l(551).Point,b=l(551).PointD,T=l(551).DimensionD,k=l(551).Layout,C=l(551).Integer,w=l(551).IGeometry,S=l(551).LGraph,R=l(551).Transform,L=l(551).LinkedList;function N(){u.call(this),this.toBeTiled={},this.constraints={}}o(N,"CoSELayout"),N.prototype=Object.create(u.prototype);for(var I in u)N[I]=u[I];N.prototype.newGraphManager=function(){var _=new h(this);return this.graphManager=_,_},N.prototype.newGraph=function(_){return new d(null,this.graphManager,_)},N.prototype.newNode=function(_){return new f(this.graphManager,_)},N.prototype.newEdge=function(_){return new p(null,null,_)},N.prototype.initParameters=function(){u.prototype.initParameters.call(this,arguments),this.isSubLayout||(m.DEFAULT_EDGE_LENGTH<10?this.idealEdgeLength=10:this.idealEdgeLength=m.DEFAULT_EDGE_LENGTH,this.useSmartIdealEdgeLengthCalculation=m.DEFAULT_USE_SMART_IDEAL_EDGE_LENGTH_CALCULATION,this.gravityConstant=y.DEFAULT_GRAVITY_STRENGTH,this.compoundGravityConstant=y.DEFAULT_COMPOUND_GRAVITY_STRENGTH,this.gravityRangeFactor=y.DEFAULT_GRAVITY_RANGE_FACTOR,this.compoundGravityRangeFactor=y.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR,this.prunedNodesAll=[],this.growTreeIterations=0,this.afterGrowthIterations=0,this.isTreeGrowing=!1,this.isGrowthFinished=!1)},N.prototype.initSpringEmbedder=function(){u.prototype.initSpringEmbedder.call(this),this.coolingCycle=0,this.maxCoolingCycle=this.maxIterations/y.CONVERGENCE_CHECK_PERIOD,this.finalTemperature=.04,this.coolingAdjuster=1},N.prototype.layout=function(){var _=v.DEFAULT_CREATE_BENDS_AS_NEEDED;return _&&(this.createBendpoints(),this.graphManager.resetAllEdges()),this.level=0,this.classicLayout()},N.prototype.classicLayout=function(){if(this.nodesWithGravity=this.calculateNodesToApplyGravitationTo(),this.graphManager.setAllNodesToApplyGravitation(this.nodesWithGravity),this.calcNoOfChildrenForAllNodes(),this.graphManager.calcLowestCommonAncestors(),this.graphManager.calcInclusionTreeDepths(),this.graphManager.getRoot().calcEstimatedSize(),this.calcIdealEdgeLengths(),this.incremental){if(m.TREE_REDUCTION_ON_INCREMENTAL){this.reduceTrees(),this.graphManager.resetAllNodesToApplyGravitation();var A=new Set(this.getAllNodes()),M=this.nodesWithGravity.filter(function(B){return A.has(B)});this.graphManager.setAllNodesToApplyGravitation(M)}}else{var _=this.getFlatForest();if(_.length>0)this.positionNodesRadially(_);else{this.reduceTrees(),this.graphManager.resetAllNodesToApplyGravitation();var A=new Set(this.getAllNodes()),M=this.nodesWithGravity.filter(function(D){return A.has(D)});this.graphManager.setAllNodesToApplyGravitation(M),this.positionNodesRandomly()}}return Object.keys(this.constraints).length>0&&(g.handleConstraints(this),this.initConstraintVariables()),this.initSpringEmbedder(),m.APPLY_LAYOUT&&this.runSpringEmbedder(),!0},N.prototype.tick=function(){if(this.totalIterations++,this.totalIterations===this.maxIterations&&!this.isTreeGrowing&&!this.isGrowthFinished)if(this.prunedNodesAll.length>0)this.isTreeGrowing=!0;else return!0;if(this.totalIterations%y.CONVERGENCE_CHECK_PERIOD==0&&!this.isTreeGrowing&&!this.isGrowthFinished){if(this.isConverged())if(this.prunedNodesAll.length>0)this.isTreeGrowing=!0;else return!0;this.coolingCycle++,this.layoutQuality==0?this.coolingAdjuster=this.coolingCycle:this.layoutQuality==1&&(this.coolingAdjuster=this.coolingCycle/3),this.coolingFactor=Math.max(this.initialCoolingFactor-Math.pow(this.coolingCycle,Math.log(100*(this.initialCoolingFactor-this.finalTemperature))/Math.log(this.maxCoolingCycle))/100*this.coolingAdjuster,this.finalTemperature),this.animationPeriod=Math.ceil(this.initialAnimationPeriod*Math.sqrt(this.coolingFactor))}if(this.isTreeGrowing){if(this.growTreeIterations%10==0)if(this.prunedNodesAll.length>0){this.graphManager.updateBounds(),this.updateGrid(),this.growTree(this.prunedNodesAll),this.graphManager.resetAllNodesToApplyGravitation();var _=new Set(this.getAllNodes()),A=this.nodesWithGravity.filter(function(P){return _.has(P)});this.graphManager.setAllNodesToApplyGravitation(A),this.graphManager.updateBounds(),this.updateGrid(),m.PURE_INCREMENTAL?this.coolingFactor=y.DEFAULT_COOLING_FACTOR_INCREMENTAL/2:this.coolingFactor=y.DEFAULT_COOLING_FACTOR_INCREMENTAL}else this.isTreeGrowing=!1,this.isGrowthFinished=!0;this.growTreeIterations++}if(this.isGrowthFinished){if(this.isConverged())return!0;this.afterGrowthIterations%10==0&&(this.graphManager.updateBounds(),this.updateGrid()),m.PURE_INCREMENTAL?this.coolingFactor=y.DEFAULT_COOLING_FACTOR_INCREMENTAL/2*((100-this.afterGrowthIterations)/100):this.coolingFactor=y.DEFAULT_COOLING_FACTOR_INCREMENTAL*((100-this.afterGrowthIterations)/100),this.afterGrowthIterations++}var M=!this.isTreeGrowing&&!this.isGrowthFinished,D=this.growTreeIterations%10==1&&this.isTreeGrowing||this.afterGrowthIterations%10==1&&this.isGrowthFinished;return this.totalDisplacement=0,this.graphManager.updateBounds(),this.calcSpringForces(),this.calcRepulsionForces(M,D),this.calcGravitationalForces(),this.moveNodes(),this.animate(),!1},N.prototype.getPositionsData=function(){for(var _=this.graphManager.getAllNodes(),A={},M=0;M<_.length;M++){var D=_[M].rect,P=_[M].id;A[P]={id:P,x:D.getCenterX(),y:D.getCenterY(),w:D.width,h:D.height}}return A},N.prototype.runSpringEmbedder=function(){this.initialAnimationPeriod=25,this.animationPeriod=this.initialAnimationPeriod;var _=!1;if(y.ANIMATE==="during")this.emit("layoutstarted");else{for(;!_;)_=this.tick();this.graphManager.updateBounds()}},N.prototype.moveNodes=function(){for(var _=this.getAllNodes(),A,M=0;M<_.length;M++)A=_[M],A.calculateDisplacement();Object.keys(this.constraints).length>0&&this.updateDisplacements();for(var M=0;M<_.length;M++)A=_[M],A.move()},N.prototype.initConstraintVariables=function(){var _=this;this.idToNodeMap=new Map,this.fixedNodeSet=new Set;for(var A=this.graphManager.getAllNodes(),M=0;M0&&(D.fixedNodeWeight=B)}}if(this.constraints.relativePlacementConstraint){var O=new Map,$=new Map;if(this.dummyToNodeForVerticalAlignment=new Map,this.dummyToNodeForHorizontalAlignment=new Map,this.fixedNodesOnHorizontal=new Set,this.fixedNodesOnVertical=new Set,this.fixedNodeSet.forEach(function(U){_.fixedNodesOnHorizontal.add(U),_.fixedNodesOnVertical.add(U)}),this.constraints.alignmentConstraint){if(this.constraints.alignmentConstraint.vertical)for(var V=this.constraints.alignmentConstraint.vertical,M=0;M=2*U.length/3;le--)oe=Math.floor(Math.random()*(le+1)),te=U[le],U[le]=U[oe],U[oe]=te;return U},this.nodesInRelativeHorizontal=[],this.nodesInRelativeVertical=[],this.nodeToRelativeConstraintMapHorizontal=new Map,this.nodeToRelativeConstraintMapVertical=new Map,this.nodeToTempPositionMapHorizontal=new Map,this.nodeToTempPositionMapVertical=new Map,this.constraints.relativePlacementConstraint.forEach(function(U){if(U.left){var oe=O.has(U.left)?O.get(U.left):U.left,te=O.has(U.right)?O.get(U.right):U.right;_.nodesInRelativeHorizontal.includes(oe)||(_.nodesInRelativeHorizontal.push(oe),_.nodeToRelativeConstraintMapHorizontal.set(oe,[]),_.dummyToNodeForVerticalAlignment.has(oe)?_.nodeToTempPositionMapHorizontal.set(oe,_.idToNodeMap.get(_.dummyToNodeForVerticalAlignment.get(oe)[0]).getCenterX()):_.nodeToTempPositionMapHorizontal.set(oe,_.idToNodeMap.get(oe).getCenterX())),_.nodesInRelativeHorizontal.includes(te)||(_.nodesInRelativeHorizontal.push(te),_.nodeToRelativeConstraintMapHorizontal.set(te,[]),_.dummyToNodeForVerticalAlignment.has(te)?_.nodeToTempPositionMapHorizontal.set(te,_.idToNodeMap.get(_.dummyToNodeForVerticalAlignment.get(te)[0]).getCenterX()):_.nodeToTempPositionMapHorizontal.set(te,_.idToNodeMap.get(te).getCenterX())),_.nodeToRelativeConstraintMapHorizontal.get(oe).push({right:te,gap:U.gap}),_.nodeToRelativeConstraintMapHorizontal.get(te).push({left:oe,gap:U.gap})}else{var le=$.has(U.top)?$.get(U.top):U.top,ie=$.has(U.bottom)?$.get(U.bottom):U.bottom;_.nodesInRelativeVertical.includes(le)||(_.nodesInRelativeVertical.push(le),_.nodeToRelativeConstraintMapVertical.set(le,[]),_.dummyToNodeForHorizontalAlignment.has(le)?_.nodeToTempPositionMapVertical.set(le,_.idToNodeMap.get(_.dummyToNodeForHorizontalAlignment.get(le)[0]).getCenterY()):_.nodeToTempPositionMapVertical.set(le,_.idToNodeMap.get(le).getCenterY())),_.nodesInRelativeVertical.includes(ie)||(_.nodesInRelativeVertical.push(ie),_.nodeToRelativeConstraintMapVertical.set(ie,[]),_.dummyToNodeForHorizontalAlignment.has(ie)?_.nodeToTempPositionMapVertical.set(ie,_.idToNodeMap.get(_.dummyToNodeForHorizontalAlignment.get(ie)[0]).getCenterY()):_.nodeToTempPositionMapVertical.set(ie,_.idToNodeMap.get(ie).getCenterY())),_.nodeToRelativeConstraintMapVertical.get(le).push({bottom:ie,gap:U.gap}),_.nodeToRelativeConstraintMapVertical.get(ie).push({top:le,gap:U.gap})}});else{var z=new Map,W=new Map;this.constraints.relativePlacementConstraint.forEach(function(U){if(U.left){var oe=O.has(U.left)?O.get(U.left):U.left,te=O.has(U.right)?O.get(U.right):U.right;z.has(oe)?z.get(oe).push(te):z.set(oe,[te]),z.has(te)?z.get(te).push(oe):z.set(te,[oe])}else{var le=$.has(U.top)?$.get(U.top):U.top,ie=$.has(U.bottom)?$.get(U.bottom):U.bottom;W.has(le)?W.get(le).push(ie):W.set(le,[ie]),W.has(ie)?W.get(ie).push(le):W.set(ie,[le])}});var H=o(function(oe,te){var le=[],ie=[],ae=new L,Re=new Set,be=0;return oe.forEach(function(Pe,Ge){if(!Re.has(Ge)){le[be]=[],ie[be]=!1;var Oe=Ge;for(ae.push(Oe),Re.add(Oe),le[be].push(Oe);ae.length!=0;){Oe=ae.shift(),te.has(Oe)&&(ie[be]=!0);var ue=oe.get(Oe);ue.forEach(function(ye){Re.has(ye)||(ae.push(ye),Re.add(ye),le[be].push(ye))})}be++}}),{components:le,isFixed:ie}},"constructComponents"),j=H(z,_.fixedNodesOnHorizontal);this.componentsOnHorizontal=j.components,this.fixedComponentsOnHorizontal=j.isFixed;var Q=H(W,_.fixedNodesOnVertical);this.componentsOnVertical=Q.components,this.fixedComponentsOnVertical=Q.isFixed}}},N.prototype.updateDisplacements=function(){var _=this;if(this.constraints.fixedNodeConstraint&&this.constraints.fixedNodeConstraint.forEach(function(Q){var U=_.idToNodeMap.get(Q.nodeId);U.displacementX=0,U.displacementY=0}),this.constraints.alignmentConstraint){if(this.constraints.alignmentConstraint.vertical)for(var A=this.constraints.alignmentConstraint.vertical,M=0;M1){var $;for($=0;$D&&(D=Math.floor(O.y)),B=Math.floor(O.x+m.DEFAULT_COMPONENT_SEPERATION)}this.transform(new b(v.WORLD_CENTER_X-O.x/2,v.WORLD_CENTER_Y-O.y/2))},N.radialLayout=function(_,A,M){var D=Math.max(this.maxDiagonalInTree(_),m.DEFAULT_RADIAL_SEPARATION);N.branchRadialLayout(A,null,0,359,0,D);var P=S.calculateBounds(_),B=new R;B.setDeviceOrgX(P.getMinX()),B.setDeviceOrgY(P.getMinY()),B.setWorldOrgX(M.x),B.setWorldOrgY(M.y);for(var O=0;O<_.length;O++){var $=_[O];$.transform(B)}var V=new b(P.getMaxX(),P.getMaxY());return B.inverseTransformPoint(V)},N.branchRadialLayout=function(_,A,M,D,P,B){var O=(D-M+1)/2;O<0&&(O+=180);var $=(O+M)%360,V=$*w.TWO_PI/360,G=Math.cos(V),z=P*Math.cos(V),W=P*Math.sin(V);_.setCenter(z,W);var H=[];H=H.concat(_.getEdges());var j=H.length;A!=null&&j--;for(var Q=0,U=H.length,oe,te=_.getEdgesBetween(A);te.length>1;){var le=te[0];te.splice(0,1);var ie=H.indexOf(le);ie>=0&&H.splice(ie,1),U--,j--}A!=null?oe=(H.indexOf(te[0])+1)%U:oe=0;for(var ae=Math.abs(D-M)/j,Re=oe;Q!=j;Re=++Re%U){var be=H[Re].getOtherEnd(_);if(be!=A){var Pe=(M+Q*ae)%360,Ge=(Pe+ae)%360;N.branchRadialLayout(be,_,Pe,Ge,P+B,B),Q++}}},N.maxDiagonalInTree=function(_){for(var A=C.MIN_VALUE,M=0;M<_.length;M++){var D=_[M],P=D.getDiagonal();P>A&&(A=P)}return A},N.prototype.calcRepulsionRange=function(){return 2*(this.level+1)*this.idealEdgeLength},N.prototype.groupZeroDegreeMembers=function(){var _=this,A={};this.memberGroups={},this.idToDummyNode={};for(var M=[],D=this.graphManager.getAllNodes(),P=0;P"u"&&(A[$]=[]),A[$]=A[$].concat(B)}Object.keys(A).forEach(function(V){if(A[V].length>1){var G="DummyCompound_"+V;_.memberGroups[G]=A[V];var z=A[V][0].getParent(),W=new f(_.graphManager);W.id=G,W.paddingLeft=z.paddingLeft||0,W.paddingRight=z.paddingRight||0,W.paddingBottom=z.paddingBottom||0,W.paddingTop=z.paddingTop||0,_.idToDummyNode[G]=W;var H=_.getGraphManager().add(_.newGraph(),W),j=z.getChild();j.add(W);for(var Q=0;QP?(D.rect.x-=(D.labelWidth-P)/2,D.setWidth(D.labelWidth),D.labelMarginLeft=(D.labelWidth-P)/2):D.labelPosHorizontal=="right"&&D.setWidth(P+D.labelWidth)),D.labelHeight&&(D.labelPosVertical=="top"?(D.rect.y-=D.labelHeight,D.setHeight(B+D.labelHeight),D.labelMarginTop=D.labelHeight):D.labelPosVertical=="center"&&D.labelHeight>B?(D.rect.y-=(D.labelHeight-B)/2,D.setHeight(D.labelHeight),D.labelMarginTop=(D.labelHeight-B)/2):D.labelPosVertical=="bottom"&&D.setHeight(B+D.labelHeight))}})},N.prototype.repopulateCompounds=function(){for(var _=this.compoundOrder.length-1;_>=0;_--){var A=this.compoundOrder[_],M=A.id,D=A.paddingLeft,P=A.paddingTop,B=A.labelMarginLeft,O=A.labelMarginTop;this.adjustLocations(this.tiledMemberPack[M],A.rect.x,A.rect.y,D,P,B,O)}},N.prototype.repopulateZeroDegreeMembers=function(){var _=this,A=this.tiledZeroDegreePack;Object.keys(A).forEach(function(M){var D=_.idToDummyNode[M],P=D.paddingLeft,B=D.paddingTop,O=D.labelMarginLeft,$=D.labelMarginTop;_.adjustLocations(A[M],D.rect.x,D.rect.y,P,B,O,$)})},N.prototype.getToBeTiled=function(_){var A=_.id;if(this.toBeTiled[A]!=null)return this.toBeTiled[A];var M=_.getChild();if(M==null)return this.toBeTiled[A]=!1,!1;for(var D=M.getNodes(),P=0;P0)return this.toBeTiled[A]=!1,!1;if(B.getChild()==null){this.toBeTiled[B.id]=!1;continue}if(!this.getToBeTiled(B))return this.toBeTiled[A]=!1,!1}return this.toBeTiled[A]=!0,!0},N.prototype.getNodeDegree=function(_){for(var A=_.id,M=_.getEdges(),D=0,P=0;Pz&&(z=H.rect.height)}M+=z+_.verticalPadding}},N.prototype.tileCompoundMembers=function(_,A){var M=this;this.tiledMemberPack=[],Object.keys(_).forEach(function(D){var P=A[D];if(M.tiledMemberPack[D]=M.tileNodes(_[D],P.paddingLeft+P.paddingRight),P.rect.width=M.tiledMemberPack[D].width,P.rect.height=M.tiledMemberPack[D].height,P.setCenter(M.tiledMemberPack[D].centerX,M.tiledMemberPack[D].centerY),P.labelMarginLeft=0,P.labelMarginTop=0,m.NODE_DIMENSIONS_INCLUDE_LABELS){var B=P.rect.width,O=P.rect.height;P.labelWidth&&(P.labelPosHorizontal=="left"?(P.rect.x-=P.labelWidth,P.setWidth(B+P.labelWidth),P.labelMarginLeft=P.labelWidth):P.labelPosHorizontal=="center"&&P.labelWidth>B?(P.rect.x-=(P.labelWidth-B)/2,P.setWidth(P.labelWidth),P.labelMarginLeft=(P.labelWidth-B)/2):P.labelPosHorizontal=="right"&&P.setWidth(B+P.labelWidth)),P.labelHeight&&(P.labelPosVertical=="top"?(P.rect.y-=P.labelHeight,P.setHeight(O+P.labelHeight),P.labelMarginTop=P.labelHeight):P.labelPosVertical=="center"&&P.labelHeight>O?(P.rect.y-=(P.labelHeight-O)/2,P.setHeight(P.labelHeight),P.labelMarginTop=(P.labelHeight-O)/2):P.labelPosVertical=="bottom"&&P.setHeight(O+P.labelHeight))}})},N.prototype.tileNodes=function(_,A){var M=this.tileNodesByFavoringDim(_,A,!0),D=this.tileNodesByFavoringDim(_,A,!1),P=this.getOrgRatio(M),B=this.getOrgRatio(D),O;return B$&&($=Q.getWidth())});var V=B/P,G=O/P,z=Math.pow(M-D,2)+4*(V+D)*(G+M)*P,W=(D-M+Math.sqrt(z))/(2*(V+D)),H;A?(H=Math.ceil(W),H==W&&H++):H=Math.floor(W);var j=H*(V+D)-D;return $>j&&(j=$),j+=D*2,j},N.prototype.tileNodesByFavoringDim=function(_,A,M){var D=m.TILING_PADDING_VERTICAL,P=m.TILING_PADDING_HORIZONTAL,B=m.TILING_COMPARE_BY,O={rows:[],rowWidth:[],rowHeight:[],width:0,height:A,verticalPadding:D,horizontalPadding:P,centerX:0,centerY:0};B&&(O.idealRowWidth=this.calcIdealRowWidth(_,M));var $=o(function(U){return U.rect.width*U.rect.height},"getNodeArea"),V=o(function(U,oe){return $(oe)-$(U)},"areaCompareFcn");_.sort(function(Q,U){var oe=V;return O.idealRowWidth?(oe=B,oe(Q.id,U.id)):oe(Q,U)});for(var G=0,z=0,W=0;W<_.length;W++){var H=_[W];G+=H.getCenterX(),z+=H.getCenterY()}O.centerX=G/_.length,O.centerY=z/_.length;for(var W=0;W<_.length;W++){var H=_[W];if(O.rows.length==0)this.insertNodeToRow(O,H,0,A);else if(this.canAddHorizontal(O,H.rect.width,H.rect.height)){var j=O.rows.length-1;O.idealRowWidth||(j=this.getShortestRowIndex(O)),this.insertNodeToRow(O,H,j,A)}else this.insertNodeToRow(O,H,O.rows.length,A);this.shiftToLastRow(O)}return O},N.prototype.insertNodeToRow=function(_,A,M,D){var P=D;if(M==_.rows.length){var B=[];_.rows.push(B),_.rowWidth.push(P),_.rowHeight.push(0)}var O=_.rowWidth[M]+A.rect.width;_.rows[M].length>0&&(O+=_.horizontalPadding),_.rowWidth[M]=O,_.width0&&($+=_.verticalPadding);var V=0;$>_.rowHeight[M]&&(V=_.rowHeight[M],_.rowHeight[M]=$,V=_.rowHeight[M]-V),_.height+=V,_.rows[M].push(A)},N.prototype.getShortestRowIndex=function(_){for(var A=-1,M=Number.MAX_VALUE,D=0;D<_.rows.length;D++)_.rowWidth[D]M&&(A=D,M=_.rowWidth[D]);return A},N.prototype.canAddHorizontal=function(_,A,M){if(_.idealRowWidth){var D=_.rows.length-1,P=_.rowWidth[D];return P+A+_.horizontalPadding<=_.idealRowWidth}var B=this.getShortestRowIndex(_);if(B<0)return!0;var O=_.rowWidth[B];if(O+_.horizontalPadding+A<=_.width)return!0;var $=0;_.rowHeight[B]0&&($=M+_.verticalPadding-_.rowHeight[B]);var V;_.width-O>=A+_.horizontalPadding?V=(_.height+$)/(O+A+_.horizontalPadding):V=(_.height+$)/_.width,$=M+_.verticalPadding;var G;return _.widthB&&A!=M){D.splice(-1,1),_.rows[M].push(P),_.rowWidth[A]=_.rowWidth[A]-B,_.rowWidth[M]=_.rowWidth[M]+B,_.width=_.rowWidth[instance.getLongestRowIndex(_)];for(var O=Number.MIN_VALUE,$=0;$O&&(O=D[$].height);A>0&&(O+=_.verticalPadding);var V=_.rowHeight[A]+_.rowHeight[M];_.rowHeight[A]=O,_.rowHeight[M]0)for(var j=P;j<=B;j++)H[0]+=this.grid[j][O-1].length+this.grid[j][O].length-1;if(B0)for(var j=O;j<=$;j++)H[3]+=this.grid[P-1][j].length+this.grid[P][j].length-1;for(var Q=C.MAX_VALUE,U,oe,te=0;te{var u=l(551).FDLayoutNode,h=l(551).IMath;function d(p,m,g,y){u.call(this,p,m,g,y)}o(d,"CoSENode"),d.prototype=Object.create(u.prototype);for(var f in u)d[f]=u[f];d.prototype.calculateDisplacement=function(){var p=this.graphManager.getLayout();this.getChild()!=null&&this.fixedNodeWeight?(this.displacementX+=p.coolingFactor*(this.springForceX+this.repulsionForceX+this.gravitationForceX)/this.fixedNodeWeight,this.displacementY+=p.coolingFactor*(this.springForceY+this.repulsionForceY+this.gravitationForceY)/this.fixedNodeWeight):(this.displacementX+=p.coolingFactor*(this.springForceX+this.repulsionForceX+this.gravitationForceX)/this.noOfChildren,this.displacementY+=p.coolingFactor*(this.springForceY+this.repulsionForceY+this.gravitationForceY)/this.noOfChildren),Math.abs(this.displacementX)>p.coolingFactor*p.maxNodeDisplacement&&(this.displacementX=p.coolingFactor*p.maxNodeDisplacement*h.sign(this.displacementX)),Math.abs(this.displacementY)>p.coolingFactor*p.maxNodeDisplacement&&(this.displacementY=p.coolingFactor*p.maxNodeDisplacement*h.sign(this.displacementY)),this.child&&this.child.getNodes().length>0&&this.propogateDisplacementToChildren(this.displacementX,this.displacementY)},d.prototype.propogateDisplacementToChildren=function(p,m){for(var g=this.getChild().getNodes(),y,v=0;v{function u(g){if(Array.isArray(g)){for(var y=0,v=Array(g.length);y0){var mt=0;xe.forEach(function(ft){he=="horizontal"?(Be.set(ft,x.has(ft)?b[x.get(ft)]:fe.get(ft)),mt+=Be.get(ft)):(Be.set(ft,x.has(ft)?T[x.get(ft)]:fe.get(ft)),mt+=Be.get(ft))}),mt=mt/xe.length,Ke.forEach(function(ft){X.has(ft)||Be.set(ft,mt)})}else{var Le=0;Ke.forEach(function(ft){he=="horizontal"?Le+=x.has(ft)?b[x.get(ft)]:fe.get(ft):Le+=x.has(ft)?T[x.get(ft)]:fe.get(ft)}),Le=Le/Ke.length,Ke.forEach(function(ft){Be.set(ft,Le)})}});for(var $e=o(function(){var xe=He.shift(),mt=q.get(xe);mt.forEach(function(Le){if(Be.get(Le.id)ft&&(ft=gt),Zewt&&(wt=Ze)}}catch(vr){St=!0,At=vr}finally{try{!zt&&bt.return&&bt.return()}finally{if(St)throw At}}var Ee=(mt+ft)/2-(Le+wt)/2,tt=!0,at=!1,ot=void 0;try{for(var Wt=Ke[Symbol.iterator](),Bt;!(tt=(Bt=Wt.next()).done);tt=!0){var qt=Bt.value;Be.set(qt,Be.get(qt)+Ee)}}catch(vr){at=!0,ot=vr}finally{try{!tt&&Wt.return&&Wt.return()}finally{if(at)throw ot}}})}return Be},"findAppropriatePositionForRelativePlacement"),I=o(function(q){var he=0,X=0,fe=0,K=0;if(q.forEach(function(Ne){Ne.left?b[x.get(Ne.left)]-b[x.get(Ne.right)]>=0?he++:X++:T[x.get(Ne.top)]-T[x.get(Ne.bottom)]>=0?fe++:K++}),he>X&&fe>K)for(var qe=0;qeX)for(var _e=0;_eK)for(var Be=0;Be1)y.fixedNodeConstraint.forEach(function(ne,q){D[q]=[ne.position.x,ne.position.y],P[q]=[b[x.get(ne.nodeId)],T[x.get(ne.nodeId)]]}),B=!0;else if(y.alignmentConstraint)(function(){var ne=0;if(y.alignmentConstraint.vertical){for(var q=y.alignmentConstraint.vertical,he=o(function(Be){var Ne=new Set;q[Be].forEach(function(Xe){Ne.add(Xe)});var He=new Set([].concat(u(Ne)).filter(function(Xe){return $.has(Xe)})),$e=void 0;He.size>0?$e=b[x.get(He.values().next().value)]:$e=L(Ne).x,q[Be].forEach(function(Xe){D[ne]=[$e,T[x.get(Xe)]],P[ne]=[b[x.get(Xe)],T[x.get(Xe)]],ne++})},"_loop2"),X=0;X0?$e=b[x.get(He.values().next().value)]:$e=L(Ne).y,fe[Be].forEach(function(Xe){D[ne]=[b[x.get(Xe)],$e],P[ne]=[b[x.get(Xe)],T[x.get(Xe)]],ne++})},"_loop3"),qe=0;qeW&&(W=z[j].length,H=j);if(W0){var ce={x:0,y:0};y.fixedNodeConstraint.forEach(function(ne,q){var he={x:b[x.get(ne.nodeId)],y:T[x.get(ne.nodeId)]},X=ne.position,fe=R(X,he);ce.x+=fe.x,ce.y+=fe.y}),ce.x/=y.fixedNodeConstraint.length,ce.y/=y.fixedNodeConstraint.length,b.forEach(function(ne,q){b[q]+=ce.x}),T.forEach(function(ne,q){T[q]+=ce.y}),y.fixedNodeConstraint.forEach(function(ne){b[x.get(ne.nodeId)]=ne.position.x,T[x.get(ne.nodeId)]=ne.position.y})}if(y.alignmentConstraint){if(y.alignmentConstraint.vertical)for(var re=y.alignmentConstraint.vertical,J=o(function(q){var he=new Set;re[q].forEach(function(K){he.add(K)});var X=new Set([].concat(u(he)).filter(function(K){return $.has(K)})),fe=void 0;X.size>0?fe=b[x.get(X.values().next().value)]:fe=L(he).x,he.forEach(function(K){$.has(K)||(b[x.get(K)]=fe)})},"_loop4"),se=0;se0?fe=T[x.get(X.values().next().value)]:fe=L(he).y,he.forEach(function(K){$.has(K)||(T[x.get(K)]=fe)})},"_loop5"),we=0;we{a.exports=e})},r={};function n(a){var s=r[a];if(s!==void 0)return s.exports;var l=r[a]={exports:{}};return t[a](l,l.exports,n),l.exports}o(n,"__webpack_require__");var i=n(45);return i})()})});var _9e=Io((ik,Xj)=>{"use strict";o((function(t,r){typeof ik=="object"&&typeof Xj=="object"?Xj.exports=r(jj()):typeof define=="function"&&define.amd?define(["cose-base"],r):typeof ik=="object"?ik.cytoscapeFcose=r(jj()):t.cytoscapeFcose=r(t.coseBase)}),"webpackUniversalModuleDefinition")(ik,function(e){return(()=>{"use strict";var t={658:(a=>{a.exports=Object.assign!=null?Object.assign.bind(Object):function(s){for(var l=arguments.length,u=Array(l>1?l-1:0),h=1;h{var u=(function(){function f(p,m){var g=[],y=!0,v=!1,x=void 0;try{for(var b=p[Symbol.iterator](),T;!(y=(T=b.next()).done)&&(g.push(T.value),!(m&&g.length===m));y=!0);}catch(k){v=!0,x=k}finally{try{!y&&b.return&&b.return()}finally{if(v)throw x}}return g}return o(f,"sliceIterator"),function(p,m){if(Array.isArray(p))return p;if(Symbol.iterator in Object(p))return f(p,m);throw new TypeError("Invalid attempt to destructure non-iterable instance")}})(),h=l(140).layoutBase.LinkedList,d={};d.getTopMostNodes=function(f){for(var p={},m=0;m0&&B.merge(G)});for(var O=0;O1){T=x[0],k=T.connectedEdges().length,x.forEach(function(P){P.connectedEdges().length0&&g.set("dummy"+(g.size+1),S),R},d.relocateComponent=function(f,p,m){if(!m.fixedNodeConstraint){var g=Number.POSITIVE_INFINITY,y=Number.NEGATIVE_INFINITY,v=Number.POSITIVE_INFINITY,x=Number.NEGATIVE_INFINITY;if(m.quality=="draft"){var b=!0,T=!1,k=void 0;try{for(var C=p.nodeIndexes[Symbol.iterator](),w;!(b=(w=C.next()).done);b=!0){var S=w.value,R=u(S,2),L=R[0],N=R[1],I=m.cy.getElementById(L);if(I){var _=I.boundingBox(),A=p.xCoords[N]-_.w/2,M=p.xCoords[N]+_.w/2,D=p.yCoords[N]-_.h/2,P=p.yCoords[N]+_.h/2;Ay&&(y=M),Dx&&(x=P)}}}catch(G){T=!0,k=G}finally{try{!b&&C.return&&C.return()}finally{if(T)throw k}}var B=f.x-(y+g)/2,O=f.y-(x+v)/2;p.xCoords=p.xCoords.map(function(G){return G+B}),p.yCoords=p.yCoords.map(function(G){return G+O})}else{Object.keys(p).forEach(function(G){var z=p[G],W=z.getRect().x,H=z.getRect().x+z.getRect().width,j=z.getRect().y,Q=z.getRect().y+z.getRect().height;Wy&&(y=H),jx&&(x=Q)});var $=f.x-(y+g)/2,V=f.y-(x+v)/2;Object.keys(p).forEach(function(G){var z=p[G];z.setCenter(z.getCenterX()+$,z.getCenterY()+V)})}}},d.calcBoundingBox=function(f,p,m,g){for(var y=Number.MAX_SAFE_INTEGER,v=Number.MIN_SAFE_INTEGER,x=Number.MAX_SAFE_INTEGER,b=Number.MIN_SAFE_INTEGER,T=void 0,k=void 0,C=void 0,w=void 0,S=f.descendants().not(":parent"),R=S.length,L=0;LT&&(y=T),vC&&(x=C),b{var u=l(548),h=l(140).CoSELayout,d=l(140).CoSENode,f=l(140).layoutBase.PointD,p=l(140).layoutBase.DimensionD,m=l(140).layoutBase.LayoutConstants,g=l(140).layoutBase.FDLayoutConstants,y=l(140).CoSEConstants,v=o(function(b,T){var k=b.cy,C=b.eles,w=C.nodes(),S=C.edges(),R=void 0,L=void 0,N=void 0,I={};b.randomize&&(R=T.nodeIndexes,L=T.xCoords,N=T.yCoords);var _=o(function(G){return typeof G=="function"},"isFn"),A=o(function(G,z){return _(G)?G(z):G},"optFn"),M=u.calcParentsWithoutChildren(k,C),D=o(function V(G,z,W,H){for(var j=z.length,Q=0;Q0){var ae=void 0;ae=W.getGraphManager().add(W.newGraph(),te),V(ae,oe,W,H)}}},"processChildrenList"),P=o(function(G,z,W){for(var H=0,j=0,Q=0;Q0?y.DEFAULT_EDGE_LENGTH=g.DEFAULT_EDGE_LENGTH=H/j:_(b.idealEdgeLength)?y.DEFAULT_EDGE_LENGTH=g.DEFAULT_EDGE_LENGTH=50:y.DEFAULT_EDGE_LENGTH=g.DEFAULT_EDGE_LENGTH=b.idealEdgeLength,y.MIN_REPULSION_DIST=g.MIN_REPULSION_DIST=g.DEFAULT_EDGE_LENGTH/10,y.DEFAULT_RADIAL_SEPARATION=g.DEFAULT_EDGE_LENGTH)},"processEdges"),B=o(function(G,z){z.fixedNodeConstraint&&(G.constraints.fixedNodeConstraint=z.fixedNodeConstraint),z.alignmentConstraint&&(G.constraints.alignmentConstraint=z.alignmentConstraint),z.relativePlacementConstraint&&(G.constraints.relativePlacementConstraint=z.relativePlacementConstraint)},"processConstraints");b.nestingFactor!=null&&(y.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR=g.PER_LEVEL_IDEAL_EDGE_LENGTH_FACTOR=b.nestingFactor),b.gravity!=null&&(y.DEFAULT_GRAVITY_STRENGTH=g.DEFAULT_GRAVITY_STRENGTH=b.gravity),b.numIter!=null&&(y.MAX_ITERATIONS=g.MAX_ITERATIONS=b.numIter),b.gravityRange!=null&&(y.DEFAULT_GRAVITY_RANGE_FACTOR=g.DEFAULT_GRAVITY_RANGE_FACTOR=b.gravityRange),b.gravityCompound!=null&&(y.DEFAULT_COMPOUND_GRAVITY_STRENGTH=g.DEFAULT_COMPOUND_GRAVITY_STRENGTH=b.gravityCompound),b.gravityRangeCompound!=null&&(y.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR=g.DEFAULT_COMPOUND_GRAVITY_RANGE_FACTOR=b.gravityRangeCompound),b.initialEnergyOnIncremental!=null&&(y.DEFAULT_COOLING_FACTOR_INCREMENTAL=g.DEFAULT_COOLING_FACTOR_INCREMENTAL=b.initialEnergyOnIncremental),b.tilingCompareBy!=null&&(y.TILING_COMPARE_BY=b.tilingCompareBy),b.quality=="proof"?m.QUALITY=2:m.QUALITY=0,y.NODE_DIMENSIONS_INCLUDE_LABELS=g.NODE_DIMENSIONS_INCLUDE_LABELS=m.NODE_DIMENSIONS_INCLUDE_LABELS=b.nodeDimensionsIncludeLabels,y.DEFAULT_INCREMENTAL=g.DEFAULT_INCREMENTAL=m.DEFAULT_INCREMENTAL=!b.randomize,y.ANIMATE=g.ANIMATE=m.ANIMATE=b.animate,y.TILE=b.tile,y.TILING_PADDING_VERTICAL=typeof b.tilingPaddingVertical=="function"?b.tilingPaddingVertical.call():b.tilingPaddingVertical,y.TILING_PADDING_HORIZONTAL=typeof b.tilingPaddingHorizontal=="function"?b.tilingPaddingHorizontal.call():b.tilingPaddingHorizontal,y.DEFAULT_INCREMENTAL=g.DEFAULT_INCREMENTAL=m.DEFAULT_INCREMENTAL=!0,y.PURE_INCREMENTAL=!b.randomize,m.DEFAULT_UNIFORM_LEAF_NODE_SIZES=b.uniformNodeDimensions,b.step=="transformed"&&(y.TRANSFORM_ON_CONSTRAINT_HANDLING=!0,y.ENFORCE_CONSTRAINTS=!1,y.APPLY_LAYOUT=!1),b.step=="enforced"&&(y.TRANSFORM_ON_CONSTRAINT_HANDLING=!1,y.ENFORCE_CONSTRAINTS=!0,y.APPLY_LAYOUT=!1),b.step=="cose"&&(y.TRANSFORM_ON_CONSTRAINT_HANDLING=!1,y.ENFORCE_CONSTRAINTS=!1,y.APPLY_LAYOUT=!0),b.step=="all"&&(b.randomize?y.TRANSFORM_ON_CONSTRAINT_HANDLING=!0:y.TRANSFORM_ON_CONSTRAINT_HANDLING=!1,y.ENFORCE_CONSTRAINTS=!0,y.APPLY_LAYOUT=!0),b.fixedNodeConstraint||b.alignmentConstraint||b.relativePlacementConstraint?y.TREE_REDUCTION_ON_INCREMENTAL=!1:y.TREE_REDUCTION_ON_INCREMENTAL=!0;var O=new h,$=O.newGraphManager();return D($.addRoot(),u.getTopMostNodes(w),O,b),P(O,$,S),B(O,b),O.runLayout(),I},"coseLayout");a.exports={coseLayout:v}}),212:((a,s,l)=>{var u=(function(){function b(T,k){for(var C=0;C0)if(P){var $=f.getTopMostNodes(C.eles.nodes());if(_=f.connectComponents(w,C.eles,$),_.forEach(function(Oe){var ue=Oe.boundingBox();A.push({x:ue.x1+ue.w/2,y:ue.y1+ue.h/2})}),C.randomize&&_.forEach(function(Oe){C.eles=Oe,R.push(m(C))}),C.quality=="default"||C.quality=="proof"){var V=w.collection();if(C.tile){var G=new Map,z=[],W=[],H=0,j={nodeIndexes:G,xCoords:z,yCoords:W},Q=[];if(_.forEach(function(Oe,ue){Oe.edges().length==0&&(Oe.nodes().forEach(function(ye,ke){V.merge(Oe.nodes()[ke]),ye.isParent()||(j.nodeIndexes.set(Oe.nodes()[ke].id(),H++),j.xCoords.push(Oe.nodes()[0].position().x),j.yCoords.push(Oe.nodes()[0].position().y))}),Q.push(ue))}),V.length>1){var U=V.boundingBox();A.push({x:U.x1+U.w/2,y:U.y1+U.h/2}),_.push(V),R.push(j);for(var oe=Q.length-1;oe>=0;oe--)_.splice(Q[oe],1),R.splice(Q[oe],1),A.splice(Q[oe],1)}}_.forEach(function(Oe,ue){C.eles=Oe,I.push(y(C,R[ue])),f.relocateComponent(A[ue],I[ue],C)})}else _.forEach(function(Oe,ue){f.relocateComponent(A[ue],R[ue],C)});var te=new Set;if(_.length>1){var le=[],ie=S.filter(function(Oe){return Oe.css("display")=="none"});_.forEach(function(Oe,ue){var ye=void 0;if(C.quality=="draft"&&(ye=R[ue].nodeIndexes),Oe.nodes().not(ie).length>0){var ke={};ke.edges=[],ke.nodes=[];var ce=void 0;Oe.nodes().not(ie).forEach(function(re){if(C.quality=="draft")if(!re.isParent())ce=ye.get(re.id()),ke.nodes.push({x:R[ue].xCoords[ce]-re.boundingbox().w/2,y:R[ue].yCoords[ce]-re.boundingbox().h/2,width:re.boundingbox().w,height:re.boundingbox().h});else{var J=f.calcBoundingBox(re,R[ue].xCoords,R[ue].yCoords,ye);ke.nodes.push({x:J.topLeftX,y:J.topLeftY,width:J.width,height:J.height})}else I[ue][re.id()]&&ke.nodes.push({x:I[ue][re.id()].getLeft(),y:I[ue][re.id()].getTop(),width:I[ue][re.id()].getWidth(),height:I[ue][re.id()].getHeight()})}),Oe.edges().forEach(function(re){var J=re.source(),se=re.target();if(J.css("display")!="none"&&se.css("display")!="none")if(C.quality=="draft"){var ge=ye.get(J.id()),Te=ye.get(se.id()),we=[],Me=[];if(J.isParent()){var ve=f.calcBoundingBox(J,R[ue].xCoords,R[ue].yCoords,ye);we.push(ve.topLeftX+ve.width/2),we.push(ve.topLeftY+ve.height/2)}else we.push(R[ue].xCoords[ge]),we.push(R[ue].yCoords[ge]);if(se.isParent()){var ne=f.calcBoundingBox(se,R[ue].xCoords,R[ue].yCoords,ye);Me.push(ne.topLeftX+ne.width/2),Me.push(ne.topLeftY+ne.height/2)}else Me.push(R[ue].xCoords[Te]),Me.push(R[ue].yCoords[Te]);ke.edges.push({startX:we[0],startY:we[1],endX:Me[0],endY:Me[1]})}else I[ue][J.id()]&&I[ue][se.id()]&&ke.edges.push({startX:I[ue][J.id()].getCenterX(),startY:I[ue][J.id()].getCenterY(),endX:I[ue][se.id()].getCenterX(),endY:I[ue][se.id()].getCenterY()})}),ke.nodes.length>0&&(le.push(ke),te.add(ue))}});var ae=D.packComponents(le,C.randomize).shifts;if(C.quality=="draft")R.forEach(function(Oe,ue){var ye=Oe.xCoords.map(function(ce){return ce+ae[ue].dx}),ke=Oe.yCoords.map(function(ce){return ce+ae[ue].dy});Oe.xCoords=ye,Oe.yCoords=ke});else{var Re=0;te.forEach(function(Oe){Object.keys(I[Oe]).forEach(function(ue){var ye=I[Oe][ue];ye.setCenter(ye.getCenterX()+ae[Re].dx,ye.getCenterY()+ae[Re].dy)}),Re++})}}}else{var B=C.eles.boundingBox();if(A.push({x:B.x1+B.w/2,y:B.y1+B.h/2}),C.randomize){var O=m(C);R.push(O)}C.quality=="default"||C.quality=="proof"?(I.push(y(C,R[0])),f.relocateComponent(A[0],I[0],C)):f.relocateComponent(A[0],R[0],C)}var be=o(function(ue,ye){if(C.quality=="default"||C.quality=="proof"){typeof ue=="number"&&(ue=ye);var ke=void 0,ce=void 0,re=ue.data("id");return I.forEach(function(se){re in se&&(ke={x:se[re].getRect().getCenterX(),y:se[re].getRect().getCenterY()},ce=se[re])}),C.nodeDimensionsIncludeLabels&&(ce.labelWidth&&(ce.labelPosHorizontal=="left"?ke.x+=ce.labelWidth/2:ce.labelPosHorizontal=="right"&&(ke.x-=ce.labelWidth/2)),ce.labelHeight&&(ce.labelPosVertical=="top"?ke.y+=ce.labelHeight/2:ce.labelPosVertical=="bottom"&&(ke.y-=ce.labelHeight/2))),ke==null&&(ke={x:ue.position("x"),y:ue.position("y")}),{x:ke.x,y:ke.y}}else{var J=void 0;return R.forEach(function(se){var ge=se.nodeIndexes.get(ue.id());ge!=null&&(J={x:se.xCoords[ge],y:se.yCoords[ge]})}),J==null&&(J={x:ue.position("x"),y:ue.position("y")}),{x:J.x,y:J.y}}},"getPositions");if(C.quality=="default"||C.quality=="proof"||C.randomize){var Pe=f.calcParentsWithoutChildren(w,S),Ge=S.filter(function(Oe){return Oe.css("display")=="none"});C.eles=S.not(Ge),S.nodes().not(":parent").not(Ge).layoutPositions(k,C,be),Pe.length>0&&Pe.forEach(function(Oe){Oe.position(be(Oe))})}else console.log("If randomize option is set to false, then quality option must be 'default' or 'proof'.")},"run")}]),b})();a.exports=x}),657:((a,s,l)=>{var u=l(548),h=l(140).layoutBase.Matrix,d=l(140).layoutBase.SVD,f=o(function(m){var g=m.cy,y=m.eles,v=y.nodes(),x=y.nodes(":parent"),b=new Map,T=new Map,k=new Map,C=[],w=[],S=[],R=[],L=[],N=[],I=[],_=[],A=void 0,M=void 0,D=1e8,P=1e-9,B=m.piTol,O=m.samplingType,$=m.nodeSeparation,V=void 0,G=o(function(){for(var he=0,X=0,fe=!1;X=qe;){Be=K[qe++];for(var Ke=C[Be],xe=0;xe$e&&($e=L[Le],Xe=Le)}return Xe},"BFS"),W=o(function(he){var X=void 0;if(he){X=Math.floor(Math.random()*M),A=X;for(var K=0;K=1)break;$e=He}for(var Ke=0;Ke=1)break;$e=He}for(var mt=0;mt0&&(X.isParent()?C[he].push(k.get(X.id())):C[he].push(X.id()))})});var Pe=o(function(he){var X=T.get(he),fe=void 0;b.get(he).forEach(function(K){g.getElementById(K).isParent()?fe=k.get(K):fe=K,C[X].push(fe),C[T.get(fe)].push(he)})},"_loop"),Ge=!0,Oe=!1,ue=void 0;try{for(var ye=b.keys()[Symbol.iterator](),ke;!(Ge=(ke=ye.next()).done);Ge=!0){var ce=ke.value;Pe(ce)}}catch(q){Oe=!0,ue=q}finally{try{!Ge&&ye.return&&ye.return()}finally{if(Oe)throw ue}}M=T.size;var re=void 0;if(M>2){V=M{var u=l(212),h=o(function(f){f&&f("layout","fcose",u)},"register");typeof cytoscape<"u"&&h(cytoscape),a.exports=h}),140:(a=>{a.exports=e})},r={};function n(a){var s=r[a];if(s!==void 0)return s.exports;var l=r[a]={exports:{}};return t[a](l,l.exports,n),l.exports}o(n,"__webpack_require__");var i=n(579);return i})()})});function Kj(e,t){if(e===0)return t();let r=Math.random,n=e>>>0;Math.random=function(){n=n+1831565813>>>0;let i=n;return i=Math.imul(i^i>>>15,i|1),i^=i+Math.imul(i^i>>>7,i|61),((i^i>>>14)>>>0)/4294967296};try{return t()}finally{Math.random=r}}var L9e=F(()=>{"use strict";o(Kj,"withSeededRandom")});var gx,M0,Zj=F(()=>{"use strict";Vl();gx=o(e=>`${e}`,"wrapIcon"),M0={prefix:"mermaid-architecture",height:80,width:80,icons:{database:{body:gx('')},server:{body:gx('')},disk:{body:gx('')},internet:{body:gx('')},cloud:{body:gx('')},unknown:d8,blank:{body:gx("")}}}});var D9e,I9e,M9e,N9e,P9e=F(()=>{"use strict";Xt();Ls();Vl();Vr();Zj();sD();Qt();D9e=o(async function(e,t,r,n){let i=r.getConfigField("padding"),a=r.getConfigField("iconSize"),s=a/2,l=a/6,u=l/2;await Promise.all(t.edges().map(async h=>{let{source:d,sourceDir:f,sourceArrow:p,sourceGroup:m,target:g,targetDir:y,targetArrow:v,targetGroup:x,label:b}=aD(h),{x:T,y:k}=h[0].sourceEndpoint(),{x:C,y:w}=h[0].midpoint(),{x:S,y:R}=h[0].targetEndpoint(),L=i+4;if(m&&(ms(f)?T+=f==="L"?-L:L:k+=f==="T"?-L:L+18),x&&(ms(y)?S+=y==="L"?-L:L:R+=y==="T"?-L:L+18),!m&&r.getNode(d)?.type==="junction"&&(ms(f)?T+=f==="L"?s:-s:k+=f==="T"?s:-s),!x&&r.getNode(g)?.type==="junction"&&(ms(y)?S+=y==="L"?s:-s:R+=y==="T"?s:-s),h[0]._private.rscratch){let N=e.insert("g");if(N.insert("path").attr("d",`M ${T},${k} L ${C},${w} L${S},${R} `).attr("class","edge").attr("id",`${n}-${eu(d,g,{prefix:"L"})}`),p){let I=ms(f)?Jw[f](T,l):T-u,_=ju(f)?Jw[f](k,l):k-u;N.insert("polygon").attr("points",Gj[f](l)).attr("transform",`translate(${I},${_})`).attr("class","arrow")}if(v){let I=ms(y)?Jw[y](S,l):S-u,_=ju(y)?Jw[y](R,l):R-u;N.insert("polygon").attr("points",Gj[y](l)).attr("transform",`translate(${I},${_})`).attr("class","arrow")}if(b){let I=ek(f,y)?"XY":ms(f)?"X":"Y",_=0;I==="X"?_=Math.abs(T-S):I==="Y"?_=Math.abs(k-R)/1.5:_=Math.abs(T-S)/2;let A=N.append("g");if(await Pn(A,b,{useHtmlLabels:!1,width:_,classes:"architecture-service-label"},Ae()),A.attr("dy","1em").attr("alignment-baseline","middle").attr("dominant-baseline","middle").attr("text-anchor","middle"),I==="X")A.attr("transform","translate("+C+", "+w+")");else if(I==="Y")A.attr("transform","translate("+C+", "+w+") rotate(-90)");else if(I==="XY"){let M=tk(f,y);if(M&&b9e(M)){let D=A.node().getBoundingClientRect(),[P,B]=C9e(M);A.attr("dominant-baseline","auto").attr("transform",`rotate(${-1*P*B*45})`);let O=A.node().getBoundingClientRect();A.attr("transform",` + translate(${C}, ${w-D.height/2}) + translate(${P*O.width/2}, ${B*O.height/2}) + rotate(${-1*P*B*45}, 0, ${D.height/2}) + `)}}}}}))},"drawEdges"),I9e=o(async function(e,t,r,n){let a=r.getConfigField("padding")*.75,s=r.getConfigField("fontSize"),u=r.getConfigField("iconSize")/2;await Promise.all(t.nodes().map(async h=>{let d=Ip(h);if(d.type==="group"){let{h:f,w:p,x1:m,y1:g}=h.boundingBox(),y=e.append("rect");y.attr("id",`${n}-group-${d.id}`).attr("x",m+u).attr("y",g+u).attr("width",p).attr("height",f).attr("class","node-bkg");let v=e.append("g"),x=m,b=g;if(d.icon){let T=v.append("g");T.html(`${await ts(d.icon,{height:a,width:a,fallbackPrefix:M0.prefix})}`),T.attr("transform","translate("+(x+u+1)+", "+(b+u+1)+")"),x+=a,b+=s/2-1-2}if(d.label){let T=v.append("g");await Pn(T,d.label,{useHtmlLabels:!1,width:p,classes:"architecture-service-label"},Ae()),T.attr("dy","1em").attr("alignment-baseline","middle").attr("dominant-baseline","start").attr("text-anchor","start"),T.attr("transform","translate("+(x+u+4)+", "+(b+u+2)+")")}r.setElementForId(d.id,y)}}))},"drawGroups"),M9e=o(async function(e,t,r,n){let i=Ae();for(let a of r){let s=t.append("g"),l=e.getConfigField("iconSize");if(a.title){let f=s.append("g");await Pn(f,a.title,{useHtmlLabels:!1,width:l*1.5,classes:"architecture-service-label"},i),f.attr("dy","1em").attr("alignment-baseline","middle").attr("dominant-baseline","middle").attr("text-anchor","middle"),f.attr("transform","translate("+l/2+", "+l+")")}let u=s.append("g");if(a.icon)u.html(`${await ts(a.icon,{height:l,width:l,fallbackPrefix:M0.prefix})}`);else if(a.iconText){u.html(`${await ts("blank",{height:l,width:l,fallbackPrefix:M0.prefix})}`);let m=u.append("g").append("foreignObject").attr("width",l).attr("height",l).append("div").attr("class","node-icon-text").attr("style",`height: ${l}px;`).append("div").html(mr(a.iconText,i)),g=parseInt(window.getComputedStyle(m.node(),null).getPropertyValue("font-size").replace(/\D/g,""))??16;m.attr("style",`-webkit-line-clamp: ${Math.floor((l-2)/g)};`)}else u.append("path").attr("class","node-bkg").attr("id",`${n}-node-${a.id}`).attr("d",`M0,${l} V5 Q0,0 5,0 H${l-5} Q${l},0 ${l},5 V${l} Z`);s.attr("id",`${n}-service-${a.id}`).attr("class","architecture-service");let{width:h,height:d}=s.node().getBBox();a.width=h,a.height=d,e.setElementForId(a.id,s)}return 0},"drawServices"),N9e=o(function(e,t,r,n){r.forEach(i=>{let a=t.append("g"),s=e.getConfigField("iconSize");a.append("g").append("rect").attr("id",`${n}-node-${i.id}`).attr("fill-opacity","0").attr("width",s).attr("height",s),a.attr("class","architecture-junction");let{width:u,height:h}=a._groups[0][0].getBBox();a.width=u,a.height=h,e.setElementForId(i.id,a)})},"drawJunctions")});function T8t(e,t,r){e.forEach(n=>{t.add({group:"nodes",data:{type:"service",id:n.id,icon:n.icon,label:n.title,parent:n.in,width:r.getConfigField("iconSize"),height:r.getConfigField("iconSize")},classes:"node-service"})})}function C8t(e,t,r){e.forEach(n=>{t.add({group:"nodes",data:{type:"junction",id:n.id,parent:n.in,width:r.getConfigField("iconSize"),height:r.getConfigField("iconSize")},classes:"node-junction"})})}function w8t(e,t){t.nodes().map(r=>{let n=Ip(r);if(n.type==="group")return;n.x=r.position().x,n.y=r.position().y,e.getElementById(n.id).attr("transform","translate("+(n.x||0)+","+(n.y||0)+")")})}function k8t(e,t){e.forEach(r=>{t.add({group:"nodes",data:{type:"group",id:r.id,icon:r.icon,label:r.title,parent:r.in},classes:"node-group"})})}function S8t(e,t){e.forEach(r=>{let{lhsId:n,rhsId:i,lhsInto:a,lhsGroup:s,rhsInto:l,lhsDir:u,rhsDir:h,rhsGroup:d,title:f}=r,p=ek(r.lhsDir,r.rhsDir)?"segments":"straight",m={id:`${n}-${i}`,label:f,source:n,sourceDir:u,sourceArrow:a,sourceGroup:s,sourceEndpoint:u==="L"?"0 50%":u==="R"?"100% 50%":u==="T"?"50% 0":"50% 100%",target:i,targetDir:h,targetArrow:l,targetGroup:d,targetEndpoint:h==="L"?"0 50%":h==="R"?"100% 50%":h==="T"?"50% 0":"50% 100%"};t.add({group:"edges",data:m,classes:p})})}function E8t(e,t,r,n=[]){let i=o((p,m)=>{let g=new Map;for(let[y,v]of p.entries()){let x=`${y}`,b=0,T=[...v.entries()];if(T.length===1){g.set(x,T[0][1]);continue}for(let k=0;k{let m=new Map,g=new Map;return p.forEach(([y,v],x)=>{let b=e.getNode(x)?.in??"default",T=m.get(v)??new Map;m.has(v)||m.set(v,T);let k=g.get(y)??new Map;g.has(y)||g.set(y,k);for(let C of[T,k]){let w=C.get(b)??[];C.has(b)||C.set(b,w),w.push(x)}}),{horiz:[...i(m,"horizontal").values()].filter(y=>y.length>1),vert:[...i(g,"vertical").values()].filter(y=>y.length>1)}}),[s,l]=a.reduce(([p,m],{horiz:g,vert:y})=>[[...p,...g],[...m,...y]],[[],[]]),u=new Set;n.forEach(p=>p.members.forEach(m=>u.add(m)));let h=o(p=>p.filter(m=>!m.some(g=>u.has(g))),"dropOverlapping"),d=h(s),f=h(l);return n.forEach(p=>{p.members.length<2||(p.direction==="row"?d.push([...p.members]):f.push([...p.members]))}),{horizontal:d,vertical:f}}function A8t(e,t,r=[]){let n=[],i=t.getConfigField("iconSize"),a=t.getConfigField("idealEdgeLengthMultiplier"),s=a*i,l=new Set;r.forEach(d=>{for(let f=0;f`${d[0]},${d[1]}`,"posToStr"),h=o(d=>d.split(",").map(f=>parseInt(f)),"strToPos");return e.forEach(d=>{let f=new Map([...d.entries()].map(([y,v])=>[u(v),y])),p=[u([0,0])],m={},g={L:[-1,0],R:[1,0],T:[0,1],B:[0,-1]};for(;p.length>0;){let y=p.shift();if(y){m[y]=1;let v=f.get(y);if(v){let x=h(y);Object.entries(g).forEach(([b,T])=>{let k=u([x[0]+T[0],x[1]+T[1]]),C=f.get(k);if(C&&!m[k]){if(p.push(k),l.has(`${v}|${C}`))return;n.push({[zj[b]]:C,[zj[x9e(b)]]:v,gap:a*i})}})}}}}),n}function R8t(e,t,r,n,i,{spatialMaps:a,groupAlignments:s}){return new Promise(l=>{let u=et("body").append("div").attr("id","cy").attr("style","display:none"),h=El({container:document.getElementById("cy"),style:[{selector:"edge",style:{"curve-style":"straight","source-endpoint":"data(sourceEndpoint)","target-endpoint":"data(targetEndpoint)"}},{selector:"edge[label]",style:{label:"data(label)"}},{selector:"edge.segments",style:{"curve-style":"segments","segment-weights":"0","segment-distances":[.5],"edge-distances":"endpoints","source-endpoint":"data(sourceEndpoint)","target-endpoint":"data(targetEndpoint)"}},{selector:"node",style:{"compound-sizing-wrt-labels":"include"}},{selector:"node[label]",style:{"text-valign":"bottom","text-halign":"center","font-size":`${i.getConfigField("fontSize")}px`}},{selector:".node-service",style:{label:"data(label)",width:"data(width)",height:"data(height)"}},{selector:".node-junction",style:{width:"data(width)",height:"data(height)"}},{selector:".node-group",style:{padding:`${i.getConfigField("padding")}px`}}],layout:{name:"grid",boundingBox:{x1:0,x2:100,y1:0,y2:100}}});u.remove(),k8t(r,h),T8t(e,h,i),C8t(t,h,i),S8t(n,h);let d=i.getLayoutHints(),f=E8t(i,a,s,d),p=A8t(a,i,d),m=i.getConfigField("iconSize"),g=i.getConfigField("idealEdgeLengthMultiplier")*m,y=.5*m,v=i.getConfigField("edgeElasticity"),x=i.getConfigField("seed"),b=h.layout({name:"fcose",quality:"proof",randomize:i.getConfigField("randomize"),nodeSeparation:i.getConfigField("nodeSeparation"),numIter:i.getConfigField("numIter"),styleEnabled:!1,animate:!1,nodeDimensionsIncludeLabels:!1,idealEdgeLength(T){let[k,C]=T.connectedNodes(),{parent:w}=Ip(k),{parent:S}=Ip(C);return w===S?g:y},edgeElasticity(T){let[k,C]=T.connectedNodes(),{parent:w}=Ip(k),{parent:S}=Ip(C);return w===S?v:.001},alignmentConstraint:f,relativePlacementConstraint:p});b.one("layoutstop",()=>{function T(k,C,w,S){let R,L,{x:N,y:I}=k,{x:_,y:A}=C;L=(S-I+(N-w)*(I-A)/(N-_))/Math.sqrt(1+Math.pow((I-A)/(N-_),2)),R=Math.sqrt(Math.pow(S-I,2)+Math.pow(w-N,2)-Math.pow(L,2));let M=Math.sqrt(Math.pow(_-N,2)+Math.pow(A-I,2));R=R/M;let D=(_-N)*(S-I)-(A-I)*(w-N);switch(!0){case D>=0:D=1;break;case D<0:D=-1;break}let P=(_-N)*(w-N)+(A-I)*(S-I);switch(!0){case P>=0:P=1;break;case P<0:P=-1;break}return L=Math.abs(L)*D,R=R*P,{distances:L,weights:R}}o(T,"getSegmentWeights"),h.startBatch();for(let k of Object.values(h.edges()))if(k.data?.()){let{x:C,y:w}=k.source().position(),{x:S,y:R}=k.target().position();if(C!==S&&w!==R){let L=k.sourceEndpoint(),N=k.targetEndpoint(),{sourceDir:I}=aD(k),[_,A]=ju(I)?[L.x,N.y]:[N.x,L.y],{weights:M,distances:D}=T(L,N,_,A);k.style("segment-distances",D),k.style("segment-weights",M)}}h.endBatch(),Kj(x,()=>b.run())});try{Kj(x,()=>b.run())}catch(T){throw T instanceof RangeError&&T.message.includes("Invalid array length")?new Error("Architecture layout failed: a declared `align row|column` directive likely contradicts the edge directions, or two declared alignments overlap on a shared node. Check that the order of members in each `align` chain is consistent with the edges between them, and that no node appears in two `align` directives along the same axis."):T}h.ready(T=>{Z.info("Ready",T),l(h)})})}var O9e,_8t,B9e,$9e=F(()=>{"use strict";WF();O9e=Xs(_9e(),1);L9e();$r();vt();Vl();Ka();$n();Zj();sD();P9e();ty([{name:M0.prefix,icons:M0}]);El.use(O9e.default);o(T8t,"addServices");o(C8t,"addJunctions");o(w8t,"positionNodes");o(k8t,"addGroups");o(S8t,"addEdges");o(E8t,"getAlignments");o(A8t,"getRelativeConstraints");o(R8t,"layoutArchitecture");_8t=o(async(e,t,r,n)=>{let i=n.db;i.setDiagramId(t);let a=i.getServices(),s=i.getJunctions(),l=i.getGroups(),u=i.getEdges(),h=i.getDataStructures(),d=xn(t),f=d.append("g");f.attr("class","architecture-edges");let p=d.append("g");p.attr("class","architecture-services");let m=d.append("g");m.attr("class","architecture-groups"),await M9e(i,p,a,t),N9e(i,p,s,t);let g=await R8t(a,s,l,u,i,h);await D9e(f,g,i,t),await I9e(m,g,i,t),w8t(i,g),ul(void 0,d,i.getConfigField("padding"),i.getConfigField("useMaxWidth"))},"draw"),B9e={draw:_8t}});var F9e={};ir(F9e,{diagram:()=>L8t});var L8t,z9e=F(()=>{"use strict";E9e();Wj();R9e();$9e();L8t={parser:qj,get db(){return new mx},renderer:B9e,styles:A9e}});var Qj,Jj,oD,eX,W9e=F(()=>{"use strict";Qj="position frame",Jj="frame positioned",oD="position relation",eX="relation positioned"});function B8t(){nX={}}function z8t(){let e=V8t,{ast:t}=nX,r=H9e();if(!t)throw new Error("No data for EventModel");return t.frames.forEach((n,i)=>{let a=j8t(n,t.dataEntities,r);e=rX(e,{$kind:Qj,index:i,frame:n,textProps:a});let s;eIt(n)?(Z.debug("source frame",n.sourceFrames),s=t.frames.filter(l=>n.sourceFrames.some(u=>u.$refText===l.name)),s.forEach(l=>{e=rX(e,{$kind:oD,index:i,frame:n,sourceFrame:l})})):e=rX(e,{$kind:oD,index:i,frame:n})}),e={...e,sortedSwimlanesArray:U9e(e.swimlanes)},e}function G8t(e){nX.ast=e}function H9e(){return bn}function W8t(e){let t=e.split(".");if(t.length===2)return t[0]}function q8t(e){let t=e.split(".");return t.length===2?t[1]:e}function H8t(e,t){if(!(!t||t.length===0))return Object.values(e).find(r=>r.namespace===t)}function tX(e,t,r){return Math.max(t,...Object.keys(e).filter(n=>{let i=Number.parseInt(n);return i>t&&iNumber.parseInt(n)))+1}function U8t(e,t){let r=W8t(e.entityIdentifier),n=H8t(t,r);switch(e.modelEntityType){case"ui":case"pcr":case"processor":return n?{index:n.index,label:n.namespace||bn.labelUiAutomation}:r?{index:tX(t,0,100),label:bn.labelUiAutomationPrefix+r}:{index:0,label:bn.labelUiAutomation};case"rmo":case"readmodel":case"cmd":case"command":return n?{index:n.index,label:n.namespace||bn.labelCommandReadModel}:r?{index:tX(t,100,200),label:bn.labelCommandReadModelPrefix+r}:{index:100,label:bn.labelCommandReadModel};case"evt":case"event":default:return n?{index:n.index,label:n.namespace||bn.labelEvents}:r?{index:tX(t,200,300),label:bn.labelEventsPrefix+r}:{index:200,label:bn.labelEvents}}}function Y8t(e){let{themeVariables:t}=_t();switch(e.modelEntityType){case"ui":return{fill:t.emUiFill??"white",stroke:t.emUiStroke??"#dbdada"};case"pcr":case"processor":return{fill:t.emProcessorFill??"#edb3f6",stroke:t.emProcessorStroke??"#b88cbf"};case"rmo":case"readmodel":return{fill:t.emReadModelFill??"#d3f1a2",stroke:t.emReadModelStroke??"#a3b732"};case"cmd":case"command":return{fill:t.emCommandFill??"#bcd6fe",stroke:t.emCommandStroke??"#679ac3"};case"evt":case"event":return{fill:t.emEventFill??"#ffb778",stroke:t.emEventStroke??"#c19a0f"};default:return{fill:"red",stroke:"black"}}}function j8t(e,t,r){let n=_t(),i=mr(q8t(e.entityIdentifier)??"",n),a,s={fontSize:16,fontWeight:700,fontFamily:'"trebuchet ms", verdana, arial, sans-serif',joinWith:"
"},u=`${vm(i,r.textMaxWidth,s)}`;if(e.dataInlineValue&&(a=e.dataInlineValue,a=a.substring(a.indexOf("{")+1),a=a.substring(0,a.lastIndexOf("}")-1),a=mr(a,n),a=vm(a,r.textMaxWidth,s),a=a.replaceAll(" "," ")),e.dataReference){let g=t.find(y=>y.name===e.dataReference?.$refText);g&&(a=g.dataBlockValue,a=a.substring(a.indexOf(`{ +`)+2),a=a.substring(0,a.lastIndexOf("}")-1),a=mr(a,n),a=vm(a,r.textMaxWidth,s),a=a.replaceAll(" "," "),a+="
")}let h=a!==void 0;h&&(u+=`

${a}`);let d={fontSize:s.fontSize,fontWeight:s.fontWeight,fontFamily:s.fontFamily},f=T2(u,d),p=h?f.width/3:f.width,m={content:u,width:p,height:f.height};return Z.debug(`[${e.name}] ${e.entityIdentifier} text`,m),m}function X8t(e,t){let r=t,n=Y8t(r.frame),i={width:r.textProps.width+2*bn.boxTextPadding,height:r.textProps.height+2*bn.boxTextPadding};return[{$kind:Jj,frame:r.frame,index:r.index,visual:n,dimension:i,textProps:r.textProps}]}function K8t(e,t,r){return t===void 0?bn.contentStartX:t.index===e.index&&e.r?e.r+bn.boxPadding:r===void 0?bn.contentStartX:r.r-bn.boxOverlap+bn.boxPadding}function Z8t(e,t){let r=[...e.map(n=>n.r),t];return Math.max(...r)}function U9e(e){return Object.values(e).sort((t,r)=>t.index-r.index)}function Q8t(e,t){let r=t,n=U8t(r.frame,e.swimlanes),i;n.index in e.swimlanes?i=e.swimlanes[n.index]:i={index:n.index,label:n.label,r:0,y:n.index*bn.swimlaneMinHeight+bn.swimlaneGap,height:bn.swimlaneMinHeight,maxHeight:bn.swimlaneMinHeight};let a=e.boxes.length>0?e.boxes[e.boxes.length-1]:void 0,s=e.previousSwimlaneNumber!==void 0?e.swimlanes[e.previousSwimlaneNumber]:void 0,l={width:Math.max(bn.boxMinWidth,Math.min(bn.boxMaxWidth,r.dimension.width))+2*bn.boxPadding,height:Math.max(bn.boxMinHeight,Math.min(bn.boxMaxHeight,r.dimension.height))+2*bn.boxPadding},u=K8t(i,s,a),h=u+l.width+bn.boxPadding,d=Z8t(Object.values(e.swimlanes),h);i.r=u+l.width,i.maxHeight=Math.max(i.maxHeight,l.height),i.height=Math.max(bn.swimlaneMinHeight,i.maxHeight)+2*bn.swimlanePadding;let f={x:u,y:bn.swimlanePadding+i.y,r:h,dimension:l,leftSibling:!1,swimlane:i,visual:r.visual,text:r.textProps.content,frame:r.frame,index:r.index},p={...e,boxes:[...e.boxes,f],swimlanes:{...e.swimlanes,[`${i.index}`]:i},previousSwimlaneNumber:n.index,previousFrame:r.frame,maxR:d},m=U9e(p.swimlanes);m.length>0&&(m[0].y=0);for(let g=1;g0}function q9e(e,t){if(t!=null)return e.find(r=>r.frame.name===t.name)}function tIt(e,t,r){if(!(r<0))for(let n=r;n>=0;n--){let i=e[n];if(i.swimlane.index!==t)return i}}function rIt(e,t){let r=t;if(S_(r.frame)||J8t(r.index,r.frame))return[];let n=q9e(e.boxes,r.frame);if(n===void 0)throw new Error(`Target box not found for frame ${r.frame.name}`);let i;return r.sourceFrame?i=q9e(e.boxes,r.sourceFrame):i=tIt(e.boxes,n.swimlane.index,r.index-1),i===void 0?[]:[{$kind:eX,frame:r.frame,index:r.index,sourceBox:i,targetBox:n}]}function nIt(e,t){let r=t,n={visual:{fill:"none",stroke:"#000"},source:{x:r.sourceBox.x,y:r.sourceBox.y},target:{x:r.targetBox.x,y:r.targetBox.y},sourceBox:r.sourceBox,targetBox:r.targetBox};return{...e,relations:[...e.relations,n]}}function sIt(e,t){let r=iIt[t.$kind];if(r==null)return[];let n=r(e,t);return Z.debug("decided events",n),n}function oIt(e,t){let r=t.reduce((n,i)=>{let a=aIt[i.$kind];return a==null?n:a(n,i)},e);return Z.debug("evolve events",{state:e,newState:r,events:t}),r}function rX(e,t){let r=sIt(e,t);return oIt(e,r)}var N8t,P8t,O8t,$8t,F8t,nX,bn,V8t,iIt,aIt,ak,iX=F(()=>{"use strict";vt();Qt();Qt();ur();Nn();Vr();Wi();Xa();W9e();N8t=o(function(e){Z.debug("options str",e)},"setOptions"),P8t=o(function(){return{}},"getOptions"),O8t=o(function(){B8t(),yr()},"clear");o(B8t,"reset");$8t=cr.eventmodeling,F8t=o(()=>qr({...$8t,..._t().eventmodeling}),"getConfig"),nX={};o(z8t,"getState");o(G8t,"setAst");bn={swimlaneMinHeight:70,swimlanePadding:15,swimlaneGap:10,boxPadding:10,boxOverlap:90,boxDefaultY:0,boxMinWidth:80,boxMaxWidth:450,boxMinHeight:80,boxMaxHeight:750,contentStartX:250,textMaxWidth:430,boxTextFontWeight:"bold",boxTextPadding:10,swimlaneTextFontWeight:"bold",labelUiAutomation:"UI/Automation",labelUiAutomationPrefix:"UI/A: ",labelCommandReadModel:"Command/Read Model",labelCommandReadModelPrefix:"C/RM: ",labelEvents:"Events",labelEventsPrefix:"Stream: "};o(H9e,"getDiagramProps");V8t={boxes:[],swimlanes:{},relations:[],maxR:0,sortedSwimlanesArray:[]};o(W8t,"extractNamespace");o(q8t,"extractName");o(H8t,"findSwimlaneByNamespace");o(tX,"findNextAvailableIndex");o(U8t,"calculateSwimlaneProps");o(Y8t,"calculateEntityVisualProps");o(j8t,"calculateTextProps");o(X8t,"decidePositionFrame");o(K8t,"calculateX");o(Z8t,"calculateMaxRight");o(U9e,"sortedSwimlanesArray");o(Q8t,"evolveFramePositioned");o(J8t,"isFirstFrame");o(eIt,"hasSourceFrame");o(q9e,"findBoxByFrame");o(tIt,"findBoxByLineIndex");o(rIt,"decidePositionRelation");o(nIt,"evolveRelationPositioned");iIt={[Qj]:X8t,[oD]:rIt},aIt={[Jj]:Q8t,[eX]:nIt};o(sIt,"decide");o(oIt,"evolve");o(rX,"dispatch");ak={getConfig:F8t,setOptions:N8t,getOptions:P8t,clear:O8t,setAccTitle:kr,getAccTitle:Ar,getAccDescription:_r,setAccDescription:Rr,setDiagramTitle:Or,getDiagramTitle:Lr,setAst:G8t,getDiagramProps:H9e,getState:z8t}});var Y9e,j9e=F(()=>{"use strict";Xa();vt();Hs();iX();Y9e={parse:o(async e=>{let t=await Si("eventmodeling",e);Z.debug(t),ak.setAst(t),Gn(t,ak)},"parse")}});function uIt(e,t){return r=>{let n=r.swimlane.y+t.swimlanePadding,i=e.append("g").attr("class","em-box");i.append("rect").attr("x",r.x).attr("y",n).attr("rx","3").attr("width",r.dimension.width).attr("height",r.dimension.height).attr("stroke",r.visual.stroke).attr("fill",r.visual.fill),i.append("foreignObject").attr("x",r.x+t.boxPadding).attr("y",n+10).attr("width",r.dimension.width-2*t.boxPadding).attr("height",r.dimension.height-2*t.boxPadding).append("xhtml:div").style("display","table").style("height","100%").style("width","100%").append("span").style("display","table-cell").style("text-align","center").style("vertical-align","middle").html(r.text)}}function hIt(e,t){return e>t}function dIt(e,t,r,n){return i=>{let a=i.sourceBox.swimlane.y+t.swimlanePadding,s=i.targetBox.swimlane.y+t.swimlanePadding,l=hIt(a,s),u=i.sourceBox.x+i.sourceBox.dimension.width*2/3,h=i.targetBox.x+i.targetBox.dimension.width/3,d,f;Z.debug(`rendering relation up=${l} for `,{sourceBox:i.sourceBox,targetBox:i.targetBox}),l?(d=a,f=s+i.targetBox.dimension.height):(d=a+i.sourceBox.dimension.height,f=s);let p=n.emRelationStroke??i.visual.stroke;e.append("path").attr("class","em-relation").attr("fill",i.visual.fill).attr("stroke",p).attr("stroke-width","1").attr("marker-end",`url(#${r})`).attr("d",`M${u} ${d} L${h} ${f}`)}}function fIt(e,t,r,n){return i=>{let a=e.append("g").attr("class","em-swimlane"),s=n.emSwimlaneBackgroundOdd??"rgb(250,250,250)",l=n.emSwimlaneBackgroundStroke??"rgb(240,240,240)";a.append("rect").attr("x",0).attr("y",i.y).attr("rx","3").attr("width",t+r.swimlanePadding).attr("height",i.height).attr("fill",s).attr("stroke",l),a.append("text").attr("font-weight",r.swimlaneTextFontWeight).attr("x",30).attr("y",i.y+30).text(i.label)}}var lIt,cIt,pIt,X9e,K9e=F(()=>{"use strict";$r();Xt();vt();lIt=Ae(),cIt=lIt?.eventmodeling;o(uIt,"renderD3Box");o(hIt,"dirUpwards");o(dIt,"renderD3Relation");o(fIt,"renderD3Swimlane");pIt=o(function(e,t,r,n){if(Z.debug("in eventmodeling renderer",e+` +`,"id:",t,r),!cIt)throw new Error("EventModeling config not found");let i=n.db,{themeVariables:a,eventmodeling:s}=Ae(),l=et(`[id="${t}"]`),u=i.getDiagramProps(),h=i.getState(),d=`em-arrowhead-${t}`,f=a.emArrowhead??"#000000";h.sortedSwimlanesArray.forEach(fIt(l,h.maxR,u,a)),h.boxes.forEach(uIt(l,u)),h.relations.forEach(dIt(l,u,d,a)),l.append("defs").append("marker").attr("id",d).attr("markerWidth","10").attr("markerHeight","7").attr("refX","10").attr("refY","3.5").attr("orient","auto").append("polygon").attr("points","0 0, 10 3.5, 0 7").attr("fill",f),db(void 0,l,s?.padding??30,s?.useMaxWidth)},"draw"),X9e={draw:pIt}});var mIt,Z9e,Q9e=F(()=>{"use strict";mIt=o(e=>"","getStyles"),Z9e=mIt});var J9e={};ir(J9e,{diagram:()=>gIt});var gIt,eBe=F(()=>{"use strict";j9e();iX();K9e();Q9e();gIt={parser:Y9e,db:ak,renderer:X9e,styles:Z9e}});var aX,nBe,iBe=F(()=>{"use strict";aX=(function(){var e=o(function(x,b,T,k){for(T=T||{},k=x.length;k--;T[x[k]]=b);return T},"o"),t=[1,4],r=[1,14],n=[1,12],i=[1,13],a=[6,7,8],s=[1,20],l=[1,18],u=[1,19],h=[6,7,11],d=[1,6,13,14],f=[1,23],p=[1,24],m=[1,6,7,11,13,14],g={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,ishikawa:4,spaceLines:5,SPACELINE:6,NL:7,ISHIKAWA:8,document:9,stop:10,EOF:11,statement:12,SPACELIST:13,TEXT:14,$accept:0,$end:1},terminals_:{2:"error",6:"SPACELINE",7:"NL",8:"ISHIKAWA",11:"EOF",13:"SPACELIST",14:"TEXT"},productions_:[0,[3,1],[3,2],[5,1],[5,2],[5,2],[4,2],[4,3],[10,1],[10,1],[10,1],[10,2],[10,2],[9,3],[9,2],[12,2],[12,1],[12,1],[12,1]],performAction:o(function(b,T,k,C,w,S,R){var L=S.length-1;switch(w){case 6:case 7:return C;case 15:C.addNode(S[L-1].length,S[L].trim());break;case 16:C.addNode(0,S[L].trim());break}},"anonymous"),table:[{3:1,4:2,5:3,6:[1,5],8:t},{1:[3]},{1:[2,1]},{4:6,6:[1,7],7:[1,8],8:t},{6:r,7:[1,10],9:9,12:11,13:n,14:i},e(a,[2,3]),{1:[2,2]},e(a,[2,4]),e(a,[2,5]),{1:[2,6],6:r,12:15,13:n,14:i},{6:r,9:16,12:11,13:n,14:i},{6:s,7:l,10:17,11:u},e(h,[2,18],{14:[1,21]}),e(h,[2,16]),e(h,[2,17]),{6:s,7:l,10:22,11:u},{1:[2,7],6:r,12:15,13:n,14:i},e(d,[2,14],{7:f,11:p}),e(m,[2,8]),e(m,[2,9]),e(m,[2,10]),e(h,[2,15]),e(d,[2,13],{7:f,11:p}),e(m,[2,11]),e(m,[2,12])],defaultActions:{2:[2,1],6:[2,2]},parseError:o(function(b,T){if(T.recoverable)this.trace(b);else{var k=new Error(b);throw k.hash=T,k}},"parseError"),parse:o(function(b){var T=this,k=[0],C=[],w=[null],S=[],R=this.table,L="",N=0,I=0,_=0,A=2,M=1,D=S.slice.call(arguments,1),P=Object.create(this.lexer),B={yy:{}};for(var O in this.yy)Object.prototype.hasOwnProperty.call(this.yy,O)&&(B.yy[O]=this.yy[O]);P.setInput(b,B.yy),B.yy.lexer=P,B.yy.parser=this,typeof P.yylloc>"u"&&(P.yylloc={});var $=P.yylloc;S.push($);var V=P.options&&P.options.ranges;typeof B.yy.parseError=="function"?this.parseError=B.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function G(Pe){k.length=k.length-2*Pe,w.length=w.length-Pe,S.length=S.length-Pe}o(G,"popStack");function z(){var Pe;return Pe=C.pop()||P.lex()||M,typeof Pe!="number"&&(Pe instanceof Array&&(C=Pe,Pe=C.pop()),Pe=T.symbols_[Pe]||Pe),Pe}o(z,"lex");for(var W,H,j,Q,U,oe,te={},le,ie,ae,Re;;){if(j=k[k.length-1],this.defaultActions[j]?Q=this.defaultActions[j]:((W===null||typeof W>"u")&&(W=z()),Q=R[j]&&R[j][W]),typeof Q>"u"||!Q.length||!Q[0]){var be="";Re=[];for(le in R[j])this.terminals_[le]&&le>A&&Re.push("'"+this.terminals_[le]+"'");P.showPosition?be="Parse error on line "+(N+1)+`: +`+P.showPosition()+` +Expecting `+Re.join(", ")+", got '"+(this.terminals_[W]||W)+"'":be="Parse error on line "+(N+1)+": Unexpected "+(W==M?"end of input":"'"+(this.terminals_[W]||W)+"'"),this.parseError(be,{text:P.match,token:this.terminals_[W]||W,line:P.yylineno,loc:$,expected:Re})}if(Q[0]instanceof Array&&Q.length>1)throw new Error("Parse Error: multiple actions possible at state: "+j+", token: "+W);switch(Q[0]){case 1:k.push(W),w.push(P.yytext),S.push(P.yylloc),k.push(Q[1]),W=null,H?(W=H,H=null):(I=P.yyleng,L=P.yytext,N=P.yylineno,$=P.yylloc,_>0&&_--);break;case 2:if(ie=this.productions_[Q[1]][1],te.$=w[w.length-ie],te._$={first_line:S[S.length-(ie||1)].first_line,last_line:S[S.length-1].last_line,first_column:S[S.length-(ie||1)].first_column,last_column:S[S.length-1].last_column},V&&(te._$.range=[S[S.length-(ie||1)].range[0],S[S.length-1].range[1]]),oe=this.performAction.apply(te,[L,I,N,B.yy,Q[1],w,S].concat(D)),typeof oe<"u")return oe;ie&&(k=k.slice(0,-1*ie*2),w=w.slice(0,-1*ie),S=S.slice(0,-1*ie)),k.push(this.productions_[Q[1]][0]),w.push(te.$),S.push(te._$),ae=R[k[k.length-2]][k[k.length-1]],k.push(ae);break;case 3:return!0}}return!0},"parse")},y=(function(){var x={EOF:1,parseError:o(function(T,k){if(this.yy.parser)this.yy.parser.parseError(T,k);else throw new Error(T)},"parseError"),setInput:o(function(b,T){return this.yy=T||this.yy||{},this._input=b,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var b=this._input[0];this.yytext+=b,this.yyleng++,this.offset++,this.match+=b,this.matched+=b;var T=b.match(/(?:\r\n?|\n).*/g);return T?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),b},"input"),unput:o(function(b){var T=b.length,k=b.split(/(?:\r\n?|\n)/g);this._input=b+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-T),this.offset-=T;var C=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),k.length-1&&(this.yylineno-=k.length-1);var w=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:k?(k.length===C.length?this.yylloc.first_column:0)+C[C.length-k.length].length-k[0].length:this.yylloc.first_column-T},this.options.ranges&&(this.yylloc.range=[w[0],w[0]+this.yyleng-T]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(b){this.unput(this.match.slice(b))},"less"),pastInput:o(function(){var b=this.matched.substr(0,this.matched.length-this.match.length);return(b.length>20?"...":"")+b.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var b=this.match;return b.length<20&&(b+=this._input.substr(0,20-b.length)),(b.substr(0,20)+(b.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var b=this.pastInput(),T=new Array(b.length+1).join("-");return b+this.upcomingInput()+` +`+T+"^"},"showPosition"),test_match:o(function(b,T){var k,C,w;if(this.options.backtrack_lexer&&(w={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(w.yylloc.range=this.yylloc.range.slice(0))),C=b[0].match(/(?:\r\n?|\n).*/g),C&&(this.yylineno+=C.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:C?C[C.length-1].length-C[C.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+b[0].length},this.yytext+=b[0],this.match+=b[0],this.matches=b,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(b[0].length),this.matched+=b[0],k=this.performAction.call(this,this.yy,this,T,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),k)return k;if(this._backtrack){for(var S in w)this[S]=w[S];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var b,T,k,C;this._more||(this.yytext="",this.match="");for(var w=this._currentRules(),S=0;ST[0].length)){if(T=k,C=S,this.options.backtrack_lexer){if(b=this.test_match(k,w[S]),b!==!1)return b;if(this._backtrack){T=!1;continue}else return!1}else if(!this.options.flex)break}return T?(b=this.test_match(T,w[C]),b!==!1?b:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var T=this.next();return T||this.lex()},"lex"),begin:o(function(T){this.conditionStack.push(T)},"begin"),popState:o(function(){var T=this.conditionStack.length-1;return T>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(T){return T=this.conditionStack.length-1-Math.abs(T||0),T>=0?this.conditionStack[T]:"INITIAL"},"topState"),pushState:o(function(T){this.begin(T)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:o(function(T,k,C,w){var S=w;switch(C){case 0:return 6;case 1:return 8;case 2:return 8;case 3:return 6;case 4:return 7;case 5:return 13;case 6:return 14;case 7:return 11}},"anonymous"),rules:[/^(?:\s*%%.*)/i,/^(?:ishikawa-beta\b)/i,/^(?:ishikawa\b)/i,/^(?:[\s]+[\n])/i,/^(?:[\n]+)/i,/^(?:[\s]+)/i,/^(?:[^\n]+)/i,/^(?:$)/i],conditions:{INITIAL:{rules:[0,1,2,3,4,5,6,7],inclusive:!0}}};return x})();g.lexer=y;function v(){this.yy={}}return o(v,"Parser"),v.prototype=g,g.Parser=v,new v})();aX.parser=aX;nBe=aX});var lD,aBe=F(()=>{"use strict";Xt();Vr();Nn();lD=class{constructor(){this.stack=[];this.clear=this.clear.bind(this),this.addNode=this.addNode.bind(this),this.getRoot=this.getRoot.bind(this)}static{o(this,"IshikawaDB")}clear(){this.root=void 0,this.stack=[],this.baseLevel=void 0,yr()}getRoot(){return this.root}addNode(t,r){let n=xt.sanitizeText(r,Ae());if(!this.root){this.root={text:n,children:[]},this.stack=[{level:0,node:this.root}],Or(n);return}this.baseLevel??=t;let i=t-this.baseLevel+1;for(i<=0&&(i=1);this.stack.length>1&&this.stack[this.stack.length-1].level>=i;)this.stack.pop();let a=this.stack[this.stack.length-1].node,s={text:n,children:[]};a.children.push(s),this.stack.push({level:i,node:s})}getAccTitle(){return Ar()}setAccTitle(t){kr(t)}getAccDescription(){return _r()}setAccDescription(t){Rr(t)}getDiagramTitle(){return Lr()}setDiagramTitle(t){Or(t)}}});var bIt,yx,TIt,CIt,wIt,hBe,sBe,oBe,lBe,kIt,cBe,SIt,EIt,AIt,sX,RIt,_It,dBe,cD,uBe,vx,fBe,pBe=F(()=>{"use strict";Xt();Ka();$n();Qt();tr();bIt=14,yx=250,TIt=30,CIt=60,wIt=5,hBe=82*Math.PI/180,sBe=Math.cos(hBe),oBe=Math.sin(hBe),lBe=o((e,t,r)=>{let n=e.node().getBBox(),i=n.width+t*2,a=n.height+t*2;Wr(e,a,i,r),e.attr("viewBox",`${n.x-t} ${n.y-t} ${i} ${a}`)},"applyPaddedViewBox"),kIt=o((e,t,r,n)=>{let a=n.db.getRoot();if(!a)return;let s=Ae(),{look:l,handDrawnSeed:u,themeVariables:h}=s,d=As(s.fontSize)[0]??bIt,f=l==="handDrawn",p=a.children??[],m=s.ishikawa?.diagramPadding??20,g=s.ishikawa?.useMaxWidth??!1,y=xn(t),v=y.append("g").attr("class","ishikawa"),x=f?ut.svg(y.node()):void 0,b=x?{roughSvg:x,seed:u??0,lineColor:h?.lineColor??"#333",fillColor:h?.mainBkg??"#fff"}:void 0,T=`ishikawa-arrow-${t}`;f||v.append("defs").append("marker").attr("id",T).attr("viewBox","0 0 10 10").attr("refX",0).attr("refY",5).attr("markerWidth",6).attr("markerHeight",6).attr("orient","auto").append("path").attr("d","M 10 0 L 0 5 L 10 10 Z").attr("class","ishikawa-arrow");let k=0,C=yx,w=f?void 0:vx(v,k,C,k,C,"ishikawa-spine");if(SIt(v,k,C,a.text,d,b),!p.length){f&&vx(v,k,C,k,C,"ishikawa-spine",b),lBe(y,m,g);return}k-=20;let S=p.filter((P,B)=>B%2===0),R=p.filter((P,B)=>B%2===1),L=cBe(S),N=cBe(R),I=L.total+N.total,_=yx,A=yx;if(I>0){let P=yx*2,B=yx*.3;_=Math.max(B,P*(L.total/I)),A=Math.max(B,P*(N.total/I))}let M=d*2;_=Math.max(_,L.max*M),A=Math.max(A,N.max*M),C=Math.max(_,yx),w&&w.attr("y1",C).attr("y2",C),v.select(".ishikawa-head-group").attr("transform",`translate(0,${C})`);let D=Math.ceil(p.length/2);for(let P=0;PMath.min(O,$.getBBox().x),1/0)}if(f)vx(v,k,C,0,C,"ishikawa-spine",b);else{w.attr("x1",k);let P=`url(#${T})`;v.selectAll("line.ishikawa-branch, line.ishikawa-sub-branch").attr("marker-start",P)}lBe(y,m,g)},"draw"),cBe=o(e=>{let t=o(r=>r.children.reduce((n,i)=>n+1+t(i),0),"countDescendants");return e.reduce((r,n)=>{let i=t(n);return r.total+=i,r.max=Math.max(r.max,i),r},{total:0,max:0})},"sideStats"),SIt=o((e,t,r,n,i,a)=>{let s=Math.max(6,Math.floor(110/(i*.6))),l=e.append("g").attr("class","ishikawa-head-group").attr("transform",`translate(${t},${r})`),u=cD(l,dBe(n,s),0,0,"ishikawa-head-label","start",i),h=u.node().getBBox(),d=Math.max(60,h.width+6),f=Math.max(40,h.height*2+40),p=`M 0 ${-f/2} L 0 ${f/2} Q ${d*2.4} 0 0 ${-f/2} Z`;if(a){let m=a.roughSvg.path(p,{roughness:1.5,seed:a.seed,fill:a.fillColor,fillStyle:"hachure",fillWeight:2.5,hachureGap:5,stroke:a.lineColor,strokeWidth:2});l.insert(()=>m,":first-child").attr("class","ishikawa-head")}else l.insert("path",":first-child").attr("class","ishikawa-head").attr("d",p);u.attr("transform",`translate(${(d-h.width)/2-h.x+3},${-h.y-h.height/2})`)},"drawHead"),EIt=o((e,t)=>{let r=[],n=[],i=o((a,s,l)=>{let u=t===-1?[...a].reverse():a;for(let h of u){let d=r.length,f=h.children??[];r.push({depth:l,text:dBe(h.text,15),parentIndex:s,childCount:f.length}),l%2===0?(n.push(d),f.length&&i(f,d,l+1)):(f.length&&i(f,d,l+1),n.push(d))}},"walk");return i(e,-1,2),{entries:r,yOrder:n}},"flattenTree"),AIt=o((e,t,r,n,i,a,s)=>{let l=e.append("g").attr("class","ishikawa-label-group"),h=cD(l,t,r,n+11*i,"ishikawa-label cause","middle",a).node().getBBox();if(s){let d=s.roughSvg.rectangle(h.x-20,h.y-2,h.width+40,h.height+4,{roughness:1.5,seed:s.seed,fill:s.fillColor,fillStyle:"hachure",fillWeight:2.5,hachureGap:5,stroke:s.lineColor,strokeWidth:2});l.insert(()=>d,":first-child").attr("class","ishikawa-label-box")}else l.insert("rect",":first-child").attr("class","ishikawa-label-box").attr("x",h.x-20).attr("y",h.y-2).attr("width",h.width+40).attr("height",h.height+4)},"drawCauseLabel"),sX=o((e,t,r,n,i,a)=>{let s=Math.sqrt(n*n+i*i);if(s===0)return;let l=n/s,u=i/s,h=6,d=-u*h,f=l*h,p=t,m=r,g=`M ${p} ${m} L ${p-l*h*2+d} ${m-u*h*2+f} L ${p-l*h*2-d} ${m-u*h*2-f} Z`,y=a.roughSvg.path(g,{roughness:1,seed:a.seed,fill:a.lineColor,fillStyle:"solid",stroke:a.lineColor,strokeWidth:1});e.append(()=>y)},"drawArrowMarker"),RIt=o((e,t,r,n,i,a,s,l)=>{let u=t.children??[],h=a*(u.length?1:.2),d=-sBe*h,f=oBe*h*i,p=r+d,m=n+f;if(vx(e,r,n,p,m,"ishikawa-branch",l),l&&sX(e,r,n,r-p,n-m,l),AIt(e,t.text,p,m,i,s,l),!u.length)return;let{entries:g,yOrder:y}=EIt(u,i),v=g.length,x=new Array(v);for(let[w,S]of y.entries())x[S]=n+f*((w+1)/(v+1));let b=new Map;b.set(-1,{x0:r,y0:n,x1:p,y1:m,childCount:u.length,childrenDrawn:0});let T=-sBe,k=oBe*i,C=i<0?"ishikawa-label up":"ishikawa-label down";for(let[w,S]of g.entries()){let R=x[w],L=b.get(S.parentIndex),N=e.append("g").attr("class","ishikawa-sub-group"),I=0,_=0,A=0;if(S.depth%2===0){let M=L.y1-L.y0;I=uBe(L.x0,L.x1,M?(R-L.y0)/M:.5),_=R,A=I-(S.childCount>0?CIt+S.childCount*wIt:TIt),vx(N,I,R,A,R,"ishikawa-sub-branch",l),l&&sX(N,I,R,1,0,l),cD(N,S.text,A,R,"ishikawa-label align","end",s)}else{let M=L.childrenDrawn++;I=uBe(L.x0,L.x1,(L.childCount-M)/(L.childCount+1)),_=L.y0,A=I+T*((R-_)/k),vx(N,I,_,A,R,"ishikawa-sub-branch",l),l&&sX(N,I,_,I-A,_-R,l),cD(N,S.text,A,R,C,"end",s)}S.childCount>0&&b.set(w,{x0:I,y0:_,x1:A,y1:R,childCount:S.childCount,childrenDrawn:0})}},"drawBranch"),_It=o(e=>e.split(/|\n/),"splitLines"),dBe=o((e,t)=>{if(e.length<=t)return e;let r=[];for(let n of e.split(/\s+/)){let i=r.length-1;i>=0&&r[i].length+1+n.length<=t?r[i]+=" "+n:r.push(n)}return r.join(` +`)},"wrapText"),cD=o((e,t,r,n,i,a,s)=>{let l=_It(t),u=s*1.05,h=e.append("text").attr("class",i).attr("text-anchor",a).attr("x",r).attr("y",n-(l.length-1)*u/2);for(let[d,f]of l.entries())h.append("tspan").attr("x",r).attr("dy",d===0?0:u).text(f);return h},"drawMultilineText"),uBe=o((e,t,r)=>e+(t-e)*r,"lerp"),vx=o((e,t,r,n,i,a,s)=>{if(s){let l=s.roughSvg.line(t,r,n,i,{roughness:1.5,seed:s.seed,stroke:s.lineColor,strokeWidth:2});e.append(()=>l).attr("class",a);return}return e.append("line").attr("class",a).attr("x1",t).attr("y1",r).attr("x2",n).attr("y2",i)},"drawLine"),fBe={draw:kIt}});var LIt,mBe,gBe=F(()=>{"use strict";LIt=o(e=>` +.ishikawa .ishikawa-spine, +.ishikawa .ishikawa-branch, +.ishikawa .ishikawa-sub-branch { + stroke: ${e.lineColor}; + stroke-width: 2; + fill: none; +} + +.ishikawa .ishikawa-sub-branch { + stroke-width: 1; +} + +.ishikawa .ishikawa-arrow { + fill: ${e.lineColor}; +} + +.ishikawa .ishikawa-head { + fill: ${e.mainBkg}; + stroke: ${e.lineColor}; + stroke-width: 2; +} + +.ishikawa .ishikawa-label-box { + fill: ${e.mainBkg}; + stroke: ${e.lineColor}; + stroke-width: 2; +} + +.ishikawa text { + font-family: ${e.fontFamily}; + font-size: ${e.fontSize}; + fill: ${e.textColor}; +} + +.ishikawa .ishikawa-head-label { + font-weight: 600; + text-anchor: middle; + dominant-baseline: middle; + font-size: 14px; +} + +.ishikawa .ishikawa-label { + text-anchor: end; +} + +.ishikawa .ishikawa-label.cause { + text-anchor: middle; + dominant-baseline: middle; +} + +.ishikawa .ishikawa-label.align { + text-anchor: end; + dominant-baseline: middle; +} + +.ishikawa .ishikawa-label.up { + dominant-baseline: baseline; +} + +.ishikawa .ishikawa-label.down { + dominant-baseline: hanging; +} +`,"getStyles"),mBe=LIt});var yBe={};ir(yBe,{diagram:()=>DIt});var DIt,vBe=F(()=>{"use strict";iBe();aBe();pBe();gBe();DIt={parser:nBe,get db(){return new lD},renderer:fBe,styles:mBe}});var oX,TBe,CBe=F(()=>{"use strict";oX=(function(){var e=o(function(b,T,k,C){for(k=k||{},C=b.length;C--;k[b[C]]=T);return k},"o"),t=[5,8],r=[7,8,11,12,17,19,22,24],n=[1,17],i=[1,18],a=[7,8,11,12,14,15,16,17,19,20,21,22,24,27],s=[1,31],l=[1,39],u=[7,8,11,12,17,19,22,24,27],h=[1,57],d=[1,56],f=[1,58],p=[1,59],m=[1,60],g=[7,8,11,12,16,17,19,20,22,24,27,31,32,33],y={trace:o(function(){},"trace"),yy:{},symbols_:{error:2,start:3,optNewlines:4,VENN:5,document:6,EOF:7,NEWLINE:8,line:9,statement:10,TITLE:11,SET:12,identifier:13,BRACKET_LABEL:14,COLON:15,NUMERIC:16,UNION:17,identifierList:18,TEXT:19,IDENTIFIER:20,STRING:21,INDENT_TEXT:22,indentedTextTail:23,STYLE:24,stylesOpt:25,styleField:26,COMMA:27,styleValue:28,valueTokens:29,valueToken:30,HEXCOLOR:31,RGBCOLOR:32,RGBACOLOR:33,$accept:0,$end:1},terminals_:{2:"error",5:"VENN",7:"EOF",8:"NEWLINE",11:"TITLE",12:"SET",14:"BRACKET_LABEL",15:"COLON",16:"NUMERIC",17:"UNION",19:"TEXT",20:"IDENTIFIER",21:"STRING",22:"INDENT_TEXT",24:"STYLE",27:"COMMA",31:"HEXCOLOR",32:"RGBCOLOR",33:"RGBACOLOR"},productions_:[0,[3,4],[4,0],[4,2],[6,0],[6,2],[9,1],[9,1],[10,1],[10,2],[10,3],[10,4],[10,5],[10,2],[10,3],[10,4],[10,5],[10,3],[10,3],[10,3],[10,4],[10,4],[10,2],[10,3],[23,1],[23,1],[23,1],[23,2],[23,2],[25,1],[25,3],[26,3],[28,1],[28,1],[29,1],[29,2],[30,1],[30,1],[30,1],[30,1],[30,1],[18,1],[18,3],[13,1],[13,1]],performAction:o(function(T,k,C,w,S,R,L){var N=R.length-1;switch(S){case 1:return R[N-1];case 2:case 3:case 4:this.$=[];break;case 5:R[N-1].push(R[N]),this.$=R[N-1];break;case 6:this.$=[];break;case 7:case 22:case 32:case 36:case 37:case 38:case 39:case 40:this.$=R[N];break;case 8:w.setDiagramTitle(R[N].substr(6)),this.$=R[N].substr(6);break;case 9:w.addSubsetData([R[N]],void 0,void 0),w.setIndentMode&&w.setIndentMode(!0);break;case 10:w.addSubsetData([R[N-1]],R[N],void 0),w.setIndentMode&&w.setIndentMode(!0);break;case 11:w.addSubsetData([R[N-2]],void 0,parseFloat(R[N])),w.setIndentMode&&w.setIndentMode(!0);break;case 12:w.addSubsetData([R[N-3]],R[N-2],parseFloat(R[N])),w.setIndentMode&&w.setIndentMode(!0);break;case 13:if(R[N].length<2)throw new Error("union requires multiple identifiers");w.validateUnionIdentifiers&&w.validateUnionIdentifiers(R[N]),w.addSubsetData(R[N],void 0,void 0),w.setIndentMode&&w.setIndentMode(!0);break;case 14:if(R[N-1].length<2)throw new Error("union requires multiple identifiers");w.validateUnionIdentifiers&&w.validateUnionIdentifiers(R[N-1]),w.addSubsetData(R[N-1],R[N],void 0),w.setIndentMode&&w.setIndentMode(!0);break;case 15:if(R[N-2].length<2)throw new Error("union requires multiple identifiers");w.validateUnionIdentifiers&&w.validateUnionIdentifiers(R[N-2]),w.addSubsetData(R[N-2],void 0,parseFloat(R[N])),w.setIndentMode&&w.setIndentMode(!0);break;case 16:if(R[N-3].length<2)throw new Error("union requires multiple identifiers");w.validateUnionIdentifiers&&w.validateUnionIdentifiers(R[N-3]),w.addSubsetData(R[N-3],R[N-2],parseFloat(R[N])),w.setIndentMode&&w.setIndentMode(!0);break;case 17:case 18:case 19:w.addTextData(R[N-1],R[N],void 0);break;case 20:case 21:w.addTextData(R[N-2],R[N-1],R[N]);break;case 23:w.addStyleData(R[N-1],R[N]);break;case 24:case 25:case 26:var I=w.getCurrentSets();if(!I)throw new Error("text requires set");w.addTextData(I,R[N],void 0);break;case 27:case 28:var I=w.getCurrentSets();if(!I)throw new Error("text requires set");w.addTextData(I,R[N-1],R[N]);break;case 29:case 41:this.$=[R[N]];break;case 30:case 42:this.$=[...R[N-2],R[N]];break;case 31:this.$=[R[N-2],R[N]];break;case 33:this.$=R[N].join(" ");break;case 34:this.$=[R[N]];break;case 35:R[N-1].push(R[N]),this.$=R[N-1];break;case 43:case 44:this.$=R[N];break}},"anonymous"),table:[e(t,[2,2],{3:1,4:2}),{1:[3]},{5:[1,3],8:[1,4]},e(r,[2,4],{6:5}),e(t,[2,3]),{7:[1,6],8:[1,8],9:7,10:9,11:[1,10],12:[1,11],17:[1,12],19:[1,13],22:[1,14],24:[1,15]},{1:[2,1]},e(r,[2,5]),e(r,[2,6]),e(r,[2,7]),e(r,[2,8]),{13:16,20:n,21:i},{13:20,18:19,20:n,21:i},{13:20,18:21,20:n,21:i},{16:[1,25],20:[1,23],21:[1,24],23:22},{13:20,18:26,20:n,21:i},e(r,[2,9],{14:[1,27],15:[1,28]}),e(a,[2,43]),e(a,[2,44]),e(r,[2,13],{14:[1,29],15:[1,30],27:s}),e(a,[2,41]),{16:[1,34],20:[1,32],21:[1,33],27:s},e(r,[2,22]),e(r,[2,24],{14:[1,35]}),e(r,[2,25],{14:[1,36]}),e(r,[2,26]),{20:l,25:37,26:38,27:s},e(r,[2,10],{15:[1,40]}),{16:[1,41]},e(r,[2,14],{15:[1,42]}),{16:[1,43]},{13:44,20:n,21:i},e(r,[2,17],{14:[1,45]}),e(r,[2,18],{14:[1,46]}),e(r,[2,19]),e(r,[2,27]),e(r,[2,28]),e(r,[2,23],{27:[1,47]}),e(u,[2,29]),{15:[1,48]},{16:[1,49]},e(r,[2,11]),{16:[1,50]},e(r,[2,15]),e(a,[2,42]),e(r,[2,20]),e(r,[2,21]),{20:l,26:51},{16:h,20:d,21:[1,53],28:52,29:54,30:55,31:f,32:p,33:m},e(r,[2,12]),e(r,[2,16]),e(u,[2,30]),e(u,[2,31]),e(u,[2,32]),e(u,[2,33],{30:61,16:h,20:d,31:f,32:p,33:m}),e(g,[2,34]),e(g,[2,36]),e(g,[2,37]),e(g,[2,38]),e(g,[2,39]),e(g,[2,40]),e(g,[2,35])],defaultActions:{6:[2,1]},parseError:o(function(T,k){if(k.recoverable)this.trace(T);else{var C=new Error(T);throw C.hash=k,C}},"parseError"),parse:o(function(T){var k=this,C=[0],w=[],S=[null],R=[],L=this.table,N="",I=0,_=0,A=0,M=2,D=1,P=R.slice.call(arguments,1),B=Object.create(this.lexer),O={yy:{}};for(var $ in this.yy)Object.prototype.hasOwnProperty.call(this.yy,$)&&(O.yy[$]=this.yy[$]);B.setInput(T,O.yy),O.yy.lexer=B,O.yy.parser=this,typeof B.yylloc>"u"&&(B.yylloc={});var V=B.yylloc;R.push(V);var G=B.options&&B.options.ranges;typeof O.yy.parseError=="function"?this.parseError=O.yy.parseError:this.parseError=Object.getPrototypeOf(this).parseError;function z(Ge){C.length=C.length-2*Ge,S.length=S.length-Ge,R.length=R.length-Ge}o(z,"popStack");function W(){var Ge;return Ge=w.pop()||B.lex()||D,typeof Ge!="number"&&(Ge instanceof Array&&(w=Ge,Ge=w.pop()),Ge=k.symbols_[Ge]||Ge),Ge}o(W,"lex");for(var H,j,Q,U,oe,te,le={},ie,ae,Re,be;;){if(Q=C[C.length-1],this.defaultActions[Q]?U=this.defaultActions[Q]:((H===null||typeof H>"u")&&(H=W()),U=L[Q]&&L[Q][H]),typeof U>"u"||!U.length||!U[0]){var Pe="";be=[];for(ie in L[Q])this.terminals_[ie]&&ie>M&&be.push("'"+this.terminals_[ie]+"'");B.showPosition?Pe="Parse error on line "+(I+1)+`: +`+B.showPosition()+` +Expecting `+be.join(", ")+", got '"+(this.terminals_[H]||H)+"'":Pe="Parse error on line "+(I+1)+": Unexpected "+(H==D?"end of input":"'"+(this.terminals_[H]||H)+"'"),this.parseError(Pe,{text:B.match,token:this.terminals_[H]||H,line:B.yylineno,loc:V,expected:be})}if(U[0]instanceof Array&&U.length>1)throw new Error("Parse Error: multiple actions possible at state: "+Q+", token: "+H);switch(U[0]){case 1:C.push(H),S.push(B.yytext),R.push(B.yylloc),C.push(U[1]),H=null,j?(H=j,j=null):(_=B.yyleng,N=B.yytext,I=B.yylineno,V=B.yylloc,A>0&&A--);break;case 2:if(ae=this.productions_[U[1]][1],le.$=S[S.length-ae],le._$={first_line:R[R.length-(ae||1)].first_line,last_line:R[R.length-1].last_line,first_column:R[R.length-(ae||1)].first_column,last_column:R[R.length-1].last_column},G&&(le._$.range=[R[R.length-(ae||1)].range[0],R[R.length-1].range[1]]),te=this.performAction.apply(le,[N,_,I,O.yy,U[1],S,R].concat(P)),typeof te<"u")return te;ae&&(C=C.slice(0,-1*ae*2),S=S.slice(0,-1*ae),R=R.slice(0,-1*ae)),C.push(this.productions_[U[1]][0]),S.push(le.$),R.push(le._$),Re=L[C[C.length-2]][C[C.length-1]],C.push(Re);break;case 3:return!0}}return!0},"parse")},v=(function(){var b={EOF:1,parseError:o(function(k,C){if(this.yy.parser)this.yy.parser.parseError(k,C);else throw new Error(k)},"parseError"),setInput:o(function(T,k){return this.yy=k||this.yy||{},this._input=T,this._more=this._backtrack=this.done=!1,this.yylineno=this.yyleng=0,this.yytext=this.matched=this.match="",this.conditionStack=["INITIAL"],this.yylloc={first_line:1,first_column:0,last_line:1,last_column:0},this.options.ranges&&(this.yylloc.range=[0,0]),this.offset=0,this},"setInput"),input:o(function(){var T=this._input[0];this.yytext+=T,this.yyleng++,this.offset++,this.match+=T,this.matched+=T;var k=T.match(/(?:\r\n?|\n).*/g);return k?(this.yylineno++,this.yylloc.last_line++):this.yylloc.last_column++,this.options.ranges&&this.yylloc.range[1]++,this._input=this._input.slice(1),T},"input"),unput:o(function(T){var k=T.length,C=T.split(/(?:\r\n?|\n)/g);this._input=T+this._input,this.yytext=this.yytext.substr(0,this.yytext.length-k),this.offset-=k;var w=this.match.split(/(?:\r\n?|\n)/g);this.match=this.match.substr(0,this.match.length-1),this.matched=this.matched.substr(0,this.matched.length-1),C.length-1&&(this.yylineno-=C.length-1);var S=this.yylloc.range;return this.yylloc={first_line:this.yylloc.first_line,last_line:this.yylineno+1,first_column:this.yylloc.first_column,last_column:C?(C.length===w.length?this.yylloc.first_column:0)+w[w.length-C.length].length-C[0].length:this.yylloc.first_column-k},this.options.ranges&&(this.yylloc.range=[S[0],S[0]+this.yyleng-k]),this.yyleng=this.yytext.length,this},"unput"),more:o(function(){return this._more=!0,this},"more"),reject:o(function(){if(this.options.backtrack_lexer)this._backtrack=!0;else return this.parseError("Lexical error on line "+(this.yylineno+1)+`. You can only invoke reject() in the lexer when the lexer is of the backtracking persuasion (options.backtrack_lexer = true). +`+this.showPosition(),{text:"",token:null,line:this.yylineno});return this},"reject"),less:o(function(T){this.unput(this.match.slice(T))},"less"),pastInput:o(function(){var T=this.matched.substr(0,this.matched.length-this.match.length);return(T.length>20?"...":"")+T.substr(-20).replace(/\n/g,"")},"pastInput"),upcomingInput:o(function(){var T=this.match;return T.length<20&&(T+=this._input.substr(0,20-T.length)),(T.substr(0,20)+(T.length>20?"...":"")).replace(/\n/g,"")},"upcomingInput"),showPosition:o(function(){var T=this.pastInput(),k=new Array(T.length+1).join("-");return T+this.upcomingInput()+` +`+k+"^"},"showPosition"),test_match:o(function(T,k){var C,w,S;if(this.options.backtrack_lexer&&(S={yylineno:this.yylineno,yylloc:{first_line:this.yylloc.first_line,last_line:this.last_line,first_column:this.yylloc.first_column,last_column:this.yylloc.last_column},yytext:this.yytext,match:this.match,matches:this.matches,matched:this.matched,yyleng:this.yyleng,offset:this.offset,_more:this._more,_input:this._input,yy:this.yy,conditionStack:this.conditionStack.slice(0),done:this.done},this.options.ranges&&(S.yylloc.range=this.yylloc.range.slice(0))),w=T[0].match(/(?:\r\n?|\n).*/g),w&&(this.yylineno+=w.length),this.yylloc={first_line:this.yylloc.last_line,last_line:this.yylineno+1,first_column:this.yylloc.last_column,last_column:w?w[w.length-1].length-w[w.length-1].match(/\r?\n?/)[0].length:this.yylloc.last_column+T[0].length},this.yytext+=T[0],this.match+=T[0],this.matches=T,this.yyleng=this.yytext.length,this.options.ranges&&(this.yylloc.range=[this.offset,this.offset+=this.yyleng]),this._more=!1,this._backtrack=!1,this._input=this._input.slice(T[0].length),this.matched+=T[0],C=this.performAction.call(this,this.yy,this,k,this.conditionStack[this.conditionStack.length-1]),this.done&&this._input&&(this.done=!1),C)return C;if(this._backtrack){for(var R in S)this[R]=S[R];return!1}return!1},"test_match"),next:o(function(){if(this.done)return this.EOF;this._input||(this.done=!0);var T,k,C,w;this._more||(this.yytext="",this.match="");for(var S=this._currentRules(),R=0;Rk[0].length)){if(k=C,w=R,this.options.backtrack_lexer){if(T=this.test_match(C,S[R]),T!==!1)return T;if(this._backtrack){k=!1;continue}else return!1}else if(!this.options.flex)break}return k?(T=this.test_match(k,S[w]),T!==!1?T:!1):this._input===""?this.EOF:this.parseError("Lexical error on line "+(this.yylineno+1)+`. Unrecognized text. +`+this.showPosition(),{text:"",token:null,line:this.yylineno})},"next"),lex:o(function(){var k=this.next();return k||this.lex()},"lex"),begin:o(function(k){this.conditionStack.push(k)},"begin"),popState:o(function(){var k=this.conditionStack.length-1;return k>0?this.conditionStack.pop():this.conditionStack[0]},"popState"),_currentRules:o(function(){return this.conditionStack.length&&this.conditionStack[this.conditionStack.length-1]?this.conditions[this.conditionStack[this.conditionStack.length-1]].rules:this.conditions.INITIAL.rules},"_currentRules"),topState:o(function(k){return k=this.conditionStack.length-1-Math.abs(k||0),k>=0?this.conditionStack[k]:"INITIAL"},"topState"),pushState:o(function(k){this.begin(k)},"pushState"),stateStackSize:o(function(){return this.conditionStack.length},"stateStackSize"),options:{"case-insensitive":!0},performAction:o(function(k,C,w,S){var R=S;switch(w){case 0:break;case 1:break;case 2:break;case 3:if(k.getIndentMode&&k.getIndentMode())return k.consumeIndentText=!0,this.begin("INITIAL"),22;break;case 4:break;case 5:k.setIndentMode&&k.setIndentMode(!1),this.begin("INITIAL"),this.unput(C.yytext);break;case 6:return this.begin("bol"),8;break;case 7:break;case 8:break;case 9:return 7;case 10:return 11;case 11:return 5;case 12:return 12;case 13:return 17;case 14:if(k.consumeIndentText)k.consumeIndentText=!1;else return 19;break;case 15:return 24;case 16:return C.yytext=C.yytext.slice(2,-2),14;break;case 17:return C.yytext=C.yytext.slice(1,-1).trim(),14;break;case 18:return 16;case 19:return 31;case 20:return 33;case 21:return 32;case 22:return 20;case 23:return 21;case 24:return 27;case 25:return 15}},"anonymous"),rules:[/^(?:%%(?!\{)[^\n]*)/i,/^(?:[^\}]%%[^\n]*)/i,/^(?:[ \t]+(?=[\n\r]))/i,/^(?:[ \t]+(?=text\b))/i,/^(?:[ \t]+)/i,/^(?:[^ \t\n\r])/i,/^(?:[\n\r]+)/i,/^(?:%%[^\n]*)/i,/^(?:[ \t]+)/i,/^(?:$)/i,/^(?:title\s[^#\n;]+)/i,/^(?:venn-beta\b)/i,/^(?:set\b)/i,/^(?:union\b)/i,/^(?:text\b)/i,/^(?:style\b)/i,/^(?:\["[^\"]*"\])/i,/^(?:\[[^\]\"]+\])/i,/^(?:[+-]?(\d+(\.\d+)?|\.\d+))/i,/^(?:#[0-9a-fA-F]{3,8})/i,/^(?:rgba\(\s*[0-9.]+\s*[,]\s*[0-9.]+\s*[,]\s*[0-9.]+\s*[,]\s*[0-9.]+\s*\))/i,/^(?:rgb\(\s*[0-9.]+\s*[,]\s*[0-9.]+\s*[,]\s*[0-9.]+\s*\))/i,/^(?:[A-Za-z_][A-Za-z0-9\-_]*)/i,/^(?:"[^\"]*")/i,/^(?:,)/i,/^(?::)/i],conditions:{bol:{rules:[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25],inclusive:!0},INITIAL:{rules:[0,1,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25],inclusive:!0}}};return b})();y.lexer=v;function x(){this.yy={}}return o(x,"Parser"),x.prototype=y,y.Parser=x,new x})();oX.parser=oX;TBe=oX});function UIt(){return qr(HIt,_t().venn)}var lX,cX,uX,hX,dX,fX,NIt,PIt,sk,OIt,BIt,$It,FIt,uD,zIt,GIt,VIt,WIt,qIt,HIt,YIt,wBe,kBe=F(()=>{"use strict";Qt();ur();Nn();Wi();lX=[],cX=[],uX=[],hX=new Set,fX=!1,NIt=o((e,t,r)=>{let n=uD(e).sort(),i=r??10/Math.pow(e.length,2);dX=n,n.length===1&&hX.add(n[0]),lX.push({sets:n,size:i,label:t?sk(t):void 0})},"addSubsetData"),PIt=o(()=>lX,"getSubsetData"),sk=o(e=>{let t=e.trim();return t.length>=2&&t.startsWith('"')&&t.endsWith('"')?t.slice(1,-1):t},"normalizeText"),OIt=o(e=>e&&sk(e),"normalizeStyleValue"),BIt=o((e,t,r)=>{let n=sk(t);cX.push({sets:uD(e).sort(),id:n,label:r?sk(r):void 0})},"addTextData"),$It=o((e,t)=>{let r=uD(e).sort(),n={};for(let[i,a]of t)n[i]=OIt(a)??a;uX.push({targets:r,styles:n})},"addStyleData"),FIt=o(()=>uX,"getStyleData"),uD=o(e=>e.map(t=>sk(t)),"normalizeIdentifierList"),zIt=o(e=>{let r=uD(e).filter(n=>!hX.has(n));if(r.length>0)throw new Error(`unknown set identifier: ${r.join(", ")}`)},"validateUnionIdentifiers"),GIt=o(()=>cX,"getTextData"),VIt=o(()=>dX,"getCurrentSets"),WIt=o(()=>fX,"getIndentMode"),qIt=o(e=>{fX=e},"setIndentMode"),HIt=cr.venn;o(UIt,"getConfig");YIt=o(()=>{yr(),lX.length=0,cX.length=0,uX.length=0,hX.clear(),dX=void 0,fX=!1},"customClear"),wBe={getConfig:UIt,clear:YIt,setAccTitle:kr,getAccTitle:Ar,setDiagramTitle:Or,getDiagramTitle:Lr,getAccDescription:_r,setAccDescription:Rr,addSubsetData:NIt,getSubsetData:PIt,addTextData:BIt,addStyleData:$It,validateUnionIdentifiers:zIt,getTextData:GIt,getStyleData:FIt,getCurrentSets:VIt,getIndentMode:WIt,setIndentMode:qIt}});var jIt,SBe,EBe=F(()=>{"use strict";jIt=o(e=>` + .venn-title { + font-size: 32px; + fill: ${e.vennTitleTextColor}; + font-family: ${e.fontFamily}; + } + + .venn-circle text { + font-size: 48px; + font-family: ${e.fontFamily}; + } + + .venn-intersection text { + font-size: 48px; + fill: ${e.vennSetTextColor}; + font-family: ${e.fontFamily}; + } + + .venn-text-node { + font-family: ${e.fontFamily}; + color: ${e.vennSetTextColor}; + } +`,"getStyles"),SBe=jIt});function hD(e,t){let r=KIt(e),n=r.filter(l=>XIt(l,e)),i=0,a=0,s=[];if(n.length>1){let l=LBe(n);for(let h=0;hd.angle-h.angle);let u=n[n.length-1];for(let h=0;hg.radius*2&&(T=g.radius*2),(p==null||p.width>T)&&(p={circle:g,width:T,p1:d,p2:u,large:T>g.radius,sweep:!0})}p!=null&&(s.push(p),i+=gX(p.circle.radius,p.width),u=d)}}else{let l=e[0];for(let h=1;hMath.abs(l.radius-e[h].radius)){u=!0;break}u?i=a=0:(i=l.radius*l.radius*Math.PI,s.push({circle:l,p1:{x:l.x,y:l.y+l.radius},p2:{x:l.x-1e-10,y:l.y+l.radius},width:l.radius*2,large:!0,sweep:!0}))}return a/=2,t&&(t.area=i+a,t.arcArea=i,t.polygonArea=a,t.arcs=s,t.innerPoints=n,t.intersectionPoints=r),i+a}function XIt(e,t){return t.every(r=>Ro(e,r)=e+t)return 0;if(r<=Math.abs(e-t))return Math.PI*Math.min(e,t)*Math.min(e,t);let n=e-(r*r-t*t+e*e)/(2*r),i=t-(r*r-e*e+t*t)/(2*r);return gX(e,n)+gX(t,i)}function _Be(e,t){let r=Ro(e,t),n=e.radius,i=t.radius;if(r>=n+i||r<=Math.abs(n-i))return[];let a=(n*n-i*i+r*r)/(2*r),s=Math.sqrt(n*n-a*a),l=e.x+a*(t.x-e.x)/r,u=e.y+a*(t.y-e.y)/r,h=-(t.y-e.y)*(s/r),d=-(t.x-e.x)*(s/r);return[{x:l+h,y:u-d},{x:l-h,y:u+d}]}function LBe(e){let t={x:0,y:0};for(let r of e)t.x+=r.x,t.y+=r.y;return t.x/=e.length,t.y/=e.length,t}function ZIt(e,t,r,n){n=n||{};let i=n.maxIterations||100,a=n.tolerance||1e-10,s=e(t),l=e(r),u=r-t;if(s*l>0)throw"Initial bisect points must have opposite signs";if(s===0)return t;if(l===0)return r;for(let h=0;h=0&&(t=d),Math.abs(u)yX(t))}function xx(e,t){let r=0;for(let n=0;nC.fx-w.fx,"sortOrder"),x=t.slice(),b=t.slice(),T=t.slice(),k=t.slice();for(let C=0;C{let L=R.slice();return L.fx=R.fx,L.id=R.id,L});S.sort((R,L)=>R.id-L.id),r.history.push({x:g[0].slice(),fx:g[0].fx,simplex:S})}p=0;for(let S=0;S=g[m-1].fx){let S=!1;if(b.fx>w.fx?(gd(T,1+d,x,-d,w),T.fx=e(T),T.fx=1)break;for(let R=1;Rl+a*i*u||h>=v)y=i;else{if(Math.abs(f)<=-s*u)return i;f*(y-g)>=0&&(y=g),g=i,v=h}return 0}o(m,"zoom");for(let g=0;g<10;++g){if(gd(n.x,1,r.x,i,t),h=n.fx=e(n.x,n.fxprime),f=xx(n.fxprime,t),h>l+a*i*u||g&&h>=d)return m(p,i,d);if(Math.abs(f)<=-s*u)return i;if(f>=0)return m(i,p,h);d=h,p=i,i*=2}return i}function JIt(e,t,r){let n={x:t.slice(),fx:0,fxprime:t.slice()},i={x:t.slice(),fx:0,fxprime:t.slice()},a=t.slice(),s,l,u=1,h;r=r||{},h=r.maxIterations||t.length*20,n.fx=e(n.x,n.fxprime),s=n.fxprime.slice(),xX(s,n.fxprime,-1);for(let d=0;d{let f={};for(let p=0;pCX(e,t,n)-r,0,e+t)}function eMt(e,t={}){let r=t.distinct,n=e.map(l=>Object.assign({},l));function i(l){return l.join(";")}if(o(i,"toKey"),r){let l=new Map;for(let u of n)for(let h=0;hl===u?0:la.sets.length===2).forEach(a=>{let s=r[a.sets[0]],l=r[a.sets[1]],u=Math.sqrt(t[s].size/Math.PI),h=Math.sqrt(t[l].size/Math.PI),d=bX(u,h,a.size);n[s][l]=n[l][s]=d;let f=0;a.size+1e-10>=Math.min(t[s].size,t[l].size)?f=1:a.size<=1e-10&&(f=-1),i[s][l]=i[l][s]=f}),{distances:n,constraints:i}}function rMt(e,t,r,n){for(let a=0;a0&&g<=f||p<0&&g>=f||(i+=2*y*y,t[2*a]+=4*y*(s-h),t[2*a+1]+=4*y*(l-d),t[2*u]+=4*y*(h-s),t[2*u+1]+=4*y*(d-l))}}return i}function nMt(e,t={}){let r=aMt(e,t),n=t.lossFunction||bx;if(e.length>=8){let i=iMt(e,t),a=n(i,e),s=n(r,e);a+1e-8p.map(m=>m/l));let u=o((p,m)=>rMt(p,m,a,s),"obj"),h=null;for(let p=0;pf.sets.length===2);for(let f of e){let p=f.weight!=null?f.weight:1,m=f.sets[0],g=f.sets[1];f.size+MBe>=Math.min(n[m].size,n[g].size)&&(p=0),i[m].push({set:g,size:f.size,weight:p}),i[g].push({set:m,size:f.size,weight:p})}let a=[];Object.keys(i).forEach(f=>{let p=0;for(let m=0;me[s]));let a=n.weight!=null?n.weight:1;r+=a*(i-n.size)*(i-n.size)}return r}function NBe(e,t){let r=0;for(let n of t){if(n.sets.length===1)continue;let i;if(n.sets.length===2){let l=e[n.sets[0]],u=e[n.sets[1]];i=CX(l.radius,u.radius,Ro(l,u))}else i=hD(n.sets.map(l=>e[l]));let a=n.weight!=null?n.weight:1,s=Math.log((i+1)/(n.size+1));r+=a*s*s}return r}function sMt(e,t,r){if(r==null?e.sort((i,a)=>a.radius-i.radius):e.sort(r),e.length>0){let i=e[0].x,a=e[0].y;for(let s of e)s.x-=i,s.y-=a}if(e.length===2&&Ro(e[0],e[1])1){let i=Math.atan2(e[1].x,e[1].y)-t,a=Math.cos(i),s=Math.sin(i);for(let l of e){let u=l.x,h=l.y;l.x=a*u-s*h,l.y=s*u+a*h}}if(e.length>2){let i=Math.atan2(e[2].x,e[2].y)-t;for(;i<0;)i+=2*Math.PI;for(;i>2*Math.PI;)i-=2*Math.PI;if(i>Math.PI){let a=e[1].y/(1e-10+e[1].x);for(let s of e){var n=(s.x+a*s.y)/(1+a*a);s.x=2*n-s.x,s.y=2*n*a-s.y}}}}function oMt(e){e.forEach(i=>{i.parent=i});function t(i){return i.parent!==i&&(i.parent=t(i.parent)),i.parent}o(t,"find");function r(i,a){let s=t(i),l=t(a);s.parent=l}o(r,"union");for(let i=0;i{delete i.parent}),Array.from(n.values())}function TX(e){let t=o(r=>{let n=e.reduce((a,s)=>Math.max(a,s[r]+s.radius),Number.NEGATIVE_INFINITY),i=e.reduce((a,s)=>Math.min(a,s[r]-s.radius),Number.POSITIVE_INFINITY);return{max:n,min:i}},"minMax");return{xRange:t("x"),yRange:t("y")}}function PBe(e,t,r){t==null&&(t=Math.PI/2);let n=$Be(e).map(h=>Object.assign({},h)),i=oMt(n);for(let h of i){sMt(h,t,r);let d=TX(h);h.size=(d.xRange.max-d.xRange.min)*(d.yRange.max-d.yRange.min),h.bounds=d}i.sort((h,d)=>d.size-h.size),n=i[0];let a=n.bounds,s=(a.xRange.max-a.xRange.min)/50;function l(h,d,f){if(!h)return;let p=h.bounds,m,g;if(d)m=a.xRange.max-p.xRange.min+s;else{m=a.xRange.max-p.xRange.max;let y=(p.xRange.max-p.xRange.min)/2-(a.xRange.max-a.xRange.min)/2;y<0&&(m+=y)}if(f)g=a.yRange.max-p.yRange.min+s;else{g=a.yRange.max-p.yRange.max;let y=(p.yRange.max-p.yRange.min)/2-(a.yRange.max-a.yRange.min)/2;y<0&&(g+=y)}for(let y of h)y.x+=m,y.y+=g,n.push(y)}o(l,"addCluster");let u=1;for(;u({radius:d*m.radius,x:n+f+(m.x-s.min)*d,y:n+p+(m.y-l.min)*d,setid:m.setid})))}function BBe(e){let t={};for(let r of e)t[r.setid]=r;return t}function $Be(e){return Object.keys(e).map(r=>Object.assign(e[r],{setid:r}))}function FBe(e={}){let t=!1,r=600,n=350,i=15,a=1e3,s=Math.PI/2,l=!0,u=null,h=!0,d=!0,f=null,p=null,m=!1,g=null,y=e&&e.symmetricalTextCentre?e.symmetricalTextCentre:!1,v={},x=e&&e.colourScheme?e.colourScheme:e&&e.colorScheme?e.colorScheme:["#1f77b4","#ff7f0e","#2ca02c","#d62728","#9467bd","#8c564b","#e377c2","#7f7f7f","#bcbd22","#17becf"],b=0,T=o(function(S){if(S in v)return v[S];var R=v[S]=x[b];return b+=1,b>=x.length&&(b=0),R},"colours"),k=IBe,C=bx;function w(S){let R=S.datum(),L=new Set;R.forEach(U=>{U.size==0&&U.sets.length==1&&L.add(U.sets[0])}),R=R.filter(U=>!U.sets.some(oe=>L.has(oe)));let N={},I={};if(R.length>0){let U=k(R,{lossFunction:C,distinct:m});l&&(U=PBe(U,s,p)),N=OBe(U,r,n,i,u),I=GBe(N,R,y)}let _={};R.forEach(U=>{U.label&&(_[U.sets]=U.label)});function A(U){if(U.sets in _)return _[U.sets];if(U.sets.length==1)return""+U.sets[0]}o(A,"label"),S.selectAll("svg").data([N]).enter().append("svg");let M=S.select("svg");t?M.attr("viewBox",`0 0 ${r} ${n}`):M.attr("width",r).attr("height",n);let D={},P=!1;M.selectAll(".venn-area path").each(function(U){let oe=this.getAttribute("d");U.sets.length==1&&oe&&!m&&(P=!0,D[U.sets[0]]=uMt(oe))});function B(U){return oe=>{let te=U.sets.map(le=>{let ie=D[le],ae=N[le];return ie||(ie={x:r/2,y:n/2,radius:1}),ae||(ae={x:r/2,y:n/2,radius:1}),{x:ie.x*(1-oe)+ae.x*oe,y:ie.y*(1-oe)+ae.y*oe,radius:ie.radius*(1-oe)+ae.radius*oe}});return RBe(te,g)}}o(B,"pathTween");let O=M.selectAll(".venn-area").data(R,U=>U.sets),$=O.enter().append("g").attr("class",U=>`venn-area venn-${U.sets.length==1?"circle":"intersection"}${U.colour||U.color?" venn-coloured":""}`).attr("data-venn-sets",U=>U.sets.join("_")),V=$.append("path"),G=$.append("text").attr("class","label").text(U=>A(U)).attr("text-anchor","middle").attr("dy",".35em").attr("x",r/2).attr("y",n/2);d&&(V.style("fill-opacity","0").filter(U=>U.sets.length==1).style("fill",U=>U.colour?U.colour:U.color?U.color:T(U.sets)).style("fill-opacity",".25"),G.style("fill",U=>U.colour||U.color?"#FFF":e.textFill?e.textFill:U.sets.length==1?T(U.sets):"#444"));function z(U){return typeof U.transition=="function"?U.transition("venn").duration(a):U}o(z,"asTransition");let W=S;P&&typeof W.transition=="function"?(W=z(S),W.selectAll("path").attrTween("d",B)):W.selectAll("path").attr("d",U=>RBe(U.sets.map(oe=>N[oe])),g);let H=W.selectAll("text").filter(U=>U.sets in I).text(U=>A(U)).attr("x",U=>Math.floor(I[U.sets].x)).attr("y",U=>Math.floor(I[U.sets].y));h&&(P?"on"in H?H.on("end",pX(N,A)):H.each("end",pX(N,A)):H.each(pX(N,A)));let j=z(O.exit()).remove();typeof O.transition=="function"&&j.selectAll("path").attrTween("d",B);let Q=j.selectAll("text").attr("x",r/2).attr("y",n/2);return f!==null&&(G.style("font-size","0px"),H.style("font-size",f),Q.style("font-size","0px")),{circles:N,textCentres:I,nodes:O,enter:$,update:W,exit:j}}return o(w,"chart"),w.wrap=function(S){return arguments.length?(h=S,w):h},w.useViewBox=function(){return t=!0,w},w.width=function(S){return arguments.length?(r=S,w):r},w.height=function(S){return arguments.length?(n=S,w):n},w.padding=function(S){return arguments.length?(i=S,w):i},w.distinct=function(S){return arguments.length?(m=S,w):m},w.colours=function(S){return arguments.length?(T=S,w):T},w.colors=function(S){return arguments.length?(T=S,w):T},w.fontSize=function(S){return arguments.length?(f=S,w):f},w.round=function(S){return arguments.length?(g=S,w):g},w.duration=function(S){return arguments.length?(a=S,w):a},w.layoutFunction=function(S){return arguments.length?(k=S,w):k},w.normalize=function(S){return arguments.length?(l=S,w):l},w.scaleToFit=function(S){return arguments.length?(u=S,w):u},w.styled=function(S){return arguments.length?(d=S,w):d},w.orientation=function(S){return arguments.length?(s=S,w):s},w.orientationOrder=function(S){return arguments.length?(p=S,w):p},w.lossFunction=function(S){return arguments.length?(C=S==="default"?bx:S==="logRatio"?NBe:S,w):C},w}function pX(e,t){return function(r){let n=this,i=e[r.sets[0]].radius||50,a=t(r)||"",s=a.split(/\s+/).reverse(),u=(a.length+s.length)/3,h=s.pop(),d=[h],f=0,p=1.1;n.textContent=null;let m=[];function g(T){let k=n.ownerDocument.createElementNS(n.namespaceURI,"tspan");return k.textContent=T,m.push(k),n.append(k),k}o(g,"append");let y=g(h);for(;h=s.pop(),!!h;){d.push(h);let T=d.join(" ");y.textContent=T,T.length>u&&y.getComputedTextLength()>i&&(d.pop(),y.textContent=d.join(" "),d=[h],y=g(h),f++)}let v=.35-f*p/2,x=n.getAttribute("x"),b=n.getAttribute("y");m.forEach((T,k)=>{T.setAttribute("x",x),T.setAttribute("y",b),T.setAttribute("dy",`${v+k*p}em`)})}}function mX(e,t,r){let n=t[0].radius-Ro(t[0],e);for(let i=1;i=a&&(i=n[d],a=f)}let s=DBe(d=>-1*mX({x:d[0],y:d[1]},e,t),[i.x,i.y],{maxIterations:500,minErrorDelta:1e-10}).x,l={x:r?0:s[0],y:s[1]},u=!0;for(let d of e)if(Ro(l,d)>d.radius){u=!1;break}for(let d of t)if(Ro(l,d)d.p1))}function lMt(e){let t={},r=Object.keys(e);for(let n of r)t[n]=[];for(let n=0;n0&&console.log("WARNING: area "+s+" not represented on screen")}return n}function cMt(e,t,r){let n=[];return n.push(` +M`,e,t),n.push(` +m`,-r,0),n.push(` +a`,r,r,0,1,0,r*2,0),n.push(` +a`,r,r,0,1,0,-r*2,0),n.join(" ")}function uMt(e){let t=e.split(" ");return{x:Number.parseFloat(t[1]),y:Number.parseFloat(t[2]),radius:-Number.parseFloat(t[4])}}function VBe(e){if(e.length===0)return[];let t={};return hD(e,t),t.arcs}function WBe(e,t){if(e.length===0)return"M 0 0";let r=Math.pow(10,t||0),n=t!=null?a=>Math.round(a*r)/r:a=>a;if(e.length==1){let a=e[0].circle;return cMt(n(a.x),n(a.y),n(a.radius))}let i=[` +M`,n(e[0].p2.x),n(e[0].p2.y)];for(let a of e){let s=n(a.circle.radius);i.push(` +A`,s,s,0,a.large?1:0,a.sweep?1:0,n(a.p1.x),n(a.p1.y))}return i.join(" ")}function RBe(e,t){return WBe(VBe(e),t)}function qBe(e,t={}){let{lossFunction:r,layoutFunction:n=IBe,normalize:i=!0,orientation:a=Math.PI/2,orientationOrder:s,width:l=600,height:u=350,padding:h=15,scaleToFit:d=!1,symmetricalTextCentre:f=!1,distinct:p,round:m=2}=t,g=n(e,{lossFunction:r==="default"||!r?bx:r==="logRatio"?NBe:r,distinct:p});i&&(g=PBe(g,a,s));let y=OBe(g,l,u,h,d),v=GBe(y,e,f),x=new Map(Object.keys(y).map(k=>[k,{set:k,x:y[k].x,y:y[k].y,radius:y[k].radius}])),b=e.map(k=>{let C=k.sets.map(R=>x.get(R)),w=VBe(C),S=WBe(w,m);return{circles:C,arcs:w,path:S,area:k,has:new Set(k.sets)}});function T(k){let C="";for(let w of b)w.has.size>k.length&&k.every(S=>w.has.has(S))&&(C+=" "+w.path);return C}return o(T,"genDistinctPath"),b.map(({circles:k,arcs:C,path:w,area:S})=>({data:S,text:v[S.sets],circles:k,arcs:C,path:w,distinctPath:w+T(S.sets)}))}var MBe,HBe=F(()=>{"use strict";o(hD,"intersectionArea");o(XIt,"containedInCircles");o(KIt,"getIntersectionPoints");o(gX,"circleArea");o(Ro,"distance");o(CX,"circleOverlap");o(_Be,"circleCircleIntersection");o(LBe,"getCenter");o(ZIt,"bisect");o(yX,"zeros");o(ABe,"zerosM");o(xx,"dot");o(vX,"norm2");o(xX,"scale");o(gd,"weightedSum");o(DBe,"nelderMead");o(QIt,"wolfeLineSearch");o(JIt,"conjugateGradient");o(IBe,"venn");MBe=1e-10;o(bX,"distanceFromIntersectArea");o(eMt,"addMissingAreas");o(tMt,"getDistanceMatrices");o(rMt,"constrainedMDSGradient");o(nMt,"bestInitialLayout");o(iMt,"constrainedMDSLayout");o(aMt,"greedyLayout");o(bx,"lossFunction");o(NBe,"logRatioLossFunction");o(sMt,"orientateCircles");o(oMt,"disjointCluster");o(TX,"getBoundingBox");o(PBe,"normalizeSolution");o(OBe,"scaleSolution");o(BBe,"toObjectNotation");o($Be,"fromObjectNotation");o(FBe,"VennDiagram");o(pX,"wrapText");o(mX,"circleMargin");o(zBe,"computeTextCentre");o(lMt,"getOverlappingCircles");o(GBe,"computeTextCentres");o(cMt,"circlePath");o(uMt,"circleFromPath");o(VBe,"intersectionAreaArcs");o(WBe,"arcsToPath");o(RBe,"intersectionAreaPath");o(qBe,"layout")});function dMt(e){let t=new Map;for(let r of e){let n=r.targets.join("|"),i=t.get(n);i?Object.assign(i,r.styles):t.set(n,{...r.styles})}return t}function N0(e){return e.join("|")}function pMt(e,t,r,n,i,a){let s=e?.useDebugLayout??!1,u=r.select("svg").append("g").attr("class","venn-text-nodes"),h=new Map;for(let d of n){let f=N0(d.sets),p=h.get(f);p?p.push(d):h.set(f,[d])}for(let[d,f]of h.entries()){let p=t.get(d);if(!p?.text)continue;let m=p.text.x,g=p.text.y,y=Math.min(...p.circles.map(M=>M.radius)),v=Math.min(...p.circles.map(M=>M.radius-Math.hypot(m-M.x,g-M.y))),x=Number.isFinite(v)?Math.max(0,v):0;x===0&&Number.isFinite(y)&&(x=y*.6);let b=u.append("g").attr("class","venn-text-area").attr("font-size",`${40*i}px`);s&&b.append("circle").attr("class","venn-text-debug-circle").attr("cx",m).attr("cy",g).attr("r",x).attr("fill","none").attr("stroke","purple").attr("stroke-width",1.5*i).attr("stroke-dasharray",`${6*i} ${4*i}`);let T=Math.max(80*i,x*2*.95),k=Math.max(60*i,x*2*.95),S=(p.data.label&&p.data.label.length>0?Math.min(32*i,x*.25):0)+(f.length<=2?30*i:0),R=m-T/2,L=g-k/2+S,N=Math.max(1,Math.ceil(Math.sqrt(f.length))),I=Math.max(1,Math.ceil(f.length/N)),_=T/N,A=k/I;for(let[M,D]of f.entries()){let P=M%N,B=Math.floor(M/N),O=R+_*(P+.5),$=L+A*(B+.5);s&&b.append("rect").attr("class","venn-text-debug-cell").attr("x",R+_*P).attr("y",L+A*B).attr("width",_).attr("height",A).attr("fill","none").attr("stroke","teal").attr("stroke-width",1*i).attr("stroke-dasharray",`${4*i} ${3*i}`);let V=_*.9,G=A*.9,z=b.append("foreignObject").attr("class","venn-text-node-fo").attr("width",V).attr("height",G).attr("x",O-V/2).attr("y",$-G/2).attr("overflow","visible"),W=a.get(D.id)?.color,H=z.append("xhtml:span").attr("class","venn-text-node").style("display","flex").style("width","100%").style("height","100%").style("white-space","normal").style("align-items","center").style("justify-content","center").style("text-align","center").style("overflow-wrap","normal").style("word-break","normal").text(D.label??D.id);W&&H.style("color",W)}}}function mMt(e){let t=new Set(e.map(i=>[...i.sets].sort().join("|"))),r=new Map(e.filter(i=>i.sets.length===1&&i.size!==void 0).map(i=>[i.sets[0],i.size])),n=[];for(let i of e){if(i.sets.length<3)continue;let a=[...i.sets].sort();for(let s=0;s0?[...e,...n]:e}var fMt,UBe,YBe=F(()=>{"use strict";$r();zi();ur();Ka();HBe();$n();tr();o(dMt,"buildStyleByKey");fMt=o((e,t,r,n)=>{let i=n.db,a=i.getConfig?.(),{themeVariables:s,look:l,handDrawnSeed:u}=_t(),h=l==="handDrawn",d=[s.venn1,s.venn2,s.venn3,s.venn4,s.venn5,s.venn6,s.venn7,s.venn8].filter(Boolean),f=i.getDiagramTitle?.(),p=i.getSubsetData(),m=i.getTextData(),g=dMt(i.getStyleData()),y=mMt(p),v=a?.width??800,x=a?.height??450,T=v/1600,k=f?48*T:0,C=s.primaryTextColor??s.textColor,w=xn(t);w.attr("viewBox",`0 0 ${v} ${x}`),f&&w.append("text").text(f).attr("class","venn-title").attr("font-size",`${32*T}px`).attr("text-anchor","middle").attr("dominant-baseline","middle").attr("x","50%").attr("y",32*T).style("fill",s.vennTitleTextColor||s.titleColor);let S=et(document.createElement("div")),R=FBe().width(v).height(x-k);S.datum(y).call(R);let L=h?ut.svg(S.select("svg").node()):void 0,N=qBe(y,{width:v,height:x-k,padding:a?.padding??15}),I=new Map;for(let D of N){let P=N0([...D.data.sets].sort());I.set(P,D)}m.length>0&&pMt(a,I,S,m,T,g);let _=Tn(s.background||"#f4f4f4");S.selectAll(".venn-circle").each(function(D,P){let B=et(this),$=N0([...D.sets].sort()),V=g.get($),G=V?.fill||d[P%d.length]||s.primaryColor;B.classed(`venn-set-${P%8}`,!0);let z=V?.["fill-opacity"]??.1,W=V?.stroke||G,H=V?.["stroke-width"]||`${5*T}`;if(h&&L){let Q=I.get($);if(Q&&Q.circles.length>0){let U=Q.circles[0],oe=L.circle(U.x,U.y,U.radius*2,{roughness:.7,seed:u,fill:Sk(G,.7),fillStyle:"hachure",fillWeight:2,hachureGap:8,hachureAngle:-41+P*60,stroke:W,strokeWidth:parseFloat(String(H))});B.select("path").remove(),B.node()?.insertBefore(oe,B.select("text").node())}}else B.select("path").style("fill",G).style("fill-opacity",z).style("stroke",W).style("stroke-width",H).style("stroke-opacity",.95);let j=V?.color||(_?Qe(G,30):Je(G,30));B.select("text").style("font-size",`${48*T}px`).style("fill",j)}),h&&L?S.selectAll(".venn-intersection").each(function(D){let P=et(this),O=N0([...D.sets].sort()),$=g.get(O),V=$?.fill;if(V){let G=P.select("path"),z=G.attr("d");if(z){let W=L.path(z,{roughness:.7,seed:u,fill:Sk(V,.3),fillStyle:"cross-hatch",fillWeight:2,hachureGap:6,hachureAngle:60,stroke:"none"}),H=G.node();H?.parentNode?.insertBefore(W,H),G.remove()}}else P.select("path").style("fill-opacity",0);P.select("text").style("font-size",`${48*T}px`).style("fill",$?.color??s.vennSetTextColor??C)}):(S.selectAll(".venn-intersection text").style("font-size",`${48*T}px`).style("fill",D=>{let B=N0([...D.sets].sort());return g.get(B)?.color??s.vennSetTextColor??C}),S.selectAll(".venn-intersection path").style("fill-opacity",D=>{let B=N0([...D.sets].sort());return g.get(B)?.fill?1:0}).style("fill",D=>{let B=N0([...D.sets].sort());return g.get(B)?.fill??"transparent"}));let A=w.append("g").attr("transform",`translate(0, ${k})`),M=S.select("svg").node();if(M&&"childNodes"in M)for(let D of[...M.childNodes])A.node()?.appendChild(D);Wr(w,x,v,a?.useMaxWidth??!0)},"draw");o(N0,"stableSetsKey");o(pMt,"renderTextNodes");o(mMt,"ensurePairwiseSubsets");UBe={draw:fMt}});var jBe={};ir(jBe,{diagram:()=>gMt});var gMt,XBe=F(()=>{"use strict";CBe();kBe();EBe();YBe();gMt={parser:TBe,db:wBe,renderer:UBe,styles:SBe}});var Tx,wX=F(()=>{"use strict";Wi();ur();Qt();Jt();Nn();Tx=class{constructor(){this.nodes=[];this.levels=new Map;this.outerNodes=[];this.classes=new Map;this.setAccTitle=kr;this.getAccTitle=Ar;this.setDiagramTitle=Or;this.getDiagramTitle=Lr;this.getAccDescription=_r;this.setAccDescription=Rr}static{o(this,"TreeMapDB")}getNodes(){return this.nodes}getConfig(){let t=cr,r=_t();return qr({...t.treemap,...r.treemap??{}})}addNode(t,r){this.nodes.push(t),this.levels.set(t,r),r===0&&(this.outerNodes.push(t),this.root??=t)}getRoot(){return{name:"",children:this.outerNodes}}addClass(t,r){let n=this.classes.get(t)??{id:t,styles:[],textStyles:[]},i=r.replace(/\\,/g,"\xA7\xA7\xA7").replace(/,/g,";").replace(/§§§/g,",").split(";");i&&i.forEach(a=>{N2(a)&&(n?.textStyles?n.textStyles.push(a):n.textStyles=[a]),n?.styles?n.styles.push(a):n.styles=[a]}),this.classes.set(t,n)}getClasses(){return this.classes}getStylesForClass(t){return this.classes.get(t)?.styles??[]}clear(){yr(),this.nodes=[],this.levels=new Map,this.outerNodes=[],this.classes=new Map,this.root=void 0}}});function QBe(e){if(!e.length)return[];let t=[],r=[];return e.forEach(n=>{let i={name:n.name,children:n.type==="Leaf"?void 0:[]};for(i.classSelector=n?.classSelector,n?.cssCompiledStyles&&(i.cssCompiledStyles=n.cssCompiledStyles),n.type==="Leaf"&&n.value!==void 0&&(i.value=n.value);r.length>0&&r[r.length-1].level>=n.level;)r.pop();if(r.length===0)t.push(i);else{let a=r[r.length-1].node;a.children?a.children.push(i):a.children=[i]}n.type!=="Leaf"&&r.push({node:i,level:n.level})}),t}var JBe=F(()=>{"use strict";o(QBe,"buildHierarchy")});var bMt,TMt,kX,e$e=F(()=>{"use strict";Xa();vt();Hs();JBe();wX();bMt=o((e,t)=>{Gn(e,t);let r=[];for(let a of e.TreemapRows??[])a.$type==="ClassDefStatement"&&t.addClass(a.className??"",a.styleText??"");for(let a of e.TreemapRows??[]){let s=a.item;if(!s)continue;let l=a.indent?parseInt(a.indent):0,u=TMt(s),h=s.classSelector?t.getStylesForClass(s.classSelector):[],d=h.length>0?h:void 0,f={level:l,name:u,type:s.$type,value:s.value,classSelector:s.classSelector,cssCompiledStyles:d};r.push(f)}let n=QBe(r),i=o((a,s)=>{for(let l of a)t.addNode(l,s),l.children&&l.children.length>0&&i(l.children,s+1)},"addNodesRecursively");i(n,0)},"populate"),TMt=o(e=>e.name?String(e.name):"","getItemName"),kX={parser:{yy:void 0},parse:o(async e=>{try{let r=await Si("treemap",e);Z.debug("Treemap AST:",r);let n=kX.parser?.yy;if(!(n instanceof Tx))throw new Error("parser.parser?.yy was not a TreemapDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.");bMt(r,n)}catch(t){throw Z.error("Error parsing treemap:",t),t}},"parse")}});var CMt,Cx,ok,wMt,kMt,t$e,r$e=F(()=>{"use strict";Ka();ep();$n();$r();Jt();ur();vt();CMt=10,Cx=10,ok=25,wMt=o((e,t,r,n)=>{let i=n.db,a=i.getConfig(),s=a.padding??CMt,l=i.getDiagramTitle(),u=i.getRoot(),{themeVariables:h}=_t();if(!u)return;let d=l?30:0,f=xn(t),p=a.nodeWidth?a.nodeWidth*Cx:960,m=a.nodeHeight?a.nodeHeight*Cx:500,g=p,y=m+d;f.attr("viewBox",`0 0 ${g} ${y}`),Wr(f,y,g,a.useMaxWidth);let v;try{let z=a.valueFormat||",";if(z==="$0,0")v=o(W=>"$"+Xc(",")(W),"valueFormat");else if(z.startsWith("$")&&z.includes(",")){let W=/\.\d+/.exec(z),H=W?W[0]:"";v=o(j=>"$"+Xc(","+H)(j),"valueFormat")}else if(z.startsWith("$")){let W=z.substring(1);v=o(H=>"$"+Xc(W||"")(H),"valueFormat")}else v=Xc(z)}catch(z){Z.error("Error creating format function:",z),v=Xc(",")}let x=Oo().range(["transparent",h.cScale0,h.cScale1,h.cScale2,h.cScale3,h.cScale4,h.cScale5,h.cScale6,h.cScale7,h.cScale8,h.cScale9,h.cScale10,h.cScale11]),b=Oo().range(["transparent",h.cScalePeer0,h.cScalePeer1,h.cScalePeer2,h.cScalePeer3,h.cScalePeer4,h.cScalePeer5,h.cScalePeer6,h.cScalePeer7,h.cScalePeer8,h.cScalePeer9,h.cScalePeer10,h.cScalePeer11]),T=Oo().range([h.cScaleLabel0,h.cScaleLabel1,h.cScaleLabel2,h.cScaleLabel3,h.cScaleLabel4,h.cScaleLabel5,h.cScaleLabel6,h.cScaleLabel7,h.cScaleLabel8,h.cScaleLabel9,h.cScaleLabel10,h.cScaleLabel11]);l&&f.append("text").attr("x",g/2).attr("y",d/2).attr("class","treemapTitle").attr("text-anchor","middle").attr("dominant-baseline","middle").text(l);let k=f.append("g").attr("transform",`translate(0, ${d})`).attr("class","treemapContainer"),C=vy(u).sum(z=>z.value??0).sort((z,W)=>(W.value??0)-(z.value??0)),S=uE().size([p,m]).paddingTop(z=>z.children&&z.children.length>0?ok+Cx:0).paddingInner(s).paddingLeft(z=>z.children&&z.children.length>0?Cx:0).paddingRight(z=>z.children&&z.children.length>0?Cx:0).paddingBottom(z=>z.children&&z.children.length>0?Cx:0).round(!0)(C),R=S.descendants().filter(z=>z.children&&z.children.length>0),L=k.selectAll(".treemapSection").data(R).enter().append("g").attr("class","treemapSection").attr("transform",z=>`translate(${z.x0},${z.y0})`);L.append("rect").attr("width",z=>z.x1-z.x0).attr("height",ok).attr("class","treemapSectionHeader").attr("fill","none").attr("fill-opacity",.6).attr("stroke-width",.6).attr("style",z=>z.depth===0?"display: none;":""),L.append("clipPath").attr("id",(z,W)=>`clip-section-${t}-${W}`).append("rect").attr("width",z=>Math.max(0,z.x1-z.x0-12)).attr("height",ok),L.append("rect").attr("width",z=>z.x1-z.x0).attr("height",z=>z.y1-z.y0).attr("class",(z,W)=>`treemapSection section${W}`).attr("fill",z=>x(z.data.name)).attr("fill-opacity",.6).attr("stroke",z=>b(z.data.name)).attr("stroke-width",2).attr("stroke-opacity",.4).attr("style",z=>{if(z.depth===0)return"display: none;";let W=ct({cssCompiledStyles:z.data.cssCompiledStyles});return W.nodeStyles+";"+W.borderStyles.join(";")}),L.append("text").attr("class","treemapSectionLabel").attr("x",6).attr("y",ok/2).attr("dominant-baseline","middle").text(z=>z.depth===0?"":z.data.name).attr("font-weight","bold").attr("clip-path",(z,W)=>`url(#clip-section-${t}-${W})`).attr("style",z=>{if(z.depth===0)return"display: none;";let W="dominant-baseline: middle; font-size: 12px; fill:"+T(z.data.name)+"; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;",H=ct({cssCompiledStyles:z.data.cssCompiledStyles});return W+H.labelStyles.replace("color:","fill:")}).each(function(z){if(z.depth===0)return;let W=et(this),H=z.data.name;W.text(H);let j=z.x1-z.x0,Q=6,U;a.showValues!==!1&&z.value?U=j-10-30-10-Q:U=j-Q-6;let te=Math.max(15,U),le=W.node();if(le.getComputedTextLength()>te){let Re=H;for(;Re.length>0;){if(Re=H.substring(0,Re.length-1),Re.length===0){W.text("..."),le.getComputedTextLength()>te&&W.text("");break}if(W.text(Re+"..."),le.getComputedTextLength()<=te)break}}}),a.showValues!==!1&&L.append("text").attr("class","treemapSectionValue").attr("x",z=>z.x1-z.x0-10).attr("y",ok/2).attr("text-anchor","end").attr("dominant-baseline","middle").text(z=>z.value?v(z.value):"").attr("font-style","italic").attr("style",z=>{if(z.depth===0)return"display: none;";let W="text-anchor: end; dominant-baseline: middle; font-size: 10px; fill:"+T(z.data.name)+"; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;",H=ct({cssCompiledStyles:z.data.cssCompiledStyles});return W+H.labelStyles.replace("color:","fill:")});let N=S.leaves(),I=N.length>20,_=I?16:38,A=I?14:28,M=I?4:8,D=I?4:6,P=I?2:4,B=I?8:10,O=I?1:2,$=k.selectAll(".treemapLeafGroup").data(N).enter().append("g").attr("class",(z,W)=>`treemapNode treemapLeafGroup leaf${W}${z.data.classSelector?` ${z.data.classSelector}`:""}x`).attr("transform",z=>`translate(${z.x0},${z.y0})`);$.append("rect").attr("width",z=>z.x1-z.x0).attr("height",z=>z.y1-z.y0).attr("class","treemapLeaf").attr("fill",z=>z.parent?x(z.parent.data.name):x(z.data.name)).attr("style",z=>ct({cssCompiledStyles:z.data.cssCompiledStyles}).nodeStyles).attr("fill-opacity",.3).attr("stroke",z=>z.parent?x(z.parent.data.name):x(z.data.name)).attr("stroke-width",3),$.append("clipPath").attr("id",(z,W)=>`clip-${t}-${W}`).append("rect").attr("width",z=>Math.max(0,z.x1-z.x0-4)).attr("height",z=>Math.max(0,z.y1-z.y0-4)),$.append("text").attr("class","treemapLabel").attr("x",z=>(z.x1-z.x0)/2).attr("y",z=>(z.y1-z.y0)/2).attr("style",z=>{let W=`text-anchor: middle; dominant-baseline: middle; font-size: ${_}px;fill:`+T(z.data.name)+";",H=ct({cssCompiledStyles:z.data.cssCompiledStyles});return W+H.labelStyles.replace("color:","fill:")}).attr("clip-path",(z,W)=>`url(#clip-${t}-${W})`).text(z=>z.data.name).each(function(z){let W=et(this),H=z.x1-z.x0,j=z.y1-z.y0,Q=W.node(),U=H-2*P,oe=j-2*P;if(UU&&te>M;)te--,W.style("font-size",`${te}px`);let ie=Math.max(D,Math.min(A,Math.round(te*le))),ae=te+O+ie;for(;ae>oe&&te>M&&(te--,ie=Math.max(D,Math.min(A,Math.round(te*le))),!(ieoe;W.style("font-size",`${te}px`),I?(teU||te(W.x1-W.x0)/2).attr("y",function(W){return(W.y1-W.y0)/2}).attr("style",W=>{let H=`text-anchor: middle; dominant-baseline: hanging; font-size: ${A}px;fill:`+T(W.data.name)+";",j=ct({cssCompiledStyles:W.data.cssCompiledStyles});return H+j.labelStyles.replace("color:","fill:")}).attr("clip-path",(W,H)=>`url(#clip-${t}-${H})`).text(W=>W.value?v(W.value):"").each(function(W){let H=et(this),j=this.parentNode;if(!j){H.style("display","none");return}let Q=et(j).select(".treemapLabel");if(Q.empty()||Q.style("display")==="none"){H.style("display","none");return}let U=parseFloat(Q.style("font-size")),te=Math.max(D,Math.min(A,Math.round(U*.6)));H.style("font-size",`${te}px`);let ie=(W.y1-W.y0)/2+U/2+O;H.attr("y",ie);let ae=W.x1-W.x0,Pe=W.y1-W.y0-4,Ge=ae-2*P;H.node().getComputedTextLength()>Ge||ie+te>Pe||te{"use strict";Qt();Pc();ur();SMt={sectionStrokeColor:"black",sectionStrokeWidth:"1",sectionFillColor:"#efefef",leafStrokeColor:"black",leafStrokeWidth:"1",leafFillColor:"#efefef",labelFontSize:"12px",valueFontSize:"10px",titleFontSize:"14px"},EMt=o(({treemap:e}={})=>{let t=ma(),r=_t(),n=qr(t,r.themeVariables),i=qr(SMt,e),a=i.titleColor??n.titleColor,s=i.labelColor??n.textColor,l=i.valueColor??n.textColor;return` + .treemapNode.section { + stroke: ${i.sectionStrokeColor}; + stroke-width: ${i.sectionStrokeWidth}; + fill: ${i.sectionFillColor}; + } + .treemapNode.leaf { + stroke: ${i.leafStrokeColor}; + stroke-width: ${i.leafStrokeWidth}; + fill: ${i.leafFillColor}; + } + .treemapLabel { + fill: ${s}; + font-size: ${i.labelFontSize}; + } + .treemapValue { + fill: ${l}; + font-size: ${i.valueFontSize}; + } + .treemapTitle { + fill: ${a}; + font-size: ${i.titleFontSize}; + } + `},"getStyles"),n$e=EMt});var a$e={};ir(a$e,{diagram:()=>AMt});var AMt,s$e=F(()=>{"use strict";wX();e$e();r$e();i$e();AMt={parser:kX,get db(){return new Tx},renderer:t$e,styles:n$e}});var dD,P0,c$e,LMt,DMt,SX,u$e=F(()=>{"use strict";Xa();vt();Hs();dD=o((e,t)=>{let r=e<=1?e*100:e;if(r<0||r>100)throw new Error(`${t} must be between 0-1 (decimal) or 0-100 (percentage). Received: ${e}`);return r},"toPercent"),P0=o((e,t,r)=>({x:dD(t,`${r} evolution`),y:dD(e,`${r} visibility`)}),"toCoordinates"),c$e=o(e=>{if(e){if(e==="+<>")return"bidirectional";if(e==="+<")return"backward";if(e==="+>")return"forward"}},"getFlowFromPort"),LMt=o(e=>{if(!e?.startsWith("+"))return{};let r=/^\+'([^']*)'/.exec(e)?.[1];return e.includes("<>")?{flow:"bidirectional",label:r}:e.includes("<")?{flow:"backward",label:r}:e.includes(">")?{flow:"forward",label:r}:{label:r}},"extractFlowFromArrow"),DMt=o((e,t)=>{if(Gn(e,t),e.size&&t.setSize(e.size.width,e.size.height),e.evolution){let r=e.evolution.stages.map(i=>i.secondName?`${i.name.trim()} / ${i.secondName.trim()}`:i.name.trim()),n=e.evolution.stages.filter(i=>i.boundary!==void 0).map(i=>i.boundary);t.updateAxes({stages:r,stageBoundaries:n})}if(e.anchors.forEach(r=>{let n=P0(r.visibility,r.evolution,`Anchor "${r.name}"`);t.addNode(r.name,r.name,n.x,n.y,"anchor")}),e.components.forEach(r=>{let n=P0(r.visibility,r.evolution,`Component "${r.name}"`),i=r.label?(r.label.negX?-1:1)*r.label.offsetX:void 0,a=r.label?(r.label.negY?-1:1)*r.label.offsetY:void 0,s=r.decorator?.strategy;t.addNode(r.name,r.name,n.x,n.y,"component",i,a,r.inertia,s)}),e.notes.forEach(r=>{let n=P0(r.visibility,r.evolution,`Note "${r.text}"`);t.addNote(r.text,n.x,n.y)}),e.pipelines.forEach(r=>{let n=t.getNode(r.parent);if(!n||typeof n.y!="number")throw new Error(`Pipeline "${r.parent}" must reference an existing component with coordinates.`);let i=n.y;t.startPipeline(r.parent),r.components.forEach(a=>{let s=`${r.parent}_${a.name}`,l=a.label?(a.label.negX?-1:1)*a.label.offsetX:void 0,u=a.label?(a.label.negY?-1:1)*a.label.offsetY:void 0,h=dD(a.evolution,`Pipeline component "${a.name}" evolution`);t.addNode(s,a.name,h,i,"pipeline-component",l,u),t.addPipelineComponent(r.parent,s)})}),e.links.forEach(r=>{let n=!!r.arrow&&(r.arrow.includes("-.->")||r.arrow.includes(".-.")),i=c$e(r.fromPort)??c$e(r.toPort),{flow:a,label:s}=LMt(r.arrow);!i&&a&&(i=a);let l=r.linkLabel,u=s??l;t.addLink(t.resolveNodeId(r.from),t.resolveNodeId(r.to),n,u,i)}),e.evolves.forEach(r=>{let n=t.getNode(r.component);if(n?.y!==void 0){let i=dD(r.target,`Evolve target for "${r.component}"`);t.addTrend(r.component,i,n.y)}}),e.annotations.length>0){let r=e.annotations[0],n=P0(r.x,r.y,"Annotations box");t.setAnnotationsBox(n.x,n.y)}e.annotation.forEach(r=>{let n=P0(r.x,r.y,`Annotation ${r.number}`);t.addAnnotation(r.number,[{x:n.x,y:n.y}],r.text)}),e.accelerators.forEach(r=>{let n=P0(r.x,r.y,`Accelerator "${r.name}"`);t.addAccelerator(r.name,n.x,n.y)}),e.deaccelerators.forEach(r=>{let n=P0(r.x,r.y,`Deaccelerator "${r.name}"`);t.addDeaccelerator(r.name,n.x,n.y)})},"populateDb"),SX={parser:{yy:void 0},parse:o(async e=>{let t=await Si("wardley",e);Z.debug(t);let r=SX.parser?.yy;if(!r||typeof r.addNode!="function")throw new Error("parser.parser?.yy was not a WardleyDB. This is due to a bug within Mermaid, please report this issue at https://github.com/mermaid-js/mermaid/issues.");DMt(t,r)},"parse")}});var fD,h$e=F(()=>{"use strict";fD=class{constructor(){this.nodes=new Map;this.links=[];this.trends=new Map;this.pipelines=new Map;this.annotations=[];this.notes=[];this.accelerators=[];this.deaccelerators=[];this.axes={}}static{o(this,"WardleyBuilder")}addNode(t){let r=this.nodes.get(t.id)??{id:t.id,label:t.label},n={...r,...t,className:t.className??r.className,labelOffsetX:t.labelOffsetX??r.labelOffsetX,labelOffsetY:t.labelOffsetY??r.labelOffsetY};this.nodes.set(t.id,n)}addLink(t){this.links.push(t)}addTrend(t){this.trends.set(t.nodeId,t)}startPipeline(t){this.pipelines.set(t,{nodeId:t,componentIds:[]});let r=this.nodes.get(t);r&&(r.isPipelineParent=!0)}addPipelineComponent(t,r){let n=this.pipelines.get(t);n&&n.componentIds.push(r);let i=this.nodes.get(r);i&&(i.inPipeline=!0)}addAnnotation(t){this.annotations.push(t)}addNote(t){this.notes.push(t)}addAccelerator(t){this.accelerators.push(t)}addDeaccelerator(t){this.deaccelerators.push(t)}setAnnotationsBox(t,r){this.annotationsBox={x:t,y:r}}setAxes(t){this.axes={...this.axes,...t}}setSize(t,r){this.size={width:t,height:r}}getNode(t){return this.nodes.get(t)}resolveNodeId(t){if(this.nodes.has(t))return t;for(let[r,n]of this.nodes)if(n.label===t)return r;return t}build(){let t=[];for(let r of this.nodes.values()){if(typeof r.x!="number"||typeof r.y!="number")throw new Error(`Node "${r.label}" is missing coordinates`);t.push(r)}return{nodes:t,links:[...this.links],trends:[...this.trends.values()],pipelines:[...this.pipelines.values()],annotations:[...this.annotations],notes:[...this.notes],accelerators:[...this.accelerators],deaccelerators:[...this.deaccelerators],annotationsBox:this.annotationsBox,axes:{...this.axes},size:this.size}}clear(){this.nodes.clear(),this.links=[],this.trends.clear(),this.pipelines.clear(),this.annotations=[],this.notes=[],this.accelerators=[],this.deaccelerators=[],this.annotationsBox=void 0,this.axes={},this.size=void 0}}});function IMt(){return Ae()["wardley-beta"]}function MMt(e,t,r,n,i,a,s,l,u){gs.addNode({id:e,label:t,x:r,y:n,className:i,labelOffsetX:a,labelOffsetY:s,inertia:l,sourceStrategy:u})}function NMt(e,t,r=!1,n,i){gs.addLink({source:e,target:t,dashed:r,label:n,flow:i})}function PMt(e,t,r){gs.addTrend({nodeId:e,targetX:t,targetY:r})}function OMt(e,t,r){gs.addAnnotation({number:e,coordinates:t,text:r})}function BMt(e,t,r){gs.addNote({text:e,x:t,y:r})}function $Mt(e,t,r){gs.addAccelerator({name:e,x:t,y:r})}function FMt(e,t,r){gs.addDeaccelerator({name:e,x:t,y:r})}function zMt(e,t){gs.setAnnotationsBox(e,t)}function GMt(e,t){gs.setSize(e,t)}function VMt(e){gs.startPipeline(e)}function WMt(e,t){gs.addPipelineComponent(e,t)}function qMt(e){gs.setAxes(e)}function HMt(e){return gs.getNode(e)}function UMt(e){return gs.resolveNodeId(e)}function YMt(){return gs.build()}function jMt(){gs.clear(),yr()}var gs,d$e,f$e=F(()=>{"use strict";Xt();Nn();h$e();gs=new fD;o(IMt,"getConfig");o(MMt,"addNode");o(NMt,"addLink");o(PMt,"addTrend");o(OMt,"addAnnotation");o(BMt,"addNote");o($Mt,"addAccelerator");o(FMt,"addDeaccelerator");o(zMt,"setAnnotationsBox");o(GMt,"setSize");o(VMt,"startPipeline");o(WMt,"addPipelineComponent");o(qMt,"updateAxes");o(HMt,"getNode");o(UMt,"resolveNodeId");o(YMt,"getWardleyData");o(jMt,"clear");d$e={getConfig:IMt,addNode:MMt,addLink:NMt,addTrend:PMt,addAnnotation:OMt,addNote:BMt,addAccelerator:$Mt,addDeaccelerator:FMt,setAnnotationsBox:zMt,setSize:GMt,startPipeline:VMt,addPipelineComponent:WMt,updateAxes:qMt,getNode:HMt,resolveNodeId:UMt,getWardleyData:YMt,clear:jMt,setAccTitle:kr,getAccTitle:Ar,setDiagramTitle:Or,getDiagramTitle:Lr,getAccDescription:_r,setAccDescription:Rr}});var XMt,KMt,ZMt,QMt,p$e,m$e=F(()=>{"use strict";Xt();vt();Ka();$n();XMt=["Genesis","Custom Built","Product","Commodity"],KMt=o(()=>{let{themeVariables:e}=Ae();return{backgroundColor:e.wardley?.backgroundColor??e.background??"#fff",axisColor:e.wardley?.axisColor??"#000",axisTextColor:e.wardley?.axisTextColor??e.primaryTextColor??"#222",gridColor:e.wardley?.gridColor??"rgba(100, 100, 100, 0.2)",componentFill:e.wardley?.componentFill??"#fff",componentStroke:e.wardley?.componentStroke??"#000",componentLabelColor:e.wardley?.componentLabelColor??e.primaryTextColor??"#222",linkStroke:e.wardley?.linkStroke??"#000",evolutionStroke:e.wardley?.evolutionStroke??"#dc3545",annotationStroke:e.wardley?.annotationStroke??"#000",annotationTextColor:e.wardley?.annotationTextColor??e.primaryTextColor??"#222",annotationFill:e.wardley?.annotationFill??e.background??"#fff"}},"getTheme"),ZMt=o(()=>{let e=Ae()["wardley-beta"];return{width:e?.width??900,height:e?.height??600,padding:e?.padding??48,nodeRadius:e?.nodeRadius??6,nodeLabelOffset:e?.nodeLabelOffset??8,axisFontSize:e?.axisFontSize??12,labelFontSize:e?.labelFontSize??10,showGrid:e?.showGrid??!1,useMaxWidth:e?.useMaxWidth??!0}},"getConfigValues"),QMt=o((e,t,r,n)=>{Z.debug(`Rendering Wardley map +`+e);let i=ZMt(),a=KMt(),s=i.nodeRadius*1.6,l=n.db,u=l.getWardleyData(),h=l.getDiagramTitle(),d=u.size?.width??i.width,f=u.size?.height??i.height,p=xn(t);p.selectAll("*").remove(),Wr(p,f,d,i.useMaxWidth),p.attr("viewBox",`0 0 ${d} ${f}`);let m=p.append("g").attr("class","wardley-map"),g=p.append("defs");g.append("marker").attr("id",`arrow-${t}`).attr("viewBox","0 0 10 10").attr("refX",9).attr("refY",5).attr("markerWidth",6).attr("markerHeight",6).attr("orient","auto-start-reverse").append("path").attr("d","M 0 0 L 10 5 L 0 10 z").attr("fill",a.evolutionStroke).attr("stroke","none"),g.append("marker").attr("id",`link-arrow-end-${t}`).attr("viewBox","0 0 10 10").attr("refX",9).attr("refY",5).attr("markerWidth",5).attr("markerHeight",5).attr("orient","auto").append("path").attr("d","M 0 0 L 10 5 L 0 10 z").attr("fill",a.linkStroke).attr("stroke","none"),g.append("marker").attr("id",`link-arrow-start-${t}`).attr("viewBox","0 0 10 10").attr("refX",1).attr("refY",5).attr("markerWidth",5).attr("markerHeight",5).attr("orient","auto").append("path").attr("d","M 10 0 L 0 5 L 10 10 z").attr("fill",a.linkStroke).attr("stroke","none"),m.append("rect").attr("class","wardley-background").attr("width",d).attr("height",f).attr("fill",a.backgroundColor);let y=d-i.padding*2,v=f-i.padding*2;h&&m.append("text").attr("class","wardley-title").attr("x",d/2).attr("y",i.padding/2).attr("fill",a.axisTextColor).attr("font-size",i.axisFontSize*1.05).attr("font-weight","bold").attr("text-anchor","middle").attr("dominant-baseline","middle").text(h);let x=o(O=>i.padding+O/100*y,"projectX"),b=o(O=>f-i.padding-O/100*v,"projectY"),T=m.append("g").attr("class","wardley-axes");T.append("line").attr("x1",i.padding).attr("x2",d-i.padding).attr("y1",f-i.padding).attr("y2",f-i.padding).attr("stroke",a.axisColor).attr("stroke-width",1),T.append("line").attr("x1",i.padding).attr("x2",i.padding).attr("y1",i.padding).attr("y2",f-i.padding).attr("stroke",a.axisColor).attr("stroke-width",1);let k=u.axes.xLabel??"Evolution",C=u.axes.yLabel??"Visibility";T.append("text").attr("class","wardley-axis-label wardley-axis-label-x").attr("x",i.padding+y/2).attr("y",f-i.padding/4).attr("fill",a.axisTextColor).attr("font-size",i.axisFontSize).attr("font-weight","bold").attr("text-anchor","middle").text(k),T.append("text").attr("class","wardley-axis-label wardley-axis-label-y").attr("x",i.padding/3).attr("y",i.padding+v/2).attr("fill",a.axisTextColor).attr("font-size",i.axisFontSize).attr("font-weight","bold").attr("text-anchor","middle").attr("transform",`rotate(-90 ${i.padding/3} ${i.padding+v/2})`).text(C);let w=u.axes.stages&&u.axes.stages.length>0?u.axes.stages:XMt;if(w.length>0){let O=m.append("g").attr("class","wardley-stages"),$=u.axes.stageBoundaries,V=[];if($&&$.length===w.length){let G=0;$.forEach(z=>{V.push({start:G,end:z}),G=z})}else{let G=1/w.length;w.forEach((z,W)=>{V.push({start:W*G,end:(W+1)*G})})}w.forEach((G,z)=>{let W=V[z],H=i.padding+W.start*y,j=i.padding+W.end*y,Q=(H+j)/2;z>0&&O.append("line").attr("x1",H).attr("x2",H).attr("y1",i.padding).attr("y2",f-i.padding).attr("stroke","#000").attr("stroke-width",1).attr("stroke-dasharray","5 5").attr("opacity",.8),O.append("text").attr("class","wardley-stage-label").attr("x",Q).attr("y",f-i.padding/1.5).attr("fill",a.axisTextColor).attr("font-size",i.axisFontSize-2).attr("text-anchor","middle").text(G)})}if(i.showGrid){let O=m.append("g").attr("class","wardley-grid");for(let $=1;$<4;$++){let V=$/4,G=i.padding+y*V;O.append("line").attr("x1",G).attr("x2",G).attr("y1",i.padding).attr("y2",f-i.padding).attr("stroke",a.gridColor).attr("stroke-dasharray","2 6"),O.append("line").attr("x1",i.padding).attr("x2",d-i.padding).attr("y1",f-i.padding-v*V).attr("y2",f-i.padding-v*V).attr("stroke",a.gridColor).attr("stroke-dasharray","2 6")}}let S=new Map;if(u.nodes.forEach(O=>{S.set(O.id,{x:x(O.x),y:b(O.y),node:O})}),u.pipelines.length>0){let O=m.append("g").attr("class","wardley-pipelines"),$=m.append("g").attr("class","wardley-pipeline-links");u.pipelines.forEach(V=>{if(V.componentIds.length===0)return;let G=V.componentIds.map(j=>({id:j,pos:S.get(j),node:u.nodes.find(Q=>Q.id===j)})).filter(j=>j.pos&&j.node).sort((j,Q)=>j.node.x-Q.node.x);for(let j=0;j{let Q=S.get(j);Q&&(z=Math.min(z,Q.x),W=Math.max(W,Q.x),H=Q.y)}),z!==1/0&&W!==-1/0){let Q=i.nodeRadius*4,U=H-Q/2,oe=S.get(V.nodeId);if(oe){let te=(z+W)/2;oe.x=te,oe.y=U-s/6}O.append("rect").attr("class","wardley-pipeline-box").attr("x",z-15).attr("y",U).attr("width",W-z+30).attr("height",Q).attr("fill","none").attr("stroke",a.axisColor).attr("stroke-width",1.5).attr("rx",4).attr("ry",4)}})}let R=m.append("g").attr("class","wardley-links"),L=new Map;u.pipelines.forEach(O=>{L.set(O.nodeId,new Set(O.componentIds))});let N=u.links.filter(O=>!(!S.has(O.source)||!S.has(O.target)||L.get(O.target)?.has(O.source)));R.selectAll("line").data(N).enter().append("line").attr("class",O=>`wardley-link${O.dashed?" wardley-link--dashed":""}`).attr("x1",O=>{let $=S.get(O.source),V=S.get(O.target),z=u.nodes.find(Q=>Q.id===O.source).isPipelineParent?s/Math.sqrt(2):i.nodeRadius,W=V.x-$.x,H=V.y-$.y,j=Math.sqrt(W*W+H*H);return $.x+W/j*z}).attr("y1",O=>{let $=S.get(O.source),V=S.get(O.target),z=u.nodes.find(Q=>Q.id===O.source).isPipelineParent?s/Math.sqrt(2):i.nodeRadius,W=V.x-$.x,H=V.y-$.y,j=Math.sqrt(W*W+H*H);return $.y+H/j*z}).attr("x2",O=>{let $=S.get(O.source),V=S.get(O.target),z=u.nodes.find(Q=>Q.id===O.target).isPipelineParent?s/Math.sqrt(2):i.nodeRadius,W=$.x-V.x,H=$.y-V.y,j=Math.sqrt(W*W+H*H);return V.x+W/j*z}).attr("y2",O=>{let $=S.get(O.source),V=S.get(O.target),z=u.nodes.find(Q=>Q.id===O.target).isPipelineParent?s/Math.sqrt(2):i.nodeRadius,W=$.x-V.x,H=$.y-V.y,j=Math.sqrt(W*W+H*H);return V.y+H/j*z}).attr("stroke",a.linkStroke).attr("stroke-width",1).attr("stroke-dasharray",O=>O.dashed?"6 6":null).attr("marker-end",O=>O.flow==="forward"||O.flow==="bidirectional"?`url(#link-arrow-end-${t})`:null).attr("marker-start",O=>O.flow==="backward"||O.flow==="bidirectional"?`url(#link-arrow-start-${t})`:null),R.selectAll("text").data(N.filter(O=>O.label)).enter().append("text").attr("class","wardley-link-label").attr("x",O=>{let $=S.get(O.source),V=S.get(O.target),G=($.x+V.x)/2,z=V.y-$.y,W=V.x-$.x,H=Math.sqrt(W*W+z*z),j=8,Q=z/H;return G+Q*j}).attr("y",O=>{let $=S.get(O.source),V=S.get(O.target),G=($.y+V.y)/2,z=V.x-$.x,W=V.y-$.y,H=Math.sqrt(z*z+W*W),j=8,Q=-z/H;return G+Q*j}).attr("fill",a.axisTextColor).attr("font-size",i.labelFontSize).attr("text-anchor","middle").attr("dominant-baseline","middle").attr("transform",O=>{let $=S.get(O.source),V=S.get(O.target),G=($.x+V.x)/2,z=($.y+V.y)/2,W=V.x-$.x,H=V.y-$.y,j=Math.sqrt(W*W+H*H),Q=8,U=H/j,oe=-W/j,te=G+U*Q,le=z+oe*Q,ie=Math.atan2(H,W)*180/Math.PI;return(ie>90||ie<-90)&&(ie+=180),`rotate(${ie} ${te} ${le})`}).text(O=>O.label);let I=m.append("g").attr("class","wardley-trends"),_=u.trends.map(O=>{let $=S.get(O.nodeId);if(!$)return null;let V=x(O.targetX),G=b(O.targetY),z=V-$.x,W=G-$.y,H=Math.sqrt(z*z+W*W),j=i.nodeRadius+2,Q=H>j?V-z/H*j:V,U=H>j?G-W/H*j:G;return{origin:$,targetX:V,targetY:G,adjustedX2:Q,adjustedY2:U}}).filter(O=>O!==null);I.selectAll("line").data(_).enter().append("line").attr("class","wardley-trend").attr("x1",O=>O.origin.x).attr("y1",O=>O.origin.y).attr("x2",O=>O.adjustedX2).attr("y2",O=>O.adjustedY2).attr("stroke",a.evolutionStroke).attr("stroke-width",1).attr("stroke-dasharray","4 4").attr("marker-end",`url(#arrow-${t})`);let M=m.append("g").attr("class","wardley-nodes").selectAll("g").data(u.nodes).enter().append("g").attr("class",O=>["wardley-node",O.className?`wardley-node--${O.className}`:""].filter(Boolean).join(" "));M.filter(O=>O.sourceStrategy==="outsource").append("circle").attr("class","wardley-outsource-overlay").attr("cx",O=>S.get(O.id).x).attr("cy",O=>S.get(O.id).y).attr("r",i.nodeRadius*2).attr("fill","#666").attr("stroke",a.componentStroke).attr("stroke-width",1),M.filter(O=>O.sourceStrategy==="buy").append("circle").attr("class","wardley-buy-overlay").attr("cx",O=>S.get(O.id).x).attr("cy",O=>S.get(O.id).y).attr("r",i.nodeRadius*2).attr("fill","#ccc").attr("stroke",a.componentStroke).attr("stroke-width",1),M.filter(O=>O.sourceStrategy==="build").append("circle").attr("class","wardley-build-overlay").attr("cx",O=>S.get(O.id).x).attr("cy",O=>S.get(O.id).y).attr("r",i.nodeRadius*2).attr("fill","#eee").attr("stroke","#000").attr("stroke-width",1);let D=M.filter(O=>O.sourceStrategy==="market");D.append("circle").attr("class","wardley-market-overlay").attr("cx",O=>S.get(O.id).x).attr("cy",O=>S.get(O.id).y).attr("r",i.nodeRadius*2).attr("fill","white").attr("stroke",a.componentStroke).attr("stroke-width",1),M.filter(O=>!O.isPipelineParent&&O.sourceStrategy!=="market"&&O.className!=="anchor").append("circle").attr("cx",O=>S.get(O.id).x).attr("cy",O=>S.get(O.id).y).attr("r",i.nodeRadius).attr("fill",a.componentFill).attr("stroke",a.componentStroke).attr("stroke-width",1);let P=i.nodeRadius*.7,B=i.nodeRadius*1.2;if(D.append("line").attr("class","wardley-market-line").attr("x1",O=>S.get(O.id).x).attr("y1",O=>S.get(O.id).y-B).attr("x2",O=>S.get(O.id).x-B*Math.cos(Math.PI/6)).attr("y2",O=>S.get(O.id).y+B*Math.sin(Math.PI/6)).attr("stroke",a.componentStroke).attr("stroke-width",1),D.append("line").attr("class","wardley-market-line").attr("x1",O=>S.get(O.id).x-B*Math.cos(Math.PI/6)).attr("y1",O=>S.get(O.id).y+B*Math.sin(Math.PI/6)).attr("x2",O=>S.get(O.id).x+B*Math.cos(Math.PI/6)).attr("y2",O=>S.get(O.id).y+B*Math.sin(Math.PI/6)).attr("stroke",a.componentStroke).attr("stroke-width",1),D.append("line").attr("class","wardley-market-line").attr("x1",O=>S.get(O.id).x+B*Math.cos(Math.PI/6)).attr("y1",O=>S.get(O.id).y+B*Math.sin(Math.PI/6)).attr("x2",O=>S.get(O.id).x).attr("y2",O=>S.get(O.id).y-B).attr("stroke",a.componentStroke).attr("stroke-width",1),D.append("circle").attr("class","wardley-market-dot").attr("cx",O=>S.get(O.id).x).attr("cy",O=>S.get(O.id).y-B).attr("r",P).attr("fill","white").attr("stroke",a.componentStroke).attr("stroke-width",2),D.append("circle").attr("class","wardley-market-dot").attr("cx",O=>S.get(O.id).x-B*Math.cos(Math.PI/6)).attr("cy",O=>S.get(O.id).y+B*Math.sin(Math.PI/6)).attr("r",P).attr("fill","white").attr("stroke",a.componentStroke).attr("stroke-width",2),D.append("circle").attr("class","wardley-market-dot").attr("cx",O=>S.get(O.id).x+B*Math.cos(Math.PI/6)).attr("cy",O=>S.get(O.id).y+B*Math.sin(Math.PI/6)).attr("r",P).attr("fill","white").attr("stroke",a.componentStroke).attr("stroke-width",2),M.filter(O=>O.isPipelineParent===!0).append("rect").attr("x",O=>S.get(O.id).x-s/2).attr("y",O=>S.get(O.id).y-s/2).attr("width",s).attr("height",s).attr("fill",a.componentFill).attr("stroke",a.componentStroke).attr("stroke-width",1),M.filter(O=>O.inertia===!0).append("line").attr("class","wardley-inertia").attr("x1",O=>{let $=S.get(O.id),V=O.isPipelineParent?s/2+15:i.nodeRadius+15;return O.sourceStrategy&&(V+=i.nodeRadius+10),$.x+V}).attr("y1",O=>{let $=S.get(O.id),V=O.isPipelineParent?s:i.nodeRadius*2;return $.y-V/2}).attr("x2",O=>{let $=S.get(O.id),V=O.isPipelineParent?s/2+15:i.nodeRadius+15;return O.sourceStrategy&&(V+=i.nodeRadius+10),$.x+V}).attr("y2",O=>{let $=S.get(O.id),V=O.isPipelineParent?s:i.nodeRadius*2;return $.y+V/2}).attr("stroke",a.componentStroke).attr("stroke-width",6),M.append("text").attr("x",O=>{let $=S.get(O.id);if(O.className==="anchor")return O.labelOffsetX!==void 0?$.x+O.labelOffsetX:$.x;let V=i.nodeLabelOffset;O.sourceStrategy&&O.labelOffsetX===void 0&&(V+=10);let G=O.labelOffsetX??V;return $.x+G}).attr("y",O=>{let $=S.get(O.id);if(O.className==="anchor")return O.labelOffsetY!==void 0?$.y+O.labelOffsetY:$.y-3;let V=-i.nodeLabelOffset;O.sourceStrategy&&O.labelOffsetY===void 0&&(V-=10);let G=O.labelOffsetY??V;return $.y+G}).attr("class","wardley-node-label").attr("fill",O=>O.className==="evolved"?a.evolutionStroke:O.className==="anchor"?"#000":a.componentLabelColor).attr("font-size",i.labelFontSize).attr("font-weight",O=>O.className==="anchor"?"bold":"normal").attr("text-anchor",O=>O.className==="anchor"?"middle":"start").attr("dominant-baseline",O=>O.className==="anchor"?"middle":"auto").text(O=>O.label),u.annotations.length>0){let O=m.append("g").attr("class","wardley-annotations");if(u.annotations.forEach($=>{let V=$.coordinates.map(G=>({x:x(G.x),y:b(G.y)}));if(V.length>1)for(let G=0;G{let z=O.append("g").attr("class","wardley-annotation");z.append("circle").attr("cx",G.x).attr("cy",G.y).attr("r",10).attr("fill","white").attr("stroke",a.axisColor).attr("stroke-width",1.5),z.append("text").attr("x",G.x).attr("y",G.y).attr("text-anchor","middle").attr("dominant-baseline","central").attr("font-size",10).attr("fill",a.axisTextColor).attr("font-weight","bold").text($.number)})}),u.annotationsBox){let $=x(u.annotationsBox.x),V=b(u.annotationsBox.y),G=10,z=16,W=11,H=O.append("g").attr("class","wardley-annotations-box"),j=[...u.annotations].filter(U=>U.text).sort((U,oe)=>U.number-oe.number),Q=[];if(j.forEach((U,oe)=>{let te=H.append("text").attr("x",$+G).attr("y",V+G+(oe+1)*z).attr("font-size",W).attr("fill",a.axisTextColor).attr("text-anchor","start").attr("dominant-baseline","middle").text(`${U.number}. ${U.text}`);Q.push(te)}),Q.length>0){let U=0,oe=0;Q.forEach(Pe=>{let Ge=Pe.node(),Oe=Ge.getComputedTextLength();U=Math.max(U,Oe);let ue=Ge.getBBox();oe=Math.max(oe,ue.height)});let te=U+G*2+105,le=j.length*z+G*2+oe/2,ie=i.padding,ae=d-i.padding-te,Re=i.padding,be=f-i.padding-le;$=Math.max(ie,Math.min($,ae)),V=Math.max(Re,Math.min(V,be)),Q.forEach((Pe,Ge)=>{Pe.attr("x",$+G).attr("y",V+G+(Ge+1)*z)}),H.insert("rect","text").attr("x",$).attr("y",V).attr("width",te).attr("height",le).attr("fill","white").attr("stroke",a.axisColor).attr("stroke-width",1.5).attr("rx",4).attr("ry",4)}}}if(u.notes.length>0){let O=m.append("g").attr("class","wardley-notes");u.notes.forEach($=>{let V=x($.x),G=b($.y);O.append("text").attr("x",V).attr("y",G).attr("text-anchor","start").attr("font-size",11).attr("fill",a.axisTextColor).attr("font-weight","bold").text($.text)})}if(u.accelerators.length>0){let O=m.append("g").attr("class","wardley-accelerators");u.accelerators.forEach($=>{let V=x($.x),G=b($.y),z=60,W=30,H=20,j=` + M ${V} ${G-W/2} + L ${V+z-H} ${G-W/2} + L ${V+z-H} ${G-W/2-8} + L ${V+z} ${G} + L ${V+z-H} ${G+W/2+8} + L ${V+z-H} ${G+W/2} + L ${V} ${G+W/2} + Z + `;O.append("path").attr("d",j).attr("fill","white").attr("stroke",a.componentStroke).attr("stroke-width",1),O.append("text").attr("x",V+z/2).attr("y",G+W/2+15).attr("text-anchor","middle").attr("font-size",10).attr("fill",a.axisTextColor).attr("font-weight","bold").text($.name)})}if(u.deaccelerators.length>0){let O=m.append("g").attr("class","wardley-deaccelerators");u.deaccelerators.forEach($=>{let V=x($.x),G=b($.y),z=60,W=30,H=20,j=` + M ${V+z} ${G-W/2} + L ${V+H} ${G-W/2} + L ${V+H} ${G-W/2-8} + L ${V} ${G} + L ${V+H} ${G+W/2+8} + L ${V+H} ${G+W/2} + L ${V+z} ${G+W/2} + Z + `;O.append("path").attr("d",j).attr("fill","white").attr("stroke",a.componentStroke).attr("stroke-width",1),O.append("text").attr("x",V+z/2).attr("y",G+W/2+15).attr("text-anchor","middle").attr("font-size",10).attr("fill",a.axisTextColor).attr("font-weight","bold").text($.name)})}},"draw"),p$e={draw:QMt}});var g$e,y$e=F(()=>{"use strict";Qt();Pc();ur();g$e=o(({wardley:e}={})=>{let t=ma(),r=_t(),n=qr(t,r.themeVariables),i=qr(n.wardley,e);return` + .wardley-background { + fill: ${i.backgroundColor}; + } + .wardley-axes line, .wardley-axes path { + stroke: ${i.axisColor}; + } + .wardley-axis-label { + fill: ${i.axisTextColor}; + } + .wardley-stage-label { + fill: ${i.axisTextColor}; + } + .wardley-grid line { + stroke: ${i.gridColor}; + } + .wardley-node circle { + fill: ${i.componentFill}; + stroke: ${i.componentStroke}; + } + .wardley-node-label { + fill: ${i.componentLabelColor}; + } + .wardley-link { + stroke: ${i.linkStroke}; + } + .wardley-link--dashed { + stroke-dasharray: 4 4; + } + .wardley-link-label { + fill: ${i.axisTextColor}; + } + .wardley-trend line { + stroke: ${i.evolutionStroke}; + } + .wardley-annotation-line { + stroke: ${i.annotationStroke}; + } + .wardley-annotation circle { + fill: ${i.annotationFill}; + stroke: ${i.annotationStroke}; + } + .wardley-annotation text { + fill: ${i.annotationTextColor}; + } + .wardley-annotations-box rect { + fill: ${i.annotationFill}; + stroke: ${i.annotationStroke}; + } + .wardley-annotations-box text { + fill: ${i.annotationTextColor}; + } + .wardley-pipeline-box { + stroke: ${i.componentStroke}; + } + .wardley-notes text { + fill: ${i.axisTextColor}; + } + `},"styles")});var v$e={};ir(v$e,{diagram:()=>JMt});var JMt,x$e=F(()=>{"use strict";u$e();f$e();m$e();y$e();JMt={parser:SX,db:d$e,renderer:p$e,styles:g$e}});var C$e,lk,nNt,iNt,aNt,sNt,oNt,lNt,wx,EX=F(()=>{"use strict";ur();Wi();vt();Qt();Nn();C$e=o(()=>({domains:new Map,transitions:[]}),"createDefaultData"),lk=C$e(),nNt=o(()=>lk.domains,"getDomains"),iNt=o(()=>lk.transitions,"getTransitions"),aNt=o(e=>{if(e)for(let t of e){let r=t.domain,n=(t.items??[]).map(i=>({label:i.label}));lk.domains.set(r,{name:r,items:n})}},"setDomains"),sNt=o(e=>{e&&(lk.transitions=e.filter(t=>t.from===t.to?(Z.warn(`Cynefin: self-loop transition on domain "${t.from}" is not meaningful and will be skipped.`),!1):!0).map(t=>({from:t.from,to:t.to,label:t.label||void 0})))},"setTransitions"),oNt=o(()=>qr({...cr.cynefin,..._t().cynefin}),"getConfig"),lNt=o(()=>{yr(),lk=C$e()},"clear"),wx={getDomains:nNt,getTransitions:iNt,setDomains:aNt,setTransitions:sNt,getConfig:oNt,clear:lNt,setAccTitle:kr,getAccTitle:Ar,setDiagramTitle:Or,getDiagramTitle:Lr,getAccDescription:_r,setAccDescription:Rr}});var cNt,w$e,k$e=F(()=>{"use strict";Xa();vt();Hs();EX();cNt=o(e=>{Gn(e,wx),wx.setDomains(e.domains),wx.setTransitions(e.transitions)},"populate"),w$e={parse:o(async e=>{let t=await Si("cynefin",e);Z.debug(t),cNt(t)},"parse")}});function pD(e){let t=e+1831565813|0;return t=Math.imul(t^t>>>15,t|1),t^=t+Math.imul(t^t>>>7,t|61),((t^t>>>14)>>>0)/4294967296}function uNt(e){let t=0;for(let r=0;r{"use strict";o(pD,"seededRandom");o(uNt,"hashString");o(S$e,"resolveSeed");o(E$e,"generateFoldPath");o(A$e,"generateHorizontalBoundary");o(R$e,"generateCliffPath");o(_$e,"generateConfusionPath")});var D$e,hNt,dNt,AX,fNt,I$e,M$e=F(()=>{"use strict";Ka();$n();vt();ur();Pc();Qt();L$e();D$e={complex:{model:"Probe \u2192 Sense \u2192 Respond",practice:"Emergent Practices"},complicated:{model:"Sense \u2192 Analyse \u2192 Respond",practice:"Good Practices"},clear:{model:"Sense \u2192 Categorise \u2192 Respond",practice:"Best Practices"},chaotic:{model:"Act \u2192 Sense \u2192 Respond",practice:"Novel Practices"},confusion:{model:"",practice:"Disorder"}},hNt=o((e,t)=>{let r=e/2,n=t/2;return{complex:{cx:r/2,cy:n/2,x:0,y:0,w:r,h:n},complicated:{cx:r+r/2,cy:n/2,x:r,y:0,w:r,h:n},chaotic:{cx:r/2,cy:n+n/2,x:0,y:n,w:r,h:n},clear:{cx:r+r/2,cy:n+n/2,x:r,y:n,w:r,h:n},confusion:{cx:r,cy:n,x:r*.7,y:n*.7,w:r*.6,h:n*.6}}},"getDomainLayouts"),dNt=o(()=>{let e=ma(),t=_t();return qr(e,t.themeVariables).cynefin},"getCynefinDomainColors"),AX=3,fNt=o((e,t,r,n)=>{let i=n.db,a=i.getDomains(),s=i.getTransitions(),l=i.getDiagramTitle(),u=i.getAccTitle(),h=i.getAccDescription(),d=i.getConfig(),f=dNt();Z.debug("Rendering Cynefin diagram");let p=d.width,m=d.height,g=d.padding,y=d.showDomainDescriptions,v=d.boundaryAmplitude,x=p+g*2,b=m+g*2,T={complex:f.complexBg,complicated:f.complicatedBg,clear:f.clearBg,chaotic:f.chaoticBg,confusion:f.confusionBg},k=xn(t);Wr(k,b,x,d.useMaxWidth??!0),k.attr("viewBox",`0 0 ${x} ${b}`),u&&k.append("title").text(u),h&&k.append("desc").text(h);let C=k.append("g").attr("transform",`translate(${g}, ${g})`),w=hNt(p,m),S=S$e(d.seed,t),R=C.append("g").attr("class","cynefin-backgrounds"),L=["complex","complicated","chaotic","clear"];for(let O of L){let $=w[O];R.append("rect").attr("class","cynefinDomain").attr("x",$.x).attr("y",$.y).attr("width",$.w).attr("height",$.h).attr("fill",T[O]).attr("fill-opacity",.4).attr("stroke","none")}let N=C.append("g").attr("class","cynefin-boundaries");N.append("path").attr("class","cynefinBoundary").attr("d",E$e(p,m,S,v)).attr("fill","none"),N.append("path").attr("class","cynefinBoundary").attr("d",A$e(p,m,S+100,v)).attr("fill","none"),N.append("path").attr("class","cynefinCliff").attr("d",R$e(p,m)).attr("fill","none");let I=p*.15,_=m*.15;C.append("path").attr("class","cynefinConfusion").attr("d",_$e(p/2,m/2,I,_)).attr("fill",T.confusion).attr("fill-opacity",.5);let A=C.append("g").attr("class","cynefin-labels");for(let O of L){let $=w[O];A.append("text").attr("class","cynefinDomainLabel").attr("x",$.cx).attr("y",y?$.cy-30:$.cy).attr("text-anchor","middle").attr("dominant-baseline","middle").text(O.charAt(0).toUpperCase()+O.slice(1))}if(A.append("text").attr("class","cynefinDomainLabel").attr("x",p/2).attr("y",y?m/2-10:m/2).attr("text-anchor","middle").attr("dominant-baseline","middle").text("Confusion"),y){let O=C.append("g").attr("class","cynefin-subtitles");for(let $ of L){let V=w[$],G=D$e[$];O.append("text").attr("class","cynefinSubtitle").attr("x",V.cx).attr("y",V.cy-10).attr("text-anchor","middle").attr("dominant-baseline","middle").text(G.model),O.append("text").attr("class","cynefinSubtitle").attr("x",V.cx).attr("y",V.cy+5).attr("text-anchor","middle").attr("dominant-baseline","middle").text(G.practice)}O.append("text").attr("class","cynefinSubtitle").attr("x",p/2).attr("y",m/2+8).attr("text-anchor","middle").attr("dominant-baseline","middle").text(D$e.confusion.practice)}let M=C.append("g").attr("class","cynefin-items"),D=26,P=10,B=["complex","complicated","chaotic","clear","confusion"];for(let O of B){let $=a.get(O);if(!$||$.items.length===0)continue;let V=w[O],G=O==="confusion",z=$.items,W=0;G&&$.items.length>AX&&(W=$.items.length-AX,z=$.items.slice(0,AX));let H;if(G){let j=y?22:14;H=V.cy+j}else H=V.cy+(y?25:15);if([...z].forEach((j,Q)=>{let U=H+Q*(D+4),oe=M.append("g"),te=oe.append("text").attr("class","cynefinItemText").attr("x",0).attr("y",D/2).attr("text-anchor","middle").attr("dominant-baseline","central").text(j.label),le=j.label.length*7,ie=te.node();if(ie&&typeof ie.getBBox=="function"){let be=ie.getBBox();be.width>0&&(le=be.width)}let ae=le+P*2,Re=V.cx-ae/2;oe.attr("transform",`translate(${Re}, ${U})`),oe.insert("rect","text").attr("class","cynefinItem").attr("x",0).attr("y",0).attr("width",ae).attr("height",D).attr("rx",4).attr("ry",4).attr("fill",T[O]).attr("fill-opacity",.95),te.attr("x",ae/2).attr("y",D/2)}),W>0){let j=H+z.length*(D+4),Q=`+${W} more`,U=M.append("g"),oe=U.append("text").attr("class","cynefinItemText").attr("x",0).attr("y",D/2).attr("text-anchor","middle").attr("dominant-baseline","central").text(Q),te=Q.length*7,le=oe.node();if(le&&typeof le.getBBox=="function"){let Re=le.getBBox();Re.width>0&&(te=Re.width)}let ie=te+P*2,ae=V.cx-ie/2;U.attr("transform",`translate(${ae}, ${j})`),U.insert("rect","text").attr("class","cynefinItemOverflow").attr("x",0).attr("y",0).attr("width",ie).attr("height",D).attr("rx",4).attr("ry",4).attr("fill",T[O]).attr("fill-opacity",.6),oe.attr("x",ie/2).attr("y",D/2)}}if(s.length>0){let O=k.select("defs").empty()?k.append("defs"):k.select("defs"),$=`cynefin-arrow-${t}`;O.append("marker").attr("id",$).attr("viewBox","0 0 10 10").attr("refX",9).attr("refY",5).attr("markerWidth",6).attr("markerHeight",6).attr("orient","auto-start-reverse").append("path").attr("d","M 0 0 L 10 5 L 0 10 z").attr("class","cynefinArrowHead");let V=C.append("g").attr("class","cynefin-arrows");s.forEach(G=>{let z=w[G.from],W=w[G.to];if(!z||!W)return;if(G.from===G.to){Z.warn(`Cynefin renderer: skipping self-loop on domain "${G.from}"`);return}let H=z.cx,j=z.cy,Q=W.cx,U=W.cy,oe=(H+Q)/2,te=(j+U)/2,le=Q-H,ie=U-j,ae=Math.sqrt(le*le+ie*ie),Re=ae*.15,be=-ie/ae,Pe=le/ae,Ge=oe+be*Re,Oe=te+Pe*Re;V.append("path").attr("class","cynefinArrowLine").attr("d",`M${H},${j} Q${Ge},${Oe} ${Q},${U}`).attr("fill","none").attr("marker-end",`url(#${$})`),G.label&&V.append("text").attr("class","cynefinArrowLabel").attr("x",Ge).attr("y",Oe-6).attr("text-anchor","middle").attr("dominant-baseline","auto").text(G.label)})}l&&C.append("text").attr("class","cynefinTitle").attr("x",p/2).attr("y",-g/2).attr("text-anchor","middle").attr("dominant-baseline","middle").text(l)},"draw"),I$e={draw:fNt}});var pNt,mNt,N$e,P$e=F(()=>{"use strict";Qt();Pc();ur();pNt=o(()=>{let e=ma(),t=_t();return qr(e,t.themeVariables).cynefin},"getCynefinTheme"),mNt=o(()=>{let e=pNt();return` + .cynefinDomain { + stroke: none; + } + .cynefinDomainLabel { + font-size: ${e.domainFontSize}px; + font-weight: bold; + fill: ${e.labelColor}; + } + .cynefinSubtitle { + font-size: ${e.itemFontSize-1}px; + fill: ${e.textColor}; + font-style: italic; + } + .cynefinItem { + fill-opacity: 0.95; + stroke: ${e.boundaryColor}; + stroke-width: 1; + } + .cynefinItemText { + font-size: ${e.itemFontSize}px; + fill: ${e.textColor}; + } + .cynefinItemOverflow { + fill-opacity: 0.6; + stroke: ${e.boundaryColor}; + stroke-width: 1; + stroke-dasharray: 3 2; + } + .cynefinBoundary { + stroke: ${e.boundaryColor}; + stroke-width: ${e.boundaryWidth}; + stroke-dasharray: 6 3; + } + .cynefinCliff { + stroke: ${e.cliffColor}; + stroke-width: ${e.cliffWidth}; + } + .cynefinConfusion { + stroke: ${e.boundaryColor}; + stroke-width: 1.5; + stroke-dasharray: 4 2; + } + .cynefinArrowLine { + stroke: ${e.arrowColor}; + stroke-width: ${e.arrowWidth}; + fill: none; + } + .cynefinArrowHead { + fill: ${e.arrowColor}; + stroke: none; + } + .cynefinArrowLabel { + font-size: ${e.itemFontSize-1}px; + fill: ${e.textColor}; + } + .cynefinTitle { + font-size: ${e.domainFontSize+2}px; + font-weight: bold; + fill: ${e.labelColor}; + } + `},"styles"),N$e=mNt});var O$e={};ir(O$e,{diagram:()=>gNt});var gNt,B$e=F(()=>{"use strict";k$e();EX();M$e();P$e();gNt={parser:w$e,db:wx,renderer:I$e,styles:N$e}});var RX,_X,LX,DX,mD,Mp,kx,xNt,z$e,G$e,bNt,TNt,CNt,wNt,kNt,SNt,ENt,ANt,RNt,sn,Xu=F(()=>{"use strict";Xt();vt();Nn();Vr();RX="",_X="",LX="",DX=[],mD=new Map,Mp=o(e=>mr(e,Ae()),"sanitizeText"),kx=o(e=>{switch(e.type){case"terminal":return{...e,value:Mp(e.value)};case"nonterminal":return{...e,name:Mp(e.name)};case"sequence":return{...e,elements:e.elements.map(kx)};case"choice":return{...e,alternatives:e.alternatives.map(kx)};case"optional":return{...e,element:kx(e.element)};case"repetition":return{...e,element:kx(e.element),separator:e.separator?kx(e.separator):void 0};case"special":return{...e,text:Mp(e.text)}}},"sanitizeAstNode"),xNt=o(()=>{RX="",_X="",LX="",DX.length=0,mD.clear(),yr(),Z.debug("[Railroad] Database cleared")},"clear"),z$e=o(e=>{RX=Mp(e),Z.debug("[Railroad] Title set:",e)},"setTitle"),G$e=o(()=>RX,"getTitle"),bNt=o(e=>{let t={...e,name:Mp(e.name),definition:kx(e.definition),comment:e.comment?Mp(e.comment):void 0};Z.debug("[Railroad] Adding rule:",t.name),mD.has(t.name)&&Z.warn(`[Railroad] Rule '${t.name}' is already defined. Overwriting.`),DX.push(t),mD.set(t.name,t)},"addRule"),TNt=o(()=>DX,"getRules"),CNt=o(e=>mD.get(e),"getRule"),wNt=o(e=>{_X=Mp(e).replace(/^\s+/g,""),Z.debug("[Railroad] Accessibility title set:",e)},"setAccTitle"),kNt=o(()=>_X,"getAccTitle"),SNt=o(e=>{LX=Mp(e).replace(/\n\s+/g,` +`),Z.debug("[Railroad] Accessibility description set:",e)},"setAccDescription"),ENt=o(()=>LX,"getAccDescription"),ANt=z$e,RNt=G$e,sn={clear:xNt,setTitle:z$e,getTitle:G$e,addRule:bNt,getRules:TNt,getRule:CNt,setAccTitle:wNt,getAccTitle:kNt,setAccDescription:SNt,getAccDescription:ENt,setDiagramTitle:ANt,getDiagramTitle:RNt}});var _Nt,Sx,LNt,DNt,V$e,W$e=F(()=>{"use strict";Xa();vt();Hs();Xu();_Nt=Vv().Railroad.parser.LangiumParser,Sx=o(e=>{switch(e.$type){case"RailroadTerminalExpr":return{type:"terminal",value:e.value};case"RailroadNonTerminalExpr":return{type:"nonterminal",name:e.name};case"RailroadSpecialExpr":return{type:"special",text:e.text};case"RailroadSequenceExpr":{let t=e.elements.map(Sx);return t.length===1?t[0]:{type:"sequence",elements:t}}case"RailroadChoiceExpr":{let t=e.alternatives.map(Sx);return t.length===1?t[0]:{type:"choice",alternatives:t}}case"RailroadOptionalExpr":return{type:"optional",element:Sx(e.element)};case"RailroadOneOrMoreExpr":return{type:"repetition",element:Sx(e.element),min:1,max:1/0};case"RailroadZeroOrMoreExpr":return{type:"repetition",element:Sx(e.element),min:0,max:1/0};default:throw new Error(`Unsupported railroad expression: ${e.$type}`)}},"transformExpression"),LNt=o(e=>({name:e.name,definition:Sx(e.definition)}),"transformRule"),DNt=o(e=>{Gn(e,sn),e.title&&sn.setTitle(e.title),e.rules.map(t=>sn.addRule(LNt(t)))},"populateDb"),V$e={parse:o(e=>{sn.clear(),Z.debug("[Railroad Parser] Starting Langium parse");let t=_Nt.parse(e);if(t.lexerErrors.length>0||t.parserErrors.length>0)throw new sd(t);let r=t.value;Z.debug("[Railroad Parser] Parsed rules:",r.rules.length),DNt(r),Z.debug("[Railroad Parser] Parse complete")},"parse"),parser:{yy:sn}}});var Na,q$e=F(()=>{"use strict";Na={compactMode:!1,padding:10,verticalSeparation:8,horizontalSeparation:10,arcRadius:10,fontSize:14,fontFamily:"monospace",terminalFill:"#FFFFC0",terminalStroke:"#000000",terminalTextColor:"#000000",nonTerminalFill:"#FFFFFF",nonTerminalStroke:"#000000",nonTerminalTextColor:"#000000",lineColor:"#000000",strokeWidth:2,markerFill:"#000000",commentFill:"#E8E8E8",commentStroke:"#888888",commentTextColor:"#666666",specialFill:"#F0E0FF",specialStroke:"#8800CC",ruleNameColor:"#000066",showMarkers:!0,markerRadius:5}});var INt,MNt,NNt,H$e,PNt,ONt,Vn,U$e,O0,BNt,$Nt,gD,Np,Ex=F(()=>{"use strict";ur();Pc();q$e();INt=/^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$|^(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\([\d\s%+,./-]+\)$|^[a-z]+$/i,MNt=/^[\w "',.-]+$/,NNt=new Set(["compactMode","padding","verticalSeparation","horizontalSeparation","arcRadius","fontSize","fontFamily","terminalFill","terminalStroke","terminalTextColor","nonTerminalFill","nonTerminalStroke","nonTerminalTextColor","lineColor","strokeWidth","markerFill","commentFill","commentStroke","commentTextColor","specialFill","specialStroke","ruleNameColor","showMarkers","markerRadius"]),H$e=o(e=>e?Object.keys(e).every(t=>t==="railroad"||NNt.has(t)):!1,"isRailroadStyleOptions"),PNt=o(e=>e?"railroad"in e&&e.railroad?e.railroad:H$e(e)?e:{}:{},"extractRailroadOverrides"),ONt=o(e=>{if(!e||H$e(e))return{};let{railroad:t,svgId:r,theme:n,look:i,...a}=e;return a},"extractThemeOverrides"),Vn=o((e,t)=>{if(typeof e!="string")return t;let r=e.trim();return INt.test(r)?r:t},"sanitizeColorValue"),U$e=o((e,t)=>{if(typeof e!="string")return t;let r=e.trim();return MNt.test(r)?r:t},"sanitizeFontFamilyValue"),O0=o((e,t)=>{let r=typeof e=="number"?e:typeof e=="string"?Number.parseFloat(e):Number.NaN;return Number.isFinite(r)&&r>=0?r:t},"sanitizeNumberValue"),BNt=o(e=>{let t=typeof e=="number"?e:typeof e=="string"?Number.parseFloat(e):Number.NaN;return Number.isFinite(t)&&t>0?t:void 0},"parseThemeFontSize"),$Nt=o(e=>{let t=U$e(e.fontFamily,Na.fontFamily),r=BNt(e.fontSize)??Na.fontSize;return{...Na,fontFamily:t,fontSize:r,terminalFill:Vn(e.secondBkg??e.secondaryColor,Na.terminalFill),terminalStroke:Vn(e.secondaryBorderColor??e.lineColor,Na.terminalStroke),terminalTextColor:Vn(e.secondaryTextColor??e.textColor,Na.terminalTextColor),nonTerminalFill:Vn(e.mainBkg??e.background,Na.nonTerminalFill),nonTerminalStroke:Vn(e.primaryBorderColor??e.lineColor,Na.nonTerminalStroke),nonTerminalTextColor:Vn(e.primaryTextColor??e.textColor,Na.nonTerminalTextColor),lineColor:Vn(e.lineColor,Na.lineColor),markerFill:Vn(e.lineColor,Na.markerFill),commentFill:Vn(e.labelBackground??e.tertiaryColor,Na.commentFill),commentStroke:Vn(e.tertiaryBorderColor??e.lineColor,Na.commentStroke),commentTextColor:Vn(e.tertiaryTextColor??e.textColor,Na.commentTextColor),specialFill:Vn(e.tertiaryColor??e.secondaryColor,Na.specialFill),specialStroke:Vn(e.tertiaryBorderColor??e.secondaryBorderColor,Na.specialStroke),ruleNameColor:Vn(e.titleColor??e.textColor,Na.ruleNameColor)}},"buildThemeDefaults"),gD=o(e=>{let t=_t(),r={...ma(),...t.themeVariables??{},...ONt(e)},n=$Nt(r),i={...t.railroad??{},...PNt(e)};return{compactMode:i.compactMode??n.compactMode,padding:O0(i.padding,n.padding),verticalSeparation:O0(i.verticalSeparation,n.verticalSeparation),horizontalSeparation:O0(i.horizontalSeparation,n.horizontalSeparation),arcRadius:O0(i.arcRadius,n.arcRadius),fontSize:O0(i.fontSize,n.fontSize),fontFamily:U$e(i.fontFamily,n.fontFamily),terminalFill:Vn(i.terminalFill,n.terminalFill),terminalStroke:Vn(i.terminalStroke,n.terminalStroke),terminalTextColor:Vn(i.terminalTextColor,n.terminalTextColor),nonTerminalFill:Vn(i.nonTerminalFill,n.nonTerminalFill),nonTerminalStroke:Vn(i.nonTerminalStroke,n.nonTerminalStroke),nonTerminalTextColor:Vn(i.nonTerminalTextColor,n.nonTerminalTextColor),lineColor:Vn(i.lineColor,n.lineColor),strokeWidth:O0(i.strokeWidth,n.strokeWidth),markerFill:Vn(i.markerFill,n.markerFill),commentFill:Vn(i.commentFill,n.commentFill),commentStroke:Vn(i.commentStroke,n.commentStroke),commentTextColor:Vn(i.commentTextColor,n.commentTextColor),specialFill:Vn(i.specialFill,n.specialFill),specialStroke:Vn(i.specialStroke,n.specialStroke),ruleNameColor:Vn(i.ruleNameColor,n.ruleNameColor),showMarkers:i.showMarkers??n.showMarkers,markerRadius:O0(i.markerRadius,n.markerRadius)}},"buildRailroadStyleOptions"),Np=o(e=>{let{fontFamily:t,fontSize:r,terminalFill:n,terminalStroke:i,terminalTextColor:a,nonTerminalFill:s,nonTerminalStroke:l,nonTerminalTextColor:u,lineColor:h,strokeWidth:d,markerFill:f,commentFill:p,commentStroke:m,commentTextColor:g,specialFill:y,specialStroke:v,ruleNameColor:x}=gD(e);return` + .railroad-diagram { + font-family: ${t}; + font-size: ${r}px; + } + + .railroad-terminal rect { + fill: ${n}; + stroke: ${i}; + stroke-width: ${d}px; + } + + .railroad-terminal text { + fill: ${a}; + font-family: ${t}; + font-size: ${r}px; + text-anchor: middle; + dominant-baseline: middle; + } + + .railroad-nonterminal rect { + fill: ${s}; + stroke: ${l}; + stroke-width: ${d}px; + } + + .railroad-nonterminal text { + fill: ${u}; + font-family: ${t}; + font-size: ${r}px; + text-anchor: middle; + dominant-baseline: middle; + } + + .railroad-line { + stroke: ${h}; + stroke-width: ${d}px; + fill: none; + } + + .railroad-start circle, + .railroad-end circle { + fill: ${f}; + } + + .railroad-comment ellipse { + fill: ${p}; + stroke: ${m}; + stroke-width: ${d}px; + } + + .railroad-comment text { + fill: ${g}; + font-style: italic; + font-family: ${t}; + font-size: ${r}px; + text-anchor: middle; + dominant-baseline: middle; + } + + .railroad-special rect { + fill: ${y}; + stroke: ${v}; + stroke-width: ${d}px; + stroke-dasharray: 5,3; + } + + .railroad-special text { + fill: ${u}; + font-family: ${t}; + font-size: ${r}px; + text-anchor: middle; + dominant-baseline: middle; + } + + .railroad-rule-name { + font-weight: bold; + fill: ${x}; + font-family: ${t}; + font-size: ${r}px; + } + + .railroad-group { + /* Grouping container, no specific styles */ + } +`},"getStyles")});var _o,IX,Y$e,FNt,Pp,ck=F(()=>{"use strict";vt();ur();Ka();$n();Xu();Ex();_o=class{constructor(){this.d=""}static{o(this,"PathBuilder")}moveTo(t,r){return this.d+=`M ${t} ${r} `,this}lineTo(t,r){return this.d+=`L ${t} ${r} `,this}horizontalTo(t){return this.d+=`H ${t} `,this}verticalTo(t){return this.d+=`V ${t} `,this}arcTo(t,r,n,i,a,s,l){return this.d+=`A ${t} ${r} ${n} ${i?1:0} ${a?1:0} ${s} ${l} `,this}build(){return this.d.trim()}},IX=class{constructor(t,r=gD()){this.textCache=new Map;this.svg=t,this.config=r}static{o(this,"RailroadRenderer")}measureText(t){if(this.textCache.has(t))return this.textCache.get(t);let r=this.svg.append("text").attr("font-family",this.config.fontFamily).attr("font-size",this.config.fontSize).text(t),n=r.node().getBBox(),i={width:n.width,height:n.height};return r.remove(),this.textCache.set(t,i),i}renderTerminal(t,r){let n=this.measureText(r),i=n.width+this.config.padding*2,a=n.height+this.config.padding*2,s=t.append("g").attr("class","railroad-terminal");return s.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",a).attr("rx",10).attr("ry",10),s.append("text").attr("x",i/2).attr("y",a/2).text(r),{element:s.node(),dimensions:{width:i,height:a,up:a/2,down:a/2}}}renderNonTerminal(t,r){let n=this.measureText(r),i=n.width+this.config.padding*2,a=n.height+this.config.padding*2,s=t.append("g").attr("class","railroad-nonterminal");return s.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",a),s.append("text").attr("x",i/2).attr("y",a/2).text(r),{element:s.node(),dimensions:{width:i,height:a,up:a/2,down:a/2}}}renderSequence(t,r){let n=r.map(h=>this.renderExpression(t,h)),i=0,a=0,s=0;for(let h of n)i+=h.dimensions.width,a=Math.max(a,h.dimensions.up),s=Math.max(s,h.dimensions.down);i+=(n.length-1)*this.config.horizontalSeparation;let l=t.append("g").attr("class","railroad-sequence"),u=0;for(let h=0;hthis.renderExpression(t,p)),i=0,a=0;for(let p of n)i=Math.max(i,p.dimensions.width),a+=p.dimensions.height;a+=(n.length-1)*this.config.verticalSeparation;let s=this.config.arcRadius,l=s*4,u=i+l,h=t.append("g").attr("class","railroad-choice"),d=0,f=a/2;for(let p of n){let m=d,g=m+p.dimensions.up,y=s*2+(i-p.dimensions.width)/2;h.node().appendChild(p.element).setAttribute("transform",`translate(${y}, ${m})`);let x=new _o,b=g>f;g===f?x.moveTo(0,f).lineTo(y,g):x.moveTo(0,f).arcTo(s,s,0,!1,b,s,f+(b?s:-s)).lineTo(s,g-(b?s:-s)).arcTo(s,s,0,!1,!b,s*2,g).lineTo(y,g),h.append("path").attr("class","railroad-line").attr("d",x.build());let T=new _o,k=y+p.dimensions.width,C=u-s*2;g===f?T.moveTo(k,g).lineTo(u,f):T.moveTo(k,g).lineTo(C,g).arcTo(s,s,0,!1,!b,u-s,g+(b?-s:s)).lineTo(u-s,f+(b?s:-s)).arcTo(s,s,0,!1,b,u,f),h.append("path").attr("class","railroad-line").attr("d",T.build()),d+=p.dimensions.height+this.config.verticalSeparation}return{element:h.node(),dimensions:{width:u,height:a,up:f,down:a-f}}}renderOptional(t,r){let n=this.renderExpression(t,r),i=this.config.arcRadius,a=i*2,s=n.dimensions.width+i*4,l=n.dimensions.height+a,u=t.append("g").attr("class","railroad-optional"),h=i*2,d=a;u.node().appendChild(n.element).setAttribute("transform",`translate(${h}, ${d})`);let p=d+n.dimensions.up,m=new _o().moveTo(0,p).lineTo(i*2,p);u.append("path").attr("class","railroad-line").attr("d",m.build());let g=new _o().moveTo(h+n.dimensions.width,p).lineTo(s,p);u.append("path").attr("class","railroad-line").attr("d",g.build());let y=new _o().moveTo(0,p).arcTo(i,i,0,!1,!1,i,p-i).lineTo(i,i).arcTo(i,i,0,!1,!0,i*2,0).lineTo(s-i*2,0).arcTo(i,i,0,!1,!0,s-i,i).lineTo(s-i,p-i).arcTo(i,i,0,!1,!1,s,p);return u.append("path").attr("class","railroad-line").attr("d",y.build()),{element:u.node(),dimensions:{width:s,height:l,up:p,down:l-p}}}renderRepetition(t,r,n){let i=this.renderExpression(t,r),a=this.config.arcRadius,s=a*2,l=i.dimensions.width+a*4,u=n===0,h=i.dimensions.height+s+(u?s:0),d=t.append("g").attr("class","railroad-repetition"),f=a*2,p=u?s:0;d.node().appendChild(i.element).setAttribute("transform",`translate(${f}, ${p})`);let g=p+i.dimensions.up;d.append("path").attr("class","railroad-line").attr("d",new _o().moveTo(0,g).lineTo(a*2,g).build()),d.append("path").attr("class","railroad-line").attr("d",new _o().moveTo(f+i.dimensions.width,g).lineTo(l,g).build());let y=p+i.dimensions.height+a,v=new _o().moveTo(f+i.dimensions.width,g).arcTo(a,a,0,!1,!0,f+i.dimensions.width+a,g+a).lineTo(f+i.dimensions.width+a,y).arcTo(a,a,0,!1,!0,f+i.dimensions.width,y+a).lineTo(a*2,y+a).arcTo(a,a,0,!1,!0,a,y).lineTo(a,g+a).arcTo(a,a,0,!1,!0,a*2,g);if(d.append("path").attr("class","railroad-line").attr("d",v.build()),u){let x=new _o().moveTo(0,g).arcTo(a,a,0,!1,!1,a,g-a).lineTo(a,a).arcTo(a,a,0,!1,!0,a*2,0).lineTo(l-a*2,0).arcTo(a,a,0,!1,!0,l-a,a).lineTo(l-a,g-a).arcTo(a,a,0,!1,!1,l,g);d.append("path").attr("class","railroad-line").attr("d",x.build())}return{element:d.node(),dimensions:{width:l,height:h,up:g,down:h-g}}}renderSpecial(t,r){let n=this.measureText("? "+r+" ?"),i=n.width+this.config.padding*2,a=n.height+this.config.padding*2,s=t.append("g").attr("class","railroad-special");return s.append("rect").attr("x",0).attr("y",0).attr("width",i).attr("height",a),s.append("text").attr("x",i/2).attr("y",a/2).text("? "+r+" ?"),{element:s.node(),dimensions:{width:i,height:a,up:a/2,down:a/2}}}renderExpression(t,r){switch(r.type){case"terminal":return this.renderTerminal(t,r.value);case"nonterminal":return this.renderNonTerminal(t,r.name);case"sequence":return this.renderSequence(t,r.elements);case"choice":return this.renderChoice(t,r.alternatives);case"optional":return this.renderOptional(t,r.element);case"repetition":return this.renderRepetition(t,r.element,r.min);case"special":return this.renderSpecial(t,r.text);default:throw new Error(`Unknown node type: ${r.type}`)}}renderRule(t,r){let n=this.svg.append("g").attr("class","railroad-rule").attr("transform",`translate(0, ${r})`),i=t.name+" =",a=this.measureText(i).width+20,s=a+20,l=n.append("g"),u=this.renderExpression(l,t.definition),h=Math.max(20,u.dimensions.up),d=h-u.dimensions.up;return l.attr("transform",`translate(${s}, ${d})`),n.append("g").attr("class","railroad-rule-name-group").append("text").attr("class","railroad-rule-name").attr("x",0).attr("y",h).text(i),n.append("g").attr("class","railroad-start").append("circle").attr("cx",a).attr("cy",h).attr("r",this.config.markerRadius),n.append("g").attr("class","railroad-end").append("circle").attr("cx",s+u.dimensions.width+10).attr("cy",h).attr("r",this.config.markerRadius),n.append("path").attr("class","railroad-line").attr("d",new _o().moveTo(a+this.config.markerRadius,h).lineTo(s,h).build()),n.append("path").attr("class","railroad-line").attr("d",new _o().moveTo(s+u.dimensions.width,h).lineTo(s+u.dimensions.width+10-this.config.markerRadius,h).build()),{height:Math.max(40,d+u.dimensions.height+this.config.padding*2),width:s+u.dimensions.width+10+this.config.markerRadius}}renderDiagram(t){let r=this.config.padding,n=0;for(let i of t){let a=this.renderRule(i,r);r+=a.height+this.config.verticalSeparation,n=Math.max(n,a.width)}return{width:n+this.config.padding*2,height:r+this.config.padding}}},Y$e=o((e,t,r)=>{Wr(e,t.height,t.width,r),e.attr("viewBox",`0 0 ${t.width} ${t.height}`)},"configureRailroadSvgSize"),FNt=o((e,t,r)=>{Z.debug(`[Railroad] Rendering diagram +`+e);try{let n=xn(t);n.attr("class","railroad-diagram");let a=_t().railroad?.useMaxWidth??!0,s=sn.getRules();if(Z.debug(`[Railroad] Rendering ${s.length} rules`),s.length===0){Z.warn("[Railroad] No rules to render"),Y$e(n,{height:100,width:200},a);return}let u=new IX(n,gD()).renderDiagram(s);Y$e(n,u,a),Z.debug("[Railroad] Render complete")}catch(n){throw Z.error("[Railroad] Render error:",n),n}},"draw"),Pp={draw:FNt}});var X$e={};ir(X$e,{default:()=>zNt,diagram:()=>j$e});var j$e,zNt,K$e=F(()=>{"use strict";W$e();Xu();ck();Ex();j$e={parser:V$e,db:sn,renderer:Pp,styles:Np},zNt=j$e});var WNt,yD,qNt,J$e,HNt,UNt,YNt,jNt,eFe,tFe=F(()=>{"use strict";Xa();vt();Hs();Xu();WNt=Wv().RailroadEbnf.parser.LangiumParser,yD=o(e=>{let t=e.alternatives.map(qNt);return t.length===1?t[0]:{type:"choice",alternatives:t}},"transformChoice"),qNt=o(e=>{let t=e.elements.map(UNt);return t.length===1?t[0]:{type:"sequence",elements:t}},"transformSequence"),J$e=o(e=>{switch(e.$type){case"EbnfTerminal":return{type:"terminal",value:e.value};case"EbnfNonTerminal":return{type:"nonterminal",name:e.name};case"EbnfSpecial":return{type:"special",text:e.text};case"EbnfGroup":return yD(e.element);case"EbnfOptional":return{type:"optional",element:yD(e.element)};case"EbnfRepetition":return{type:"repetition",element:yD(e.element),min:0,max:1/0};default:throw new Error(`Unsupported EBNF primary node: ${e.$type}`)}},"transformPrimary"),HNt=o((e,t)=>{switch(t.$type){case"EbnfOptionalPostfix":return{type:"optional",element:e};case"EbnfZeroOrMorePostfix":return{type:"repetition",element:e,min:0,max:1/0};case"EbnfOneOrMorePostfix":return{type:"repetition",element:e,min:1,max:1/0};case"EbnfExceptionPostfix":return{type:"sequence",elements:[e,{type:"terminal",value:"-"},J$e(t.except)]};default:throw new Error(`Unsupported EBNF postfix node: ${t.$type}`)}},"transformPostfix"),UNt=o(e=>e.postfixes.reduce((t,r)=>HNt(t,r),J$e(e.base)),"transformTerm"),YNt=o(e=>({name:e.name,definition:yD(e.definition)}),"transformRule"),jNt=o(e=>{Gn(e,sn),e.title&&sn.setTitle(e.title),e.rules.map(t=>sn.addRule(YNt(t)))},"populateDb"),eFe={parse:o(e=>{sn.clear(),Z.debug("[EBNF Parser] Starting Langium parse");let t=WNt.parse(e);if(t.lexerErrors.length>0||t.parserErrors.length>0)throw new sd(t);let r=t.value;Z.debug("[EBNF Parser] Parsed rules:",r.rules.length),jNt(r),Z.debug("[EBNF Parser] Parse complete")},"parse"),parser:{yy:sn}}});var rFe={};ir(rFe,{diagram:()=>XNt});var XNt,nFe=F(()=>{"use strict";tFe();Xu();ck();Ex();XNt={parser:eFe,db:sn,renderer:Pp,styles:Np}});var QNt,MX,JNt,ePt,tPt,rPt,nPt,iPt,sFe,oFe=F(()=>{"use strict";Xa();vt();Hs();Xu();QNt=qv().RailroadAbnf.parser.LangiumParser,MX=o(e=>{let t=e.alternatives.map(JNt);return t.length===1?t[0]:{type:"choice",alternatives:t}},"transformAlternation"),JNt=o(e=>{let t=e.elements.map(tPt);return t.length===1?t[0]:{type:"sequence",elements:t}},"transformConcatenation"),ePt=o(e=>{if(e.includes("*")){let[r,n]=e.split("*"),i=r?parseInt(r,10):0,a=n?parseInt(n,10):1/0;return{min:i,max:a}}let t=parseInt(e,10);return{min:t,max:t}},"parseRepeat"),tPt=o(e=>{let t=rPt(e.primary);if(!e.repeat)return t;let{min:r,max:n}=ePt(e.repeat);return r===0&&n===1?{type:"optional",element:t}:{type:"repetition",element:t,min:r,max:n}},"transformElement"),rPt=o(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return MX(e.element);case"AbnfOptionalGroup":return{type:"optional",element:MX(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),nPt=o(e=>({name:e.name,definition:MX(e.definition)}),"transformRule"),iPt=o(e=>{Gn(e,sn),e.title&&sn.setTitle(e.title),e.rules.map(t=>sn.addRule(nPt(t)))},"populateDb"),sFe={parse:o(e=>{sn.clear(),Z.debug("[ABNF Parser] Starting Langium parse");let t=QNt.parse(e);if(t.lexerErrors.length>0||t.parserErrors.length>0)throw new sd(t);let r=t.value;Z.debug("[ABNF Parser] Parsed rules:",r.rules.length),iPt(r),Z.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:sn}}});var lFe={};ir(lFe,{diagram:()=>aPt});var aPt,cFe=F(()=>{"use strict";oFe();Xu();ck();Ex();aPt={parser:sFe,db:sn,renderer:Pp,styles:Np}});var lPt,fFe,cPt,uPt,dFe,hPt,dPt,fPt,pPt,pFe,mFe=F(()=>{"use strict";Xa();vt();Hs();Xu();lPt=Hv().RailroadPeg.parser.LangiumParser,fFe=o(e=>{let t=e.alternatives.map(cPt);return t.length===1?t[0]:{type:"choice",alternatives:t}},"transformOrderedChoice"),cPt=o(e=>{let t=e.elements.map(uPt);return t.length===1?t[0]:{type:"sequence",elements:t}},"transformSequence"),uPt=o(e=>{let t=hPt(e.suffix);return e.operator?{type:"special",text:e.operator==="&"?`&${dFe(t)}`:`!${dFe(t)}`}:t},"transformPrefix"),dFe=o(e=>{switch(e.type){case"terminal":return`"${e.value}"`;case"nonterminal":return e.name;case"special":return e.text;default:return"(...)"}},"nodeToLabel"),hPt=o(e=>{let t=dPt(e.primary);if(!e.operator)return t;switch(e.operator){case"?":return{type:"optional",element:t};case"*":return{type:"repetition",element:t,min:0,max:1/0};case"+":return{type:"repetition",element:t,min:1,max:1/0};default:throw new Error(`Unsupported PEG suffix operator: ${e.operator}`)}},"transformSuffix"),dPt=o(e=>{switch(e.$type){case"PegLiteral":return{type:"terminal",value:e.value};case"PegIdentifier":return{type:"nonterminal",name:e.name};case"PegGroup":return fFe(e.element);case"PegAny":return{type:"special",text:e.dot};default:throw new Error(`Unsupported PEG primary node: ${e.$type}`)}},"transformPrimary"),fPt=o(e=>({name:e.name,definition:fFe(e.definition)}),"transformRule"),pPt=o(e=>{Gn(e,sn),e.title&&sn.setTitle(e.title),e.rules.map(t=>sn.addRule(fPt(t)))},"populateDb"),pFe={parse:o(e=>{sn.clear(),Z.debug("[PEG Parser] Starting Langium parse");let t=lPt.parse(e);if(t.lexerErrors.length>0||t.parserErrors.length>0)throw new sd(t);let r=t.value;Z.debug("[PEG Parser] Parsed rules:",r.rules.length),pPt(r),Z.debug("[PEG Parser] Parse complete")},"parse"),parser:{yy:sn}}});var gFe={};ir(gFe,{diagram:()=>mPt});var mPt,yFe=F(()=>{"use strict";mFe();Xu();ck();Ex();mPt={parser:pFe,db:sn,renderer:Pp,styles:Np}});var nOt={};ir(nOt,{default:()=>rOt});Vl();f8();Yp();var Fje=o(e=>/^\s*C4Context|C4Container|C4Component|C4Dynamic|C4Deployment/.test(e),"detector"),zje=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(yae(),gae));return{id:"c4",diagram:e}},"loader"),Gje={id:"c4",detector:Fje,loader:zje},vae=Gje;var gTe="flowchart",a0t=o((e,t)=>t?.flowchart?.defaultRenderer==="dagre-wrapper"||t?.flowchart?.defaultRenderer==="elk"?!1:/^\s*graph/.test(e),"detector"),s0t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(lC(),LA));return{id:gTe,diagram:e}},"loader"),o0t={id:gTe,detector:a0t,loader:s0t},yTe=o0t;var vTe="flowchart-v2",l0t=o((e,t)=>t?.flowchart?.defaultRenderer==="dagre-d3"?!1:(t?.flowchart?.defaultRenderer==="elk"&&(t.layout="elk"),/^\s*graph/.test(e)&&t?.flowchart?.defaultRenderer==="dagre-wrapper"?!0:/^\s*flowchart/.test(e)),"detector"),c0t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(lC(),LA));return{id:vTe,diagram:e}},"loader"),u0t={id:vTe,detector:l0t,loader:c0t},xTe=u0t;var kTe="swimlane",f0t=o(e=>/^\s*swimlane-beta\b/.test(e),"detector"),p0t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(wTe(),CTe));return{id:kTe,diagram:e}},"loader"),m0t={id:kTe,detector:f0t,loader:p0t},STe=m0t;var b0t=o(e=>/^\s*erDiagram/.test(e),"detector"),T0t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(NTe(),MTe));return{id:"er",diagram:e}},"loader"),C0t={id:"er",detector:b0t,loader:T0t},PTe=C0t;var QDe="gitGraph",Y3t=o(e=>/^\s*gitGraph/.test(e),"detector"),j3t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(ZDe(),KDe));return{id:QDe,diagram:e}},"loader"),X3t={id:QDe,detector:Y3t,loader:j3t},JDe=X3t;var I7e="gantt",B5t=o(e=>/^\s*gantt/.test(e),"detector"),$5t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(D7e(),L7e));return{id:I7e,diagram:e}},"loader"),F5t={id:I7e,detector:B5t,loader:$5t},M7e=F5t;var V7e="info",q5t=o(e=>/^\s*info/.test(e),"detector"),H5t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(G7e(),z7e));return{id:V7e,diagram:e}},"loader"),W7e={id:V7e,detector:q5t,loader:H5t};var iAt=o(e=>/^\s*pie/.test(e),"detector"),aAt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(Q7e(),Z7e));return{id:"pie",diagram:e}},"loader"),J7e={id:"pie",detector:iAt,loader:aAt};var d8e="quadrantChart",CAt=o(e=>/^\s*quadrantChart/.test(e),"detector"),wAt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(h8e(),u8e));return{id:d8e,diagram:e}},"loader"),kAt={id:d8e,detector:CAt,loader:wAt},f8e=kAt;var z8e="xychart",GAt=o(e=>/^\s*xychart(-beta)?/.test(e),"detector"),VAt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(F8e(),$8e));return{id:z8e,diagram:e}},"loader"),WAt={id:z8e,detector:GAt,loader:VAt},G8e=WAt;var K8e="requirement",jAt=o(e=>/^\s*requirement(Diagram)?/.test(e),"detector"),XAt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(X8e(),j8e));return{id:K8e,diagram:e}},"loader"),KAt={id:K8e,detector:jAt,loader:XAt},Z8e=KAt;var mIe="sequence",U6t=o(e=>/^\s*sequenceDiagram/.test(e),"detector"),Y6t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(pIe(),fIe));return{id:mIe,diagram:e}},"loader"),j6t={id:mIe,detector:U6t,loader:Y6t},gIe=j6t;var CIe="class",eRt=o((e,t)=>t?.class?.defaultRenderer==="dagre-wrapper"?!1:/^\s*classDiagram/.test(e),"detector"),tRt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(TIe(),bIe));return{id:CIe,diagram:e}},"loader"),rRt={id:CIe,detector:eRt,loader:tRt},wIe=rRt;var EIe="classDiagram",iRt=o((e,t)=>/^\s*classDiagram/.test(e)&&t?.class?.defaultRenderer==="dagre-wrapper"?!0:/^\s*classDiagram-v2/.test(e),"detector"),aRt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(SIe(),kIe));return{id:EIe,diagram:e}},"loader"),sRt={id:EIe,detector:iRt,loader:aRt},AIe=sRt;var aMe="state",IRt=o((e,t)=>t?.state?.defaultRenderer==="dagre-wrapper"?!1:/^\s*stateDiagram/.test(e),"detector"),MRt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(iMe(),nMe));return{id:aMe,diagram:e}},"loader"),NRt={id:aMe,detector:IRt,loader:MRt},sMe=NRt;var cMe="stateDiagram",ORt=o((e,t)=>!!(/^\s*stateDiagram-v2/.test(e)||/^\s*stateDiagram/.test(e)&&t?.state?.defaultRenderer==="dagre-wrapper"),"detector"),BRt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(lMe(),oMe));return{id:cMe,diagram:e}},"loader"),$Rt={id:cMe,detector:ORt,loader:BRt},uMe=$Rt;var SMe="journey",a_t=o(e=>/^\s*journey/.test(e),"detector"),s_t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(kMe(),wMe));return{id:SMe,diagram:e}},"loader"),o_t={id:SMe,detector:a_t,loader:s_t},EMe=o_t;vt();Ka();$n();var l_t=o((e,t,r)=>{Z.debug(`rendering svg for syntax error +`);let n=xn(t),i=n.append("g");n.attr("viewBox","0 0 2412 512"),Wr(n,100,512,!0),i.append("path").attr("class","error-icon").attr("d","m411.313,123.313c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32-9.375,9.375-20.688-20.688c-12.484-12.5-32.766-12.5-45.25,0l-16,16c-1.261,1.261-2.304,2.648-3.31,4.051-21.739-8.561-45.324-13.426-70.065-13.426-105.867,0-192,86.133-192,192s86.133,192 192,192 192-86.133 192-192c0-24.741-4.864-48.327-13.426-70.065 1.402-1.007 2.79-2.049 4.051-3.31l16-16c12.5-12.492 12.5-32.758 0-45.25l-20.688-20.688 9.375-9.375 32.001-31.999zm-219.313,100.687c-52.938,0-96,43.063-96,96 0,8.836-7.164,16-16,16s-16-7.164-16-16c0-70.578 57.422-128 128-128 8.836,0 16,7.164 16,16s-7.164,16-16,16z"),i.append("path").attr("class","error-icon").attr("d","m459.02,148.98c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l16,16c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16.001-16z"),i.append("path").attr("class","error-icon").attr("d","m340.395,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688 6.25-6.25 6.25-16.375 0-22.625l-16-16c-6.25-6.25-16.375-6.25-22.625,0s-6.25,16.375 0,22.625l15.999,16z"),i.append("path").attr("class","error-icon").attr("d","m400,64c8.844,0 16-7.164 16-16v-32c0-8.836-7.156-16-16-16-8.844,0-16,7.164-16,16v32c0,8.836 7.156,16 16,16z"),i.append("path").attr("class","error-icon").attr("d","m496,96.586h-32c-8.844,0-16,7.164-16,16 0,8.836 7.156,16 16,16h32c8.844,0 16-7.164 16-16 0-8.836-7.156-16-16-16z"),i.append("path").attr("class","error-icon").attr("d","m436.98,75.605c3.125,3.125 7.219,4.688 11.313,4.688 4.094,0 8.188-1.563 11.313-4.688l32-32c6.25-6.25 6.25-16.375 0-22.625s-16.375-6.25-22.625,0l-32,32c-6.251,6.25-6.251,16.375-0.001,22.625z"),i.append("text").attr("class","error-text").attr("x",1440).attr("y",250).attr("font-size","150px").style("text-anchor","middle").text("Syntax error in text"),i.append("text").attr("class","error-text").attr("x",1250).attr("y",400).attr("font-size","100px").style("text-anchor","middle").text(`mermaid version ${r}`)},"draw"),qY={draw:l_t},AMe=qY;var c_t={db:{},renderer:qY,parser:{parse:o(()=>{},"parse")}},RMe=c_t;var _Me="flowchart-elk",u_t=o((e,t={})=>/^\s*flowchart-elk/.test(e)||/^\s*(flowchart|graph)/.test(e)&&t?.flowchart?.defaultRenderer==="elk"?(t.layout="elk",!0):!1,"detector"),h_t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(lC(),LA));return{id:_Me,diagram:e}},"loader"),d_t={id:_Me,detector:u_t,loader:h_t},LMe=d_t;var hNe="timeline",B_t=o(e=>/^\s*timeline/.test(e),"detector"),$_t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(uNe(),cNe));return{id:hNe,diagram:e}},"loader"),F_t={id:hNe,detector:B_t,loader:$_t},dNe=F_t;var _Ne="mindmap",j_t=o(e=>/^\s*mindmap/.test(e),"detector"),X_t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(RNe(),ANe));return{id:_Ne,diagram:e}},"loader"),K_t={id:_Ne,detector:j_t,loader:X_t},LNe=K_t;var VNe="kanban",dLt=o(e=>/^\s*kanban/.test(e),"detector"),fLt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(GNe(),zNe));return{id:VNe,diagram:e}},"loader"),pLt={id:VNe,detector:dLt,loader:fLt},WNe=pLt;var kPe="sankey",$Lt=o(e=>/^\s*sankey(-beta)?/.test(e),"detector"),FLt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(wPe(),CPe));return{id:kPe,diagram:e}},"loader"),zLt={id:kPe,detector:$Lt,loader:FLt},SPe=zLt;var MPe="packet",XLt=o(e=>/^\s*packet(-beta)?/.test(e),"detector"),KLt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(IPe(),DPe));return{id:MPe,diagram:e}},"loader"),NPe={id:MPe,detector:XLt,loader:KLt};var HPe="radar",vDt=o(e=>/^\s*radar-beta/.test(e),"detector"),xDt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(qPe(),WPe));return{id:HPe,diagram:e}},"loader"),UPe={id:HPe,detector:vDt,loader:xDt};var XOe="block",V7t=o(e=>/^\s*block(-beta)?/.test(e),"detector"),W7t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(jOe(),YOe));return{id:XOe,diagram:e}},"loader"),q7t={id:XOe,detector:V7t,loader:W7t},KOe=q7t;var y9e="treeView",p8t=o(e=>/^\s*treeView-beta/.test(e),"detector"),m8t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(g9e(),m9e));return{id:y9e,diagram:e}},"loader"),g8t={id:y9e,detector:p8t,loader:m8t},v9e=g8t;var G9e="architecture",D8t=o(e=>/^\s*architecture/.test(e),"detector"),I8t=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(z9e(),F9e));return{id:G9e,diagram:e}},"loader"),M8t={id:G9e,detector:D8t,loader:I8t},V9e=M8t;var tBe="eventmodeling",yIt=o(e=>/^\s*eventmodeling/.test(e),"detector"),vIt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(eBe(),J9e));return{id:tBe,diagram:e}},"loader"),xIt={id:tBe,detector:yIt,loader:vIt},rBe=xIt;var xBe="ishikawa",IIt=o(e=>/^\s*ishikawa(-beta)?\b/i.test(e),"detector"),MIt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(vBe(),yBe));return{id:xBe,diagram:e}},"loader"),bBe={id:xBe,detector:IIt,loader:MIt};var KBe="venn",yMt=o(e=>/^\s*venn-beta/.test(e),"detector"),vMt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(XBe(),jBe));return{id:KBe,diagram:e}},"loader"),xMt={id:KBe,detector:yMt,loader:vMt},ZBe=xMt;Yp();Xt();var o$e="treemap",RMt=o(e=>/^\s*treemap/.test(e),"detector"),_Mt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(s$e(),a$e));return{id:o$e,diagram:e}},"loader"),l$e={id:o$e,detector:RMt,loader:_Mt};var b$e="wardley",eNt=o(e=>/^\s*wardley-beta/i.test(e),"detector"),tNt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(x$e(),v$e));return{id:b$e,diagram:e}},"loader"),rNt={id:b$e,detector:eNt,loader:tNt},T$e=rNt;var $$e="cynefin",yNt=o(e=>/^\s*cynefin-beta(?:[\s:]|$)/.test(e),"detector"),vNt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(B$e(),O$e));return{id:$$e,diagram:e}},"loader"),F$e={id:$$e,detector:yNt,loader:vNt};var Z$e="railroad",GNt=o(e=>/^\s*railroad-beta/i.test(e),"detector"),VNt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(K$e(),X$e));return{id:Z$e,diagram:e}},"loader"),Q$e={id:Z$e,detector:GNt,loader:VNt};var iFe="railroadEbnf",KNt=o(e=>/^\s*railroad-ebnf-beta/i.test(e),"detector"),ZNt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(nFe(),rFe));return{id:iFe,diagram:e}},"loader"),aFe={id:iFe,detector:KNt,loader:ZNt};var uFe="railroadAbnf",sPt=o(e=>/^\s*railroad-abnf-beta/i.test(e),"detector"),oPt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(cFe(),lFe));return{id:uFe,diagram:e}},"loader"),hFe={id:uFe,detector:sPt,loader:oPt};var vFe="railroadPeg",gPt=o(e=>/^\s*railroad-peg-beta/i.test(e),"detector"),yPt=o(async()=>{let{diagram:e}=await Promise.resolve().then(()=>(yFe(),gFe));return{id:vFe,diagram:e}},"loader"),xFe={id:vFe,detector:gPt,loader:yPt};var bFe=!1,Ax=o(()=>{bFe||(bFe=!0,jp("error",RMe,e=>e.toLowerCase().trim()==="error"),jp("---",{db:{clear:o(()=>{},"clear")},styles:{},renderer:{draw:o(()=>{},"draw")},parser:{parse:o(()=>{throw new Error("Diagrams beginning with --- are not valid. If you were trying to use a YAML front-matter, please ensure that you've correctly opened and closed the YAML front-matter with un-indented `---` blocks")},"parse")},init:o(()=>null,"init")},e=>e.toLowerCase().trimStart().startsWith("---")),ob(LMe,LNe,V9e),ob(vae,WNe,AIe,wIe,PTe,M7e,W7e,J7e,Z8e,gIe,STe,xTe,yTe,dNe,JDe,uMe,sMe,EMe,f8e,SPe,NPe,G8e,KOe,rBe,v9e,UPe,bBe,l$e,Q$e,aFe,hFe,xFe,ZBe,T$e,F$e))},"addDiagrams");vt();Yp();Xt();var TFe=o(async()=>{Z.debug("Loading registered diagrams");let t=(await Promise.allSettled(Object.entries(lh).map(async([r,{detector:n,loader:i}])=>{if(i)try{fb(r)}catch{try{let{diagram:a,id:s}=await i();jp(s,a,n)}catch(a){throw Z.error(`Failed to load external diagram with key ${r}. Removing from detectors.`),delete lh[r],a}}}))).filter(r=>r.status==="rejected");if(t.length>0){Z.error(`Failed to load ${t.length} external diagrams`);for(let r of t)Z.error(r);throw new Error(`Failed to load ${t.length} external diagrams`)}},"loadRegisteredDiagrams");vt();$r();var Rx="comm",vD="rule",xD="decl";var CFe="@media",wFe="@import";var kFe="@supports";var SFe="@namespace",uk="@keyframes";var bD="@layer",EFe="@scope";var NX=Math.abs,hk=String.fromCharCode;function TD(e){return e.trim()}o(TD,"trim");function _x(e,t,r){return e.replace(t,r)}o(_x,"replace");function AFe(e,t,r){return e.indexOf(t,r)}o(AFe,"indexof");function yd(e,t){return e.charCodeAt(t)|0}o(yd,"charat");function vd(e,t,r){return e.slice(t,r)}o(vd,"substr");function Lo(e){return e.length}o(Lo,"strlen");function CD(e){return e.length}o(CD,"sizeof");function Lx(e,t){return t.push(e),e}o(Lx,"append");var wD=1,Dx=1,RFe=0,Bl=0,ea=0,Mx="";function kD(e,t,r,n,i,a,s,l){return{value:e,root:t,parent:r,type:n,props:i,children:a,line:wD,column:Dx,length:s,return:"",siblings:l}}o(kD,"node");function _Fe(){return ea}o(_Fe,"char");function LFe(){return ea=Bl>0?yd(Mx,--Bl):0,Dx--,ea===10&&(Dx=1,wD--),ea}o(LFe,"prev");function $l(){return ea=Bl2||Ix(ea)>3?"":" "}o(MFe,"whitespace");function NFe(e,t){for(;--t&&$l()&&!(ea<48||ea>102||ea>57&&ea<65||ea>70&&ea<97););return SD(e,dk()+(t<6&&xd()==32&&$l()==32))}o(NFe,"escaping");function PX(e){for(;$l();)switch(ea){case e:return Bl;case 34:case 39:e!==34&&e!==39&&PX(ea);break;case 40:e===41&&PX(e);break;case 92:$l();break}return Bl}o(PX,"delimiter");function PFe(e,t){for(;$l()&&e+ea!==57;)if(e+ea===84&&xd()===47)break;return"/*"+SD(t,Bl-1)+"*"+hk(e===47?e:$l())}o(PFe,"commenter");function OFe(e){for(;!Ix(xd());)$l();return SD(e,Bl)}o(OFe,"identifier");function FFe(e){return IFe(AD("",null,null,null,[""],e=DFe(e),0,[0],e))}o(FFe,"compile");function AD(e,t,r,n,i,a,s,l,u){for(var h=0,d=0,f=s,p=0,m=0,g=0,y=1,v=1,x=1,b=0,T="",k=i,C=a,w=n,S=T;v;)switch(g=b,b=$l()){case 40:if(g!=108&&yd(S,f-1)==58){AFe(S+=_x(ED(b),"&","&\f"),"&\f",NX(h?l[h-1]:0))!=-1&&(x=-1);break}case 34:case 39:case 91:S+=ED(b);break;case 9:case 10:case 13:case 32:S+=MFe(g);break;case 92:S+=NFe(dk()-1,7);continue;case 47:switch(xd()){case 42:case 47:Lx(xPt(PFe($l(),dk()),t,r,u),u),(Ix(g||1)==5||Ix(xd()||1)==5)&&Lo(S)&&vd(S,-1,void 0)!==" "&&(S+=" ");break;default:S+="/"}break;case 123*y:l[h++]=Lo(S)*x;case 125*y:case 59:case 0:switch(b){case 0:case 125:v=0;case 59+d:x==-1&&(S=_x(S,/\f/g,"")),m>0&&(Lo(S)-f||y===0&&g===47)&&Lx(m>32?$Fe(S+";",n,r,f-1,u):$Fe(_x(S," ","")+";",n,r,f-2,u),u);break;case 59:S+=";";default:if(Lx(w=BFe(S,t,r,h,d,i,l,T,k=[],C=[],f,a),a),b===123)if(d===0)AD(S,t,w,w,k,a,f,l,C);else{switch(p){case 99:if(yd(S,3)===110)break;case 108:if(yd(S,2)===97)break;default:d=0;case 100:case 109:case 115:}d?AD(e,w,w,n&&Lx(BFe(e,w,w,0,0,i,l,T,i,k=[],f,C),C),i,C,f,l,n?k:C):AD(S,w,w,w,[""],C,0,l,C)}}h=d=m=0,y=x=1,T=S="",f=s;break;case 58:f=1+Lo(S),m=g;default:if(y<1){if(b==123)--y;else if(b==125&&y++==0&&LFe()==125)continue}switch(S+=hk(b),b*y){case 38:x=d>0?1:(S+="\f",-1);break;case 44:l[h++]=(Lo(S)-1)*x,x=1;break;case 64:xd()===45&&(S+=ED($l())),p=xd(),d=f=Lo(T=S+=OFe(dk())),b++;break;case 45:g===45&&Lo(S)==2&&(y=0)}}return a}o(AD,"parse");function BFe(e,t,r,n,i,a,s,l,u,h,d,f){for(var p=i-1,m=i===0?a:[""],g=CD(m),y=0,v=0,x=0;y0?m[b]+" "+T:_x(T,/&\f/g,m[b])))&&(u[x++]=k);return kD(e,t,r,i===0?vD:l,u,h,d,f)}o(BFe,"ruleset");function xPt(e,t,r,n){return kD(e,t,r,Rx,hk(_Fe()),vd(e,2,-2),0,n)}o(xPt,"comment");function $Fe(e,t,r,n,i){return kD(e,t,r,xD,vd(e,0,n),vd(e,n+1,-1),n,i)}o($Fe,"declaration");function RD(e,t){for(var r="",n=0;n{qFe.forEach(e=>{e()}),qFe=[]},"attachFunctions");vt();var UFe=o(e=>e.replace(/^\s*%%(?!{)[^\n]+\n?/gm,"").trimStart(),"cleanupComments");yS();R2();function YFe(e){let t=e.match(gS);if(!t)return{text:e,metadata:{}};let r=t[1],n=r?t[2].split(` +`).map(s=>s.startsWith(r)?s.slice(r.length):s).join(` +`):t[2],i=Jd(n,{schema:Qd})??{};i=typeof i=="object"&&!Array.isArray(i)?i:{};let a={};return i.displayMode&&(a.displayMode=i.displayMode.toString()),i.title&&(a.title=i.title.toString()),i.config&&(a.config=i.config),{text:e.slice(t[0].length),metadata:a}}o(YFe,"extractFrontMatter");Qt();var TPt=o(e=>e.replace(/\r\n?/g,` +`).replace(/<(\w+)([^>]*)>/g,(t,r,n)=>"<"+r+n.replace(/="([^"]*)"/g,"='$1'")+">"),"cleanupText"),CPt=o(e=>{let{text:t,metadata:r}=YFe(e),{displayMode:n,title:i,config:a={}}=r;return n&&(a.gantt||(a.gantt={}),a.gantt.displayMode=n),{title:i,config:a,text:t}},"processFrontmatter"),wPt=o(e=>{let t=Zt.detectInit(e)??{},r=Zt.detectDirective(e,"wrap");return Array.isArray(r)?t.wrap=r.some(({type:n})=>n==="wrap"):r?.type==="wrap"&&(t.wrap=!0),{text:aae(e),directive:t}},"processDirectives");function OX(e){let t=TPt(e),r=CPt(t),n=wPt(r.text),i=qr(r.config,n.directive);return e=UFe(n.text),{code:e,title:r.title,config:i}}o(OX,"preprocessDiagram");v8();Ek();Qt();function jFe(e){let t=new TextEncoder().encode(e),r=Array.from(t,n=>String.fromCodePoint(n)).join("");return btoa(r)}o(jFe,"toBase64");Ak();var kPt=5e4,SPt="graph TB;a[Maximum text size in diagram exceeded];style a fill:#faa",EPt="sandbox",APt="loose",RPt="http://www.w3.org/2000/svg",_Pt="http://www.w3.org/1999/xlink",LPt="http://www.w3.org/1999/xhtml",DPt="100%",IPt="100%",MPt="border:0;margin:0;",NPt="margin:0",PPt="allow-top-navigation-by-user-activation allow-popups",OPt='The "iframe" tag is not supported by your browser.',BPt=["foreignobject"],$Pt=["dominant-baseline"];function QFe(e){let t=OX(e);return qx(),lZ(t.config??{}),t}o(QFe,"processAndSetConfigs");async function FPt(e,t){Ax();try{let{code:r,config:n}=QFe(e);return{diagramType:(await JFe(r)).type,config:n}}catch(r){if(t?.suppressErrors)return!1;throw r}}o(FPt,"parse");var XFe=o((e,t,r=[])=>{let n=c7(`{ ${r.join(" !important; ")} !important; }`);return`.${e} ${t} ${n}`},"cssImportantStyles"),zPt=o((e,t=new Map)=>{let r=new CSSStyleSheet;if(e.fontFamily!==void 0&&r.insertRule(`:root { --mermaid-font-family: ${e.fontFamily}}`,r.cssRules.length),e.altFontFamily!==void 0&&r.insertRule(`:root { --mermaid-alt-font-family: ${e.altFontFamily}}`,r.cssRules.length),t instanceof Map){let l=Gr(e)?["> *","span"]:["rect","polygon","ellipse","circle","path"];t.forEach(u=>{a4(u.styles)||l.forEach(h=>{r.insertRule(XFe(u.id,h,u.styles),r.cssRules.length)}),a4(u.textStyles)||r.insertRule(XFe(u.id,"tspan",(u?.textStyles||[]).map(h=>h.replace("color","fill"))),r.cssRules.length)})}let n="";if(e.themeCSS!==void 0)if(typeof r.replaceSync=="function"){let i=new CSSStyleSheet;i.replaceSync(e.themeCSS),n=y8(i)+` +`}else n+=`${e.themeCSS} +`;return n+y8(r)},"createCssStyles"),GPt=o((e,t)=>RD(FFe(`${e}{${t}}`),GFe([o(function(n,i,a,s){if(n.type==="rule"&&Array.isArray(n.props)){if(n.parent&&n.parent.type===uk)return;n.props=n.props.map(l=>l===e&&Array.isArray(n.children)&&n.children.every(h=>h.type!=="decl"?!1:new Set(["font-family","font-size","fill"]).has(h.props))||(l.startsWith(`${e} `)||l.startsWith(`${e}>`))&&!l.startsWith(`${e} ||`)?l:`${e} ${l}`)}else n.type.startsWith("@")&&([...[CFe,kFe,bD,EFe,"@container","@starting-style"],uk].includes(n.type)||(Z.warn(`Removing unsupported at-rule ${n.type} from CSS`),n.type=Rx))},"addNamespace"),zFe])),"compileCSS"),VPt=o((e,t,r,n)=>{let i=zPt(e,r),a=uJ(t,i,{...e.themeVariables,theme:e.theme,look:e.look},n);return GPt(n,a)},"createUserStyles"),WPt=o((e="",t,r)=>{let n=e;return!r&&!t&&(n=n.replace(/marker-end="url\([\d+./:=?A-Za-z-]*?#/g,'marker-end="url(#')),n=Rs(n),n=n.replace(/
/g,"
"),n},"cleanUpSvgCode"),qPt=o((e="",t)=>{let r=t?.viewBox?.baseVal?.height?t.viewBox.baseVal.height+"px":IPt,n=jFe(`${e}`);return``},"putIntoIFrame"),KFe=o((e,t,r,n,i)=>{let a=e.append("div");a.attr("id",r),n&&a.attr("style",n);let s=a.append("svg").attr("id",t).attr("width","100%").attr("xmlns",RPt);return i&&s.attr("xmlns:xlink",i),s.append("g"),e},"appendDivSvgG");function ZFe(e,t){return e.append("iframe").attr("id",t).attr("style","width: 100%; height: 100%;").attr("sandbox","")}o(ZFe,"sandboxedIframe");var HPt=o((e,t,r,n)=>{e.getElementById(t)?.remove(),e.getElementById(r)?.remove(),e.getElementById(n)?.remove()},"removeExistingElements"),UPt=o(async function(e,t,r){Ax();let n=QFe(t);t=n.code;let i=_t();Z.debug(i),t.length>(i?.maxTextSize??kPt)&&(t=SPt);let a=`#${e}`,s="i"+e,l="#"+s,u="d"+e,h="#"+u,d=o(()=>{let A=et(p?l:h).node();A&&"remove"in A&&A.remove()},"removeTempElements"),f=et(document.body),p=i.securityLevel===EPt,m=i.securityLevel===APt,g=i.fontFamily;if(r!==void 0){if(r&&(r.innerHTML=""),p){let _=ZFe(et(r),s);f=et(_.nodes()[0].contentDocument.body),f.node().style.margin="0"}else f=et(r);KFe(f,e,u,`font-family: ${g}`,_Pt)}else{if(HPt(document,e,u,s),p){let _=ZFe(et(document.body),s);f=et(_.nodes()[0].contentDocument.body),f.node().style.margin="0"}else f=et("body");KFe(f,e,u)}let y,v;try{y=await Nx.fromText(t,{title:n.title})}catch(_){if(i.suppressErrorRendering)throw d(),_;y=await Nx.fromText("error"),v=_}let x=f.select(h).node(),b=y.type,T=x.firstChild,k=T.firstChild,C=y.renderer.getClasses?.(t,y),w=VPt(i,b,C,a),S=document.createElement("style");S.innerHTML=w,T.insertBefore(S,k);try{await y.renderer.draw(t,e,"11.16.1",y)}catch(_){throw i.suppressErrorRendering?d():AMe.draw(t,e,"11.16.1"),_}let R=f.select(`${h} svg`),L=y.db.getAccTitle?.(),N=y.db.getAccDescription?.();jPt(b,R,L,N),f.select(`[id="${e}"]`).selectAll("foreignobject > *").attr("xmlns",LPt);let I=f.select(h).node().innerHTML;if(Z.debug("config.arrowMarkerAbsolute",i.arrowMarkerAbsolute),I=WPt(I,p,ya(i.arrowMarkerAbsolute)),p){let _=f.select(h+" svg").node();I=qPt(I,_)}else m||(I=Zs.sanitize(I,{ADD_TAGS:BPt,ADD_ATTR:$Pt,HTML_INTEGRATION_POINTS:{foreignobject:!0}}));if(HFe(),v)throw v;return d(),{diagramType:b,svg:I,bindFunctions:y.db.bindFunctions}},"render");function YPt(e={}){let t=ri({},e);t?.fontFamily&&!t.themeVariables?.fontFamily&&(t.themeVariables||(t.themeVariables={}),t.themeVariables.fontFamily=t.fontFamily),aZ(t),t?.theme&&t.theme in sl?t.themeVariables=sl[t.theme].getThemeVariables(t.themeVariables):t&&(t.themeVariables=sl.default.getThemeVariables(t.themeVariables));let r=typeof t=="object"?u7(t):h7();Bx(r.logLevel),Ax()}o(YPt,"initialize");var JFe=o((e,t={})=>{let{code:r}=OX(e);return Nx.fromText(r,t)},"getDiagramFromText");function jPt(e,t,r,n){VFe(t,e),WFe(t,r,n,t.attr("id"))}o(jPt,"addA11yInfo");var Op=Object.freeze({render:UPt,parse:FPt,getDiagramFromText:JFe,initialize:YPt,getConfig:_t,setConfig:_k,getSiteConfig:h7,updateSiteConfig:sZ,reset:o(()=>{qx()},"reset"),globalReset:o(()=>{qx(wd)},"globalReset"),defaultConfig:wd});Bx(_t().logLevel);qx(_t());Jf();Qt();var XPt=o((e,t,r)=>{Z.warn(e),mP(e)?(r&&r(e.str,e.hash),t.push({...e,message:e.str,error:e})):(r&&r(e),e instanceof Error&&t.push({str:e.message,message:e.message,hash:e.name,error:e}))},"handleError"),eze=o(async function(e={querySelector:".mermaid"}){try{await KPt(e)}catch(t){if(mP(t)&&Z.error(t.str),bd.parseError&&bd.parseError(t),!e.suppressErrors)throw Z.error("Use the suppressErrors option to suppress these errors"),t}},"run"),KPt=o(async function({postRenderCallback:e,querySelector:t,nodes:r}={querySelector:".mermaid"}){let n=Op.getConfig();Z.debug(`${e?"":"No "}Callback function found`);let i;if(r)i=r;else if(t)i=document.querySelectorAll(t);else throw new Error("Nodes and querySelector are both undefined");Z.debug(`Found ${i.length} diagrams`),n?.startOnLoad!==void 0&&(Z.debug("Start On Load: "+n?.startOnLoad),Op.updateSiteConfig({startOnLoad:n?.startOnLoad}));let a=new Zt.InitIDGenerator(n.deterministicIds,n.deterministicIDSeed),s,l=[];for(let u of Array.from(i)){Z.info("Rendering diagram: "+u.id);if(u.getAttribute("data-processed"))continue;u.setAttribute("data-processed","true");let h=`mermaid-${a.next()}`;s=u.innerHTML,s=mS(Zt.entityDecode(s)).trim().replace(//gi,"
");let d=Zt.detectInit(s);d&&Z.debug("Detected early reinit: ",d);try{let{svg:f,bindFunctions:p}=await ize(h,s,u);u.innerHTML=f,e&&await e(h),p&&p(u)}catch(f){XPt(f,l,bd.parseError)}}if(l.length>0)throw l[0]},"runThrowsErrors"),tze=o(function(e){Op.initialize(e)},"initialize"),ZPt=o(async function(e,t,r){Z.warn("mermaid.init is deprecated. Please use run instead."),e&&tze(e);let n={postRenderCallback:r,querySelector:".mermaid"};typeof t=="string"?n.querySelector=t:t&&(t instanceof HTMLElement?n.nodes=[t]:n.nodes=t),await eze(n)},"init"),QPt=o(async(e,{lazyLoad:t=!0}={})=>{Ax(),ob(...e),t===!1&&await TFe()},"registerExternalDiagrams"),rze=o(function(){if(bd.startOnLoad){let{startOnLoad:e}=Op.getConfig();e&&bd.run().catch(t=>Z.error("Mermaid failed to initialize",t))}},"contentLoaded");if(typeof document<"u"){window.addEventListener("load",rze,!1)}var JPt=o(function(e){bd.parseError=e},"setParseErrorHandler"),_D=[],BX=!1,nze=o(async()=>{if(!BX){for(BX=!0;_D.length>0;){let e=_D.shift();if(e)try{await e()}catch(t){Z.error("Error executing queue",t)}}BX=!1}},"executeQueue"),eOt=o(async(e,t)=>new Promise((r,n)=>{let i=o(()=>new Promise((a,s)=>{Op.parse(e,t).then(l=>{a(l),r(l)},l=>{Z.error("Error parsing",l),bd.parseError?.(l),s(l),n(l)})}),"performCall");_D.push(i),nze().catch(n)}),"parse"),ize=o((e,t,r)=>new Promise((n,i)=>{let a=o(()=>new Promise((s,l)=>{Op.render(e,t,r).then(u=>{s(u),n(u)},u=>{Z.error("Error parsing",u),bd.parseError?.(u),l(u),i(u)})}),"performCall");_D.push(a),nze().catch(i)}),"render"),tOt=o(()=>Object.keys(lh).map(e=>({id:e})),"getRegisteredDiagramsMetadata"),bd={startOnLoad:!0,mermaidAPI:Op,parse:eOt,render:ize,init:ZPt,run:eze,registerExternalDiagrams:QPt,registerLayoutLoaders:XF,initialize:tze,parseError:void 0,contentLoaded:rze,setParseErrorHandler:JPt,detectType:ny,registerIconPacks:ty,getRegisteredDiagramsMetadata:tOt},rOt=bd;return uze(nOt);})(); +/*! Bundled license information: + +lodash-es/lodash.js: + (** + * @license + * Lodash (Custom Build) + * Build: `lodash modularize exports="es" -o ./` + * Copyright OpenJS Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + *) +*/ +/*! Check if previously processed */ +/*! + * Wait for document loaded before starting the execution + */ +/*! Bundled license information: + +dompurify/dist/purify.es.mjs: + (*! @license DOMPurify 3.4.0 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.4.0/LICENSE *) + +js-yaml/dist/js-yaml.mjs: + (*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT *) + +lodash-es/lodash.js: + (** + * @license + * Lodash (Custom Build) + * Build: `lodash modularize exports="es" --repo lodash/lodash#4.18.1 -o ./` + * Copyright OpenJS Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + *) + +cytoscape/dist/cytoscape.esm.mjs: + (*! + Embeddable Minimum Strictly-Compliant Promises/A+ 1.1.1 Thenable + Copyright (c) 2013-2014 Ralf S. Engelschall (http://engelschall.com) + Licensed under The MIT License (http://opensource.org/licenses/MIT) + *) + (*! + Event object based on jQuery events, MIT license + + https://jquery.org/license/ + https://tldrlegal.com/license/mit-license + https://github.com/jquery/jquery/blob/master/src/event.js + *) + (*! Bezier curve function generator. Copyright Gaetan Renaudeau. MIT License: http://en.wikipedia.org/wiki/MIT_License *) + (*! Runge-Kutta spring physics function generator. Adapted from Framer.js, copyright Koen Bok. MIT License: http://en.wikipedia.org/wiki/MIT_License *) +*/ +globalThis["mermaid"] = globalThis.__esbuild_esm_mermaid_nm["mermaid"].default; diff --git a/static/sw.js b/static/sw.js index fb24d3fe6..735c0221a 100644 --- a/static/sw.js +++ b/static/sw.js @@ -7,7 +7,21 @@ // - Other static assets (images/fonts/libs): cache-first with bg refresh. // - API / non-GET: never cached. // Bump CACHE_NAME whenever the precache list or SW logic changes. -const CACHE_NAME = 'odysseus-v378-shared-config-image-editor'; +const CACHE_NAME = 'odysseus-v380-shared-config-image-editor-lazy-katex-mermaid'; + +// KaTeX resolves these from its own stylesheet, so caching the CSS without them +// gives offline math fallback glyphs instead of proper typesetting. +const KATEX_FONTS = [ + 'AMS-Regular', 'Caligraphic-Bold', 'Caligraphic-Regular', + 'Fraktur-Bold', 'Fraktur-Regular', + 'Main-Bold', 'Main-BoldItalic', 'Main-Italic', 'Main-Regular', + 'Math-BoldItalic', 'Math-Italic', + 'SansSerif-Bold', 'SansSerif-Italic', 'SansSerif-Regular', + 'Script-Regular', + 'Size1-Regular', 'Size2-Regular', 'Size3-Regular', 'Size4-Regular', + 'Typewriter-Regular', +].map(name => `/static/lib/katex/fonts/KaTeX_${name}.woff2`); + // Two lists, two jobs — they are no longer the same set and must not be // "resynced" back into one: @@ -73,6 +87,15 @@ const PRECACHE = [ '/static/js/sidebar-layout.js', '/static/js/section-management.js', '/static/lib/highlight.min.js', + // Math turns up in ordinary answers and KaTeX is small, so precaching it and + // its fonts keeps formulas typeset offline. Mermaid is deliberately NOT + // precached: at 3.5 MB it would re-download on every CACHE_NAME bump, a poor + // trade for a library most sessions never touch. The cache-first rule below + // picks it up the first time a diagram renders, which is also when it starts + // mattering offline. + '/static/lib/katex/katex.min.js', + '/static/lib/katex/katex.min.css', + ...KATEX_FONTS, ]; // Lazily-imported panel modules (js/panels.js). Not in index.html by design; diff --git a/tests/test_markdown_lazy_lib_loading_js.py b/tests/test_markdown_lazy_lib_loading_js.py new file mode 100644 index 000000000..e9d781caa --- /dev/null +++ b/tests/test_markdown_lazy_lib_loading_js.py @@ -0,0 +1,510 @@ +"""KaTeX and Mermaid must be vendored and fetched only on first real use. + +They used to load from cdn.jsdelivr.net in every , costing ~985 KB on the +wire per page load, breaking offline installs and announcing each session to a +third party. These tests pin the replacement contract: one fetch per library, +never before a formula or a ```mermaid fence actually shows up, and math still +renders once the library lands. +""" + +import json +import re +import shutil +import subprocess +import textwrap +from pathlib import Path + +import pytest + +_REPO = Path(__file__).resolve().parent.parent +_HAS_NODE = shutil.which("node") is not None + +MERMAID_SRC = "/static/lib/mermaid.min.js" +KATEX_SRC = "/static/lib/katex/katex.min.js" +KATEX_CSS = "/static/lib/katex/katex.min.css" + + +@pytest.fixture(scope="module") +def node_available(): + if not _HAS_NODE: + pytest.skip("node binary not on PATH") + + +def _katex_fonts_block(sw_source: str) -> str: + """The literal body of sw.js's KATEX_FONTS array.""" + match = re.search(r"const KATEX_FONTS = \[(.*?)\]", sw_source, re.S) + assert match, "sw.js no longer defines a KATEX_FONTS array" + return match.group(1) + + +# A DOM stub small enough to reason about: it records every - + - - + + diff --git a/static/js/chat.js b/static/js/chat.js index b19730050..a5c95e434 100644 --- a/static/js/chat.js +++ b/static/js/chat.js @@ -8,8 +8,8 @@ import Storage from './storage.js'; import uiModule from './ui.js'; import sessionModule from './sessions.js'; -import chatRenderer from './chatRenderer.js?v=20260815toolapproval4'; -import chatStream from './chatStream.js?v=20260815approvalsave1'; +import chatRenderer from './chatRenderer.js?v=20260819approvalcontrol1'; +import chatStream from './chatStream.js?v=20260819approvalcontrol1'; import { addAITTSButton } from './tts-ai.js'; import markdownModule from './markdown.js'; import spinnerModule from './spinner.js'; @@ -62,20 +62,18 @@ import { loadPanel } from './panels.js'; let _contextHeaderBound = false; let _pendingToolApproval = null; - function _submitToolApprovalWhenIdle(approvalId, label) { + function _submitToolApprovalWhenIdle(approvalId) { if ( !_pendingToolApproval || _pendingToolApproval.approval_id !== approvalId ) return; if (isStreaming || _sendInFlight) { - setTimeout(() => _submitToolApprovalWhenIdle(approvalId, label), 120); + setTimeout(() => _submitToolApprovalWhenIdle(approvalId), 120); return; } const input = document.getElementById('message'); if (input) { _pendingToolApproval.draft = input.value || ''; - input.value = label; - input.dispatchEvent(new Event('input', { bubbles: true })); } const sendButton = document.querySelector('.send-btn'); if (sendButton) sendButton.click(); @@ -84,16 +82,13 @@ import { loadPanel } from './panels.js'; document.addEventListener('odysseus:tool-approval', (event) => { const detail = event && event.detail ? event.detail : {}; const decision = String(detail.decision || '').toLowerCase(); - if (!detail.approval_id || !['approve', 'deny'].includes(decision)) return; + if (!detail.approval_id || !['approve', 'approve_task', 'deny'].includes(decision)) return; _pendingToolApproval = { approval_id: String(detail.approval_id), decision, document_id: String(detail.document_id || ''), }; - _submitToolApprovalWhenIdle( - _pendingToolApproval.approval_id, - detail.label || (decision === 'approve' ? 'Allow once' : 'Deny'), - ); + _submitToolApprovalWhenIdle(_pendingToolApproval.approval_id); }); function _fmtContextNumber(n) { @@ -1309,10 +1304,10 @@ import { loadPanel } from './panels.js'; } const el = uiModule.el; - const msg = el('message').value; + const msg = approvalForSend ? '' : el('message').value; // Allow empty text when a regen carries over the original message's // attachment ids — a photo-only message still has something to send. - if (!msg.trim() && !fileHandlerModule.getPendingCount() && !(_pendingRegenAttachments && _pendingRegenAttachments.length)) { _releaseSendFlag(); return; } + if (!msg.trim() && !approvalForSend && !fileHandlerModule.getPendingCount() && !(_pendingRegenAttachments && _pendingRegenAttachments.length)) { _releaseSendFlag(); return; } // --- Slash commands: execute directly without AI (no session needed) --- if (!approvalForSend && isCommand(msg.trim())) { @@ -1590,7 +1585,7 @@ import { loadPanel } from './panels.js'; const userDisplay = _displayOverride || msg; _displayOverride = null; - const skipBubble = _hideUserBubble; + const skipBubble = _hideUserBubble || !!approvalForSend; _hideUserBubble = false; // Auto-recovery counter: carries across a turn's auto-continues, but resets // when the user genuinely sends a new message (so each task gets a fresh cap). @@ -1833,7 +1828,7 @@ import { loadPanel } from './panels.js'; if (_inject.suffix) _finalMsgWithInject = _finalMsgWithInject + ' ' + _inject.suffix; const fd = new FormData(); - fd.append('message', _finalMsgWithInject); + fd.append('message', approvalForSend ? '' : _finalMsgWithInject); fd.append('session', streamSessionId); if (approvalForSend) { fd.append('tool_approval_id', approvalForSend.approval_id); @@ -2873,7 +2868,7 @@ import { loadPanel } from './panels.js'; if (spinner && spinner.element) spinner.destroy(); break; } - if (json.delta || json.type === 'agent_prep' || json.type === 'generated_image' || json.type === 'tool_start' || json.type === 'tool_output' || json.type === 'tool_progress' || json.type === 'agent_step' || json.type === 'loop_breaker_triggered' || json.type === 'intent_nudge_exhausted' || json.type === 'doc_stream_open' || json.type === 'doc_stream_delta' || json.type === 'research_progress') { + if (json.delta || json.type === 'agent_prep' || json.type === 'tool_approval_resolved' || json.type === 'generated_image' || json.type === 'tool_start' || json.type === 'tool_output' || json.type === 'tool_progress' || json.type === 'agent_step' || json.type === 'loop_breaker_triggered' || json.type === 'intent_nudge_exhausted' || json.type === 'doc_stream_open' || json.type === 'doc_stream_delta' || json.type === 'research_progress') { clearResponseTimeout(); clearProcessingProbe(); clearFirstTokenWaitTimers(); @@ -2890,6 +2885,14 @@ import { loadPanel } from './panels.js'; } continue; } + if (json.type === 'tool_approval_resolved') { + _cancelThinkingTimer(); + _removeThinkingSpinner(); + if (spinner && spinner.element) spinner.destroy(); + if (!_isBg && roundHolder && roundHolder !== holder) roundHolder.remove(); + if (!_isBg && holder) holder.remove(); + continue; + } if (json.delta) { _cancelThinkingTimer(); _removeThinkingSpinner(); diff --git a/static/js/chatRenderer.js b/static/js/chatRenderer.js index dfb936d4f..b5ed364f9 100644 --- a/static/js/chatRenderer.js +++ b/static/js/chatRenderer.js @@ -2327,6 +2327,42 @@ export function removeAskUserCards(root) { scope.querySelectorAll('.ask-user-card').forEach((node) => node.remove()); } +// While a choice card is visible, let plain 1–3 activate the corresponding +// rendered option. Reuse the option's click path so the question keeps its +// existing submission semantics. Tool approval cards are excluded: that card +// exists to make consent deliberate after untrusted context influenced the +// run, and its first option is the widest grant, so a stray digit must not +// answer it. +function _handleAskUserShortcut(event) { + if ( + event.defaultPrevented + || event.repeat + || event.isComposing + || event.ctrlKey + || event.altKey + || event.metaKey + || event.shiftKey + ) return; + if (!/^[1-3]$/.test(event.key)) return; + + const target = event.target; + if (target?.closest?.('input, textarea, select, [contenteditable="true"]')) return; + + const focusedCard = document.activeElement?.closest?.('.ask-user-card') || null; + const mainCard = document.querySelector('#chat-history .ask-user-card'); + const compareCards = document.querySelectorAll('.compare-pane .ask-user-card'); + const card = focusedCard || mainCard || (compareCards.length === 1 ? compareCards[0] : null); + if (!card) return; + if (card.dataset.askUserKind === 'tool_approval') return; + const option = card.querySelectorAll('.ask-user-option')[Number(event.key) - 1]; + if (!option || option.disabled) return; + + event.preventDefault(); + option.click(); +} + +document.addEventListener('keydown', _handleAskUserShortcut); + /** * Render an ask_user payload as a durable choice card. * @@ -2336,11 +2372,15 @@ export function removeAskUserCards(root) { */ export function renderAskUserCard(payload, options) { const aq = payload || {}; + if (aq.resolved) return null; const opts = Array.isArray(aq.options) ? aq.options : []; - const chatBox = document.getElementById('chat-history'); + const renderOptions = options || {}; + const chatBox = renderOptions.root || document.getElementById('chat-history'); + const onSubmit = typeof renderOptions.onSubmit === 'function' + ? renderOptions.onSubmit + : null; if (!chatBox || !aq.question || opts.length < 2) return null; - const renderOptions = options || {}; removeAskUserCards(chatBox); const card = document.createElement('div'); @@ -2349,6 +2389,7 @@ export function renderAskUserCard(payload, options) { card.tabIndex = -1; const multi = !!aq.multi; const isToolApproval = aq.kind === 'tool_approval' && !!aq.approval_id; + card.dataset.askUserKind = isToolApproval ? 'tool_approval' : 'question'; const emojiText = (value) => svgifyEmoji(uiModule.esc(String(value))); const head = document.createElement('div'); @@ -2357,7 +2398,6 @@ export function renderAskUserCard(payload, options) { closeBtn.type = 'button'; closeBtn.className = 'modal-close ask-user-close'; closeBtn.setAttribute('aria-label', 'Dismiss question'); - closeBtn.textContent = '×'; closeBtn.addEventListener('click', () => { card.remove(); const input = uiModule.el('message'); @@ -2400,6 +2440,17 @@ export function renderAskUserCard(payload, options) { const send = (text) => { if (!text) return; + if (onSubmit) { + const accepted = onSubmit({ + kind: 'answer', + text, + label: text, + payload: aq, + card, + }); + if (accepted !== false) card.remove(); + return; + } card.remove(); const input = uiModule.el('message'); if (input) input.value = text; @@ -2433,17 +2484,26 @@ export function renderAskUserCard(payload, options) { row.type = 'button'; row.addEventListener('click', () => { if (isToolApproval) { - card.remove(); - document.dispatchEvent(new CustomEvent('odysseus:tool-approval', { - detail: { - approval_id: aq.approval_id, - decision: String((opt && opt.value) || '').toLowerCase(), - label, - document_id: aq.action && aq.action.document_id - ? String(aq.action.document_id) - : '', - }, - })); + const detail = { + approval_id: aq.approval_id, + decision: String((opt && opt.value) || '').toLowerCase(), + label, + document_id: aq.action && aq.action.document_id + ? String(aq.action.document_id) + : '', + }; + if (onSubmit) { + const accepted = onSubmit({ + kind: 'tool_approval', + ...detail, + payload: aq, + card, + }); + if (accepted !== false) card.remove(); + } else { + card.remove(); + document.dispatchEvent(new CustomEvent('odysseus:tool-approval', { detail })); + } } else { send(label); } @@ -2628,7 +2688,7 @@ export function addMessage(role, content, modelName, metadata) { box.appendChild(threadWrap); } for (const ev of roundTools) { - if (ev.ask_user) pendingAskUser = ev.ask_user; + if (ev.ask_user && !ev.ask_user.resolved) pendingAskUser = ev.ask_user; const ok = (ev.exit_code === 0 || ev.exit_code == null); let outHtml = ''; if (ev.output && ev.output.trim()) { diff --git a/static/js/chatStream.js b/static/js/chatStream.js index 19bf66753..5e0a0e263 100644 --- a/static/js/chatStream.js +++ b/static/js/chatStream.js @@ -9,6 +9,35 @@ import markdownModule from './markdown.js'; import sessionModule from './sessions.js'; import documentModule from './document.js?v=20260815approvalsave1'; +// Tool approvals are control-plane submits for the current chat. chat.js +// deliberately leaves the composer untouched, then programmatically clicks the +// shared send button after it records the sealed approval id/decision. That +// button is polymorphic: with an empty composer it can mean New chat or Record +// voice instead of Send. Intercept only the programmatic approval click and +// route it through the form submit path, which already reaches chat.js directly. +document.addEventListener('odysseus:tool-approval', () => { + const sendButton = document.querySelector('.send-btn'); + const chatForm = document.getElementById('chat-form'); + if (!sendButton || !chatForm) return; + + const interceptApprovalClick = (event) => { + // A real user click must retain the normal send/new-chat/STT behavior. + if (event.isTrusted) return; + sendButton.removeEventListener('click', interceptApprovalClick, true); + event.preventDefault(); + event.stopImmediatePropagation(); + if (chatForm.requestSubmit) chatForm.requestSubmit(); + else chatForm.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); + }; + + sendButton.addEventListener('click', interceptApprovalClick, true); + // Fail-safe cleanup if the approval continuation never reaches its deferred + // synthetic click (for example because the surrounding view is torn down). + setTimeout(() => { + sendButton.removeEventListener('click', interceptApprovalClick, true); + }, 60000); +}, true); + /** * Handle a ui_control SSE event — AI-driven UI manipulation. * Extracted from the duplicated ui_control + tool_output.ui_event handlers. diff --git a/static/js/compare/index.js b/static/js/compare/index.js index 1c64e084b..120fb5836 100644 --- a/static/js/compare/index.js +++ b/static/js/compare/index.js @@ -21,7 +21,7 @@ import { EVAL_PROMPTS, WAVE_FRAMES, import { fetchModels, _persistSelections, _modelDisplayNames, getExcludedModels, setExcludedModels } from './models.js'; import { showModelSelector, disableToolToggles, restoreToolToggles, _syncToolbarIndicator } from './selector.js?v=20260723compareicon2'; import { _checkUnprobed, _clearProbeWaves } from './probe.js'; -import { streamToPane, _renderSearchResults, _runSynthForPane, _formatMs, registerStreamActions } from './stream.js'; +import { streamToPane, _renderSearchResults, _runSynthForPane, _formatMs, registerStreamActions } from './stream.js?v=20260819approvalcontrol1'; import { stopAll, stopPane, rerollPane, shufflePanePositions, resetCompare, _addPane, _removePane, toggleExpandPane, togglePanePreview, copyPaneResponse, @@ -1006,11 +1006,16 @@ async function _executeCompare(message) { console.error('Compare error:', err); if (uiModule) uiModule.showError('Compare failed: ' + err.message); } finally { - state._streaming = false; - _setSendBtn('send'); - // Re-enable header buttons - document.querySelectorAll('#compare-shuffle-btn, #compare-check-btn, #compare-add-btn').forEach(b => { - b.disabled = false; b.style.opacity = '0.7'; b.style.pointerEvents = ''; + // A pane may have started its own ask_user/approval continuation while the + // original all-pane Promise was settling. Keep Compare busy until every + // pane-owned controller is gone instead of exposing a second broadcast send. + const compareStillStreaming = state._abortControllers.some(Boolean); + state._streaming = compareStillStreaming; + _setSendBtn(compareStillStreaming ? 'stop' : 'send'); + document.querySelectorAll('#compare-shuffle-btn, #compare-check-btn, #compare-add-btn').forEach((button) => { + button.disabled = compareStillStreaming; + button.style.opacity = compareStillStreaming ? '0.25' : '0.7'; + button.style.pointerEvents = compareStillStreaming ? 'none' : ''; }); } } @@ -1514,7 +1519,7 @@ async function showShufflePoolEditor() { // ──────────────────────────────────────────────────────────────────────────── registerCompareActions({ stopAll, resetCompare }); -registerStreamActions({ rerollPane, autoPreviewHtml: _autoPreviewHtml }); +registerStreamActions({ rerollPane, autoPreviewHtml: _autoPreviewHtml, setSendBtn: _setSendBtn }); registerPaneActions({ setSendBtn: _setSendBtn, deactivate, streamToPane, renderSearchResults: _renderSearchResults, fetchModels }); // ──────────────────────────────────────────────────────────────────────────── diff --git a/static/js/compare/stream.js b/static/js/compare/stream.js index 5bb7f9bcc..7f41797fd 100644 --- a/static/js/compare/stream.js +++ b/static/js/compare/stream.js @@ -1,7 +1,7 @@ // compare/stream.js — SSE streaming to panes import state from './state.js'; import { addFinishBadge } from './vote.js'; -import { getModelCost, safeDisplayImageSrc } from '../chatRenderer.js'; +import { getModelCost, renderAskUserCard, safeDisplayImageSrc } from '../chatRenderer.js?v=20260819approvalcontrol1'; import markdownModule from '../markdown.js'; import spinnerModule from '../spinner.js'; import uiModule from '../ui.js'; @@ -24,11 +24,157 @@ function _safeHttpHref(raw) { // ── Lazy-registered functions from compare.js (avoids circular deps) ── let _rerollPane = null; let _autoPreviewHtml = null; +let _setSendBtn = null; /** Register external functions that live in compare.js. */ -function registerStreamActions({ rerollPane, autoPreviewHtml }) { +function registerStreamActions({ rerollPane, autoPreviewHtml, setSendBtn }) { _rerollPane = rerollPane; _autoPreviewHtml = autoPreviewHtml; + _setSendBtn = setSendBtn; +} + +function _paneSessionIsCurrent(paneIdx, sessionId) { + return Boolean( + state.isActive + && state._paneSessionIds[paneIdx] === sessionId + && document.getElementById('cmp-history-' + paneIdx) + ); +} + +function _setCompareBusy(active) { + state._streaming = Boolean(active); + if (_setSendBtn) _setSendBtn(active ? 'stop' : 'send'); + document.querySelectorAll('#compare-shuffle-btn, #compare-check-btn, #compare-add-btn').forEach((button) => { + button.disabled = Boolean(active); + button.style.opacity = active ? '0.25' : '0.7'; + button.style.pointerEvents = active ? 'none' : ''; + }); +} + +function _syncCompareBusyFromPanes() { + _setCompareBusy((state._abortControllers || []).some(Boolean)); +} + +function _appendPaneMessage(hist, role, text) { + const message = document.createElement('div'); + message.className = 'msg ' + (role === 'user' ? 'msg-user' : 'msg-ai'); + const roleEl = document.createElement('div'); + roleEl.className = 'role'; + roleEl.textContent = role === 'user' ? 'You' : 'AI'; + const body = document.createElement('div'); + body.className = 'body'; + body.textContent = text || ''; + message.appendChild(roleEl); + message.appendChild(body); + hist.appendChild(message); + return message; +} + +function _createPaneContinuationMessage(hist) { + const message = _appendPaneMessage(hist, 'assistant', ''); + const body = message.querySelector('.body'); + if (spinnerModule) { + const spinner = spinnerModule.create('Continuing...', 'right'); + body.appendChild(spinner.createElement()); + spinner.start(); + message._spinner = spinner; + } + return message; +} + +function _restorePaneAskUserCard(paneIdx, sessionId, submission, originController) { + const hist = document.getElementById('cmp-history-' + paneIdx); + const restored = _renderPaneAskUserCard( + paneIdx, + sessionId, + submission.payload || {}, + hist, + null, + originController, + ); + if (uiModule) { + uiModule.showError( + restored + ? 'This pane is still streaming — choose again once it settles.' + : 'Compare pane is still streaming; the choice was not sent.', + ); + } + return restored; +} + +function _resumePaneChoiceWhenIdle(paneIdx, sessionId, originController, submission) { + if (!_paneSessionIsCurrent(paneIdx, sessionId)) return false; + + const startedAt = Date.now(); + const resume = () => { + if (!_paneSessionIsCurrent(paneIdx, sessionId)) return; + const activeController = state._abortControllers[paneIdx]; + if (activeController === originController) { + if (Date.now() - startedAt < 10000) { + setTimeout(resume, 25); + return; + } + // The originating stream never released the pane. The card was already + // removed when the choice was accepted, so put it back rather than + // swallowing a decision the user made. + _restorePaneAskUserCard(paneIdx, sessionId, submission, originController); + return; + } + // A reroll/model replacement already owns this pane. Never send the stale + // choice into that replacement stream or session UI. + if (activeController) return; + + const hist = document.getElementById('cmp-history-' + paneIdx); + if (!hist) return; + hist.querySelectorAll('.ask-user-card').forEach((card) => card.remove()); + + const isApproval = submission.kind === 'tool_approval'; + const message = isApproval ? '' : String(submission.text || submission.label || ''); + if (!isApproval) _appendPaneMessage(hist, 'user', message); + const aiMessage = _createPaneContinuationMessage(hist); + hist.scrollTop = hist.scrollHeight; + + const resumeOptions = { skipBadge: true }; + if (isApproval) { + resumeOptions.toolApproval = { + approval_id: String(submission.approval_id || ''), + decision: String(submission.decision || '').toLowerCase(), + }; + } + + _setCompareBusy(true); + streamToPane(paneIdx, sessionId, message, aiMessage, resumeOptions) + .catch((error) => { + console.error('Compare pane continuation failed:', error); + if (uiModule) uiModule.showError('Compare continuation failed: ' + error.message); + }) + .finally(_syncCompareBusyFromPanes); + }; + + setTimeout(resume, 0); + return true; +} + +function _renderPaneAskUserCard(paneIdx, sessionId, payload, hist, aiMsgEl, originController) { + if (!hist || !hist.isConnected || !_paneSessionIsCurrent(paneIdx, sessionId)) return null; + if (aiMsgEl && aiMsgEl._spinner) { + if (aiMsgEl._spinner.element) aiMsgEl._spinner.destroy(); + aiMsgEl._spinner = null; + } + const card = renderAskUserCard(payload, { + root: hist, + onSubmit: (submission) => _resumePaneChoiceWhenIdle( + paneIdx, + sessionId, + originController, + submission, + ), + }); + if (card) { + card.dataset.comparePane = String(paneIdx); + card.dataset.compareSession = String(sessionId); + } + return card; } /** Format milliseconds as human-readable duration (e.g. "120ms", "1.23s", "4.5s"). */ @@ -164,6 +310,7 @@ async function streamToPane(paneIdx, sessionId, message, aiMsgEl, opts) { let metrics = null; let timedOut = false; let streamOk = false; + let awaitingChoice = false; let currentToolBlock = null; // track active agent tool block // Idle timeout — abort only if no data is received for this many seconds. // Long generations (SVG, big code) are fine as long as the stream stays @@ -219,6 +366,10 @@ async function streamToPane(paneIdx, sessionId, message, aiMsgEl, opts) { const fd = new FormData(); fd.append('message', message); fd.append('session', sessionId); + if (opts.toolApproval) { + fd.append('tool_approval_id', opts.toolApproval.approval_id || ''); + fd.append('tool_approval_decision', opts.toolApproval.decision || ''); + } // Compare mode determines what tools/features are enabled const isAgent = state._compareMode === 'agent'; @@ -322,6 +473,36 @@ async function streamToPane(paneIdx, sessionId, message, aiMsgEl, opts) { } } + // ── Pane-local question / approval selector ── + } else if (json.type === 'ask_user') { + awaitingChoice = true; + _renderPaneAskUserCard( + paneIdx, + sessionId, + json.data || {}, + hist, + aiMsgEl, + ac, + ); + if (hist) hist.scrollTop = hist.scrollHeight; + + // Deny ends as a tiny resolution-only stream, so replace the + // continuation spinner with an explicit pane-local result. + } else if (json.type === 'tool_approval_resolved') { + if (aiMsgEl._spinner) { + if (aiMsgEl._spinner.element) aiMsgEl._spinner.destroy(); + aiMsgEl._spinner = null; + } + accumulated = json.decision === 'deny' ? 'Denied.' : 'Approval recorded.'; + let target = aiMsgEl._textEl; + if (!target) { + target = document.createElement('div'); + target.className = 'compare-text-content'; + aiBody.appendChild(target); + aiMsgEl._textEl = target; + } + target.textContent = accumulated; + // ── Tool start (bash, web search agent tool) ── } else if (json.type === 'tool_start') { // Finalize any accumulated text before the tool block @@ -640,19 +821,21 @@ async function streamToPane(paneIdx, sessionId, message, aiMsgEl, opts) { // TTFT removed from the header per user request — just show total time. _timerEl.textContent = _formatMs(_totalMs); } - state._abortControllers[paneIdx] = null; + if (state._abortControllers[paneIdx] === ac) { + state._abortControllers[paneIdx] = null; + } // Hide stop button, show response action buttons const _paneElFinal = document.querySelector(`.compare-pane[data-pane="${paneIdx}"]`); if (_paneElFinal) { const _stopBtnFinal = _paneElFinal.querySelector('.pane-stop-btn'); if (_stopBtnFinal) _stopBtnFinal.style.display = 'none'; - if (accumulated.trim()) { + if (!awaitingChoice && accumulated.trim()) { _paneElFinal.querySelectorAll('.pane-needs-response').forEach(b => b.style.display = ''); } } state._paneMetrics[paneIdx] = metrics; state._paneElapsed[paneIdx] = _totalMs; - if (!opts.skipBadge) { + if (!opts.skipBadge && !awaitingChoice) { if (streamOk) { state._finishOrder++; if (state._parallel) { @@ -682,7 +865,7 @@ async function streamToPane(paneIdx, sessionId, message, aiMsgEl, opts) { } } // Auto-grade against expected answer — stamps ✓ or ✗ on the pane header. - if (streamOk && state._expectedAnswer) { + if (streamOk && !awaitingChoice && state._expectedAnswer) { _stampGradeBadge(paneIdx, accumulated, state._expectedAnswer); } // Show copy/reroll buttons now that response exists diff --git a/tests/test_compare_ask_user_routing.py b/tests/test_compare_ask_user_routing.py new file mode 100644 index 000000000..2634c1381 --- /dev/null +++ b/tests/test_compare_ask_user_routing.py @@ -0,0 +1,61 @@ +from pathlib import Path + + +def test_compare_renders_ask_user_in_the_originating_pane(): + root = Path(__file__).resolve().parents[1] + stream = (root / "static/js/compare/stream.js").read_text(encoding="utf-8") + + assert "renderAskUserCard" in stream + assert "} else if (json.type === 'ask_user') {" in stream + assert "root: hist" in stream + assert "state._paneSessionIds[paneIdx] === sessionId" in stream + assert "streamToPane(paneIdx, sessionId, message, aiMessage, resumeOptions)" in stream + assert "handleCompareSubmit" not in stream + + +def test_compare_submits_approval_only_to_the_pane_session(): + root = Path(__file__).resolve().parents[1] + stream = (root / "static/js/compare/stream.js").read_text(encoding="utf-8") + + assert "fd.append('tool_approval_id', opts.toolApproval.approval_id || '');" in stream + assert "fd.append('tool_approval_decision', opts.toolApproval.decision || '');" in stream + assert "const isApproval = submission.kind === 'tool_approval';" in stream + assert "const message = isApproval ? ''" in stream + assert "if (!isApproval) _appendPaneMessage(hist, 'user', message);" in stream + assert "json.type === 'tool_approval_resolved'" in stream + assert "json.decision === 'deny' ? 'Denied.'" in stream + + +def test_compare_continuation_does_not_lose_or_replace_pane_ownership(): + root = Path(__file__).resolve().parents[1] + stream = (root / "static/js/compare/stream.js").read_text(encoding="utf-8") + index = (root / "static/js/compare/index.js").read_text(encoding="utf-8") + + assert "if (state._abortControllers[paneIdx] === ac)" in stream + assert "if (activeController === originController)" in stream + assert "if (activeController) return;" in stream + assert "registerStreamActions({ rerollPane, autoPreviewHtml: _autoPreviewHtml, setSendBtn: _setSendBtn });" in index + assert "const compareStillStreaming = state._abortControllers.some(Boolean);" in index + assert "_setSendBtn(compareStillStreaming ? 'stop' : 'send');" in index + + +def test_compare_restores_the_card_instead_of_dropping_a_timed_out_choice(): + """The card is removed the moment a choice is accepted. + + If the originating stream still owns the pane when the resume deadline + passes, the decision has nowhere to go — so the card has to come back + rather than the click vanishing silently. + """ + + root = Path(__file__).resolve().parents[1] + stream = (root / "static/js/compare/stream.js").read_text(encoding="utf-8") + + assert "function _restorePaneAskUserCard(" in stream + assert "_restorePaneAskUserCard(paneIdx, sessionId, submission, originController);" in stream + assert "submission.payload || {}" in stream + + start = stream.index("function _resumePaneChoiceWhenIdle(") + end = stream.index("function _renderPaneAskUserCard(", start) + resume = stream[start:end] + # The deadline must not fall through to a bare return any more. + assert "if (Date.now() - startedAt < 10000) setTimeout(resume, 25);" not in resume diff --git a/tests/test_foreground_model_routing.py b/tests/test_foreground_model_routing.py index e2ceb8762..fc77956ac 100644 --- a/tests/test_foreground_model_routing.py +++ b/tests/test_foreground_model_routing.py @@ -344,7 +344,7 @@ async def test_chat_stream_approval_restores_exact_shell_turn_toggle(monkeypatch @pytest.mark.asyncio -async def test_chat_stream_denial_keeps_originating_run_tainted(monkeypatch): +async def test_chat_stream_denial_returns_control_resolution(monkeypatch): from src.tool_capabilities import capabilities_for_action captured = {} @@ -368,11 +368,13 @@ async def test_chat_stream_denial_keeps_originating_run_tainted(monkeypatch): ) response = await endpoint(request) - async for _ in response.body_iterator: - pass + chunks = [chunk async for chunk in response.body_iterator] + event = json.loads(chunks[0][len("data: "):]) + assert event == {"type": "tool_approval_resolved", "decision": "deny"} + assert chunks[-1] == "data: [DONE]\n\n" + assert "agent" not in captured assert "exact_approval" not in captured - assert captured["agent_external_untrusted_context_seen"] is True assert chat_routes.tool_approval_store.peek(pending.approval_id) is None diff --git a/tests/test_tool_approval_frontend_routing.py b/tests/test_tool_approval_frontend_routing.py new file mode 100644 index 000000000..51935c79c --- /dev/null +++ b/tests/test_tool_approval_frontend_routing.py @@ -0,0 +1,103 @@ +from pathlib import Path + + +def test_tool_approval_bypasses_polymorphic_send_button_actions(): + root = Path(__file__).resolve().parents[1] + chat = (root / "static/js/chat.js").read_text(encoding="utf-8") + stream = (root / "static/js/chatStream.js").read_text(encoding="utf-8") + + # chat.js still defers the sealed approval through a synthetic button click. + assert "if (sendButton) sendButton.click();" in chat + + # The capture listener must intercept only that synthetic click and route it + # through the chat form submit path, before app.js can reinterpret an empty + # composer as New chat or Record voice. + assert "if (event.isTrusted) return;" in stream + assert "event.stopImmediatePropagation();" in stream + assert "chatForm.requestSubmit()" in stream + assert "sendButton.dataset.mode = ''" not in stream + + +def test_ask_user_close_button_uses_one_css_glyph(): + root = Path(__file__).resolve().parents[1] + renderer = (root / "static/js/chatRenderer.js").read_text(encoding="utf-8") + styles = (root / "static/style.css").read_text(encoding="utf-8") + + assert "closeBtn.className = 'modal-close ask-user-close';" in renderer + assert "closeBtn.setAttribute('aria-label', 'Dismiss question');" in renderer + assert "closeBtn.textContent = '×';" not in renderer + assert ".modal-close::before" in styles + + +def test_ask_user_number_shortcuts_reuse_option_click_path(): + root = Path(__file__).resolve().parents[1] + renderer = (root / "static/js/chatRenderer.js").read_text(encoding="utf-8") + start = renderer.index("function _handleAskUserShortcut(event)") + end = renderer.index("document.addEventListener('keydown', _handleAskUserShortcut);", start) + shortcut = renderer[start:end] + + assert "if (!/^[1-3]$/.test(event.key)) return;" in shortcut + assert "event.repeat" in shortcut + assert "event.ctrlKey" in shortcut + assert "event.altKey" in shortcut + assert "event.metaKey" in shortcut + assert "event.shiftKey" in shortcut + assert "input, textarea, select, [contenteditable=\"true\"]" in shortcut + assert "card.querySelectorAll('.ask-user-option')[Number(event.key) - 1]" in shortcut + assert "event.preventDefault();" in shortcut + assert "option.click();" in shortcut + + +def test_digit_shortcuts_never_answer_a_tool_approval_card(): + """A stray digit must not grant a scope the user did not deliberately pick.""" + + root = Path(__file__).resolve().parents[1] + renderer = (root / "static/js/chatRenderer.js").read_text(encoding="utf-8") + start = renderer.index("function _handleAskUserShortcut(event)") + end = renderer.index("document.addEventListener('keydown', _handleAskUserShortcut);", start) + shortcut = renderer[start:end] + + assert "if (card.dataset.askUserKind === 'tool_approval') return;" in shortcut + # The renderer has to label the card for that guard to ever fire. + assert ( + "card.dataset.askUserKind = isToolApproval ? 'tool_approval' : 'question';" + in renderer + ) + + +def test_ask_user_renderer_accepts_scoped_root_and_submit_callback(): + root = Path(__file__).resolve().parents[1] + renderer = (root / "static/js/chatRenderer.js").read_text(encoding="utf-8") + + assert "const chatBox = renderOptions.root || document.getElementById('chat-history');" in renderer + assert "const onSubmit = typeof renderOptions.onSubmit === 'function'" in renderer + assert "kind: 'answer'" in renderer + assert "kind: 'tool_approval'" in renderer + assert "if (accepted !== false) card.remove();" in renderer + assert "document.dispatchEvent(new CustomEvent('odysseus:tool-approval', { detail }))" in renderer + + +def test_every_changed_approval_module_is_cache_busted_together(): + """A stale module here silently reinterprets the approval click. + + chat.js leaves the composer empty and clicks the polymorphic send button, + so a browser that pairs the new chat.js with a cached chatStream.js has no + interceptor and lands on the New chat branch instead. The same holds for + the compare pane modules, which chatRenderer now shares a keydown listener + with. + """ + + root = Path(__file__).resolve().parents[1] + version = "20260819approvalcontrol1" + index = (root / "static/index.html").read_text(encoding="utf-8") + app = (root / "static/app.js").read_text(encoding="utf-8") + chat = (root / "static/js/chat.js").read_text(encoding="utf-8") + compare_index = (root / "static/js/compare/index.js").read_text(encoding="utf-8") + compare_stream = (root / "static/js/compare/stream.js").read_text(encoding="utf-8") + + assert f"chatStream.js?v={version}" in index + assert f"chatStream.js?v={version}" in chat + assert f"compare/index.js?v={version}" in app + assert f"stream.js?v={version}" in compare_index + # One chatRenderer instance, so the ask_user keydown listener binds once. + assert f"chatRenderer.js?v={version}" in compare_stream diff --git a/tests/test_tool_approval_single_action_scope.py b/tests/test_tool_approval_single_action_scope.py new file mode 100644 index 000000000..f3673b331 --- /dev/null +++ b/tests/test_tool_approval_single_action_scope.py @@ -0,0 +1,89 @@ +"""Callers with no resumable chat keep the original one-use approval scope. + +The chat card reuses the wire value ``approve`` for chat-session scope, so any +caller that still sends ``approve`` meaning "once" has to say so explicitly or +it silently inherits a run-long gate bypass. +""" + +from pathlib import Path + +from src.tool_approval_scopes import ToolApprovalScope +from src.tool_approvals import ToolApprovalStore +from src.tool_capabilities import ToolRunSecurityContext, capabilities_for_action + + +def _pending(store: ToolApprovalStore, *, session_id=""): + content = "printf exact" + return store.create( + owner="Alice", + session_id=session_id, + origin_run_id="run-1", + tool_name="bash", + content=content, + workspace=None, + external_untrusted_context_seen=True, + capabilities=capabilities_for_action("bash", content), + ) + + +def test_single_action_grant_leaves_the_gate_armed_behind_the_sealed_action(): + store = ToolApprovalStore() + pending = _pending(store) + + grant = store.consume( + pending.approval_id, + decision="approve", + owner="alice", + session_id=None, + allow_continuation=False, + ) + + assert grant is not None + assert grant.scope is ToolApprovalScope.SINGLE_ACTION + assert grant.allow_remaining_actions is False + assert grant.grants_chat_session is False + + resumed = ToolRunSecurityContext( + external_untrusted_context_seen=True, + approval_gate_bypassed=grant.allow_remaining_actions, + ) + assert resumed.decision_for("bash").allowed is False + + +def test_chat_callers_still_get_the_continuation_scope_they_asked_for(): + store = ToolApprovalStore() + pending = _pending(store, session_id="session-1") + + grant = store.consume( + pending.approval_id, + decision="approve_task", + owner="alice", + session_id="session-1", + ) + + assert grant is not None + assert grant.scope is ToolApprovalScope.TASK + assert grant.allow_remaining_actions is True + + +def test_deny_is_unaffected_by_the_single_action_flag(): + store = ToolApprovalStore() + pending = _pending(store) + + assert store.consume( + pending.approval_id, + decision="deny", + owner="alice", + session_id=None, + allow_continuation=False, + ) is None + assert store.peek(pending.approval_id) is None + + +def test_skill_test_approval_route_opts_out_of_continuation(): + root = Path(__file__).resolve().parents[1] + skills = (root / "routes/skills_routes.py").read_text(encoding="utf-8") + + approve_call = skills.index("exact_approval = tool_approval_store.consume(") + end = skills.index(")", skills.index("allow_continuation", approve_call)) + assert "allow_continuation=False" in skills[approve_call:end] diff --git a/tests/test_tool_approval_task_scope.py b/tests/test_tool_approval_task_scope.py new file mode 100644 index 000000000..00803939a --- /dev/null +++ b/tests/test_tool_approval_task_scope.py @@ -0,0 +1,351 @@ +"""Task- and chat-scoped approval continuation coverage for issue #6112.""" + +import asyncio +import json +from dataclasses import replace +from pathlib import Path +from types import SimpleNamespace + +from core.models import ChatMessage, Session +from src.tool_approval_scopes import ( + CHAT_SESSION_APPROVAL_CONTEXT_MARKER, + ToolApprovalScope, +) +from src.tool_approvals import ExactToolApproval, ToolApprovalStore +from src.tool_capabilities import ToolRunSecurityContext, capabilities_for_action + + +def _pending( + store: ToolApprovalStore, + *, + selected_tools=None, + continuation_query="inspect the project using memory and skills", +): + content = "printf exact" + return store.create( + owner="Alice", + session_id="session-1", + origin_run_id="run-1", + tool_name="bash", + content=content, + workspace=None, + external_untrusted_context_seen=True, + selected_tools=selected_tools, + continuation_query=continuation_query, + capabilities=capabilities_for_action("bash", content), + ) + + +def test_card_offers_task_chat_session_and_deny_without_leaking_private_state(): + pending = _pending( + ToolApprovalStore(), + selected_tools=["manage_skills", "bash", "manage_skills"], + ) + + payload = pending.public_payload() + + assert payload["session_id"] == "session-1" + assert [option["value"] for option in payload["options"]] == [ + "approve_task", + "approve", + "deny", + ] + assert [option["label"] for option in payload["options"]] == [ + "Allow for this task", + "Allow for this chat session", + "Deny", + ] + serialized = json.dumps(payload, sort_keys=True) + assert "Allow once" not in serialized + assert "selected_tools" not in serialized + assert "continuation_query" not in serialized + assert "manage_skills" not in serialized + assert "inspect the project" not in serialized + + +def test_allow_for_task_bypasses_only_the_resumed_run_gate(): + store = ToolApprovalStore() + pending = _pending(store, selected_tools=["bash", "manage_skills"]) + grant = store.consume( + pending.approval_id, + decision="approve_task", + owner="alice", + session_id="session-1", + ) + + assert grant is not None + assert grant.scope is ToolApprovalScope.TASK + assert grant.allow_remaining_actions is True + assert grant.grants_chat_session is False + assert grant.pending.continuation_query == ( + "inspect the project using memory and skills" + ) + + resumed = ToolRunSecurityContext( + external_untrusted_context_seen=True, + approval_gate_bypassed=grant.allow_remaining_actions, + ) + assert resumed.decision_for("bash").allowed is True + + # A new ordinary user turn constructs a fresh context and asks again. + fresh = ToolRunSecurityContext(external_untrusted_context_seen=True) + assert fresh.decision_for("bash").allowed is False + + +def test_allow_for_chat_session_applies_to_later_turns_in_only_that_chat(): + store = ToolApprovalStore() + pending = _pending(store, selected_tools=["bash", "manage_skills"]) + grant = store.consume( + pending.approval_id, + decision="approve", + owner="alice", + session_id="session-1", + ) + + assert grant is not None + assert grant.scope is ToolApprovalScope.CHAT_SESSION + assert grant.allow_remaining_actions is True + assert grant.grants_chat_session is True + assert grant.pending.selected_tools == ("bash", "manage_skills") + assert grant.pending.continuation_query.startswith("inspect the project") + + resolved_card = pending.public_payload() + resolved_card["resolved"] = "approve" + history = [ + ChatMessage( + "assistant", + "approval requested", + {"tool_events": [{"ask_user": resolved_card}]}, + ), + ChatMessage("user", "continue the work"), + ] + session = Session( + id="session-1", + name="Chat", + endpoint_url="http://example.invalid", + model="test", + history=history, + ) + + messages = session.get_context_messages() + assert messages[-1]["metadata"][CHAT_SESSION_APPROVAL_CONTEXT_MARKER] is True + assert history[-1].metadata is None + + future_turn = ToolRunSecurityContext(external_untrusted_context_seen=True) + future_turn.observe_messages(messages) + assert future_turn.approval_gate_bypassed is True + assert future_turn.decision_for("bash").allowed is True + + # The persisted card is bound to its original chat id, so a fork/copy does + # not inherit the grant merely by copying transcript metadata. + other_session = Session( + id="session-2", + name="Fork", + endpoint_url="http://example.invalid", + model="test", + history=history, + ) + other_messages = other_session.get_context_messages() + assert CHAT_SESSION_APPROVAL_CONTEXT_MARKER not in ( + other_messages[-1].get("metadata") or {} + ) + other_turn = ToolRunSecurityContext(external_untrusted_context_seen=True) + other_turn.observe_messages(other_messages) + assert other_turn.decision_for("bash").allowed is False + + +def test_deny_executes_nothing_and_grants_no_task_or_chat_scope(): + store = ToolApprovalStore() + pending = _pending(store) + + assert store.consume( + pending.approval_id, + decision="deny", + owner="alice", + session_id="session-1", + ) is None + assert store.peek(pending.approval_id) is None + + denied_card = pending.public_payload() + denied_card["resolved"] = "deny" + session = Session( + id="session-1", + name="Chat", + endpoint_url="http://example.invalid", + model="test", + history=[ + ChatMessage( + "assistant", + "approval requested", + {"tool_events": [{"ask_user": denied_card}]}, + ), + ChatMessage("user", "another request"), + ], + ) + messages = session.get_context_messages() + assert CHAT_SESSION_APPROVAL_CONTEXT_MARKER not in ( + messages[-1].get("metadata") or {} + ) + + +def test_private_continuation_state_is_canonical_bounded_and_digest_bound(): + selected_tools = ["manage_skills", "bash", "manage_skills", "", 7] + selected_tools.extend(f"tool_{index:04d}" for index in range(600)) + selected_tools.append("x" * 513) + pending = _pending( + ToolApprovalStore(), + selected_tools=selected_tools, + continuation_query=" " + ("original request " * 500), + ) + assert pending.selected_tools[:2] == ("bash", "manage_skills") + assert len(pending.selected_tools) == 512 + assert all(len(name) <= 512 for name in pending.selected_tools) + assert "x" * 513 not in pending.selected_tools + assert pending.continuation_query.startswith("original request") + assert len(pending.continuation_query) == 4000 + + tampered = replace( + pending, + selected_tools=("bash", "manage_skills", "send_email"), + continuation_query="different request", + ) + grant = ExactToolApproval(tampered) + assert grant.matches( + owner="alice", + session_id="session-1", + tool_name="bash", + content="printf exact", + workspace=None, + ) is False + + +def test_consumed_card_resolution_updates_memory_and_persisted_metadata(monkeypatch): + from routes import chat_routes + + ask_user = { + "kind": "tool_approval", + "approval_id": "approval-1", + "session_id": "session-1", + } + metadata = { + "_db_id": "message-1", + "tool_events": [{"ask_user": ask_user}], + } + sess = SimpleNamespace( + id="session-1", + history=[SimpleNamespace(metadata=metadata)], + ) + db_message = SimpleNamespace(meta_data=None) + + class Column: + def __eq__(self, value): + return value + + class FakeDBMessage: + id = Column() + session_id = Column() + + class FakeQuery: + def filter(self, *conditions): + return self + + def first(self): + return db_message + + class FakeDB: + committed = False + rolled_back = False + closed = False + + def query(self, model): + assert model is FakeDBMessage + return FakeQuery() + + def commit(self): + self.committed = True + + def rollback(self): + self.rolled_back = True + + def close(self): + self.closed = True + + db = FakeDB() + monkeypatch.setattr(chat_routes, "DBChatMessage", FakeDBMessage) + monkeypatch.setattr(chat_routes, "SessionLocal", lambda: db) + + assert chat_routes._mark_tool_approval_resolved( + sess, + "approval-1", + "approve", + ) is True + assert ask_user["resolved"] == "approve" + persisted = json.loads(db_message.meta_data) + assert persisted["tool_events"][0]["ask_user"]["resolved"] == "approve" + assert "_db_id" not in persisted + assert db.committed is True + assert db.rolled_back is False + assert db.closed is True + + +def test_deny_resolution_stream_is_control_only(): + from routes.chat_routes import _tool_approval_resolution_stream + + async def collect(): + return [chunk async for chunk in _tool_approval_resolution_stream("deny")] + + chunks = asyncio.run(collect()) + assert chunks[-1] == "data: [DONE]\n\n" + event = json.loads(chunks[0][len("data: "):]) + assert event == {"type": "tool_approval_resolved", "decision": "deny"} + assert "Denied the" not in "".join(chunks) + + +def test_route_context_agent_frontend_and_cache_bust_wire_the_contract(): + root = Path(__file__).resolve().parents[1] + route = (root / "routes/chat_routes.py").read_text(encoding="utf-8") + helpers = (root / "routes/chat_helpers.py").read_text(encoding="utf-8") + agent = (root / "src/agent_loop.py").read_text(encoding="utf-8") + frontend = (root / "static/js/chat.js").read_text(encoding="utf-8") + renderer = (root / "static/js/chatRenderer.js").read_text(encoding="utf-8") + app = (root / "static/app.js").read_text(encoding="utf-8") + index = (root / "static/index.html").read_text(encoding="utf-8") + approvals = (root / "src/tool_approvals.py").read_text(encoding="utf-8") + capabilities = (root / "src/tool_capabilities.py").read_text(encoding="utf-8") + models = (root / "core/models.py").read_text(encoding="utf-8") + + assert 'decision not in {"approve", "approve_task", "deny"}' in route + assert "set(pending_tool_approval.selected_tools)" in route + assert "pending_tool_approval.continuation_query" in route + assert "persist_user_message=not tool_approval_continuation" in route + assert "_mark_tool_approval_resolved(" in route + assert "_tool_approval_resolution_stream(decision)" in route + assert "Approved the exact" not in route + assert "Denied the" not in route + assert "continuation_context_message: str | None = None" in helpers + assert "persist_user_message: bool = True" in helpers + assert "_without_latest_matching_user_message(" not in helpers + assert "selected_tools=approval_selected_tools" in agent + assert "continuation_query=_retrieval_query or _last_user" in agent + assert "approval_gate_bypassed=bool(" in agent + assert "['approve', 'approve_task', 'deny']" in frontend + assert "input.value = label" not in frontend + assert "const msg = approvalForSend ? '' : el('message').value;" in frontend + assert "const skipBubble = _hideUserBubble || !!approvalForSend;" in frontend + assert "fd.append('message', approvalForSend ? '' : _finalMsgWithInject);" in frontend + assert "json.type === 'tool_approval_resolved'" in frontend + assert "if (aq.resolved) return null;" in renderer + assert "ev.ask_user && !ev.ask_user.resolved" in renderer + assert '"label": "Allow once"' not in approvals + assert '"label": "Allow for this task"' in approvals + assert '"label": "Allow for this chat session"' in approvals + assert "scope_for_decision(normalized_decision)" in approvals + assert "CHAT_SESSION_APPROVAL_CONTEXT_MARKER" in capabilities + assert "CHAT_SESSION_APPROVAL_CONTEXT_MARKER" in models + + version = "20260819approvalcontrol1" + assert f"chat.js?v={version}" in app + assert f"chat.js?v={version}" in index + assert f"chatRenderer.js?v={version}" in frontend + assert f"chatRenderer.js?v={version}" in app + assert f"chatRenderer.js?v={version}" in index From 85297cee44f8c5b3aa4bbf54ab482f5f7513baa5 Mon Sep 17 00:00:00 2001 From: Nikhil Chaudhary Date: Wed, 19 Aug 2026 21:08:24 +0530 Subject: [PATCH 124/125] fix(core): clean up orphaned temp files on atomic write failure (#6068) * fix(core): clean up orphaned temp files on atomic write failure * fixed reviewer suggestion * removed whitespace --- core/atomic_io.py | 38 +++++++++++++++++++++++--------- tests/test_atomic_io.py | 49 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/core/atomic_io.py b/core/atomic_io.py index 40a51adbe..831b90848 100644 --- a/core/atomic_io.py +++ b/core/atomic_io.py @@ -30,11 +30,20 @@ def atomic_write_json(path: str, data: Any, *, indent: Optional[int] = None) -> """ os.makedirs(os.path.dirname(path) or ".", exist_ok=True) tmp = f"{path}.tmp.{uuid.uuid4().hex}" - with open(tmp, "w", encoding="utf-8") as f: - json.dump(data, f, indent=indent) - f.flush() - os.fsync(f.fileno()) - os.replace(tmp, path) + + try: + with open(tmp, "w", encoding="utf-8") as f: + json.dump(data, f, indent=indent) + f.flush() + os.fsync(f.fileno()) + os.replace(tmp, path) + finally: + # Directly unlink to avoid a check-then-act race condition. + # Swallows FileNotFoundError (on success path) and other cleanup OSErrors. + try: + os.unlink(tmp) + except OSError: + pass def atomic_write_text(path: str, text: str) -> None: @@ -42,8 +51,17 @@ def atomic_write_text(path: str, text: str) -> None: raise TypeError("atomic_write_text expects a string") os.makedirs(os.path.dirname(path) or ".", exist_ok=True) tmp = f"{path}.tmp.{uuid.uuid4().hex}" - with open(tmp, "w", encoding="utf-8") as f: - f.write(text) - f.flush() - os.fsync(f.fileno()) - os.replace(tmp, path) + + try: + with open(tmp, "w", encoding="utf-8") as f: + f.write(text) + f.flush() + os.fsync(f.fileno()) + os.replace(tmp, path) + finally: + # Directly unlink to avoid a check-then-act race condition. + # Swallows FileNotFoundError (on success path) and other cleanup OSErrors. + try: + os.unlink(tmp) + except OSError: + pass \ No newline at end of file diff --git a/tests/test_atomic_io.py b/tests/test_atomic_io.py index e08da9db8..e02252189 100644 --- a/tests/test_atomic_io.py +++ b/tests/test_atomic_io.py @@ -123,7 +123,7 @@ def test_atomic_write_json_concurrent_writers_do_not_collide(tmp_path): # --------------------------------------------------------------------------- -# atomic_write_json — failure path: target preserved on serialization error. +# atomic_write_json — failure paths # --------------------------------------------------------------------------- def test_atomic_write_json_preserves_target_when_serialization_fails(tmp_path): target = tmp_path / "data.json" @@ -136,6 +136,26 @@ def test_atomic_write_json_preserves_target_when_serialization_fails(tmp_path): atomic_write_json(str(target), {"bad": {1, 2, 3}}) assert target.read_text(encoding="utf-8") == before + # Temp file should be cleaned up + assert _tmp_siblings(tmp_path, "data.json") == [] + + +def test_atomic_write_json_preserves_target_when_replace_fails(tmp_path, monkeypatch): + target = tmp_path / "data.json" + atomic_write_json(str(target), {"existing": "value"}) + before = target.read_text(encoding="utf-8") + + def boom(src, dst): + raise PermissionError("replace failed") + + monkeypatch.setattr(atomic_io.os, "replace", boom) + + with pytest.raises(PermissionError, match="replace failed"): + atomic_write_json(str(target), {"new": "content"}) + + assert target.read_text(encoding="utf-8") == before + # Temp file should be cleaned up + assert _tmp_siblings(tmp_path, "data.json") == [] # --------------------------------------------------------------------------- @@ -187,7 +207,7 @@ def test_atomic_write_text_rejects_non_string_before_tmp_file(tmp_path): # --------------------------------------------------------------------------- -# atomic_write_text — failure path: target preserved when replace fails. +# atomic_write_text — failure paths # --------------------------------------------------------------------------- def test_atomic_write_text_preserves_target_when_replace_fails(tmp_path, monkeypatch): target = tmp_path / "note.txt" @@ -195,11 +215,32 @@ def test_atomic_write_text_preserves_target_when_replace_fails(tmp_path, monkeyp before = target.read_text(encoding="utf-8") def boom(src, dst): - raise OSError("replace failed") + raise PermissionError("replace failed") monkeypatch.setattr(atomic_io.os, "replace", boom) - with pytest.raises(OSError): + with pytest.raises(PermissionError, match="replace failed"): atomic_write_text(str(target), "new content that never lands") assert target.read_text(encoding="utf-8") == before + # Temp file should be cleaned up + assert _tmp_siblings(tmp_path, "note.txt") == [] + + +def test_cleanup_error_swallows_and_preserves_original_exception(tmp_path, monkeypatch): + target = tmp_path / "note.txt" + atomic_write_text(str(target), "original content") + + def replace_boom(src, dst): + raise PermissionError("replace failed") + + def unlink_boom(path): + raise OSError("unlink failed") + + monkeypatch.setattr(atomic_io.os, "replace", replace_boom) + monkeypatch.setattr(atomic_io.os, "unlink", unlink_boom) + + # If BOTH the replace fails AND the cleanup unlink fails, + # the original replace error should surface, completely swallowing the unlink error. + with pytest.raises(PermissionError, match="replace failed"): + atomic_write_text(str(target), "new content") \ No newline at end of file From b4d12932a953b3cdfc745b3525c7ecd5dffd8b3c Mon Sep 17 00:00:00 2001 From: Joeseph Grey <212606152+StressTestor@users.noreply.github.com> Date: Thu, 20 Aug 2026 05:06:22 -0600 Subject: [PATCH 125/125] fix(agent): drop the empty assistant turn from an approved-action replay (#6124) The approved-action replay appends the sealed tool result with no assistant prose for that round, which produced an assistant message with content "". Anthropic's Messages API rejects a non-final assistant message with empty content, so a resumed turn after a tool approval failed before the model saw the result. A turn carrying neither prose nor reasoning has nothing to say to any provider, so it is no longer appended. A round with prose, and a reasoning-only round that DeepSeek thinking mode needs, both still append. --- src/agent_loop.py | 15 ++- tests/test_approved_replay_message_shape.py | 100 ++++++++++++++++++++ 2 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 tests/test_approved_replay_message_shape.py diff --git a/src/agent_loop.py b/src/agent_loop.py index 296c0ddce..9cea44068 100644 --- a/src/agent_loop.py +++ b/src/agent_loop.py @@ -3081,10 +3081,17 @@ def _append_tool_results( messages.append(result_message) else: tool_output_text = "\n\n".join(tool_results) - msg = {"role": "assistant", "content": round_response} - if round_reasoning: - msg["reasoning_content"] = round_reasoning - messages.append(msg) + # An approved-action replay injects the sealed tool result with no + # assistant prose for that round, which used to append an assistant turn + # whose content was "". Anthropic's Messages API rejects a non-final + # assistant message with empty content (HTTP 400), so the resumed turn + # died before the model saw the result. A turn carrying neither prose nor + # reasoning has nothing to say to any provider, so skip it entirely. + if round_response.strip() or round_reasoning: + msg = {"role": "assistant", "content": round_response} + if round_reasoning: + msg["reasoning_content"] = round_reasoning + messages.append(msg) # Tool output (shell/python stdout, file reads, fetched pages, email # bodies, MCP results) is sourced from outside the server. Wrap it as # untrusted data so prompt-injection inside a tool result is treated as diff --git a/tests/test_approved_replay_message_shape.py b/tests/test_approved_replay_message_shape.py new file mode 100644 index 000000000..5c4a69990 --- /dev/null +++ b/tests/test_approved_replay_message_shape.py @@ -0,0 +1,100 @@ +"""Regression coverage for the assistant turn an approved-action replay appends. + +Anthropic's Messages API rejects a non-final assistant message whose content is +empty, so the resumed turn after a tool approval used to fail before the model +ever saw the sealed result. The replay injects its result with no assistant +prose for that round, which is the only path that produced such a turn. +""" + +import src.llm_core as llm_core +from src.agent_loop import _append_tool_results + +_RESULT = "bash: ok\nhello" +_RECORD = { + "tool_name": "bash", + "content": "printf hello", + "result": {"output": "hello", "exit_code": 0}, + "text": _RESULT, +} + + +def _replay_messages(round_response="", round_reasoning=""): + """Mirror the approved-action injection in stream_agent_loop.""" + messages = [ + {"role": "system", "content": "system preface"}, + {"role": "user", "content": "run the command and summarise it"}, + ] + _append_tool_results( + messages, + round_response, + [], + [_RESULT], + [_RESULT], + False, + 0, + round_reasoning=round_reasoning, + tool_result_records=[_RECORD], + ) + return messages + + +def _empty_assistant_turns(messages): + return [ + index + for index, message in enumerate(messages) + if message.get("role") == "assistant" + and not str(message.get("content") or "").strip() + and not message.get("tool_calls") + ] + + +def test_replay_appends_no_empty_assistant_turn(): + assert _empty_assistant_turns(_replay_messages()) == [] + + +def test_replay_payload_is_a_single_non_empty_user_turn_for_anthropic(): + # Asserting the whole sequence rather than scanning a slice: once the empty + # spacer is gone the payload is one message, so a "no offenders in + # chat[:-1]" check would pass without inspecting anything. + sanitized = llm_core._sanitize_llm_messages(_replay_messages()) + payload = llm_core._build_anthropic_payload( + "claude-sonnet-5", sanitized, 0.2, 512 + ) + chat = payload["messages"] + assert [message["role"] for message in chat] == ["user"] + assert all(str(message.get("content") or "").strip() for message in chat) + + +def test_replay_merges_tool_output_after_the_request_with_its_fence_intact(): + # Dropping the empty assistant turn leaves two adjacent user messages, which + # the sanitizer merges. Pin that shape: the tool output must still sit + # behind its untrusted fence and must not precede the operator's request. + sanitized = llm_core._sanitize_llm_messages(_replay_messages()) + user_turns = [m for m in sanitized if m.get("role") == "user"] + assert len(user_turns) == 1 + merged = user_turns[0]["content"] + assert merged.index("run the command") < merged.index("UNTRUSTED SOURCE DATA") + assert merged.index("UNTRUSTED SOURCE DATA") < merged.index("hello") + + +def test_a_round_with_prose_still_appends_its_assistant_turn(): + messages = _replay_messages(round_response="running that now") + assistants = [m for m in messages if m.get("role") == "assistant"] + assert [m["content"] for m in assistants] == ["running that now"] + + +def test_reasoning_only_round_keeps_its_carrier_and_its_known_empty_content(): + """Characterises the one empty-content turn this change deliberately leaves. + + A round with reasoning and no prose still appends its carrier, and that + carrier's content is still "", which Anthropic would still reject. Dropping + it would lose the reasoning DeepSeek thinking mode requires on the next + request, and the approval replay never takes this path because it passes no + reasoning. Left alone on purpose; this test makes the gap visible instead of + silent, and should be updated by whoever closes it. + """ + messages = _replay_messages(round_reasoning="thinking about it") + assistants = [m for m in messages if m.get("role") == "assistant"] + assert len(assistants) == 1 + assert assistants[0].get("reasoning_content") == "thinking about it" + assert assistants[0]["content"] == ""