diff --git a/.github/workflows/_security.yml b/.github/workflows/_security.yml index 54d3a5a3d..f1cf88bd5 100644 --- a/.github/workflows/_security.yml +++ b/.github/workflows/_security.yml @@ -21,9 +21,13 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: "Layer 1: Static allow-list audit" - run: scripts/security-audit.sh + run: | + bash tests/test_security_audit_subprocess_calls.sh + scripts/security-audit.sh - name: "Layer 6: UI security audit" - run: scripts/security-ui.sh + run: | + bash tests/test_security_ui_urls.sh + scripts/security-ui.sh - name: "Layer 8: Vendored dependency integrity" run: scripts/security-vendored.sh diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 2f8c2af76..75fd01a09 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -301,6 +301,28 @@ jobs: shell: bash run: scripts/ci/test-package-wrappers.sh + # CodeAtlasWeb (graph-ui) leg: the frontend's own vitest suite (2000+ + # tests; the render tests run under jsdom), its hardcoded-string and + # promise scans, its portable frozen acceptance checks, and a production + # build with the same npm ci the product build runs. One canonical script + # (scripts/ci/test-ui.sh) is the whole leg, here and locally. No browser + # is downloaded: the browser proofs (graph-ui/tools/smoke-*.mjs) need a + # running server and Playwright browsers, and they stay a local, manually + # dispatched venue documented in graph-ui/README.md rather than a hosted + # gate. + test-ui: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "22" + + - name: CodeAtlasWeb leg (install, unit, style, promises, acceptance, build) + run: scripts/ci/test-ui.sh + # ThreadSanitizer data-race gate. The threaded production surfaces (watcher, # subprocess, httpd, pipeline, mcp, worker pool, diagnostics, the runnable # daemon-coordination paths) plus allocator concurrency all run under TSan diff --git a/Makefile.cbm b/Makefile.cbm index cd5eb4124..54517bbac 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -438,13 +438,13 @@ PREPROCESSOR_SRC = $(CBM_DIR)/preprocessor.cpp SQLITE_WRITER_SRC = $(CBM_DIR)/sqlite_writer.c # Store module (new) -STORE_SRCS = src/store/store.c src/store/store_impact.c src/store/store_graph_digest.c src/store/store_content_digest.c +STORE_SRCS = src/store/store.c src/store/architecture_projection.c src/store/store_impact.c src/store/store_graph_digest.c src/store/store_content_digest.c # Cypher module (new) CYPHER_SRCS = src/cypher/cypher.c # MCP server module (new) -MCP_SRCS = src/mcp/mcp.c src/mcp/index_supervisor.c src/mcp/compact_out.c \ +MCP_SRCS = src/mcp/mcp.c src/mcp/index_supervisor.c src/mcp/compact_out.c src/mcp/architecture_jobs.c \ src/mcp/test_impact.c src/mcp/test_impact_diff.c src/mcp/test_impact_classify.c src/mcp/test_impact_source.c src/mcp/test_impact_seed.c src/mcp/test_impact_engine.c src/mcp/test_impact_engine_snapshot.c src/mcp/test_impact_engine_reach.c src/mcp/test_impact_engine_artifact.c src/mcp/test_impact_cli.c src/mcp/test_impact_artifact.c src/mcp/test_impact_changes.c src/mcp/test_impact_origins.c src/mcp/test_impact_coverage.c src/mcp/test_impact_config.c src/mcp/test_impact_select.c src/mcp/test_impact_policy.c src/mcp/test_impact_git.c src/mcp/test_impact_inventory.c src/mcp/test_impact_inventory_input.c src/mcp/test_impact_tree.c src/mcp/test_impact_tree_audit.c src/mcp/test_impact_tree_read.c src/mcp/test_impact_tree_batch.c src/mcp/test_impact_tree_digest.c src/mcp/test_impact_tree_names.c src/mcp/test_impact_tree_plan.c src/mcp/test_impact_tree_posix.c src/mcp/test_impact_tree_posix_acl.c src/mcp/test_impact_tree_posix_io.c src/mcp/test_impact_tree_util.c src/mcp/test_impact_tree_windows.c src/mcp/test_impact_tree_windows_acl.c src/mcp/test_impact_tree_windows_io.c \ src/mcp/test_impact_result.c src/mcp/test_impact_result_json.c src/mcp/test_impact_result_project.c src/mcp/test_impact_result_receipt.c src/mcp/test_impact_result_rows.c src/mcp/test_impact_result_util.c src/mcp/test_impact_result_validate.c src/mcp/test_impact_result_wire.c @@ -547,7 +547,7 @@ WATCHER_SRCS = src/watcher/watcher.c GIT_SRCS = src/git/git_context.c # CLI module (new) -CLI_SRCS = src/cli/cli.c src/cli/progress_sink.c src/cli/hook_augment.c src/cli/client_adapter.c \ +CLI_SRCS = src/cli/cli.c src/cli/runtime_settings.c src/cli/progress_sink.c src/cli/hook_augment.c src/cli/client_adapter.c \ src/cli/agent_clients.c src/cli/agent_profiles.c \ src/cli/config_json_like.c src/cli/config_toml_edit.c src/cli/config_yaml_edit.c \ src/cli/config_text_edit.c src/cli/config_edit_path.c src/cli/activation_transaction.c @@ -556,7 +556,16 @@ CLI_SRCS = src/cli/cli.c src/cli/progress_sink.c src/cli/hook_augment.c src/cli/ UI_SRCS = \ src/ui/config.c \ src/ui/http_server.c \ + src/ui/activity.c \ + src/ui/atlas_repository.c \ src/ui/layout3d.c \ + src/ui/layout_regions.c \ + src/ui/atlas_api.c \ + src/ui/atlas_flows.c \ + src/ui/atlas_handout.c \ + src/ui/atlas_why.c \ + src/ui/atlas_metrics.c \ + src/ui/atlas_impact.c \ src/ui/httpd.c \ src/ui/embedded_stub.c @@ -714,6 +723,7 @@ TEST_STORE_SRCS = \ tests/test_store_graph_digest.c \ tests/test_store_search.c \ tests/test_store_arch.c \ + tests/test_architecture_projection.c \ tests/test_store_bulk.c \ tests/test_store_pragmas.c \ tests/test_store_checkpoint.c \ @@ -818,7 +828,7 @@ TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c tests/ TEST_TRACES_SRCS = tests/test_traces.c -TEST_CLI_SRCS = tests/test_cli.c tests/test_agent_clients.c tests/test_agent_profiles.c \ +TEST_CLI_SRCS = tests/test_cli.c tests/test_runtime_settings.c tests/test_agent_clients.c tests/test_agent_profiles.c \ tests/test_config_json_like.c \ tests/test_config_toml_edit.c tests/test_config_yaml_edit.c tests/test_config_text_edit.c \ tests/test_activation_transaction.c @@ -964,7 +974,7 @@ $(BUILD_DIR)/test-foundation: $(TEST_FOUNDATION_SRCS) $(FOUNDATION_SRCS) $(PROJE $(CC) $(CFLAGS_TEST) -o $@ $(TEST_FOUNDATION_SRCS) $(FOUNDATION_SRCS) $(LDFLAGS_TEST) test-foundation: $(BUILD_DIR)/test-foundation - cd $(CURDIR) && $(BUILD_DIR)/test-foundation + cd "$(CURDIR)" && $(BUILD_DIR)/test-foundation # ── Grammar/TS/LSP object files (compiled with relaxed warnings) ─ @@ -1123,13 +1133,13 @@ $(BUILD_DIR)/test-runner: $(ALL_TEST_SRCS) $(PROD_SRCS) $(EXTRACTION_SRCS) $(AC_ $(LDFLAGS_TEST) test: $(BUILD_DIR)/test-runner - cd $(CURDIR) && $(BUILD_DIR)/test-runner + cd "$(CURDIR)" && $(BUILD_DIR)/test-runner # Parallel variant: every suite as its own process; identical gate quality # (see the ZERO-LOSS CONTRACT in scripts/run-tests-parallel.sh — union guard # against --list-suites + aggregated pass/fail/skip totals). test-par: $(BUILD_DIR)/test-runner - cd $(CURDIR) && bash scripts/run-tests-parallel.sh $(BUILD_DIR)/test-runner + cd "$(CURDIR)" && bash scripts/run-tests-parallel.sh $(BUILD_DIR)/test-runner # Focused native development is intentionally a separate, explicit target so # an inherited TEST_SUITES value cannot silently narrow the gating test target. @@ -1137,7 +1147,7 @@ TEST_SUITES ?= test-focused: $(BUILD_DIR)/test-runner @test -n "$(strip $(TEST_SUITES))" || \ (echo "TEST_SUITES is required for test-focused"; exit 2) - cd $(CURDIR) && $(BUILD_DIR)/test-runner $(TEST_SUITES) + cd "$(CURDIR)" && $(BUILD_DIR)/test-runner $(TEST_SUITES) # ── Cumulative bug-reproduction runner (RED board by design, non-gating) ── # Mirrors test-runner's link line but uses repro_main.c (own main + counters) @@ -1309,8 +1319,11 @@ PROD_SRCS_WITH_ASSETS = $(subst src/ui/embedded_stub.c,src/ui/embedded_assets.c, # Embedded asset object files (generated by embed script) EMBED_OBJS = $(wildcard $(BUILD_DIR)/embedded/embed_*.o) +# PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: the UI's dev dependencies include +# playwright for its local browser proofs; a plain `npm ci` would otherwise +# download browsers, which no build or gate needs. frontend: - cd graph-ui && npm ci && npm run build + cd graph-ui && PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 npm ci && npm run build embed: frontend scripts/embed-frontend.sh graph-ui/dist $(BUILD_DIR)/embedded diff --git a/README.md b/README.md index acf836f66..1ef376d9a 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ High-quality parsing through [tree-sitter](https://tree-sitter.github.io/tree-si - **162 languages** — vendored tree-sitter grammars compiled into the binary. Nothing to install, nothing that breaks. - **120x fewer tokens** — 5 structural queries: ~3,400 tokens vs ~412,000 via file-by-file search. One graph query replaces dozens of grep/read cycles. - **45 supported automatic/conditional client surfaces** — `install` configures detected clients and safely activates conditional clients only when their documented platform, marker, or explicit existing config path is present. See [Multi-Agent Support](#multi-agent-support) for the complete matrix and manual/UI-only boundaries. -- **Built-in graph visualization** — 3D interactive UI at `localhost:9749`, served from the binary itself. +- **CBM Atlas** — the built-in human interface at `localhost:9749`: a reading IDE over the index (explorer and read-only Monaco reader, a semantic twin that follows the caret, flow, bug-hunt and change-scope reading modes, the 3D galaxy, source-cited answers, project management), served from the binary itself. - **Infrastructure-as-code indexing** — Dockerfiles, Kubernetes manifests, and Kustomize overlays indexed as graph nodes with cross-references. `Resource` nodes for K8s kinds, `Module` nodes for Kustomize overlays with `IMPORTS` edges to referenced resources. - **17 MCP tools** — search, trace, architecture, impact analysis, targeted index-coverage checks, Cypher queries, dead code detection, cross-service HTTP linking, ADR management, and more. @@ -131,9 +131,9 @@ Package-manager setup (npm, PyPI, or Go) verifies and publishes a coherent priva The ordinary `cli` mode runs one command locally, but it is not disconnected from the daemon above: it connects to the shared coordination daemon — starting one if none is already running — for that same admission barrier plus per-project locks on graph mutations. Its connection is a `cli_session`: it never registers with the background watcher and never starts a UI, and it holds the admission lease only for the command's lifetime. If the command's own connection is what started the daemon, that daemon exits again once the command closes and no other session is attached; if a daemon was already running, the command simply joins and leaves it exactly as found. While the command is running, a temporary monitor separately lets activation cancel that operation and its supervised worker safely; the monitor exits with the command and never becomes a standing daemon. See [CLI Mode](#cli-mode) for details. -### Graph Visualization UI +### CBM Lens Beta — the human interface -The graph UI is built into the binary — every install on every channel has it. Then run it: +The MCP tools are the agent's interface to the code graph; **CBM Lens Beta** is the human's. It is built into the binary — every install on every channel has it: ```bash codebase-memory-mcp --ui=true --port=9749 @@ -141,6 +141,21 @@ codebase-memory-mcp --ui=true --port=9749 Open `http://localhost:9749` in your browser. The UI is owned by the shared coordination daemon, so concurrent agent sessions do not start duplicate HTTP servers. +CBM Lens builds on **CodeAtlasWeb**, a reading IDE built by [Bernhard Jackiewicz](https://github.com/BernhardJackiewicz) on the CBM read surface (design and tracking in #1964). It runs air-gapped on loopback, reaches no cloud, and every panel names where its facts come from; what the index did not record is shown as a gap, never filled in with a guess. + +When something in CBM Lens does not load, the page has already written it down: its console output, uncaught errors and failed requests go to `${CBM_CACHE_DIR}/logs/ui.log` (default `~/.cache/codebase-memory-mcp/logs/ui.log`, one JSON line per entry, rotated once at 5 MiB). The projects panel (alt+p) shows the tail under "This server", `GET http://localhost:9749/api/ui-log?lines=200` returns it, and a bug report can attach the file. + +- **Explorer and reader** — the indexed file tree and a read-only Monaco reader whose source comes from the index. +- **Semantic twin** — follows the caret: what the symbol under it holds, calls, raises and touches, as facts or as pseudocode, pitched at who is reading. +- **Reading modes** — `[w]hy am I here` picks a way in (hunt a bug, scope a change, understand the project, pick an entry point); **flow** walks the calls in the order they run; **bug hunt** compares the expected path into a symbol with what `ingest_traces` observed; **change scope** shows what a change reaches and what covers it. +- **Galaxy** — the persistent 3D graph with focus following in both directions, and a hierarchy view of the entry subgraph. +- **Command line** — search by meaning while you type; a line ending in `?` builds a source-cited answer from indexed fact cards, with an optional local-model wording pass (a llama-server sidecar you start yourself; off by default, and no model or binary ships). +- **Live agents** — an opt-in overlay of AI agents working in the same repository, fed by a loopback bridge over tool-hook events (`graph-ui/agents/`). +- **`[p]rojects`** — index a repository, check or remove an index, edit the project's decision record, and see the server's processes and log. +- **`[?]help`** — what it reads, what it cannot do, and every key it listens to, derived from the wiring itself. + +Open a project with `?project=` in the address, or pick it in the projects panel. The frontend's own README, plan, honesty rules and recorded proof runs live under `graph-ui/`. + ### Auto-Index Enable automatic indexing on MCP session start: diff --git a/docs/graph-ui-screenshot.png b/docs/graph-ui-screenshot.png index ffb5ee58c..e0e2972d5 100644 Binary files a/docs/graph-ui-screenshot.png and b/docs/graph-ui-screenshot.png differ diff --git a/graph-ui/.gitignore b/graph-ui/.gitignore new file mode 100644 index 000000000..fc184ba03 --- /dev/null +++ b/graph-ui/.gitignore @@ -0,0 +1,60 @@ +.DS_Store +cbm/ +node_modules/ +dist/ +*.log +.vite/ +*.tsbuildinfo + +# Die lokalen Modelle und die gebaute Inferenz-Runtime: zusammen ueber 11 GB +# und beide reproduzierbar aus Pruefsummen und Baubefehl. Was committet wird, +# ist die Herkunft und nicht das Gewicht. +# +# Die vier Zeilen je Verzeichnis sind kein Umweg, sondern das, was git +# verlangt: ein ausgeschlossenes VERZEICHNIS wird gar nicht erst betreten, also +# koennte eine Ausnahme darin nie greifen (git-scm, gitignore(5): "It is not +# possible to re-include a file if a parent directory of that file is +# excluded"). Die Verzeichnisregel steht trotzdem zuerst, weil sie die Absicht +# nennt; die naechste hebt sie fuer das Verzeichnis selbst wieder auf, die +# dritte schliesst seinen Inhalt aus, und erst die vierte holt die eine Datei +# zurueck, die committet gehoert. +models/ +!models/ +models/* +!models/SHA256SUMS + +vendor/ +!vendor/ +vendor/* +!vendor/llama/ +vendor/llama/* +!vendor/llama/HERKUNFT.md + +# Die Laufspuren des Sidecars. Die PID-Datei gehoert dem laufenden Prozess, +# das Log ist ohnehin schon durch *.log erfasst. +llm/.pid + +# Der Node-Bau des Context-Compilers fuer die Eval. Er wird bei jedem Lauf neu +# erzeugt (tools/lib/compiler-bundle.mjs); ein committetes Bundle waere eine +# zweite, alternde Fassung der Dateien daneben. +.eval-build/ + +# Die Ereignisdatei der Agenten (W11a). Sie enthaelt keine Dateiinhalte, aber +# sie verraet, an welchen Dateien jemand gearbeitet und welche Befehle er +# gefahren hat. Das ist die Arbeitsspur einer Person und gehoert nicht in ein +# Repository; was committet ist, ist die AUFZEICHNUNG des Beweislaufs unter +# fixtures/agent-events/, und deren Herkunft steht dort daneben. +.atlas-trace/ +events.jsonl +events.jsonl.seq + +# Local visual captures and development reports; JSON/JSONL fixtures stay tracked. +/verification/**/*.png +/verification/**/*.jpg +/verification/**/*.jpeg +/verification/**/*.gif +/verification/**/*.webp +/verification/**/*.svg +/verification/**/*.webm +/verification/**/*.mp4 +/verification/**/*.md diff --git a/graph-ui/@/components/ui/badge.tsx b/graph-ui/@/components/ui/badge.tsx deleted file mode 100644 index 6eb2a057a..000000000 --- a/graph-ui/@/components/ui/badge.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { Slot } from "radix-ui" - -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90", - outline: - "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - link: "text-primary underline-offset-4 [a&]:hover:underline", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -function Badge({ - className, - variant = "default", - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot.Root : "span" - - return ( - - ) -} - -export { Badge, badgeVariants } diff --git a/graph-ui/@/components/ui/button.tsx b/graph-ui/@/components/ui/button.tsx deleted file mode 100644 index 4d38506ce..000000000 --- a/graph-ui/@/components/ui/button.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { Slot } from "radix-ui" - -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-8", - "icon-lg": "size-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -function Button({ - className, - variant = "default", - size = "default", - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps & { - asChild?: boolean - }) { - const Comp = asChild ? Slot.Root : "button" - - return ( - - ) -} - -export { Button, buttonVariants } diff --git a/graph-ui/@/components/ui/card.tsx b/graph-ui/@/components/ui/card.tsx deleted file mode 100644 index acf57dc5a..000000000 --- a/graph-ui/@/components/ui/card.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -function Card({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( -
- ) -} - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -} diff --git a/graph-ui/@/components/ui/checkbox.tsx b/graph-ui/@/components/ui/checkbox.tsx deleted file mode 100644 index f5a7e433f..000000000 --- a/graph-ui/@/components/ui/checkbox.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client" - -import * as React from "react" -import { CheckIcon } from "lucide-react" -import { Checkbox as CheckboxPrimitive } from "radix-ui" - -import { cn } from "@/lib/utils" - -function Checkbox({ - className, - ...props -}: React.ComponentProps) { - return ( - - - - - - ) -} - -export { Checkbox } diff --git a/graph-ui/@/components/ui/input.tsx b/graph-ui/@/components/ui/input.tsx deleted file mode 100644 index f1124aea1..000000000 --- a/graph-ui/@/components/ui/input.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - - ) -} - -export { Input } diff --git a/graph-ui/@/components/ui/scroll-area.tsx b/graph-ui/@/components/ui/scroll-area.tsx deleted file mode 100644 index e38a492f2..000000000 --- a/graph-ui/@/components/ui/scroll-area.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import * as React from "react" -import { ScrollArea as ScrollAreaPrimitive } from "radix-ui" - -import { cn } from "@/lib/utils" - -function ScrollArea({ - className, - children, - ...props -}: React.ComponentProps) { - return ( - - - {children} - - - - - ) -} - -function ScrollBar({ - className, - orientation = "vertical", - ...props -}: React.ComponentProps) { - return ( - - - - ) -} - -export { ScrollArea, ScrollBar } diff --git a/graph-ui/@/components/ui/separator.tsx b/graph-ui/@/components/ui/separator.tsx deleted file mode 100644 index db91e97ad..000000000 --- a/graph-ui/@/components/ui/separator.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from "react" -import { Separator as SeparatorPrimitive } from "radix-ui" - -import { cn } from "@/lib/utils" - -function Separator({ - className, - orientation = "horizontal", - decorative = true, - ...props -}: React.ComponentProps) { - return ( - - ) -} - -export { Separator } diff --git a/graph-ui/CONFIGURATION.md b/graph-ui/CONFIGURATION.md new file mode 100644 index 000000000..22afda714 --- /dev/null +++ b/graph-ui/CONFIGURATION.md @@ -0,0 +1,73 @@ +# Frontend configuration + +Open **System**, then **Configuration**. Settings are grouped by indexing, +resources, server, diagnostics, and browser. The **External inputs** reference +covers repository files, installation, launch flags, integration paths, and +development settings whose owner is outside the running daemon. + +## Daemon overrides + +Daemon edits are staged until **Save changes**. A batch is validated and committed +atomically to `_config.db` in the CBM cache directory. Invalid keys or values reject +the whole batch. Each row exposes its source, default, saved override, current +process value, and application timing. **Reset** removes the saved override; it +does not overwrite the inherited environment or replace it with a hardcoded default. +Legacy environment values outside the registry's canonical format are shown as +runtime-interpreted inputs, rather than guessed effective values. Saving an override +requires a validated value. + +Supported environment overrides are loaded once when a process starts, ahead of +the inherited environment and built-in defaults. They do not mutate or export the +process environment. Restart CBM to apply changes throughout the installation; +newly launched index workers can load saved values before the existing daemon is +restarted. Explicit command flags, per-index options, and supervisor recovery caps +retain their documented precedence. + +The existing `auto_index`, `auto_index_limit`, `auto_watch`, `watcher_enabled`, and +`ui-lang` settings share their canonical SQLite values with CLI configuration. +UI enablement and port overrides use the same transactional store, with legacy +UI `config.json` as a fallback. Explicit CLI/UI saves update that store too. +Concurrent edits use a revision check; a stale save fails without changing data. + +Launch locations, filesystem access grants, arbitrary output destinations, +installer settings, test injection, and supervisor controls are references rather +than editable process overrides. Platform-specific or unlinked functionality is +marked unavailable. No generic environment editor or secret enumeration is exposed. + +## Browser preferences + +Browser changes apply immediately. Graph display and edge motion use their existing +preference stores. Galaxy budgets, coverage shadow, and architecture appearance +share a versioned, per-project browser store with the controls in those views. +Changing these values preserves the current selection. If storage is unavailable, +preferences remain usable for the current session. + +The top-bar agent status opens separate local model controls. Opening configuration never +downloads weights or starts a model. Repository extension mappings and ignore files +remain repository-owned and take effect on subsequent indexing. + +## Local HTTP API + +`GET /api/config` returns `{ revision, settings }`. Each setting declares its type, +validation bounds or enum choices, lifecycle, provenance, and editability. +The registry lives in `src/cli/runtime_settings.c`; consumer locations are included +in its metadata. Browser response validation lives in `src/settings/config-model.ts`. + +`POST /api/config` accepts JSON with the revision just read and a nonempty changes +object. Values are canonical strings; `null` removes an override: + +```json +{ + "revision": "12", + "changes": { + "CBM_WORKERS": "4", + "CBM_SEMANTIC_ENABLED": "false", + "CBM_SQLITE_MMAP_SIZE": null + } +} +``` + +Success returns the updated snapshot. Invalid batches return 400, conflicting +revisions return 409, and storage failures return 500. Existing loopback Host, +same-origin, and JSON-content checks protect the endpoint. Frontend drafts survive +errors so users can inspect the conflict before reloading or discarding them. diff --git a/graph-ui/README.md b/graph-ui/README.md new file mode 100644 index 000000000..276dd51f8 --- /dev/null +++ b/graph-ui/README.md @@ -0,0 +1,132 @@ +# CBM Lens Beta + +CBM Lens is the local exploration frontend served by `codebase-memory-mcp --ui=true`. +It combines source, the indexed graph and optional browser inference. Source files +are read-only; project architecture decisions can be edited in the ADR workspace. +The frontend was contributed by Bernhard Jackiewicz as CodeAtlasWeb. + +## Explore a project + +- **Explore** follows the current file or marked source. Its graph requests direct + dependencies separately from the capped repository layout. File-level impact + summaries use graph and local Git evidence, with details available on demand. +- **Galaxy** searches nodes, files and folders. Select a scope and expand incoming + or outgoing relationships by depth and edge type. Overview budgets limit drawing; + a capped layout does not establish a complete neighborhood. Bounded caches reuse + positions and verified adjacency, and fresh index generations invalidate evidence. +- **Architecture** presents the repository map, routes, hotspots, system structure + and behavior. Connectivity clusters are not proof of architectural boundaries. + Moving arrows show edge direction, not observed execution. Reduced-motion settings + retain stationary direction markers. +- **Coverage** explains indexing gaps. **System** contains daemon status, indexes, + diagnostics and MCP configuration. See [configuration and precedence](CONFIGURATION.md). +- **ADR** stores one editable Markdown decision document per project through + `GET/POST /api/adr`, shared with `manage_adr`. It does not edit repository files. + +Use **Add project index** in the project dropdown (or Alt+P) to choose a local +repository, name its index and follow daemon-reported progress. Files are indexed +in place, not uploaded. Closing the dialog does not cancel indexing. + +ADR drafts stay in this tab's session storage. Before saving, the UI compares the +server document with the editing baseline and retains conflicting drafts. This is +not an atomic document lock. Saves are limited to 8,000 UTF-8 bytes and a 16 KiB +serialized request. The examples in `fixtures/adr/` are explicitly synthetic and +are never inserted into real projects automatically. + +## Local agent + +The top-bar agent status opens model configuration; the adjacent chat control folds +or unfolds the conversation. Model downloads require explicit activation. The chat +uses browser-local inference; the selected source is not sent to a remote model. +Model provenance and license limitations are in [THIRD_PARTY.md](THIRD_PARTY.md). + +Explorer provides the exact marked text, or the loaded file when nothing is marked. +A caret alone does not narrow the context. One replaceable source block precedes +conversation history; each message captures the current selection. Navigation does +not alter an in-flight request, and Retry uses its original snapshot. Missing or +partial source is disclosed. Token counting uses the selected model's tokenizer; +oversized requests are rejected rather than silently shortened. + +Automatic explanations respond to new selections independently of follow-up chat. +Browser history is separated by project, retained for 24 hours, and can be cleared +in Agent configuration. + +## Build and preview + +From the repository root: + +```sh +make -f Makefile.cbm cbm-with-ui +``` + +This installs the locked frontend dependencies, builds `dist/` and embeds it in the +native binary. For frontend development, run `npm ci` and `npm run dev` in this +directory. Vite listens on 5173 and proxies `/api` and `/rpc` to the local server on +9749. `npm run build` creates a production bundle. + +The standalone test preview in `tools/lib/static-proxy.mjs` snapshots `dist/` when +it starts. Restart that preview after building and compare the served `index.html` +asset name with `dist/index.html` before reviewing changes. + +The canonical frontend CI entry point is `scripts/ci/test-ui.sh` from the repository +root. It runs unit tests, style and promise checks, acceptance checks and a production +build. Browser smoke commands require a running server and Playwright Chromium; +install that browser explicitly with `npx playwright install chromium` when needed. + +## Experimental agent activity + +Build with `VITE_EXPERIMENTAL_AGENTS=true` to expose recorded tool activity, hook +setup and graph overlays. The value must be exactly `true`; URL parameters and saved +preferences cannot enable it. Browser-local chat is independent of this flag. +Activity remains off until enabled in the UI. It requires an event adapter and is +not automatic observation of arbitrary coding agents. + +The supported adapter is `agents/hooks/atlas-trace.py`. **Connect your agent** +downloads it and offers a project-specific install command. Review and run it locally: + +```sh +python3 ~/Downloads/cbm-atlas-trace.py --install-claude --root /absolute/repository --project INDEXED_NAME --daemon-url http://127.0.0.1:9749 +``` + +The installer preserves unrelated settings and hooks, rejects conflicts, symlinks +and malformed configuration, and writes only the selected repository's +`.claude/settings.local.json` and `.claude/hooks/`. +Start a new session in your configured client after installation. Other clients +need their own adapter. + +For manual hook setup, set `ATLAS_PROJECT` to the indexed name, optionally +`ATLAS_AGENT_NAME` and `ATLAS_DAEMON_URL`, and pass the tool event on stdin. The hook +records tool, run, sequence, timestamp, path and explicit line ranges. It omits source +contents, tool output and free-form commands, search patterns and descriptions. Paths +can still be sensitive. Destinations must be local; proxies and redirects are refused. + +A transactional outbox at `~/.atlas-trace/events.jsonl.outbox.sqlite3` buffers up to +10,000 pending events. `ATLAS_TRACE_FILE` changes the path prefix. Delivery resumes +on the next hook or `python3 agents/hooks/atlas-trace.py --flush`; one flush sends up +to 100 events from one project. The older JSONL/replay tools are fixture utilities. + +## Activity and diagnostic APIs + +`POST /api/agent-events` accepts `{project, events: [...]}` with at most 100 events. +Each event needs `ts`, `agent`, `run`, integer `seq`, `phase: start|end` and `tool`. +Optional fields include `path`, `lines`, `detail`, `source`, self-reported `intent` +and explicit `replay`. Unknown fields are discarded. The UI distinguishes recorded +facts from self-reports and leaves unmapped paths visible. + +The daemon stores activity in `/activity.db`, independently of replaceable +project indexes. Monotonic cursors, `(project, run, seq)` identities and retired +sequence floors prevent duplicate or expired events from reappearing. Retention is +bounded to 10,000 events; expired history is incomplete. Poll with +`GET /api/agent-events?project=NAME&after=CURSOR&limit=200` on the graph's origin. + +Daemon and frontend logs share the SQLite journal. `GET /api/logs` accepts `project`, +`scope=unattributed`, `min_level`, literal `q`, `after` and `lines`; filters apply +before counts and pagination. Unattributed records are not assigned to the selected +project. Responses identify severity, source, receipt time, durable ID and database +generation. A bounded memory fallback is explicitly marked `persistent:false`. + +`POST /api/ui-log` batches frontend errors and request failures. Failed journal writes +return 503 for retry. `GET /api/ui-log` reads the same journal; `path` names +`activity.db`, while `export_path` names the rotating compatibility JSONL export. +Changing that export does not change HTTP history. These APIs publish nothing +externally. diff --git a/graph-ui/THIRD_PARTY.md b/graph-ui/THIRD_PARTY.md new file mode 100644 index 000000000..eb4270362 --- /dev/null +++ b/graph-ui/THIRD_PARTY.md @@ -0,0 +1,149 @@ +# CBM Lens third-party notices + +This document records imported source, package licenses and optional model provenance. +Preserve the copyright and license headers in individual source files. The npm +lockfile records the exact installed dependency versions. + +## Galaxy source + +The initial scene was adapted from [DeusData/codebase-memory-mcp](https://github.com/DeusData/codebase-memory-mcp), +`feat/atlas-r1` at `bda46086`, on 2026-08-28. It is MIT licensed, +Copyright (c) 2025 DeusData; the full license is in the repository's `LICENSE`. + +| Lens file | Original under `graph-ui/src` | +| --- | --- | +| `src/galaxy/GraphScene.tsx` | `components/GraphScene.tsx` | +| `src/galaxy/NodeCloud.tsx` | `components/NodeCloud.tsx` | +| `src/galaxy/EdgeLines.tsx` | `components/EdgeLines.tsx` | +| `src/galaxy/NodeLabels.tsx` | `components/NodeLabels.tsx` | +| `src/galaxy/HaloLayer.tsx` | `components/HaloLayer.tsx` | +| `src/galaxy/types.ts` | `lib/types.ts` | +| `src/galaxy/density.ts` | `lib/density.ts` | + +Adaptations include component-local canvas access, memoized scene measurements, +frontend-specific props and types, and revised graph interaction/rendering. +`NodeTooltipCard.tsx` is a separate implementation using Lens styling rather than +the original Tailwind tooltip. File headers retain the initial import details; +subsequent changes are recorded in Git history. + +## Packages + +| Package | Recorded version | License | +| --- | --- | --- | +| `three` | 0.183.2 | MIT | +| `@react-three/fiber` | 9.5.0 | MIT | +| `@react-three/drei` | 10.7.7 | MIT | +| `@react-three/postprocessing` | 3.0.4 | MIT | +| `postprocessing` | 6.38.3 | Zlib | +| `three-stdlib` | 2.36.1 | MIT | +| `@types/three` | 0.183.1 | MIT | +| `@huggingface/transformers` | 4.2.0 | Apache-2.0 | +| `onnxruntime-web` | 1.26.0-dev.20260416-b7804b056c | MIT | +| `sharp` | 0.35.0 | Apache-2.0 | +| `react-markdown` | 10.1.0 | MIT | +| `remark-gfm` | 4.0.1 | MIT | + +These packages are used in their published form. `postprocessing` requires accurate +origin attribution and identification of modified versions under its Zlib license. +React, Vite, Vitest and Monaco use MIT; TypeScript and Playwright use Apache-2.0. +`sharp` is a transitive Node dependency with a targeted override, not browser code. +Full Markdown renderer license texts appear below. + +## CodeAtlasIDE provenance + +Provider, semantic IR, twin and semantic-search modules were ported from Bernhard +Jackiewicz's CodeAtlasIDE project by the same author. Their source headers retain +that origin. They are original contributions from Bernhard, not imports from an +unrelated third party. + +The concept of deriving an entry tour from project structure was inspired by +Understand-Anything (MIT). `src/tours/tour-generator.ts` contains original Kahn +traversal, role labels, wording and cycle handling; no source was copied from that +project. Its direct code origin is CodeAtlasIDE. + +## Optional browser models + +Transformers.js and ONNX Runtime assets are served locally. Weights are not bundled; +downloading them requires an explicit browser action. Source inference stays in the +browser. Model policy pins revisions and permitted files; it does not execute remote +model scripts. + +The following metadata was recorded on 2026-09-08. Download sizes include tokenizer +and configuration files and do not describe GPU memory requirements. + +| Model | Fixed revision | Download bytes | Recorded license | +| --- | --- | --- | --- | +| [onnx-community/Qwen2.5-Coder-0.5B-Instruct](https://huggingface.co/onnx-community/Qwen2.5-Coder-0.5B-Instruct) | `f0292f665fd307846ff3c318a91a1bc29d091492` | 566366194 | Apache-2.0 for the base model | +| [onnx-community/Qwen3-0.6B-ONNX](https://huggingface.co/onnx-community/Qwen3-0.6B-ONNX) | `da1453100cf3ff33ef56d17983fc7a8648706db6` | 578917626 | Apache-2.0 | +| [LiquidAI/LFM2.5-1.2B-Instruct-ONNX](https://huggingface.co/LiquidAI/LFM2.5-1.2B-Instruct-ONNX) | `10f72e70abf67ac0fd7ebf15bc5854726891d864` | 763763755 | Liquid AI license; review commercial terms above $10M annual revenue | +| [onnx-community/Qwen3.5-2B-ONNX-OPT](https://huggingface.co/onnx-community/Qwen3.5-2B-ONNX-OPT) | `2ea7886f48b926aca97de8b0e041ffca7e3ebaa9` | 1402850762 | Apache-2.0 | + +All use `q4f16`. Qwen3.5 loads the text decoder and embeddings only, not vision +files. The Qwen2.5 ONNX weight file is 554935833 bytes; its published LFS SHA-256 is +`60c076ac0d3910881fe0cad75997e803459a5f71bb74bcbcd82407ef83db7ba0`. +The converter's model card does not declare a separate license; its +[official base model](https://huggingface.co/Qwen/Qwen2.5-Coder-0.5B-Instruct) +is Apache-2.0. + +`src/browser-ai/model-policy.ts` retains complete file lists, published LFS hashes +and allowed hosts, including `huggingface.co` and the recorded redirect host +`us.aws.cdn.hf.co`. The loader does not recompute the published hashes. +The browser context limit is 8192 tokens including 512 reserved output tokens; +requests are counted with the selected template and tokenizer rather than silently +truncated. Qwen3 and Qwen3.5 default to non-thinking mode. Runtime support is not a +benchmark or a compatibility guarantee for every browser/GPU combination. + +## Markdown renderer licenses + +### react-markdown + +```text +The MIT License (MIT) + +Copyright (c) Espen Hovlandsdal + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +### remark-gfm + +```text +(The MIT License) + +Copyright (c) Titus Wormer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` diff --git a/graph-ui/agents/hooks/atlas-trace-watch.mjs b/graph-ui/agents/hooks/atlas-trace-watch.mjs new file mode 100644 index 000000000..879519aad --- /dev/null +++ b/graph-ui/agents/hooks/atlas-trace-watch.mjs @@ -0,0 +1,99 @@ +#!/usr/bin/env node +/** + * Die zweite Quelle: Schreibvorgaenge auf der Platte. + * + * Sie ist der Rueckfall fuer alles, was keine Werkzeug-Hooks kennt. Ein Agent, + * der mit einem Editor oder einem Skript arbeitet, ruft keinen Hook auf; was + * dabei trotzdem sichtbar ist, ist die Datei, die sich aendert. + * + * ## Was sie sieht, und was nicht + * + * Sie sieht **nur Schreibvorgaenge**. Kein Lesen, kein Suchen, kein Testlauf, + * kein Befehl. Genau so ist jedes Ereignis gekennzeichnet (`source: "fs"`), und + * das ist keine Formalie: wer spaeter eine Aufzeichnung ansieht, muss wissen, + * dass die Ruhe zwischen zwei Schreibvorgaengen nicht Ruhe bedeutet, sondern + * Arbeit, die hier nicht ankommt. Ein Protokoll, das seine eigene Blindheit + * verschweigt, ist schlimmer als keines. + * + * Sie schreibt **keine Dateiinhalte**, so wenig wie der Hook daneben: ein + * Ereignis nennt den Pfad und die Zeit, mehr nicht. Einen Zeilenbereich kann + * sie nicht nennen, denn ein Dateisystem meldet keine Zeilen; das Feld fehlt + * darum, statt eine Zahl zu erfinden. + * + * ## Aufruf + * + * node agents/hooks/atlas-trace-watch.mjs + * + * Wohin geschrieben wird, sagt ATLAS_TRACE_FILE; ohne die Variable ist es + * ~/.atlas-trace/events.jsonl. Wie der Agent heisst, sagt ATLAS_AGENT_NAME; + * ohne sie steht dort "file-writes", weil das die Wahrheit ueber diese Quelle + * ist: sie kennt keinen Agenten, sie kennt eine Datei, die sich geaendert hat. + * + * Kein Paket, nur node:fs. + */ +import { appendFileSync, mkdirSync, watch } from 'node:fs'; +import { dirname, join, relative } from 'node:path'; +import { homedir } from 'node:os'; + +const ROOT = process.argv[2] ?? process.cwd(); +const OUT = process.env['ATLAS_TRACE_FILE'] + ?? join(homedir(), '.atlas-trace', 'events.jsonl'); +const AGENT = process.env['ATLAS_AGENT_NAME'] ?? 'file-writes'; + +/** Der Lauf. Acht Zeichen, wie die Kennung des Hooks daneben. */ +const RUN = String(Date.now()).slice(-8); + +/** Was nie interessiert: Bauwerk, Fremdcode, Beweisbilder, das Protokoll selbst. */ +const IGNORE = /(^|\/)(node_modules|\.git|dist|cbm|vendor|models|verification|coverage)(\/|$)/; + +/** + * Derselbe Pfad zweimal in dieser Zeitspanne ist ein Ereignis, nicht zwei. + * + * Ein Editor schreibt gern mehrfach (Datei anlegen, Inhalt schreiben, Rechte + * setzen), und ein Bild soll die Handlung zeigen und nicht die Schreibtechnik. + */ +const COALESCE_MS = 400; + +const lastSeen = new Map(); +let seq = 0; + +function record(path) { + const now = Date.now(); + if (now - (lastSeen.get(path) ?? 0) < COALESCE_MS) { + return; + } + lastSeen.set(path, now); + seq += 1; + const line = { + ts: now, + agent: AGENT, + run: RUN, + seq, + phase: 'end', + tool: 'Write', + path, + source: 'fs', + detail: 'seen as a write on disk; reads, searches and test runs are invisible here', + }; + try { + appendFileSync(OUT, JSON.stringify(line) + '\n', 'utf8'); + process.stdout.write(`${new Date(now).toISOString()} ${path}\n`); + } catch { + /* Ein volles Dateisystem kostet das Protokoll, nicht die Arbeit. */ + } +} + +try { + mkdirSync(dirname(OUT), { recursive: true }); +} catch { + /* Steht es schon da, ist alles gut; steht es nicht, meldet der erste Schreibversuch es. */ +} + +watch(ROOT, { recursive: true }, (_event, name) => { + if (typeof name !== 'string' || IGNORE.test(name)) { + return; + } + record(relative('.', name) || name); +}); + +process.stdout.write(`watching ${ROOT}, appending to ${OUT}\n`); diff --git a/graph-ui/agents/hooks/atlas-trace.py b/graph-ui/agents/hooks/atlas-trace.py new file mode 100644 index 000000000..86c9cde4a --- /dev/null +++ b/graph-ui/agents/hooks/atlas-trace.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +"""Record tool metadata in a local outbox and POST it to the existing daemon. + +Configure ATLAS_PROJECT to the indexed project name and ATLAS_DAEMON_URL when +using a nondefault daemon port. No bridge, listener or external transmission. +Pending events survive downtime and retry on the next hook or --flush. +The hook always returns zero so observing work cannot break the tool call. +""" +import json +import os +import sys +import time +import sqlite3 +import uuid +import urllib.request +import urllib.parse +import argparse +import pathlib +import shlex +import tempfile + +DEFAULT_FILE = os.path.join(os.path.expanduser('~'), '.atlas-trace', 'events.jsonl') + +def trace_file(): + return os.environ.get('ATLAS_TRACE_FILE') or DEFAULT_FILE + + +def open_outbox(): + path = trace_file() + '.outbox.sqlite3' + os.makedirs(os.path.dirname(os.path.abspath(path)), mode=0o700, exist_ok=True) + db = sqlite3.connect(path, timeout=0.5) + os.chmod(path, 0o600) + db.executescript(""" + PRAGMA journal_mode=WAL; + CREATE TABLE IF NOT EXISTS counters(run TEXT PRIMARY KEY, seq INTEGER NOT NULL); + CREATE TABLE IF NOT EXISTS pending(id INTEGER PRIMARY KEY, project TEXT NOT NULL, + payload TEXT NOT NULL); + """) + return db + + +def enqueue(db, project, row): + # Counter allocation and enqueue are one SQLite transaction: parallel hooks + # cannot assign the same run/sequence pair and silently discard an event. + with db: + db.execute('BEGIN IMMEDIATE') + db.execute('INSERT INTO counters VALUES(?,1) ON CONFLICT(run) DO UPDATE SET seq=seq+1', + (row['run'],)) + row['seq'] = db.execute('SELECT seq FROM counters WHERE run=?', (row['run'],)).fetchone()[0] + db.execute('INSERT INTO pending(project,payload) VALUES(?,?)', + (project, json.dumps(row, ensure_ascii=False))) + # A prolonged daemon outage cannot fill the disk without bound. Sequence + # gaps remain visible when older undelivered metadata expires. + db.execute('DELETE FROM pending WHERE id IN (SELECT id FROM pending ORDER BY id DESC LIMIT -1 OFFSET 10000)') + + +def flush(db): + origin = os.environ.get('ATLAS_DAEMON_URL', 'http://127.0.0.1:9749') + parsed = urllib.parse.urlsplit(origin) + if (parsed.scheme != 'http' or parsed.hostname not in ('127.0.0.1', 'localhost') + or parsed.username or parsed.password or parsed.path not in ('', '/') + or parsed.query or parsed.fragment): + return # Never forward local metadata to an external endpoint. + first = db.execute('SELECT project FROM pending ORDER BY id LIMIT 1').fetchone() + if not first: + return + rows = db.execute('SELECT id,payload FROM pending WHERE project=? ORDER BY id LIMIT 100', + first).fetchall() + data = json.dumps({'project': first[0], 'events': [json.loads(row[1]) for row in rows]}).encode() + request = urllib.request.Request(origin.rstrip('/') + '/api/agent-events', data=data, + headers={'Content-Type': 'application/json'}) + # No environment proxy, redirects, or second server; post only to loopback. + class NoRedirect(urllib.request.HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + return None + opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect()) + with opener.open(request, timeout=0.75) as response: + result = json.load(response) + if response.status != 200 or result.get('accepted', 0) + result.get('duplicates', 0) != len(rows): + return + with db: + db.executemany('DELETE FROM pending WHERE id=?', [(row[0],) for row in rows]) + + +def line_span(file_path, args): + """Use an explicit requested source range, never infer it after an edit. + + PostToolUse runs after mutation. Searching old_string could locate a different, + unchanged occurrence and falsely attribute that line to the observed edit. + """ + offset = args.get('offset') + limit = args.get('limit') + if isinstance(offset, int) and not isinstance(offset, bool) and offset > 0: + if isinstance(limit, int) and not isinstance(limit, bool) and limit > 0: + return [offset, offset + limit - 1] + return [offset, offset] + + return None + + +def daemon_origin(value): + parsed = urllib.parse.urlsplit(value) + if (parsed.scheme != 'http' or parsed.hostname not in ('127.0.0.1', 'localhost') + or parsed.username or parsed.password or parsed.path not in ('', '/') + or parsed.query or parsed.fragment): + raise ValueError('The daemon URL must be a loopback HTTP origin.') + return value.rstrip('/') + + +def install_claude(root, project, origin): + """Explicit, project-local install; preserve unrelated settings and hooks. + + No package downloads or network requests. Refuse symlinks, malformed settings, + and conflicting older installations rather than replace user configuration. + """ + root = pathlib.Path(root).resolve(strict=True) + if not root.is_dir() or not project or len(project.encode()) > 255: + raise ValueError('Choose the indexed repository root and exact project name.') + origin = daemon_origin(origin) + directory = root / '.claude' + hook_dir = directory / 'hooks' + settings = directory / 'settings.local.json' + target = hook_dir / 'cbm-atlas-trace.py' + if any(path.is_symlink() for path in (directory, hook_dir, settings, target)): + raise ValueError('Refusing symlinked hook or settings paths.') + original = settings.read_bytes() if settings.exists() else None + config = json.loads(original) if original is not None else {} + if not isinstance(config, dict): + raise ValueError('Existing settings must be a JSON object.') + hooks = config.setdefault('hooks', {}) + if not isinstance(hooks, dict): + raise ValueError('Existing hooks must be an object.') + entries = hooks.setdefault('PostToolUse', []) + if not isinstance(entries, list): + raise ValueError('Existing PostToolUse hooks must be an array.') + command = ('ATLAS_PROJECT=' + shlex.quote(project) + ' ATLAS_AGENT_NAME=' + shlex.quote('Claude Code') + + ' ATLAS_DAEMON_URL=' + shlex.quote(origin) + + ' python3 ' + shlex.quote(str(target))) + proposed = {'matcher': '', 'hooks': [{'type': 'command', 'command': command, 'timeout': 3}]} + installed = proposed in entries + for entry in entries: + for handler in entry.get('hooks', []) if isinstance(entry, dict) else []: + if not isinstance(handler, dict): + continue + try: + words = shlex.split(str(handler.get('command', ''))) + except ValueError: + words = [] + references_target = any(word in (str(target), '.claude/hooks/cbm-atlas-trace.py', + './.claude/hooks/cbm-atlas-trace.py') for word in words) + if references_target and entry != proposed: + raise ValueError('An existing Atlas hook differs. Review its matcher, command and timeout before changing it.') + source = pathlib.Path(__file__).read_bytes() + if target.exists() and target.read_bytes() != source: + raise ValueError('An existing Atlas hook file differs. Review it before replacing it.') + if not installed: + entries.append(proposed) + encoded = (json.dumps(config, indent=2, ensure_ascii=False) + '\n').encode() + # A changed file is never overwritten after our read. Directory operations + # happen only after the complete, reviewable configuration was validated. + directory.mkdir(mode=0o700, exist_ok=True) + hook_dir.mkdir(mode=0o700, exist_ok=True) + if (settings.read_bytes() if settings.exists() else None) != original: + raise ValueError('Settings changed during setup; retry after reviewing them.') + created_hook = not target.exists() + if created_hook: + with target.open('xb') as handle: + handle.write(source) + target.chmod(0o600) + if not installed: + descriptor, temporary = tempfile.mkstemp(prefix='.cbm-atlas-settings-', dir=directory) + try: + with os.fdopen(descriptor, 'wb') as handle: + handle.write(encoded) + if (settings.read_bytes() if settings.exists() else None) != original: + raise ValueError('Settings changed during setup; nothing was replaced.') + os.replace(temporary, settings) + finally: + if os.path.exists(temporary): + os.unlink(temporary) + return {'installed': True, 'changed': not installed or created_hook, + 'settings': str(settings), 'hook': str(target), 'project': project, + 'daemon': origin, 'next': 'Start a new Claude Code session in this repository, perform a tool call, then load activity.'} + + +def setup_main(): + parser = argparse.ArgumentParser(description='Install the local Code Atlas Claude Code tool hook.') + parser.add_argument('--install-claude', action='store_true', required=True) + parser.add_argument('--root', required=True, help='Exact indexed repository root') + parser.add_argument('--project', required=True, help='Exact indexed project name') + parser.add_argument('--daemon-url', default='http://127.0.0.1:9749') + args = parser.parse_args() + try: + result = install_claude(args.root, args.project, args.daemon_url) + except Exception as error: + print('Atlas setup: ' + str(error), file=sys.stderr) + return 1 + print(json.dumps(result, ensure_ascii=False)) + return 0 + + +def main(): + if '--flush' in sys.argv: + try: + with open_outbox() as db: + flush(db) + except Exception: + pass + return 0 + try: + payload = json.load(sys.stdin) + except Exception: + return 0 + try: + path = trace_file() + directory = os.path.dirname(path) + if directory: + os.makedirs(directory, exist_ok=True) + + tool = payload.get('tool_name') or '' + args = payload.get('tool_input') or {} + if not isinstance(args, dict): + args = {} + file_path = args.get('file_path') or args.get('notebook_path') or '' + run = str(payload.get('session_id') or uuid.uuid4()) + root = str(payload.get('cwd') or os.getcwd()) + project = os.environ.get('ATLAS_PROJECT', os.path.basename(root.rstrip(os.sep))) + + row = { + 'ts': int(time.time() * 1000), + 'agent': os.environ.get('ATLAS_AGENT_NAME', 'agent'), + 'run': run, + 'seq': 0, + 'phase': 'end', + 'tool': tool, + 'path': str(file_path or args.get('path') or ''), + # Commands, patterns and descriptions may contain credentials. + 'detail': '', + 'source': 'tool-hook', + } + span = line_span(file_path, args) if tool == 'Read' else None + if span is not None: + row['lines'] = span + + if os.path.isabs(row['path']): + row['path'] = os.path.relpath(row['path'], root) + with open_outbox() as db: + enqueue(db, project, row) + flush(db) + except Exception: + pass + return 0 + + +if __name__ == '__main__': + sys.exit(setup_main() if '--install-claude' in sys.argv else main()) diff --git a/graph-ui/agents/hooks/test_atlas_trace.py b/graph-ui/agents/hooks/test_atlas_trace.py new file mode 100644 index 000000000..cc43594b5 --- /dev/null +++ b/graph-ui/agents/hooks/test_atlas_trace.py @@ -0,0 +1,218 @@ +"""Reproducible local producer tests; no external network or agent accounts.""" +import concurrent.futures +import importlib.util +import io +import json +import os +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +spec = importlib.util.spec_from_file_location('atlas_trace', Path(__file__).with_name('atlas-trace.py')) +hook = importlib.util.module_from_spec(spec) +spec.loader.exec_module(hook) + + +class TraceTests(unittest.TestCase): + def setUp(self): + self.directory = tempfile.TemporaryDirectory() + self.environment = patch.dict(os.environ, { + 'ATLAS_TRACE_FILE': os.path.join(self.directory.name, 'events'), + 'ATLAS_DAEMON_URL': 'http://127.0.0.1:9749', + }) + self.environment.start() + + def tearDown(self): + self.environment.stop() + self.directory.cleanup() + + def record(self, number): + with hook.open_outbox() as db: + hook.enqueue(db, 'fixture', {'run': 'concurrent-run', 'seq': 0, 'ts': number, + 'agent': 'fixture', 'tool': 'Read', 'phase': 'end'}) + + def assert_private_tool_input_is_omitted(self, tool, field, value, private_marker): + root = Path(self.directory.name) + source = root / 'src' / 'example.py' + payload = {'tool_name': tool, 'session_id': 'privacy-run', 'cwd': str(root), + 'tool_input': {field: value, 'file_path': str(source)}} + generated = [] + original_enqueue = hook.enqueue + + def capture_enqueue(db, project, row): + generated.append(json.loads(json.dumps(row))) + original_enqueue(db, project, row) + + with patch.dict(os.environ, {'ATLAS_PROJECT': 'fixture', 'ATLAS_AGENT_NAME': 'fixture'}), \ + patch.object(hook.sys, 'argv', ['atlas-trace.py']), \ + patch.object(hook.sys, 'stdin', io.StringIO(json.dumps(payload))), \ + patch.object(hook.time, 'time', return_value=1234.5), \ + patch.object(hook, 'enqueue', side_effect=capture_enqueue), \ + patch.object(hook.urllib.request, 'build_opener') as transport: + transport.return_value.open.side_effect = OSError('offline fixture') + self.assertEqual(hook.main(), 0) + transport.return_value.open.assert_called_once() + request = transport.return_value.open.call_args.args[0] + + with hook.open_outbox() as db: + pending = db.execute('SELECT project,payload FROM pending').fetchall() + self.assertEqual(len(generated), 1) + self.assertEqual(len(pending), 1) + self.assertEqual(pending[0][0], 'fixture') + outbound = json.loads(request.data) + self.assertEqual(outbound['project'], 'fixture') + self.assertEqual(len(outbound['events']), 1) + for surface, event, serialized in ( + ('generated', generated[0], json.dumps(generated[0])), + ('persisted', json.loads(pending[0][1]), pending[0][1]), + ('outgoing', outbound['events'][0], request.data.decode())): + with self.subTest(surface=surface): + self.assertEqual(event['tool'], tool) + self.assertEqual(event['path'], os.path.join('src', 'example.py')) + self.assertEqual(event['agent'], 'fixture') + self.assertEqual(event['run'], 'privacy-run') + self.assertEqual(event['phase'], 'end') + self.assertEqual(event['source'], 'tool-hook') + self.assertEqual(event['ts'], 1234500) + self.assertNotIn(private_marker, serialized) + self.assertNotIn(value, serialized) + + def test_command_private_values_never_enter_event_payloads(self): + marker = 'SYNTHETIC_COMMAND_PRIVATE_VALUE_71E5' + self.assert_private_tool_input_is_omitted( + 'Bash', 'command', 'AUTH_TOKEN=' + marker + ' example-tool --quiet', marker) + + def test_search_patterns_never_enter_event_payloads(self): + marker = 'SYNTHETIC_PATTERN_PRIVATE_VALUE_83A6' + self.assert_private_tool_input_is_omitted('Grep', 'pattern', marker, marker) + + def test_description_private_values_never_enter_event_payloads(self): + marker = 'SYNTHETIC_DESCRIPTION_PRIVATE_VALUE_92B7' + self.assert_private_tool_input_is_omitted( + 'Bash', 'description', 'Inspect the value ' + marker, marker) + + def test_concurrent_hooks_allocate_unique_sequences_and_keep_pending_after_failure(self): + with hook.open_outbox(): + pass + with concurrent.futures.ThreadPoolExecutor(max_workers=4) as pool: + list(pool.map(self.record, range(20))) + with hook.open_outbox() as db: + rows = db.execute('SELECT payload FROM pending').fetchall() + self.assertEqual(sorted(json.loads(row[0])['seq'] for row in rows), list(range(1, 21))) + with patch.object(hook.urllib.request, 'build_opener', side_effect=OSError('offline')): + with self.assertRaises(OSError): + hook.flush(db) + self.assertEqual(db.execute('SELECT count(*) FROM pending').fetchone()[0], 20) + + def test_external_destinations_and_credentials_are_never_contacted(self): + self.record(1) + for origin in ('https://example.com', 'http://example.com', + 'http://127.0.0.1.evil.test', 'http://secret@localhost:9749', + 'http://localhost:9749/redirect', 'http://localhost:9749?secret=1'): + with patch.dict(os.environ, {'ATLAS_DAEMON_URL': origin}), \ + patch.object(hook.urllib.request, 'build_opener') as network, \ + hook.open_outbox() as db: + hook.flush(db) + network.assert_not_called() + self.assertEqual(db.execute('SELECT count(*) FROM pending').fetchone()[0], 1) + + def test_explicit_setup_preserves_settings_is_idempotent_and_escapes_names(self): + root = Path(self.directory.name) / "repo with 'quote'" + (root / '.claude').mkdir(parents=True) + settings = root / '.claude/settings.local.json' + original = {'permissions': {'allow': ['Read']}, 'hooks': {'PostToolUse': [ + {'matcher': 'Read', 'hooks': [{'type': 'command', 'command': 'existing-hook'}]}]}} + settings.write_text(json.dumps(original)) + with patch.object(hook.urllib.request, 'build_opener') as network: + result = hook.install_claude(root, "project'; echo bad", 'http://127.0.0.1:9749') + again = hook.install_claude(root, "project'; echo bad", 'http://127.0.0.1:9749') + network.assert_not_called() + self.assertTrue(result['changed']) + self.assertFalse(again['changed']) + installed = json.loads(settings.read_text()) + self.assertEqual(installed['permissions'], original['permissions']) + self.assertEqual(installed['hooks']['PostToolUse'][0], original['hooks']['PostToolUse'][0]) + self.assertEqual(len(installed['hooks']['PostToolUse']), 2) + command = installed['hooks']['PostToolUse'][1]['hooks'][0]['command'] + self.assertEqual(hook.shlex.split(command)[0], "ATLAS_PROJECT=project'; echo bad") + self.assertEqual(hook.shlex.split(command)[-1], str(root.resolve() / '.claude/hooks/cbm-atlas-trace.py')) + before = settings.read_bytes() + with self.assertRaises(ValueError): + hook.install_claude(root, 'another-project', 'http://127.0.0.1:9749') + self.assertEqual(settings.read_bytes(), before) + + def test_setup_refuses_symlinks_malformed_settings_and_external_daemons_before_writes(self): + root = Path(self.directory.name) / 'repo' + root.mkdir() + with self.assertRaises(ValueError): + hook.install_claude(root, 'fixture', 'https://example.com') + self.assertFalse((root / '.claude').exists()) + (root / '.claude').mkdir() + settings = root / '.claude/settings.local.json' + settings.write_text('malformed user data') + with self.assertRaises(ValueError): + hook.install_claude(root, 'fixture', 'http://127.0.0.1:9749') + self.assertEqual(settings.read_text(), 'malformed user data') + settings.unlink() + target = root / 'external-settings.json' + target.write_text('{}') + settings.symlink_to(target) + with self.assertRaises(ValueError): + hook.install_claude(root, 'fixture', 'http://127.0.0.1:9749') + self.assertEqual(target.read_text(), '{}') + self.assertFalse((root / '.claude/hooks').exists()) + + def test_setup_refuses_a_second_execution_with_a_different_matcher_or_timeout(self): + root = Path(self.directory.name) / 'TEST fixture' + root.mkdir() + hook.install_claude(root, 'fixture', 'http://127.0.0.1:9749') + settings = root / '.claude/settings.local.json' + pristine = json.loads(settings.read_text()) + for change in ('matcher', 'timeout', 'relative-command'): + config = json.loads(json.dumps(pristine)) + entry = config['hooks']['PostToolUse'][0] + if change == 'matcher': + entry['matcher'] = 'Read' + elif change == 'timeout': + entry['hooks'][0]['timeout'] = 5 + else: + entry['hooks'][0]['command'] = 'python3 .claude/hooks/cbm-atlas-trace.py' + settings.write_text(json.dumps(config)) + before = settings.read_bytes() + with self.assertRaisesRegex(ValueError, 'existing Atlas hook differs'): + hook.install_claude(root, 'fixture', 'http://127.0.0.1:9749') + self.assertEqual(settings.read_bytes(), before) + self.assertEqual(len(json.loads(settings.read_text())['hooks']['PostToolUse']), 1) + + def test_post_edit_range_is_unknown_instead_of_an_unmodified_duplicate_occurrence(self): + source = Path(self.directory.name) / 'edited.txt' + source.write_text('new value\nold value\n') + with patch('builtins.open', side_effect=AssertionError('Metadata hook must not read source')): + self.assertIsNone(hook.line_span(str(source), {'old_string': 'old value', 'new_string': 'new value'})) + self.assertEqual(hook.line_span(str(source), {'offset': 8, 'limit': 3}), [8, 10]) + self.assertIsNone(hook.line_span(str(source), {'offset': True})) + + def test_acknowledged_duplicate_retry_removes_only_acknowledged_rows(self): + self.record(1) + self.record(2) + class Response: + status = 200 + def __enter__(self): return self + def __exit__(self, *args): return None + def read(self): return b'{"accepted":0,"duplicates":2}' + class Opener: + def open(self, request, timeout): + self_body = json.loads(request.data) + assert request.full_url == 'http://127.0.0.1:9749/api/agent-events' + assert self_body['project'] == 'fixture' + assert len(self_body['events']) == 2 + return Response() + with patch.object(hook.urllib.request, 'build_opener', return_value=Opener()), \ + hook.open_outbox() as db: + hook.flush(db) + self.assertEqual(db.execute('SELECT count(*) FROM pending').fetchone()[0], 0) + + +if __name__ == '__main__': + unittest.main() diff --git a/graph-ui/components.json b/graph-ui/components.json deleted file mode 100644 index 86ac0b1c9..000000000 --- a/graph-ui/components.json +++ /dev/null @@ -1 +0,0 @@ -{"$schema":"https://ui.shadcn.com/schema.json","style":"new-york","rsc":false,"tsx":true,"tailwind":{"config":"","css":"src/styles/globals.css","baseColor":"neutral","cssVariables":true},"aliases":{"components":"@/components","utils":"@/lib/utils","ui":"@/components/ui","lib":"@/lib","hooks":"@/hooks"},"iconLibrary":"lucide"} diff --git a/graph-ui/docs/adr/0001-modellwahl.md b/graph-ui/docs/adr/0001-modellwahl.md new file mode 100644 index 000000000..ce562d6bd --- /dev/null +++ b/graph-ui/docs/adr/0001-modellwahl.md @@ -0,0 +1,70 @@ +# ADR 0001: Optional local inference and model evidence + +## Decision + +CBM Lens offers optional browser inference. Weights are downloaded only after the +user opts in; they are not embedded in the executable. Model selection remains a +user choice. Browser model IDs, fixed revisions, allowed files and licenses live in +`src/browser-ai/model-policy.ts`; [THIRD_PARTY.md](../../THIRD_PARTY.md) documents them. + +The older GGUF sidecar catalogue remains supported by local scripts and its recorded +evaluation. It is separate from the browser ONNX catalogue. Its measurements do not +predict browser performance, another machine's performance or a model's general rank. + +## Sidecar model sources + +The six Q4_K_M candidates below were recorded in August 2026. Class A used a 3072-token +context and class B used 8192 tokens. Exact download sizes refer to those evaluated +files, not runtime memory. Checksums are retained in `models/SHA256SUMS`. + +| Model | Class | Download source | Recorded size | License recorded for the base model | +| --- | --- | --- | --- | --- | +| Qwen3.5-2B | A | [unsloth/Qwen3.5-2B-GGUF](https://huggingface.co/unsloth/Qwen3.5-2B-GGUF) | 1280835840 Bytes | Apache 2.0 | +| LFM2.5-1.2B-Instruct | A | [LiquidAI/LFM2.5-1.2B-Instruct-GGUF](https://huggingface.co/LiquidAI/LFM2.5-1.2B-Instruct-GGUF) | 730895168 Bytes | LFM Open License v1.0; commercial terms apply at $10M annual revenue | +| MiniCPM5-1B | A | [openbmb/MiniCPM5-1B-GGUF](https://huggingface.co/openbmb/MiniCPM5-1B-GGUF) | 688065920 Bytes | Apache 2.0 | +| Qwen2.5-Coder-1.5B-Instruct | A | [Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF](https://huggingface.co/Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF) | 1117320768 Bytes | Apache 2.0 | +| Qwen3.5-4B | B | [unsloth/Qwen3.5-4B-MTP-GGUF](https://huggingface.co/unsloth/Qwen3.5-4B-MTP-GGUF) | 2834975040 Bytes | Apache 2.0 | +| Gemma 4 E4B-it | B | [unsloth/gemma-4-E4B-it-GGUF](https://huggingface.co/unsloth/gemma-4-E4B-it-GGUF) | 4977171584 Bytes | Apache 2.0 | + +Review current model-card terms before downloading or redistribution. The recorded +LFM license allows free commercial use below $10M annual revenue; use beyond that +threshold requires separate consideration. No evaluation result overrides licensing. + +## Evaluation limits + +`verification/w5/eval.json` records 44 fixture questions, including 23 in German, +with temperature 0 and seed 42. The figures below summarize that saved run. Citation +compliance measures the evaluation's citation rule, not factual correctness. + +| Model | Class | Pass rate | Citation compliance | Mean tokens/s, rounded | +| --- | --- | --- | --- | --- | +| Qwen3.5-2B | A | 0.682 | 0.932 | 86 | +| LFM2.5-1.2B | A | 0.295 | 0.432 | 110 | +| MiniCPM5-1B | A | 0.250 | 0.545 | 171 | +| Qwen2.5-Coder-1.5B | A | 0.227 | 0.705 | 84 | +| Qwen3.5-4B | B | 0.818 | 0.955 | 38 | +| gemma-4-E4B | B | 0.841 | 1.000 | 32 | + +The recorded class recommendations were Qwen3.5-2B and gemma-4-E4B, with pass-rate +and citation thresholds of 0.6 and 0.9. They are recommendations, not installed +defaults. A reported repeat run has no separate saved artifact and is not independent +reproduction evidence. The original record does not measure unscorable citations; +missing measurements must not be displayed as zero. + +## Engine boundary + +The sidecar evaluation used llama.cpp on Apple Silicon with Metal and GGUF files. +Its source/build record is in [runtime provenance](../../vendor/llama/HERKUNFT.md). +`llm/start.sh` also supports a local model cache and OpenAI-compatible transport; +the class-specific script options reproduce the recorded evaluation. + +Browser inference uses a different runtime and format. Compare actual device and +workload behavior before drawing performance conclusions, especially prompt prefill, +GPU memory limits and large-file handling. Relevant engine sources retained from +the original investigation are [llama.cpp](https://github.com/ggml-org/llama.cpp), +[wllama](https://github.com/ngxson/wllama), [WebLLM](https://webllm.mlc.ai/docs/), +[Transformers.js](https://github.com/huggingface/transformers.js), +[WebGPU limits](https://developer.mozilla.org/en-US/docs/Web/API/GPUSupportedLimits), +and the [ggml WebGPU study](https://arxiv.org/html/2605.20706v1). +These references explain the formats and tradeoffs; the sidecar timings above are +not browser benchmarks. diff --git a/graph-ui/eval/questions.json b/graph-ui/eval/questions.json new file mode 100644 index 000000000..a70732996 --- /dev/null +++ b/graph-ui/eval/questions.json @@ -0,0 +1,357 @@ +[ + { + "id": "who-01-de", + "class": "who-calls", + "focusQn": "src.services.userService.createUser", + "question": "Wer ruft createUser auf?", + "expected": ["registerUserRoutes", "create"], + "depth": 1 + }, + { + "id": "who-02-en", + "class": "who-calls", + "focusQn": "src.services.userService.listUsers", + "question": "Who calls listUsers?", + "expected": ["registerUserRoutes", "createUser"], + "depth": 1 + }, + { + "id": "who-03-de", + "class": "who-calls", + "focusQn": "src.util.validate.validateUser", + "question": "Wer ruft validateUser?", + "expected": ["createUser"], + "depth": 1 + }, + { + "id": "who-04-en", + "class": "who-calls", + "focusQn": "src.repo.db.insert", + "question": "Who calls insert?", + "expected": ["createUser"], + "depth": 1 + }, + { + "id": "who-05-de", + "class": "who-calls", + "focusQn": "src.services.orderService.getOrder", + "question": "Wer ruft getOrder?", + "expected": ["registerOrderRoutes"], + "depth": 1 + }, + { + "id": "who-06-en", + "class": "who-calls", + "focusQn": "src.repo.db.query", + "question": "Who calls query?", + "expected": ["listUsers"], + "depth": 1 + }, + { + "id": "who-07-de", + "class": "who-calls", + "focusQn": "src.routes.users.registerUserRoutes", + "question": "Wer ruft registerUserRoutes?", + "expected": ["createApp"], + "depth": 1 + }, + { + "id": "who-08-en", + "class": "who-calls", + "focusQn": "src.config.loadConfig", + "question": "Who calls loadConfig?", + "expected": ["main"], + "depth": 1 + }, + { + "id": "what-01-de", + "class": "what-is", + "focusQn": "src.services.userService.createUser", + "question": "Was macht createUser?", + "expected": ["validateUser", "insert"], + "depth": 1 + }, + { + "id": "what-02-en", + "class": "what-is", + "focusQn": "src.services.userService.listUsers", + "question": "What does listUsers do?", + "expected": ["query"], + "depth": 1 + }, + { + "id": "what-03-de", + "class": "what-is", + "focusQn": "src.services.orderService.getOrder", + "question": "Was macht getOrder?", + "expected": ["validateId", "query"], + "depth": 1 + }, + { + "id": "what-04-en", + "class": "what-is", + "focusQn": "src.repo.db.hotspotScan", + "question": "What does hotspotScan do?", + "expected": ["query"], + "depth": 1 + }, + { + "id": "what-05-de", + "class": "what-is", + "focusQn": "src.server.main", + "question": "Was macht main?", + "expected": ["loadConfig", "createApp"], + "depth": 1 + }, + { + "id": "what-06-en", + "class": "what-is", + "focusQn": "src.server.createApp", + "question": "What does createApp do?", + "expected": ["registerUserRoutes"], + "depth": 1 + }, + { + "id": "what-07-de", + "class": "what-is", + "focusQn": "src.config.loadConfig", + "question": "Was macht loadConfig?", + "expected": ["loadConfig"], + "depth": 0 + }, + { + "id": "what-08-en", + "class": "what-is", + "focusQn": "src.util.validate.ValidationError", + "question": "What is ValidationError?", + "expected": ["validate.ts"], + "depth": 0 + }, + { + "id": "what-09-de", + "class": "what-is", + "focusQn": "src.services.userService.create", + "question": "Was tut `userService.create`?", + "expected": ["createUser"], + "depth": 1 + }, + { + "id": "what-10-en", + "class": "what-is", + "focusQn": "src.routes.orders.registerOrderRoutes", + "question": "What does registerOrderRoutes do?", + "expected": ["getOrder"], + "depth": 1 + }, + { + "id": "err-01-de", + "class": "why-error", + "focusQn": "src.services.userService.createUser", + "question": "Welche Fehler kann createUser werfen?", + "expected": ["ValidationError"], + "depth": 1 + }, + { + "id": "err-02-en", + "class": "why-error", + "focusQn": "src.util.validate.validateUser", + "question": "What errors can validateUser raise?", + "expected": ["ValidationError"], + "depth": 1 + }, + { + "id": "err-03-de", + "class": "why-error", + "focusQn": "src.util.validate.validateId", + "question": "Welche Ausnahme wirft validateId?", + "expected": ["ValidationError"], + "depth": 1 + }, + { + "id": "err-04-en", + "class": "why-error", + "focusQn": "src.services.orderService.getOrder", + "question": "What errors can getOrder raise?", + "expected": ["ValidationError"], + "depth": 1 + }, + { + "id": "err-05-de", + "class": "why-error", + "focusQn": "src.services.userService.createUser", + "question": "Warum schlaegt createUser fehl, wenn DB_URL nicht gesetzt ist?", + "expected": ["ValidationError", "DB_URL"], + "depth": 1 + }, + { + "id": "err-06-en", + "class": "why-error", + "focusQn": "src.repo.db.insert", + "question": "What errors can insert raise?", + "expected": ["insert"], + "forbidden": ["ValidationError"], + "depth": 1 + }, + { + "id": "if-01-de", + "class": "what-if", + "focusQn": "src.services.userService.createUser", + "question": "Was passiert, wenn ich createUser aendere?", + "expected": ["registerUserRoutes", "create"], + "depth": 1 + }, + { + "id": "if-02-en", + "class": "what-if", + "focusQn": "src.services.userService.listUsers", + "question": "What happens if I change listUsers?", + "expected": ["registerUserRoutes"], + "depth": 1 + }, + { + "id": "if-03-de", + "class": "what-if", + "focusQn": "src.repo.db.query", + "question": "Welche Auswirkung haette eine Aenderung an query?", + "expected": ["listUsers"], + "depth": 1 + }, + { + "id": "if-04-en", + "class": "what-if", + "focusQn": "src.util.validate.validateUser", + "question": "What is the blast radius of validateUser?", + "expected": ["createUser"], + "depth": 1 + }, + { + "id": "if-05-de", + "class": "what-if", + "focusQn": "src.repo.db.insert", + "question": "Was passiert, wenn ich insert aendere?", + "expected": ["createUser"], + "depth": 2 + }, + { + "id": "if-06-en", + "class": "what-if", + "focusQn": "src.routes.users.registerUserRoutes", + "question": "What happens if registerUserRoutes changes?", + "expected": ["createApp"], + "depth": 1 + }, + { + "id": "entry-01-de", + "class": "where-entry", + "focusQn": "src.server.main", + "question": "Welche Routen gibt es in diesem Projekt?", + "expected": ["/users"], + "depth": 1 + }, + { + "id": "entry-02-en", + "class": "where-entry", + "focusQn": "src.server.main", + "question": "Which HTTP routes does this project expose?", + "expected": ["/users", "/orders"], + "depth": 1 + }, + { + "id": "entry-03-de", + "class": "where-entry", + "focusQn": "src.server.main", + "question": "Wo faengt dieses Projekt an?", + "expected": ["src/server.ts"], + "depth": 1 + }, + { + "id": "entry-04-en", + "class": "where-entry", + "focusQn": "src.server.main", + "question": "Where do I start reading this project?", + "expected": ["src/server.ts"], + "depth": 1 + }, + { + "id": "entry-05-de", + "class": "where-entry", + "focusQn": "src.services.userService.createUser", + "question": "Welche Route fuehrt zu createUser?", + "expected": ["/users"], + "depth": 1 + }, + { + "id": "cmp-01-de", + "class": "compare", + "focusQn": "src.services.userService.createUser", + "question": "Was ist der Unterschied zwischen createUser und listUsers?", + "expected": ["validateUser", "query"], + "depth": 1 + }, + { + "id": "cmp-02-en", + "class": "compare", + "focusQn": "src.services.orderService.getOrder", + "question": "What is the difference between getOrder and createUser?", + "expected": ["validateId", "validateUser"], + "depth": 1 + }, + { + "id": "cmp-03-de", + "class": "compare", + "focusQn": "src.services.userService.listUsers", + "question": "Vergleiche listUsers und createUser.", + "expected": ["query", "validateUser"], + "depth": 1 + }, + { + "id": "cmp-04-en", + "class": "compare", + "focusQn": "src.repo.db.hotspotScan", + "question": "What is the difference between hotspotScan and walk?", + "expected": ["query"], + "depth": 1 + }, + { + "id": "over-01-de", + "class": "overview", + "focusQn": "src.server.main", + "question": "Gib mir einen Ueberblick ueber dieses Projekt.", + "expected": ["TypeScript"], + "depth": 1 + }, + { + "id": "over-02-en", + "class": "overview", + "focusQn": "src.server.main", + "question": "What is this project made of?", + "expected": ["TypeScript"], + "depth": 1 + }, + { + "id": "over-03-de", + "class": "overview", + "focusQn": "src.server.main", + "question": "Wie ist die Architektur dieses Projekts aufgebaut?", + "expected": ["symbol"], + "depth": 1 + }, + { + "id": "honest-01-de", + "class": "other", + "focusQn": "src.services.userService.createUser", + "question": "Hallo, alles klar bei dir?", + "expected": ["no card"], + "forbidden": ["createUser"], + "depth": 1 + }, + { + "id": "honest-02-en", + "class": "what-is", + "focusQn": "src.services.userService.createUser", + "question": "What is @notASymbolAnywhereInThisRepo?", + "expected": ["no card"], + "forbidden": ["createUser"], + "depth": 1 + } +] diff --git a/graph-ui/fixtures/adr/cbm-preview.md b/graph-ui/fixtures/adr/cbm-preview.md new file mode 100644 index 000000000..da314a950 --- /dev/null +++ b/graph-ui/fixtures/adr/cbm-preview.md @@ -0,0 +1,63 @@ +# Architecture decisions + +> Preview examples for cbm-preview. These editable notes capture frontend design choices discussed during development; they are not a historical ADR archive or graph-inferred facts. Status: proposed for review. + +## 001: Explain architecture from evidence + +**Status:** Proposed · **Scope:** Architecture workspace + +### Context + +A repository map explains where code lives. System structure and behavior also need to explain relationships, boundaries, and execution steps. A convincing picture should remain traceable to its inputs. + +### Decision + +Combine indexed graph relationships with source evidence. Keep the repository map, routes, hotspots, system structure, and behavior as distinct lenses. Mark inferred relationships as inferred, and make their evidence inspectable. Do not embed project-specific architectural explanations in the visualization code. + +### Consequences + +- The same analysis can be applied to other indexed repositories. +- Missing evidence remains a visible limitation rather than a fabricated connection. +- A source or graph change can alter the view; these written decisions change only when someone edits them. + +**Implementation references:** `graph-ui/src/architecture/`, `src/store/architecture_projection.c`. + +## 002: Keep browser AI optional + +**Status:** Proposed · **Scope:** Local chat + +### Context + +Code exploration should work before any model is downloaded. Users may want to ask about a file or a literal code selection without sending source to a hosted model. + +### Decision + +Run the chat model locally in the browser after explicit setup. Download weights only after opt-in. Attach the selected code or graph context to the next prompt and render answers as Markdown. + +### Consequences + +| Benefit | Tradeoff | +| --- | --- | +| Exploration works without model setup | Chat is unavailable until a model is enabled | +| Inference stays on the device | Model quality and speed depend on local resources | +| The prompt can use exact marked code | A bounded context cannot include an entire large repository | + +**Implementation references:** `graph-ui/src/browser-ai/`, `graph-ui/src/reader/MonacoReader.tsx`. + +## 003: Keep agent activity experimental + +**Status:** Proposed · **Scope:** Agents workspace + +### Context + +The current integration records events from explicitly configured agent hooks. The UI cannot infer every agent operation merely by observing the graph. + +### Decision + +Hide Agents unless the experimental build flag is enabled. Treat hook events as observed activity, and avoid implying complete task execution, tool outcomes, or duration data. + +### Consequences + +The default navigation remains focused. Further agent adapters and a richer event contract can be introduced before promoting the view to a standard workspace. + +**Implementation references:** `graph-ui/src/app/feature-flags.ts`, `graph-ui/agents/hooks/atlas-trace.py`, `src/ui/activity.c`. diff --git a/graph-ui/fixtures/adr/train-ticket.md b/graph-ui/fixtures/adr/train-ticket.md new file mode 100644 index 000000000..f6ae48f32 --- /dev/null +++ b/graph-ui/fixtures/adr/train-ticket.md @@ -0,0 +1,44 @@ +# Architecture decisions + +> Preview examples for the indexed Train Ticket repository. These are proposed review notes for exercising the ADR workspace, not decisions attributed to the upstream maintainers. + +## 001: Review services as deployment boundaries + +**Status:** Proposed · **Scope:** Routes review + +### Context + +A large service graph is difficult to understand when every source file has equal visual weight. Deployment declarations provide another useful level of organization. + +### Decision + +Use detected service and container boundaries to organize the routes review. Keep the declaration behind each boundary inspectable. Treat a deployment dependency and a source-level request as different kinds of evidence. + +### Consequences + +- The initial view can show relationships between services before drilling into handlers. +- A declared dependency alone does not prove that a request is sent. +- Services without discovered connections should remain visible. + +## 002: Follow a request through its evidence + +**Status:** Proposed · **Scope:** Cross-service investigation + +### Context + +Matching a caller to a route helps explain the system, but static analysis may not resolve dynamic hosts, route parameters, or runtime configuration. + +### Decision + +During review, follow a connection from its caller through the route evidence to the destination handler. Keep unresolved destinations visible as unknowns. Use source inspection to verify a suspected relationship before treating it as established. + +### Review checklist + +- [ ] Inspect an outgoing request in the selected service. +- [ ] Compare its path and method with the destination route. +- [ ] Check relevant configuration and service naming. +- [ ] Record unresolved runtime assumptions here. + +### Consequences + +This creates a repeatable review path while avoiding claims that a static graph proves runtime behavior. diff --git a/graph-ui/fixtures/adr/voting-services.md b/graph-ui/fixtures/adr/voting-services.md new file mode 100644 index 000000000..c253837e2 --- /dev/null +++ b/graph-ui/fixtures/adr/voting-services.md @@ -0,0 +1,25 @@ +# Architecture decisions + +> Preview example for voting-services. This proposed review note is test content, not an upstream architectural decision. + +## 001: Distinguish declared and observed connections + +**Status:** Proposed · **Scope:** Service map review + +### Context + +A small connected application is useful for checking whether a service visualization makes each relationship understandable. + +### Decision + +Review deployment dependencies, request relationships, and datastore connections separately. Inspect the source or configuration evidence for each edge. Do not interpret an edge count as traffic volume or a static dependency as an observed runtime call. + +### Consequences + +The service map can explain what is connected and why without implying measurements that were never collected. Missing connections become concrete questions for further inspection. + +### Next review + +- [ ] Select a service and inspect its neighboring connections. +- [ ] Verify one connection against source or configuration. +- [ ] Edit this note with the outcome and save it. diff --git a/graph-ui/fixtures/agent-events/HERKUNFT.md b/graph-ui/fixtures/agent-events/HERKUNFT.md new file mode 100644 index 000000000..be44c6f07 --- /dev/null +++ b/graph-ui/fixtures/agent-events/HERKUNFT.md @@ -0,0 +1,56 @@ +# Agent-event fixture provenance + +These stable replay inputs exercise activity visualization without requiring a live +coding session. They contain no source contents or tool output. They are test +fixtures, not evidence of current agent activity. + +## `w11a-replay.jsonl` + +Derived from a real tool-hook recording made while working on this repository on +29-30 August 2026. The private original is not committed because it contains local +paths and commands. This fixture preserves its event shape, relative tool mix, +initial timestamp (`1788038561000`) and 44 consecutive time intervals in milliseconds: + +```text +10065, 13599, 115703, 5123, 18182, 6789, 1140, 13387, 5243, 5570, 6676, +21168, 51146, 6330, 6339, 28257, 4029, 318, 2709, 709, 83, 6176, 1116, +3373, 3642, 3149, 2275, 4175, 2884, 4066, 2310, 2028, 2838, 33596, 5878, +17621, 826, 5788, 11062, 6988, 5724, 10001, 17767, 13461 +``` + +The intervals came from the first 45-event window without a pause over two minutes, +beginning at line 57 of the original. The following content was deliberately changed: + +- Absolute paths became relative `fixtures/atlas-sample` paths. `package.json` is + intentionally outside the indexed graph to exercise unmapped activity. +- One original agent became the synthetic roles `implementer`, `checker` and + `explorer`; their eight-character run IDs are invented. +- Explicit `lines` ranges were added from fixture symbols. The original hook did + not record line ranges. +- Run `b7e4d19c` jumps from sequence 6 to 9 to expose two missing events. +- Two synthetic `intent` fields exercise explicitly labeled self-reports. +- `detail` values describe fixture operations instead of retaining private commands. + +The result contains 45 events, three runs, four work types, one two-event sequence +gap, two self-reports and one path outside the index. + +## `w11b-replay.jsonl` + +Entirely constructed test data, not a session recording. Its paths and line spans +come from `fixtures/atlas-sample`; timestamps and activity are deliberately chosen. + +- Eight agents plus the browser reader exercise the eight-body display limit. +- `explorer` and `planner` end 75 and 85 seconds before the last event. Both are + idle but still within retention; the oldest actor exercises the display cap. +- `implementer` writes `createUser` lines 23-36 five times in 1.8 seconds to test + aggregation into one visible wave, and visits fifteen symbols to exceed trail limits. +- `reviewer`, `indexer` and `tracer` end at `hotspotScan` in `src/repo/db.ts`, lines + 39-54, to test deterministic orbit separation at one symbol. +- Activity rates differ: fifteen recent events for `implementer`, two for `packer`, + none for idle `planner`. One `package.json` path remains unmapped and one event + has a synthetic self-reported `intent`. + +There are 73 events across eight runs and four work types over 145 seconds. Last +activity offsets are 0, 3.8, 4.4, 5, 7, 10, 75 and 85 seconds for `implementer`, +`tracer`, `indexer`, `reviewer`, `packer`, `checker`, `explorer` and `planner`. +The fixture does not encode progress, quality assessments or source contents. diff --git a/graph-ui/fixtures/agent-events/w11a-replay.jsonl b/graph-ui/fixtures/agent-events/w11a-replay.jsonl new file mode 100644 index 000000000..7dad2bc35 --- /dev/null +++ b/graph-ui/fixtures/agent-events/w11a-replay.jsonl @@ -0,0 +1,45 @@ +{"ts": 1788038561000, "agent": "explorer", "run": "c3a80f52", "seq": 1, "phase": "end", "tool": "Grep", "path": "src", "detail": "validate"} +{"ts": 1788038571065, "agent": "implementer", "run": "a1f2c3d4", "seq": 1, "phase": "end", "tool": "Read", "path": "src/services/userService.ts", "detail": "read the whole module before touching it", "lines": [1, 43]} +{"ts": 1788038584664, "agent": "checker", "run": "b7e4d19c", "seq": 1, "phase": "end", "tool": "Read", "path": "package.json", "detail": "read the scripts of the fixture repository"} +{"ts": 1788038700367, "agent": "explorer", "run": "c3a80f52", "seq": 2, "phase": "end", "tool": "Grep", "path": "src", "detail": "createUser"} +{"ts": 1788038705490, "agent": "implementer", "run": "a1f2c3d4", "seq": 2, "phase": "end", "tool": "Read", "path": "src/util/validate.ts", "detail": "read validateUser", "lines": [19, 31]} +{"ts": 1788038723672, "agent": "implementer", "run": "a1f2c3d4", "seq": 3, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "detail": "replaced the validation branch of createUser", "lines": [24, 30]} +{"ts": 1788038730461, "agent": "checker", "run": "b7e4d19c", "seq": 2, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "detail": "read the whole file"} +{"ts": 1788038731601, "agent": "explorer", "run": "c3a80f52", "seq": 3, "phase": "end", "tool": "Grep", "path": "src", "detail": "query"} +{"ts": 1788038744988, "agent": "implementer", "run": "a1f2c3d4", "seq": 4, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "detail": "reflowed createUser after the edit above", "lines": [23, 36]} +{"ts": 1788038750231, "agent": "checker", "run": "b7e4d19c", "seq": 3, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"} +{"ts": 1788038755801, "agent": "explorer", "run": "c3a80f52", "seq": 4, "phase": "end", "tool": "Read", "path": "src/routes/users.ts", "detail": "read registerUserRoutes", "lines": [7, 18]} +{"ts": 1788038762477, "agent": "implementer", "run": "a1f2c3d4", "seq": 5, "phase": "end", "tool": "Edit", "path": "src/util/validate.ts", "detail": "validateUser now names the field it rejected", "lines": [19, 31]} +{"ts": 1788038783645, "agent": "checker", "run": "b7e4d19c", "seq": 4, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"} +{"ts": 1788038834791, "agent": "explorer", "run": "c3a80f52", "seq": 5, "phase": "end", "tool": "Grep", "path": "src", "detail": "Order"} +{"ts": 1788038841121, "agent": "implementer", "run": "a1f2c3d4", "seq": 6, "phase": "end", "tool": "Read", "path": "src/services/orderService.ts", "detail": "read getOrder", "lines": [8, 26]} +{"ts": 1788038847460, "agent": "checker", "run": "b7e4d19c", "seq": 5, "phase": "end", "tool": "Read", "path": "test/userService.test.ts", "detail": "read the test file itself"} +{"ts": 1788038875717, "agent": "implementer", "run": "a1f2c3d4", "seq": 7, "phase": "end", "tool": "Edit", "path": "src/services/orderService.ts", "detail": "create now carries the customer id it was given", "lines": [30, 35]} +{"ts": 1788038879746, "agent": "explorer", "run": "c3a80f52", "seq": 6, "phase": "end", "tool": "Grep", "path": "src", "detail": "insert"} +{"ts": 1788038880064, "agent": "checker", "run": "b7e4d19c", "seq": 6, "phase": "end", "tool": "Bash", "path": "", "detail": "node --test tests/scaffold/"} +{"ts": 1788038882773, "agent": "implementer", "run": "a1f2c3d4", "seq": 8, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "detail": "read insert", "lines": [31, 35]} +{"ts": 1788038883482, "agent": "explorer", "run": "c3a80f52", "seq": 7, "phase": "end", "tool": "Read", "path": "src/config.ts", "detail": "read loadConfig", "lines": [11, 18]} +{"ts": 1788038883565, "agent": "implementer", "run": "a1f2c3d4", "seq": 9, "phase": "end", "tool": "Edit", "path": "src/repo/db.ts", "detail": "insert writes the payload it was handed", "lines": [31, 35], "intent": "making insert keep the payload instead of dropping it"} +{"ts": 1788038889741, "agent": "checker", "run": "b7e4d19c", "seq": 9, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"} +{"ts": 1788038890857, "agent": "explorer", "run": "c3a80f52", "seq": 8, "phase": "end", "tool": "Grep", "path": "src", "detail": "walk"} +{"ts": 1788038894230, "agent": "implementer", "run": "a1f2c3d4", "seq": 10, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "detail": "read hotspotScan", "lines": [39, 54]} +{"ts": 1788038897872, "agent": "checker", "run": "b7e4d19c", "seq": 10, "phase": "end", "tool": "Read", "path": "src/server.ts", "detail": "read createApp", "lines": [32, 37]} +{"ts": 1788038901021, "agent": "implementer", "run": "a1f2c3d4", "seq": 11, "phase": "end", "tool": "Edit", "path": "src/repo/db.ts", "detail": "hotspotScan stops at the first needle it finds", "lines": [39, 54]} +{"ts": 1788038903296, "agent": "explorer", "run": "c3a80f52", "seq": 9, "phase": "end", "tool": "Read", "path": "src/types.ts", "detail": "read UserEntity", "lines": [37, 53]} +{"ts": 1788038907471, "agent": "checker", "run": "b7e4d19c", "seq": 11, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"} +{"ts": 1788038910355, "agent": "implementer", "run": "a1f2c3d4", "seq": 12, "phase": "end", "tool": "Write", "path": "src/util/validate.ts", "detail": "wrote validateId over the old body", "lines": [33, 38]} +{"ts": 1788038914421, "agent": "explorer", "run": "c3a80f52", "seq": 10, "phase": "end", "tool": "Grep", "path": "src", "detail": "Router"} +{"ts": 1788038916731, "agent": "checker", "run": "b7e4d19c", "seq": 12, "phase": "end", "tool": "Read", "path": "src/routes/orders.ts", "detail": "read the whole route module"} +{"ts": 1788038918759, "agent": "implementer", "run": "a1f2c3d4", "seq": 13, "phase": "end", "tool": "Read", "path": "src/services/userService.ts", "detail": "read listUsers", "lines": [18, 21]} +{"ts": 1788038921597, "agent": "explorer", "run": "c3a80f52", "seq": 11, "phase": "end", "tool": "Read", "path": "src/util/validate.ts", "detail": "read ValidationError", "lines": [4, 12]} +{"ts": 1788038955193, "agent": "checker", "run": "b7e4d19c", "seq": 13, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"} +{"ts": 1788038961071, "agent": "implementer", "run": "a1f2c3d4", "seq": 14, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "detail": "listUsers takes the limit it is given", "lines": [18, 21]} +{"ts": 1788038978692, "agent": "explorer", "run": "c3a80f52", "seq": 12, "phase": "end", "tool": "Grep", "path": "src", "detail": "loadConfig"} +{"ts": 1788038979518, "agent": "checker", "run": "b7e4d19c", "seq": 14, "phase": "end", "tool": "Read", "path": "src/config.ts", "detail": "read isProduction", "lines": [20, 22]} +{"ts": 1788038985306, "agent": "implementer", "run": "a1f2c3d4", "seq": 15, "phase": "end", "tool": "Read", "path": "src/routes/users.ts", "detail": "read registerUserRoutes before wiring it", "lines": [7, 18]} +{"ts": 1788038996368, "agent": "explorer", "run": "c3a80f52", "seq": 13, "phase": "end", "tool": "Read", "path": "src/services/orderService.ts", "detail": "read the whole order module"} +{"ts": 1788039003356, "agent": "checker", "run": "b7e4d19c", "seq": 15, "phase": "end", "tool": "Read", "path": "src/server.ts", "detail": "read main", "lines": [39, 46]} +{"ts": 1788039009080, "agent": "implementer", "run": "a1f2c3d4", "seq": 16, "phase": "end", "tool": "Edit", "path": "src/routes/users.ts", "detail": "the user routes hand the limit through", "lines": [7, 18]} +{"ts": 1788039019081, "agent": "implementer", "run": "a1f2c3d4", "seq": 17, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "detail": "tightened the input check of createUser again", "lines": [24, 30], "intent": "tightening the input check in createUser"} +{"ts": 1788039036848, "agent": "checker", "run": "b7e4d19c", "seq": 16, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"} +{"ts": 1788039050309, "agent": "explorer", "run": "c3a80f52", "seq": 14, "phase": "end", "tool": "Grep", "path": "src", "detail": "validate"} diff --git a/graph-ui/fixtures/agent-events/w11b-replay.jsonl b/graph-ui/fixtures/agent-events/w11b-replay.jsonl new file mode 100644 index 000000000..1a1c68d17 --- /dev/null +++ b/graph-ui/fixtures/agent-events/w11b-replay.jsonl @@ -0,0 +1,73 @@ +{"ts": 1788139200000, "agent": "explorer", "run": "c3a80f52", "seq": 1, "phase": "end", "tool": "Grep", "path": "src", "detail": "validate"} +{"ts": 1788139201200, "agent": "implementer", "run": "a1f2c3d4", "seq": 1, "phase": "end", "tool": "Read", "path": "src/services/userService.ts", "lines": [23, 36], "detail": "read createUser before touching it"} +{"ts": 1788139202400, "agent": "planner", "run": "1b8d5f27", "seq": 1, "phase": "end", "tool": "Read", "path": "package.json", "detail": "read the scripts of the fixture repository"} +{"ts": 1788139205000, "agent": "reviewer", "run": "d5b1e807", "seq": 1, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [17, 29], "detail": "read query"} +{"ts": 1788139207000, "agent": "tracer", "run": "e9c72f16", "seq": 1, "phase": "end", "tool": "Read", "path": "src/types.ts", "lines": [37, 53], "detail": "read UserEntity"} +{"ts": 1788139209000, "agent": "indexer", "run": "f2d0a34b", "seq": 1, "phase": "end", "tool": "Glob", "path": "src", "detail": "loadConfig"} +{"ts": 1788139211000, "agent": "packer", "run": "0a4e6c81", "seq": 1, "phase": "end", "tool": "Read", "path": "src/config.ts", "lines": [11, 18], "detail": "read loadConfig"} +{"ts": 1788139213000, "agent": "packer", "run": "0a4e6c81", "seq": 2, "phase": "end", "tool": "Read", "path": "src/server.ts", "lines": [32, 37], "detail": "read createApp"} +{"ts": 1788139216000, "agent": "explorer", "run": "c3a80f52", "seq": 2, "phase": "end", "tool": "Grep", "path": "src", "detail": "createUser"} +{"ts": 1788139218000, "agent": "implementer", "run": "a1f2c3d4", "seq": 2, "phase": "end", "tool": "Read", "path": "src/util/validate.ts", "lines": [19, 31], "detail": "read validateUser"} +{"ts": 1788139221000, "agent": "checker", "run": "b7e4d19c", "seq": 1, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"} +{"ts": 1788139224000, "agent": "reviewer", "run": "d5b1e807", "seq": 2, "phase": "end", "tool": "Edit", "path": "src/repo/db.ts", "lines": [31, 35], "detail": "insert writes the payload it was handed"} +{"ts": 1788139227000, "agent": "tracer", "run": "e9c72f16", "seq": 2, "phase": "end", "tool": "Read", "path": "src/services/orderService.ts", "lines": [8, 26], "detail": "read getOrder"} +{"ts": 1788139230000, "agent": "indexer", "run": "f2d0a34b", "seq": 2, "phase": "end", "tool": "Glob", "path": "src", "detail": "Router"} +{"ts": 1788139233000, "agent": "packer", "run": "0a4e6c81", "seq": 3, "phase": "end", "tool": "Read", "path": "src/routes/users.ts", "lines": [7, 18], "detail": "read registerUserRoutes"} +{"ts": 1788139236000, "agent": "reviewer", "run": "d5b1e807", "seq": 3, "phase": "end", "tool": "Read", "path": "src/config.ts", "lines": [20, 22], "detail": "read isProduction"} +{"ts": 1788139239000, "agent": "explorer", "run": "c3a80f52", "seq": 3, "phase": "end", "tool": "Read", "path": "src/routes/orders.ts", "lines": [6, 15], "detail": "read registerOrderRoutes"} +{"ts": 1788139242000, "agent": "implementer", "run": "a1f2c3d4", "seq": 3, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "lines": [18, 21], "detail": "listUsers takes the limit it is given"} +{"ts": 1788139245000, "agent": "tracer", "run": "e9c72f16", "seq": 3, "phase": "end", "tool": "Read", "path": "src/server.ts", "lines": [39, 46], "detail": "read main"} +{"ts": 1788139248000, "agent": "checker", "run": "b7e4d19c", "seq": 2, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"} +{"ts": 1788139251000, "agent": "reviewer", "run": "d5b1e807", "seq": 4, "phase": "end", "tool": "Read", "path": "src/util/validate.ts", "lines": [33, 38], "detail": "read validateId"} +{"ts": 1788139254000, "agent": "tracer", "run": "e9c72f16", "seq": 4, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [58, 66], "detail": "read walk"} +{"ts": 1788139257000, "agent": "indexer", "run": "f2d0a34b", "seq": 3, "phase": "end", "tool": "Glob", "path": "src", "detail": "insert"} +{"ts": 1788139260000, "agent": "packer", "run": "0a4e6c81", "seq": 4, "phase": "end", "tool": "Read", "path": "src/types.ts", "lines": [27, 30], "detail": "read User"} +{"ts": 1788139260000, "agent": "planner", "run": "1b8d5f27", "seq": 2, "phase": "end", "tool": "Read", "path": "src/server.ts", "lines": [32, 37], "detail": "read createApp"} +{"ts": 1788139266000, "agent": "implementer", "run": "a1f2c3d4", "seq": 4, "phase": "end", "tool": "Edit", "path": "src/util/validate.ts", "lines": [19, 31], "detail": "validateUser now names the field it rejected"} +{"ts": 1788139269000, "agent": "checker", "run": "b7e4d19c", "seq": 3, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [4, 8], "detail": "read Row"} +{"ts": 1788139270000, "agent": "explorer", "run": "c3a80f52", "seq": 4, "phase": "end", "tool": "Grep", "path": "src", "detail": "Order"} +{"ts": 1788139272000, "agent": "reviewer", "run": "d5b1e807", "seq": 5, "phase": "end", "tool": "Read", "path": "src/services/orderService.ts", "lines": [30, 35], "detail": "read create"} +{"ts": 1788139275000, "agent": "tracer", "run": "e9c72f16", "seq": 5, "phase": "end", "tool": "Read", "path": "src/server.ts", "lines": [16, 30], "detail": "read SimpleRouter"} +{"ts": 1788139278000, "agent": "indexer", "run": "f2d0a34b", "seq": 4, "phase": "end", "tool": "Glob", "path": "src", "detail": "query"} +{"ts": 1788139281000, "agent": "checker", "run": "b7e4d19c", "seq": 4, "phase": "end", "tool": "Read", "path": "src/config.ts", "lines": [4, 9], "detail": "read AppConfig"} +{"ts": 1788139284000, "agent": "implementer", "run": "a1f2c3d4", "seq": 5, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [17, 29], "detail": "read query"} +{"ts": 1788139286000, "agent": "implementer", "run": "a1f2c3d4", "seq": 6, "phase": "end", "tool": "Read", "path": "src/services/orderService.ts", "lines": [8, 26], "detail": "read getOrder"} +{"ts": 1788139287000, "agent": "checker", "run": "b7e4d19c", "seq": 5, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"} +{"ts": 1788139289000, "agent": "implementer", "run": "a1f2c3d4", "seq": 7, "phase": "end", "tool": "Edit", "path": "src/services/orderService.ts", "lines": [30, 35], "detail": "create now carries the customer id it was given"} +{"ts": 1788139290000, "agent": "checker", "run": "b7e4d19c", "seq": 6, "phase": "end", "tool": "Read", "path": "src/types.ts", "lines": [22, 25], "detail": "read Entity"} +{"ts": 1788139292000, "agent": "implementer", "run": "a1f2c3d4", "seq": 8, "phase": "end", "tool": "Read", "path": "src/types.ts", "lines": [37, 53], "detail": "read UserEntity"} +{"ts": 1788139293000, "agent": "packer", "run": "0a4e6c81", "seq": 5, "phase": "end", "tool": "Read", "path": "src/services/userService.ts", "lines": [40, 42], "detail": "read create"} +{"ts": 1788139295000, "agent": "implementer", "run": "a1f2c3d4", "seq": 9, "phase": "end", "tool": "Edit", "path": "src/util/validate.ts", "lines": [33, 38], "detail": "validateId rejects the empty string by name"} +{"ts": 1788139296000, "agent": "reviewer", "run": "d5b1e807", "seq": 6, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [39, 54], "detail": "read hotspotScan"} +{"ts": 1788139298000, "agent": "implementer", "run": "a1f2c3d4", "seq": 10, "phase": "end", "tool": "Read", "path": "src/server.ts", "lines": [39, 46], "detail": "read main"} +{"ts": 1788139299000, "agent": "tracer", "run": "e9c72f16", "seq": 6, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [17, 29], "detail": "read query"} +{"ts": 1788139301000, "agent": "implementer", "run": "a1f2c3d4", "seq": 11, "phase": "end", "tool": "Edit", "path": "src/routes/users.ts", "lines": [7, 18], "detail": "the user routes hand the limit through"} +{"ts": 1788139302000, "agent": "indexer", "run": "f2d0a34b", "seq": 5, "phase": "end", "tool": "Glob", "path": "src", "detail": "hotspot"} +{"ts": 1788139304000, "agent": "implementer", "run": "a1f2c3d4", "seq": 12, "phase": "end", "tool": "Read", "path": "src/routes/orders.ts", "lines": [6, 15], "detail": "read registerOrderRoutes"} +{"ts": 1788139305000, "agent": "tracer", "run": "e9c72f16", "seq": 7, "phase": "end", "tool": "Read", "path": "src/config.ts", "lines": [11, 18], "detail": "read loadConfig"} +{"ts": 1788139307000, "agent": "implementer", "run": "a1f2c3d4", "seq": 13, "phase": "end", "tool": "Edit", "path": "src/config.ts", "lines": [11, 18], "detail": "loadConfig reads the port it was given"} +{"ts": 1788139308000, "agent": "checker", "run": "b7e4d19c", "seq": 7, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"} +{"ts": 1788139310000, "agent": "implementer", "run": "a1f2c3d4", "seq": 14, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [31, 35], "detail": "read insert"} +{"ts": 1788139311000, "agent": "tracer", "run": "e9c72f16", "seq": 8, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [31, 35], "detail": "read insert"} +{"ts": 1788139313000, "agent": "implementer", "run": "a1f2c3d4", "seq": 15, "phase": "end", "tool": "Edit", "path": "src/repo/db.ts", "lines": [39, 54], "detail": "hotspotScan stops at the first needle it finds"} +{"ts": 1788139314000, "agent": "indexer", "run": "f2d0a34b", "seq": 6, "phase": "end", "tool": "Glob", "path": "src", "detail": "walk"} +{"ts": 1788139316000, "agent": "implementer", "run": "a1f2c3d4", "seq": 16, "phase": "end", "tool": "Read", "path": "src/services/userService.ts", "lines": [9, 16], "detail": "read toUser"} +{"ts": 1788139317000, "agent": "indexer", "run": "f2d0a34b", "seq": 7, "phase": "end", "tool": "Read", "path": "src/util/validate.ts", "lines": [4, 12], "detail": "read ValidationError"} +{"ts": 1788139319000, "agent": "implementer", "run": "a1f2c3d4", "seq": 17, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "lines": [18, 21], "detail": "listUsers takes the limit it is given"} +{"ts": 1788139320000, "agent": "checker", "run": "b7e4d19c", "seq": 8, "phase": "end", "tool": "Read", "path": "src/types.ts", "lines": [32, 35], "detail": "read Order"} +{"ts": 1788139322000, "agent": "implementer", "run": "a1f2c3d4", "seq": 18, "phase": "end", "tool": "Read", "path": "src/util/validate.ts", "lines": [19, 31], "detail": "read validateUser"} +{"ts": 1788139323000, "agent": "reviewer", "run": "d5b1e807", "seq": 7, "phase": "end", "tool": "Edit", "path": "src/repo/db.ts", "lines": [58, 66], "detail": "walk returns the names it visited"} +{"ts": 1788139325000, "agent": "implementer", "run": "a1f2c3d4", "seq": 19, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [58, 66], "detail": "read walk"} +{"ts": 1788139326000, "agent": "tracer", "run": "e9c72f16", "seq": 9, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [4, 8], "detail": "read Row"} +{"ts": 1788139329000, "agent": "indexer", "run": "f2d0a34b", "seq": 8, "phase": "end", "tool": "Glob", "path": "src", "detail": "Row"} +{"ts": 1788139332000, "agent": "checker", "run": "b7e4d19c", "seq": 9, "phase": "end", "tool": "Grep", "path": "src", "detail": "insert"} +{"ts": 1788139335000, "agent": "checker", "run": "b7e4d19c", "seq": 10, "phase": "end", "tool": "Bash", "path": "", "detail": "npx vitest run test/userService.test.ts"} +{"ts": 1788139338000, "agent": "packer", "run": "0a4e6c81", "seq": 6, "phase": "end", "tool": "Read", "path": "src/config.ts", "lines": [20, 22], "detail": "read isProduction"} +{"ts": 1788139340000, "agent": "reviewer", "run": "d5b1e807", "seq": 8, "phase": "end", "tool": "Edit", "path": "src/repo/db.ts", "lines": [39, 54], "detail": "hotspotScan keeps the first hit"} +{"ts": 1788139340600, "agent": "indexer", "run": "f2d0a34b", "seq": 9, "phase": "end", "tool": "Glob", "path": "src/repo/db.ts", "lines": [39, 54], "detail": "hotspotScan"} +{"ts": 1788139341200, "agent": "tracer", "run": "e9c72f16", "seq": 10, "phase": "end", "tool": "Read", "path": "src/repo/db.ts", "lines": [39, 54], "detail": "read hotspotScan"} +{"ts": 1788139343200, "agent": "implementer", "run": "a1f2c3d4", "seq": 20, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "lines": [23, 36], "detail": "tightened the input check of createUser", "intent": "tightening the input check in createUser"} +{"ts": 1788139343650, "agent": "implementer", "run": "a1f2c3d4", "seq": 21, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "lines": [23, 36], "detail": "tightened the input check of createUser"} +{"ts": 1788139344100, "agent": "implementer", "run": "a1f2c3d4", "seq": 22, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "lines": [23, 36], "detail": "tightened the input check of createUser"} +{"ts": 1788139344550, "agent": "implementer", "run": "a1f2c3d4", "seq": 23, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "lines": [23, 36], "detail": "tightened the input check of createUser"} +{"ts": 1788139345000, "agent": "implementer", "run": "a1f2c3d4", "seq": 24, "phase": "end", "tool": "Edit", "path": "src/services/userService.ts", "lines": [23, 36], "detail": "tightened the input check of createUser"} diff --git a/graph-ui/fixtures/atlas-sample-large/HERKUNFT.md b/graph-ui/fixtures/atlas-sample-large/HERKUNFT.md new file mode 100644 index 000000000..a696c289f --- /dev/null +++ b/graph-ui/fixtures/atlas-sample-large/HERKUNFT.md @@ -0,0 +1,10 @@ +# Large-source fixture provenance + +Generated for this frontend on 2026-08-28; no external code was copied. +`src/big.ts` deterministically contains 100 small exported functions and `runAll`. +`src/support.ts` and `src/index.ts` were written for the fixture. + +The 717-line source exceeds the 500-line `get_code_snippet` limit and exercises +`source_clipped`. The reader must disclose missing source rather than presenting +an incomplete file as complete. This corpus is separate from `atlas-sample` to +preserve that fixture's graph counts. It is indexing input, not a runnable package. diff --git a/graph-ui/fixtures/atlas-sample-large/package.json b/graph-ui/fixtures/atlas-sample-large/package.json new file mode 100644 index 000000000..1d4f263b6 --- /dev/null +++ b/graph-ui/fixtures/atlas-sample-large/package.json @@ -0,0 +1,8 @@ +{ + "name": "atlas-sample-large", + "version": "1.0.0", + "private": true, + "description": "Cap-Mess-Fixture fuer CodeAtlas. Never built, installed or executed by the monorepo.", + "main": "src/index.ts", + "license": "UNLICENSED" +} diff --git a/graph-ui/fixtures/atlas-sample-large/src/big.ts b/graph-ui/fixtures/atlas-sample-large/src/big.ts new file mode 100644 index 000000000..25be226dd --- /dev/null +++ b/graph-ui/fixtures/atlas-sample-large/src/big.ts @@ -0,0 +1,717 @@ +// Generierte Datei fuer die Cap-Messung des Readers (2026-08-28). +// Deterministisch erzeugt: 100 kleine exportierte Funktionen, damit der +// Indexer sie sauber frisst und die Datei sicher ueber 550 Zeilen liegt. +// Der 500-Zeilen-Cap von get_code_snippet schneidet sie also mitten durch, +// und genau das soll der Reader ehrlich benennen statt zu verschweigen. +// +// Von Hand aendern hat keinen Sinn: die Datei ist Messmaterial, kein Code, +// der gelesen werden will. + +import { normalize, weigh } from './support'; + +/** Schritt 1 des generierten Korpus. */ +export function step001(input: number): number { + const scaled = input * 1 + 7; + return normalize(scaled); +} + +/** Schritt 2 des generierten Korpus. */ +export function step002(input: number): number { + const scaled = input * 2 + 1; + return weigh(scaled); +} + +/** Schritt 3 des generierten Korpus. */ +export function step003(input: number): number { + const scaled = input * 3 + 8; + return normalize(scaled); +} + +/** Schritt 4 des generierten Korpus. */ +export function step004(input: number): number { + const scaled = input * 4 + 2; + return weigh(scaled); +} + +/** Schritt 5 des generierten Korpus. */ +export function step005(input: number): number { + const scaled = input * 5 + 9; + return normalize(scaled); +} + +/** Schritt 6 des generierten Korpus. */ +export function step006(input: number): number { + const scaled = input * 6 + 3; + return weigh(scaled); +} + +/** Schritt 7 des generierten Korpus. */ +export function step007(input: number): number { + const scaled = input * 7 + 10; + return normalize(scaled); +} + +/** Schritt 8 des generierten Korpus. */ +export function step008(input: number): number { + const scaled = input * 8 + 4; + return weigh(scaled); +} + +/** Schritt 9 des generierten Korpus. */ +export function step009(input: number): number { + const scaled = input * 9 + 11; + return normalize(scaled); +} + +/** Schritt 10 des generierten Korpus. */ +export function step010(input: number): number { + const scaled = input * 10 + 5; + return weigh(scaled); +} + +/** Schritt 11 des generierten Korpus. */ +export function step011(input: number): number { + const scaled = input * 11 + 12; + return normalize(scaled); +} + +/** Schritt 12 des generierten Korpus. */ +export function step012(input: number): number { + const scaled = input * 12 + 6; + return weigh(scaled); +} + +/** Schritt 13 des generierten Korpus. */ +export function step013(input: number): number { + const scaled = input * 13 + 0; + return normalize(scaled); +} + +/** Schritt 14 des generierten Korpus. */ +export function step014(input: number): number { + const scaled = input * 14 + 7; + return weigh(scaled); +} + +/** Schritt 15 des generierten Korpus. */ +export function step015(input: number): number { + const scaled = input * 15 + 1; + return normalize(scaled); +} + +/** Schritt 16 des generierten Korpus. */ +export function step016(input: number): number { + const scaled = input * 16 + 8; + return weigh(scaled); +} + +/** Schritt 17 des generierten Korpus. */ +export function step017(input: number): number { + const scaled = input * 17 + 2; + return normalize(scaled); +} + +/** Schritt 18 des generierten Korpus. */ +export function step018(input: number): number { + const scaled = input * 18 + 9; + return weigh(scaled); +} + +/** Schritt 19 des generierten Korpus. */ +export function step019(input: number): number { + const scaled = input * 19 + 3; + return normalize(scaled); +} + +/** Schritt 20 des generierten Korpus. */ +export function step020(input: number): number { + const scaled = input * 20 + 10; + return weigh(scaled); +} + +/** Schritt 21 des generierten Korpus. */ +export function step021(input: number): number { + const scaled = input * 21 + 4; + return normalize(scaled); +} + +/** Schritt 22 des generierten Korpus. */ +export function step022(input: number): number { + const scaled = input * 22 + 11; + return weigh(scaled); +} + +/** Schritt 23 des generierten Korpus. */ +export function step023(input: number): number { + const scaled = input * 23 + 5; + return normalize(scaled); +} + +/** Schritt 24 des generierten Korpus. */ +export function step024(input: number): number { + const scaled = input * 24 + 12; + return weigh(scaled); +} + +/** Schritt 25 des generierten Korpus. */ +export function step025(input: number): number { + const scaled = input * 25 + 6; + return normalize(scaled); +} + +/** Schritt 26 des generierten Korpus. */ +export function step026(input: number): number { + const scaled = input * 26 + 0; + return weigh(scaled); +} + +/** Schritt 27 des generierten Korpus. */ +export function step027(input: number): number { + const scaled = input * 27 + 7; + return normalize(scaled); +} + +/** Schritt 28 des generierten Korpus. */ +export function step028(input: number): number { + const scaled = input * 28 + 1; + return weigh(scaled); +} + +/** Schritt 29 des generierten Korpus. */ +export function step029(input: number): number { + const scaled = input * 29 + 8; + return normalize(scaled); +} + +/** Schritt 30 des generierten Korpus. */ +export function step030(input: number): number { + const scaled = input * 30 + 2; + return weigh(scaled); +} + +/** Schritt 31 des generierten Korpus. */ +export function step031(input: number): number { + const scaled = input * 31 + 9; + return normalize(scaled); +} + +/** Schritt 32 des generierten Korpus. */ +export function step032(input: number): number { + const scaled = input * 32 + 3; + return weigh(scaled); +} + +/** Schritt 33 des generierten Korpus. */ +export function step033(input: number): number { + const scaled = input * 33 + 10; + return normalize(scaled); +} + +/** Schritt 34 des generierten Korpus. */ +export function step034(input: number): number { + const scaled = input * 34 + 4; + return weigh(scaled); +} + +/** Schritt 35 des generierten Korpus. */ +export function step035(input: number): number { + const scaled = input * 35 + 11; + return normalize(scaled); +} + +/** Schritt 36 des generierten Korpus. */ +export function step036(input: number): number { + const scaled = input * 36 + 5; + return weigh(scaled); +} + +/** Schritt 37 des generierten Korpus. */ +export function step037(input: number): number { + const scaled = input * 37 + 12; + return normalize(scaled); +} + +/** Schritt 38 des generierten Korpus. */ +export function step038(input: number): number { + const scaled = input * 38 + 6; + return weigh(scaled); +} + +/** Schritt 39 des generierten Korpus. */ +export function step039(input: number): number { + const scaled = input * 39 + 0; + return normalize(scaled); +} + +/** Schritt 40 des generierten Korpus. */ +export function step040(input: number): number { + const scaled = input * 40 + 7; + return weigh(scaled); +} + +/** Schritt 41 des generierten Korpus. */ +export function step041(input: number): number { + const scaled = input * 41 + 1; + return normalize(scaled); +} + +/** Schritt 42 des generierten Korpus. */ +export function step042(input: number): number { + const scaled = input * 42 + 8; + return weigh(scaled); +} + +/** Schritt 43 des generierten Korpus. */ +export function step043(input: number): number { + const scaled = input * 43 + 2; + return normalize(scaled); +} + +/** Schritt 44 des generierten Korpus. */ +export function step044(input: number): number { + const scaled = input * 44 + 9; + return weigh(scaled); +} + +/** Schritt 45 des generierten Korpus. */ +export function step045(input: number): number { + const scaled = input * 45 + 3; + return normalize(scaled); +} + +/** Schritt 46 des generierten Korpus. */ +export function step046(input: number): number { + const scaled = input * 46 + 10; + return weigh(scaled); +} + +/** Schritt 47 des generierten Korpus. */ +export function step047(input: number): number { + const scaled = input * 47 + 4; + return normalize(scaled); +} + +/** Schritt 48 des generierten Korpus. */ +export function step048(input: number): number { + const scaled = input * 48 + 11; + return weigh(scaled); +} + +/** Schritt 49 des generierten Korpus. */ +export function step049(input: number): number { + const scaled = input * 49 + 5; + return normalize(scaled); +} + +/** Schritt 50 des generierten Korpus. */ +export function step050(input: number): number { + const scaled = input * 50 + 12; + return weigh(scaled); +} + +/** Schritt 51 des generierten Korpus. */ +export function step051(input: number): number { + const scaled = input * 51 + 6; + return normalize(scaled); +} + +/** Schritt 52 des generierten Korpus. */ +export function step052(input: number): number { + const scaled = input * 52 + 0; + return weigh(scaled); +} + +/** Schritt 53 des generierten Korpus. */ +export function step053(input: number): number { + const scaled = input * 53 + 7; + return normalize(scaled); +} + +/** Schritt 54 des generierten Korpus. */ +export function step054(input: number): number { + const scaled = input * 54 + 1; + return weigh(scaled); +} + +/** Schritt 55 des generierten Korpus. */ +export function step055(input: number): number { + const scaled = input * 55 + 8; + return normalize(scaled); +} + +/** Schritt 56 des generierten Korpus. */ +export function step056(input: number): number { + const scaled = input * 56 + 2; + return weigh(scaled); +} + +/** Schritt 57 des generierten Korpus. */ +export function step057(input: number): number { + const scaled = input * 57 + 9; + return normalize(scaled); +} + +/** Schritt 58 des generierten Korpus. */ +export function step058(input: number): number { + const scaled = input * 58 + 3; + return weigh(scaled); +} + +/** Schritt 59 des generierten Korpus. */ +export function step059(input: number): number { + const scaled = input * 59 + 10; + return normalize(scaled); +} + +/** Schritt 60 des generierten Korpus. */ +export function step060(input: number): number { + const scaled = input * 60 + 4; + return weigh(scaled); +} + +/** Schritt 61 des generierten Korpus. */ +export function step061(input: number): number { + const scaled = input * 61 + 11; + return normalize(scaled); +} + +/** Schritt 62 des generierten Korpus. */ +export function step062(input: number): number { + const scaled = input * 62 + 5; + return weigh(scaled); +} + +/** Schritt 63 des generierten Korpus. */ +export function step063(input: number): number { + const scaled = input * 63 + 12; + return normalize(scaled); +} + +/** Schritt 64 des generierten Korpus. */ +export function step064(input: number): number { + const scaled = input * 64 + 6; + return weigh(scaled); +} + +/** Schritt 65 des generierten Korpus. */ +export function step065(input: number): number { + const scaled = input * 65 + 0; + return normalize(scaled); +} + +/** Schritt 66 des generierten Korpus. */ +export function step066(input: number): number { + const scaled = input * 66 + 7; + return weigh(scaled); +} + +/** Schritt 67 des generierten Korpus. */ +export function step067(input: number): number { + const scaled = input * 67 + 1; + return normalize(scaled); +} + +/** Schritt 68 des generierten Korpus. */ +export function step068(input: number): number { + const scaled = input * 68 + 8; + return weigh(scaled); +} + +/** Schritt 69 des generierten Korpus. */ +export function step069(input: number): number { + const scaled = input * 69 + 2; + return normalize(scaled); +} + +/** Schritt 70 des generierten Korpus. */ +export function step070(input: number): number { + const scaled = input * 70 + 9; + return weigh(scaled); +} + +/** Schritt 71 des generierten Korpus. */ +export function step071(input: number): number { + const scaled = input * 71 + 3; + return normalize(scaled); +} + +/** Schritt 72 des generierten Korpus. */ +export function step072(input: number): number { + const scaled = input * 72 + 10; + return weigh(scaled); +} + +/** Schritt 73 des generierten Korpus. */ +export function step073(input: number): number { + const scaled = input * 73 + 4; + return normalize(scaled); +} + +/** Schritt 74 des generierten Korpus. */ +export function step074(input: number): number { + const scaled = input * 74 + 11; + return weigh(scaled); +} + +/** Schritt 75 des generierten Korpus. */ +export function step075(input: number): number { + const scaled = input * 75 + 5; + return normalize(scaled); +} + +/** Schritt 76 des generierten Korpus. */ +export function step076(input: number): number { + const scaled = input * 76 + 12; + return weigh(scaled); +} + +/** Schritt 77 des generierten Korpus. */ +export function step077(input: number): number { + const scaled = input * 77 + 6; + return normalize(scaled); +} + +/** Schritt 78 des generierten Korpus. */ +export function step078(input: number): number { + const scaled = input * 78 + 0; + return weigh(scaled); +} + +/** Schritt 79 des generierten Korpus. */ +export function step079(input: number): number { + const scaled = input * 79 + 7; + return normalize(scaled); +} + +/** Schritt 80 des generierten Korpus. */ +export function step080(input: number): number { + const scaled = input * 80 + 1; + return weigh(scaled); +} + +/** Schritt 81 des generierten Korpus. */ +export function step081(input: number): number { + const scaled = input * 81 + 8; + return normalize(scaled); +} + +/** Schritt 82 des generierten Korpus. */ +export function step082(input: number): number { + const scaled = input * 82 + 2; + return weigh(scaled); +} + +/** Schritt 83 des generierten Korpus. */ +export function step083(input: number): number { + const scaled = input * 83 + 9; + return normalize(scaled); +} + +/** Schritt 84 des generierten Korpus. */ +export function step084(input: number): number { + const scaled = input * 84 + 3; + return weigh(scaled); +} + +/** Schritt 85 des generierten Korpus. */ +export function step085(input: number): number { + const scaled = input * 85 + 10; + return normalize(scaled); +} + +/** Schritt 86 des generierten Korpus. */ +export function step086(input: number): number { + const scaled = input * 86 + 4; + return weigh(scaled); +} + +/** Schritt 87 des generierten Korpus. */ +export function step087(input: number): number { + const scaled = input * 87 + 11; + return normalize(scaled); +} + +/** Schritt 88 des generierten Korpus. */ +export function step088(input: number): number { + const scaled = input * 88 + 5; + return weigh(scaled); +} + +/** Schritt 89 des generierten Korpus. */ +export function step089(input: number): number { + const scaled = input * 89 + 12; + return normalize(scaled); +} + +/** Schritt 90 des generierten Korpus. */ +export function step090(input: number): number { + const scaled = input * 90 + 6; + return weigh(scaled); +} + +/** Schritt 91 des generierten Korpus. */ +export function step091(input: number): number { + const scaled = input * 91 + 0; + return normalize(scaled); +} + +/** Schritt 92 des generierten Korpus. */ +export function step092(input: number): number { + const scaled = input * 92 + 7; + return weigh(scaled); +} + +/** Schritt 93 des generierten Korpus. */ +export function step093(input: number): number { + const scaled = input * 93 + 1; + return normalize(scaled); +} + +/** Schritt 94 des generierten Korpus. */ +export function step094(input: number): number { + const scaled = input * 94 + 8; + return weigh(scaled); +} + +/** Schritt 95 des generierten Korpus. */ +export function step095(input: number): number { + const scaled = input * 95 + 2; + return normalize(scaled); +} + +/** Schritt 96 des generierten Korpus. */ +export function step096(input: number): number { + const scaled = input * 96 + 9; + return weigh(scaled); +} + +/** Schritt 97 des generierten Korpus. */ +export function step097(input: number): number { + const scaled = input * 97 + 3; + return normalize(scaled); +} + +/** Schritt 98 des generierten Korpus. */ +export function step098(input: number): number { + const scaled = input * 98 + 10; + return weigh(scaled); +} + +/** Schritt 99 des generierten Korpus. */ +export function step099(input: number): number { + const scaled = input * 99 + 4; + return normalize(scaled); +} + +/** Schritt 100 des generierten Korpus. */ +export function step100(input: number): number { + const scaled = input * 100 + 11; + return weigh(scaled); +} + +/** Alle Schritte der Reihe nach, damit die Datei einen Einstiegspunkt hat. */ +export function runAll(seed: number): number { + const steps = [ + step001, + step002, + step003, + step004, + step005, + step006, + step007, + step008, + step009, + step010, + step011, + step012, + step013, + step014, + step015, + step016, + step017, + step018, + step019, + step020, + step021, + step022, + step023, + step024, + step025, + step026, + step027, + step028, + step029, + step030, + step031, + step032, + step033, + step034, + step035, + step036, + step037, + step038, + step039, + step040, + step041, + step042, + step043, + step044, + step045, + step046, + step047, + step048, + step049, + step050, + step051, + step052, + step053, + step054, + step055, + step056, + step057, + step058, + step059, + step060, + step061, + step062, + step063, + step064, + step065, + step066, + step067, + step068, + step069, + step070, + step071, + step072, + step073, + step074, + step075, + step076, + step077, + step078, + step079, + step080, + step081, + step082, + step083, + step084, + step085, + step086, + step087, + step088, + step089, + step090, + step091, + step092, + step093, + step094, + step095, + step096, + step097, + step098, + step099, + step100, + ]; + return steps.reduce((acc, step) => step(acc), seed); +} diff --git a/graph-ui/fixtures/atlas-sample-large/src/index.ts b/graph-ui/fixtures/atlas-sample-large/src/index.ts new file mode 100644 index 000000000..ffa80bb68 --- /dev/null +++ b/graph-ui/fixtures/atlas-sample-large/src/index.ts @@ -0,0 +1,11 @@ +// Einstiegspunkt des Mess-Korpus. Er tut absichtlich fast nichts: gemessen +// wird der Reader an big.ts, nicht dieses Projekt. + +import { runAll, step001 } from './big'; +import { normalize } from './support'; + +/** Faehrt den generierten Korpus einmal durch und gibt das Ergebnis zurueck. */ +export function main(seed = 1): number { + const first = step001(seed); + return normalize(runAll(first)); +} diff --git a/graph-ui/fixtures/atlas-sample-large/src/support.ts b/graph-ui/fixtures/atlas-sample-large/src/support.ts new file mode 100644 index 000000000..f7f3f4613 --- /dev/null +++ b/graph-ui/fixtures/atlas-sample-large/src/support.ts @@ -0,0 +1,16 @@ +// Zwei Hilfsfunktionen, die big.ts aufruft. Sie sind hier, damit die grosse +// Datei nicht beziehungslos im Graphen haengt: der Indexer sieht Importe und +// Aufrufkanten und der Reader hat neben big.ts noch etwas Kleines zu zeigen. + +/** Haelt einen Wert in einem festen Fenster, damit runAll nicht davonlaeuft. */ +export function normalize(value: number): number { + if (!Number.isFinite(value)) { + return 0; + } + return ((value % 997) + 997) % 997; +} + +/** Gewichtet einen Wert und normalisiert ihn danach. */ +export function weigh(value: number): number { + return normalize(Math.trunc(value / 3) + 11); +} diff --git a/graph-ui/fixtures/atlas-sample-large/tsconfig.json b/graph-ui/fixtures/atlas-sample-large/tsconfig.json new file mode 100644 index 000000000..2d2d5f15b --- /dev/null +++ b/graph-ui/fixtures/atlas-sample-large/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "rootDir": ".", + "noEmit": true + }, + "include": ["src/**/*.ts"] +} diff --git a/graph-ui/fixtures/atlas-sample/HERKUNFT.md b/graph-ui/fixtures/atlas-sample/HERKUNFT.md new file mode 100644 index 000000000..c79f397c8 --- /dev/null +++ b/graph-ui/fixtures/atlas-sample/HERKUNFT.md @@ -0,0 +1,8 @@ +# Sample fixture provenance + +Copied from Bernhard Jackiewicz's CodeAtlasIDE project on 2026-08-28, +`fixtures/atlas-sample`, by the same author. + +The twelve TypeScript and configuration files form a deliberately small, stable +demo and test corpus. Graph assertions depend on that stable source. Changes here +apply only to this fixture; they do not update its source project. diff --git a/graph-ui/fixtures/atlas-sample/package.json b/graph-ui/fixtures/atlas-sample/package.json new file mode 100644 index 000000000..1ff41f48c --- /dev/null +++ b/graph-ui/fixtures/atlas-sample/package.json @@ -0,0 +1,8 @@ +{ + "name": "atlas-sample", + "version": "1.0.0", + "private": true, + "description": "Indexing fixture for CodeAtlas. Never built, installed or executed by the monorepo.", + "main": "src/server.ts", + "license": "UNLICENSED" +} diff --git a/graph-ui/fixtures/atlas-sample/src/config.ts b/graph-ui/fixtures/atlas-sample/src/config.ts new file mode 100644 index 000000000..595e02545 --- /dev/null +++ b/graph-ui/fixtures/atlas-sample/src/config.ts @@ -0,0 +1,22 @@ +// Configuration loader. Every value comes from the environment, which gives +// the fixture a stable set of environment read sites for the graph. + +export interface AppConfig { + host: string; + port: number; + databaseUrl: string; + logLevel: string; +} + +export function loadConfig(): AppConfig { + return { + host: process.env.HOST ?? '127.0.0.1', + port: Number(process.env.PORT ?? '8080'), + databaseUrl: process.env.DB_URL ?? 'memory://atlas-sample', + logLevel: process.env.LOG_LEVEL ?? 'info', + }; +} + +export function isProduction(): boolean { + return process.env.NODE_ENV === 'production'; +} diff --git a/graph-ui/fixtures/atlas-sample/src/repo/db.ts b/graph-ui/fixtures/atlas-sample/src/repo/db.ts new file mode 100644 index 000000000..b2ade6588 --- /dev/null +++ b/graph-ui/fixtures/atlas-sample/src/repo/db.ts @@ -0,0 +1,66 @@ +// In-memory storage stand-in. No driver and no network: the fixture only needs +// call sites and control flow that an indexer can see. + +export interface Row { + id: string; + table: string; + payload: Record; +} + +export interface TreeNode { + name: string; + children: TreeNode[]; +} + +const rows: Row[] = []; + +export function query(table: string, limit: number): Row[] { + const matches: Row[] = []; + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if (row.table === table) { + matches.push(row); + } + if (matches.length >= limit) { + break; + } + } + return matches; +} + +export function insert(table: string, id: string, payload: Record): Row { + const row: Row = { id, table, payload }; + rows.push(row); + return row; +} + +// Deliberate hotspot: three nested loops with a linear lookup in the innermost +// body. The complexity view is expected to flag this function. +export function hotspotScan(tables: string[], keys: string[], needles: string[]): Row[] { + const found: Row[] = []; + for (const table of tables) { + for (const key of keys) { + for (const needle of needles) { + const candidates = query(table, 1000); + const position = candidates.map((row) => row.id).indexOf(needle); + const hit = candidates.find((row) => String(row.payload[key]) === needle); + if (position >= 0 && hit) { + found.push(hit); + } + } + } + } + return found; +} + +// Deliberately unguarded recursion: walk has no base case of its own and +// relies on the child list running out. +export function walk(node: TreeNode): string[] { + const names: string[] = [node.name]; + for (const child of node.children) { + for (const name of walk(child)) { + names.push(name); + } + } + return names; +} diff --git a/graph-ui/fixtures/atlas-sample/src/routes/orders.ts b/graph-ui/fixtures/atlas-sample/src/routes/orders.ts new file mode 100644 index 000000000..f220082f3 --- /dev/null +++ b/graph-ui/fixtures/atlas-sample/src/routes/orders.ts @@ -0,0 +1,17 @@ +// Order routes. A single parameterised lookup that delegates to the service. + +import { getOrder } from '../services/orderService'; +// Deliberately unused harness fixture: proves the file-level import finding. +import { insert } from '../repo/db'; +import type { HttpRequest, HttpResponse, Router } from '../types'; + +export function registerOrderRoutes(router: Router): void { + router.get('/orders/:id', (req: HttpRequest, res: HttpResponse) => { + const order = getOrder(req.params.id); + if (!order) { + res.status(404).json({ error: 'order not found' }); + return; + } + res.json({ order }); + }); +} diff --git a/graph-ui/fixtures/atlas-sample/src/routes/users.ts b/graph-ui/fixtures/atlas-sample/src/routes/users.ts new file mode 100644 index 000000000..32f010d6a --- /dev/null +++ b/graph-ui/fixtures/atlas-sample/src/routes/users.ts @@ -0,0 +1,18 @@ +// User routes. Handlers stay thin so the call edge from the route to the +// service is the only thing the graph has to resolve. + +import { createUser, listUsers } from '../services/userService'; +import type { HttpRequest, HttpResponse, Router } from '../types'; + +export function registerUserRoutes(router: Router): void { + router.get('/users', (req: HttpRequest, res: HttpResponse) => { + const limit = Number(req.query.limit ?? '50'); + const users = listUsers(limit); + res.json({ users, count: users.length }); + }); + + router.post('/users', (req: HttpRequest, res: HttpResponse) => { + const user = createUser(req.body); + res.status(201).json({ user }); + }); +} diff --git a/graph-ui/fixtures/atlas-sample/src/server.ts b/graph-ui/fixtures/atlas-sample/src/server.ts new file mode 100644 index 000000000..cf5236cd6 --- /dev/null +++ b/graph-ui/fixtures/atlas-sample/src/server.ts @@ -0,0 +1,48 @@ +// Entry point of the atlas-sample fixture. Builds the router, registers the +// route modules and prints the routing table. Nothing here opens a socket: +// the fixture is indexed, never executed. + +import { loadConfig } from './config'; +import { registerOrderRoutes } from './routes/orders'; +import { registerUserRoutes } from './routes/users'; +import type { RouteHandler, Router } from './types'; + +interface Route { + method: string; + path: string; + handler: RouteHandler; +} + +export class SimpleRouter implements Router { + private readonly routes: Route[] = []; + + get(path: string, handler: RouteHandler): void { + this.routes.push({ method: 'GET', path, handler }); + } + + post(path: string, handler: RouteHandler): void { + this.routes.push({ method: 'POST', path, handler }); + } + + table(): string[] { + return this.routes.map((route) => `${route.method} ${route.path}`); + } +} + +export function createApp(): SimpleRouter { + const router = new SimpleRouter(); + registerUserRoutes(router); + registerOrderRoutes(router); + return router; +} + +export function main(): void { + const config = loadConfig(); + const app = createApp(); + for (const entry of app.table()) { + console.log(`[atlas-sample] route ${entry}`); + } + console.log(`[atlas-sample] would listen on ${config.host}:${config.port}`); +} + +main(); diff --git a/graph-ui/fixtures/atlas-sample/src/services/orderService.ts b/graph-ui/fixtures/atlas-sample/src/services/orderService.ts new file mode 100644 index 000000000..daa7f4bdd --- /dev/null +++ b/graph-ui/fixtures/atlas-sample/src/services/orderService.ts @@ -0,0 +1,35 @@ +// Order use cases. getOrder wraps the repository call in try/catch so the +// fixture has a recoverable failure path to render. + +import { insert, query } from '../repo/db'; +import type { Order } from '../types'; +import { validateId } from '../util/validate'; + +export function getOrder(id: string): Order | undefined { + try { + const key = validateId(id); + const rows = query('orders', 100); + const row = rows.find((candidate) => candidate.id === key); + if (!row) { + return undefined; + } + return { + id: row.id, + createdAt: String(row.payload.createdAt ?? ''), + customerId: String(row.payload.customerId ?? ''), + total: Number(row.payload.total ?? 0), + }; + } catch (error) { + console.error(`[orders] lookup failed for ${id}: ${String(error)}`); + return undefined; + } +} + +// Same bare name as userService.create on purpose: the pair is the ambiguity +// fixture for path tracing. +export function create(customerId: string, total: number): Order { + const createdAt = new Date(0).toISOString(); + const id = `order-${customerId}-${total}`; + insert('orders', id, { customerId, total, createdAt }); + return { id, createdAt, customerId, total }; +} diff --git a/graph-ui/fixtures/atlas-sample/src/services/userService.ts b/graph-ui/fixtures/atlas-sample/src/services/userService.ts new file mode 100644 index 000000000..d71b8b85d --- /dev/null +++ b/graph-ui/fixtures/atlas-sample/src/services/userService.ts @@ -0,0 +1,42 @@ +// User use cases. Kept small on purpose so the facts the graph reports for +// createUser stay stable: it calls, throws and reads exactly one thing each. + +import { insert, query, Row } from '../repo/db'; +import { UserEntity } from '../types'; +import type { User } from '../types'; +import { ValidationError, validateUser } from '../util/validate'; + +function toUser(row: Row): User { + return { + id: row.id, + createdAt: String(row.payload.createdAt ?? ''), + email: String(row.payload.email ?? ''), + name: String(row.payload.name ?? ''), + }; +} + +export function listUsers(limit = 50): User[] { + const rows = query('users', limit); + return rows.map(toUser); +} + +export function createUser(input: unknown): User { + const parsed = validateUser(input); + const dsn = process.env.DB_URL ?? ''; + if (dsn.length === 0) { + throw new ValidationError('DB_URL', 'DB_URL must be set before writing users'); + } + const entity = new UserEntity(`user-${listUsers().length + 1}`, parsed.email, parsed.name); + const row = insert('users', entity.id, { + email: entity.email, + name: entity.name, + createdAt: entity.createdAt, + }); + return toUser(row); +} + +// Thin alias kept on purpose: orderService exports a function with the same +// bare name, which makes "create" an ambiguous trace target. +export function create(input: unknown): User { + return createUser(input); +} diff --git a/graph-ui/fixtures/atlas-sample/src/types.ts b/graph-ui/fixtures/atlas-sample/src/types.ts new file mode 100644 index 000000000..c60333353 --- /dev/null +++ b/graph-ui/fixtures/atlas-sample/src/types.ts @@ -0,0 +1,53 @@ +// Shared types for the atlas-sample fixture: a small express-like HTTP surface +// plus the domain entities the services work with. No runtime dependencies. + +export interface HttpRequest { + params: Record; + query: Record; + body: unknown; +} + +export interface HttpResponse { + status(code: number): HttpResponse; + json(payload: unknown): void; +} + +export type RouteHandler = (req: HttpRequest, res: HttpResponse) => void; + +export interface Router { + get(path: string, handler: RouteHandler): void; + post(path: string, handler: RouteHandler): void; +} + +export interface Entity { + id: string; + createdAt: string; +} + +export interface User extends Entity { + email: string; + name: string; +} + +export interface Order extends Entity { + customerId: string; + total: number; +} + +export class UserEntity implements Entity { + readonly id: string; + readonly createdAt: string; + readonly email: string; + readonly name: string; + + constructor(id: string, email: string, name: string) { + this.id = id; + this.email = email; + this.name = name; + this.createdAt = new Date(0).toISOString(); + } + + label(): string { + return `${this.name} <${this.email}>`; + } +} diff --git a/graph-ui/fixtures/atlas-sample/src/util/validate.ts b/graph-ui/fixtures/atlas-sample/src/util/validate.ts new file mode 100644 index 000000000..ba130d0d2 --- /dev/null +++ b/graph-ui/fixtures/atlas-sample/src/util/validate.ts @@ -0,0 +1,38 @@ +// Input validation. ValidationError lives here so both services throw the +// same class and the graph has a single error type to point at. + +export class ValidationError extends Error { + readonly field: string; + + constructor(field: string, message: string) { + super(message); + this.name = 'ValidationError'; + this.field = field; + } +} + +export interface UserInput { + email: string; + name: string; +} + +export function validateUser(input: unknown): UserInput { + const candidate = input as Partial | null; + if (!candidate || typeof candidate.email !== 'string') { + throw new ValidationError('email', 'email must be a string'); + } + if (!candidate.email.includes('@')) { + throw new ValidationError('email', 'email must contain an at sign'); + } + if (typeof candidate.name !== 'string' || candidate.name.length === 0) { + throw new ValidationError('name', 'name must be a non-empty string'); + } + return { email: candidate.email, name: candidate.name }; +} + +export function validateId(value: string): string { + if (value.trim().length === 0) { + throw new ValidationError('id', 'id must not be empty'); + } + return value; +} diff --git a/graph-ui/fixtures/atlas-sample/test/userService.test.ts b/graph-ui/fixtures/atlas-sample/test/userService.test.ts new file mode 100644 index 000000000..a9904d037 --- /dev/null +++ b/graph-ui/fixtures/atlas-sample/test/userService.test.ts @@ -0,0 +1,16 @@ +// Minimal fixture test. It exercises listUsers and nothing else, so the graph +// is expected to show a test edge for that function only. + +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { listUsers } from '../src/services/userService'; + +test('listUsers returns an array', () => { + const users = listUsers(); + assert.ok(Array.isArray(users)); +}); + +test('listUsers honours the limit', () => { + const users = listUsers(1); + assert.ok(users.length <= 1); +}); diff --git a/graph-ui/fixtures/atlas-sample/tsconfig.json b/graph-ui/fixtures/atlas-sample/tsconfig.json new file mode 100644 index 000000000..6faf3fc9e --- /dev/null +++ b/graph-ui/fixtures/atlas-sample/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "CommonJS", + "moduleResolution": "node", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "rootDir": ".", + "noEmit": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +} diff --git a/graph-ui/index.html b/graph-ui/index.html index e20a6503b..7e195a941 100644 --- a/graph-ui/index.html +++ b/graph-ui/index.html @@ -1,11 +1,13 @@ - + - Codebase Memory — Graph + + + CBM Lens Beta - +
diff --git a/graph-ui/llm/fetch-model.sh b/graph-ui/llm/fetch-model.sh new file mode 100644 index 000000000..594d72776 --- /dev/null +++ b/graph-ui/llm/fetch-model.sh @@ -0,0 +1,134 @@ +#!/bin/sh +# Ein Modell von Hugging Face in das Cache-Verzeichnis dieses Projekts holen. +# +# Aufruf: +# llm/fetch-model.sh /[:] holt das Modell +# llm/fetch-model.sh --print / zeigt nur den Befehl +# llm/fetch-model.sh --list zeigt, was im Cache liegt +# +# ## Warum dieses Skript und nicht ein Knopf in der Oberflaeche +# +# Die Oberflaeche ist eine SPA ohne eigenes Backend. Sie startet keinen Prozess +# und schreibt nichts auf die Platte, also kann sie auch nichts herunterladen. +# Was sie kann, tut sie: sie zeigt die Vorschlaege mit ihren gemessenen Zahlen +# und den fertigen Aufruf zum Kopieren. Der Aufruf ist dieser hier. +# +# ## Was hier wirklich passiert +# +# llama-server kann selbst laden: `-hf user/repo[:quant]` holt die Datei von +# huggingface.co in das Verzeichnis, das llama.cpp als Cache liest +# (Umgebungsvariable LLAMA_CACHE, ohne sie auf macOS $HOME/Library/Caches/ +# llama.cpp). Dieses Skript setzt LLAMA_CACHE auf das Cache-Verzeichnis dieses +# Projekts, damit ein geholtes Modell in derselben Liste auftaucht wie die schon +# vorhandenen und llm/start.sh es ohne weitere Angabe findet. +# +# Der Quant ist optional; ohne ihn nimmt llama-server Q4_K_M, und wenn es den +# nicht gibt, die erste Datei des Repositories. Er wird ohne Ansehen der +# Gross- und Kleinschreibung gesucht. +# +# ## Der eine Satz, der vor jedem Lauf dasteht +# +# Dieser Befehl GEHT INS NETZ. Er ist damit die eine Stelle, an der dieses +# ansonsten abgeschottete Produkt eine fremde Adresse anspricht, und er sagt das +# vorher, samt Ziel und Ablageort. Ein Beweislauf fuehrt ihn nicht aus. + +set -eu + +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "$HERE/.." && pwd) +SERVER="$ROOT/vendor/llama/llama-server" + +# Dasselbe Verzeichnis wie in llm/start.sh, mit derselben Reihenfolge. Zwei +# Vorgaben fuer denselben Ort waeren ein geholtes Modell, das in der Liste des +# Starters nicht auftaucht. +MODELS_DIR=${ATLAS_MODELS_DIR:-${LLAMA_CACHE:-$ROOT/models}} + +usage() { + echo "Aufruf: llm/fetch-model.sh /[:]" >&2 + echo " llm/fetch-model.sh --print /[:]" >&2 + echo " llm/fetch-model.sh --list" >&2 + echo "" >&2 + echo "Beispiele (die sechs gemessenen Kandidaten stehen in docs/adr/0001-modellwahl.md):" >&2 + echo " llm/fetch-model.sh unsloth/Qwen3.5-2B-GGUF" >&2 + echo " llm/fetch-model.sh LiquidAI/LFM2.5-1.2B-Instruct-GGUF:Q4_K_M" >&2 + echo "" >&2 + echo "Cache-Verzeichnis: $MODELS_DIR (ueber ATLAS_MODELS_DIR aenderbar)" >&2 +} + +PRINT_ONLY=0 +case "${1:-}" in + --list) + echo "Cache-Verzeichnis: $MODELS_DIR" + FOUND=0 + for CANDIDATE in "$MODELS_DIR"/*.gguf; do + if [ -f "$CANDIDATE" ]; then + FOUND=1 + echo " $(basename "$CANDIDATE")" + fi + done + if [ "$FOUND" -eq 0 ]; then + echo " (leer)" + echo "Ein Modell holen: llm/fetch-model.sh unsloth/Qwen3.5-2B-GGUF" + fi + exit 0 + ;; + --print) + PRINT_ONLY=1 + shift + ;; + -h|--help|'') + usage + exit 2 + ;; +esac + +REPO=${1:-} +if [ -z "$REPO" ]; then + usage + exit 2 +fi + +# Eine reine Formpruefung, keine Netzabfrage: user/repo, wahlweise mit :quant. +# Ob es das Repository gibt, weiss nur huggingface.co, und dieses Skript +# behauptet darueber nichts. +if ! printf '%s' "$REPO" | grep -Eq '^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+(:[A-Za-z0-9._-]+)?$'; then + echo "Das ist keine Repo-Kennung: $REPO" >&2 + echo "Erwartet wird user/repo oder user/repo:quant, zum Beispiel" >&2 + echo "unsloth/Qwen3.5-2B-GGUF oder unsloth/Qwen3.5-2B-GGUF:Q4_K_M." >&2 + exit 2 +fi + +COMMAND="LLAMA_CACHE=\"$MODELS_DIR\" $SERVER -hf $REPO --no-warmup -c 512" + +echo "Dieser Aufruf geht ins Netz: er laedt $REPO von huggingface.co" +echo "und legt die Datei in $MODELS_DIR ab." +echo "Die Anwendung selbst laedt nichts herunter; sie zeigt nur diesen Aufruf." +echo "" +echo "$COMMAND" + +if [ "$PRINT_ONLY" -eq 1 ]; then + exit 0 +fi + +if [ ! -x "$SERVER" ]; then + echo "" >&2 + echo "llama-server fehlt: $SERVER" >&2 + echo "vendor/llama/ ist gitignoriert; Herkunft und Baubefehl stehen in vendor/llama/HERKUNFT.md." >&2 + exit 4 +fi + +mkdir -p "$MODELS_DIR" + +# Im Vordergrund und ohne Kunstgriffe. +# +# `-hf` laedt und startet danach den Dienst; einen Modus "nur laden" gibt es +# nicht. Dieses Skript raet deshalb nicht, wann der Download fertig ist, sondern +# sagt, woran man es sieht, und ueberlaesst das Beenden dem Leser. Ein Skript, +# das nach einer geschaetzten Frist zuschlaegt, waere ein Skript, das einen +# halben Download fuer fertig erklaeren kann. +echo "" +echo "Der Server laedt jetzt und startet danach von selbst. Sobald in seiner" +echo "Ausgabe \"server is listening\" steht, ist die Datei im Cache: dann mit" +echo "Ctrl-C beenden. Die Datei bleibt liegen, und llm/start.sh findet sie." +echo "" +exec env LLAMA_CACHE="$MODELS_DIR" "$SERVER" -hf "$REPO" --no-warmup -c 512 diff --git a/graph-ui/llm/start.sh b/graph-ui/llm/start.sh new file mode 100644 index 000000000..657ce70a3 --- /dev/null +++ b/graph-ui/llm/start.sh @@ -0,0 +1,280 @@ +#!/bin/sh +# Den lokalen Modell-Sidecar starten. Ein Prozess, ein Port, eine PID-Datei. +# +# Warum es dieses Skript gibt und nicht einen Knopf in der Oberflaeche: die SPA +# hat kein eigenes Backend und kann keinen Prozess starten (ADR 0001, +# Abschnitt "Engine-Entscheidung"). Ein Knopf, der einen Prozess verspricht, +# waere eine Luege in Knopfform. Der Manager im Browser erkennt, zeigt und +# leitet an; gestartet wird hier, von Hand oder aus einem Beweislauf. +# +# ## Die Vorgabe ist seit W10 der Router, nicht ein Dateiname +# +# Ohne Argument faehrt dieses Skript llama-server ueber ein CACHE-VERZEICHNIS: +# +# --models-dir "$ATLAS_MODELS_DIR" --models-max N --models-autoload +# +# Damit listet /v1/models, was in dem Verzeichnis liegt, und das Feld "model" +# einer Chat-Anfrage waehlt daraus aus. Das Einstellungen-Panel kann so zwischen +# Modellen umschalten, ohne dass jemand einen Prozess neu startet, und ohne dass +# die Oberflaeche einen Prozess anfassen muesste, was sie nicht darf. +# +# Der Grund fuer die Umstellung steht im Nachtrag W10 von docs/adr/0001: das +# Programm liefert kein Modell mehr aus und nennt auch keins als gesetzt. Welche +# Datei laeuft, entscheidet der Leser. +# +# ## Woher das Cache-Verzeichnis kommt +# +# ATLAS_MODELS_DIR, sonst $LLAMA_CACHE (die Umgebungsvariable, die llama.cpp +# selbst fuer seinen Cache liest), sonst /models. Das letzte ist kein +# Notbehelf: models/ IST das lokale Cache-Verzeichnis dieses Projekts. Es ist +# gitignoriert, sein Inhalt gehoert dem Leser, und llm/fetch-model.sh laedt in +# genau dieses Verzeichnis, damit ein geholtes Modell in derselben Liste +# auftaucht wie die schon vorhandenen. +# +# ## Aufruf +# +# llm/start.sh Router ueber das Cache-Verzeichnis +# llm/start.sh .gguf eine bestimmte Datei +# llm/start.sh eine Datei aus dem Cache-Verzeichnis +# llm/start.sh hf:user/repo[:quant] direkt von Hugging Face (geht ins Netz) +# llm/start.sh class-a die Reproduktionsgriffe der ADR-Eval +# llm/start.sh --print-command [..] zeigt die Befehlszeile und startet nichts +# +# Aus der Umgebung: ATLAS_MODEL (wie das Argument), ATLAS_CTX (Kontextfenster), +# ATLAS_MODELS_DIR, ATLAS_MODELS_MAX (wie viele Instanzen gleichzeitig). +# +# ## --print-command, und warum es das gibt +# +# Ein Beweislauf soll pruefen koennen, WOMIT dieses Skript den Server aufruft, +# ohne dafuer ein Modell zu laden. Ohne diesen Schalter muesste er Gigabyte in +# den Speicher heben, um eine Zeichenkette zu sehen. Er schreibt die Zeile auf +# stdout, startet nichts und fasst weder Port noch PID-Datei an. +# +# Idempotent: laeuft der eigene Sidecar schon, passiert nichts und das Skript +# endet mit 0. Lauscht etwas ANDERES auf dem Port, wird nichts gestartet und +# nichts abgeschossen; das Skript sagt, was es vorgefunden hat, und endet mit 3. + +set -eu + +HERE=$(cd "$(dirname "$0")" && pwd) +ROOT=$(cd "$HERE/.." && pwd) +PORT=4141 +HOST=127.0.0.1 +PID_FILE="$HERE/.pid" +LOG_FILE="$HERE/llama-server.log" +SERVER="$ROOT/vendor/llama/llama-server" + +PRINT_ONLY=0 +if [ "${1:-}" = "--print-command" ]; then + PRINT_ONLY=1 + shift +fi + +# Das Cache-Verzeichnis. Siehe Kopf: die Reihenfolge ist Argument-Umgebung, +# llama.cpp-Umgebung, Projektverzeichnis. +MODELS_DIR=${ATLAS_MODELS_DIR:-${LLAMA_CACHE:-$ROOT/models}} +# Wie viele Instanzen der Router gleichzeitig geladen halten darf. Zwei, weil +# genau zwei gebraucht werden, um ohne Neustart umzuschalten: die laufende und +# die gewaehlte. Jede weitere kostet den Speicher eines ganzen Modells fuer eine +# Auswahl, die niemand getroffen hat. Die Vorgabe von llama-server waere 4. +MODELS_MAX=${ATLAS_MODELS_MAX:-2} + +CHOICE=${1:-${ATLAS_MODEL:-}} + +# --------------------------------------------------------------------------- +# Die Reproduktionsgriffe der ADR-Eval. +# +# Das ist KEINE Vorgabe mehr, und die Reihenfolge in dieser Datei sagt das: die +# Vorgabe ist der Router weiter unten. Diese sechs Namen sind die Griffe, mit +# denen die Laeufe von W5 wiederholbar bleiben (tools/smoke-w5a.mjs faehrt +# class-a und erwartet Qwen3.5-2B mit Kontext 3072, tools/smoke-w5b.mjs und +# tools/smoke-w6-full.mjs fahren die anderen, tools/eval-check.mjs erwartet die +# Dateien unter models/). Sie sind Belege und keine Empfehlung; welches Modell +# gut ist, sagt die Eval-Tabelle im ADR, und welches laeuft, sagt der Leser. +# +# Sie loesen darum ausdruecklich gegen /models auf und nicht gegen +# $ATLAS_MODELS_DIR: die Dateien der Eval liegen dort, und ein gesetztes +# LLAMA_CACHE wuerde sonst einen Reproduktionslauf ins Leere schicken. +# +# Die Wahlen hiessen bis zum 2026-08-29 "1b" und "4b". Befund 17 des +# unabhaengigen Audits: der Sieger der Klasse A ist ein 2B-Modell, und eine +# Beschriftung, die es "1b" nennt, sagt etwas Falsches ueber die Datei, die sie +# laedt. Die Klasse haengt ohnehin am Kontextfenster und nicht an der +# Parameterzahl (siehe modelClassOf in src/llm/sidecar.ts). Die alten Namen +# bleiben als Alias gueltig. +# --------------------------------------------------------------------------- +ALIAS_FILE='' +ALIAS_CTX='' +case "$CHOICE" in + class-a|1b) ALIAS_FILE=Qwen3.5-2B-Q4_K_M.gguf ; ALIAS_CTX=3072 ;; + class-a-lfm|1b-lfm) ALIAS_FILE=LFM2.5-1.2B-Instruct-Q4_K_M.gguf ; ALIAS_CTX=3072 ;; + class-a-minicpm|1b-minicpm) ALIAS_FILE=MiniCPM5-1B-Q4_K_M.gguf ; ALIAS_CTX=3072 ;; + class-a-coder|1b-coder) ALIAS_FILE=qwen2.5-coder-1.5b-instruct-q4_k_m.gguf ; ALIAS_CTX=3072 ;; + class-b|4b) ALIAS_FILE=Qwen3.5-4B-Q4_K_M.gguf ; ALIAS_CTX=8192 ;; + class-b-gemma|4b-gemma) ALIAS_FILE=gemma-4-E4B-it-Q4_K_M.gguf ; ALIAS_CTX=8192 ;; +esac + +case "$CHOICE" in + 1b|1b-lfm|1b-minicpm|1b-coder|4b|4b-gemma) + echo "Hinweis: \"$CHOICE\" ist der alte Name dieser Wahl. Sie heisst jetzt nach ihrer" >&2 + echo "Klasse; die Klassen sind class-a (Kontext 3072) und class-b (Kontext 8192)." >&2 + ;; +esac + +# Der Kontext: die Umgebung schlaegt den Alias, der Alias die Vorgabe. 3072 ist +# das Budget der Klasse A aus PLAN Paragraph 5 und damit die konservative Wahl +# fuer ein Modell, ueber das dieses Skript nichts weiss. +CTX=${ATLAS_CTX:-${ALIAS_CTX:-3072}} + +# --------------------------------------------------------------------------- +# Die Argumente des Servers, in genau einer Variablen. +# +# Sie entstehen hier und werden weiter unten entweder gedruckt oder gefahren. +# Zwei Stellen, an denen die Zeile entsteht, waeren zwei Zeilen: der Beweislauf +# saehe die eine und der Leser bekaeme die andere. +# --------------------------------------------------------------------------- +MODE='' +DESCRIPTION='' +set -- --host "$HOST" --port "$PORT" + +if [ -n "$ALIAS_FILE" ]; then + MODE=eval-alias + MODEL_PATH="$ROOT/models/$ALIAS_FILE" + if [ "$PRINT_ONLY" -eq 0 ] && [ ! -f "$MODEL_PATH" ]; then + echo "Modell fehlt: $MODEL_PATH" >&2 + echo "\"$CHOICE\" ist ein Reproduktionsgriff der ADR-Eval und erwartet die Datei aus" >&2 + echo "models/SHA256SUMS. Holen: llm/fetch-model.sh --list" >&2 + exit 4 + fi + # Der relative Pfad, byte-identisch zu dem, was dieses Skript vor W10 + # uebergeben hat: /props meldet ihn woertlich zurueck, und die Beweislaeufe + # von W5 und W6 lesen ihn dort. + set -- "$@" -m "models/$ALIAS_FILE" -c "$CTX" + DESCRIPTION="$ALIAS_FILE (Reproduktionsgriff $CHOICE), Kontext $CTX" +elif [ -z "$CHOICE" ]; then + MODE=router + set -- "$@" --models-dir "$MODELS_DIR" --models-max "$MODELS_MAX" --models-autoload -c "$CTX" + DESCRIPTION="Router ueber $MODELS_DIR (hoechstens $MODELS_MAX geladen), Kontext $CTX" +else + case "$CHOICE" in + hf:*) + MODE=hf + REPO=${CHOICE#hf:} + set -- "$@" -hf "$REPO" -c "$CTX" + DESCRIPTION="$REPO von huggingface.co, Kontext $CTX" + ;; + *) + MODE=file + # Ein Pfad, oder ein Name im Cache-Verzeichnis. Erst wird gefragt, + # ob die Datei so existiert; erst dann wird sie im Cache gesucht. + if [ -f "$CHOICE" ]; then + MODEL_PATH="$CHOICE" + elif [ -f "$MODELS_DIR/$CHOICE" ]; then + MODEL_PATH="$MODELS_DIR/$CHOICE" + elif [ -f "$MODELS_DIR/$CHOICE.gguf" ]; then + MODEL_PATH="$MODELS_DIR/$CHOICE.gguf" + else + echo "unbekannte Modellwahl: $CHOICE" >&2 + echo "Erwartet wird eines von:" >&2 + echo " ein Pfad auf eine .gguf-Datei" >&2 + echo " ein Dateiname aus $MODELS_DIR" >&2 + echo " hf:user/repo[:quant] (laedt von huggingface.co)" >&2 + echo " ein Reproduktionsgriff der ADR-Eval, davon" >&2 + # Diese Zeile ist eine Naht: src/llm/strings.test.ts liest sie aus + # diesem Skript und vergleicht sie mit LLM_MODEL_CHOICES. Die + # Liste steht damit einmal in der Wahrheit und einmal in der + # Anzeige, und ein Test haelt beide zusammen (Audit-Befund 17). + echo "erlaubt: class-a | class-a-lfm | class-a-minicpm | class-a-coder | class-b | class-b-gemma" >&2 + echo " (die alten Namen 1b, 1b-lfm, 1b-minicpm, 1b-coder, 4b, 4b-gemma gelten weiter)" >&2 + echo "Ohne Argument faehrt dieses Skript den Router ueber $MODELS_DIR." >&2 + exit 2 + fi + set -- "$@" -m "$MODEL_PATH" -c "$CTX" + DESCRIPTION="$MODEL_PATH, Kontext $CTX" + ;; + esac +fi + +if [ "$PRINT_ONLY" -eq 1 ]; then + printf '%s' "$SERVER" + for ARG in "$@"; do + printf ' %s' "$ARG" + done + printf '\n' + exit 0 +fi + +if [ ! -x "$SERVER" ]; then + echo "llama-server fehlt: $SERVER" >&2 + echo "vendor/llama/ ist gitignoriert; Herkunft und Baubefehl stehen in vendor/llama/HERKUNFT.md." >&2 + exit 4 +fi + +# Ein leeres Cache-Verzeichnis ist kein Start, sondern eine Auskunft. +# +# Ohne diese Pruefung liefe der Router an und antwortete auf jede Anfrage mit +# einer leeren Modell-Liste, und aus der Ferne saehe das aus wie ein Defekt. +# Gesagt wird darum, was fehlt und wie man es holt. +if [ "$MODE" = router ]; then + # Ein Glob und kein find: `find -quit` gibt es nicht ueberall, und die Frage + # ist ohnehin nur, ob EINE Datei da ist. + HAVE_GGUF=0 + for CANDIDATE in "$MODELS_DIR"/*.gguf; do + if [ -f "$CANDIDATE" ]; then + HAVE_GGUF=1 + break + fi + done + if [ "$HAVE_GGUF" -eq 0 ]; then + echo "Kein Modell im Cache-Verzeichnis: $MODELS_DIR" >&2 + echo "Dieses Programm liefert keines mit. So holen Sie eines (geht ins Netz):" >&2 + echo " llm/fetch-model.sh unsloth/Qwen3.5-2B-GGUF" >&2 + echo "oder direkt:" >&2 + echo " LLAMA_CACHE=\"$MODELS_DIR\" $SERVER -hf unsloth/Qwen3.5-2B-GGUF:Q4_K_M" >&2 + echo "Ein anderes Verzeichnis: ATLAS_MODELS_DIR=/pfad/zum/cache llm/start.sh" >&2 + echo "Die Vorschlaege mit ihren gemessenen Zahlen stehen im Einstellungen-Panel." >&2 + exit 5 + fi +fi + +# Laeuft der eigene Prozess schon? Die PID-Datei allein reicht als Auskunft +# nicht: sie ueberlebt einen Absturz. Gefragt wird deshalb das Betriebssystem. +if [ -f "$PID_FILE" ]; then + OLD=$(cat "$PID_FILE" 2>/dev/null || echo '') + if [ -n "$OLD" ] && kill -0 "$OLD" 2>/dev/null; then + echo "Sidecar laeuft bereits (pid $OLD) auf $HOST:$PORT. Nichts zu tun." + exit 0 + fi + echo "verwaiste PID-Datei ($OLD) wird entfernt." + rm -f "$PID_FILE" +fi + +# Der Port gehoert dem Sidecar. Wer sonst dort lauscht, wird nicht abgeschossen: +# ein Startskript, das fremde Prozesse beendet, ist ein Startskript, dem man +# nicht mehr zusehen kann. +if command -v lsof >/dev/null 2>&1; then + BUSY=$(lsof -ti "tcp:$PORT" 2>/dev/null || true) + if [ -n "$BUSY" ]; then + echo "Port $PORT ist belegt (pid $BUSY), aber nicht von diesem Skript." >&2 + echo "Nichts gestartet und nichts beendet." >&2 + exit 3 + fi +fi + +if [ "$MODE" = hf ]; then + echo "Dieser Aufruf geht ins Netz (huggingface.co) und laedt nach ${LLAMA_CACHE:-$MODELS_DIR}." +fi + +cd "$ROOT" +nohup "$SERVER" "$@" > "$LOG_FILE" 2>&1 & +PID=$! +echo "$PID" > "$PID_FILE" + +echo "Sidecar gestartet: pid $PID, $DESCRIPTION, http://$HOST:$PORT" +if [ "$MODE" = router ]; then + echo "Die Modelle im Cache: curl -s http://$HOST:$PORT/v1/models" + echo "Umschalten ohne Neustart: im Einstellungen-Panel der Oberflaeche." +fi +echo "Bereitschaft: curl -s http://$HOST:$PORT/health -> {\"status\":\"ok\"}" +echo "Log: $LOG_FILE" +echo "Beenden: $HERE/stop.sh" diff --git a/graph-ui/llm/stop.sh b/graph-ui/llm/stop.sh new file mode 100644 index 000000000..6cf8e280a --- /dev/null +++ b/graph-ui/llm/stop.sh @@ -0,0 +1,54 @@ +#!/bin/sh +# Den eigenen Sidecar beenden, und nur den eigenen. +# +# Beendet wird ausschliesslich die PID aus llm/.pid. Kein pkill auf einen +# Namen, kein "was auch immer auf 4141 lauscht": beides wuerde fremde Prozesse +# treffen, die dieses Skript nie gestartet hat. Steht in der Datei eine PID, die +# es nicht mehr gibt, wird die Datei aufgeraeumt und das gesagt. +# +# Aufruf: llm/stop.sh + +set -eu + +HERE=$(cd "$(dirname "$0")" && pwd) +PID_FILE="$HERE/.pid" + +if [ ! -f "$PID_FILE" ]; then + echo "keine PID-Datei ($PID_FILE): dieses Skript hat gerade nichts laufen." + exit 0 +fi + +PID=$(cat "$PID_FILE" 2>/dev/null || echo '') +if [ -z "$PID" ]; then + echo "PID-Datei war leer, wird entfernt." + rm -f "$PID_FILE" + exit 0 +fi + +if ! kill -0 "$PID" 2>/dev/null; then + echo "pid $PID laeuft nicht mehr; die PID-Datei wird entfernt." + rm -f "$PID_FILE" + exit 0 +fi + +kill "$PID" 2>/dev/null || true + +# Erst hoeflich, dann nicht mehr. Fuenf Sekunden reichen dem Sidecar, um seine +# Slots abzuraeumen; laenger zu warten hiesse, jedem Beweislauf eine Pause +# aufzuzwingen, die nur der Hoeflichkeit dient. +i=0 +while [ "$i" -lt 50 ]; do + if ! kill -0 "$PID" 2>/dev/null; then + rm -f "$PID_FILE" + echo "Sidecar beendet (pid $PID)." + exit 0 + fi + sleep 0.1 + i=$((i + 1)) +done + +echo "pid $PID reagierte nicht auf SIGTERM, sende SIGKILL." >&2 +kill -9 "$PID" 2>/dev/null || true +sleep 0.3 +rm -f "$PID_FILE" +echo "Sidecar hart beendet (pid $PID)." diff --git a/graph-ui/models/SHA256SUMS b/graph-ui/models/SHA256SUMS new file mode 100644 index 000000000..cdb546490 --- /dev/null +++ b/graph-ui/models/SHA256SUMS @@ -0,0 +1,7 @@ +# Diese sechs Summen sind der BELEG der Eval-Laeufe aus W5 (docs/adr/0001-modellwahl.md, Abschnitt "Eval-Ergebnis"), nicht eine Liste von Dateien, die dieses Programm mitliefert oder erwartet: seit W10 waehlt der Leser sein Modell selbst, models/ ist sein Cache, und wer nur drei dieser Dateien liegen hat, hat nichts Kaputtes, sondern drei Modelle. +85a896a047553e842f25297ee5b031d64ff30147d9c4af17b1e4b394cd1fab87 gemma-4-E4B-it-Q4_K_M.gguf +b1b3de114215d9507409a662a501a631095a479a419584e8a2ded6304b19b4f5 LFM2.5-1.2B-Instruct-Q4_K_M.gguf +81b64d05a23b17b34c475f42b3e72fbde62d4b92cc34541f7a8031d0752deafa MiniCPM5-1B-Q4_K_M.gguf +cc324af070c2ecbfd324a30884d2f951a7ff756aba85cb811a6ec436933bb046 qwen2.5-coder-1.5b-instruct-q4_k_m.gguf +aaf42c8b7c3cab2bf3d69c355048d4a0ee9973d48f16c731c0520ee914699223 Qwen3.5-2B-Q4_K_M.gguf +3874209241c9a397e2f62cd3f70f80fd2dfbf0dfccb6838416bdb48a714e8630 Qwen3.5-4B-Q4_K_M.gguf diff --git a/graph-ui/package-lock.json b/graph-ui/package-lock.json index b5441de62..ddac3822e 100644 --- a/graph-ui/package-lock.json +++ b/graph-ui/package-lock.json @@ -1,47 +1,39 @@ { - "name": "graph-ui", - "version": "0.1.0", + "name": "cbm-lens", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "graph-ui", - "version": "0.1.0", - "dependencies": { - "@react-three/drei": "^10.7.0", - "@react-three/fiber": "^9.5.0", - "@react-three/postprocessing": "^3.0.4", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^0.577.0", - "postprocessing": "^6.38.3", - "radix-ui": "^1.4.3", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "tailwind-merge": "^3.5.0", - "three": "~0.183.0" + "name": "cbm-lens", + "version": "0.0.1", + "dependencies": { + "@huggingface/transformers": "4.2.0", + "@react-three/drei": "10.7.7", + "@react-three/fiber": "9.5.0", + "@react-three/postprocessing": "3.0.4", + "postprocessing": "6.38.3", + "react": "19.2.8", + "react-dom": "19.2.8", + "react-markdown": "10.1.0", + "remark-gfm": "4.0.1", + "three": "0.183.2", + "yaml": "2.8.2" }, "devDependencies": { - "@tailwindcss/vite": "^4.2.1", - "@testing-library/jest-dom": "^6.6.0", - "@testing-library/react": "^16.1.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "@types/three": "~0.183.0", - "@vitejs/plugin-react": "^4.3.0", - "jsdom": "^25.0.0", - "tailwindcss": "^4.1.0", - "typescript": "^5.7.0", - "vite": "^6.4.3", - "vitest": "^4.1.11" - } - }, - "node_modules/@adobe/css-tools": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", - "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", - "dev": true, - "license": "MIT" + "@types/node": "20.19.43", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.5", + "@types/three": "0.183.1", + "@vitejs/plugin-react": "4.7.0", + "jsdom": "25.0.1", + "monaco-editor": "0.56.0", + "playwright": "1.62.1", + "three-stdlib": "2.36.1", + "typescript": "5.9.3", + "vite": "6.4.3", + "vitest": "4.1.11" + } }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", @@ -121,14 +113,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", - "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -197,9 +189,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { @@ -251,13 +243,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", - "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.7" + "@babel/types": "^7.29.8" }, "bin": { "parser": "bin/babel-parser.js" @@ -267,13 +259,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -283,13 +275,13 @@ } }, "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@babel/helper-plugin-utils": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -299,9 +291,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -323,18 +315,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", - "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", + "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", + "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -342,9 +334,9 @@ } }, "node_modules/@babel/types": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", - "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "dev": true, "license": "MIT", "dependencies": { @@ -476,6 +468,16 @@ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==", "license": "Apache-2.0" }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -918,1609 +920,723 @@ "node": ">=18" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", - "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", - "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.5", - "@floating-ui/utils": "^0.2.11" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", - "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.6" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", - "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", - "license": "MIT" - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, + "node_modules/@huggingface/jinja": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.10.tgz", + "integrity": "sha512-SgS1D1bglQ94ceD4ZCL6eayUDy9uV1xuyk61OjgSxU5GJh7upqZCKII0JoTYMc6wWsg3JUgaPRX0ElQzXPk3Cw==", "license": "MIT", "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "node": ">=18" } }, - "node_modules/@mediapipe/tasks-vision": { - "version": "0.10.17", - "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", - "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "node_modules/@huggingface/tokenizers": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@huggingface/tokenizers/-/tokenizers-0.1.3.tgz", + "integrity": "sha512-8rF/RRT10u+kn7YuUbUg0OF30K8rjTc78aHpxT+qJ1uWSqxT1MHi8+9ltwYfkFYJzT/oS+qw3JVfHtNMGAdqyA==", "license": "Apache-2.0" }, - "node_modules/@monogrid/gainmap-js": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", - "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", - "license": "MIT", + "node_modules/@huggingface/transformers": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@huggingface/transformers/-/transformers-4.2.0.tgz", + "integrity": "sha512-8BRCoBMH0XsWaEIamuR0LrJGAfftgHAfb2Vrffy0VKlSAE/MnUJ5/h/zTfEP3fDIft+nk7TqB8xXEyABGitBjQ==", + "license": "Apache-2.0", "dependencies": { - "promise-worker-transferable": "^1.0.4" - }, - "peerDependencies": { - "three": ">= 0.159.0" + "@huggingface/jinja": "^0.5.6", + "@huggingface/tokenizers": "^0.1.3", + "onnxruntime-node": "1.24.3", + "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c", + "sharp": "^0.34.5" } }, - "node_modules/@radix-ui/number": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", - "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-accessible-icon": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.7.tgz", - "integrity": "sha512-XM+E4WXl0OqUJFovy6GjmxxFyx9opfCAIUku4dlKRd5YEPqt4kALOkQOp0Of6reHuUkJuiPBEc5k0o4z4lTC8A==", + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", - "dependencies": { - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "engines": { + "node": ">=18" } }, - "node_modules/@radix-ui/react-accordion": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", - "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.0.tgz", + "integrity": "sha512-ZgaYEwaj+lx/5n4W8GmZ2IYz0PQHjN5eqRcfijWGB+2Aq7ZInZGa0qJyAn6DEtyLuWHRSrmWOqT9q3qqTBvmUQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "funding": { + "url": "https://opencollective.com/libvips" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.0" } }, - "node_modules/@radix-ui/react-alert-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", - "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.0.tgz", + "integrity": "sha512-c1z9LFpKB0slQW3RchwBE8iSVzGp70TNjUUO9k4BZwwW4HH7JBGHeIy4b+kk4n/kcBASb9evKCE3/7Slmslgiw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "funding": { + "url": "https://opencollective.com/libvips" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.0" } }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.0.tgz", + "integrity": "sha512-Li2KTev0H90kEtnJHkI9xQojXt1AqWmFBMXiPw5kqd1jQgP7gi5HVK/qC5Rmh/59NuAwUuPzzPITmX22NomYYQ==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "@img/sharp-wasm32": "0.35.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=20.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-aspect-ratio": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.7.tgz", - "integrity": "sha512-Yq6lvO9HQyPwev1onK1daHCHqXVLzPhSVjmsNjCa2Zcxy2f7uJD2itDtxknv6FzAKCwD1qQkeVDmX/cev13n/g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.0.tgz", + "integrity": "sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-avatar": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", - "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.0.tgz", + "integrity": "sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-checkbox": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", - "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.0.tgz", + "integrity": "sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", - "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.0.tgz", + "integrity": "sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context-menu": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", - "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-form": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.1.8.tgz", - "integrity": "sha512-QM70k4Zwjttifr5a4sZFts9fn8FzHYvQ5PiB19O2HsYibaHSVt9fH9rzB0XZo/YcM+b7t/p7lYCT/F5eOeF5yQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-hover-card": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", - "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-label": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", - "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menubar": { - "version": "1.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", - "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-navigation-menu": { - "version": "1.2.14", - "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", - "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-one-time-password-field": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-one-time-password-field/-/react-one-time-password-field-0.1.8.tgz", - "integrity": "sha512-ycS4rbwURavDPVjCb5iS3aG4lURFDILi6sKI/WITUMZ13gMmn/xGjpLoqBAalhJaDk8I3UbCM5GzKHrnzwHbvg==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-password-toggle-field": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-password-toggle-field/-/react-password-toggle-field-0.1.3.tgz", - "integrity": "sha512-/UuCrDBWravcaMix4TdT+qlNdVwOM1Nck9kWx/vafXsdfj1ChfhOdfi3cy9SGBpWgTXwYCuboT/oYpJy3clqfw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-is-hydrated": "0.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.0.tgz", + "integrity": "sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-progress": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", - "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.0.tgz", + "integrity": "sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-radio-group": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", - "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.0.tgz", + "integrity": "sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.0.tgz", + "integrity": "sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-scroll-area": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", - "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.0.tgz", + "integrity": "sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-select": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", - "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.0.tgz", + "integrity": "sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-separator": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", - "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.0.tgz", + "integrity": "sha512-VVlpEWwizEFIOom0zdoeKuO5nuTswzVE5uHcBNvHzmeHUpNFajY3HFfbQ+zIH4E2kVaZ/yVxmsShW56TtEy4uA==", + "cpu": [ + "arm" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "funding": { + "url": "https://opencollective.com/libvips" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.0" } }, - "node_modules/@radix-ui/react-slider": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", - "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", - "license": "MIT", - "dependencies": { - "@radix-ui/number": "1.1.1", - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.0.tgz", + "integrity": "sha512-4+4XHLNT5wDT0roYlHTEmH9lDKt0acf9Tv+3hM3iceOirkxrR404/3WjAYZ9F9CkHrxeRcGLJXbi4vluMZ9O+A==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "funding": { + "url": "https://opencollective.com/libvips" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.0" } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.0.tgz", + "integrity": "sha512-N3hzbEpUTJC8pWpPVJvgzGxM+so/MAXc8O2s/53B0LL9ZGpfXpME7Wizkc5d/8fRBlBtkDjzoZGDCqqNDHqLEw==", + "cpu": [ + "ppc64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "funding": { + "url": "https://opencollective.com/libvips" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.0" } }, - "node_modules/@radix-ui/react-switch": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", - "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-previous": "1.1.1", - "@radix-ui/react-use-size": "1.1.1" + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.0.tgz", + "integrity": "sha512-l6vmKVPnbS0RhVMbyxP5meAARsbhCnBN4fy31qz0+3a6Rv4jEqfzDrT89y6ZPkCi0AJGnwp2En528yXo401Hpw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "funding": { + "url": "https://opencollective.com/libvips" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.0" } }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.0.tgz", + "integrity": "sha512-MYlMiPFiv/EKPAHnp3yNZ9AAWFsxga9c5Bkc6wkar6bqzHLlkGVJHRm0u1ei+VXnZxp3Mz9MG9ZIsI8vSOf3sQ==", + "cpu": [ + "s390x" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "funding": { + "url": "https://opencollective.com/libvips" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.0" } }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", - "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.0.tgz", + "integrity": "sha512-TYaItB5oj1ioXjhyn2xrR208vf+YuIIcHptQWRRaBmFhvIvL9D72DXN8w75xup0KXA8UdEAhQ9Qb2S49FD/9Cw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "funding": { + "url": "https://opencollective.com/libvips" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.0" } }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", - "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.0.tgz", + "integrity": "sha512-DSTb6ijQzqe6DdAaOBVqJ/SYf1vO8EW5bK6X6LRXufEBebf2722VCdvBUtZ3rtV0x2ApfPNDy/p7LrrjaWjiyQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "funding": { + "url": "https://opencollective.com/libvips" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.0" } }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", - "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-use-controllable-state": "1.2.2" + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.0.tgz", + "integrity": "sha512-K7ykQ+26Rt6+4BTU80AuGgTPIYX86UxiAKT4rcXX/WNTo7k1ZxpKz+TguHnwVpCqQK3B5PK0vZ0ZBe6nz/ib1w==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "funding": { + "url": "https://opencollective.com/libvips" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.0" } }, - "node_modules/@radix-ui/react-toolbar": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toolbar/-/react-toolbar-1.1.11.tgz", - "integrity": "sha512-4ol06/1bLoFu1nwUqzdD4Y5RZ9oDdKeiHIsntug54Hcr1pgaHiPqHFEaXI1IFP/EsOfROQZ8Mig9VTIRza6Tjg==", - "license": "MIT", + "node_modules/@img/sharp-wasm32": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.0.tgz", + "integrity": "sha512-9woLIFORERCr+6cWu87dQ22J34EExkhc73U1kZW0c+RclQqWetoodByp4dWZ/hN8/KVmTRAx2HOnUwib8AwZdA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-toggle-group": "1.1.11" + "@emnapi/runtime": "^1.11.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=20.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "license": "MIT", + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.0.tgz", + "integrity": "sha512-t+kie1TOyaDM6Dho+f+y0VqIUNhYQaKCUahuZVi0E0frgdiaOaPsDxDW3wfKacUdaNBCnK/ZDBMg33ydvHj8uA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" + "@img/sharp-wasm32": "0.35.0" }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "engines": { + "node": ">=20.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.0.tgz", + "integrity": "sha512-M5eKxug0dabbaWgFKvPa3odNs2OpaP+81NASfGKkt4GcYXpNhSu7CaeYxWkLNV6vHmUp4hnCxnxrUyhUJhXbKA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.0.tgz", + "integrity": "sha512-z0+pZ03QCDvdVN0Ez9IX/yjWC19ikMlXrmdYMwYNLTh2BLPx3hXWPvyqWfquZ0BTO9O6GVOjIVoTcyyacMnWlQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.0.tgz", + "integrity": "sha512-feNnlz5ZHKr0MY1LPHvZQyJeBkbo4ctsn0D8FvA53VTw5TC63rfEL2UrWbkSBR19htSE7Mw78xYVwdJqoMWVHw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/libvips" } }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@radix-ui/react-use-is-hydrated": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", - "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { - "use-sync-external-store": "^1.5.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@radix-ui/react-use-previous": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", - "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz", + "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==", + "license": "Apache-2.0" + }, + "node_modules/@monogrid/gainmap-js": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz", + "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==", "license": "MIT", "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" + "promise-worker-transferable": "^1.0.4" }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "three": ">= 0.159.0" } }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" } }, - "node_modules/@radix-ui/rect": { + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" }, "node_modules/@react-three/drei": { "version": "10.7.7", @@ -2644,9 +1760,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.63.1.tgz", + "integrity": "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ==", "cpu": [ "arm" ], @@ -2658,9 +1774,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.63.1.tgz", + "integrity": "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ==", "cpu": [ "arm64" ], @@ -2672,9 +1788,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.63.1.tgz", + "integrity": "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q==", "cpu": [ "arm64" ], @@ -2686,9 +1802,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.63.1.tgz", + "integrity": "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg==", "cpu": [ "x64" ], @@ -2700,9 +1816,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.63.1.tgz", + "integrity": "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw==", "cpu": [ "arm64" ], @@ -2714,9 +1830,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.63.1.tgz", + "integrity": "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q==", "cpu": [ "x64" ], @@ -2728,9 +1844,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.63.1.tgz", + "integrity": "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw==", "cpu": [ "arm" ], @@ -2742,9 +1858,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.63.1.tgz", + "integrity": "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw==", "cpu": [ "arm" ], @@ -2756,9 +1872,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.63.1.tgz", + "integrity": "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg==", "cpu": [ "arm64" ], @@ -2770,9 +1886,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.63.1.tgz", + "integrity": "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw==", "cpu": [ "arm64" ], @@ -2784,9 +1900,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.63.1.tgz", + "integrity": "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ==", "cpu": [ "loong64" ], @@ -2798,9 +1914,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.63.1.tgz", + "integrity": "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA==", "cpu": [ "loong64" ], @@ -2812,9 +1928,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.63.1.tgz", + "integrity": "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA==", "cpu": [ "ppc64" ], @@ -2826,9 +1942,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.63.1.tgz", + "integrity": "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA==", "cpu": [ "ppc64" ], @@ -2840,9 +1956,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.63.1.tgz", + "integrity": "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w==", "cpu": [ "riscv64" ], @@ -2854,9 +1970,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.63.1.tgz", + "integrity": "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ==", "cpu": [ "riscv64" ], @@ -2868,9 +1984,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.63.1.tgz", + "integrity": "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A==", "cpu": [ "s390x" ], @@ -2882,9 +1998,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.63.1.tgz", + "integrity": "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w==", "cpu": [ "x64" ], @@ -2896,9 +2012,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.63.1.tgz", + "integrity": "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow==", "cpu": [ "x64" ], @@ -2910,9 +2026,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.63.1.tgz", + "integrity": "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA==", "cpu": [ "x64" ], @@ -2924,9 +2040,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.63.1.tgz", + "integrity": "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw==", "cpu": [ "arm64" ], @@ -2938,9 +2054,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.63.1.tgz", + "integrity": "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg==", "cpu": [ "arm64" ], @@ -2952,9 +2068,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.63.1.tgz", + "integrity": "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg==", "cpu": [ "ia32" ], @@ -2966,9 +2082,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.63.1.tgz", + "integrity": "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg==", "cpu": [ "x64" ], @@ -2980,334 +2096,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tailwindcss/node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", - "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", - "jiti": "^2.6.1", - "lightningcss": "1.31.1", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.2.1" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", - "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.1", - "@tailwindcss/oxide-darwin-arm64": "4.2.1", - "@tailwindcss/oxide-darwin-x64": "4.2.1", - "@tailwindcss/oxide-freebsd-x64": "4.2.1", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", - "@tailwindcss/oxide-linux-x64-musl": "4.2.1", - "@tailwindcss/oxide-wasm32-wasi": "4.2.1", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", - "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", - "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", - "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", - "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", - "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", - "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", - "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", - "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", - "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", - "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.8.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.1.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.8.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.1", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", - "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", - "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.63.1.tgz", + "integrity": "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w==", "cpu": [ "x64" ], @@ -3316,101 +2107,14 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">= 20" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz", - "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.2.1", - "@tailwindcss/oxide": "4.2.1", - "tailwindcss": "4.2.1" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7" - } - }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", - "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@testing-library/jest-dom": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", - "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "picocolors": "^1.1.1", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT" + ] }, - "node_modules/@testing-library/react": { - "version": "16.3.2", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", - "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } + "license": "MIT" }, "node_modules/@tweenjs/tween.js": { "version": "23.1.3", @@ -3418,14 +2122,6 @@ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==", "license": "MIT" }, - "node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3482,6 +2178,15 @@ "assertion-error": "^2.0.1" } }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -3496,12 +2201,53 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/offscreencanvas": { "version": "2019.7.3", "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", @@ -3509,19 +2255,19 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "devOptional": true, + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", + "dev": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -3557,12 +2303,32 @@ "meshoptimizer": "~1.0.1" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/webxr": { "version": "0.5.24", "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz", "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", "license": "MIT" }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, "node_modules/@use-gesture/core": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", @@ -3716,11 +2482,20 @@ } }, "node_modules/@webgpu/types": { - "version": "0.1.69", - "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.69.tgz", - "integrity": "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==", + "version": "0.1.72", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.72.tgz", + "integrity": "sha512-0cF7RFM2edNoiIS1ODJp0/Gzv4/xSXhwoR0YCza+OWpJWtn4wmo9DvK91aLlH9+uUnwIriP7ZiC3WitmyhuzBw==", "license": "BSD-3-Clause" }, + "node_modules/adm-zip": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz", + "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==", + "license": "MIT", + "engines": { + "node": ">=14.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -3731,53 +2506,6 @@ "node": ">= 14" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -3795,6 +2523,16 @@ "dev": true, "license": "MIT" }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3816,9 +2554,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.21", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", - "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "version": "2.11.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", + "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3837,10 +2575,17 @@ "require-from-string": "^2.0.2" } }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, "node_modules/browserslist": { - "version": "4.28.9", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", - "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "dev": true, "funding": [ { @@ -3858,11 +2603,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.20", - "caniuse-lite": "^1.0.30001810", - "electron-to-chromium": "^1.5.420", - "node-releases": "^2.0.54", - "update-browserslist-db": "^1.3.2" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -3910,13 +2655,13 @@ } }, "node_modules/camera-controls": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz", - "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.0.tgz", + "integrity": "sha512-w5oULNpijgTRH0ARFJJ0R5ct1nUM3R3WP7/b8A6j9uTGpRfnsypc/RBMPQV8JQDPayUe37p/TZZY1PcUr4czOQ==", "license": "MIT", "engines": { - "node": ">=22.0.0", - "npm": ">=10.5.1" + "node": ">=20.11.0", + "npm": ">=10.8.2" }, "peerDependencies": { "three": ">=0.126.1" @@ -3943,6 +2688,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -3953,25 +2708,44 @@ "node": ">=18" } }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", - "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", "funding": { - "url": "https://polar.sh/cva" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", "license": "MIT", - "engines": { - "node": ">=6" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, "node_modules/combined-stream": { @@ -3987,6 +2761,16 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -4026,13 +2810,6 @@ "node": ">= 8" } }, - "node_modules/css.escape": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", - "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", - "dev": true, - "license": "MIT" - }, "node_modules/cssstyle": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", @@ -4078,7 +2855,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4099,6 +2875,53 @@ "dev": true, "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -4113,7 +2936,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -4132,25 +2954,39 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "license": "MIT" }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", "license": "MIT", - "peer": true + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dompurify": { + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", + "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } }, "node_modules/draco3d": { "version": "1.5.7", @@ -4174,26 +3010,12 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.422", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.422.tgz", - "integrity": "sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==", + "version": "1.5.416", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.416.tgz", + "integrity": "sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA==", "dev": true, "license": "ISC" }, - "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/entities": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", @@ -4211,7 +3033,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4221,23 +3042,22 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", "dev": true, "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "dev": true, "license": "MIT", "dependencies": { @@ -4263,6 +3083,12 @@ "node": ">= 0.4" } }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -4315,6 +3141,28 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -4326,15 +3174,21 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { "node": ">=12.0.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4359,6 +3213,12 @@ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "license": "MIT" }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -4377,9 +3237,9 @@ } }, "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4436,15 +3296,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -4452,11 +3303,56 @@ "dev": true, "license": "MIT", "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/global-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-3.0.0.tgz", + "integrity": "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "es6-error": "^4.1.1", + "matcher": "^3.0.0", + "roarr": "^2.15.3", + "semver": "^7.3.2", + "serialize-error": "^7.0.1" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/global-agent/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/glsl-noise": { @@ -4469,7 +3365,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4478,13 +3373,24 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", "license": "ISC" }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -4527,10 +3433,50 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hls.js": { - "version": "1.6.15", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.15.tgz", - "integrity": "sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.7.1.tgz", + "integrity": "sha512-DlzIkeBAS9IIQ432k3BUf3HlwbsR0+trB1i2lDdN2gUkNkrehFurh0/48M5c1/EjlDkdGng1gwZIpwyPxvdZ/g==", "license": "Apache-2.0" }, "node_modules/html-encoding-sniffer": { @@ -4546,6 +3492,16 @@ "node": ">=18" } }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -4613,14 +3569,66 @@ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-potential-custom-element-name": { @@ -4654,16 +3662,6 @@ "react": "^19.0.0" } }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4725,6 +3723,12 @@ "node": ">=6" } }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -4744,343 +3748,958 @@ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "license": "MIT", "dependencies": { - "immediate": "~3.0.5" + "immediate": "~3.0.5" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/maath": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", + "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "license": "MIT", + "peerDependencies": { + "@types/three": ">=0.134.0", + "three": ">=0.134.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/matcher": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", + "integrity": "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/lightningcss": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", - "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", - "dev": true, - "license": "MPL-2.0", + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" + "@types/mdast": "^4.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.31.1", - "lightningcss-darwin-arm64": "1.31.1", - "lightningcss-darwin-x64": "1.31.1", - "lightningcss-freebsd-x64": "1.31.1", - "lightningcss-linux-arm-gnueabihf": "1.31.1", - "lightningcss-linux-arm64-gnu": "1.31.1", - "lightningcss-linux-arm64-musl": "1.31.1", - "lightningcss-linux-x64-gnu": "1.31.1", - "lightningcss-linux-x64-musl": "1.31.1", - "lightningcss-win32-arm64-msvc": "1.31.1", - "lightningcss-win32-x64-msvc": "1.31.1" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", - "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", - "cpu": [ - "arm64" + "url": "https://opencollective.com/unified" + } + }, + "node_modules/meshline": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", + "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "license": "MIT", + "peerDependencies": { + "three": ">=0.137" + } + }, + "node_modules/meshoptimizer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz", + "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==", + "license": "MIT" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "engines": { - "node": ">= 12.0.0" + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/unified" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", - "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/unified" } }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", - "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/unified" } }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", - "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/unified" } }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", - "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/unified" } }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", - "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/unified" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", - "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/unified" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", - "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", - "cpu": [ - "x64" + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", - "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", - "cpu": [ - "x64" + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", - "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", - "cpu": [ - "arm64" + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.31.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", - "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", - "cpu": [ - "x64" + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", "dependencies": { - "yallist": "^3.0.2" + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/lucide-react": { - "version": "0.577.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", - "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "peer": true, - "bin": { - "lz-string": "bin/bin.js" + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/maath": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz", - "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==", + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "peerDependencies": { - "@types/three": ">=0.134.0", - "three": ">=0.134.0" + "dependencies": { + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "micromark-util-types": "^2.0.0" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "engines": { - "node": ">= 0.4" + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/meshline": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz", - "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==", + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", - "peerDependencies": { - "three": ">=0.137" + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/meshoptimizer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.0.1.tgz", - "integrity": "sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==", + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT" }, "node_modules/mime-db": { @@ -5106,27 +4725,27 @@ "node": ">= 0.6" } }, - "node_modules/min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "node_modules/monaco-editor": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz", + "integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=4" + "dependencies": { + "dompurify": "3.4.8", + "marked": "14.0.0" } }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/n8ao": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.10.1.tgz", - "integrity": "sha512-hhI1pC+BfOZBV1KMwynBrVlIm8wqLxj/abAWhF2nZ0qQKyzTSQa1QtLVS2veRiuoBQXojxobcnp0oe+PUoxf/w==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/n8ao/-/n8ao-1.10.3.tgz", + "integrity": "sha512-JFRk4WgUAUWO2KdJTqmBT02dP9kG7rE2oQbBy64E9+YwQ3d96KB9QBanqNy/NnYUfUTYU6Z/5OS5t/yLey7+6Q==", "license": "ISC", "peerDependencies": { "postprocessing": ">=6.30.0", @@ -5163,21 +4782,101 @@ } }, "node_modules/nwsapi": { - "version": "2.2.23", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", - "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "version": "2.2.25", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.25.tgz", + "integrity": "sha512-kktSgNDIbyEIs/LrE6dtEMnfzxSDpXViFl6c4gFjz/CgtnL4GDCR3wyZXzjvAXsNB8dXU4dAfvIOgLMH2/vwLg==", "dev": true, "license": "MIT" }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/onnxruntime-common": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.3.tgz", + "integrity": "sha512-GeuPZO6U/LBJXvwdaqHbuUmoXiEdeCjWi/EG7Y1HNnDwJYuk6WUbNXpF6luSUY8yASul3cmUlLGrCCL1ZgVXqA==", + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.24.3.tgz", + "integrity": "sha512-JH7+czbc8ALA819vlTgcV+Q214/+VjGeBHDjX81+ZCD0PCVCIFGFNtT0V4sXG/1JXypKPgScQcB3ij/hk3YnTg==", + "hasInstallScript": true, + "license": "MIT", + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "adm-zip": "^0.5.16", + "global-agent": "^3.0.0", + "onnxruntime-common": "1.24.3" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.26.0-dev.20260416-b7804b056c", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.26.0-dev.20260416-b7804b056c.tgz", + "integrity": "sha512-MD6Ss4GSpQBo6zqoJzyT9LRbKYs7x/JVN23FT24EcEvlqF4VuzPOeH6X38orZPKHQDbprn7K+SBpu0/mj2CQiw==", + "license": "MIT", + "dependencies": { + "flatbuffers": "^25.1.24", + "guid-typescript": "^1.0.9", + "long": "^5.2.3", + "onnxruntime-common": "1.24.0-dev.20251116-b39e144322", + "platform": "^1.3.6", + "protobufjs": "^7.2.4" + } + }, + "node_modules/onnxruntime-web/node_modules/onnxruntime-common": { + "version": "1.24.0-dev.20251116-b39e144322", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.24.0-dev.20251116-b39e144322.tgz", + "integrity": "sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==", + "license": "MIT" + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, "node_modules/parse5": { @@ -5217,9 +4916,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -5229,10 +4928,48 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "license": "MIT" + }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/postcss": { - "version": "8.5.24", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.24.tgz", - "integrity": "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -5250,7 +4987,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.16", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5273,22 +5010,6 @@ "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", "license": "ISC" }, - "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" - }, - "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" - } - }, "node_modules/promise-worker-transferable": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz", @@ -5299,6 +5020,39 @@ "lie": "^3.0.2" } }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5309,189 +5063,62 @@ "node": ">=6" } }, - "node_modules/radix-ui": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/radix-ui/-/radix-ui-1.4.3.tgz", - "integrity": "sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-accessible-icon": "1.1.7", - "@radix-ui/react-accordion": "1.2.12", - "@radix-ui/react-alert-dialog": "1.1.15", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-aspect-ratio": "1.1.7", - "@radix-ui/react-avatar": "1.1.10", - "@radix-ui/react-checkbox": "1.3.3", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-context-menu": "2.2.16", - "@radix-ui/react-dialog": "1.1.15", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-dropdown-menu": "2.1.16", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-form": "0.1.8", - "@radix-ui/react-hover-card": "1.1.15", - "@radix-ui/react-label": "2.1.7", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-menubar": "1.1.16", - "@radix-ui/react-navigation-menu": "1.2.14", - "@radix-ui/react-one-time-password-field": "0.1.8", - "@radix-ui/react-password-toggle-field": "0.1.3", - "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-progress": "1.1.7", - "@radix-ui/react-radio-group": "1.3.8", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-scroll-area": "1.2.10", - "@radix-ui/react-select": "2.2.6", - "@radix-ui/react-separator": "1.1.7", - "@radix-ui/react-slider": "1.3.6", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-switch": "1.2.6", - "@radix-ui/react-tabs": "1.1.13", - "@radix-ui/react-toast": "1.2.15", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-toggle-group": "1.1.11", - "@radix-ui/react-toolbar": "1.1.11", - "@radix-ui/react-tooltip": "1.2.8", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-escape-keydown": "1.1.1", - "@radix-ui/react-use-is-hydrated": "0.1.0", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.4" - } - }, - "node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-remove-scroll": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", - "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "react": "^19.2.8" } }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", "license": "MIT", "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" }, - "engines": { - "node": ">=10" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" }, "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "@types/react": ">=18", + "react": ">=18" } }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } + "node": ">=0.10.0" } }, "node_modules/react-use-measure": { @@ -5509,18 +5136,70 @@ } } }, - "node_modules/redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", "license": "MIT", "dependencies": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" }, - "engines": { - "node": ">=8" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, "node_modules/require-from-string": { @@ -5532,14 +5211,31 @@ "node": ">=0.10.0" } }, + "node_modules/roarr": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", + "integrity": "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==", + "license": "BSD-3-Clause", + "dependencies": { + "boolean": "^3.0.1", + "detect-node": "^2.0.4", + "globalthis": "^1.0.1", + "json-stringify-safe": "^5.0.1", + "semver-compare": "^1.0.0", + "sprintf-js": "^1.1.2" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "version": "4.63.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.1.tgz", + "integrity": "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -5549,31 +5245,32 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.63.1", + "@rollup/rollup-android-arm64": "4.63.1", + "@rollup/rollup-darwin-arm64": "4.63.1", + "@rollup/rollup-darwin-x64": "4.63.1", + "@rollup/rollup-freebsd-arm64": "4.63.1", + "@rollup/rollup-freebsd-x64": "4.63.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", + "@rollup/rollup-linux-arm-musleabihf": "4.63.1", + "@rollup/rollup-linux-arm64-gnu": "4.63.1", + "@rollup/rollup-linux-arm64-musl": "4.63.1", + "@rollup/rollup-linux-loong64-gnu": "4.63.1", + "@rollup/rollup-linux-loong64-musl": "4.63.1", + "@rollup/rollup-linux-ppc64-gnu": "4.63.1", + "@rollup/rollup-linux-ppc64-musl": "4.63.1", + "@rollup/rollup-linux-riscv64-gnu": "4.63.1", + "@rollup/rollup-linux-riscv64-musl": "4.63.1", + "@rollup/rollup-linux-s390x-gnu": "4.63.1", + "@rollup/rollup-linux-x64-gnu": "4.63.1", + "@rollup/rollup-linux-x64-musl": "4.63.1", + "@rollup/rollup-openbsd-x64": "4.63.1", + "@rollup/rollup-openharmony-arm64": "4.63.1", + "@rollup/rollup-win32-arm64-msvc": "4.63.1", + "@rollup/rollup-win32-ia32-msvc": "4.63.1", + "@rollup/rollup-win32-x64-gnu": "4.63.1", + "@rollup/rollup-win32-x64-msvc": "4.63.1", "fsevents": "~2.3.2" } }, @@ -5620,6 +5317,83 @@ "semver": "bin/semver.js" } }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "license": "MIT" + }, + "node_modules/serialize-error": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz", + "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.13.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sharp": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.0.tgz", + "integrity": "sha512-BqvG5XbwPZ4NV0DK90d86leEECMsoa8bO0nqnKWlBDYxri4GJ7c4EDInaF6q20lTh/mATmnDIKWJFfXnoVfH5g==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.0", + "@img/sharp-darwin-x64": "0.35.0", + "@img/sharp-freebsd-wasm32": "0.35.0", + "@img/sharp-libvips-darwin-arm64": "1.3.0", + "@img/sharp-libvips-darwin-x64": "1.3.0", + "@img/sharp-libvips-linux-arm": "1.3.0", + "@img/sharp-libvips-linux-arm64": "1.3.0", + "@img/sharp-libvips-linux-ppc64": "1.3.0", + "@img/sharp-libvips-linux-riscv64": "1.3.0", + "@img/sharp-libvips-linux-s390x": "1.3.0", + "@img/sharp-libvips-linux-x64": "1.3.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.0", + "@img/sharp-libvips-linuxmusl-x64": "1.3.0", + "@img/sharp-linux-arm": "0.35.0", + "@img/sharp-linux-arm64": "0.35.0", + "@img/sharp-linux-ppc64": "0.35.0", + "@img/sharp-linux-riscv64": "0.35.0", + "@img/sharp-linux-s390x": "0.35.0", + "@img/sharp-linux-x64": "0.35.0", + "@img/sharp-linuxmusl-arm64": "0.35.0", + "@img/sharp-linuxmusl-x64": "0.35.0", + "@img/sharp-webcontainers-wasm32": "0.35.0", + "@img/sharp-win32-arm64": "0.35.0", + "@img/sharp-win32-ia32": "0.35.0", + "@img/sharp-win32-x64": "0.35.0" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5658,6 +5432,22 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "license": "BSD-3-Clause" + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -5692,23 +5482,42 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", - "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, - "node_modules/strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", "license": "MIT", "dependencies": { - "min-indent": "^1.0.0" + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" }, - "engines": { - "node": ">=8" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" } }, "node_modules/suspend-react": { @@ -5727,37 +5536,6 @@ "dev": true, "license": "MIT" }, - "node_modules/tailwind-merge": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", - "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, - "node_modules/tailwindcss": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", - "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/three": { "version": "0.183.2", "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", @@ -5804,9 +5582,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", "dev": true, "license": "MIT", "engines": { @@ -5814,14 +5592,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -5886,14 +5664,24 @@ "node": ">=18" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/troika-three-text": { - "version": "0.52.4", - "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz", - "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==", + "version": "0.52.5", + "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.5.tgz", + "integrity": "sha512-Ry3jRhic9pzcY4JduSvRRyDmVOSqEW19gT4vtK+aCiPNVcDlmkxvGG0YbFd36RTDq1wExOupXnvNF/j1oiHHDA==", "license": "MIT", "dependencies": { "bidi-js": "^1.0.2", - "troika-three-utils": "^0.52.4", + "troika-three-utils": "^0.52.5", "troika-worker-utils": "^0.52.0", "webgl-sdf-generator": "1.1.1" }, @@ -5902,9 +5690,9 @@ } }, "node_modules/troika-three-utils": { - "version": "0.52.4", - "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz", - "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==", + "version": "0.52.5", + "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.5.tgz", + "integrity": "sha512-WsePbcX8RtfidRfsxK1eCZCjF81ZDzAKHH/evLs0hdV2wpoCb0vArGZHdzdOJrSS3k4zfdtbKDaBh8+phkrYnw==", "license": "MIT", "peerDependencies": { "three": ">=0.125.0" @@ -5916,11 +5704,22 @@ "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", "license": "MIT" }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "optional": true }, "node_modules/tunnel-rat": { "version": "0.1.2", @@ -5959,6 +5758,18 @@ } } }, + "node_modules/type-fest": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", + "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -5973,6 +5784,99 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", @@ -6004,49 +5908,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -6065,6 +5926,34 @@ "node": ">= 4" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vite": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", @@ -6140,6 +6029,21 @@ } } }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/vitest": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", @@ -6335,9 +6239,9 @@ } }, "node_modules/ws": { - "version": "8.21.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", - "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "dev": true, "license": "MIT", "engines": { @@ -6380,10 +6284,25 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/zustand": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", - "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.15.tgz", + "integrity": "sha512-MpSEjRiBkA9crSYeOUH32rJC7SVqAbm0Fqcqge/bUi2PPoLcBWKOsG+C8mevmpr8TwXHBVkChbbJiyvkE+i/3A==", "license": "MIT", "engines": { "node": ">=12.20.0" @@ -6408,6 +6327,16 @@ "optional": true } } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/graph-ui/package.json b/graph-ui/package.json index b023560e8..82bce4026 100644 --- a/graph-ui/package.json +++ b/graph-ui/package.json @@ -1,47 +1,89 @@ { - "name": "graph-ui", + "name": "cbm-lens", + "version": "0.0.1", "private": true, - "version": "0.1.0", "type": "module", + "description": "Lokales Verstehens-Frontend auf dem CBM-Stack (localhost, air-gapped per Default).", "scripts": { "dev": "vite", - "build": "tsc -b && vite build", - "preview": "vite preview", - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage" - }, - "dependencies": { - "@react-three/drei": "^10.7.0", - "@react-three/fiber": "^9.5.0", - "@react-three/postprocessing": "^3.0.4", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "lucide-react": "^0.577.0", - "postprocessing": "^6.38.3", - "radix-ui": "^1.4.3", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "tailwind-merge": "^3.5.0", - "three": "~0.183.0" + "build": "tsc -b tsconfig.json tsconfig.node.json && vite build", + "test": "node --test tests/scaffold/*.test.mjs", + "test:acceptance": "node --test tests/scaffold/w*.test.mjs tests/scaffold/release-blockers.test.mjs", + "test:unit": "vitest run", + "verify": "node --test tests/scaffold/*.test.mjs", + "smoke:spike": "node tools/smoke-spike.mjs", + "smoke:w1": "node tools/smoke-w1.mjs", + "smoke:w1b": "node tools/smoke-w1b.mjs", + "smoke:w2a": "node tools/net-deny-gate.mjs --out verification/w2/netdeny.json -- node tools/smoke-w2a.mjs", + "smoke:w2b": "node tools/net-deny-gate.mjs --out verification/w2/netdeny-w2b.json -- node tools/smoke-w2b.mjs", + "smoke:w3": "node tools/net-deny-gate.mjs --out verification/w3/netdeny.json -- node tools/smoke-w3.mjs", + "smoke:w4a": "node tools/net-deny-gate.mjs --out verification/w4/netdeny.json -- node tools/smoke-w4a.mjs", + "smoke:w4b": "node tools/net-deny-gate.mjs --out verification/w4/netdeny-w4b.json -- node tools/smoke-w4b.mjs", + "smoke:w4c": "node tools/net-deny-gate.mjs --out verification/w4/netdeny-w4c.json -- node tools/smoke-w4c.mjs", + "smoke:w4d": "node tools/net-deny-gate.mjs --out verification/w4/netdeny-w4d.json -- node tools/smoke-w4d.mjs", + "smoke:w4e": "node tools/net-deny-gate.mjs --out verification/w4/netdeny-w4e.json -- node tools/smoke-w4e.mjs", + "smoke:w5a": "node tools/net-deny-gate.mjs --out verification/w5/netdeny-w5a.json -- node tools/smoke-w5a.mjs", + "smoke:w5b": "node tools/net-deny-gate.mjs --out verification/w5/netdeny-w5b.json -- node tools/smoke-w5b.mjs", + "smoke:w5c": "node tools/net-deny-gate.mjs --out verification/w5/netdeny-w5c.json -- node tools/smoke-w5c.mjs", + "smoke:w6": "node tools/net-deny-gate.mjs --out verification/w6/netdeny.json -- node tools/smoke-w6-full.mjs", + "smoke:w7a": "node tools/net-deny-gate.mjs --out verification/w7/netdeny.json -- node tools/smoke-w7a.mjs", + "smoke:w7b": "node tools/net-deny-gate.mjs --out verification/w7/netdeny-w7b.json -- node tools/smoke-w7b.mjs", + "smoke:w7c": "node tools/net-deny-gate.mjs --out verification/w7/netdeny-w7c.json -- node tools/smoke-w7c.mjs", + "smoke:w8": "node tools/net-deny-gate.mjs --out verification/w8/netdeny.json -- node tools/smoke-w8.mjs", + "smoke:w8b": "node tools/net-deny-gate.mjs --out verification/w8b/netdeny.json -- node tools/smoke-w8b.mjs", + "smoke:w8c": "node tools/net-deny-gate.mjs --out verification/w8c/netdeny.json -- node tools/smoke-w8c.mjs", + "smoke:w9": "node tools/net-deny-gate.mjs --out verification/w9/netdeny-w9.json -- node tools/smoke-w9.mjs", + "smoke:w10": "node tools/net-deny-gate.mjs --out verification/w10/netdeny.json -- node tools/smoke-w10.mjs", + "smoke:w10b": "node tools/net-deny-gate.mjs --out verification/w10b/netdeny.json -- node tools/smoke-w10b.mjs", + "smoke:w11a": "node tools/net-deny-gate.mjs --out verification/w11/netdeny.json -- node tools/smoke-w11a.mjs", + "smoke:w11b": "node tools/net-deny-gate.mjs --out verification/w11/netdeny-w11b.json -- node tools/smoke-w11b.mjs", + "smoke:w12": "node tools/net-deny-gate.mjs --out verification/w12/netdeny.json -- node tools/smoke-w12.mjs", + "smoke:w12a": "node tools/net-deny-gate.mjs --out verification/w12a/netdeny.json -- node tools/smoke-w12a.mjs", + "smoke:w13": "node tools/net-deny-gate.mjs --out verification/w13/netdeny.json -- node tools/smoke-w13.mjs", + "smoke:w14": "node tools/net-deny-gate.mjs --out verification/w14/netdeny.json -- node tools/smoke-w14.mjs", + "smoke:w15": "node tools/net-deny-gate.mjs --out verification/w15/netdeny.json -- node tools/smoke-w15.mjs", + "demo:record": "node tools/record-demo.mjs", + "gate:release": "node tools/release-gate.mjs", + "check:freshclone": "node tools/freshclone-check.mjs", + "check:style": "node tools/style-gate.mjs", + "check:promises": "node tools/promise-scan.mjs", + "eval:llm": "node tools/eval-llm.mjs", + "eval:check": "node tools/eval-check.mjs", + "verify:models": "node tools/verify-models.mjs" }, "devDependencies": { - "@tailwindcss/vite": "^4.2.1", - "@testing-library/jest-dom": "^6.6.0", - "@testing-library/react": "^16.1.0", - "@types/react": "^19.0.0", - "@types/react-dom": "^19.0.0", - "@types/three": "~0.183.0", - "@vitejs/plugin-react": "^4.3.0", - "jsdom": "^25.0.0", - "tailwindcss": "^4.1.0", - "typescript": "^5.7.0", - "vite": "^6.4.3", - "vitest": "^4.1.11" + "@types/node": "20.19.43", + "@types/react": "19.2.18", + "@types/react-dom": "19.2.5", + "@types/three": "0.183.1", + "@vitejs/plugin-react": "4.7.0", + "jsdom": "25.0.1", + "monaco-editor": "0.56.0", + "playwright": "1.62.1", + "three-stdlib": "2.36.1", + "typescript": "5.9.3", + "vite": "6.4.3", + "vitest": "4.1.11" }, "overrides": { - "form-data": ">=4.0.6", - "@babel/core": ">=7.29.6", - "nanoid": "^3.3.17" + "@huggingface/transformers": { + "sharp": "0.35.0" + }, + "onnxruntime-node@1.24.3": { + "adm-zip": "0.6.0" + } + }, + "dependencies": { + "@huggingface/transformers": "4.2.0", + "@react-three/drei": "10.7.7", + "@react-three/fiber": "9.5.0", + "@react-three/postprocessing": "3.0.4", + "postprocessing": "6.38.3", + "react": "19.2.8", + "react-dom": "19.2.8", + "react-markdown": "10.1.0", + "remark-gfm": "4.0.1", + "three": "0.183.2", + "yaml": "2.8.2" } } diff --git a/graph-ui/spike/index.html b/graph-ui/spike/index.html new file mode 100644 index 000000000..88b0cf122 --- /dev/null +++ b/graph-ui/spike/index.html @@ -0,0 +1,111 @@ + + + + +CodeAtlasWeb W0 Spike: Serverquelle in read-only Monaco + + + + +
+ CODEATLAS W0 SPIKE + quelle /rpc get_code_snippet + symbol ... + datei ... + modus read-only +
+
+
lade ...
+ + + + + diff --git a/graph-ui/spike/spike.js b/graph-ui/spike/spike.js new file mode 100644 index 000000000..eefe2b5f7 --- /dev/null +++ b/graph-ui/spike/spike.js @@ -0,0 +1,210 @@ +// W0-Spike. Wegwerf-Beweiscode, NICHT die Produktarchitektur. +// +// Ablauf: Symbol aus den Query-Parametern (?project=...&qn=...) holen, den +// Quelltext über POST /rpc (MCP tools/call get_code_snippet) beim gebauten +// C-Server abfragen, ihn read-only in Monaco rendern und genau eine +// Decoration auf die Zeile mit dem validateUser(-Aufruf setzen. +// +// Monaco kommt aus /node_modules/monaco-editor/min/vs (AMD-Loader), kein CDN. + +(function () { + 'use strict'; + + var MONACO_VS = '/node_modules/monaco-editor/min/vs'; + var CALL_NEEDLE = 'validateUser('; + + var statusEl = document.getElementById('status'); + + function setStatus(text, failed) { + statusEl.textContent = text; + statusEl.className = failed ? 'failed' : ''; + } + + function fail(stage, err) { + var message = (err && err.message) ? err.message : String(err); + setStatus('FEHLER in ' + stage + ': ' + message, true); + window.__spike = { + ready: false, + stage: stage, + error: message, + monacoReadOnly: false, + decorationCount: 0, + sourceLength: 0 + }; + } + + function params() { + var q = new URLSearchParams(window.location.search); + return { + project: q.get('project') || '', + qn: q.get('qn') || '' + }; + } + + // POST /rpc im MCP-Format. Der Mini-Proxy vor dieser Seite reicht den + // Aufruf serverseitig an den C-Server weiter (dessen Origin-Prüfung + // lässt eine fremde Seite nicht direkt an /rpc). + function callTool(name, args) { + return fetch('/rpc', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'tools/call', + params: { name: name, arguments: args } + }) + }).then(function (res) { + if (!res.ok) { + throw new Error('/rpc antwortete mit HTTP ' + res.status); + } + return res.json(); + }).then(function (body) { + if (body.error) { + throw new Error('/rpc-Fehler: ' + JSON.stringify(body.error)); + } + var content = body.result && body.result.content; + if (!content || !content.length || typeof content[0].text !== 'string') { + throw new Error('/rpc lieferte kein content[0].text'); + } + return JSON.parse(content[0].text); + }); + } + + // Die Call-Site suchen: die Zeile, die validateUser( aufruft, nicht der + // Import und nicht die Deklaration selbst. + function findCallSiteLine(source) { + var lines = source.split('\n'); + for (var i = 0; i < lines.length; i++) { + var line = lines[i]; + if (line.indexOf(CALL_NEEDLE) === -1) { continue; } + var trimmed = line.trim(); + if (trimmed.indexOf('import') === 0) { continue; } + if (/\bfunction\s+validateUser\s*\(/.test(trimmed)) { continue; } + return i + 1; // Monaco zählt Zeilen ab 1 + } + return 0; + } + + function loadMonaco() { + return new Promise(function (resolve, reject) { + if (typeof window.require !== 'function' || !window.require.config) { + reject(new Error('AMD-Loader aus ' + MONACO_VS + '/loader.js fehlt')); + return; + } + window.require.config({ paths: { vs: MONACO_VS } }); + window.require(['vs/editor/editor.main'], function () { + resolve(window.monaco); + }, function (err) { + reject(new Error('Monaco ließ sich nicht laden: ' + (err && err.message ? err.message : String(err)))); + }); + }); + } + + function render(monaco, snippet) { + monaco.editor.defineTheme('atlas-phosphor', { + base: 'vs-dark', + inherit: true, + rules: [], + colors: { + 'editor.background': '#06090b', + 'editorGutter.background': '#0b1013', + 'editorLineNumber.foreground': '#2f4a40', + 'editorLineNumber.activeForeground': '#33ff99' + } + }); + + var editor = monaco.editor.create(document.getElementById('editor'), { + value: snippet.source, + language: 'typescript', + theme: 'atlas-phosphor', + readOnly: true, + domReadOnly: true, + glyphMargin: true, + automaticLayout: true, + minimap: { enabled: false }, + scrollBeyondLastLine: false, + renderLineHighlight: 'none', + fontSize: 14, + lineNumbers: function (n) { + // Zeilennummern der echten Datei, nicht des Ausschnitts. + return String((snippet.start_line || 1) + n - 1); + } + }); + + var callLine = findCallSiteLine(snippet.source); + var ids = []; + if (callLine > 0) { + ids = editor.deltaDecorations([], [{ + range: new monaco.Range(callLine, 1, callLine, 1), + options: { + isWholeLine: true, + className: 'atlas-callsite-line', + glyphMarginClassName: 'atlas-callsite-glyph', + glyphMarginHoverMessage: { value: '1 Aufruf: validateUser' } + } + }]); + } + + return { editor: editor, decorationIds: ids, callLine: callLine }; + } + + function main() { + var p = params(); + if (!p.project || !p.qn) { + fail('parameter', new Error('project und qn sind Pflicht-Query-Parameter')); + return; + } + + document.getElementById('hdr-qn').textContent = p.qn; + setStatus('hole Quelltext über /rpc get_code_snippet ...'); + + var snippet = null; + + callTool('get_code_snippet', { project: p.project, qualified_name: p.qn }) + .then(function (result) { + snippet = result; + if (typeof snippet.source !== 'string' || snippet.source.length === 0) { + throw new Error('get_code_snippet lieferte kein source-Feld'); + } + document.getElementById('hdr-file').textContent = + (snippet.file_path || '?') + ':' + (snippet.start_line || '?'); + setStatus('lade Monaco aus ' + MONACO_VS + ' ...'); + return loadMonaco(); + }) + .then(function (monaco) { + var rendered = render(monaco, snippet); + var readOnly = rendered.editor.getOption(monaco.editor.EditorOption.readOnly); + + window.__spike = { + ready: true, + monacoReadOnly: readOnly === true, + decorationCount: rendered.decorationIds.length, + sourceLength: snippet.source.length, + qualifiedName: snippet.qualified_name || p.qn, + filePath: snippet.file_path || '', + startLine: snippet.start_line || 0, + endLine: snippet.end_line || 0, + callSiteLine: rendered.callLine, + callSiteFileLine: rendered.callLine > 0 + ? (snippet.start_line || 1) + rendered.callLine - 1 + : 0 + }; + + setStatus( + 'ok: ' + snippet.source.length + ' Zeichen vom Server, read-only=' + + (readOnly === true) + ', Decorations=' + rendered.decorationIds.length + + ' auf Datei-Zeile ' + window.__spike.callSiteFileLine + ); + }) + .catch(function (err) { + fail('spike', err); + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', main); + } else { + main(); + } +})(); diff --git a/graph-ui/src/App.test.tsx b/graph-ui/src/App.test.tsx deleted file mode 100644 index f6437c40d..000000000 --- a/graph-ui/src/App.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* @vitest-environment jsdom */ -import "@testing-library/jest-dom/vitest"; -import { cleanup, render, screen, waitFor } from "@testing-library/react"; -import { afterEach, describe, expect, it, vi } from "vitest"; -import { App } from "./App"; -import { messages } from "./lib/i18n"; - -vi.mock("./components/GraphTab", () => ({ GraphTab: () => null })); -vi.mock("./components/StatsTab", () => ({ StatsTab: () => null })); -vi.mock("./components/ControlTab", () => ({ ControlTab: () => null })); -vi.mock("./lib/i18n", async (importOriginal) => { - const actual = await importOriginal(); - return { ...actual, useUiMessages: () => messages.en }; -}); - -describe("App", () => { - afterEach(() => { - cleanup(); - vi.unstubAllGlobals(); - window.history.replaceState(null, "", "/"); - }); - - it("shows the serving binary version", async () => { - vi.stubGlobal("fetch", vi.fn(async () => - new Response(JSON.stringify({ lang: "en", version: "0.10.8" }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }), - )); - - render(); - - expect(await screen.findByText("v0.10.8")).toBeVisible(); - }); - - it("hides the version when the config has no string version", async () => { - const fetchMock = vi.fn(async () => - new Response(JSON.stringify({ lang: "en", version: 108 }), { status: 200 }), - ); - vi.stubGlobal("fetch", fetchMock); - - render(); - - await waitFor(() => expect(fetchMock).toHaveBeenCalledWith("/api/ui-config")); - expect(screen.queryByTitle("Server version")).not.toBeInTheDocument(); - }); - - it("hides the version when the config request fails", async () => { - const fetchMock = vi.fn(async () => { - throw new Error("offline"); - }); - vi.stubGlobal("fetch", fetchMock); - - render(); - - await waitFor(() => expect(fetchMock).toHaveBeenCalledWith("/api/ui-config")); - expect(screen.queryByTitle("Server version")).not.toBeInTheDocument(); - }); -}); \ No newline at end of file diff --git a/graph-ui/src/App.tsx b/graph-ui/src/App.tsx index a0d84b0b6..daebffe35 100644 --- a/graph-ui/src/App.tsx +++ b/graph-ui/src/App.tsx @@ -1,159 +1,5033 @@ -import { useCallback, useEffect, useState } from "react"; -import { GraphTab } from "./components/GraphTab"; -import { StatsTab } from "./components/StatsTab"; -import { ControlTab } from "./components/ControlTab"; -import type { TabId } from "./lib/types"; -import { useUiMessages } from "./lib/i18n"; - -const TAB_IDS: TabId[] = ["graph", "stats", "control"]; - -interface RouteState { - tab: TabId; - project: string | null; +/** + * Die Verdrahtung: Projekt aus der Adresszeile, Baum aus dem Graphen, Datei in + * den Reader. + * + * Hier steht der Zustand und sonst nichts Darstellendes. Das Chrome + * (src/app/AtlasChrome.tsx) bekommt fertige Werte, der Reader + * (src/reader/MonacoReader.tsx) bekommt ein fertiges Dokument. Diese Datei ist + * die einzige, die weiss, dass es einen Server gibt. + * + * Drei Entscheidungen, die man sonst raten muesste: + * + * 1. **Das Projekt kommt aus `?project=`.** Fehlt der Parameter, wird das erste + * Projekt aus `list_projects` genommen. Gibt es keins, sagt die Oberflaeche + * "no project" und zeigt keinen Baum, statt einen leeren zu zeigen: ein + * leerer Baum waere die Behauptung, das Projekt sei leer. + * 2. **Der Baum wird beim Laden aufgeklappt.** `/api/tree` antwortet je Anfrage + * mit einer Ebene, und ein Explorer, der mit zwei zugeklappten Ordnern + * startet, zeigt von einem indizierten Projekt so gut wie nichts. Es wird + * breitenweise nachgeladen, bis ein Deckel erreicht ist; was danach noch + * offen ist, bleibt zugeklappt und laedt beim Klick. Der Deckel wird + * genannt, wenn er greift. + * 3. **Eine Datei kommt ueber ihren Modul-Knoten.** Warum es keinen anderen Weg + * gibt und was das kostet, steht in src/reader/file-source.ts. + * + * Dazu kommt seit W2b die Caret-Strecke des Twins, und die hat drei eigene + * Entscheidungen: + * + * 4. **Aufloesen wird entprellt, Hervorheben nicht.** Ein Caret, der durch eine + * Funktion wandert, soll das Badge sofort mitnehmen, aber nicht bei jedem + * Tastendruck den Graphen fragen. Also wandert `caretLine` sofort und + * `settledCaret` nach 250 ms, und nur das zweite loest auf. + * 5. **Dasselbe Symbol wird nicht zweimal geholt.** Der Cache in + * src/twin/ir-cache.ts schluesselt ueber den qualifizierten Namen und legt + * gleichzeitige Anfragen zusammen; hier kommt nur die Regel dazu, bei + * unveraendertem Subjekt gar nicht erst in den Ladezustand zu gehen, damit + * das Panel beim Wandern des Carets nicht blinkt. + * 6. **Folgen wechselt das Subjekt sofort und laesst den Editor nachkommen.** + * Der Kopf des Panels nennt ab dem Klick das neue Symbol und der Koerper + * sagt, dass gelesen wird. Die Fakten kommen erst, wenn der Caret in der + * Zieldatei aufgeloest ist: eine IR, die aus dem Klickziel statt aus einer + * Aufloesung gebaut waere, traege eine erfundene Symbolart, und "This is a + * piece of code" ueber einer Funktion ist genau die Sorte kleiner Luege, + * gegen die dieses Panel gebaut ist. + * + * Seit W3 kommen die Galaxie und die Bedeutungssuche dazu, mit drei weiteren + * Entscheidungen: + * + * 7. **Der Fokus laeuft in beide Richtungen ueber genau einen Schluessel.** + * Das Twin-Subjekt reist als qualifizierter Name zur Galaxie, die Galaxie + * schickt einen angeklickten Knoten als Ziel zurueck durch denselben + * `followTarget`, den auch der Twin benutzt. Es gibt also keinen zweiten + * Navigationsweg, den man vergessen koennte zu pflegen, und keine zweite + * Stelle, an der ein Symbol identifiziert wird. + * 8. **Ein Klick in die Galaxie behauptet nichts ueber das Symbol.** Er + * oeffnet die Datei an der gemeldeten Zeile; was dort steht, sagt die + * Aufloesung, so wie beim Folgen aus dem Twin. Der Grund ist derselbe wie + * in Entscheidung 6. + * 9. **Getippt wird ohne Anfrage, gefragt wird nach 200 ms Ruhe.** Die + * Kommandozeile ist eine Suche und kein Terminal: sie schickt keine Zeile + * ab, sie zeigt waehrend des Tippens, was der Index kennt. Ohne die + * Entprellung waere jede Taste ein Sweep ueber den Graphen. + * + * Seit W4a kommen die Einstiegsmodi dazu, und mit ihnen vier weitere: + * + * 10. **Die Frage nach dem Warum ist eine Flaeche, kein Dialog.** Sie fuellt + * den leeren Editorbereich, wenn keine Datei offen ist und dieser Browser + * fuer dieses Projekt noch keine Antwort kennt. Ein Modal beim ersten + * Oeffnen wuerde die Oberflaeche und jeden automatisierten Lauf durch sie + * blockieren; das ist die Begruendung des Referenzprojekts und sie gilt + * hier unveraendert. + * 11. **Eine Antwort setzt genau eine Sache und wird dann vergessen.** Sie + * setzt das Leseprofil des Twins (Tiefe und Linsen, Zuordnung in + * why/why-model.ts) und wird gespeichert, damit die Frage nicht wieder + * kommt. Niemand fragt den Intent spaeter zurueck: ein weitergereichter + * Intent waere ein Modus, und ein Modus ist ein zweites Produkt im ersten. + * 12. **Ein Schritt bewegt den Reader ueber dieselbe Strecke wie ein Klick.** + * Ein Symbolschritt geht durch `followTarget`, ein Dateischritt durch + * `openFile`. Es gibt keinen zweiten Navigationsweg fuer Fuehrungen, also + * auch keine zweite Stelle, an der ein Symbol identifiziert wird, und der + * Twin folgt einem Schritt aus demselben Grund, aus dem er einem Klick + * folgt. + * 13. **Die stille Anzeige zaehlt Wege, nie Zeit.** Beim Betreten eines + * Schrittes und beim Folgen einer Zeile wird ein Checklisten-Item als + * besucht vermerkt. Verweildauer ist keine Evidenz, und die Statusleiste + * zeigt gar keinen Zaehler, solange es keine Checkliste zu zaehlen gibt, + * statt "0 of 0" zu behaupten. + * + * Seit W4b sind die beiden letzten Karten der Frage scharf, und mit ihnen + * kommen drei weitere Entscheidungen: + * + * 14. **Der BUG-Assistent uebernimmt das Subjekt des Twins und schiebt + * nichts zurueck.** Er liest neu, wenn ein anderes Symbol vor dem Leser + * steht, und veroeffentlicht selbst nichts: wer zwei Pfade vergleicht, hat + * nicht darum gebeten, dass Reader, Twin und Galaxie unter ihm wegwandern. + * Nur ein Klick auf einen Hop navigiert, und der loest den Namen in diesem + * Moment auf, statt einen gespeicherten Verweis zu benutzen. + * 15. **Ein kaputter Ref wird hier abgelehnt und nicht dort.** `detect_changes` + * nimmt ein unbekanntes `since` kommentarlos an und antwortet ueber den + * Arbeitsbaum. Ein Tippfehler ergaebe also eine plausible Antwort auf eine + * andere Frage, und deshalb prueft `refRejection` die Form im Browser und + * der Aufruf unterbleibt. Der Beweislauf zaehlt die Anfragen am Proxy und + * liest diese Zahl als die Behauptung. + * 16. **Die beiden Flaechen schliessen einander aus.** Beide brauchen den + * ganzen Platz ueber dem Editor, und zwei ganze Antworten uebereinander + * waeren keine. Escape schliesst, was offen ist; das [a]tlas-Menue macht + * beim Oeffnen der einen die andere zu. + * + * Seit W4e reicht diese Datei eine Antwort weiter, die sie bisher wegwarf: + * + * 17. **Der Walk bleibt liegen, solange seine Fuehrung laeuft.** `getClosure` + * liefert einen Graphen; daraus eine Schrittliste zu machen und den Graphen + * zu vergessen hiess, die Tiefe wegzuwerfen, die in ihm steht. Also haelt + * `walk` ihn, und das Graph-Panel zeigt ihn als Hierarchie ab dem + * gewaehlten Einstiegspunkt. Er endet, wenn die Fuehrung endet: ein Bild + * einer beendeten Frage waere eine Antwort, nach der niemand mehr fragt. + */ +import type { JSX, KeyboardEvent } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import AtlasChrome, { CLOSE_COMMAND_SEARCH_EVENT, OPEN_COMMAND_SEARCH_EVENT } from './app/AtlasChrome'; +import { AtlasTreeIndexDetails } from './app/AtlasTree'; +import ArchitecturePanel from './architecture/ArchitecturePanel'; +import AdrWorkspace from './adr/AdrWorkspace'; +import SelectionContextPanel, { type SelectionScope } from './why/SelectionContext'; +import FileImpactSummary from './impact/FileImpactSummary'; +import SourceEvidenceDrawer, { fileQualifiedName, type SourceEvidenceTarget } from './architecture/SourceEvidenceDrawer'; +import { setUiLogProject } from './app/ui-log-install'; +import DiagnosticsPanel from './diagnostics/DiagnosticsPanel'; +import { resolveRepositorySelection, useRepositorySnapshot } from './architecture/repository-source'; +import ActivityPanel from './agents/ActivityPanel'; +import WelcomePanel from './app/WelcomePanel'; +import BrowserChatDock from './browser-ai/BrowserChatDock'; +import { switchKeepingAgent } from './browser-ai/agent-handover'; +import { browserChatHistoryProjectKey } from './browser-ai/chat-history-cache'; +import type { BrowserChatAttachment } from './browser-ai/BrowserChatDock'; +import { browserGraphContext } from './browser-ai/graph-context'; +import { readerChatContext, type ReaderSourceOrigin } from './browser-ai/reader-chat-context'; +import type { BrowserChatContext } from './browser-ai/chat-model'; +import { selectionEvidenceContext } from './galaxy/selection-evidence'; +import SystemWorkspace from './system/SystemWorkspace'; +import type { Workspace, Guidance } from './app/workspace-strings'; +import { allowedWorkspace, initialWorkspace, workspaceStrings as workspaceText } from './app/workspace-strings'; +import { experimentalAgentsEnabled } from './app/feature-flags'; +import type { Chip, MenuExtra, MenuWiring, TabDescriptor } from './app/AtlasChrome'; +import { AtlasApi, TREE_ROUTE } from './app/atlas-api'; +import { ATLAS_BUILD_SUFFIX, ATLAS_VERSION } from './app/build-info'; +import { messages } from './i18n/messages'; +import { + KEY_LISTENER_OPTIONS, + menuShortcutFor, + tourKeyForEvent, +} from './app/keyboard'; +import { markHandled } from './app/key-probe'; +import { WIRED_MENU_SHORTCUTS } from './app/shortcuts'; +import { baseName, pathSegments } from './app/module-qn'; +import HelpOverlay from './help/HelpOverlay'; +import { + directoryPaths, + EMPTY_COVERAGE, + flattenTree, + mergeCoverageIntoLevels, + moveCursor, + parentPath, + treeIntent, +} from './app/tree-model'; +import type { CoverageIndex, CoverageState, TreeLevel, TreeRow } from './app/tree-model'; +import { loadCoverage, loadPathCoverage } from './app/coverage-source'; +import { keepsReadyTwinForTarget } from './app/ready-twin-target'; +import type { CoveragePathAnswer } from './app/tree-model'; +import { + freshnessNote, + freshnessNoteNeeded, + partialFileNote, + readerUnavailableNote, +} from './app/coverage-strings'; +import { RpcIntelligenceClient } from './provider/rpc-client'; +import { CbmRpcProvider, symbolKindOf } from './provider/cbm-rpc-provider'; +import MonacoReader from './reader/MonacoReader'; +import type { ReaderStatus, ReaderSelection } from './reader/MonacoReader'; +import { FileNotReadableError, loadFileDocument, READER_RPC_TOOL } from './reader/file-source'; +import type { ReaderDocument } from './reader/file-source'; +import { badgesForLines } from './core/step-badge-decorator'; +import type { SymbolRef } from './core/focus-protocol'; +import type { SemanticIR } from './core/semantic-ir'; +import { buildIr } from './ir/semantic-ir-builder'; +import { PseudocodeView, type TwinStatus } from './twin/TwinPanel'; +import SelectedCodePanel from './twin/SelectedCodePanel'; +import { loadFileSymbols, type FileSymbolResult } from './twin/selected-code-context'; +import { inspectorChatContext, matchingInspectorIr, type SelectedCodeSnapshot } from './twin/selected-code-snapshot'; +import { IrCache } from './twin/ir-cache'; +import { ATLAS_WORKSPACE_ROOT, twinLocationOf, twinTargetOf, workspacePathOf } from './twin/twin-target'; +import GalaxyPanel from './galaxy/GalaxyPanel'; +import { targetRefOfNode } from './galaxy/galaxy-model'; +import { hierarchySymbolOf, useSelectionHierarchy } from './galaxy/selection-hierarchy'; +import { markedSourceRange, symbolMatchesReader } from './galaxy/reader-graph-focus'; +import type { GraphData, GraphNode } from './galaxy/types'; +import SearchOverlay from './search/SearchOverlay'; +import type { SearchOverlayStatus } from './search/SearchOverlay'; +import { + SEARCH_DEBOUNCE_MS, + SEARCH_MIN_QUERY, + findByMeaning, + searchByMeaning, +} from './search/find-by-meaning'; +import type { RankedHit } from './search/semantic-search'; +import { + commandExamplesFor, + exampleSymbolOf, +} from './search/command-examples'; +import { EMPTY_LOCAL_INDEX, localCandidates, rankLocalCandidates, settledSearchHits } from './search/local-suggestions'; +import type { LocalIndex } from './search/local-suggestions'; +import { + isSearchable, + MAX_SEARCH_ROWS, + moveSelection, + overlayIntent, + searchHeadline, + searchRows, +} from './search/overlay-model'; +import type { SearchRow, SearchRowSource } from './search/overlay-model'; +import { Facet } from './twin/presentation-profile'; +import type { PresentationProfile } from './twin/presentation-profile'; +import type { TwinRow } from './twin/twin-view-model'; +import WhyPanel from './why/WhyPanel'; +import { WHY_MENU_LABEL } from './why/why-model'; +import type { WhyIntent } from './why/why-model'; +import { readWhyAnswer, recordWhyAnswer } from './why/why-store'; +import type { WhyAnswer } from './why/why-store'; +import TourCard from './tours/TourCard'; +import { generateProjectTour } from './tours/tour-source'; +import { entryWalkTour } from './tours/entry-walk'; +import type { ActiveTour } from './tours/entry-walk'; +import { isLastStep, playerIntent, stepMove } from './tours/tour-player'; +import { markableItemId } from './tours/tour-model'; +import { getClosure } from './provider/closure'; +import type { ClosureResult } from './provider/closure'; +import FlowOverlay from './pseudocode/FlowOverlay'; +import { buildFlowView, FLOW_CLOSURE_CAP, FLOW_CLOSURE_DEPTH } from './pseudocode/flow-view'; +import type { FlowView } from './pseudocode/flow-view'; +import { buildImportsGroup } from './pseudocode/imports-group'; +import type { ImportsGroup } from './pseudocode/imports-group'; +import { fileImportsFor } from './pseudocode/imports-source'; +import { buildPseudocode, closureDocumentOf } from './pseudocode/pseudocode-builder'; +import type { PseudocodeDocument, PseudocodeSourceRef } from './pseudocode/pseudocode-builder'; +import { FLOW_LOADING, FLOW_UNAVAILABLE } from './pseudocode/pseudocode-strings'; +import EntryPointDialog from './entry/EntryPointDialog'; +import type { EntrySearchStatus } from './entry/EntryPointDialog'; +import { entryHeadline, entryRows, routeNote } from './entry/entry-model'; +import type { ArchitectureOverviewDto, SymbolSearchHit } from './core/intelligence-provider'; +import { + browserStore, + exploredLabel, + markVisited, + readUnderstanding, + totalMarks, + understandingOf, +} from './checklist/understanding-store'; +import type { UnderstandingRecord } from './checklist/understanding-store'; +import { + TWIN_EMPTY_HINT, + TWIN_EMPTY_MESSAGE, + TWIN_FILE_NOT_INDEXED, + TWIN_FILE_NOT_INDEXED_HINT, + TWIN_LOADING, + TWIN_LOAD_FAILED, +} from './twin/strings'; +import BugWizard from './traces/BugWizard'; +import type { BugWizardStatus } from './traces/BugWizard'; +import { bugPaths, resolveHop } from './traces/bug-paths'; +import type { BugPathNode, BugPathsDto } from './traces/bug-paths'; +import { BUG_WIZARD_MENU_LABEL } from './traces/bug-wizard-strings'; +import { SIDECAR_ORIGIN } from './llm/sidecar'; +import type { CacheModel, SidecarReading, SidecarState } from './llm/sidecar'; +import AtlasChatPanel from './chat/AtlasChatPanel'; +import { askAtlas } from './chat/ask-atlas'; +import type { ChatTurn } from './chat/ask-atlas'; +import { commandIntent } from './chat/command-intent'; +import { + CHAT_HINT_OFF, + CHAT_HINT_READY, +} from './chat/chat-strings'; +import ExplainZone from './layout/ExplainZone'; +import Splitter from './layout/Splitter'; +import { explainTabs } from './layout/explain-tabs'; +import type { ExplainTabId, ExplainTabState } from './layout/explain-tabs'; +import { lineCommandOf } from './layout/layout-command'; +import { + LAYOUT_KEYS, + clampLayout, + clampLayoutValue, + defaultLayout, + layoutBounds, + layoutStorageKey, + readLayout, + sameLayout, + writeLayout, +} from './layout/layout-model'; +import type { LayoutFrame, LayoutKey, LayoutSizes } from './layout/layout-model'; +import { NEIGHBOR_DEPTHS, NEIGHBOR_DEPTH_DEFAULT } from './compiler/fact-recipes'; +import type { NeighborDepth, ObservedFact, SubjectCandidate } from './compiler/fact-recipes'; +import { modelClassOf } from './compiler/card-compiler'; +import type { CardSource } from './compiler/card-compiler'; +import { applyRefinement } from './pseudocode/refine'; +import { readLlmPolicy } from './llm/policy'; +import type { PolicyReading } from './llm/policy'; +import { resolveLlmState } from './llm/llm-state'; +import { llmChipValue } from './llm/strings'; +import SettingsPanel from './settings/SettingsPanel'; +import ConfigPanel from './settings/ConfigPanel'; +import type { SettingsMeasurement } from './settings/SettingsPanel'; +import AddProjectIndexDialog from './projects/AddProjectIndexDialog'; +import ProjectSwitcher from './projects/ProjectSwitcher'; +import type { ProjectsSource } from './projects/ProjectsPanel'; +import { ProjectWindows, useParamInAddress, type ProjectWindow } from './app/project-windows'; +import { MODEL_SUGGESTIONS, fetchCommand } from './settings/model-catalog'; +import { modelKey, readModelPreference, recordModelPreference } from './settings/model-preference'; +import { + DEFAULT_GRAPH_DISPLAY, + displayKey, + isDefaultDisplay, + loadDisplaySettings, + saveDisplaySettings, +} from './galaxy/density'; +import type { GraphDisplaySettings } from './galaxy/density'; +import { bridgePortFromSearch, useAgentStream } from './agents/agent-source'; +import { agentsMenuLabel } from './agents/agent-strings'; + +/** + * Wie viele Ebenen beim Start von selbst nachgeladen werden. + * + * Der Deckel ist da, weil jede Ebene eine eigene Anfrage ist: ein Repository + * mit tausend Verzeichnissen waere sonst tausend Anfragen beim ersten Bild. + * Vierzig Ebenen decken die Fixtures und jedes normale Projekt ab; was darueber + * liegt, klappt der Benutzer selbst auf und die Oberflaeche sagt es ihm. + */ +export const EAGER_LEVEL_BUDGET = 40; + +/** + * Wie lange der Caret stehen muss, bevor gefragt wird. + * + * 250 ms ist die Spanne, in der ein Leser mit den Pfeiltasten durch einen + * Rumpf geht, ohne stehenzubleiben. Kuerzer, und jede Zeile waere eine Frage an + * den Graphen; laenger, und das Panel haengt spuerbar hinter dem Caret. + */ +export const TWIN_CARET_DEBOUNCE_MS = 250; + +/** + * Start with the supported facts at depth two. Runtime and Changes remain + * opt-in because their unavailable states otherwise add noise to the panel. + */ +export const TWIN_PROFILE: PresentationProfile = { + id: 'custom:codeatlasweb-twin', + label: messages.twin.profileLabel, + depth: 2, + facets: [Facet.Logic, Facet.Calls, Facet.Data, Facet.Errors, Facet.Tests], + terminology: 'technical', + conceptCallouts: false, + panels: [], + aiChangeWatcher: false, +}; + +/** + * Die Adressparameter, mit denen die Grenzen des Vorwaerts-Walks von aussen + * gesetzt werden koennen. + * + * Dieselben zwei Namen wie im Referenzprojekt (pseudocode-service.ts), und sie + * sind kein Debug-Schalter: die Karte sagt am Ende eines gekappten Walks, dass + * sie gekappt wurde, und dieser Satz muss an einem echten Lauf zu sehen sein, + * nicht nur in einem Unit-Test. An einem Projekt mit zehn Dateien kappt der + * Vorgabe-Deckel von fuenfzehn Symbolen nichts; mit `?codeatlasClosureCap=3` + * kappt er, und der Beweislauf liest den Satz dort, wo ein Leser ihn saehe. + */ +export const CLOSURE_DEPTH_PARAM = 'codeatlasClosureDepth'; +export const CLOSURE_CAP_PARAM = 'codeatlasClosureCap'; + +/* + * Die sichtbaren Saetze dieser Datei stehen seit W6a im Katalog + * (src/i18n/messages.ts) und werden hier nur noch unter ihren bisherigen Namen + * weitergereicht. Die Namen bleiben, weil sie exportiert sind und anderswo + * gelesen werden; der Wortlaut hat jetzt genau eine Quelle. + */ + +/** Was die Statusleiste sagt, wenn eine Fuehrung nicht zustande kam. */ +export const TOUR_FAILED = messages.tour.failed; + +/** Was in der Kommandozeile steht, wenn nichts gesucht wird. */ +export const COMMAND_HINT_IDLE = messages.command.hintIdle; +/** Was dort steht, waehrend das Fenster offen ist. */ +export const COMMAND_HINT_OPEN = messages.command.hintOpen; +/** Was das Trefferfenster sagt, solange erst ein Zeichen getippt ist. */ +export const COMMAND_ONE_MORE_LETTER = messages.search.oneMoreLetter; +/** Was das Fenster sagt, wenn die Suche selbst gescheitert ist. */ +export const SEARCH_FAILED = messages.search.failed; +/** Was es sagt, wenn ein gewaehlter Treffer keine Datei traegt. */ +export const SEARCH_NO_FILE = messages.search.noFile; + +const emptyLevels = new Map(); +const noBadges: ReturnType = []; +const noHits: RankedHit[] = []; +/** Das Trefferfenster ohne Treffer, als stabile Referenz. */ +const noRows: SearchRow[] = []; + +/** + * Die beiden Griffe, an denen der Beweislauf die Fuehrung und die stille + * Anzeige anfasst. + * + * Absichtlich schmal, wie die Griffe des Readers, des Twins und der Galaxie: + * alles davon steht auch im DOM, der Griff spart dem Lauf das Parsen und macht + * zwei Dinge ueberhaupt messbar, naemlich "dasselbe Dokument zweimal" und "die + * Zahl der Vermerke ist gestiegen". + */ +export interface AtlasTourSeam { + id: string; + kind: 'project' | 'entry'; + title: string; + steps: number; + index: number; + paths: string[]; + titles: string[]; + endNote: string; + /** Das Dokument, wie es gerade gelaufen wird. */ + json: string; + /** Noch einmal erzeugen und serialisieren. Nur fuer die Projekt-Fuehrung. */ + regenerate?: () => Promise; +} + +/** + * Der Griff, an dem der Beweislauf den Coverage-Join anfasst. + * + * Er traegt genau das, was auch im DOM steht, und spart dem Lauf das Parsen. + * Die Zeilen kommen mit ihrer Stufe, weil "der Baum zeigt exakt das Gemessene" + * sonst nur ueber Screenshot-Vergleiche pruefbar waere, und ein Bild ist keine + * Behauptung ueber Pfade. + */ +export interface AtlasCoverageSeam { + rows: { path: string; kind: string; coverage: string; reason: string; sources: string[] }[]; + /** Je Pfad genau ein Befund, so wie der Join ihn haelt. */ + records: { path: string; kind: string; state: string; reason: string; sources: string[] }[]; + truncations: string[]; + counts: CoverageIndex['counts']; + /** Die Metadaten des Coverage-Stores, inklusive der ignored_files-Zahlen. */ + metadata: Record; + /** Was ueber die offene Datei gerade bekannt ist. */ + open: { path: string; status: string; freshness: string; action: string; note: string } | undefined; + /** Leer, solange nichts schiefging. */ + error: string; +} + +export interface AtlasChecklistSeam { + /** Das Symbol, ueber das die Statusleiste gerade zaehlt. Leer, wenn keines. */ + symbol: string; + explored: number; + total: number; + /** Genau das, was in der Statusleiste steht. Leer heisst: kein Zaehler. */ + label: string; + /** Vermerke dieses Projekts insgesamt. Die eine Zahl, die nur steigt. */ + marks: number; +} + +/** + * Der Griff, an dem der Beweislauf das lokale Modell anfasst. + * + * Er traegt zwei Dinge, die im DOM nicht stehen und ohne die der Lauf raten + * muesste: das Urteil der Policy samt Begruendung, und die Zahl der Proben, die + * ueberhaupt losgeschickt wurden. Die zweite ist der Gegenbeweis zur ersten + * Behauptung dieses Zyklus: solange das LLM aus ist, bleibt sie null, und der + * Netz-Mitschnitt sagt dasselbe aus der anderen Richtung. + */ +/** + * Der Anfangswert der Probe: nichts gefragt, nichts gefunden. + * + * Als Konstante und nicht als Literal an drei Stellen: er wird beim ersten Bild + * gesetzt, beim Projektwechsel und beim Ausschalten, und drei Fassungen davon + * waeren drei Gelegenheiten, eine zu vergessen. + */ +const EMPTY_SIDECAR_READING: SidecarReading = { + state: 'not-running', + router: false, + models: [], + detail: '', +}; + +/** Eine leere Liste mit fester Identitaet, damit sie keine Effekte anstoesst. */ +const NO_CACHE_MODELS: readonly CacheModel[] = []; + +export interface AtlasLlmSeam { + state: SidecarState; + /** Ob dieser Browser fuer dieses Projekt eingeschaltet hat. */ + preferenceOn: boolean; + /** Das Urteil der committeten Policy. Leer, solange noch nicht gelesen. */ + policyVerdict: string; + policyPath: string; + policyDetail: string; + /** Der Modellname, so wie er im Panel steht. Leer, wenn keiner bekannt ist. */ + model: string; + /** Was in der Statusleiste steht. */ + chip: string; + /** Wie oft ueberhaupt zum Sidecar gefragt wurde, seit die Seite geladen ist. */ + probes: number; +} + +/** + * Der Griff, an dem der Beweislauf den Chat und die Umformulierung anfasst. + * + * Zwei Dinge stehen hier, die im DOM nicht stehen und ohne die ein Lauf raten + * muesste: die Kartennummern, die eine Antwort wirklich zitiert hat (im Text + * stehen sie als Knoepfe, aber nicht die Liste der gegebenen Karten daneben), + * und `validateRefine`. Das zweite ist kein Nachbau: es ruft denselben + * Validator, den der Knopf ruft, mit einer absichtlich kaputten Antwort. Ein + * Lauf, der die Regel nachbaut, prueft seine Kopie und nicht das Produkt. + */ +export interface AtlasChatSeam { + turns: { + id: number; + question: string; + status: string; + klass: string; + rule: string; + depth: number; + cards: number; + cardIds: string[]; + sources: number; + citations: string[]; + unknownCitations: string[]; + uncitedLines: number; + answer: string; + refusal: string; + tokens: number; + budget: number; + tokensPerSecond: number; + /** Die Kandidaten, wenn dieser Zug auf eine Wahl wartet. */ + candidates: { name: string; qualifiedName: string; filePath: string; line: number }[]; + /** Der Name, der mehrdeutig war oder nicht gefunden wurde. Leer, wenn keiner. */ + askedName: string; + /** Das Fokus-Symbol, das statt eines nicht gefundenen Namens geantwortet hat. */ + focusFallbackUsed: string; + }[]; + /** + * Ob der Chat gerade sichtbar ist. + * + * Seit W8 heisst das: der Erklaeren-Bereich ist offen UND sein Reiter ist + * `chat`. Eingeklappt oder auf einem anderen Reiter bleiben die Zuege + * trotzdem stehen, und genau das ist die Zusicherung, die W7c erkaempft hat + * und die hier weiter gemessen wird. + */ + open: boolean; + /** Die Hoehe, die der Chat gerade hat. Seit W8 ist es die Hoehe seiner Zone. */ + height: number; + /** Der Deckel, den dieses Fenster gerade zulaesst. Ebenfalls der der Zone. */ + maxHeight: number; + depth: number; + depthDefault: number; + depthOptions: number[]; + refineAvailable: boolean; + refineState: string; + refineMessage: string; + validateRefine: (answer: string) => { applied: boolean; reason: string }; +} + +/** + * Der Griff, an dem der Beweislauf die Suche und die Menuezeile anfasst. + * + * Er traegt genau das, was im DOM nicht steht und was ein Lauf sonst aus + * Wartezeiten raten muesste: wie lange der Serverweg gedauert hat, wie oft ein + * Verlaengern des Wortes aus dem Praefix-Cache beantwortet wurde, wie oft eine + * ueberholte Runde abgebrochen wurde, und ob je eine ueberholte Antwort die + * neuere ueberschrieben hat. Die letzte Zahl ist die einzige, die falsch sein + * darf: sie ist der Gegenbeweis zur Zusicherung dieses Zyklus und muss null + * bleiben. + * + * `activatedMenus` ist die Liste der Menuepunkte, die wirklich GELAUFEN sind, + * je Buchstabe. Sie wird an der Stelle geschrieben, an der die Handlung + * ausgefuehrt wird, und nicht dort, wo geklickt wurde: ein Lauf, der einen + * Klick zaehlt, hat den Klick bewiesen und nicht die Handlung dahinter. + */ +export interface AtlasSearchSeam { + /** Die Entprellung, wie sie wirklich gilt. */ + debounceMs: number; + minQuery: number; + /** Was gerade in der Zeile steht. */ + currentQuery: string; + /** Die Anfrage, die die gezeigten Zeilen beantworten. */ + shownQuery: string; + /** Woher die gezeigten Zeilen kommen. */ + shownSource: SearchRowSource; + shownRows: number; + /** Wie oft vorlaeufige Zeilen VOR einer Antwort des Index dastanden. */ + localFirst: number; + /** Wie oft ein verlaengertes Wort aus dem Praefix-Cache beantwortet wurde. */ + prefixCacheHits: number; + /** Runden, die wirklich zum Server gingen. */ + serverRequests: number; + /** Der letzte gemessene Serverweg, in Millisekunden. */ + serverRoundtripMs: number; + /** Jeder gemessene Serverweg dieser Sitzung. */ + roundtrips: number[]; + /** Runden, die abgebrochen wurden, weil ein Tastendruck sie ueberholt hat. */ + abortedRequests: number; + /** Antworten, die zu spaet kamen und deshalb verworfen wurden. */ + staleDropped: number; + /** Muss false bleiben: eine ueberholte Antwort hat die neuere ueberschrieben. */ + staleAnswerWins: boolean; + /** Kandidaten, die ohne Serverweg zur Verfuegung stehen. */ + localCandidates: number; + /** Die Buchstaben der Menuepunkte, deren Handlung gelaufen ist, in ihrer Reihenfolge. */ + activatedMenus: string[]; + /** Ob der Griff am Fenster in der einfangenden Phase haengt. */ + keyListenerCapture: boolean; } -/* Read the active tab + selected project from the URL query string so the - * current view survives refreshes and can be bookmarked or shared. */ -function readRoute(): RouteState { - const params = new URLSearchParams(window.location.search); - const rawTab = params.get("tab"); - const tab = TAB_IDS.includes(rawTab as TabId) ? (rawTab as TabId) : "stats"; - const project = params.get("project"); - return { tab, project: project ? project : null }; +/** + * Der Griff, an dem der Beweislauf das Layout anfasst. + * + * Er traegt drei Dinge, die im DOM nicht oder nur als Pixel stehen, und jedes + * davon ist eine Frage, die ein Lauf sonst raten muesste: + * + * - **Die Zahlen mit ihren Grenzen.** Ein Lauf kann ein Rechteck messen; ob + * das gemessene Rechteck am Anschlag steht oder nur zufaellig so gross ist, + * sagt ihm erst `bounds`. Genau daran haengt AC2 ("keine Zone darf + * wegziehbar sein"): gemessen wird gegen die Grenze, die die Oberflaeche + * selbst nennt, und nicht gegen eine im Lauf abgeschriebene Zahl. + * - **Der Zustand hinter den Reitern.** AC5 verlangt, dass ein Reiterwechsel + * keinen Zustand kostet. Der Chat-Verlauf steht im DOM, der Walk-Schritt und + * die Flow-Stelle stehen dort nur, solange ihr Reiter offen ist. Hier stehen + * alle drei jederzeit, und deshalb kann ein Lauf sie VOR und NACH einem + * Wechsel vergleichen, statt zu behaupten, sie seien noch da. + * - **Der Schluessel im Speicher.** Damit ein Lauf pruefen kann, dass die + * Masse pro Projekt gefuehrt werden, ohne den Schluessel nachzubauen. + */ +export interface AtlasLayoutSeam { + /** Die Masse, die WIRKLICH gezeichnet werden. Gegen sie misst der Lauf Rechtecke. */ + sizes: LayoutSizes; + /** + * Die Masse, die der Leser gezogen hat. + * + * Sie koennen groesser sein als `sizes`: ein kleines Fenster begrenzt die + * Darstellung und nicht den Wunsch. Ohne diese zweite Zahl saehe ein Lauf + * nur, dass eine Spalte schmal ist, und nicht, ob sie es bleiben wird. + */ + requested: LayoutSizes; + bounds: Record; + defaults: LayoutSizes; + /** Ob alle vier Zahlen auf ihrer Vorgabe stehen. */ + isDefault: boolean; + frame: LayoutFrame; + explainOpen: boolean; + explainTab: string; + tabs: { id: string; enabled: boolean; reason: string; note: string }[]; + storageKey: string; + /** Was hinter den Reitern liegt, unabhaengig davon, welcher offen ist. */ + state: { + chatTurns: number; + walkStep: number; + walkSteps: number; + flowStep: number; + flowSteps: number; + }; +} + +/** + * Der Griff, an dem der Beweislauf das Einstellungen-Panel anfasst (W10). + * + * Er traegt drei Dinge, die im DOM nicht oder nur als Text stehen, und jedes + * davon ist eine Frage, die ein Lauf sonst raten muesste: + * + * - **Die Wahl und ihr Speicherort.** Ob eine Wahl den Reload ueberlebt, kann + * ein Lauf nur pruefen, wenn er den Schluessel kennt, ohne ihn nachzubauen. + * - **Was der Prozess wirklich gemeldet hat.** Die Liste der Cache-Modelle + * steht auch im DOM, aber ohne die Angabe, ob der Prozess ein Router ist, + * und genau daran haengt, ob eine Auswahl ueberhaupt wirkt. + * - **Die letzten Messungen.** Sie stehen als Saetze auf dem Schirm; hier + * stehen sie als Zahlen, mit dem Rauschband daneben, damit ein Lauf das + * Urteil nachrechnen kann, statt einen Satz zu lesen. + */ +export interface AtlasSettingsSeam { + open: boolean; + /** Ob das lokale Modell an ist. Ist es aus, hat dieses Panel nichts gefragt. */ + llmOn: boolean; + /** Wie oft ueberhaupt zum Sidecar gefragt wurde. Dieselbe Zahl wie in __atlasLlm. */ + probes: number; + /** Ob der Prozess als Router ueber ein Cache-Verzeichnis laeuft. */ + router: boolean; + /** Die id, an die die naechste Frage geht. Leer heisst: keine Wahl getroffen. */ + selectedModel: string; + /** Was `/v1/models` gelistet hat, mit der aktiven Zeile markiert. */ + cacheModels: { id: string; name: string; loaded: boolean; status: string; active: boolean }[]; + /** Die vier Zahlen des laufenden Modells, so wie das Panel sie zeigt. */ + running: { + model: string; + modelPath: string; + quantization: string; + contextTokens: number; + trainedContextTokens: number; + weightsBytes: number; + parameters: number; + }; + /** Die kuratierten Vorschlaege mit ihren gemessenen Zahlen und ihrem Befehl. */ + suggestions: { + id: string; + name: string; + repo: string; + modelClass: string; + passRate: number; + citationCompliance: number; + /** `null` heisst: der aufgezeichnete Lauf weist die Zahl nicht aus. */ + citationUnmeasured: number | null; + tokensPerSecond: number; + bytes: number; + command: string; + }[]; + /** Die Anzeige-Einstellungen, ihre Vorgabe und ob beides dasselbe ist. */ + display: GraphDisplaySettings; + displayDefault: GraphDisplaySettings; + isDefaultDisplay: boolean; + /** Die zwei Schluessel im Speicher dieses Browsers. */ + storageKeys: { model: string; display: string }; + /** Die letzten abgeschlossenen Messungen, eine je Einstellung. */ + measurements: SettingsMeasurement[]; } -/* Build the canonical URL for a route, preserving the path and hash. */ -function routeUrl(tab: TabId, project: string | null): string { - const params = new URLSearchParams(); - params.set("tab", tab); - if (project) params.set("project", project); - return `${window.location.pathname}?${params.toString()}${window.location.hash}`; +declare global { + // eslint-disable-next-line no-var + var __atlasSettings: AtlasSettingsSeam | undefined; + // eslint-disable-next-line no-var + var __atlasLayout: AtlasLayoutSeam | undefined; + // eslint-disable-next-line no-var + var __atlasSearch: AtlasSearchSeam | undefined; + // eslint-disable-next-line no-var + var __atlasChat: AtlasChatSeam | undefined; + // eslint-disable-next-line no-var + var __atlasTour: AtlasTourSeam | undefined; + // eslint-disable-next-line no-var + var __atlasChecklist: AtlasChecklistSeam | undefined; + // eslint-disable-next-line no-var + var __atlasCoverage: AtlasCoverageSeam | undefined; + // eslint-disable-next-line no-var + var __atlasLlm: AtlasLlmSeam | undefined; } -export function App() { - const t = useUiMessages(); - const [route, setRoute] = useState(readRoute); - const [version, setVersion] = useState(null); - const { tab: activeTab, project: selectedProject } = route; - - useEffect(() => { - let cancelled = false; - void fetch("/api/ui-config") - .then((response) => (response.ok ? response.json() : null)) - .then((config) => { - if (!cancelled && typeof config?.version === "string" && config.version) { - setVersion(config.version); - } - }) - .catch(() => {}); - return () => { - cancelled = true; - }; - }, []); - - /* Normalize the URL on first load so it always carries the current route. */ - useEffect(() => { - const initial = readRoute(); - window.history.replaceState(null, "", routeUrl(initial.tab, initial.project)); - }, []); - - /* Sync state when the user navigates with the browser back/forward buttons. */ - useEffect(() => { - const onPopState = () => setRoute(readRoute()); - window.addEventListener("popstate", onPopState); - return () => window.removeEventListener("popstate", onPopState); - }, []); - - /* Change the route and push a history entry (skips no-op navigations). */ - const navigate = useCallback((tab: TabId, project: string | null) => { - const url = routeUrl(tab, project); - const current = `${window.location.pathname}${window.location.search}${window.location.hash}`; - if (url === current) return; - window.history.pushState(null, "", url); - setRoute({ tab, project }); - }, []); - - const tabs: { id: TabId; label: string }[] = [ - { id: "graph", label: t.tabs.graph }, - { id: "stats", label: t.tabs.projects }, - { id: "control", label: t.tabs.control }, - ]; - - return ( -
- {/* Header */} -
-
-
-
- - Codebase Memory - - {version && ( - - {version.startsWith("v") ? version : `v${version}`} - +/** Was ein Projektfenster dem naechsten mitgibt (K24): den Arbeitsbereich, die Kopfzeile des Agenten und ob der Chat offen ist. */ +interface WindowCarry { + workspace: Workspace; + chatOpen: boolean; + agentState: 'off' | 'loading' | 'active' | 'busy' | 'error'; + agentModelName: string; + agentHasOpened: boolean; +} + +/** + * Jedes Projekt bekommt ein frisches Fenster in derselben Seite (K24, Kopf von + * src/app/project-windows.tsx). Der Agent reicht beim Wechsel sein geladenes + * Modell an das Fenster des neuen Projekts weiter. Die Adresse behaelt dabei + * den Arbeitsbereich und die Grenzen des Walks, nichts vom alten Projekt (A5). + */ +const PAGE_PARAMS = ['workspace', CLOSURE_DEPTH_PARAM, CLOSURE_CAP_PARAM] as const; +export default function App(): JSX.Element { + return around={switchKeepingAgent} keepParams={PAGE_PARAMS}> + {shown => } + ; +} + +function ProjectWindowView({ projectWindow }: { projectWindow: ProjectWindow }): JSX.Element { + const carried = projectWindow.carried; + const [workspace, setWorkspace] = useState(() => { + try { + // A project switch keeps the workspace in view; Back and Forward restore the one of their entry (K24). + const requested = new URLSearchParams(window.location.search).get('workspace') ?? carried?.workspace ?? null; + let saved: string | null = null; + try { saved = localStorage.getItem('cbm.workspace'); } catch { /* URL navigation works without storage. */ } + return initialWorkspace(requested, saved); + } catch { return 'architecture'; } + }); + const changeWorkspace = (value: Workspace): void => { + value = allowedWorkspace(value); + if (value !== workspace) { + setProactiveSelection(undefined); setChatGraphSelection(undefined); setChatAttachment(undefined); + } + setWorkspace(value); + try { localStorage.setItem('cbm.workspace', value); } catch { /* Session-only preference. */ } + }; + // The address always names the workspace in view, also one carried over a project switch, so a reload keeps it (A5). + useParamInAddress('workspace', workspace); + const [welcomeOpen, setWelcomeOpen] = useState(() => { + try { return localStorage.getItem('cbm.workspace.setup') !== 'done'; } catch { return false; } + }); + // The agent's lamp and the chat stay as they were across a project switch (K24). + const [browserAiOpen, setBrowserAiOpen] = useState(carried?.chatOpen ?? false); + const [localAgentState, setLocalAgentState] = useState<'off' | 'loading' | 'active' | 'busy' | 'error'>(carried?.agentState ?? 'off'); + const [agentSettingsRequest, setAgentSettingsRequest] = useState(0); + const [agentModelName, setAgentModelName] = useState(carried?.agentModelName ?? ''); + const agentHasOpened = useRef(carried?.agentHasOpened ?? false); + useEffect(() => { + projectWindow.report({ workspace, chatOpen: browserAiOpen, agentState: localAgentState, agentModelName, agentHasOpened: agentHasOpened.current }); + }); + const onAgentStateChange = useCallback((state: 'off' | 'loading' | 'active' | 'busy' | 'error') => { + setLocalAgentState(state); + if ((state === 'active' || state === 'busy') && !agentHasOpened.current) { + agentHasOpened.current = true; setBrowserAiOpen(true); + } + }, []); + const openAgentSettings = () => { setAgentSettingsRequest(value => value + 1); }; + const [codeDetailsOpen, setCodeDetailsOpen] = useState(false); + const [readerSelection, setReaderSelection] = useState(); + const [pinnedCode, setPinnedCode] = useState(); + const [fileSymbols, setFileSymbols] = useState({ project: '', path: '', symbols: [], message: '', status: 'ready' }); + const [chatAttachment, setChatAttachment] = useState(); + const [chatGraphSelection, setChatGraphSelection] = useState(); + const [proactiveSelection, setProactiveSelection] = useState<{ scope: string; context: BrowserChatContext }>(); + const [galaxySelection, setGalaxySelection] = useState(); + const [galaxySelectionProject, setGalaxySelectionProject] = useState(''); + const finishSetup = (): void => { + setWelcomeOpen(false); + setWhyDismissed(true); + try { localStorage.setItem('cbm.workspace.setup', 'done'); } catch { /* Session-only preference. */ } + }; + const guidance: Guidance = 'brief'; + // Requests of this window end with it: a switch to another project aborts them (K24). + const client = useMemo(() => new RpcIntelligenceClient({ signal: projectWindow.signal }), [projectWindow.signal]); + const api = useMemo(() => new AtlasApi({ signal: projectWindow.signal }), [projectWindow.signal]); + const [diagnosticsPath, setDiagnosticsPath] = useState(); + + /* + * What the projects panel asks the server. The list comes over /rpc like + * every other read of this window; the rest are the /api routes the panel + * names in its own text. Opening a project replaces this window with a + * fresh one for that project in the same page, because every panel of this + * window is keyed to the project it started with (K24). + */ + const projectsSource = useMemo( + () => ({ + listProjects: () => client.listProjects().then((result) => result.projects), + projectHealth: (name) => api.projectHealth(name), + deleteProject: (name) => api.deleteProject(name), + browse: (path) => api.browse(path), + startIndex: (rootPath, projectName) => api.startIndex(rootPath, projectName), + indexJobs: () => api.indexJobs(), + adr: (name) => api.adr(name), + saveAdr: (name, content) => api.saveAdr(name, content), + logs: (lines) => api.logs(lines), + processes: () => api.processes(), + uiLogTail: (lines) => api.uiLogTail(lines), + }), + [client, api], + ); + // Opening a project stays in this page and keeps a loaded agent (K24). + const openProject = projectWindow.open; + + const [project, setProject] = useState(''); + const selectionScope = `${project}:${workspace}`; + const onSelectionEvidence = useCallback((context: BrowserChatContext | undefined) => { + setProactiveSelection(previous => context + ? previous?.scope === selectionScope && previous.context.text === context.text ? previous : { scope: selectionScope, context } + : undefined); + }, [selectionScope]); + useEffect(() => { setProactiveSelection(undefined); setChatGraphSelection(undefined); setChatAttachment(undefined); }, [project]); + const [projectDetail, setProjectDetail] = useState('resolving project ...'); + const [serverOk, setServerOk] = useState(undefined); + const [counts, setCounts] = useState<{ nodes?: number; edges?: number }>({}); + + const [levels, setLevels] = useState>(emptyLevels); + const [expanded, setExpanded] = useState>(new Set()); + const [budgetHit, setBudgetHit] = useState(false); + const [treeError, setTreeError] = useState(''); + const [cursor, setCursor] = useState(0); + + /* + * Der Coverage-Befund: die zweite Haelfte der Baumquelle. + * + * `EMPTY_COVERAGE` heisst "noch nicht gefragt" und nicht "keine Luecken"; + * der Unterschied steht in der Notiz unter dem Baum. Ein gescheiterter Lauf + * wird darum ausdruecklich gemeldet, statt einen leeren Befund zu setzen. + */ + const [coverage, setCoverage] = useState(EMPTY_COVERAGE); + const [coverageMeta, setCoverageMeta] = useState>({}); + const [coverageAsked, setCoverageAsked] = useState(false); + const [coverageError, setCoverageError] = useState(''); + /** Was der Store ueber die offene Datei sagt, mit Frische. */ + const [openCoverage, setOpenCoverage] = useState(undefined); + + const [tabs, setTabs] = useState([]); + const [activePath, setActivePath] = useState(''); + const [document, setDocument] = useState(undefined); + const [documentOrigin, setDocumentOrigin] = useState(); + const [readerStatus, setReaderStatus] = useState('idle'); + const [readerMessage, setReaderMessage] = useState('pick a file in the explorer'); + const [command, setCommand] = useState(''); + + useEffect(() => { setPinnedCode(undefined); }, [project]); + useEffect(() => { + if (!project || !activePath) return; + let cancelled = false; + setFileSymbols({ project, path: activePath, symbols: [], message: '', status: 'loading' }); + void loadFileSymbols(client, project, activePath).then(result => { + if (!cancelled) setFileSymbols({ ...result, project, path: activePath, status: 'ready' }); + }).catch((error: unknown) => { + if (!cancelled) setFileSymbols({ project, path: activePath, symbols: [], status: 'error', + message: error instanceof Error ? error.message : String(error) }); + }); + return () => { cancelled = true; }; + }, [client, project, activePath]); + + // Zaehler gegen Wettlaeufe: klickt jemand schnell durch zwei Dateien, darf + // die langsamere Antwort die schnellere nicht ueberschreiben. + const loadTicket = useRef(0); + + // Der Coverage-Befund als Ref, damit `openFile` ihn lesen kann, ohne bei + // jeder neuen Antwort seine Identitaet zu wechseln: an `openFile` haengen + // die Fuehrungen, der Twin und die Galaxie, und ein Wechsel dort haengt + // eine Handvoll Effekte neu auf. + const coverageRef = useRef(EMPTY_COVERAGE); + coverageRef.current = coverage; + const coverageTicket = useRef(0); + + // ------------------------------------------------------- Twin ---------- + + const provider = useMemo(() => new CbmRpcProvider(client, { generation: 1 }), [client]); + // Das Profil ist seit W4a beweglich: die Antwort auf "warum bist du hier" + // setzt es. Die Regler des Lesers liegen als Overlay darauf, so wie vorher, + // und werden beim Profilwechsel geleert: eine abgeschaltete Linse, die ueber + // ein neues Profil hinweg abgeschaltet bliebe, waere eine Sektion, die + // fehlt, ohne dass jemand sagt warum. + + const [twinSymbol, setTwinSymbol] = useState(undefined); + const [twinIr, setTwinIr] = useState(undefined); + const [twinStatus, setTwinStatus] = useState('empty'); + const [twinMessage, setTwinMessage] = useState(TWIN_EMPTY_MESSAGE); + const [twinHint, setTwinHint] = useState(TWIN_EMPTY_HINT); + const [twinName, setTwinName] = useState('no symbol'); + const [caretLine, setCaretLine] = useState(undefined); + const [settledCaret, setSettledCaret] = useState(undefined); + const [pointedLine, setPointedLine] = useState(undefined); + const [reveal, setReveal] = useState<{ line: number; nonce: number } | undefined>(undefined); + + /* + * Der Erklaeren-Bereich: welcher Reiter gilt, und ob der Bereich offen ist. + * + * Zwei Angaben und nicht fuenf Schalter, und das ist der ganze Umbau von W8. + * Bis dahin hatte jede der fuenf Flaechen ihren eigenen "offen"-Zustand, und + * genau daraus entstand der Nutzerbefund vom 2026-08-29: drei davon standen + * gleichzeitig auf `true`, lagen uebereinander und waren alle drei + * angeschnitten. Mit EINEM Reiter kann das nicht mehr vorkommen; es ist + * keine Regel, die jemand einhalten muss, sondern eine Form, in der der + * Fehler nicht formulierbar ist. + * + * Eingeklappt per Vorgabe (AC4): eine Flaeche, die beim Oeffnen eines + * Projekts von selbst aufgeht, erklaert etwas, wonach niemand gefragt hat. + */ + const [explainOpen, setExplainOpen] = useState(false); + const [explainTab, setExplainTab] = useState('flow'); + /* + * Der aktive Reiter, auch fuer die Rueckrufe, die ihn nicht als Abhaengigkeit + * haben duerfen. Derselbe Umweg wie bei `menuActionsRef` und aus demselben + * Grund: ein Griff, der bei jedem Reiterwechsel ab- und wieder angemeldet + * wird, ist ein Griff, der zwischendurch taub ist. + */ + const explainTabRef = useRef('flow'); + explainTabRef.current = explainTab; + /* + * Die Reiter, wie sie das letzte Bild gezeichnet hat, fuer den Griff des + * Beweislaufs. Sie entstehen unten aus Tatsachen; hier steht nur die + * Ablage, damit der Effekt, der den Griff schreibt, sie sehen kann. + */ + const explainTabsRef = useRef([]); + /* + * Ob der Flow gerade zu sehen ist. + * + * Abgeleitet und nicht gespeichert: ein zweiter Schalter neben dem Reiter + * waere die Stelle, an der beide auseinanderlaufen. Der Schritt selbst steht + * auf -1, weil "noch keiner" kein Schritt ist; eine 0 hier waere der erste + * Schritt und wuerde beim Oeffnen ungefragt den Editor bewegen. + */ + const flowOpen = explainOpen && explainTab === 'flow'; + /* + * Die Hilfeseite (W7a). + * + * Zu per Vorgabe, aus demselben Grund wie der Erklaerer: eine Flaeche ueber + * dem Editor geht nicht ungefragt auf. Ein Zustand und kein Router-Pfad, + * weil diese Oberflaeche keine Adressen hat, hinter denen Seiten liegen; sie + * hat Flaechen, die auf- und zugehen. + */ + const [helpOpen, setHelpOpen] = useState(false); + /* + * Das Einstellungen-Panel (W10), aus denselben Gruenden zu per Vorgabe wie + * die Hilfe: eine Flaeche ueber dem Editor geht nicht ungefragt auf. + */ + const [settingsOpen, setSettingsOpen] = useState(false); + const [projectsOpen, setProjectsOpen] = useState(false); + const [indexActivity, setIndexActivity] = useState<{ name: string; status: 'indexing' | 'done' | 'error' }>(); + /* + * Die Anzeige-Einstellungen des Lesers. + * + * Sie stehen HIER und nicht im Panel, weil zwei Flaechen sie brauchen: das + * Panel stellt sie ein, das Graph-Panel zeichnet danach. Ein Zustand im + * Einstellungen-Panel waere weg, sobald jemand es zuklappt, und genau das + * darf eine Einstellung nicht sein. + */ + const [display, setDisplay] = useState(DEFAULT_GRAPH_DISPLAY); + /* + * Der Live-Modus der Agenten (W11a). + * + * AUS per Vorgabe, und er wird auch nicht gespeichert. Dieselbe Regel wie + * beim lokalen Modell: solange er aus ist, geht keine einzige Agentenabfrage an den + * Daemon, und eine Seite, die nach dem naechsten Laden von selbst wieder zu + * reden anfaengt, waere ein Schalter, ueber den der Leser nur einmal + * entschieden hat. Er steht hier und nicht im Graph-Panel, weil ihn zwei + * Wege umlegen: der Menuepunkt und die Kommandozeile. + */ + const [liveAgentsOn, setLiveAgentsOn] = useState(false); + /* + * Die Bitte um das Vollbild, als Zaehler (W11b). + * + * Dieselbe Form wie `graphRefit`: die Wahl selbst liegt im Panel, weil sie + * dort gespeichert wird; was von hier kommt, ist die Bitte. Ein zweiter Ort + * fuer denselben Zustand waere zwei Wahrheiten darueber, ob das Bild gerade + * das ganze Fenster fuellt. + */ + const [fullscreenToggle, setFullscreenToggle] = useState(0); + // Compatibility prop for existing HUD consumers; the stream uses this daemon origin. + const bridgePort = useMemo( + () => bridgePortFromSearch(typeof location === 'undefined' ? '' : location.search), + [], + ); + const agentStream = useAgentStream({ on: experimentalAgentsEnabled && liveAgentsOn, project }); + const agents = useMemo( + () => ({ ...agentStream, port: bridgePort, on: experimentalAgentsEnabled && liveAgentsOn }), + [agentStream, bridgePort, liveAgentsOn], + ); + /** Die letzten Messungen des Panels, je Einstellung. Nur fuer die Naht. */ + const [measurements, setMeasurements] = useState>({}); + const [flowView, setFlowView] = useState(undefined); + const [flowStep, setFlowStep] = useState(-1); + const [flowMessage, setFlowMessage] = useState(FLOW_LOADING); + const [imports, setImports] = useState(undefined); + const [pseudocode, setPseudocode] = useState(undefined); + const flowTicket = useRef(0); + /** + * Fuer welches Symbol der Walk schon geholt ist. + * + * Neu in W8, und der Grund ist AC5: der Flow behaelt seine Stelle ueber einen + * Reiterwechsel hinweg. Bis dahin holte der Effekt den Walk jedes Mal neu, + * wenn der Erklaerer aufging, und setzte den Schritt dabei auf -1 zurueck. + * Als Overlay war das richtig (jedes Aufschlagen war eine neue Sitzung), als + * Reiter waere es der Verlust, den dieser Zyklus verbietet. Der Vermerk + * unterscheidet die beiden Faelle: dasselbe Symbol wird nicht noch einmal + * geholt, ein anderes schon, und der Effekt darueber leert ihn. + */ + const flowLoadedFor = useRef(''); + const importsTicket = useRef(0); + /* + * Waehrend einer Stepper-Sitzung folgt der Twin dem Caret nicht. + * + * Ein Schritt kann in eine andere Datei zeigen (ein erhobener Fehlertyp + * steht fast immer im Aufgerufenen), und der Editor folgt ihm. Ohne diese + * Klammer wuerde die Aufloesung am Zielort das Subjekt wechseln, der Kasten + * waere der eines anderen Symbols, und der Leser haette die Sitzung + * verloren, die er gerade fuehrt. Escape beendet sie und gibt den Caret + * wieder frei. + */ + /** + * Das Subjekt des Twins als Zeichenkette. + * + * Der Schluessel und nicht das Objekt, und das ist keine Feinheit: die + * Aufloesung liefert bei jedem Lauf ein NEUES `SymbolRef`, auch wenn es + * dasselbe Symbol meint. Ein Effekt, der an der Objektgleichheit haengt, + * wirft dann den geladenen Walk weg, sobald der Caret sich noch einmal + * setzt, und der Erklaerer stuende dauerhaft auf "wird geladen". Genau das + * ist beim ersten Lauf dieses Zyklus passiert, und der Schluessel ist die + * Antwort darauf: gleiches Symbol, gleicher Schluessel, kein Wegwerfen. + */ + const twinKey = twinSymbol === undefined ? '' : (twinSymbol.qualifiedName ?? twinSymbol.name); + + const flowPinned = useRef(false); + /* + * Der Reiter und nicht die Sichtbarkeit haelt die Klammer. + * + * Bis W8 stand hier `flowOpen`, also "das Overlay ist zu sehen". Seit der + * Erklaerer ein Reiter ist, gibt es einen dritten Zustand: der Reiter ist + * gewaehlt, aber die Zone ist eingeklappt (Escape). Die Sitzung ist dann + * NICHT beendet, denn Escape kostet seit W8 nichts, und eine Klammer, die + * sich beim Einklappen loeste, gaebe den Caret frei und wechselte hinter dem + * eingeklappten Bereich das Subjekt. Der Leser klappte wieder auf und faende + * das Bild eines anderen Symbols. + */ + flowPinned.current = explainTab === 'flow' && flowStep >= 0; + + /** + * Was jeder verdrahtete Menuebuchstabe tut, immer auf dem Stand des Bildes. + * + * Geschrieben wird es unten bei den Menuepunkten, gelesen oben im + * Tastatur-Griff, und der Grund fuer den Umweg ueber ein Ref steht an beiden + * Stellen. Der Schluessel ist der Buchstabe aus {@link WIRED_MENU_SHORTCUTS}. + */ + const menuActionsRef = useRef void>>({}); + + /** + * Was der Tastatur-Griff ueber den Bildschirm wissen muss, ohne ihn neu + * aufzuhaengen. + * + * Zwei Fragen, und beide aendern sich mit jedem Bild: liegt gerade eine + * Flaeche ueber dem Editor, und laeuft eine Fuehrung. Sie in die + * Abhaengigkeitsliste des Effekts zu schreiben hiesse, den Griff bei jedem + * geoeffneten Panel ab- und wieder anzumelden; sie in seine Schliessung zu + * schreiben hiesse, dass er die Lage von vor hundert Bildern liest. + */ + const keyboardGuardRef = useRef<{ overlayOpen: boolean; walkRunning: boolean }>({ + overlayOpen: false, + walkRunning: false, + }); + + /* + * Das geladene Dokument als Ref. + * + * Die Imports-Lesung braucht den Text der Datei, die schon offen ist, und + * soll dafuer nicht jedesmal neu aufgehaengt werden, wenn der Reader ein + * Zeichen mehr geladen hat. Der Effekt haengt am Pfad und an der IR; der + * Text wird zum Zeitpunkt der Frage gelesen. + */ + const documentRef = useRef(undefined); + documentRef.current = document; + + const twinTicket = useRef(0); + // Das aktuelle Subjekt als Ref, damit die Aufloesung es lesen kann, ohne + // dass der Effekt bei jedem Subjektwechsel neu aufgehaengt wird. + const shownSymbol = useRef(undefined); + shownSymbol.current = twinSymbol; + // Die IR ebenfalls als Ref: der Klick auf eine Zeile muss wissen, aus + // welcher Checkliste die Zeile stammt, und darf dafuer nicht neu gebunden + // werden, sooft das Panel neu zeichnet. + const shownIr = useRef(undefined); + shownIr.current = twinIr; + + const irCache = useMemo( + () => + new IrCache( + async (symbol) => buildIr(provider, ATLAS_WORKSPACE_ROOT, symbol, { + projectName: project, + generation: 1, + }), + () => { + // Nur hier, und nur bei einer echten Anfrage. Der Beweislauf + // misst an dieser Zahl, ob ein Caret-Wechsel nachgeladen hat. + window.__atlasTwinFetches = (window.__atlasTwinFetches ?? 0) + 1; + }, + ), + [provider, project], + ); + + // ------------------------------------------------- Galaxie und Suche --- + + const [galaxyOn, setGalaxyOn] = useState(true); + const [layout, setLayout] = useState(undefined); + const [sourceEvidence, setSourceEvidence] = useState<{ project: string; target: SourceEvidenceTarget }>(); + const [hits, setHits] = useState(noHits); + const [selectedHit, setSelectedHit] = useState(0); + const [searchStatus, setSearchStatus] = useState('ready'); + const [searchMessage, setSearchMessage] = useState(''); + const [answeredQuery, setAnsweredQuery] = useState(''); + /** Woher die gezeigten Zeilen kommen: aus der Antwort des Index, oder vorlaeufig. */ + const [hitSource, setHitSource] = useState('index'); + const searchTicket = useRef(0); + /** + * Der Abbruch der laufenden Runde. + * + * Die Ticketpruefung allein reicht seit W7b nicht mehr: sie verhindert, dass + * eine ueberholte Antwort die neuere ueberschreibt, laesst die ueberholte + * Runde aber zu Ende laufen. Bei einem Wort je Anfrage sind das mehrere + * Anfragen, die niemand mehr liest, und sie stehen in der Warteschlange vor + * denen, die noch jemand liest. Beides bleibt: der Abbruch spart den Weg, + * das Ticket ist die Zusicherung. + */ + const searchAbort = useRef(undefined); + /** + * Die letzte vollstaendige Antwort des Index, roh. + * + * Sie traegt das Verlaengern eines Wortes: wer `valid` gefragt hat und dann + * `validate` tippt, sieht sofort die neu gerankten Kandidaten der vorigen + * Runde, statt auf einen zweiten Weg zum Server zu warten. + * + * Sie ERSETZT den Weg nicht, und das ist eine bewusste Grenze. Die Antwort + * des Servers auf ein laengeres Wort ist nicht garantiert eine Teilmenge + * seiner Antwort auf das kuerzere: die BM25-Suche zerlegt Bezeichner in + * Woerter, und `validate` kann ein Symbol treffen, das `valid` nicht traf. + * Ein Cache, der die Anfrage einspart, wuerde solche Treffer still + * verschlucken. Er spart also die WARTEZEIT und nicht die Wahrheit: die + * Zeilen stehen sofort da und tragen ihre Marke, und die Antwort des Index + * ersetzt sie, sobald sie da ist. + */ + const prefixCache = useRef< + { query: string; candidates: SymbolSearchHit[]; complete: boolean } | undefined + >(undefined); + /** Die Zahlen, die der Beweislauf liest. Siehe AtlasSearchSeam. */ + const searchStats = useRef({ + localFirst: 0, + prefixCacheHits: 0, + serverRequests: 0, + serverRoundtripMs: 0, + roundtrips: [] as number[], + abortedRequests: 0, + staleDropped: 0, + staleAnswerWins: false, + appliedTicket: 0, + }); + /** Die Menuepunkte, deren Handlung gelaufen ist. Siehe AtlasSearchSeam. */ + const activatedMenus = useRef([]); + + // ------------------------------- Warum, Fuehrungen und die Checkliste --- + + /** + * Der Speicher dieses Browsers. + * + * Einmal geholt und weitergereicht, damit jede Stelle, die etwas merkt oder + * nachschlaegt, denselben Speicher benutzt und ein Test ihn ersetzen kann, + * ohne globale Objekte zu verbiegen. + */ + const store = useMemo(() => browserStore(), []); + + // ------------------------------------------------ Die Zonen (W8) ------- + + /* + * Wie gross das Fenster ist, als Zustand und nicht als Abfrage. + * + * Die Hoechstmasse der vier Zonen haengen daran (src/layout/layout-model.ts), + * und ein Griff, dessen `aria-valuemax` aus der Fenstergroesse von vor dem + * letzten Ziehen stammt, meldet einen Anschlag, den es nicht gibt. Der + * Anfangswert wird beim ersten Bild gelesen und im Effekt darunter sofort + * korrigiert: `window.innerHeight` ist beim ersten Bild noch die Hoehe vor + * dem Layout. Dieselbe Ueberlegung wie beim Deckel des Antwort-Panels in + * W7c, nur fuer alle vier Masse. + */ + const [frame, setFrame] = useState(() => ({ + width: window.innerWidth, + height: window.innerHeight, + })); + const [zones, setZones] = useState(() => + defaultLayout({ width: window.innerWidth, height: window.innerHeight })); + const frameRef = useRef(frame); + frameRef.current = frame; + const zonesRef = useRef(zones); + zonesRef.current = zones; + const projectRef = useRef(''); + + useEffect(() => { + const measure = (): void => { + setFrame({ width: window.innerWidth, height: window.innerHeight }); + }; + measure(); + window.addEventListener('resize', measure); + return () => window.removeEventListener('resize', measure); + }, []); + + /** + * Die vier Zahlen setzen und merken, an einer Stelle. + * + * Beides zusammen und nicht in zwei Effekten: ein Effekt, der bei jeder + * Aenderung schreibt, schreibt beim Laden zuerst die Vorgabe ueber den + * gemerkten Stand und danach den gemerkten Stand zurueck. Das geht gut, bis + * jemand das Fenster genau dazwischen schliesst. + */ + const applyZones = useCallback((next: LayoutSizes) => { + setZones(next); + /* + * Gespeichert wird nur, was sich wirklich geaendert hat. + * + * Ein Zug schickt zwanzig Ereignisse zwischen zwei Bildern, und die + * meisten davon tragen dieselbe Zahl (der Griff steht am Anschlag, oder + * die Bewegung war kleiner als ein Pixel). Sie alle zu speichern hiesse, + * zwanzigmal in localStorage zu schreiben, um nichts zu aendern. + * + * Der Zustand wird trotzdem IMMER gesetzt, und das ist keine + * Unachtsamkeit: `setZones` mit einem neuen Objekt zeichnet ein Bild, + * und an diesem Bild haengen die Naehte, an denen der Beweislauf liest. + * Ein "reset layout" auf ein Layout, das schon die Vorgabe ist, aendert + * nichts und muss trotzdem sichtbar stattgefunden haben; ohne das Bild + * meldete der Griff des Laufs, der Menuepunkt habe nicht ausgeloest. + * Gemessen an genau diesem Fall (smoke:w7b, `a-layout`). + */ + if (!sameLayout(next, zonesRef.current)) { + writeLayout(store, projectRef.current, next); + } + }, [store]); + + /** Eine Grenze verschieben. Die Grenzen der Grenze kommen aus dem Modell. */ + const changeZone = useCallback((key: LayoutKey, value: number) => { + applyZones({ + ...zonesRef.current, + [key]: clampLayoutValue(key, value, frameRef.current), + }); + }, [applyZones]); + + /** Doppelklick auf einen Griff: genau diese eine Grenze zurueck. */ + const resetZone = useCallback((key: LayoutKey) => { + changeZone(key, defaultLayout(frameRef.current)[key]); + }, [changeZone]); + + /** + * Die Bitte an das Graph-Panel, sein Bild wieder einzupassen (W10b, AC5). + * + * Eine Zahl und kein Zustand: jede Erhoehung ist eine neue Bitte. Sie steht + * hier, weil "reset layout" sie ausloest; der Knopf in der Szene hat seinen + * eigenen Zaehler, und beide gehen im Panel durch dieselbe Rechnung. + */ + const [graphRefit, setGraphRefit] = useState(0); + + /** + * Der Weg zurueck aus AC3: alle vier Grenzen auf die Vorgabe. + * + * Eine Funktion fuer beide Wege (Menuepunkt und Kommandozeile), damit sie + * nicht auseinanderlaufen koennen. Der Erklaeren-Bereich klappt dabei NICHT + * zu: "reset layout" ist eine Aussage ueber Groessen und nicht darueber, was + * der Leser gerade erklaert haben will. + * + * Seit W10b passt der Graph sein Bild dabei wieder ein. Das ist kein zweiter + * Gegenstand, sondern derselbe: die Zonen bekommen ihre Vorgabegroesse + * zurueck, und die eingepasste Ansicht IST die Vorgabe des Graphen. Wer sich + * in der Wolke verdreht hat, kommt sonst nur ueber den Knopf in der Szene + * zurueck, und "reset layout" liesse ausgerechnet die Flaeche stehen, in der + * man sich am leichtesten verliert. + */ + const resetLayout = useCallback(() => { + applyZones(defaultLayout(frameRef.current)); + setGraphRefit((count) => count + 1); + }, [applyZones]); + + /** + * Was gezeichnet wird: der Wunsch des Lesers, so weit das Fenster ihn traegt. + * + * Zwei Zahlen und nicht eine, und der Unterschied ist ein Nutzerbefund im + * Kleinen: `zones` ist, was der Leser GEZOGEN hat, `shownZones` ist, was + * davon in dieses Fenster passt. Bis dahin hatte ein Effekt die gezogene + * Zahl beim Verkleinern des Fensters einfach ueberschrieben, und beim + * Vergroessern kam sie nicht zurueck: wer sein Fenster kurz klein macht, + * findet danach schmale Spalten vor und muss alles neu ziehen. Gemessen im + * Beweislauf von W5c, der fuer seinen Tab-Test das Fenster verkleinert und + * wieder vergroessert: die rechte Spalte blieb bei 402 statt 439 Pixeln + * stehen, und der Graph verlor seinen Anteil an der Spalte. + * + * Der Wunsch bleibt also stehen, und begrenzt wird nur die Darstellung. + */ + const shownZones = useMemo(() => clampLayout(zones, frame), [zones, frame]); + + /* + * Die gemerkten Masse dieses Projekts, beim Projektwechsel neu gelesen. + * + * Pro Projekt, wie der Contract es verlangt: ein Repository mit tiefen + * Pfaden braucht einen breiten Explorer, ein flaches nicht, und wer zwischen + * beiden wechselt, will nicht jedes Mal nachziehen. + */ + useEffect(() => { + projectRef.current = project; + setZones(readLayout(store, project, frameRef.current)); + }, [project, store]); + + const [whyAnswer, setWhyAnswer] = useState({ asked: false }); + /** Von Hand wieder aufgerufen, ueber das [a]tlas-Menue. */ + const [whyReopened, setWhyReopened] = useState(false); + /** + * Ob der Leser die Frage in dieser Sitzung weggelegt hat, indem er anfing zu + * lesen. + * + * Nutzerbefund vom 2026-08-29 mit zwei Screenshots: "wenn ich eine Klasse + * anklicke, zum Beispiel orderService.ts, bleibt das und nicht der Code wird + * gezeigt." Die Frage verschwand von selbst nur, solange KEINE Datei offen + * war; von Hand wieder aufgerufen blieb sie ueber allem stehen, auch ueber + * einer Datei. Das war Absicht fuer den Fall "mitten im Lesen den Modus + * wechseln", und es ist die falsche Absicht, sobald der Leser danach etwas + * anderes tut: der Klick im Explorer OEFFNETE die Datei, Tab und Breadcrumb + * wechselten, und sichtbar blieb die Frage. Eine Aktion, die wirkt, ohne + * dass man ihre Wirkung sieht, ist die schlimmste Sorte stillen Fehlschlags. + * + * Wer eine Datei oeffnet, ein Symbol waehlt oder eine Frage stellt, hat die + * Frage beantwortet, indem er anfing zu lesen. Nur fuer diese Sitzung und + * nicht im Speicher des Browsers: was er dauerhaft beantwortet, beantwortet + * er mit einer der vier Karten oder mit dem Knopf darunter. + */ + const [whyDismissed, setWhyDismissed] = useState(() => { + try { return localStorage.getItem('cbm.workspace.setup') === 'done'; } + catch { return true; } + }); + + const [tour, setTour] = useState(undefined); + const [tourStep, setTourStep] = useState(0); + const [tourMessage, setTourMessage] = useState(''); + const tourStepRef = useRef(0); + tourStepRef.current = tourStep; + + /* + * Der Walk hinter einem Vorwaerts-Gang, aufgehoben statt weggeworfen. + * + * Bis W4d wurde die Antwort von `getClosure` sofort in eine Fuehrung + * uebersetzt und danach vergessen. Die Fuehrung ist aber eine Liste, und + * eine Liste zeigt keine Tiefe: sie hat die Symbole ohne Datei schon + * weggelassen und aus dem Graphen eine Reihe gemacht. Das Graph-Panel + * bekommt deshalb den Walk selbst (W4e) und projiziert ihn, statt aus + * Schritten einen Graphen zurueckzurechnen, den es nie gab. + */ + const [walk, setWalk] = useState(undefined); + + const [entryOpen, setEntryOpen] = useState(false); + const [overview, setOverview] = useState(undefined); + const [overviewLoading, setOverviewLoading] = useState(false); + const [overviewError, setOverviewError] = useState(); + const [overviewRevision, setOverviewRevision] = useState(0); + const repositoryReading = useRepositorySnapshot(project, overviewRevision); + const [entryQuery, setEntryQuery] = useState(''); + const [entryHits, setEntryHits] = useState(noHits); + const [entryStatus, setEntryStatus] = useState('idle'); + const [entryMessage, setEntryMessage] = useState(''); + const entryTicket = useRef(0); + + const [marks, setMarks] = useState({ visited: {}, confirmed: {} }); + + // -------------------------------------- Der BUG-Assistent und die Aenderung + + /* + * Ob der Assistent zu sehen ist, abgeleitet aus dem Reiter. + * + * Kein eigener Schalter mehr, und das ist der Kern von W8: ein Schalter + * neben dem Reiter waere die Stelle, an der beide auseinanderlaufen, und + * zwei Flaechen mit je einem eigenen Schalter sind genau der Zustand, in + * dem der Nutzer sie am 2026-08-29 uebereinander liegen sah. + */ + const bugOpen = explainOpen && explainTab === 'bug'; + const [bugTarget, setBugTarget] = useState(undefined); + const [bugPathsDto, setBugPathsDto] = useState(undefined); + const [bugStatus, setBugStatus] = useState('idle'); + const [bugMessage, setBugMessage] = useState(''); + const bugTicket = useRef(0); + + // ------------------------------------------ Das lokale Modell (W5a) ---- + + /* + * Drei Zustaende und kein vierter: was das Projekt erlaubt, was dieser + * Browser eingestellt hat, und was der Prozess auf 4141 gerade von sich + * gibt. Die ersten beiden entscheiden ueber das Fragen, der dritte ist die + * Antwort auf das Gefragte. + * + * `llmPolicy` bleibt undefined, bis die Datei des Zielprojekts gelesen ist. + * Genau so lange ist das LLM aus, auch wenn die Praeferenz an sagt: siehe + * src/llm/llm-state.ts, Regel 3. + */ + const [llmPolicy, setLlmPolicy] = useState(undefined); + const llmPreferenceOn = false; + const [llmProbe, setLlmProbe] = useState(EMPTY_SIDECAR_READING); + const llmProbes = useRef(0); + + /* + * Die Modellwahl dieses Browsers (W10), und der Griff, der sofort nachfragt. + * + * Die Wahl steht als Ref DANEBEN, weil der Probe-Timer sie liest: haenge er + * an ihr, wuerde jede Wahl den Timer ab- und neu aufhaengen. Der Griff geht + * die andere Richtung: wer waehlt, will die Zahlen des gewaehlten Modells + * sehen, ohne drei Sekunden auf die naechste Runde zu warten. Er ruft + * dieselbe Probe wie der Timer und landet damit im selben Zaehler; ein + * zweiter Weg zum Sidecar waere eine Anfrage, die `__atlasLlm.probes` nicht + * mitzaehlt, und der Beweislauf misst an der neuen Flaeche vorbei. + */ + const [selectedModel, setSelectedModel] = useState(''); + const selectedModelRef = useRef(''); + selectedModelRef.current = selectedModel; + + // ------------------------------------------- Der Atlas-Chat (W5b) ------ + + /* + * Der Verlauf dieser Sitzung, und nur dieser. + * + * Im Speicher und nirgends sonst: kein localStorage, keine Datei, kein + * Server. Das ist keine fehlende Funktion, sondern Non-Goal des Contracts. + * Ein Verlauf, der den Reload ueberlebt, ist ein Protokoll darueber, was + * jemand nicht verstanden hat, und dieses Produkt legt keines an. + */ + const [chatTurns, setChatTurns] = useState([]); + /** Martins Kontextregel als Einstellung. Vorgabe: Fokus plus erste Nachbarschaft. */ + const [chatDepth, setChatDepth] = useState(NEIGHBOR_DEPTH_DEFAULT); + const chatTicket = useRef(0); + /* + * Ob der Chat zu sehen ist, abgeleitet aus dem Reiter. + * + * Der Zustand, den W7c hier eingefuehrt hat, ist nicht verschwunden, er ist + * eine Ebene hoeher gewandert: "hat dieser Leser schon gefragt" steht in + * `chatTurns`, "will er die Antworten gerade sehen" im Reiter des + * Erklaeren-Bereichs. Der Nutzerbefund vom 2026-08-29 dahinter gilt + * unveraendert: Zumachen laesst den Verlauf stehen, und nur `clear` loescht. + */ + const chatOpen = explainOpen && explainTab === 'chat'; + + /* + * Die Umformulierung des Pseudocode-Blocks. + * + * `refined` haelt das Ergebnis getrennt vom deterministischen Block, statt + * ihn zu ersetzen: das Original muss jederzeit zurueckholbar sein, und ein + * ueberschriebener Block waere genau das nicht mehr. + */ + const [refineState, setRefineState] = useState<'idle' | 'running' | 'applied' | 'refused'>('idle'); + const [refineMessage, setRefineMessage] = useState(''); + + /** Der Griff auf das Eingabefeld, fuer den einen Weg zurueck in die Suche. */ + const commandInputRef = useRef(null); + + /** + * Die Grenzen des Vorwaerts-Walks, einmal aus der Adresszeile gelesen. + * + * Fehlt ein Parameter, entscheidet der Vorgabewert in provider/closure.ts. + * Gelesen wird nur beim Start: eine Grenze, die sich waehrend eines Walks + * aendert, waere ein Walk, der auf halber Strecke eine andere Frage + * beantwortet. + */ + const closureBounds = useMemo(() => { + const params = new URLSearchParams(window.location.search); + const read = (name: string): number | undefined => { + const raw = params.get(name); + if (raw === null) { + return undefined; + } + const value = Number.parseInt(raw, 10); + return Number.isFinite(value) ? value : undefined; + }; + return { depth: read(CLOSURE_DEPTH_PARAM), cap: read(CLOSURE_CAP_PARAM) }; + }, []); + + /* + * Der Vorwaerts-Walk aus dem Symbol im Fokus (W10b, AC3). + * + * Nutzerbefund vom 2026-08-30: "und hierarchies werden auch nicht + * angezeigt." Die Ursache stand in GalaxyPanel.tsx: die Projektion hing am + * WALK, und einen Walk gab es nur nach einem Einstiegs-Spaziergang oder in + * einer Fuehrung. Ein Symbol im Fokus reichte nicht, obwohl der Leser genau + * dann seinen Aufrufbaum sehen will. + * + * Drei Entscheidungen stecken darin: + * + * 1. **Derselbe Closure, dieselben Grenzen.** Gerechnet wird mit + * `getClosure` und mit `closureBounds`, also Zeichen fuer Zeichen wie + * beim Einstiegs-Spaziergang. Ein zweiter, eigener Weg waere ein zweites + * Bild derselben Frage. + * 2. **Nur ohne laufenden Spaziergang.** Laeuft einer, ist er gemeint; ihn + * zu ueberschreiben, weil der Leser in ihm gerade woanders hinsieht, + * waere das Ende des Spaziergangs ohne Ansage. + * 3. **Das Ergebnis waehlt keine Ansicht.** Es macht die Hierarchie + * moeglich, mehr nicht; die Vorgabe bleibt die Galaxie (Entscheidung 17 + * im Kopf von GalaxyPanel.tsx). + */ + const markedGraphRange = useMemo(() => markedSourceRange(readerSelection, activePath), [readerSelection, activePath]); + const readerFocusRange = useMemo(() => markedGraphRange + ?? (caretLine !== undefined && caretLine > 0 ? { startLine: caretLine, endLine: caretLine } : undefined), [markedGraphRange, caretLine]); + const activeGalaxySelection = useMemo(() => resolveRepositorySelection( + galaxySelectionProject === project ? galaxySelection : undefined, repositoryReading.snapshot, + ), [galaxySelectionProject, project, galaxySelection, repositoryReading.snapshot]); + const selectionUnavailable = Boolean(galaxySelectionProject === project && galaxySelection && repositoryReading.snapshot && !activeGalaxySelection); + const selectionUnavailableText = 'The previous selection is absent from this bounded index snapshot. Choose a source element again to inspect its current evidence.'; + const selectedGraphSymbol = useMemo(() => hierarchySymbolOf(activeGalaxySelection, project), [activeGalaxySelection, project]); + const readerFocusSymbol = (twinSymbol?.projectName === undefined || twinSymbol.projectName === project) && symbolMatchesReader(twinSymbol, activePath, readerFocusRange) + ? twinSymbol : symbolMatchesReader(selectedGraphSymbol, activePath, readerFocusRange) ? selectedGraphSymbol : undefined; + const hierarchyRoot = workspace === 'galaxy' ? selectedGraphSymbol : readerFocusSymbol; + const focusHierarchy = useSelectionHierarchy(provider, ATLAS_WORKSPACE_ROOT, project, hierarchyRoot, closureBounds, + workspace === 'galaxy' || (workspace === 'explore' && walk === undefined)); + const focusWalk = focusHierarchy.walk; + + // ------------------------------------------------------- Projekt ------- + + useEffect(() => { + const fromUrl = new URLSearchParams(window.location.search).get('project') ?? ''; + if (fromUrl.length > 0) { + setUiLogProject(fromUrl); + setProject(fromUrl); + setProjectDetail(''); + return; + } + let cancelled = false; + client + .listProjects() + .then((result) => { + if (cancelled) { + return; + } + const first = result.projects[0]; + if (first === undefined) { + setProjectDetail(messages.statusbar.noProjectIndexed); + return; + } + setUiLogProject(first.name); + setProject(first.name); + setProjectDetail(''); + }) + .catch((error: unknown) => { + if (!cancelled) { + setServerOk(false); + setProjectDetail(messages.statusbar.noProjectError(String(error))); + } + }); + return () => { + cancelled = true; + }; + }, [client]); + + // ------------------------------------------------------- Kennzahlen ---- + + useEffect(() => { + if (project.length === 0) { + return; + } + let cancelled = false; + client + .indexStatus(project) + .then((status) => { + if (cancelled) { + return; + } + setServerOk(true); + const next: { nodes?: number; edges?: number } = {}; + if (status.nodes !== undefined) { + next.nodes = status.nodes; + } + if (status.edges !== undefined) { + next.edges = status.edges; + } + setCounts(next); + }) + .catch(() => { + if (!cancelled) { + setServerOk(false); + setCounts({}); + } + }); + return () => { + cancelled = true; + }; + }, [client, project, overviewRevision]); + + // ------------------------------------------------------- Baum ---------- + + useEffect(() => { + if (project.length === 0) { + return; + } + let cancelled = false; + (async () => { + const root = await api.tree(project, ''); + const nextLevels = new Map([['', root]]); + const nextExpanded = new Set(); + const queue = directoryPaths(root); + while (queue.length > 0 && nextLevels.size < EAGER_LEVEL_BUDGET) { + const dir = queue.shift(); + if (dir === undefined || nextLevels.has(dir)) { + continue; + } + const level = await api.tree(project, dir); + if (cancelled) { + return; + } + nextLevels.set(dir, level); + nextExpanded.add(dir); + queue.push(...directoryPaths(level)); + } + if (cancelled) { + return; + } + setLevels(nextLevels); + setExpanded(nextExpanded); + setBudgetHit(queue.length > 0); + setTreeError(''); + setServerOk(true); + })().catch((error: unknown) => { + if (!cancelled) { + setLevels(emptyLevels); + setExpanded(new Set()); + setTreeError(String(error instanceof Error ? error.message : error)); + setServerOk(false); + } + }); + return () => { + cancelled = true; + }; + }, [api, project, overviewRevision]); + + /* + * Die zweite Baumquelle: der Coverage-Store. + * + * Ein eigener Effekt und nicht ein Anhaengsel des Baum-Effekts, aus zwei + * Gruenden. Erstens haengen die beiden an verschiedenen Dingen: der Baum an + * `/api`, die Coverage an `/rpc`, und ein Ausfall der einen darf die andere + * nicht mitnehmen. Zweitens ist die Reihenfolge egal: der Join ist eine + * reine Funktion ueber beide Ergebnisse, also darf ankommen, was zuerst + * fertig ist. + */ + useEffect(() => { + if (project.length === 0) { + return; + } + let cancelled = false; + setCoverageAsked(false); + loadCoverage(client, project) + .then((reading) => { + if (cancelled) { + return; + } + setCoverage(reading.index); + setCoverageMeta({ ...reading.answer.metadata }); + setCoverageAsked(true); + setCoverageError(''); + }) + .catch((error: unknown) => { + if (cancelled) { + return; + } + // Kein stiller Rueckfall auf "keine Luecken": ein Baum, der + // Vollstaendigkeit behauptet, weil er nicht nachfragen konnte, + // waere die teuerste Luege dieses Zyklus. + setCoverage(EMPTY_COVERAGE); + setCoverageMeta({}); + setCoverageAsked(false); + setCoverageError(error instanceof Error ? error.message : String(error)); + }); + return () => { + cancelled = true; + }; + }, [client, project, overviewRevision]); + + /** + * Die Vereinigung, aus der der Explorer zeichnet. + * + * Der Nur-Graph-Weg ist damit weg: `rows` haengt an dieser Karte und nicht + * mehr an `levels`. Solange die Coverage-Antwort fehlt, ist die Vereinigung + * genau der Graph, und die Notiz unter dem Baum sagt, dass noch niemand + * gefragt hat. + */ + const mergedLevels = useMemo(() => mergeCoverageIntoLevels(levels, coverage), [levels, coverage]); + + const rows = useMemo(() => flattenTree(mergedLevels, expanded), [mergedLevels, expanded]); + + const loadLevel = useCallback( + async (path: string) => { + const level = await api.tree(project, path); + setLevels((current) => { + const next = new Map(current); + next.set(path, level); + return next; + }); + }, + [api, project], + ); + + const toggleDirectory = useCallback( + (row: TreeRow) => { + setExpanded((current) => { + const next = new Set(current); + if (next.has(row.path)) { + next.delete(row.path); + } else { + next.add(row.path); + } + return next; + }); + if (!levels.has(row.path)) { + void loadLevel(row.path).catch((error: unknown) => { + // Ein Ordner, den nur die Coverage-Listen kennen, hat im + // Graphen keine Ebene. Dass /api/tree ihn nicht kennt, ist + // dann der erwartete Fall und kein Baumfehler; die Zeilen + // darunter kommen ohnehin aus der Vereinigung. + if (mergedLevels.get(row.path)?.synthetic === true) { + return; + } + setTreeError(String(error instanceof Error ? error.message : error)); + }); + } + }, + [levels, loadLevel, mergedLevels], + ); + + // ------------------------------------------------------- Datei oeffnen - + + /** + * Eine Datei oeffnen, mit dem Coverage-Befund als erster Instanz. + * + * Steht die Datei in einer der Listen, die "kein Inhalt" bedeuten, wird gar + * nicht erst geladen: der Server liefert Inhalt ausschliesslich ueber einen + * Modul-Knoten, und den gibt es fuer eine uebersprungene Datei nicht. Ein + * Aufruf, dessen Ausgang schon feststeht, waere eine Anfrage, um eine + * Fehlermeldung zu erzeugen, die man vorher kannte. + * + * Scheitert das Laden trotzdem an einem fehlenden Modul-Knoten, ist das + * dieselbe Grenze und nicht ein Ausfall: `unavailable` statt `failed`, mit + * der Begruendung, die der Lader selbst geschrieben hat. + */ + const openFile = useCallback( + (path: string) => { + setWorkspace('explore'); + /* + * Wer eine Datei oeffnet, hat die Frage beantwortet, indem er + * anfing zu lesen (Nutzerbefund 2026-08-29, AC6f). Hier und nicht + * im Explorer, weil dieselbe Funktion auch die Suche, die Zitate + * des Chats, der Walk und jeder Sprung des Twin benutzen: eine + * Regel an einer Stelle statt sechs Aufrufen derselben Regel. + */ + setWhyReopened(false); + setWhyDismissed(true); + setTabs((current) => (current.includes(path) ? current : [...current, path])); + setActivePath(path); + setReaderSelection(undefined); + // Flow steps retain their root across files. Reloading the same + // document also keeps ready facts until the caret resolves again. + if (!flowPinned.current && documentRef.current?.path !== path) { + setTwinSymbol(undefined); + setTwinIr(undefined); + setTwinStatus('empty'); + setTwinName(messages.twin.noSymbol); + } + setImports(undefined); + setDocument(undefined); + setDocumentOrigin(undefined); + loadTicket.current += 1; + const ticket = loadTicket.current; + + const record = coverageRef.current.records.get(path); + const state: CoverageState = record?.state ?? 'indexed'; + if (state === 'skipped' || state === 'not-indexed' || state === 'ignored') { + setReaderStatus('unavailable'); + setReaderMessage(readerUnavailableNote(path, state, record?.reason ?? '')); + return; + } + + setReaderStatus('loading'); + setReaderMessage(messages.reader.loading(path, READER_RPC_TOOL)); + loadFileDocument(client, project, path) + .then((loaded) => { + if (ticket !== loadTicket.current) { + return; + } + setDocument(loaded); + setDocumentOrigin({ project, version: `reader-load:${ticket}` }); + setReaderStatus('ready'); + }) + .catch((error: unknown) => { + if (ticket !== loadTicket.current) { + return; + } + setDocument(undefined); + setReaderStatus(error instanceof FileNotReadableError ? 'unavailable' : 'failed'); + setReaderMessage(error instanceof Error ? error.message : String(error)); + }); + }, + [client, project], + ); + + const closeTab = useCallback( + (path: string) => { + setTabs((current) => { + const next = current.filter((entry) => entry !== path); + if (path === activePath) { + const fallback = next[next.length - 1]; + if (fallback === undefined) { + setActivePath(''); + setDocument(undefined); + setReaderStatus('idle'); + setReaderMessage(messages.reader.pickFile); + } else { + openFile(fallback); + } + } + return next; + }); + }, + [activePath, openFile], + ); + + /* + * Die Frische der offenen Datei. + * + * Eine eigene Frage je Datei, weil die Antwort nur beim Fragen entsteht: + * der Server vergleicht mtime und Groesse der Datei auf der Platte mit dem, + * was beim Indexlauf notiert wurde (mcp.c, coverage_path_freshness). Genau + * darum meldet sie eine Datei, die seit dem Index geaendert wurde, und + * genau darum steht sie nicht schon in den Listen, die beim Start geholt + * wurden. + * + * Ein Fehler hier loescht die Notiz, statt eine alte stehenzulassen: eine + * Frische-Angabe ueber die vorige Datei waere schlimmer als keine. + */ + useEffect(() => { + if (project.length === 0 || activePath.length === 0) { + setOpenCoverage(undefined); + return; + } + coverageTicket.current += 1; + const ticket = coverageTicket.current; + let cancelled = false; + setOpenCoverage(undefined); + loadPathCoverage(client, project, activePath) + .then((answer) => { + if (!cancelled && ticket === coverageTicket.current) { + setOpenCoverage(answer); + } + }) + .catch(() => { + if (!cancelled && ticket === coverageTicket.current) { + setOpenCoverage(undefined); + } + }); + return () => { + cancelled = true; + }; + }, [activePath, client, project]); + + // ------------------------------------------------------- Caret-Strecke - + + // Der Caret wandert sofort in die Anzeige (Badge-Puls, data-current) und + // erst nach der Ruhezeit in die Frage an den Graphen. + useEffect(() => { + if (caretLine === undefined) { + return; + } + const timer = window.setTimeout(() => setSettledCaret(caretLine), TWIN_CARET_DEBOUNCE_MS); + return () => window.clearTimeout(timer); + }, [caretLine]); + + // Ein Dateiwechsel loescht die alte Lage. Der Caret der neuen Datei meldet + // sich von selbst, sobald Monaco sein Modell getauscht hat; bis dahin + // stehenzulassen, was ueber die vorige Datei bekannt war, waere ein Panel, + // das ueber die falsche Datei spricht. + useEffect(() => { + setCaretLine(undefined); + setSettledCaret(undefined); + setPointedLine(undefined); + }, [activePath]); + + useEffect(() => { + if (project.length === 0 || activePath.length === 0 || settledCaret === undefined) { + return; + } + // Waehrend einer Stepper-Sitzung bleibt das Subjekt stehen. Warum, + // steht bei `flowPinned`. + if (flowPinned.current) { + return; + } + let cancelled = false; + twinTicket.current += 1; + const ticket = twinTicket.current; + const current = () => !cancelled && ticket === twinTicket.current; + + (async () => { + const resolved = await provider.resolveSymbolAt( + ATLAS_WORKSPACE_ROOT, + activePath, + settledCaret, + { projectName: project }, + ); + if (!current()) { + return; + } + if (resolved.kind !== 'ok') { + // Jeder der drei Faelle sagt etwas anderes, und keiner davon + // heisst "dieses Symbol hat keine Fakten". + setTwinSymbol(undefined); + setTwinIr(undefined); + setTwinName(messages.twin.noSymbol); + if (resolved.kind === 'file-not-indexed') { + setTwinStatus('not-indexed'); + setTwinMessage(TWIN_FILE_NOT_INDEXED); + setTwinHint(TWIN_FILE_NOT_INDEXED_HINT); + } else if (resolved.kind === 'engine-unavailable') { + setTwinStatus('failed'); + setTwinMessage(TWIN_LOAD_FAILED); + setTwinHint(resolved.reason); + } else { + setTwinStatus('empty'); + setTwinMessage(TWIN_EMPTY_MESSAGE); + setTwinHint(TWIN_EMPTY_HINT); + } + return; + } + + const symbol = resolved.symbol; + const unchanged = + shownSymbol.current?.qualifiedName !== undefined && + shownSymbol.current.qualifiedName === symbol.qualifiedName; + if (unchanged && twinIr !== undefined) { + // Gleiches Subjekt, neuer Caret: nichts zu holen und nichts zu + // melden. Auch nur kurz in den Ladezustand zu gehen waere ein + // Blinken, das behauptet, es passiere etwas. + return; + } + setTwinSymbol(symbol); + setTwinIr(undefined); + setTwinName(symbol.name); + setTwinStatus('loading'); + setTwinMessage(TWIN_LOADING); + setTwinHint(''); + const entry = await irCache.load(symbol); + if (!current()) { + return; + } + setTwinIr(entry.ir); + setTwinStatus('ready'); + })().catch((error: unknown) => { + if (!current()) { + return; + } + setTwinIr(undefined); + setTwinStatus('failed'); + setTwinMessage(TWIN_LOAD_FAILED); + setTwinHint(error instanceof Error ? error.message : String(error)); + }); + + return () => { + cancelled = true; + }; + // `twinIr` steht bewusst nicht in der Liste: es aendert sich als Folge + // dieses Effekts, und eine Abhaengigkeit darauf waere eine Schleife. + // Gelesen wird es nur, um zu entscheiden, ob schon etwas dasteht. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [provider, irCache, project, activePath, settledCaret, flowStep]); + + /** + * Einem Ziel folgen: das Subjekt wechselt sofort, der Editor kommt nach. + * + * Die Fakten kommen aus der Aufloesung in der Zieldatei und nicht aus dem + * Klickziel. Warum, steht oben im Kopf dieser Datei. + */ + /** + * Die stille Anzeige beim Folgen: das Item, dem gefolgt wurde, ist besucht. + * + * Vermerkt wird am Symbol, dessen Checkliste die Zeile traegt, und nicht am + * Ziel: die Verpflichtung "verstehe den Aufruf von validateUser" gehoert + * demjenigen, der aufruft. Findet sich keine Zeile, die auf dieses Ziel + * zeigt, wird nichts vermerkt; ein Klick auf etwas, das nicht auf der Liste + * steht, ist kein abgearbeiteter Punkt. + */ + const markFollowed = useCallback( + (target: SymbolRef) => { + const ir = shownIr.current; + const owner = ir?.symbol.qualifiedName; + if (ir === undefined || owner === undefined || project.length === 0) { + return; + } + const item = ir.checklist.find((entry) => + entry.target !== undefined + && ((target.qualifiedName !== undefined + && entry.target.qualifiedName === target.qualifiedName) + || (target.qualifiedName === undefined && entry.target.uri === target.uri))); + if (item === undefined) { + return; + } + setMarks(markVisited(store, project, owner, item.id)); + }, + [project, store], + ); + + const followTarget = useCallback( + (target: SymbolRef) => { + setWorkspace('explore'); + setFlowStep(-1); + flowPinned.current = false; + markFollowed(target); + const location = twinLocationOf(target); + const keepReadyTwin = location.path.length > 0 + && keepsReadyTwinForTarget(target, shownSymbol.current, shownIr.current, twinStatus); + setTwinName(target.name); + if (!keepReadyTwin) { + setTwinIr(undefined); + setTwinSymbol(undefined); + setTwinStatus('loading'); + setTwinMessage(TWIN_LOADING); + setTwinHint(''); + } + if (location.path.length === 0) { + setTwinStatus('empty'); + setTwinMessage(TWIN_EMPTY_MESSAGE); + setTwinHint(TWIN_EMPTY_HINT); + return; + } + if (location.path !== activePath) { + openFile(location.path); + } + setReveal((previous) => ({ line: location.line, nonce: (previous?.nonce ?? 0) + 1 })); + if (location.path === activePath) setSettledCaret(location.line); + }, + [activePath, markFollowed, openFile, twinStatus], + ); + + const pointRow = useCallback((row: TwinRow | undefined) => { + setPointedLine(row?.siteLine); + }, []); + + // ------------------------------------- Flow-Erklaerer und Pseudocode --- + + /** + * Eine Zeile oeffnen: Datei anfahren, Zeile offenbaren, Zeile anleuchten. + * + * Derselbe Weg, den `followTarget` fuer eine Twin-Zeile geht, nur ohne den + * Subjektwechsel: eine Pseudocode-Zeile zeigt auf eine Stelle im Text und + * behauptet nicht, dass dort ein anderes Symbol das Thema wird. Was dort + * steht, sagt der Reader; wer dort steht, sagt die Aufloesung, sobald der + * Leser die Sitzung beendet. + */ + const openLine = useCallback( + (ref: PseudocodeSourceRef) => { + const path = workspacePathOf(ref.uri); + if (path.length === 0) { + return; + } + if (path !== activePath) { + openFile(path); + } + setPointedLine(ref.line); + setReveal((previous) => ({ line: ref.line, nonce: (previous?.nonce ?? 0) + 1 })); + }, + [activePath, openFile], + ); + + /** + * Einen Schritt anlaufen: Kasten, STEPS-Liste und Editor bewegen sich + * zusammen. + * + * Der Kasten und die Liste folgen dem Index (das Panel rechnet beides aus + * demselben Modell), der Editor folgt der Quellangabe der Zeile. Eine + * Zeile ohne Quellangabe bewegt den Editor nicht, statt irgendwohin zu + * springen. + */ + const stepFlow = useCallback( + (index: number) => { + const step = flowView?.steps[index]; + if (step === undefined) { + return; + } + setFlowStep(index); + flowPinned.current = true; + if (step.line.sourceRef !== undefined) { + openLine(step.line.sourceRef); + } + }, + [flowView, openLine], + ); + + /** + * Der flow()-Kopf im Twin schaltet den Reiter. + * + * Steht der Bereich schon auf `flow`, klappt er zu; steht er woanders oder + * ist er zu, geht er auf `flow` auf. Das ist dieselbe Bedienung wie vorher + * (ein Kopf, ein Klick, auf und zu), und sie kostet seit W8 nichts mehr: der + * Schritt bleibt, wo er war, und ein zweiter Klick zeigt wieder dieselbe + * Stelle. Bis W8 setzte dieses Zumachen den Stepper zurueck, weil ein + * Overlay eine Sitzung war; ein Reiter ist keine. + */ + const toggleFlow = useCallback(() => { + setExplainOpen((open) => !(open && explainTabRef.current === 'flow')); + setExplainTab('flow'); + }, []); + + /** + * Etwas erklaeren lassen: den Bereich aufmachen, auf DIESEM Reiter. + * + * Der eine Weg, auf dem jede der fuenf Flaechen aufgeht (AC4). Bis W8 hatte + * jede ihren eigenen, und drei davon machten dabei die anderen zu, was der + * Grund war, aus dem der Zustand jedes Mal verlorenging. Hier geht nichts zu: + * es wird nur umgeschaltet. + */ + const openExplain = useCallback((tab: ExplainTabId) => { + setExplainTab(tab); + setExplainOpen(true); + }, []); + + /** + * Den Bereich zuklappen, ohne irgendetwas zu kosten. + * + * Escape und der Knopf im Kopf gehen denselben Weg. Was hier NICHT steht, + * ist der Punkt: kein `setChatTurns([])`, kein `setFlowStep(-1)`, kein + * `setTour(undefined)`. Zumachen ist nicht loeschen, und das war der + * Nutzerbefund, aus dem W7c den zweiten Knopf des Chats gebaut hat. + */ + const collapseExplain = useCallback(() => { + setExplainOpen(false); + }, []); + + /* + * Der Fakten-Block des aktiven Symbols. Rein aus dem, was schon dasteht: + * die Pseudocode-Ansicht kostet keine einzige weitere Anfrage. + * + * Seit W8c sind es ZWEI Dinge, die schon dastehen, und das zweite ist der + * Punkt: die IR des Symbols, und das geladene Layout der Galaxie. Aus dem + * Layout liest der Block, was der Index ueber die AUFGERUFENEN Symbole + * aufgezeichnet hat (was sie erheben koennen, was sie ihrerseits rufen, was + * sie aus der Umgebung lesen), und schreibt es an die Schrittzeile. Es ist + * dieselbe Quelle, aus der die Bedeutungssuche ihre Kandidaten nimmt + * (`localIndex` weiter oben), und aus demselben Grund: sie ist da, sie ist + * eine Antwort fuer das ganze Projekt, und sie noch einmal zu holen waere + * ein zweiter Serverweg fuer eine Antwort, die im Speicher liegt. Was das + * Layout NICHT hergibt, wird nicht nachgeladen und nicht erfunden; es steht + * als `enrichmentAvailable.missing` im Dokument (src/pseudocode/ + * step-insights.ts). + * + * Ein Subjektwechsel wirft die Umformulierung weg. Sie gehoert zu genau + * einem Block; sie ueber einen Symbolwechsel hinweg stehenzulassen waere + * eine Umschreibung der Fakten eines anderen Symbols. + */ + useEffect(() => { + const ir = twinIr; + setRefineState('idle'); + setRefineMessage(''); + if (ir === undefined) { + setPseudocode(undefined); + return; + } + setPseudocode(buildPseudocode( + { kind: 'symbol', label: ir.symbol.name }, + { irs: [ir], ...(layout === undefined ? {} : { graph: layout }) }, + )); + }, [layout, twinIr]); + + // Was die Datei um das Symbol herum hereinholt. Zwei Lesungen, eine davon + // ist der Text, den der Reader ohnehin schon geladen hat; siehe + // src/pseudocode/imports-source.ts. + useEffect(() => { + const ir = matchingInspectorIr(twinIr, twinSymbol, project, activePath); + const uri = ir?.symbol.uri ?? twinTargetOf({ name: baseName(activePath), filePath: activePath, kind: 'module' })?.uri; + if (!uri || project.length === 0 || activePath.length === 0) { + setImports(undefined); + return; + } + importsTicket.current += 1; + const ticket = importsTicket.current; + let cancelled = false; + void fileImportsFor(provider, ATLAS_WORKSPACE_ROOT, { + filePath: activePath, + source: documentRef.current?.path === activePath ? documentRef.current.source : undefined, + opts: { projectName: project, generation: 1 }, + }) + .then((answer) => { + if (cancelled || ticket !== importsTicket.current) { + return; + } + setImports(buildImportsGroup({ imports: answer, irs: ir ? [ir] : [], uri })); + }) + .catch(() => { + if (!cancelled && ticket === importsTicket.current) { + // Eine fehlgeschlagene Import-Lesung ist kein Grund, die + // Fakten zu verlieren. Der Data-Block steht ohne sie. + setImports(undefined); + } + }); + return () => { + cancelled = true; + }; + }, [activePath, document, project, provider, twinIr, twinSymbol]); + + /* + * Der Walk hinter dem Kasten. + * + * Er laeuft, sobald der Kasten offen ist und ein Subjekt dasteht: das + * Design zeigt den Kasten sichtbar, also muss er sich selbst fuellen und + * nicht auf einen zweiten Klick warten. Die IRs der erreichten Symbole + * kommen aus dem IR-Cache; ein Symbol, das der Leser eben erst besucht hat, + * kostet dabei keine Anfrage. + */ + /* + * Ein Subjektwechsel wirft den Walk weg, und zwar HIER und nicht im Lader. + * + * Bis W8 tat es der Lader mit, weil er ohnehin bei jedem Aufschlagen lief. + * Seit der Erklaerer ein Reiter ist, laeuft der Lader nicht mehr bei jedem + * Aufschlagen (sonst waere der Schritt bei jedem Reiterwechsel weg), und + * damit braucht das Wegwerfen eine eigene Stelle. Ohne sie stuende nach + * einem Symbolwechsel das Bild des vorigen Symbols da, und das waere die + * schlimmste Sorte Fehler dieser Oberflaeche: eine richtige Antwort auf eine + * andere Frage. + */ + useEffect(() => { + flowLoadedFor.current = ''; + setFlowView(undefined); + setFlowStep(-1); + setFlowMessage(twinKey.length === 0 ? FLOW_UNAVAILABLE : FLOW_LOADING); + }, [twinKey]); + + useEffect(() => { + const symbol = twinSymbol; + if (!flowOpen || symbol === undefined || project.length === 0) { + return; + } + // Dasselbe Symbol wird nicht zweimal geholt: sonst kostete jeder + // Reiterwechsel den Schritt, an dem der Leser steht. + if (flowLoadedFor.current === twinKey) { + return; + } + flowLoadedFor.current = twinKey; + flowTicket.current += 1; + const ticket = flowTicket.current; + let cancelled = false; + const current = () => !cancelled && ticket === flowTicket.current; + setFlowView(undefined); + setFlowStep(-1); + setFlowMessage(FLOW_LOADING); + + (async () => { + const walk = await getClosure(provider, ATLAS_WORKSPACE_ROOT, symbol, { + projectName: project, + generation: 1, + depth: FLOW_CLOSURE_DEPTH, + cap: FLOW_CLOSURE_CAP, + }); + if (!current()) { + return; + } + const closure = closureDocumentOf(walk); + // Was ankommt, kommt an. Ein Symbol, dessen IR nicht zu holen war, + // wird vom Builder als Gruppe ohne Zeilen gezeigt und im ehrlichen + // Block genannt; es wird nicht stillschweigend weggelassen. + const irs: SemanticIR[] = []; + for (const reached of closure.symbols) { + const entry = await irCache.load(reached).catch(() => undefined); + if (!current()) { + return; + } + if (entry !== undefined) { + irs.push(entry.ir); + } + } + setFlowView(buildFlowView({ + closure, + irs, + depth: FLOW_CLOSURE_DEPTH, + cap: FLOW_CLOSURE_CAP, + })); + })().catch(() => { + if (current()) { + // Der Vermerk faellt zurueck, damit ein zweiter Versuch moeglich + // ist. Ein gemerkter Fehlschlag waere ein Symbol, das dieses + // Fenster nie wieder zeichnet. + flowLoadedFor.current = ''; + setFlowView(undefined); + setFlowMessage(FLOW_UNAVAILABLE); + } + }); + + return () => { + cancelled = true; + }; + }, [flowOpen, irCache, project, provider, twinKey, twinSymbol]); + + // ------------------------------------------------------- Galaxie ------- + + const onLayout = useCallback((loaded: GraphData) => { + setLayout(loaded); + }, [project]); + + /** + * Ein Klick in die Galaxie. + * + * Er geht durch denselben `followTarget` wie ein Klick im Twin: Datei + * oeffnen, Zeile anfahren, und was dort steht, sagt die Aufloesung. Ein + * Knoten ohne Datei kommt hier gar nicht an; das Panel sagt selbst, warum. + */ + const openGalaxyNode = useCallback( + (node: GraphNode) => { + setGalaxySelection(node); + setGalaxySelectionProject(project); + const target = targetRefOfNode(node); + if (target === undefined) { + return; + } + followTarget(target); + }, + [followTarget, project], + ); + + // ------------------------------------------------- Bedeutungssuche ----- + + /** + * Wie oft der Index ein Symbol erreicht, aus dem geladenen Layout. + * + * Die flache Suchform des Servers traegt keine solche Spalte + * des Fan-in; das Layout traegt sie an jedem Knoten. Solange + * keines geladen ist, hat kein Kandidat eine Zahl, und das entscheidet nur + * bei Gleichstand. + */ + const fanIn = useMemo(() => { + const readings = new Map(); + for (const node of layout?.nodes ?? []) { + const qualifiedName = node.qualified_name; + if (qualifiedName !== undefined && node.in_calls !== undefined) { + readings.set(qualifiedName, node.in_calls); + } + } + return readings; + }, [layout]); + + const fanInOf = useCallback( + (hit: { qualifiedName?: string }) => fanIn.get(hit.qualifiedName ?? '') ?? 0, + [fanIn], + ); + + /** + * Was dieses Fenster schon weiss, als Kandidatenmenge fuer die Suche. + * + * Zwei Quellen, beide schon im Speicher, beide ohne einen Serverweg: die + * Knoten der Galaxie (Name, qualifizierter Name, Datei, Zeile) und die + * Dateien des Baums. Warum daraus Vorschlaege werden duerfen und was sie + * NICHT behaupten, steht in src/search/local-suggestions.ts. + * + * Gemerkt, weil daran der Sofort-Vorschlag zu jedem Tastendruck haengt: die + * Menge neu zu bauen, sooft jemand einen Buchstaben tippt, waere die eine + * Rechnung, die diesen Weg wieder langsam machen wuerde. + */ + const localIndex = useMemo(() => { + const nodes = layout?.nodes ?? []; + if (nodes.length === 0 && rows.length === 0) { + return EMPTY_LOCAL_INDEX; + } + return { + symbols: nodes + .filter((node) => node.name.length > 0) + .map((node) => ({ + name: node.name, + qualifiedName: node.qualified_name, + kind: symbolKindOf(node.label), + filePath: node.file_path, + line: node.start_line, + })), + files: rows.filter((row) => row.kind === 'file').map((row) => row.path), + }; + }, [layout, rows]); + + const localPool = useMemo(() => localCandidates(localIndex), [localIndex]); + + /* + * Woran die Beispiele der Kommandozeile haengen (AC6c). + * + * Ein Name aus dem geladenen Index und keine feste Liste: ein Beispiel mit + * einem Symbol, das dieses Projekt nicht hat, waere eine Einladung ins + * Leere. Gerechnet aus `localIndex.symbols`, also aus den Knoten des + * Layouts, weil die den Namen UND die Art tragen; die Dateien des Baums + * bleiben aussen vor, denn eine Datei ruft nichts, und zwei der drei + * Beispiele fragen nach Aufrufen. Steht nichts, gibt es keine Beispiele. + */ + const exampleSymbol = useMemo( + () => exampleSymbolOf(localIndex.symbols.map((hit) => ({ name: hit.name, kind: hit.kind }))), + [localIndex], + ); + const commandExamples = useMemo( + () => (exampleSymbol === undefined ? [] : commandExamplesFor(exampleSymbol)), + [exampleSymbol], + ); + + /** + * Die Zeilen, die sofort dastehen koennen, ohne dass jemand gefragt wird. + * + * Erst der Praefix-Cache (die vorige Antwort des Index, neu gerankt), dann + * das schon Geladene, und beides in EINEN Rang: zwei Listen zu mischen + * hiesse, eine zweite Ordnung neben der einen zu haben, die dieses Produkt + * hat. + */ + const instantHits = useCallback( + (query: string): RankedHit[] => { + const cached = prefixCache.current; + const extendsCached = + cached !== undefined + && cached.complete + && query.length > cached.query.length + && query.startsWith(cached.query); + if (extendsCached) { + searchStats.current.prefixCacheHits += 1; + } + const pool = extendsCached + ? [...(cached?.candidates ?? []), ...localPool] + : localPool; + return pool.length === 0 + ? noHits + : rankLocalCandidates(pool, query, fanInOf, MAX_SEARCH_ROWS); + }, + [fanInOf, localPool], + ); + + /* + * Der Sofort-Vorschlag als Ref, und nicht als Abhaengigkeit des Effekts. + * + * Er haengt am Baum und an der Galaxie, und beide wachsen waehrend des + * Ladens weiter. Stuende er in der Abhaengigkeitsliste, wuerde jede + * nachgeladene Baumebene die laufende Suche zuruecksetzen und noch einmal + * fragen. Gelesen wird er beim Tastendruck, und dann ist er auf dem Stand + * dieses Bildes. + */ + const instantRef = useRef(instantHits); + instantRef.current = instantHits; + + /* + * Die Suche, in zwei Schritten statt in einem. + * + * Nutzerbefund vom 2026-08-29: die Vorschlaege erscheinen zu langsam. Bis + * dahin lief alles hintereinander: tippen, 200 ms Entprellung, ein Weg zum + * Server je Wort, dann die erste Zeile. Der Bildschirm blieb in dieser Zeit + * leer, obwohl die Antwort auf einen guten Teil der Frage schon im Browser + * lag. + * + * Jetzt passieren zwei Dinge, und die Reihenfolge ist die Aussage: + * + * 1. **Sofort**, im selben Bild wie der Tastendruck, stehen die + * vorlaeufigen Zeilen da: aus der vorigen Antwort des Index, wenn das + * Wort nur verlaengert wurde, sonst aus Baum und Galaxie. Sie tragen + * ihre Marke, damit niemand sie fuer die Antwort haelt. + * 2. **Nach der Entprellung** geht die Frage an den Index, mit einem + * Abbruch am Hals. Liefert die Antwort keine Treffer, bleiben die + * geladenen Vorschlaege erhalten und als geladen gekennzeichnet. + * + * Die Ticketpruefung bleibt und ist die eigentliche Zusicherung: eine + * Antwort, die zu spaet kommt, wird verworfen und nicht angezeigt. Der + * Abbruch spart zusaetzlich den Weg, den niemand mehr liest. + */ + useEffect(() => { + const query = command.trim(); + // Retire the previous response before the next debounce window begins. + searchTicket.current += 1; + const ticket = searchTicket.current; + searchAbort.current?.abort(); + searchAbort.current = undefined; + if (project.length === 0 || !isSearchable(query, SEARCH_MIN_QUERY)) { + setHits(noHits); + setAnsweredQuery(''); + setHitSource('index'); + setSearchStatus('ready'); + setSearchMessage(''); + return; + } + + // Schritt 1: was ohne Serverweg zu haben ist, und zwar jetzt. + const instant = instantRef.current(query); + setHits(instant); + setSelectedHit(0); + setAnsweredQuery(query); + setHitSource('loaded'); + setSearchStatus('searching'); + setSearchMessage(''); + if (instant.length > 0) { + searchStats.current.localFirst += 1; + } + + // Schritt 2: die Frage an den Index, sobald die Zeile still steht. + const timer = window.setTimeout(() => { + searchAbort.current?.abort(); + const controller = new AbortController(); + searchAbort.current = controller; + searchStats.current.serverRequests += 1; + const started = performance.now(); + searchByMeaning( + provider, + ATLAS_WORKSPACE_ROOT, + query, + { projectName: project, signal: controller.signal }, + fanInOf, + ) + .then((answer) => { + const elapsed = Math.round(performance.now() - started); + if (answer.aborted) { + searchStats.current.abortedRequests += 1; + return; + } + if (ticket !== searchTicket.current) { + searchStats.current.staleDropped += 1; + return; + } + if (ticket < searchStats.current.appliedTicket) { + // Kann nicht passieren, solange die Zeile darueber + // greift. Steht trotzdem hier, weil eine Zusicherung, + // die niemand nachrechnet, keine ist. + searchStats.current.staleAnswerWins = true; + return; + } + searchStats.current.appliedTicket = ticket; + searchStats.current.serverRoundtripMs = elapsed; + searchStats.current.roundtrips.push(elapsed); + prefixCache.current = { + query, + candidates: answer.candidates, + complete: answer.complete, + }; + const settled = settledSearchHits(answer.hits, instant); + setHits(settled.hits); + setSelectedHit(0); + setAnsweredQuery(query); + setHitSource(settled.source); + setSearchStatus('ready'); + setSearchMessage(''); + }) + .catch((error: unknown) => { + if (controller.signal.aborted) { + searchStats.current.abortedRequests += 1; + return; + } + if (ticket !== searchTicket.current) { + searchStats.current.staleDropped += 1; + return; + } + setHits(noHits); + setAnsweredQuery(query); + setHitSource('index'); + setSearchStatus('failed'); + setSearchMessage( + `${SEARCH_FAILED}: ${error instanceof Error ? error.message : String(error)}`, + ); + }); + }, SEARCH_DEBOUNCE_MS); + return () => { + window.clearTimeout(timer); + searchTicket.current += 1; + searchAbort.current?.abort(); + }; + }, [command, project, provider, fanInOf]); + + const closeSearch = useCallback(() => { + window.dispatchEvent(new Event(CLOSE_COMMAND_SEARCH_EVENT)); + searchTicket.current += 1; + searchAbort.current?.abort(); + searchAbort.current = undefined; + setCommand(''); + setHits(noHits); + setAnsweredQuery(''); + setHitSource('index'); + setSearchStatus('ready'); + setSearchMessage(''); + }, []); + + /** + * Einen Treffer waehlen: Datei oeffnen, Twin folgen, Galaxie hinterher. + * + * Die Kamerafahrt steht hier nicht: sie haengt am Twin-Subjekt, und das + * wechselt, sobald die Aufloesung in der Zieldatei da ist. Sie von hier aus + * zusaetzlich anzustossen hiesse, zweimal auf dasselbe zu zeigen und beim + * zweiten Mal auf den Treffer statt auf das aufgeloeste Symbol. + */ + const chooseHit = useCallback( + (index: number) => { + const chosen = hits[index]; + if (chosen === undefined) { + return; + } + const target = twinTargetOf({ + name: chosen.hit.name, + qualifiedName: chosen.hit.qualifiedName, + kind: chosen.hit.kind, + filePath: chosen.hit.filePath, + startLine: chosen.hit.line, + }); + if (target === undefined) { + setSearchMessage(SEARCH_NO_FILE); + return; + } + closeSearch(); + followTarget(target); + }, + [closeSearch, followTarget, hits], + ); + + // ------------------------------- Warum, Fuehrungen und die Checkliste --- + + // Was dieser Browser ueber dieses Projekt weiss, beim Projektwechsel neu + // gelesen. Beides ist an das Projekt gebunden und an nichts sonst. + useEffect(() => { + setWhyAnswer(readWhyAnswer(store, project)); + setMarks(readUnderstanding(store, project)); + setWhyReopened(false); + setTour(undefined); + setTourStep(0); + setTourMessage(''); + setWalk(undefined); + }, [project, store]); + + /** + * Wann die Frage dasteht. + * + * Von selbst nur im leeren Editorbereich und nur, solange dieser Browser + * fuer dieses Projekt keine Antwort kennt. Waehrend einer laufenden + * Fuehrung nie: die Fuehrung ist die Antwort, die gerade ausgefuehrt wird. + * Von Hand ueber das Menue jederzeit, und dann auch ueber einer offenen + * Datei, weil der Wiederaufruf sonst genau dann nicht ginge, wenn jemand + * mitten im Lesen den Modus wechseln will. + */ + const whyVisible = + tour === undefined + && !entryOpen + && !bugOpen + && project.length > 0 + && !whyDismissed + && (whyReopened || (!whyAnswer.asked && activePath.length === 0)); + + /* + * Der Stand fuer den Tastatur-Griff, bei jedem Bild neu geschrieben. + * + * Hier und nicht weiter unten, weil `whyVisible` hier entsteht: die Liste + * soll an der Stelle stehen, an der ihr letzter Bestandteil bekannt ist, + * damit niemand sie fuer vollstaendig haelt, waehrend eine Flaeche fehlt. + */ + keyboardGuardRef.current = { + /* + * Nur die zwei Flaechen, die die Tastatur wirklich fuehren: die Hilfe + * nimmt Escape, der Einstiegsdialog hat sein eigenes Suchfeld. + * + * Der Erklaeren-Bereich stand hier bis W8 mit drin, solange er ein + * Overlay ueber dem Editor war. Als ZONE gehoert er nicht mehr dazu: er + * liegt neben dem Reader, die Kommandozeile ist sichtbar, und wer bei + * offenem Flow lostippt, meint die Zeile, die er sieht. Genau diese + * Lesart hatten die Frage nach dem Warum, der Assistent und die + * Aenderungsansicht schon vorher, und aus demselben Grund. + */ + /* + * Seit W10 sind es drei: das Einstellungen-Panel fuehrt die Tastatur + * genauso wie die Hilfe (Escape schliesst es) und hat ausserdem ein + * eigenes Eingabefeld. Ohne diesen Eintrag fielen Buchstaben, die + * jemand vor dem offenen Panel tippt, in die Kommandozeile dahinter. + */ + overlayOpen: helpOpen || entryOpen || settingsOpen || projectsOpen, + walkRunning: tour !== undefined, + }; + + const closeTour = useCallback(() => { + setTour(undefined); + setTourStep(0); + // Mit der Fuehrung geht auch der Walk: das Graph-Panel wuerde sonst die + // Hierarchie einer Frage zeigen, die der Leser beendet hat. + setWalk(undefined); + }, []); + + /** + * Der Vorwaerts-Walk ab einem gewaehlten Symbol. + * + * Zuerst wird die Wahl aufgeloest und erst danach gelaufen. Eine Suchzeile + * und ein Eintrag der Zusammenfassung nennen eine Stelle, kein Symbol; der + * Walk fragt den Graphen nach den Aufrufen eines qualifizierten Namens, und + * die Wurzel muss deshalb das sein, was der Index an dieser Stelle kennt. + * Scheitert die Aufloesung, wird mit dem gewaehlten Ziel gelaufen, damit + * eine Wahl nicht folgenlos bleibt; was dabei fehlt, sagt der Walk selbst. + */ + const startEntryWalk = useCallback( + async (chosen: { name: string; qualifiedName?: string; kind: SymbolRef['kind']; filePath: string; line?: number }) => { + setEntryOpen(false); + setTourMessage(''); + const fallback = twinTargetOf({ + name: chosen.name, + qualifiedName: chosen.qualifiedName, + kind: chosen.kind, + filePath: chosen.filePath, + startLine: chosen.line, + }); + try { + let root = fallback; + if (chosen.line !== undefined) { + const resolved = await provider.resolveSymbolAt( + ATLAS_WORKSPACE_ROOT, + chosen.filePath, + chosen.line, + { projectName: project }, + ); + if (resolved.kind === 'ok') { + root = resolved.symbol; + } + } + if (root === undefined) { + setTourMessage(messages.tour.failedNoFile(chosen.name)); + return; + } + const closure = await getClosure(provider, ATLAS_WORKSPACE_ROOT, root, { + projectName: project, + generation: 1, + ...(closureBounds.depth === undefined ? {} : { depth: closureBounds.depth }), + ...(closureBounds.cap === undefined ? {} : { cap: closureBounds.cap }), + }); + setWalk(closure); + setTour(entryWalkTour(closure)); + setTourStep(0); + openExplain('walk'); + } catch (error) { + setTour(undefined); + setWalk(undefined); + setTourMessage(`${TOUR_FAILED}: ${error instanceof Error ? error.message : String(error)}`); + } + }, + [closureBounds, openExplain, project, provider], + ); + + const chooseIntent = useCallback( + (intent: WhyIntent) => { + setWhyAnswer(recordWhyAnswer(store, project, intent)); + setWhyReopened(false); + if (intent === 'understand') { + changeWorkspace('architecture'); + } else if (intent === 'entry') { + setEntryOpen(true); + } else if (intent === 'bug') { + openExplain('bug'); + } else if (intent === 'change') { + changeWorkspace('explore'); + } + }, + [openExplain, project, changeWorkspace, store], + ); + + const declineWhy = useCallback(() => { + // Der Knopf merkt sich nur die Antwort. Kein Profilwechsel: wer nichts + // gesagt hat, hat nichts ueber seine Lesetiefe gesagt. + setWhyAnswer(recordWhyAnswer(store, project)); + setWhyReopened(false); + setWhyDismissed(true); + }, [project, store]); + + /** + * Die Frage weglegen, weil der Leser angefangen hat zu lesen. + * + * Anders als der Knopf darunter schreibt das NICHTS in den Speicher dieses + * Browsers: eine Datei zu oeffnen ist keine Antwort auf "warum bist du + * hier", sondern der Beweis, dass die Frage gerade nicht dran ist. Beim + * naechsten Projekt oder nach dem Menuepunkt steht sie wieder da. + */ + const dismissWhy = useCallback(() => { + setWhyReopened(false); + setWhyDismissed(true); + }, []); + + // Ein Schritt bewegt den Reader. Die beiden Rueckrufe liegen in Refs, damit + // dieser Effekt nur an Schritt und Fuehrung haengt: `followTarget` wechselt + // seine Identitaet, sobald die geoeffnete Datei wechselt, was genau die + // Folge dieses Effekts ist, und eine Abhaengigkeit darauf waere eine + // Schleife. + const followRef = useRef(followTarget); + followRef.current = followTarget; + const openRef = useRef(openFile); + openRef.current = openFile; + + useEffect(() => { + const step = tour?.document.steps[tourStep]; + if (step === undefined) { + return; + } + const primary = step.primary; + if (primary.kind === 'symbol') { + const target = twinTargetOf({ + name: primary.name, + qualifiedName: primary.qualifiedName, + kind: primary.symbolKind, + filePath: primary.filePath, + startLine: primary.line, + }); + if (target !== undefined) { + followRef.current(target); + return; + } + } + openRef.current(primary.filePath); + }, [tour, tourStep]); + + /** + * Ein betretener Schritt ist ein besuchtes Item. + * + * Vermerkt wird erst, wenn die IR des Schritt-Symbols da ist: vorher gibt es + * keine Checkliste, und ein Vermerk auf eine Id, die dieses Symbol gar nicht + * hat, waere ein Haken an nichts. Welches Item, entscheidet + * `markableItemId` aus dem portierten tour-model: das erste core-logic-Item, + * so wie im Referenzprojekt. + */ + useEffect(() => { + if (tour === undefined || twinIr === undefined || project.length === 0) { + return; + } + const owner = twinIr.symbol.qualifiedName; + if (owner === undefined) { + return; + } + const state = understandingOf(readUnderstanding(store, project), owner, twinIr.checklist); + const item = markableItemId(state.items); + if (item === undefined) { + return; + } + setMarks(markVisited(store, project, owner, item)); + }, [tour, twinIr, project, store]); + + /** Die Zusammenfassung, sobald der Einstiegsdialog sie braucht. */ + useEffect(() => { + if ((!entryOpen && workspace !== 'architecture') || project.length === 0) { + return; + } + let cancelled = false; + setOverviewLoading(true); + setOverviewError(undefined); + provider + .architectureOverview(ATLAS_WORKSPACE_ROOT, { projectName: project, generation: 1 }) + .then((loaded) => { + if (!cancelled) { + setOverview(loaded); + setOverviewLoading(false); + } + }) + .catch((error: unknown) => { + if (!cancelled) { + setOverviewLoading(false); + setOverviewError(error instanceof Error ? error.message : String(error)); + setEntryMessage(messages.entry.overviewFailed( + error instanceof Error ? error.message : String(error), + )); + } + }); + return () => { + cancelled = true; + }; + }, [entryOpen, workspace, project, provider, overviewRevision]); + + // Die Suche des Dialogs ist dieselbe Suche wie in der Kommandozeile, mit + // derselben Entprellung. Zwei Rangfolgen fuer dasselbe Wort waeren zwei + // Antworten auf eine Frage. + useEffect(() => { + const query = entryQuery.trim(); + if (!entryOpen || project.length === 0 || !isSearchable(query, SEARCH_MIN_QUERY)) { + setEntryHits(noHits); + setEntryStatus('idle'); + return; + } + setEntryStatus('searching'); + const timer = window.setTimeout(() => { + entryTicket.current += 1; + const ticket = entryTicket.current; + findByMeaning(provider, ATLAS_WORKSPACE_ROOT, query, { projectName: project }, fanInOf) + .then((ranked) => { + if (ticket !== entryTicket.current) { + return; + } + setEntryHits(ranked); + setEntryStatus('ready'); + setEntryMessage(ranked.length === 0 ? messages.entry.noAnswer(query) : ''); + }) + .catch((error: unknown) => { + if (ticket !== entryTicket.current) { + return; + } + setEntryHits(noHits); + setEntryStatus('failed'); + setEntryMessage( + `${SEARCH_FAILED}: ${error instanceof Error ? error.message : String(error)}`, + ); + }); + }, SEARCH_DEBOUNCE_MS); + return () => window.clearTimeout(timer); + }, [entryOpen, entryQuery, fanInOf, project, provider]); + + const offered = useMemo(() => entryRows(overview), [overview]); + const entryHitRows = useMemo(() => searchRows(entryHits), [entryHits]); + + const chooseFlagged = useCallback( + (key: string) => { + const row = offered.rows.find((entry) => entry.key === key); + const target = row?.target; + if (target?.filePath === undefined) { + return; + } + void startEntryWalk({ + name: target.name, + qualifiedName: target.qualifiedName, + kind: target.kind, + filePath: target.filePath, + line: target.startLine, + }); + }, + [offered, startEntryWalk], + ); + + const chooseEntryHit = useCallback( + (index: number) => { + const chosen = entryHits[index]; + if (chosen === undefined) { + return; + } + if (chosen.hit.filePath === undefined || chosen.hit.filePath.length === 0) { + setEntryMessage(SEARCH_NO_FILE); + return; + } + void startEntryWalk({ + name: chosen.hit.name, + qualifiedName: chosen.hit.qualifiedName, + kind: chosen.hit.kind, + filePath: chosen.hit.filePath, + line: chosen.hit.line, + }); + }, + [entryHits, startEntryWalk], + ); + + // ------------------------- Der BUG-Assistent: Ziel, Lesung, Hop-Klick --- + + /** + * Das Subjekt, das der Assistent uebernimmt. + * + * Das des Twins, und nur bei einem echten Symbolwechsel: ein Assistent, der + * sich das erste Symbol merkt, das er gesehen hat, vergliche noch Pfade in + * eine Funktion, die der Leser vor zehn Minuten verlassen hat, und nichts auf + * dem Bildschirm sagte es. Umgekehrt darf ein Caret, der innerhalb desselben + * Symbols wandert, die Lesung nicht neu anstossen. + */ + useEffect(() => { + if (!bugOpen) { + return; + } + const next: BugPathNode | undefined = + twinSymbol === undefined + ? undefined + : { + name: twinSymbol.name, + qualifiedName: twinSymbol.qualifiedName, + filePath: workspacePathOf(twinSymbol.uri), + }; + setBugTarget((current) => + current?.name === next?.name && current?.qualifiedName === next?.qualifiedName + ? current + : next); + }, [bugOpen, twinSymbol]); + + useEffect(() => { + if (!bugOpen || project.length === 0 || bugTarget === undefined) { + return; + } + bugTicket.current += 1; + const ticket = bugTicket.current; + setBugStatus('loading'); + setBugMessage(''); + bugPaths(client, api, bugTarget, { project }) + .then((paths) => { + if (ticket !== bugTicket.current) { + return; + } + setBugPathsDto(paths); + setBugStatus('ready'); + }) + .catch((error: unknown) => { + if (ticket !== bugTicket.current) { + return; + } + setBugPathsDto(undefined); + setBugStatus('failed'); + setBugMessage(error instanceof Error ? error.message : String(error)); + }); + }, [api, bugOpen, bugTarget, client, project]); + + /** + * Ein Klick auf einen Hop. + * + * Der Name wird jetzt aufgeloest und nicht vorher: was in einer Aufzeichnung + * steht, hat irgendwessen Rekorder geschrieben, und ein gespeicherter + * SymbolRef ohne nodeId waere die Behauptung, dieses Symbol sei nicht + * indiziert. Loest der Index nichts auf, wird nichts geoeffnet und der + * Assistent sagt es. + */ + const openHop = useCallback( + (hop: BugPathNode) => { + setBugMessage(''); + void resolveHop(provider, ATLAS_WORKSPACE_ROOT, hop, { projectName: project }) + .then((symbol) => { + if (symbol === undefined) { + setBugMessage(messages.wizard.unresolvedHop(hop.name)); + return; + } + followRef.current(symbol); + }) + .catch((error: unknown) => { + setBugMessage(error instanceof Error ? error.message : String(error)); + }); + }, + [project, provider], + ); + + // ------------------------------------------ Das lokale Modell (W5a) ---- + + /* + * Die Praeferenz dieses Browsers, beim Projektwechsel neu gelesen, so wie + * die Antwort auf die Warum-Frage und die Vermerke. Sie haengt am Projekt + * und an nichts sonst. + */ + useEffect(() => { + setLlmPolicy(undefined); + setLlmProbe(EMPTY_SIDECAR_READING); + // Die Modellwahl und die Anzeige haengen am selben Projekt und werden + // deshalb hier mitgelesen. Beides ist pro Projekt gefuehrt, wie die + // Zonenmasse und die Vermerke: ein anderes Repository ist eine andere + // Frage und darf eine andere Antwort haben. + setSelectedModel(readModelPreference(store, project).id); + setDisplay(loadDisplaySettings(store, project)); + }, [project, store]); + + /* + * Die committete Policy des ZIELprojekts, einmal je Projekt. + * + * Sie wird gelesen, sobald ein Projekt dasteht, und nicht erst, wenn jemand + * den Schalter anfasst. Der Grund ist AC4 selbst: der Schalter soll + * WIRKUNGSLOS sein und das auch sagen, bevor ihn jemand drueckt. Eine + * Policy, die erst beim Einschalten gelesen wird, waere eine, die man einmal + * umgehen kann. + * + * Das ist eine Anfrage an den Graph-Server, nicht an den Sidecar: sie geht + * ueber denselben Weg wie jede Datei dieses Frontends und sagt ueber 4141 + * nichts. + */ + useEffect(() => { + if (project.length === 0) { + return; + } + let cancelled = false; + readLlmPolicy(client, project) + .then((reading) => { + if (!cancelled) { + setLlmPolicy(reading); + } + }) + .catch((error: unknown) => { + if (!cancelled) { + // Kein stiller Rueckfall auf "erlaubt": wer die Datei nicht + // lesen konnte, weiss nicht, ob dort eine Sperre steht. + setLlmPolicy({ + verdict: 'unreadable', + path: '.codeatlas/policy.json', + detail: error instanceof Error ? error.message : String(error), + }); + } + }); + return () => { + cancelled = true; + }; + }, [client, project]); + + // Browser models have their own explicit opt-in. Saved sidecar settings cannot enable them. + const llmMode = resolveLlmState(llmPolicy?.verdict, false); + + const llmState: SidecarState = + llmMode === 'disabled-by-policy' ? 'disabled-by-policy' : llmMode === 'off' ? 'off' : llmProbe.state; + const llmFacts = llmState === 'ready' ? llmProbe.facts : undefined; + const llmModel = llmFacts?.model ?? ''; + /* + * Die Liste des Cache-Verzeichnisses, und ob eine Auswahl daraus ueberhaupt + * wirkt. Beides gilt nur, solange der Prozess wirklich antwortet: eine Liste + * von vorhin unter einem gestorbenen Sidecar waere eine Auswahl ohne + * Gegenstelle. + */ + const llmRouter = llmState === 'ready' && llmProbe.router; + const llmModels: readonly CacheModel[] = llmState === 'ready' ? llmProbe.models : NO_CACHE_MODELS; + + /** + * Das Modell, das eine Chat-Anfrage mitschickt, oder nichts. + * + * Nur im Router-Modus: ein Einzel-Server ignoriert ein fremdes `model`-Feld + * stillschweigend (gemessen, siehe src/llm/sidecar.ts), und ein Feld, das + * nichts bewirkt, mitzuschicken hiesse, sich auf eine Wirkung zu verlassen, + * die es nicht gibt. Ohne Wahl bleibt der Rumpf der Anfrage genau der, den + * er vor W10 hatte. + */ + const requestModel = llmRouter && selectedModel.length > 0 ? selectedModel : undefined; + const selectedModelName = requestModel === undefined + ? llmModel + : llmModels.find((model) => model.id === requestModel)?.name ?? llmModel; + + /** Die Wahl umlegen: gemerkt, und sofort nachgefragt. */ + const chooseModel = useCallback( + (id: string) => { + const selected = recordModelPreference(store, project, id).id; + /* The immediate probe below runs before React necessarily renders + * the new state. Keep its request and the next explicit AI request + * on the same router id. */ + selectedModelRef.current = selected; + setSelectedModel(selected); + }, + [project, store], + ); + + /** Eine Anzeige-Einstellung umlegen: gemerkt, und sofort wirksam. */ + const changeDisplay = useCallback( + (next: GraphDisplaySettings) => { + setDisplay(saveDisplaySettings(store, project, next)); + }, + [project, store], + ); + + /** + * Der Schalter. + * + * Gegen eine Sperre tut er nichts, und zwar sichtbar nichts: die Praeferenz + * wird dann nicht einmal geschrieben. Eine gespeicherte Praeferenz, die + * niemals wirkt, waere ein Wert, der beim naechsten Projekt ohne Policy + * ploetzlich etwas einschaltet, um das an dieser Stelle niemand gebeten hat. + */ + const toggleLlm = useCallback(() => { + setBrowserAiOpen(open => !open); + }, []); + + // ------------------------------------------- Der Atlas-Chat (W5b) ------ + + /** + * Was eine Aufzeichnung auf dem Weg in ein Symbol gesehen hat. + * + * Eine Anfrage je Aufrufer an /api/trace, und keine fuenfte: die Route + * haengt `observed` an einen Hop einer Antwort, die aus dem STATISCHEN + * Graphen gerechnet ist (siehe src/traces/bug-paths.ts). Es gibt also keine + * Liste beobachteter Aufrufe, die man abfragen koennte, sondern nur die + * Frage "was wurde auf diesem Weg gesehen", und genau die wird hier + * gestellt. Ohne importierte Aufzeichnung kommt nichts zurueck, und das + * Rezept schreibt das als Notiz unter die Antwort statt es zu verschweigen. + */ + const observedInto = useCallback( + async (subject: SymbolRef, callers: readonly { qualifiedName?: string; name: string }[]) => { + const to = subject.qualifiedName; + if (to === undefined || project.length === 0) { + return [] as ObservedFact[]; + } + const found: ObservedFact[] = []; + for (const caller of callers) { + const from = caller.qualifiedName; + if (from === undefined) { + continue; + } + const answer = await api.trace(project, from, to).catch(() => undefined); + if (answer === undefined || !answer.reachable) { + continue; + } + answer.path.forEach((node, index) => { + const observed = node.observed; + const previous = answer.path[index - 1]; + if (observed !== undefined && previous !== undefined) { + found.push({ + from: previous.name, + to: node.name, + count: observed.count, + lastSeen: observed.lastSeen, + }); + } + }); + } + return found; + }, + [api, project], + ); + + /** Eine Karte oeffnen: derselbe Weg, den jeder andere Klick dieser Oberflaeche geht. */ + const openCardSource = useCallback( + (source: CardSource) => { + const target = twinTargetOf({ + name: source.name, + qualifiedName: source.qualifiedName, + kind: 'unknown', + filePath: source.filePath, + startLine: source.line, + }); + if (target === undefined) { + return; + } + followRef.current(target); + }, + [], + ); + + /** + * Eine Frage an den Atlas. + * + * Der Compiler steht vor dem optionalen Modellzug: auch wenn das Modell + * aus, gesperrt oder nicht gestartet ist, bleiben Karten und ihre Quellen + * lesbar. Der Zug nennt den echten Grund und schickt nichts an den Sidecar. + */ + const askQuestion = useCallback( + (question: string, chosenSubject?: string) => { + chatTicket.current += 1; + const id = chatTicket.current; + const base: ChatTurn = { + id, + question, + status: 'compiling', + depth: chatDepth, + cards: [], + gaps: [], + sources: [], + answer: '', + message: '', + }; + const put = (turn: ChatTurn): void => { + setChatTurns((current) => current.map((entry) => (entry.id === id ? turn : entry))); + }; + setChatTurns((current) => [...current, base]); + // Wer fragt, hat angefangen. Siehe AC6f und `dismissWhy`. + setWhyReopened(false); + setWhyDismissed(true); + // Eine Antwort hinter einer eingeklappten Flaeche waere genau die + // Stille, gegen die dieses Panel gebaut ist. Seit W8 heisst das: der + // Erklaeren-Bereich geht auf, und zwar auf DEM Reiter, der die + // Antwort traegt, statt einen zweiten Kasten aufzumachen. + openExplain('chat'); + + void askAtlas({ + question, + source: provider, + root: ATLAS_WORKSPACE_ROOT, + origin: SIDECAR_ORIGIN, + fetch: (url, init) => window.fetch(url, init), + modelName: llmModel, + modelClass: modelClassOf(llmFacts?.contextTokens), + /* + * Die Modellwahl des Lesers, wenn sie wirkt. Siehe + * `requestModel`: ohne Router bleibt sie weg, und der Rumpf der + * Anfrage ist dann derselbe wie vor W10. + */ + depth: chatDepth, + context: { + focusName: twinSymbol?.name, + focusQualifiedName: twinSymbol?.qualifiedName, + mode: bugOpen ? 'bug' : entryOpen ? 'entry' + : tour !== undefined ? 'tour' : 'none', + }, + ...(twinSymbol === undefined ? {} : { focus: twinSymbol }), + ...(chosenSubject === undefined ? {} : { chosenSubject }), + observed: observedInto, + opts: { projectName: project, generation: 1 }, + /* The normal turn always stops at the built card path. The + * model is an explicit choice in the rendered answer. */ + useModel: false, + ...(llmState === 'ready' ? {} : { + modelUnavailableReason: llmState === 'disabled-by-policy' + ? 'policy' + : llmState === 'not-running' ? 'not-running' : 'off', + }), + }, id).then(put); + }, + [ + bugOpen, chatDepth, entryOpen, llmFacts, llmMode, llmModel, llmState, + observedInto, project, provider, tour, twinSymbol, + ], + ); + + /** + * Der Leser hat aus einer Kandidatenliste gewaehlt. + * + * Dieselbe Frage noch einmal, mit dem qualifizierten Namen daneben. Ein + * NEUER Zug und keine Aenderung am alten: der Zug mit der Liste bleibt + * stehen, damit sichtbar bleibt, dass es die Wahl gab und wie sie ausging. + * Ein Zug, der sich unter der Hand in eine Antwort verwandelt, waere ein + * Verlauf, dem man nicht ansieht, was passiert ist. + */ + const pickCandidate = useCallback( + (turn: ChatTurn, candidate: SubjectCandidate) => { + askQuestion(turn.question, candidate.qualifiedName ?? candidate.name); + }, + [askQuestion], + ); + + /* + * Der Layout-Griff, bei jedem Bild neu geschrieben. + * + * Ohne Abhaengigkeitsliste, wie die anderen Naehte dieser Datei: eine Liste + * waere eine zweite Stelle, an der jemand vergisst, ein Feld nachzutragen, + * und ein Griff, der eine Lage von vorhin meldet, ist schlimmer als keiner. + */ + useEffect(() => { + globalThis.__atlasLayout = { + sizes: { ...shownZones }, + requested: { ...zones }, + bounds: Object.fromEntries( + LAYOUT_KEYS.map((key) => [key, layoutBounds(key, frame)]), + ) as Record, + defaults: defaultLayout(frame), + isDefault: sameLayout(shownZones, defaultLayout(frame)), + frame: { ...frame }, + explainOpen, + explainTab, + tabs: explainTabsRef.current.map((tab) => ({ + id: tab.id, + enabled: tab.enabled, + reason: tab.reason, + note: tab.note, + })), + storageKey: layoutStorageKey(project), + state: { + chatTurns: chatTurns.length, + walkStep: tour === undefined ? -1 : tourStep, + walkSteps: tour?.document.steps.length ?? 0, + flowStep, + flowSteps: flowView?.steps.length ?? 0, + }, + }; + }); + + useEffect(() => { + globalThis.__atlasChat = { + turns: chatTurns.map((turn) => ({ + id: turn.id, + question: turn.question, + status: turn.status, + klass: turn.klass ?? '', + rule: turn.rule ?? '', + depth: turn.depth, + cards: turn.cards.length, + cardIds: turn.cards.map((card) => card.id), + sources: turn.sources.length, + citations: turn.check?.cited ?? [], + unknownCitations: turn.check?.unknown ?? [], + uncitedLines: (turn.check?.violations ?? []) + .filter((entry) => entry.reason === 'no-citation').length, + answer: turn.answer, + refusal: turn.refusal ?? '', + tokens: turn.tokens ?? 0, + budget: turn.budget ?? 0, + tokensPerSecond: turn.tokensPerSecond ?? 0, + candidates: (turn.choice?.candidates ?? []).map((candidate) => ({ + name: candidate.name, + qualifiedName: candidate.qualifiedName ?? '', + filePath: candidate.filePath ?? '', + line: candidate.line ?? 0, + })), + askedName: turn.choice?.name ?? turn.focusFallback?.asked ?? '', + focusFallbackUsed: turn.focusFallback?.used ?? '', + })), + open: chatOpen, + height: shownZones.explainHeight, + maxHeight: layoutBounds('explainHeight', frame).max, + depth: chatDepth, + depthDefault: NEIGHBOR_DEPTH_DEFAULT, + depthOptions: [...NEIGHBOR_DEPTHS], + refineAvailable: llmState === 'ready', + refineState, + refineMessage, + // Der echte Validator, an derselben Stelle, an der ihn der Knopf + // ruft. Ein Beweislauf, der eine kaputte Antwort nachbaut, prueft + // sonst eine Kopie der Regel statt der Regel. + validateRefine: (answer: string) => { + if (pseudocode === undefined) { + return { applied: false, reason: 'no block is shown' }; + } + const outcome = applyRefinement(pseudocode, answer); + return outcome.kind === 'applied' + ? { applied: true, reason: '' } + : { applied: false, reason: outcome.reason }; + }, + }; + }); + + useEffect(() => { + globalThis.__atlasLlm = { + state: llmState, + preferenceOn: llmPreferenceOn, + policyVerdict: llmPolicy?.verdict ?? '', + policyPath: llmPolicy?.path ?? '', + policyDetail: llmPolicy?.detail ?? '', + model: llmModel, + chip: llmChipValue(llmState, llmModel), + probes: llmProbes.current, + }; + }); + + /* + * Der Griff des Einstellungen-Panels, bei jedem Bild neu. + * + * Er steht HIER und nicht im Panel, obwohl das Panel die Messungen macht: + * die Wahl, die Anzeige-Einstellungen und die Modell-Liste gehoeren der + * Anwendung und ueberleben das Zuklappen. Ein Griff, den nur ein offenes + * Panel schreibt, waere nach dem Schliessen eine Beschreibung von vorhin, + * und `llmOffMakesNoRequests` muss auch bei geschlossenem Panel lesbar sein. + */ + useEffect(() => { + globalThis.__atlasSettings = { + open: settingsOpen, + llmOn: llmMode === 'on', + probes: llmProbes.current, + router: llmRouter, + selectedModel, + cacheModels: llmModels.map((model) => ({ + id: model.id, + name: model.name, + loaded: model.loaded, + status: model.status, + active: model.id === selectedModel + || (selectedModel.length === 0 && model.name === llmModel), + })), + running: { + model: llmModel, + modelPath: llmFacts?.modelPath ?? '', + quantization: llmFacts?.quantization ?? '', + contextTokens: llmFacts?.contextTokens ?? 0, + trainedContextTokens: llmFacts?.trainedContextTokens ?? 0, + weightsBytes: llmFacts?.weightsBytes ?? 0, + parameters: llmFacts?.parameters ?? 0, + }, + suggestions: MODEL_SUGGESTIONS.map((entry) => ({ + id: entry.id, + name: entry.name, + repo: entry.repo, + modelClass: entry.modelClass, + passRate: entry.passRate, + citationCompliance: entry.citationCompliance, + citationUnmeasured: entry.citationUnmeasured ?? null, + tokensPerSecond: entry.tokensPerSecond, + bytes: entry.bytes, + command: fetchCommand(entry.repo, entry.quant), + })), + display: { ...display }, + displayDefault: { ...DEFAULT_GRAPH_DISPLAY }, + isDefaultDisplay: isDefaultDisplay(display), + storageKeys: { model: modelKey(project), display: displayKey(project) }, + measurements: Object.values(measurements), + }; + }); + + // ------------------------------------------------------- Tastatur ------ + + const overlayOpen = isSearchable(command, SEARCH_MIN_QUERY); + /** + * Ob genau ein Zeichen dasteht: zu wenig zum Suchen, genug fuer eine + * Antwort auf die Frage "warum passiert nichts". + */ + const oneCharTyped = command.trim().length > 0 && !overlayOpen; + const hitRows = useMemo(() => searchRows(hits, MAX_SEARCH_ROWS, hitSource), [hits, hitSource]); + + /* + * Der Griff der Suche und der Menuezeile, bei jedem Bild neu geschrieben. + * + * Bei jedem Bild und nicht nur bei einer Aenderung: die Zaehler stehen in + * Refs, also weiss React nichts von ihnen, und ein Effekt mit + * Abhaengigkeitsliste wuerde sie nur zufaellig mitnehmen. + */ + useEffect(() => { + globalThis.__atlasSearch = { + debounceMs: SEARCH_DEBOUNCE_MS, + minQuery: SEARCH_MIN_QUERY, + currentQuery: command.trim(), + shownQuery: answeredQuery, + shownSource: hitSource, + shownRows: hitRows.length, + localFirst: searchStats.current.localFirst, + prefixCacheHits: searchStats.current.prefixCacheHits, + serverRequests: searchStats.current.serverRequests, + serverRoundtripMs: searchStats.current.serverRoundtripMs, + roundtrips: [...searchStats.current.roundtrips], + abortedRequests: searchStats.current.abortedRequests, + staleDropped: searchStats.current.staleDropped, + staleAnswerWins: searchStats.current.staleAnswerWins, + localCandidates: localPool.length, + activatedMenus: [...activatedMenus.current], + keyListenerCapture: KEY_LISTENER_OPTIONS.capture, + }; + }); + + /** + * Die Tasten des Suchfensters. + * + * Nur solange es offen ist, und nur die vier, die ihm gehoeren. Ein + * verbrauchtes Ereignis wird als verbraucht markiert, damit das Chrome + * Escape nicht ein zweites Mal liest und das Feld den Fokus verliert, + * waehrend der Leser noch tippt. + */ + /** + * Was Enter in der Kommandozeile bedeutet. + * + * Die Entscheidung selbst steht in src/chat/command-intent.ts, samt + * Begruendung: die Suche behaelt den Vorrang, und eine Zeile mit + * Fragezeichen, eine Zeile mit @ und eine Zeile, auf die die Suche mit + * nichts geantwortet hat, sind Fragen. Hier steht nur, was mit der + * Entscheidung passiert. + */ + const enterIntent = useMemo( + () => commandIntent({ + line: command, + hitCount: hitRows.length, + answered: answeredQuery === command.trim() && searchStatus === 'ready', + }), + [answeredQuery, command, hitRows.length, searchStatus], + ); + + const onCommandKeyDown = useCallback( + (event: KeyboardEvent) => { + /* + * Der eine Befehl steht VOR der Suche und vor der Frage. + * + * Nicht, weil er wichtiger waere, sondern weil er sonst nie + * ankaeme: "reset layout" findet kein Symbol, also waere die Zeile + * eine Frage an das Modell, und der Leser bekaeme eine Antwort + * darueber, dass es kein Symbol dieses Namens gibt. Die Erkennung + * ist absichtlich eng (src/layout/layout-command.ts); jede + * Unschaerfe hier waere eine Suche, die stattdessen das Layout + * umbaut. + */ + if (event.key === 'Enter') { + const line = lineCommandOf(command); + if (line === 'reset-layout') { + event.preventDefault(); + resetLayout(); + setCommand(''); + closeSearch(); + return; + } + if (line === 'open-settings') { + event.preventDefault(); + setSettingsOpen(true); + setCommand(''); + closeSearch(); + return; + } + if (experimentalAgentsEnabled && line === 'toggle-live-agents') { + event.preventDefault(); + setLiveAgentsOn((on) => !on); + setCommand(''); + closeSearch(); + return; + } + /* + * "fullscreen" (W11b). Der Modus ist der Rahmen der + * Agenten-Ansicht; ist der Live-Modus aus, geht er mit an, + * statt dass die Zeile still nichts tut. + */ + if (experimentalAgentsEnabled && line === 'toggle-fullscreen') { + event.preventDefault(); + setLiveAgentsOn(true); + setFullscreenToggle((count) => count + 1); + setCommand(''); + closeSearch(); + return; + } + } + // Enter gilt auch dann, wenn kein Suchfenster offen ist: eine Frage + // von zwei Zeichen Laenge oeffnet keines, und sie muss trotzdem + // abschickbar sein. + if (event.key === 'Enter' && enterIntent === 'ask') { + event.preventDefault(); + const question = command.trim(); + closeSearch(); + askQuestion(question); + return; + } + if (!overlayOpen) { + return; + } + const intent = overlayIntent(event.key); + if (intent === 'none') { + return; + } + event.preventDefault(); + switch (intent) { + case 'up': + setSelectedHit((current) => moveSelection(hitRows.length, current, -1)); + return; + case 'down': + setSelectedHit((current) => moveSelection(hitRows.length, current, 1)); + return; + case 'close': + closeSearch(); + return; + case 'choose': + chooseHit(selectedHit); + return; + default: + return; + } + }, + [ + askQuestion, chooseHit, closeSearch, command, enterIntent, hitRows.length, overlayOpen, + resetLayout, selectedHit, + ], + ); + + /* + * Der eine Griff am Fenster, der entscheidet, wer eine Taste bekommt. + * + * Er liegt am Fenster und nicht am Menue oder an der Zeile: beides soll + * gelten, waehrend der Fokus im Baum, auf einem Knopf oder nirgends steht. + * WANN eine Taste was bedeutet, entscheidet src/app/keyboard.ts (dort steht + * auch, warum die Kuerzel seit dem 2026-08-29 Alt/Option tragen); WAS ein + * Kuerzel tut, steht in `menuActionsRef`, das weiter unten bei den + * Menuepunkten geschrieben wird. + * + * Die Reihenfolge ist die ganze Aussage, und sie steht hier einmal: + * + * 1. **Ein Menuekuerzel zuerst.** Alt/Option + Buchstabe, oder die + * reservierte Hilfetaste. + * 2. **Eine Flaeche, die die Tastatur fuehrt, behaelt sie.** Das sind seit + * W8 noch zwei, die Hilfe und der Einstiegsdialog (siehe + * `keyboardGuardRef`); hinter ihnen faellt kein Zeichen in eine Zeile, + * die der Leser gerade nicht ansieht. + * 3. **Einer laufenden Fuehrung gehoeren ihre vier Tasten.** Welche das + * sind, sagt `playerIntent` und nicht eine zweite Liste hier. + * 4. **Alles andere Druckbare gehoert der Kommandozeile.** Sie holt sich + * den Fokus und bekommt das Zeichen, damit Tippen sichtbar ankommt, + * statt im Hintergrund Panels zu oeffnen (Nutzerbefund 2026-08-29). + * + * Seit W7b haengt der Griff in der EINFANGENDEN Phase. Der Grund steht bei + * KEY_LISTENER_OPTIONS in src/app/keyboard.ts: der Nutzerbefund "alt plus + * letter funktioniert nur fuer atlas" liess sich nicht nachstellen, also + * wird nicht an einer Ursache geraten, sondern die eine Klasse von Ursachen + * ausgeschlossen, die diese Anwendung selbst in der Hand hat, naemlich ein + * Griff auf dem Weg nach oben, der die Taste vorher verbraucht. In der + * einfangenden Phase sieht das Fenster den Druck als erster, und niemand + * kann ihm zuvorkommen. + * + * Ein verbrauchtes Ereignis wird zusaetzlich gezeichnet (`markHandled`). + * Das ist nicht fuer die Verdrahtung, sondern fuer den Tastentest der + * Hilfeseite: `defaultPrevented` allein kann nicht sagen, ob DIESER Griff + * die Taste genommen hat oder jemand vor ihm, und genau das ist die Frage, + * die der Nutzerbefund stellt. + */ + useEffect(() => { + const onKeyDown = (event: globalThis.KeyboardEvent): void => { + const target = event.target as Element | null; + const shortcut = menuShortcutFor(event, target, WIRED_MENU_SHORTCUTS); + if (shortcut !== undefined) { + const act = menuActionsRef.current[shortcut]; + if (act !== undefined) { + event.preventDefault(); + markHandled(event, shortcut); + act(); + } + return; + } + const guard = keyboardGuardRef.current; + if (guard.overlayOpen) { + return; + } + if (guard.walkRunning && playerIntent(event.key) !== 'none') { + return; + } + }; + window.addEventListener('keydown', onKeyDown, KEY_LISTENER_OPTIONS); + return () => window.removeEventListener('keydown', onKeyDown, KEY_LISTENER_OPTIONS); + }, []); + + /* + * Escape schliesst die Hilfe, auch von ausserhalb der Hilfe. + * + * Die Seite faengt Escape schon selbst ab (sie holt sich den Fokus beim + * Aufschlagen). Dieser Griff ist fuer den Fall danach: wer im Baum oder in + * der Kommandozeile geklickt hat, waehrend die Hilfe offen steht, drueckt + * Escape und meint trotzdem die Hilfe. Er haengt nur, solange sie offen ist, + * damit Escape sonst genau so wenig bedeutet wie vorher. + */ + useEffect(() => { + if (!helpOpen) { + return; + } + const onKeyDown = (event: globalThis.KeyboardEvent): void => { + if (event.key !== 'Escape' || event.defaultPrevented) { + return; + } + event.preventDefault(); + setHelpOpen(false); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [helpOpen]); + + // Dasselbe fuer das Einstellungen-Panel, und aus demselben Grund: wer im + // Baum oder in der Kommandozeile geklickt hat, waehrend es offen steht, + // drueckt Escape und meint trotzdem das Panel. + useEffect(() => { + if (!settingsOpen) { + return; + } + const onKeyDown = (event: globalThis.KeyboardEvent): void => { + if (event.key !== 'Escape' || event.defaultPrevented) { + return; + } + event.preventDefault(); + setSettingsOpen(false); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [settingsOpen]); + + // And for the projects panel, which has input fields of its own as well. + useEffect(() => { + if (!projectsOpen) { + return; + } + const onKeyDown = (event: globalThis.KeyboardEvent): void => { + if (event.key !== 'Escape' || event.defaultPrevented) { + return; + } + event.preventDefault(); + setProjectsOpen(false); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [projectsOpen]); + + /** + * Die vier Tasten einer laufenden Fuehrung. + * + * Am Fenster und nicht an der Karte, weil der Fokus waehrend einer Fuehrung + * fast immer im Editor steht: waeren die Tasten an der Karte, muesste der + * Leser vor jedem Schritt erst dorthin klicken. Wo sie NICHT gelten, sagt + * `tourKeyForEvent`; in Kuerze: in einem echten Eingabefeld. + * + * `d` ist die vierte und bewegt die Fuehrung nicht: sie schlaegt das Bild + * zum Schritt auf. Es gibt es nur, wenn der Schritt ein Symbol hat, und + * ohne Symbol passiert nichts und die Taste bleibt unverbraucht, damit ein + * gedrueckte `d` nicht so aussieht, als haette es etwas getan. + */ + useEffect(() => { + const steps = tour?.document.steps.length ?? 0; + if (steps === 0) { + return; + } + const onKeyDown = (event: globalThis.KeyboardEvent): void => { + const key = tourKeyForEvent(event, event.target as Element | null); + if (key === undefined) { + return; + } + const intent = playerIntent(key); + if (intent === 'none') { + return; + } + if (intent === 'diagram') { + const step = tour?.document.steps[tourStepRef.current]; + if (step?.primary.kind !== 'symbol') { + return; + } + event.preventDefault(); + openExplain('flow'); + return; + } + event.preventDefault(); + if (intent === 'exit') { + closeTour(); + return; + } + if (intent === 'prev') { + setTourStep(stepMove(steps, tourStepRef.current, -1)); + return; + } + if (isLastStep(tourStepRef.current, steps)) { + closeTour(); + return; + } + setTourStep(stepMove(steps, tourStepRef.current, 1)); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [closeTour, openExplain, tour]); + + // Escape schliesst den Einstiegsdialog, auch aus seinem eigenen Suchfeld + // heraus: dort ist Escape sonst nichts, und ein Dialog ohne Ausgang ueber + // die Tastatur ist ein Dialog, aus dem man nur mit der Maus herauskommt. + useEffect(() => { + if (!entryOpen) { + return; + } + const onKeyDown = (event: globalThis.KeyboardEvent): void => { + if (event.key !== 'Escape') { + return; + } + event.preventDefault(); + setEntryOpen(false); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [entryOpen]); + + /* + * Escape schliesst die Frage nach dem Warum. + * + * Sie stand bis W8b NICHT im Escape-Griff dieser Datei: dort standen Hilfe, + * Flow, Einstieg, Assistent, Aenderungsansicht und Suchfenster, und die + * einzige Flaeche, die ueber dem Editor liegen und dort bleiben konnte, + * hatte keinen Ausgang ueber die Tastatur (Nutzerbefund 2026-08-29, AC6f). + * Der einzige Ausweg war ein Knopf, der "Not now" hiess. + * + * Die Stelle in der Reihenfolge ist die richtige und keine beliebige: die + * Hilfe und der Einstiegsdialog liegen UEBER der Frage, das Suchfenster + * gehoert der Kommandozeile, und der Erklaeren-Bereich liegt darunter. Der + * Griff nimmt die Taste also nach den drei ersten und vor dem letzten, und + * er markiert sie als verbraucht, damit der Bereich sie liegenlaesst. + */ + useEffect(() => { + if (!whyVisible) { + return; + } + const onKeyDown = (event: globalThis.KeyboardEvent): void => { + if (event.key !== 'Escape' || event.defaultPrevented) { + return; + } + if (helpOpen || entryOpen || overlayOpen) { + return; + } + event.preventDefault(); + dismissWhy(); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [dismissWhy, entryOpen, helpOpen, overlayOpen, whyVisible]); + + /* + * Escape klappt den Erklaeren-Bereich ein, und zwar zuletzt. + * + * Ein Griff fuer alle fuenf Reiter, wo bis W8 drei Griffe standen (Chat, + * Flow-Overlay, und die Sammelklausel fuer Assistent und Aenderungsansicht). + * Was dabei gilt, ist genau die Zusage aus W7c, nur breiter: Escape kostet + * NICHTS. Kein Zug wird geloescht, kein Schritt zurueckgesetzt, keine + * Fuehrung beendet; der Streifen darunter sagt weiter, was noch dasteht, und + * ein Klick auf den Reiter bringt alles zurueck. Der Nutzerbefund vom + * 2026-08-29, aus dem diese Trennung entstanden ist, gilt damit fuer jede + * der fuenf Flaechen statt nur fuer den Chat. + * + * Der Griff nimmt die Taste nur, wenn sie sonst niemand braucht, und das ist + * keine Hoeflichkeit, sondern die Reihenfolge: die Hilfe und der + * Einstiegsdialog liegen ueber dem Editor, das Suchfenster steht ueber der + * Kommandozeile, und eine Zone weiter unten, die ihnen Escape wegnaehme, + * waere eine Zone, die eine offene Flaeche stehenlaesst. Dazu das Feld unter + * dem Zeiger: wer in der Kommandozeile steht, verlaesst mit Escape die + * Zeile, und dieser Griff hat dort nichts zu suchen. + */ + useEffect(() => { + if (!explainOpen) { + return; + } + const onKeyDown = (event: globalThis.KeyboardEvent): void => { + if (event.key !== 'Escape' || event.defaultPrevented) { + return; + } + if (helpOpen || entryOpen || overlayOpen) { + return; + } + /* + * Genau EIN Feld behaelt Escape fuer sich, und es ist die + * Kommandozeile: wer dort steht, verlaesst mit Escape die Zeile. + * Nicht `isTypingTarget`, obwohl der Chat-Griff aus W7c das so + * hatte: der Editor faellt darunter, und der Flow-Erklaerer schickt + * bei jedem Schritt den Caret dorthin. Mit der weiten Pruefung waere + * Escape genau dann taub, wenn der Leser gerade Schritte gegangen + * ist, und das war bis W8 der Fall, den das Overlay mit einem + * eigenen Griff ohne diese Pruefung abgedeckt hat. + */ + if (event.target === commandInputRef.current) { + return; + } + event.preventDefault(); + collapseExplain(); + }; + window.addEventListener('keydown', onKeyDown); + return () => window.removeEventListener('keydown', onKeyDown); + }, [collapseExplain, entryOpen, explainOpen, helpOpen, overlayOpen]); + + /** + * Der Griff, an dem der Beweislauf die Fuehrung anfasst. + * + * `regenerate` ist der Determinismus-Beweis und steht deshalb hier und nicht + * im Lauf: der Lauf soll dieselbe Erzeugung noch einmal ausloesen, die die + * Anwendung benutzt, und nicht eine nachgebaute daneben. Angeboten wird sie + * nur fuer die Projekt-Fuehrung; ein Vorwaerts-Walk haengt an einer Wahl und + * ist nicht dieselbe Frage zweimal. + */ + useEffect(() => { + globalThis.__atlasTour = + tour === undefined + ? undefined + : { + id: tour.document.id, + kind: tour.kind, + title: tour.document.title, + steps: tour.document.steps.length, + index: tourStep, + paths: tour.document.steps.map((step) => step.primary.filePath), + titles: tour.document.steps.map((step) => step.title), + endNote: tour.endNote, + json: JSON.stringify(tour.document), + regenerate: + tour.kind === 'project' + ? async () => + JSON.stringify( + ( + await generateProjectTour(provider, ATLAS_WORKSPACE_ROOT, { + projectName: project, + generation: 1, + }) + ).document, + ) + : undefined, + }; + }); + + const onTreeKeyDown = useCallback( + (event: KeyboardEvent) => { + const row = rows[cursor]; + const intent = treeIntent(event.key, row); + if (intent === 'none') { + return; + } + event.preventDefault(); + switch (intent) { + case 'down': + setCursor(moveCursor(rows.length, cursor, 1)); + return; + case 'up': + setCursor(moveCursor(rows.length, cursor, -1)); + return; + case 'open': + if (row !== undefined) { + openFile(row.path); + } + return; + case 'expand': + case 'collapse': + if (row !== undefined) { + toggleDirectory(row); + } + return; + case 'toParent': { + if (row === undefined) { + return; + } + const parent = parentPath(row.path); + const index = rows.findIndex((candidate) => candidate.path === parent); + if (index >= 0) { + setCursor(index); + } + return; + } + default: + return; + } + }, + [cursor, openFile, rows, toggleDirectory], + ); + + // ------------------------------------------------------- Anzeige ------- + + /** + * Die stille Anzeige fuer das Symbol vor dem Leser. + * + * Undefiniert, solange es keine Checkliste gibt, und die Statusleiste laesst + * den Punkt dann ganz weg. "explored 0 of 0" waere ein Befund ueber das + * Symbol, wo in Wahrheit noch niemand gefragt hat. + */ + const understanding = useMemo(() => { + const owner = twinIr?.symbol.qualifiedName; + return owner === undefined || twinIr === undefined + ? undefined + : understandingOf(marks, owner, twinIr.checklist); + }, [marks, twinIr]); + const explored = exploredLabel(understanding); + + useEffect(() => { + globalThis.__atlasChecklist = { + symbol: understanding?.symbolQualifiedName ?? '', + explored: understanding?.exploration.visited ?? 0, + total: understanding?.exploration.total ?? 0, + label: explored ?? '', + marks: totalMarks(marks), + }; + }); + + const rootLevel = levels.get(''); + const treeNoteParts: string[] = []; + if (treeError.length > 0) { + treeNoteParts.push(`tree unavailable: ${treeError}`); + } else if (rootLevel !== undefined) { + treeNoteParts.push(`${rootLevel.files} files, ${rootLevel.symbols} symbols from ${TREE_ROUTE}`); + const dropped = [...levels.values()].reduce((sum, level) => sum + level.droppedNonPaths, 0); + if (dropped > 0) { + treeNoteParts.push( + dropped === 1 + ? '1 index entry names no path and is not shown' + : `${dropped} index entries name no path and are not shown`, + ); + } + if (budgetHit) { + treeNoteParts.push(`only ${EAGER_LEVEL_BUDGET} folders opened up front, the rest load on click`); + } + } else if (project.length === 0) { + treeNoteParts.push(projectDetail.length > 0 ? projectDetail : 'no project'); + } else { + treeNoteParts.push('loading the tree ...'); + } + // Die zweite Quelle bekommt ihren eigenen Satz. "Noch nicht gefragt" und + // "gefragt, nichts gefunden" sehen im Baum gleich aus und sind es nicht. + if (coverageError.length > 0) { + treeNoteParts.push(`coverage unavailable: ${coverageError}`); + } else if (!coverageAsked) { + treeNoteParts.push('joining the coverage lists ...'); + } else { + treeNoteParts.push( + `${coverage.counts.partial} partial, ${coverage.counts.skipped} skipped, ` + + `${coverage.counts.notIndexedFiles} files and ${coverage.counts.notIndexedDirs} folders ` + + `not indexed, ${coverage.counts.scopeEntries} coverage entries joined in`, + ); + } + + const chips: Chip[] = [ + { + label: messages.statusbar.chipProject, + value: project.length > 0 ? project : messages.app.noProject, + state: project.length > 0 ? 'plain' : 'absent', + }, + { + label: messages.statusbar.chipSymbols, + value: counts.nodes === undefined ? messages.statusbar.noCount : String(counts.nodes), + state: counts.nodes === undefined ? 'absent' : 'plain', + }, + { + label: messages.statusbar.chipEdges, + value: counts.edges === undefined ? messages.statusbar.noCount : String(counts.edges), + state: counts.edges === undefined ? 'absent' : 'plain', + }, + ]; + + const status: Chip[] = [ + { + label: messages.statusbar.chipProject, + value: project.length > 0 ? project : messages.app.noProject, + state: project.length > 0 ? 'ok' : 'absent', + }, + { + label: messages.statusbar.chipServer, + value: serverOk === undefined + ? messages.statusbar.serverContacting + : serverOk ? messages.statusbar.serverReady : messages.statusbar.serverUnreachable, + state: serverOk === true ? 'ok' : serverOk === false ? 'absent' : 'plain', + }, + { + label: messages.statusbar.chipPort, + value: window.location.port.length > 0 ? window.location.port : '80', + }, + { label: messages.statusbar.chipTree, value: TREE_ROUTE }, + { + label: messages.statusbar.chipSource, + value: document === undefined ? messages.statusbar.noFileLoaded : READER_RPC_TOOL, + state: document === undefined ? 'absent' : 'ok', + }, + { + label: messages.statusbar.chipGalaxy, + value: + layout === undefined + ? messages.galaxy.noLayout + : messages.galaxy.nodeCount(layout.nodes.length, !galaxyOn), + state: layout === undefined ? 'absent' : galaxyOn ? 'ok' : 'plain', + }, + /* + * Der Chip des lokalen Modells. Er steht IMMER da, auch im Zustand aus, + * und das ist der Punkt: "es gibt hier ein lokales Modell und es ist + * aus" ist eine andere Aussage als ein Chip, der erst auftaucht, wenn + * etwas laeuft. Der zweite waere aus der Ferne nicht von "dieses Produkt + * kann das nicht" zu unterscheiden. + */ + { + label: messages.statusbar.chipLlm, + value: llmChipValue(llmState, llmModel), + state: llmState === 'ready' ? 'ok' : llmState === 'starting' ? 'plain' : 'absent', + }, + ]; + /* + * Die Frische-Notiz der offenen Datei. + * + * Sie steht nur da, wenn der Server etwas zu sagen hat: `metadata_match` + * zusammen mit `no_recorded_issue` ist die Lage, in der die Datei seit dem + * Index unveraendert ist und keine Quelle etwas gemeldet hat. Ein Chip, der + * auch das anzeigt, waere eine Ampel, die immer leuchtet. + */ + const coverageAnswer = openCoverage; + const freshnessLabel = + coverageAnswer !== undefined + && freshnessNoteNeeded(coverageAnswer.status, coverageAnswer.freshness) + ? freshnessNote( + coverageAnswer.status, + coverageAnswer.freshness, + coverageAnswer.recommendedAction, + ) + : ''; + if (freshnessLabel.length > 0) { + status.push({ label: messages.statusbar.chipCoverage, value: freshnessLabel, state: 'absent' }); + } + // Die stille Anzeige steht nur da, wenn sie etwas zu zaehlen hat. + if (explored !== undefined) { + status.push({ label: messages.statusbar.chipExplored, value: explored, state: 'ok' }); + } + + /* + * Die Notiz ueber dem Editor. + * + * Nur fuer die eine Lage, in der der Text dasteht und trotzdem + * unvollstaendig gelesen wurde: eine partiell geparste Datei. Die Faelle + * ohne Inhalt sagen ihre Begruendung an der Stelle, an der sonst der Text + * stuende, und brauchen darum keine zweite Zeile darueber. + */ + const openRecord = coverage.records.get(activePath); + const coverageNote = + readerStatus === 'ready' && openRecord?.state === 'partial' + ? { state: 'partially parsed', text: partialFileNote(activePath, openRecord.reason) } + : undefined; + + useEffect(() => { + globalThis.__atlasCoverage = { + rows: rows.map((row) => ({ + path: row.path, + kind: row.kind, + coverage: row.coverage ?? 'indexed', + reason: row.coverageReason ?? '', + sources: row.coverageSources ?? [], + })), + records: [...coverage.records.values()].map((record) => ({ + path: record.path, + kind: record.kind, + state: record.state, + reason: record.reason, + sources: [...record.sources], + })), + truncations: [...coverage.truncations], + counts: { ...coverage.counts }, + metadata: { ...coverageMeta }, + open: + coverageAnswer === undefined + ? undefined + : { + path: coverageAnswer.path, + status: coverageAnswer.status, + freshness: coverageAnswer.freshness, + action: coverageAnswer.recommendedAction, + note: freshnessLabel, + }, + error: coverageError, + }; + }); + if (tourMessage.length > 0) { + status.push({ label: messages.statusbar.chipWalk, value: tourMessage, state: 'absent' }); + } + + const tabDescriptors: TabDescriptor[] = tabs.map((path) => ({ + path, + name: baseName(path), + active: path === activePath, + })); + + // Badges nur, solange der Reader die Datei zeigt, in der das Symbol steht. + // Ohne diese Bedingung malte ein Dateiwechsel fuer einen Wimpernschlag die + // Aufrufstellen des vorigen Symbols in die neue Datei. + const twinPath = twinSymbol === undefined ? '' : workspacePathOf(twinSymbol.uri); + // Gemerkt, damit der Editor seine Dekorationen nur dann neu setzt, wenn sich + // die Schritte wirklich geaendert haben, und nicht bei jedem Bild. + const badges = useMemo( + () => + twinIr !== undefined && twinPath === activePath + ? badgesForLines( + twinIr.steps.value.map((call) => ({ line: call.line, label: call.targetName })), + ) + : noBadges, + [twinIr, twinPath, activePath], + ); + + /* + * Die vier Eintraege der Atlas-Zeile, EINMAL. + * + * Sie stehen als Liste da und nicht zweimal ausgeschrieben, weil sie seit + * dem 2026-08-29 zwei Leser haben: das Menue zeichnet sie, und die Tastatur + * ruft sie. Zwei Fassungen derselben Liste waeren zwei Gelegenheiten, ein + * Etikett zu aendern und das Kuerzel dabei zu vergessen, und ein Menuepunkt, + * dessen Buchstabe etwas anderes tut, als daneben steht, ist schlimmer als + * einer ohne Buchstaben. + * + * `key` ist das Suffix von `data-menu` (also `a-why`) und bleibt, was es + * war: die Beweislaeufe fassen die Eintraege daran an. `shortcut` ist der + * Buchstabe, und er steht im Etikett in Klammern, weil ein Terminal keine + * Unterstreichung hat. + */ + const atlasEntries: readonly (MenuExtra & { shortcut: string })[] = [ + { + key: 'why', + shortcut: 'w', + label: WHY_MENU_LABEL, + title: messages.menu.why, + onSelect: () => { + // Die Frage neu zu stellen heisst, von vorn anzufangen: die + // laufende Fuehrung endet, und der Erklaeren-Bereich klappt zu, + // damit die Frage die leere Editorflaeche bekommt. + closeTour(); + setEntryOpen(false); + collapseExplain(); + setWhyDismissed(false); + setWhyReopened(true); + }, + }, + { + key: 'bug', + shortcut: 'b', + label: BUG_WIZARD_MENU_LABEL, + title: messages.menu.bug, + onSelect: () => { + /* + * Kein `closeTour()` mehr, und das ist eine Aenderung mit Grund. + * Bis W8 deckte der Assistent den Editor ab und die Schrittkarte + * lag darunter, also musste die Fuehrung weichen. Jetzt teilen + * sich beide einen Platz und wechseln ueber einen Reiter; eine + * Fuehrung zu beenden, weil jemand kurz in den Assistenten + * schaut, waere genau der Zustandsverlust, den dieser Zyklus + * abschafft. + */ + setEntryOpen(false); + setWhyReopened(false); + openExplain('bug'); + }, + }, + /* + * Der Weg zurueck aus AC3, als Menuepunkt. + * + * Er steht in derselben Zeile wie die anderen vier und traegt wie sie + * einen Buchstaben: eine Handlung, die es nur in der Kommandozeile gibt, + * ist in einer Oberflaeche, deren Vorbild die Tastatur ist, eine halbe + * Handlung. + */ + { + key: 'layout', + shortcut: 'r', + label: messages.layout.resetMenuLabel, + title: messages.layout.resetTooltip, + onSelect: resetLayout, + }, + /* + * Der Schalter im Menue, mit demselben Ausgang wie der im Panel. + * Ein Extra traegt keinen Zustand, also traegt ihn sein Etikett: + * `[l]lm off` heisst "es ist aus" und nicht "hier ausschalten". + * Diese Lesart ist dieselbe wie bei [a]tlas, dessen `data-state` + * ebenfalls die Lage und nicht die Wirkung faerbt. + */ + { + key: 'llm', + shortcut: 'l', + label: messages.browserChat.menuLabel, + title: messages.browserChat.menuTitle, + onSelect: toggleLlm, + }, + /* + * Das Einstellungen-Panel (W10), als Eintrag der Atlas-Zeile. + * + * Es schaltet, wie [a]tlas und [?]help: der Buchstabe steht fuer die + * Flaeche, und ein zweiter Druck macht sie wieder zu. Ein Eintrag, der + * nur oeffnet, waere in dieser Zeile der einzige. + */ + { + key: 'settings', + shortcut: 's', + label: messages.settings.menuLabel, + title: messages.menu.settings, + onSelect: () => setSettingsOpen((open) => !open), + }, + /* + * The projects panel, as an entry of the atlas row. It toggles like + * [s]ettings next to it: the letter stands for the surface, and a + * second press closes it again. + */ + { + key: 'projects', + shortcut: 'p', + label: messages.projects.addIndexLabel, + title: messages.projects.addIndexTitle, + onSelect: () => setProjectsOpen((open) => !open), + }, + /* + * Der Live-Modus der Agenten (W11a), als Eintrag der Atlas-Zeile. + * + * Er schaltet, wie [l]ocal llm daneben, und sein Etikett traegt den + * ZUSTAND und nicht die Wirkung: "off" heisst "es ist aus". Es gibt ihn + * hier und in der Kommandozeile ("live agents"), weil ein Modus, der per + * Vorgabe aus ist, sonst nur denen gehoert, die das Kuerzel kennen. + */ + ...(experimentalAgentsEnabled ? [{ + key: 'agents', + shortcut: 'g', + label: agentsMenuLabel(liveAgentsOn), + title: liveAgentsOn ? messages.menu.agentsOn : messages.menu.agentsOff, + onSelect: () => setLiveAgentsOn((on) => !on), + }] : []), + ]; + + /* + * Was ein Buchstabe tut, in einem Ref und nicht in einer Abhaengigkeit. + * + * Der Griff am Fenster wird einmal eingehaengt (unten, mit leerer + * Abhaengigkeitsliste), weil ein Menuekuerzel keinen Grund hat, bei jedem + * Bild ab- und wieder anzumelden. Die Handlungen darin haengen aber an + * `llmState`, an `tour` und an drei Zustandssetzern; sie in die Schliessung + * des Effekts zu schreiben hiesse, dass der Buchstabe die Lage von vor + * hundert Bildern schaltet. Das Ref ist die Stelle, an der beides + * zusammengeht: bei jedem Bild neu geschrieben, vom Griff bei jedem + * Tastendruck frisch gelesen. + */ + /** + * Eine Handlung, mit einem Vermerk davor. + * + * Der Vermerk steht dort, wo die Handlung LAEUFT, und nicht dort, wo + * geklickt wird: ein Beweislauf, der Klicks zaehlt, hat Klicks bewiesen. Er + * ist auch der Grund, aus dem Knopf und Taste dieselbe Funktion rufen statt + * zwei gleich aussehender: dass die Maus und die Tastatur dasselbe tun, + * soll keine Behauptung sein, sondern eine Eigenschaft der Verdrahtung. + */ + const menuAct = (letter: string, act: () => void) => () => { + setWorkspace('explore'); + activatedMenus.current = [...activatedMenus.current, letter]; + act(); + }; + + menuActionsRef.current = { + a: menuAct('a', () => setGalaxyOn((current) => !current)), + '?': menuAct('?', () => setHelpOpen((current) => !current)), + ...Object.fromEntries( + atlasEntries.map((entry) => [entry.shortcut, menuAct(entry.shortcut, entry.onSelect)]), + ), + }; + + const menus: Partial> = { + a: { + title: galaxyOn ? messages.menu.atlasHide : messages.menu.atlasShow, + state: galaxyOn ? 'on' : 'off', + onSelect: menuActionsRef.current['a'] ?? (() => undefined), + extras: atlasEntries.map((entry) => ({ + ...entry, + onSelect: menuActionsRef.current[entry.shortcut] ?? entry.onSelect, + })), + }, + /* + * `[?]help` schaltet, statt nur zu oeffnen, und zwar aus demselben + * Grund wie `[a]tlas`: der Buchstabe steht fuer die Flaeche, und ein + * zweiter Druck auf denselben Buchstaben macht sie wieder zu. Ein + * Menuepunkt, der nur oeffnet, waere der einzige der Zeile, dessen + * `data-state` eine Lage anzeigt, die er selbst nicht umkehren kann. + */ + '?': { + title: helpOpen ? messages.menu.helpClose : messages.menu.helpOpen, + state: helpOpen ? 'on' : 'off', + onSelect: menuActionsRef.current['?'] ?? (() => undefined), + }, + }; + + const commandOverlay = overlayOpen ? ( + + ) : oneCharTyped ? ( + /* + * Ein einziges Zeichen sucht noch nicht, und das steht jetzt DORT, wo + * die Treffer stehen wuerden. + * + * Nutzerbefund vom 2026-08-29: der Grund stand nur klein am rechten Rand + * der Zeile, und wer tippt, sieht nach oben, wo die Antwort erscheint. + * Dort stand nichts, also sah es aus, als antworte die Suche gar nicht. + * Dasselbe Fenster, dieselbe Stelle, ein Satz statt einer Leere. + */ + + ) : undefined; + + /* + * Wo die Fuehrung steht, als qualifizierter Name. + * + * Nur fuer den Vorwaerts-Gang: die Fuehrung durchs Projekt hat keinen + * Subgraphen, in dem ein Schritt liegen koennte. Ein Dateischritt hat kein + * Symbol und liefert deshalb nichts; das Bild sagt dann, dass nichts von + * diesem Walk im Fokus steht, statt auf den naechstbesten Punkt zu zeigen. + */ + const walkStep = tour?.kind === 'entry' ? tour.document.steps[tourStep]?.primary : undefined; + const walkStepQualifiedName = + walkStep !== undefined && walkStep.kind === 'symbol' ? walkStep.qualifiedName : undefined; + + const liveIr = matchingInspectorIr(twinIr, readerFocusSymbol, project, activePath); + const liveOutline = useMemo(() => liveIr ? buildPseudocode( + { kind: 'symbol', label: liveIr.symbol.name }, + { irs: [liveIr], ...(layout === undefined ? {} : { graph: layout }) }, + ) : undefined, [liveIr, layout]); + const liveSelection = readerSelection?.path === activePath && document?.path === activePath + ? readerSelection : undefined; + const currentReaderContext = useMemo(() => readerChatContext({ + project, path: activePath, status: readerStatus, document, origin: documentOrigin, selection: liveSelection, + }), [project, activePath, readerStatus, document, documentOrigin, liveSelection]); + const symbolList = fileSymbols.project === project && fileSymbols.path === activePath ? fileSymbols : undefined; + const liveCode: SelectedCodeSnapshot = { + project, filePath: activePath, symbol: readerFocusSymbol, ir: liveIr, + selection: liveSelection, document: document?.path === activePath ? document : undefined, + imports: activePath ? imports : undefined, + pseudocode: liveOutline, + status: liveIr ? 'ready' : readerStatus === 'loading' ? 'loading' + : twinStatus === 'failed' ? 'failed' : twinStatus === 'not-indexed' ? 'not-indexed' + : readerFocusSymbol ? 'loading' : 'empty', + message: twinHint || twinMessage, + coverageNote: coverageNote?.text, + fileSymbols: symbolList?.symbols, + fileSymbolsStatus: symbolList?.status ?? (activePath ? 'loading' : 'ready'), + fileSymbolsMessage: symbolList?.message, + }; + const inspector = pinnedCode?.project === project ? pinnedCode : liveCode; + const evidenceGraph = repositoryReading.snapshot; + const inspectorNode = evidenceGraph?.nodes.find(node => inspector.symbol?.qualifiedName + ? node.qualified_name === inspector.symbol.qualifiedName + : node.file_path === inspector.filePath && (node.label === 'File' || node.label === 'Module')); + const exploreEvidence = ({ filePath, line, name }: SourceEvidenceTarget): void => { + setSourceEvidence(undefined); + changeWorkspace('explore'); + setPinnedCode(undefined); + const target = twinTargetOf({ filePath, startLine: line, name: name ?? filePath, kind: 'unknown' }); + if (target) followTarget(target); + }; + const navigateEvidence = (filePath: string, line?: number, name?: string): void => { + setSourceEvidence({ project, target: { filePath, line, name } }); + }; + const readMapSource = useCallback(async (path: string, startLine = 1, maxLines = 65) => { + const qualifiedName = fileQualifiedName(repositoryReading.snapshot, path); + if (!qualifiedName) throw new Error(workspaceText.sourceOutsideSnapshot); + return client.getCodeSnippet(project, qualifiedName, { startLine, maxLines }); + }, [client, project, repositoryReading.snapshot]); + const clearGraphSelection = () => { + setGalaxySelection(undefined); + setGalaxySelectionProject(''); + setChatGraphSelection(undefined); + setProactiveSelection(undefined); + }; + const readerImpactTarget = activePath ? { filePath: activePath, + qualifiedName: markedGraphRange ? readerFocusSymbol?.qualifiedName : undefined, + name: markedGraphRange ? readerFocusSymbol?.name : undefined } : undefined; + const selectMapNode = (node: GraphNode): void => { + setGalaxySelection(node); + setGalaxySelectionProject(project); + }; + const mapSelectionPanel = selectionUnavailable ?

{selectionUnavailableText}

: activeGalaxySelection && (workspace === 'architecture' || workspace === 'galaxy') && <> + + ; + /* + * Galaxy reads its selection from the loaded scope (hand test K13). The + * repository snapshot is capped at 20,000 nodes by file path; a symbol in + * the scope but behind that cap is still a valid selection here. + */ + const galaxySelectionPanel = (scope: SelectionScope | undefined) => { + const raw = galaxySelectionProject === project ? galaxySelection : undefined; + const inScope = raw && scope?.graph.nodes.some(node => raw.qualified_name + ? node.qualified_name === raw.qualified_name && node.file_path === raw.file_path : node.id === raw.id); + const selected = inScope ? raw : activeGalaxySelection; + if (!selected) return selectionUnavailable ?

{selectionUnavailableText}

: undefined; + return ; + }; + const followInspectorTarget = (target: SymbolRef): void => { + setPinnedCode(undefined); + followTarget(target); + }; + const askInspector = (): void => { + if (!inspector.filePath) return; + setChatAttachment(workspace !== 'explore' && inspector.selection ? { ...inspector.selection, project: inspector.project, id: crypto.randomUUID() } : undefined); + // Explorer supplies its current source separately, once per request. + setChatGraphSelection(inspectorChatContext(workspace === 'explore' ? { ...inspector, document: undefined } : inspector, crypto.randomUUID())); + setBrowserAiOpen(true); + }; + const showInspectorGraph = (): void => { + const node = layout?.nodes.find(entry => inspector.symbol?.qualifiedName + ? entry.qualified_name === inspector.symbol.qualifiedName + : entry.file_path === inspector.filePath && (entry.label === 'File' || entry.label === 'Module')); + if (node) { + setGalaxySelection(node); + setGalaxySelectionProject(project); + changeWorkspace('galaxy'); + } else { + setGalaxyOn(true); + if (inspector.symbol) followTarget(inspector.symbol); + else if (inspector.filePath) openFile(inspector.filePath); + } + }; + const selectedCode = } + pinned={pinnedCode?.project === project} + onTogglePin={() => setPinnedCode(current => current?.project === project ? undefined : { ...liveCode, + selection: liveCode.selection ? { ...liveCode.selection } : undefined })} + onAsk={askInspector} onShowGraph={showInspectorGraph} onFollow={followInspectorTarget} + onPointRow={inspector.filePath === activePath ? pointRow : undefined} + onOpenFlow={inspector.symbol && inspector.symbol.qualifiedName === twinSymbol?.qualifiedName ? toggleFlow : undefined} + callOutline={inspector.pseudocode && inspector.symbol ? : undefined} />; + + const galaxy = ( + { + setGalaxySelection(node); + setGalaxySelectionProject(project); + } : undefined} + onSelectShadowNode={(node) => { + setGalaxySelection(undefined); + onSelectionEvidence(selectionEvidenceContext({ project, view: 'galaxy-coverage', source: '/api/layout missed_graph', + label: messages.galaxy.coverageSelection(node.name), selected: { id: node.sourceId, name: node.name, path: node.file_path, kind: node.label }, + limitations: 'Coverage metadata only, not a code symbol. Detailed reasons, source ranges, relationships and freshness are unavailable.' })); + }} + onLayout={onLayout} + walk={workspace === 'galaxy' ? undefined : walk} + focusWalk={focusWalk} + focusWalkStatus={focusHierarchy.status} + focusWalkMessage={focusHierarchy.message} + refit={graphRefit} + stepQualifiedName={walkStepQualifiedName} + onToggleVisible={() => setGalaxyOn((current) => !current)} + display={display} + {...(experimentalAgentsEnabled ? { + agents, + agentLayer: display.agents, + agentEffects: { + tails: display.agentTails, + trails: display.agentTrails, + waves: display.agentWaves, + timeline: display.agentTimeline, + }, + fullscreenToggle, + } : {})} + /* + * Der Vollbildmodus nimmt Escape nur, wenn sonst niemand sie + * braucht. Dieselbe Reihenfolge wie ueberall in dieser Datei: was + * ueber dem Panel liegt, hat den Vortritt. + */ + escapeTaken={helpOpen || entryOpen || overlayOpen || settingsOpen || projectsOpen} + /> + ); + + /* + * Die fuenf Reiter des Erklaeren-Bereichs, und was gerade dahinter liegt. + * + * Die Liste entsteht aus Tatsachen und nicht aus Schaltern + * (src/layout/explain-tabs.ts): ein Reiter ist bedienbar, WEIL etwas + * dahinterliegt, und nicht, weil ihn jemand aufgeschlagen hat. + */ + const explainTabList = explainTabs({ + hasProject: project.length > 0, + flowSubject: twinSymbol === undefined ? '' : twinName, + flowStep, + walkRunning: tour !== undefined, + walkStep: tourStep, + walkSteps: tour?.document.steps.length ?? 0, + chatTurns: chatTurns.length, + }).filter(tab => tab.id !== 'change'); + explainTabsRef.current = explainTabList; + + /* + * Der Inhalt des aktiven Reiters, und NUR er. + * + * Fuenf Flaechen gleichzeitig im Baum zu halten und vier davon + * wegzublenden waere die bequeme Loesung: kein Zustand ginge verloren, und + * niemand muesste darueber nachdenken, wo er liegt. Genau darum ist es die + * falsche: der Zustand laege dann in fuenf Komponenten, das Wegblenden + * waere eine CSS-Regel, an der man sich vertun kann, und die Zusicherung + * "immer nur eine Flaeche" waere eine Behauptung ueber eine Regel statt + * eine Eigenschaft der Form. Der Zustand liegt oben in dieser Datei; ein + * Reiterwechsel haengt nur die Darstellung aus. + */ + const explainPanel = + explainTab === 'flow' ? ( + + ) : explainTab === 'walk' && tour !== undefined ? ( + setTourStep(stepMove(tour.document.steps.length, tourStep, -1))} + onNext={() => + isLastStep(tourStep, tour.document.steps.length) + ? closeTour() + : setTourStep(stepMove(tour.document.steps.length, tourStep, 1)) + } + onExit={closeTour} + /* + * Das Bild zum Schritt. Der Kasten steht schon auf dem Symbol + * dieses Schrittes: die Fuehrung setzt bei jedem Schritt das + * Subjekt des Twins (der Effekt mit `followRef` weiter oben), + * und der Flow haengt am Subjekt. Aufzuschlagen ist also genau + * dieser Reiter, und ein zweiter Weg, dasselbe Symbol noch + * einmal zu setzen, waere ein zweiter Weg, an dem es abweichen + * kann. Die Fuehrung laeuft dabei weiter: sie hat ihren Reiter + * und ihren Schritt, und beide stehen noch, wenn der Leser + * zurueckwechselt. + */ + onDiagram={() => openExplain('flow')} + /> + ) : explainTab === 'chat' && chatTurns.length > 0 ? ( + setChatTurns([])} + onPickCandidate={pickCandidate} + /* + * Das Angebot aus AC6b: dieselbe Frage noch einmal, mit der + * Tiefe, die jetzt eingestellt ist. `askQuestion` liest + * `chatDepth` selbst, also braucht der Aufruf keine Zahl; und + * er legt einen NEUEN Zug an, waehrend der alte mit seiner + * alten Tiefe stehenbleibt. Genau darum geht es: zwei Antworten + * nebeneinander, von denen jede sagt, womit sie gerechnet + * wurde, statt einer, die sich unter der Hand aendert und ihre + * Zitate dabei entwertet. + */ + onAskAgain={(turn) => askQuestion(turn.question)} + aiAvailable={llmState === 'ready'} + modelName={selectedModelName} + requestModel={requestModel} + /> + ) : explainTab === 'bug' && project.length > 0 ? ( + { + // Derselbe Weg wie jede andere Suche dieser Oberflaeche. + // Der Assistent bleibt offen und uebernimmt das neue + // Subjekt, sobald der Twin es hat. + window.dispatchEvent(new Event(OPEN_COMMAND_SEARCH_EVENT)); + }} + onClose={collapseExplain} + /> + ) : undefined; + + /** Ein Griff, mit seinen Grenzen aus dem Modell. */ + const splitter = ( + key: LayoutKey, + testId: string, + orientation: 'vertical' | 'horizontal', + label: string, + invert = false, + ): JSX.Element => { + const bounds = layoutBounds(key, frame); + return ( + changeZone(key, value)} + onReset={() => resetZone(key)} + /> + ); + }; + + const attachSelection = (selection: ReaderSelection): void => { + if (selection.path !== activePath || document?.path !== activePath) return; + const range = { startLine: selection.startLine, endLine: selection.endLine + - (selection.endColumn === 1 && selection.endLine > selection.startLine ? 1 : 0) }; + const symbol = symbolMatchesReader(liveCode.symbol, selection.path, range) ? liveCode.symbol : undefined; + setReaderSelection(selection); + setChatAttachment(undefined); + setChatGraphSelection(inspectorChatContext({ ...liveCode, symbol, + ir: matchingInspectorIr(liveCode.ir, symbol, project, activePath), selection }, crypto.randomUUID())); + setBrowserAiOpen(true); + }; + // The chat grounds explanations in the selected symbol's source, read like "Read source evidence" (K14). + const readSymbolSource = useCallback((qualifiedName: string, window: { maxLines: number }) => + client.getCodeSnippet(project, qualifiedName, window), [client, project]); + const clearAttachment = (id: string): void => { + setChatAttachment(current => current?.id === id ? undefined : current); + }; + const clearChatGraphSelection = (id: string): void => { + setChatGraphSelection(current => current?.id === id ? undefined : current); + }; + + return ( + setProjectsOpen(true)} indexActivity={indexActivity} />} + onWorkspaceChange={value => { setDiagnosticsPath(undefined); changeWorkspace(value); }} + onOpenBrowserAi={openAgentSettings} + onToggleBrowserAi={() => setBrowserAiOpen(open => !open)} + agentState={localAgentState} agentModelName={agentModelName} + onOpenSystem={() => changeWorkspace('system')} + daemonState={serverOk === undefined ? 'checking' : serverOk ? 'connected' : 'disconnected'} + chatOpen={browserAiOpen} + chatDock={ setBrowserAiOpen(false)} + historyKey={project ? browserChatHistoryProjectKey(window.location.origin, project) : undefined} + proactiveSelection={proactiveSelection?.scope === selectionScope ? proactiveSelection.context : undefined} + selectionScope={selectionScope} onAgentStateChange={onAgentStateChange} onAgentModelChange={setAgentModelName} settingsRequest={agentSettingsRequest} + readerContext={workspace === 'explore' ? currentReaderContext : undefined} + pendingContext={chatGraphSelection} onContextConsumed={clearChatGraphSelection} onContextRemoved={clearChatGraphSelection} + context={browserGraphContext(inspector.ir, project, inspector.filePath, inspector.symbol ? workspacePathOf(inspector.symbol.uri) : '')} + attachment={chatAttachment} onAttachmentConsumed={clearAttachment} onAttachmentRemoved={clearAttachment} readSource={readSymbolSource} />} + readerActions={<> + + + } + readerSummary={workspace === 'explore' && activePath ? exploreEvidence({ filePath: target.filePath, line: target.line, name: target.name })} /> : undefined} + readerDetails={codeDetailsOpen ? selectedCode : undefined} + globalOverlay={<> + {sourceEvidence?.project === project && setSourceEvidence(undefined)} onExplore={exploreEvidence} />} + {welcomeOpen && { finishSetup(); setBrowserAiOpen(true); }} />} + setProjectsOpen(false)} /> + {helpOpen && setHelpOpen(false)} />} + {settingsOpen && { setSettingsOpen(false); openAgentSettings(); }} + router={llmRouter} models={llmModels} selectedModel={selectedModel} + onSelectModel={chooseModel} + display={display} onDisplay={changeDisplay} + onMeasurement={(measurement) => setMeasurements((current) => ({ + ...current, [measurement.setting]: measurement, + }))} + onClose={() => setSettingsOpen(false)} />} + } + guidance={guidance} + workspacePanel={<> + + {experimentalAgentsEnabled && } + + + + } + version={ATLAS_VERSION} + buildSuffix={ATLAS_BUILD_SUFFIX} + chips={chips} + tabs={tabDescriptors} + onSelectTab={openFile} + onCloseTab={closeTab} + tree={{ + explained: false, + projectName: project.length > 0 ? project : messages.app.noProject, + rows, + cursor, + activePath, + note: treeNoteParts.join('; '), + noteIsAbsence: treeError.length > 0 || coverageError.length > 0 || project.length === 0, + truncations: coverage.truncations, + onCursorChange: setCursor, + onOpen: (row) => openFile(row.path), + onToggle: toggleDirectory, + onKeyDown: onTreeKeyDown, + }} + breadcrumb={activePath.length > 0 ? pathSegments(activePath) : []} + truncationNote={document?.truncationNote ?? ''} + coverageNote={coverageNote} + commandValue={command} + onCommandChange={setCommand} + onCommandKeyDown={onCommandKeyDown} + commandInputRef={commandInputRef} + commandOverlay={commandOverlay} + commandPlaceholder={workspaceText.searchPlaceholder} + commandExamples={commandExamples} + /* + * Ein Beispiel wird in die Zeile GESCHRIEBEN und nicht + * abgeschickt: der Leser soll den Namen darin gegen seinen + * eigenen tauschen koennen, bevor er drueckt. Der Fokus geht + * dabei zurueck ins Feld, weil er sonst auf einem Knopf staende, + * der im selben Moment verschwindet. + */ + onCommandExample={(text) => { + setCommand(text); + window.dispatchEvent(new Event(OPEN_COMMAND_SEARCH_EVENT)); + }} + commandHint={ + enterIntent === 'ask' + ? llmState === 'ready' ? CHAT_HINT_READY : CHAT_HINT_OFF + : overlayOpen ? COMMAND_HINT_OPEN : COMMAND_HINT_IDLE + } + menus={menus} + status={status} + galaxy={galaxy} + zones={{ + leftWidth: shownZones.leftWidth, + rightWidth: shownZones.rightWidth, + twinHeight: shownZones.twinHeight, + }} + splitLeft={splitter('leftWidth', 'atlas-split-left', 'vertical', messages.layout.splitter.left)} + splitExplain={splitter( + 'explainHeight', + 'atlas-split-explain', + 'horizontal', + messages.layout.splitter.explain, + // Nach oben ziehen macht den Bereich groesser: die Kante folgt + // dem Zeiger, statt vor ihm wegzulaufen. + true, + )} + splitRight={splitter( + 'rightWidth', + 'atlas-split-right', + 'vertical', + messages.layout.splitter.right, + true, )} -
- - {/* Tabs inline in header */} - -
- - {selectedProject && ( -
- - {t.graph.selectedLabel} - - - {selectedProject} - - -
- )} -
- - {/* Content */} -
- {activeTab === "graph" ? ( - - ) : activeTab === "control" ? ( - - ) : ( - navigate("graph", p)} - /> - )} -
-
- ); + {explainPanel} + + ) : undefined} + > + {/* + * Die Frage und der Einstiegsdialog liegen UEBER dem Editor statt + * an seiner Stelle, aus demselben Grund wie der Platzhalter des + * Readers: ein Editor, der nur manchmal im Layout haengt, misst + * sich beim Auftauchen auf null und zeichnet nichts, bis ihn etwas + * anstoesst. + */} + {whyVisible && ( + + )} + {/* + * Der Einstiegsdialog liegt aus demselben Grund ueber dem Editor + * wie die Frage: eine Flaeche, die nur manchmal im Layout haengt, + * misst sich beim Auftauchen auf null. + * + * Der Assistent und die Aenderungsansicht standen bis W8 hier + * daneben. Sie sind seit W8 Reiter des Erklaeren-Bereichs: sie + * ERKLAEREN den gelesenen Code, also gehoeren sie an den Platz, an + * dem erklaert wird, und nicht vor den Text, den sie erklaeren. + */} + {entryOpen && ( + setEntryOpen(false)} + /> + )} + + + ); } diff --git a/graph-ui/src/adr/AdrWorkspace.refresh.test.tsx b/graph-ui/src/adr/AdrWorkspace.refresh.test.tsx new file mode 100644 index 000000000..a56ef8aaf --- /dev/null +++ b/graph-ui/src/adr/AdrWorkspace.refresh.test.tsx @@ -0,0 +1,60 @@ +// @vitest-environment jsdom +/* + * Review of K42 ("other Refresh buttons the same way"): Refresh in ADR read + * the decision record again and nothing on screen changed (measured in the + * browser: no visible state for a single frame). It now says that it runs and + * then when it ran and whether anything changed, as the Architecture + * Refresh buttons do. + */ +import { act } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, expect, it, vi } from 'vitest'; +import AdrWorkspace from './AdrWorkspace'; +import type { AdrRecord } from '../projects/projects-model'; +import { clearAdrDraft } from './adr-model'; + +let host: HTMLDivElement, root: Root; +const project = 'adr-refresh-test'; +const record = (content: string): AdrRecord => ({ hasAdr: true, content, updatedAt: '2026-09-14T12:00:00Z' }); +beforeEach(() => { + (globalThis as unknown as Record).IS_REACT_ACT_ENVIRONMENT = true; + clearAdrDraft(project); + host = document.createElement('div'); document.body.append(host); root = createRoot(host); +}); +afterEach(async () => { await act(async () => root.unmount()); host.remove(); clearAdrDraft(project); }); + +function deferred() { + let resolve!: (value: T) => void, reject!: (reason: unknown) => void; + const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); + return { promise, resolve, reject }; +} +const refresh = () => [...host.querySelectorAll('.adr-actions button')][0] as HTMLButtonElement; +const status = () => host.querySelector('.adr-actions .atlas-refresh-status')?.textContent; +const TIME = '\\d\\d:\\d\\d:\\d\\d'; + +it('K42: Refresh in ADR says that it runs, then when it ran and whether the record changed, or that it failed', async () => { + let next = deferred(); + const adr = vi.fn().mockResolvedValueOnce(record('# Decisions')).mockImplementation(() => next.promise); + await act(async () => root.render()); + expect(refresh().textContent).toBe('Refresh'); + expect(status()).toBe(''); + + await act(async () => refresh().click()); + expect(refresh().textContent).toBe('Refreshing…'); + expect(refresh().getAttribute('aria-disabled')).toBe('true'); + await act(async () => next.resolve(record('# Decisions'))); + expect(refresh().textContent).toBe('Refresh'); + expect(status()).toMatch(new RegExp(`^Up to date at ${TIME}: no changes since the last load$`)); + + next = deferred(); + await act(async () => refresh().click()); + await act(async () => next.resolve(record('# Decisions\n\n## Storage'))); + expect(status()).toMatch(new RegExp(`^Decisions refreshed at ${TIME}$`)); + + next = deferred(); + await act(async () => refresh().click()); + await act(async () => next.reject(new Error('offline'))); + // The alert below the toolbar says what failed and offers Try again; the status beside the button only says when. + expect(status()).toMatch(new RegExp(`^Refresh failed at ${TIME}$`)); + expect(host.querySelector('.adr-notice[role="alert"]')?.textContent).toContain('Could not load the decision record.'); +}); diff --git a/graph-ui/src/adr/AdrWorkspace.test.tsx b/graph-ui/src/adr/AdrWorkspace.test.tsx new file mode 100644 index 000000000..fb1807e0e --- /dev/null +++ b/graph-ui/src/adr/AdrWorkspace.test.tsx @@ -0,0 +1,344 @@ +// @vitest-environment jsdom +import { act } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, expect, it, vi } from 'vitest'; +import AdrWorkspace, { type AdrWorkspaceProps } from './AdrWorkspace'; +import type { AdrRecord } from '../projects/projects-model'; +import { clearAdrDraft, readAdrDraft } from './adr-model'; + +let host: HTMLDivElement; +let root: Root; +const record = (content = '# Decisions\n\n## Storage\n\nUse a local database.'): AdrRecord => ({ hasAdr: true, content, updatedAt: '2026-09-14T12:00:00Z' }); +const empty: AdrRecord = { hasAdr: false, content: '', updatedAt: '' }; +const projects = ['adr-test-alpha', 'adr-test-beta', 'adr-test-restored']; +const project = projects[0]; + +beforeEach(() => { + (globalThis as unknown as Record).IS_REACT_ACT_ENVIRONMENT = true; + for (const name of projects) clearAdrDraft(name); + host = document.createElement('div'); document.body.append(host); root = createRoot(host); +}); +afterEach(async () => { + await act(async () => root.unmount()); host.remove(); + for (const name of projects) clearAdrDraft(name); + vi.restoreAllMocks(); +}); + +function source(initial = record()) { + let current = initial; + return { + adr: vi.fn(async (_project: string) => ({ ...current })), + saveAdr: vi.fn(async (_project: string, content: string) => { current = record(content); }), + replace(next: AdrRecord) { current = next; }, + }; +} +async function render(api: AdrWorkspaceProps['source'], overrides: Partial = {}) { + await act(async () => root.render()); +} +function button(name: string): HTMLButtonElement { + const found = [...host.querySelectorAll('button')].find(candidate => candidate.textContent === name); + if (!found) throw new Error(`Missing button: ${name}`); + return found; +} +async function click(name: string) { await act(async () => button(name).click()); } +async function edit(content: string) { + if (!host.querySelector('textarea')) await click('Edit'); + await act(async () => { + const input = host.querySelector('textarea')!; + Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')!.set!.call(input, content); + input.dispatchEvent(new Event('input', { bubbles: true })); + }); +} +function deferred() { + let resolve!: (value: T) => void; + let reject!: (reason: unknown) => void; + const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); + return { promise, resolve, reject }; +} + +it('loads only while active and refreshes the same project when a clean reader is reopened', async () => { + const api = source(); + await render(api, { active: false }); + expect(api.adr).not.toHaveBeenCalled(); + await render(api); + expect(api.adr).toHaveBeenCalledWith(project); + expect(host.querySelector('article')?.textContent).toContain('Use a local database.'); + await render(api, { active: false }); + api.replace(record('# New decision')); + await render(api); + expect(api.adr).toHaveBeenCalledTimes(2); + expect(host.querySelector('article')?.textContent).toBe('New decision'); +}); + +it('preflights and verifies an explicit save; editing never writes automatically', async () => { + const api = source(); + await render(api); + await edit('# Decisions\n\nUse SQLite.'); + expect(api.saveAdr).not.toHaveBeenCalled(); + expect(readAdrDraft(project)?.content).toContain('Use SQLite.'); + await click('Save'); + expect(api.adr).toHaveBeenCalledTimes(3); + expect(api.saveAdr).toHaveBeenCalledExactlyOnceWith(project, '# Decisions\n\nUse SQLite.'); + expect(host.querySelector('textarea')).toBeNull(); + expect(host.querySelector('[role="status"]')?.textContent).toBe('Saved'); + expect(readAdrDraft(project)).toBeUndefined(); +}); + +it('retains dirty edits when hidden, switched to another project, and remounted', async () => { + const api = source(); + await render(api); + await edit('# My unsaved choice'); + await render(api, { active: false }); + await render(api); + expect(api.adr).toHaveBeenCalledTimes(1); + expect(host.querySelector('textarea')?.value).toBe('# My unsaved choice'); + await render(api, { project: projects[1] }); + expect(host.querySelector('textarea')).toBeNull(); + await render(api); + expect(host.querySelector('textarea')?.value).toBe('# My unsaved choice'); + expect(host.querySelector('strong')?.textContent).toBe(project); + await act(async () => root.unmount()); + root = createRoot(host); + await render(api); + expect(host.querySelector('textarea')?.value).toBe('# My unsaved choice'); + expect(api.saveAdr).not.toHaveBeenCalled(); +}); + +it('restores a session-stored draft and its base after reload, then catches an intervening change', async () => { + const name = projects[2]; + sessionStorage.setItem(`cbm.adr.draft:${encodeURIComponent(name)}`, JSON.stringify({ version: 1, base: record(), content: '# Restored draft' })); + const api = source(record('# Updated elsewhere')); + await render(api, { project: name }); + expect(host.querySelector('textarea')?.value).toBe('# Restored draft'); + expect(host.textContent).toContain('The server document changed.'); + expect(button('Save').disabled).toBe(true); + expect(readAdrDraft(name)?.base.content).toBe(record().content); +}); + +it('does not replace a new project with the previous project’s late read', async () => { + const pending = deferred(); + const api = source(); + api.adr.mockImplementationOnce(() => pending.promise); + await render(api); + await render(api, { project: projects[1] }); + await act(async () => pending.resolve(record('# Wrong old project'))); + expect(host.querySelector('strong')?.textContent).toBe(projects[1]); + expect(host.textContent).not.toContain('Wrong old project'); +}); + +it('shows a retry on read failure and prevents editing or saving an unread record', async () => { + const api = source(); + api.adr.mockRejectedValueOnce(new Error('offline')); + await render(api); + expect(host.textContent).toContain('Could not load the decision record.'); + expect(host.querySelector('textarea')).toBeNull(); + expect([...host.querySelectorAll('button')].map(value => value.textContent)).not.toContain('Save'); + await click('Try again'); + await edit('# Recovered'); + expect(button('Save').disabled).toBe(false); +}); + +it('keeps the draft through a busy save and succeeds on an explicit retry', async () => { + const api = source(); + api.saveAdr.mockRejectedValueOnce({ status: 423 }); + await render(api); + await edit('# Pending decision'); + await click('Save'); + expect(host.textContent).toContain('The project is busy.'); + expect(host.querySelector('textarea')?.value).toBe('# Pending decision'); + expect(readAdrDraft(project)?.content).toBe('# Pending decision'); + expect(host.querySelector('[role="status"]')?.textContent).not.toBe('Saved'); + await click('Save'); + expect(host.querySelector('[role="status"]')?.textContent).toBe('Saved'); +}); + +it('does not write when its preflight read fails', async () => { + const api = source(); + await render(api); + await edit('# Keep until the server is readable'); + api.adr.mockRejectedValueOnce(new Error('offline')); + await click('Save'); + expect(api.saveAdr).not.toHaveBeenCalled(); + expect(host.textContent).toContain('Could not check the latest version.'); + expect(readAdrDraft(project)?.content).toBe('# Keep until the server is readable'); +}); + +it('does not refresh a dirty draft on reentry after an earlier read retry', async () => { + const api = source(); + api.adr.mockRejectedValueOnce(new Error('offline')); + await render(api); + await click('Try again'); + await edit('# Keep my draft'); + await render(api, { active: false }); + await render(api); + expect(api.adr).toHaveBeenCalledTimes(2); + expect(host.querySelector('textarea')?.value).toBe('# Keep my draft'); +}); + +it('keeps a draft across project switches when session storage is unavailable', async () => { + const api = source(); + await render(api); + // Node's Web Storage can survive jsdom global setup; target the storage actually used. + const write = vi.spyOn(Object.getPrototypeOf(sessionStorage), 'setItem') + .mockImplementation(() => { throw new Error('storage unavailable'); }); + await edit('# In-memory draft'); + expect(write).toHaveBeenCalled(); + expect(host.textContent).toContain('browser storage is unavailable'); + await render(api, { project: projects[1] }); + await render(api); + expect(host.querySelector('textarea')?.value).toBe('# In-memory draft'); + expect(host.textContent).toContain('browser storage is unavailable'); +}); + +it('blocks conflicting writes until the latest document has been explicitly reviewed', async () => { + const api = source(); + await render(api); + await edit('# My change'); + api.replace(record('# Another writer\n\nPreserve this decision.')); + await click('Save'); + expect(api.saveAdr).not.toHaveBeenCalled(); + expect(button('Save').disabled).toBe(true); + expect(host.querySelector('textarea')?.value).toBe('# My change'); + expect([...host.querySelectorAll('button')].map(value => value.textContent)).not.toContain('Use latest as edit base'); + await click('Review latest version'); + expect(host.querySelector('[aria-label="Latest server version"]')?.textContent).toContain('Preserve this decision.'); + await click('Use latest as edit base'); + await edit('# Another writer\n\nPreserve this decision.\n\n## My change'); + await click('Save'); + expect(api.saveAdr).toHaveBeenCalledExactlyOnceWith(project, '# Another writer\n\nPreserve this decision.\n\n## My change'); +}); + +it('can discard its conflicting draft and load the reviewed server document', async () => { + const api = source(); + await render(api); + await edit('# My change'); + api.replace(record('# New server record')); + await click('Save'); + await click('Review latest version'); + await click('Discard draft and load latest'); + expect(host.querySelector('textarea')).toBeNull(); + expect(host.querySelector('article')?.textContent).toBe('New server record'); + expect(readAdrDraft(project)).toBeUndefined(); + expect(api.saveAdr).not.toHaveBeenCalled(); +}); + +it('does not report Saved after a verification failure and retains the draft for retry', async () => { + const api = source(); + await render(api); + await edit('# Awaiting verification'); + api.adr.mockResolvedValueOnce(record()).mockRejectedValueOnce(new Error('read-back failed')); + await click('Save'); + expect(host.textContent).toContain('The save could not be verified.'); + expect(readAdrDraft(project)?.content).toBe('# Awaiting verification'); + expect(host.querySelector('[role="status"]')?.textContent).not.toBe('Saved'); + await click('Save'); + expect(api.saveAdr).toHaveBeenCalledTimes(1); + expect(host.querySelector('[role="status"]')?.textContent).toBe('Saved'); +}); + +it('keeps a draft if the verified server content differs from what was submitted', async () => { + const api = source(); + await render(api); + await edit('# Submitted'); + api.adr.mockResolvedValueOnce(record()).mockResolvedValueOnce(record('# Concurrent replacement')); + await click('Save'); + expect(host.textContent).toContain('The server returned different content after saving.'); + expect(host.querySelector('textarea')?.value).toBe('# Submitted'); + expect(button('Save').disabled).toBe(true); + expect(readAdrDraft(project)?.content).toBe('# Submitted'); +}); + +it('disables edits during a save and ignores its completion after a project switch', async () => { + const pending = deferred(); + const api = source(); + api.saveAdr.mockImplementationOnce(() => pending.promise); + await render(api); + await edit('# Original project draft'); + await click('Save'); + expect(host.querySelector('textarea')?.disabled).toBe(true); + expect(button('Cancel').disabled).toBe(true); + await render(api, { project: projects[1] }); + await act(async () => pending.resolve()); + expect(host.querySelector('strong')?.textContent).toBe(projects[1]); + expect(host.querySelector('[role="status"]')?.textContent).not.toBe('Saved'); + expect(readAdrDraft(project)?.content).toBe('# Original project draft'); +}); + +it('enforces UTF-8 content and complete JSON request limits', async () => { + const api = source(); + await render(api); + await edit('🧭'.repeat(2001)); + expect(button('Save').disabled).toBe(true); + expect(host.textContent).toContain('8,004 / 8,000 bytes'); + await edit('\u0001'.repeat(3000)); + expect(button('Save').disabled).toBe(true); + expect(host.textContent).toContain('The encoded save request is too large.'); + await edit('a'.repeat(8000)); + expect(button('Save').disabled).toBe(false); +}); + +it('renders Markdown safely and builds a navigable outline from actual headings', async () => { + const api = source(record('# Decisions\n\n## Same title\n\n| Choice | Reason |\n| --- | --- |\n| A | B |\n\n## Same title\n\n```js\nrun();\n```\n\n[unsafe](javascript:alert(1))\n\nRaw HTML stays text\n\n![remote](https://example.com/tracker.png)')); + const scroll = vi.fn(); + const original = HTMLElement.prototype.scrollIntoView; + HTMLElement.prototype.scrollIntoView = scroll; + try { + await render(api); + expect(host.querySelector('table')).not.toBeNull(); + expect(host.querySelector('pre code')?.textContent).toBe('run();\n'); + expect(host.querySelector('article strong, img, a[href^="javascript:"]')).toBeNull(); + const nav = host.querySelector('[aria-label="On this page"]')!; + expect([...nav.querySelectorAll('button')].map(value => value.textContent)).toEqual(['Same title', 'Same title']); + const headings = [...host.querySelectorAll('article h1, article h2')]; + expect(new Set(headings.map(value => value.id)).size).toBe(3); + await act(async () => nav.querySelectorAll('button')[1].click()); + expect(scroll).toHaveBeenCalledOnce(); + expect(document.activeElement).toBe(headings[2]); + } finally { HTMLElement.prototype.scrollIntoView = original; } +}); + +it('does not reserve an outline column for documents without headings', async () => { + await render(source(record('A short decision without a heading.'))); + expect(host.querySelector('[aria-label="On this page"]')).toBeNull(); + expect(host.querySelector('.adr-body')?.getAttribute('data-outline')).toBe('false'); +}); + +it('creates a generic decision scaffold without saving until requested and cancels explicitly', async () => { + const api = source(empty); + await render(api); + await click('Create decision record'); + const text = host.querySelector('textarea')?.value ?? ''; + expect(text).toContain('### Context'); + expect(text).toContain('### Alternatives'); + expect(text).not.toContain(project); + expect(api.saveAdr).not.toHaveBeenCalled(); + await click('Preview'); + expect(host.querySelector('article h1')?.textContent).toBe('Architecture decisions'); + await click('Cancel'); + expect(readAdrDraft(project)).toBeUndefined(); + expect(host.querySelector('textarea')).toBeNull(); +}); + +it('warns before unloading only while a draft is dirty', async () => { + const api = source(); + await render(api); + let event = new Event('beforeunload', { cancelable: true }); + window.dispatchEvent(event); + expect(event.defaultPrevented).toBe(false); + await edit('# Draft'); + event = new Event('beforeunload', { cancelable: true }); + window.dispatchEvent(event); + expect(event.defaultPrevented).toBe(true); + await click('Cancel'); + event = new Event('beforeunload', { cancelable: true }); + window.dispatchEvent(event); + expect(event.defaultPrevented).toBe(false); +}); + +it('does not request or offer writes without a selected project', async () => { + const api = source(); + await render(api, { project: '' }); + expect(api.adr).not.toHaveBeenCalled(); + expect(host.textContent).toContain('Select a project'); + expect(host.querySelector('button')).toBeNull(); +}); diff --git a/graph-ui/src/adr/AdrWorkspace.tsx b/graph-ui/src/adr/AdrWorkspace.tsx new file mode 100644 index 000000000..a329e4ba6 --- /dev/null +++ b/graph-ui/src/adr/AdrWorkspace.tsx @@ -0,0 +1,218 @@ +import { useEffect, useId, useRef, useState } from 'react'; +import type { JSX } from 'react'; +import ChatMarkdown from '../browser-ai/ChatMarkdown'; +import type { ProjectsSource } from '../projects/ProjectsPanel'; +import type { AdrRecord } from '../projects/projects-model'; +import { ADR_CONTENT_LIMIT, adrSize, appendDecision, clearAdrDraft, readAdrDraft, sameAdr, storeAdrDraft } from './adr-model'; +import { adrStrings as s } from './adr-strings'; +import { RefreshControl, useRefreshFeedback } from '../ui/refresh/refresh-feedback'; +import './adr.css'; + +export interface AdrWorkspaceProps { + project: string; + active: boolean; + source: Pick; +} + +function busyError(error: unknown): boolean { + return typeof error === 'object' && error !== null && 'status' in error && error.status === 423; +} + +interface EditorState { + base?: AdrRecord; + content: string; + editing: boolean; + preview: boolean; + phase: 'idle' | 'loading' | 'ready' | 'error'; + error: string; + notice: string; + conflict?: AdrRecord; + reviewing: boolean; + /** How many readings of the record started; the Refresh status speaks about one of them (review of K42). */ + reads: number; +} + +function ProjectAdrWorkspace({ project, active, source }: AdrWorkspaceProps): JSX.Element { + const [state, setState] = useState(() => { + const draft = readAdrDraft(project); + return { base: draft?.base, content: draft?.content ?? '', editing: !!draft, preview: false, + phase: 'idle', error: '', notice: draft ? s.restored : '', reviewing: false, reads: 0 }; + }); + const [saving, setSaving] = useState(false); + const [storageAvailable, setStorageAvailable] = useState(true); + const [reload, setReload] = useState(0); + const stateRef = useRef(state); + stateRef.current = state; + const mounted = useRef(true); + const savingRef = useRef(false); + const loadTicket = useRef(0); + const lastReload = useRef(0); + const hasLoaded = useRef(false); + const editor = useRef(null); + const document = useRef(null); + const headingPrefix = useId(); + const [outline, setOutline] = useState<{ id: string; label: string; level: number }[]>([]); + const dirty = !!state.base && state.content !== state.base.content; + const size = adrSize(project, state.content); + + useEffect(() => { mounted.current = true; return () => { mounted.current = false; loadTicket.current += 1; }; }, []); + + useEffect(() => { + const current = stateRef.current; + const forced = reload !== lastReload.current; + lastReload.current = reload; + if (!active || savingRef.current || (hasLoaded.current && current.phase === 'ready' && current.base && current.content !== current.base.content && !forced)) return; + const ticket = ++loadTicket.current; + setState(value => ({ ...value, phase: 'loading', error: '', reads: value.reads + 1 })); + void source.adr(project).then(record => { + if (!mounted.current || ticket !== loadTicket.current) return; + hasLoaded.current = true; + setState(value => { + if (value.base && value.content !== value.base.content) { + return { ...value, phase: 'ready', error: '', conflict: sameAdr(value.base, record) ? undefined : record }; + } + return { ...value, base: record, content: record.content, phase: 'ready', error: '', conflict: undefined }; + }); + }, error => { + if (mounted.current && ticket === loadTicket.current) setState(value => ({ ...value, phase: 'error', error: busyError(error) ? s.busy : s.readFailed })); + }); + return () => { if (ticket === loadTicket.current) loadTicket.current += 1; }; + }, [active, project, source, reload]); + + useEffect(() => { + if (state.base && dirty) setStorageAvailable(storeAdrDraft(project, { version: 1, base: state.base, content: state.content })); + else if (state.base) clearAdrDraft(project); + }, [project, state.base, state.content, dirty]); + + useEffect(() => { + if (!dirty) return; + const warn = (event: BeforeUnloadEvent) => { event.preventDefault(); event.returnValue = ''; }; + window.addEventListener('beforeunload', warn); + return () => window.removeEventListener('beforeunload', warn); + }, [dirty]); + + useEffect(() => { + const headings = [...(document.current?.querySelectorAll('h1, h2, h3, h4, h5, h6') ?? [])]; + const entries = headings.map((heading, index) => { + heading.id = `${headingPrefix}-heading-${index}`; + heading.tabIndex = -1; + return { id: heading.id, label: heading.textContent ?? '', level: Number(heading.tagName.slice(1)) }; + }); + const level = entries.some(heading => heading.level === 2) ? 2 : Math.min(...entries.map(heading => heading.level)); + setOutline(entries.filter(heading => heading.level === level)); + }, [state.content, state.editing, state.preview, state.phase, headingPrefix]); + + useEffect(() => { if (state.editing && active) editor.current?.focus(); }, [state.editing, active]); + + const acceptSaved = (record: AdrRecord): void => { + clearAdrDraft(project); + setState(value => ({ ...value, base: record, content: record.content, phase: 'ready', editing: false, + error: '', notice: s.saved, conflict: undefined, reviewing: false })); + }; + const save = async (): Promise => { + const current = stateRef.current; + if (savingRef.current || current.phase !== 'ready' || !current.base || current.conflict + || current.content === current.base.content || !adrSize(project, current.content).valid) return; + savingRef.current = true; + loadTicket.current += 1; + setSaving(true); + setState(value => ({ ...value, error: '', notice: '' })); + let stage: 'check' | 'write' | 'verify' = 'check'; + try { + const latest = await source.adr(project); + if (!mounted.current) return; + if (!sameAdr(current.base, latest)) { + if (latest.content === current.content && (latest.hasAdr || current.content === '')) acceptSaved(latest); + else setState(value => ({ ...value, conflict: latest, reviewing: false })); + return; + } + stage = 'write'; + await source.saveAdr(project, current.content); + if (!mounted.current) return; + stage = 'verify'; + const verified = await source.adr(project); + if (!mounted.current) return; + if (verified.content !== current.content || (!verified.hasAdr && current.content !== '')) { + setState(value => ({ ...value, conflict: verified, reviewing: false, error: s.mismatch })); + return; + } + acceptSaved(verified); + } catch (error) { + if (mounted.current) setState(value => ({ ...value, error: busyError(error) ? s.busy + : stage === 'check' ? s.checkFailed : stage === 'verify' ? s.verifyFailed : s.saveFailed })); + } finally { + savingRef.current = false; + if (mounted.current) setSaving(false); + } + }; + const startEditing = (append = false): void => setState(value => ({ ...value, editing: true, + content: append ? appendDecision(value.content) : value.content, error: '', notice: '' })); + const cancel = (): void => { + clearAdrDraft(project); + setState(value => ({ ...value, base: value.conflict ?? value.base, content: (value.conflict ?? value.base)?.content ?? '', editing: false, + preview: false, conflict: undefined, reviewing: false, error: '', notice: '' })); + }; + const useLatest = (discard: boolean): void => setState(value => value.conflict ? ({ ...value, + base: value.conflict, content: discard ? value.conflict.content : value.content, + editing: !discard, conflict: undefined, reviewing: false, phase: 'ready', error: '', notice: discard ? '' : s.rebased }) : value); + const ready = state.phase === 'ready'; + const documentVisible = !state.editing || state.preview; + /* + * Review of K42: Refresh read the record again and nothing on screen + * changed. Now it says that it runs, then when and whether the record + * changed; a failure only gets its time here, the alert below says what + * failed and offers Try again. + */ + const refresh = useRefreshFeedback({ key: String(state.reads), settled: state.phase === 'ready' || state.phase === 'error', + value: state.base && { content: state.base.content, updatedAt: state.base.updatedAt, hasAdr: state.base.hasAdr }, error: state.phase === 'error' ? '' : undefined }); + + return
+
+
{project}{saving ? s.saving : dirty ? s.draft : state.notice || (state.base?.updatedAt ? s.edited : s.noDate)} + {!dirty && !state.notice && state.base?.updatedAt && }
+
{state.editing ? <> + + + + : <> + { refresh.begin(); setReload(value => value + 1); }} /> + {ready && state.base?.hasAdr && } + {ready && state.base?.hasAdr && } + }
+
+ {state.error &&
{state.error} + {state.phase === 'error' && }
} + {dirty && state.notice &&

{state.notice}

} + {dirty && !storageAvailable &&

{s.storageUnavailable}

} + {state.conflict &&
+
{s.conflict}
+ {state.reviewing && <>
+

{s.latest}

{state.conflict.content ? :

{s.latestEmpty}

} +
+
} +
} + {state.phase === 'loading' && !state.base &&

{s.loading}

} + {state.editing &&
{s.size(size.bytes)} + {!size.valid && {size.bytes > ADR_CONTENT_LIMIT ? s.sizeExceeded : s.bodyExceeded}}
} + {state.base && (state.editing || state.base.hasAdr) &&
0 && documentVisible}> + {state.editing &&