diff --git a/docs/developer-guide/working-with-ai.md b/docs/developer-guide/working-with-ai.md
new file mode 100644
index 0000000000..629ba7b126
--- /dev/null
+++ b/docs/developer-guide/working-with-ai.md
@@ -0,0 +1,149 @@
+import {DeveloperDocsTabs} from '@site/src/components/docs/developer-docs-tabs';
+
+# Working with AI Coding Agents
+
+
+
+AI coding agents can help design, implement, and debug luma.gl applications when they
+work from the version actually installed in the application and can observe the result
+in a real browser. This page describes a practical workflow for application developers
+first, followed by additional guidance for contributors to the luma.gl repository.
+
+## Start from local truth
+
+Do not ask an agent to rely on its memory of luma.gl APIs. First give it the application
+and ask it to identify the installed package versions:
+
+```bash
+npm ls @luma.gl/core @luma.gl/engine @luma.gl/shadertools @luma.gl/webgpu @luma.gl/webgl
+```
+
+Use the equivalent command for the project's package manager. The agent should then
+inspect each installed package's `package.json`, exported TypeScript declarations, and
+the documentation for that release. Installed declarations are the authority for exact
+constructor props, methods, and types; current website documentation can describe a
+newer release.
+
+When a project is pinned to an older release, use the documentation in the matching
+GitHub release branch. Ask the agent to cite the declaration or documentation page it
+used when an API choice is uncertain.
+
+## Choose the right luma.gl level
+
+Give the agent the smallest API surface appropriate to the task:
+
+| Level | Start here when | Typical packages |
+| --- | --- | --- |
+| Engine API | Building a rendered application, model, animation loop, geometry, picking, or common GPU transform | `@luma.gl/engine` plus one or more adapters |
+| Core GPU API | Managing devices, buffers, textures, pipelines, bindings, render passes, or compute passes directly | `@luma.gl/core`, `@luma.gl/webgpu`, `@luma.gl/webgl` |
+| Shader API | Assembling reusable shader modules, hooks, plugins, or matching WGSL and GLSL implementations | `@luma.gl/shadertools`, usually with Engine or Core |
+
+Most applications should start with `Model` and `AnimationLoop` from Engine, then use
+Core resources only where explicit control is useful. Shader tooling composes shader
+code; it does not replace Engine or Core execution. See
+[A Tale of Three APIs](/docs/api-guide) for the complete object model.
+
+## Install the luma.gl skill
+
+The repository contains an official progressive Agent Skill that routes application
+design, portability, GPU debugging, and repository contribution tasks:
+
+```bash
+npx skills add visgl/luma.gl --skill lumagl
+```
+
+After installation, ask your agent to use the `lumagl` skill. A useful prompt names the
+goal, supported backends, installed luma.gl version, and observable success condition:
+
+> Use the lumagl skill to add this render path for WebGPU and WebGL 2. Confirm the
+> installed package APIs, run the typecheck, exercise both backends in a real browser,
+> and report console errors and screenshots.
+
+The skill provides procedural judgment and debugging order. It does not copy the entire
+API reference into every conversation.
+
+## Give agents exact documentation
+
+The website publishes [llms.txt](https://luma.gl/llms.txt), an index of current
+tutorials, API guides, API references, and developer guides. Each listed page has a raw
+Markdown sibling. For example:
+
+- [Getting Started Markdown](https://luma.gl/docs/getting-started.md)
+- [Portable Shaders Markdown](https://luma.gl/docs/api-guide/shaders/writing-portable-shaders.md)
+- [Model API Markdown](https://luma.gl/docs/api-reference/engine/model.md)
+
+Ask the agent to fetch only the pages needed for the task. The Markdown is generated
+from the rendered site, so tabs, cards, and other MDX components become readable
+content rather than JSX. Generated TypeDoc pages are included as raw Markdown
+references as well.
+
+`llms.txt` is an inference-time documentation index. It helps an agent select context;
+it is not crawler access control, a training opt-out, or a substitute for accurate,
+versioned documentation. Crawler policy belongs in mechanisms such as `robots.txt` and
+provider controls.
+
+## Require an observable verification loop
+
+Typechecking is necessary but cannot prove that GPU code renders correctly. Ask the
+agent to produce evidence in this order:
+
+1. Run the application typecheck and relevant unit tests.
+2. Open the actual application in a current browser, not a DOM-only test environment.
+3. Capture console messages, page errors, failed requests, and shader compiler output.
+4. Confirm the selected device and canvas context, then inspect layouts, bindings,
+ uploaded data, render-pass state, and draw counts.
+5. Capture a screenshot after the expected frame has rendered.
+6. Exercise WebGPU and WebGL 2 separately when the feature is intended to be portable.
+7. Use browser GPU diagnostics or a frame debugger when logs and screenshots do not
+ isolate the fault.
+
+For a blank canvas, debug the pipeline from the outside inward:
+device and adapter availability → canvas context → shader compilation → layouts and
+bindings → uploaded data → render pass and draw call. Changing shaders at random before
+confirming device and binding state usually hides the original failure.
+
+When a feature is genuinely WebGPU-only, state that constraint and test the unsupported
+path explicitly. Do not ask the agent to invent a WebGL fallback for compute shaders,
+storage textures, or another capability the backend does not provide.
+
+## Working inside the luma.gl repository
+
+Repository contributors should direct the agent to read the root `AGENTS.md` before
+editing. It records coding conventions, ownership boundaries, and the stable command
+surface. Prefer these root commands over runner-specific invocations:
+
+```bash
+yarn test-node
+yarn test-browser
+yarn test-headless
+yarn test-coverage
+yarn website-debug --example hello-triangle --backend webgpu-core
+yarn website-debug --example hello-triangle --backend webgl2
+```
+
+`website-debug` records the final URL, a screenshot, a WebGPU capability probe, and page
+diagnostics under `.playwright-artifacts/`. Ask the agent to inspect those artifacts and
+report concrete evidence instead of concluding that a browser task passed because the
+process exited successfully.
+
+Before merge, follow the checks required by `AGENTS.md`. In particular, targeted tests
+do not replace the final repository `yarn build` and `yarn test` gates. This workflow
+does not add an AI disclosure requirement or otherwise change the contribution policy.
+
+## How other frameworks inform this model
+
+| Project | Forward-looking support | Lesson used by luma.gl |
+| --- | --- | --- |
+| [Next.js](https://nextjs.org/blog/agentic-future) | Agent-oriented documentation and upgrade workflows | Make version-aware framework knowledge easy to retrieve |
+| [Nuxt](https://nuxt.com/docs/4.x/guide/ai/llms-txt) | Documented `llms.txt` variants for agent context | Publish a curated index and page-level Markdown |
+| [Svelte](https://svelte.dev/llms.txt) | A public machine-readable documentation index | Give agents stable, direct documentation URLs |
+| [TanStack](https://tanstack.com/intent/latest/docs/overview) | Intent and procedural guidance beyond API lookup | Encode task routing and judgment in a skill |
+| [MapLibre](https://github.com/maplibre/maplibre-agent-skills) | Installable framework-specific Agent Skills | Ship the skill with the framework source |
+
+luma.gl combines these ideas into four layers: accurate human documentation, raw
+Markdown plus `llms.txt` for knowledge retrieval, one installable skill for procedural
+work, and an offline corpus for manually comparing agent behavior. The repository
+documents the [evaluation protocol](https://github.com/visgl/luma.gl/blob/master/test/llm/README.md);
+CI validates the corpus but does not invoke a model. An embedded assistant, MCP server,
+additional specialized skills, and a monolithic `llms-full.txt` are intentionally
+deferred until evaluation or runtime-observability evidence shows they are needed.
diff --git a/docs/table-of-contents.json b/docs/table-of-contents.json
index ae5fa628c0..7178d58c7a 100644
--- a/docs/table-of-contents.json
+++ b/docs/table-of-contents.json
@@ -362,6 +362,7 @@
"label": "Developer Guide",
"items": [
"developer-guide/README",
+ "developer-guide/working-with-ai",
"developer-guide/installing",
"developer-guide/editing",
"developer-guide/debugging",
diff --git a/package.json b/package.json
index a054b9b17d..1f96508f81 100644
--- a/package.json
+++ b/package.json
@@ -69,7 +69,8 @@
"h3-js": "^4.4.0",
"pre-commit": "^1.2.2",
"vite": "^8.0.0",
- "vitest": "^4.0.18"
+ "vitest": "^4.0.18",
+ "yaml": "^2.8.1"
},
"resolutions": {
"@docusaurus/plugin-client-redirects": "^3.9.2",
diff --git a/skills/lumagl/SKILL.md b/skills/lumagl/SKILL.md
new file mode 100644
index 0000000000..09ffeba169
--- /dev/null
+++ b/skills/lumagl/SKILL.md
@@ -0,0 +1,63 @@
+---
+name: lumagl
+description: Design, implement, update, and debug luma.gl applications and repository changes with version-aware API selection, WebGPU/WebGL portability, browser-based GPU diagnosis, and project-specific verification. Use when working with @luma.gl packages, luma.gl shaders or resources, blank or incorrect canvases, backend compatibility, or the visgl/luma.gl repository.
+---
+
+# luma.gl
+
+Work from the installed packages and observable browser behavior. Do not substitute
+model memory for the consuming project's declarations, and do not claim rendering
+success from typechecking alone.
+
+## Establish local truth
+
+1. Identify whether the task is in a consumer application or the luma.gl repository.
+2. Inspect the package manager lockfile and installed `@luma.gl/*` versions.
+3. Read the installed packages' `package.json` exports and TypeScript declarations for
+ every API used. Treat those declarations as authoritative.
+4. Consult documentation for the same release. When using current
+ [luma.gl documentation](https://luma.gl/llms.txt), fetch only the raw Markdown pages
+ relevant to the task.
+5. State any version or backend constraint that changes the implementation.
+
+Never silently modernize code to an API that is absent from the installed declarations.
+Do not infer API availability from a code sample without checking its version.
+
+## Route the task
+
+- For application architecture, package selection, or API-level decisions, read
+ [references/architecture.md](references/architecture.md).
+- For code that must run on WebGPU and WebGL 2, or for a backend-specific feature, read
+ [references/portability.md](references/portability.md).
+- For a blank canvas, shader failure, bad output, device loss, or binding problem, read
+ [references/debugging.md](references/debugging.md).
+- For work inside `visgl/luma.gl`, read
+ [references/contributing.md](references/contributing.md) and the repository's root
+ `AGENTS.md` before editing.
+
+Read every reference that applies; portability and debugging often overlap.
+
+## Implement
+
+1. Select Engine, Core GPU, and Shader APIs deliberately.
+2. Preserve the application's existing package manager, build setup, style, and
+ resource-ownership conventions.
+3. Keep the first change minimal and independently verifiable.
+4. Label GPU resources with meaningful `id` values when the local API supports them.
+5. Destroy owned GPU resources and animation infrastructure at the matching lifecycle
+ boundary. Do not destroy borrowed resources.
+6. Keep backend-specific code behind an explicit capability or adapter boundary.
+
+## Verify
+
+Run the project's typecheck and focused tests, then exercise the actual application in
+a current browser. Collect console output, page errors, failed requests, shader
+compiler messages, and a screenshot after the expected frame.
+
+For portable rendering, run WebGPU and WebGL 2 explicitly. For a WebGPU-only feature,
+verify the supported path and a clear unsupported path. Report which backends and
+browser environment were actually observed.
+
+Do not stop at a successful process exit if the expected frame was not observed. When
+the output is wrong, follow the debugging sequence in the reference instead of making
+unrelated shader or pipeline changes.
diff --git a/skills/lumagl/references/architecture.md b/skills/lumagl/references/architecture.md
new file mode 100644
index 0000000000..413df78f6b
--- /dev/null
+++ b/skills/lumagl/references/architecture.md
@@ -0,0 +1,74 @@
+# Architecture and API Selection
+
+## Source precedence
+
+Use evidence in this order:
+
+1. The consuming project's lockfile and installed package versions.
+2. Installed `package.json` exports and TypeScript declarations.
+3. Documentation from the matching release branch.
+4. Current raw Markdown selected through `https://luma.gl/llms.txt`.
+5. Model memory only for forming hypotheses to verify against the sources above.
+
+A website version and an installed version may differ. Check all directly imported
+`@luma.gl/*` packages because mixed package versions can create type or runtime
+incompatibilities.
+
+Useful inspection commands include:
+
+```bash
+npm ls @luma.gl/core @luma.gl/engine @luma.gl/shadertools @luma.gl/webgpu @luma.gl/webgl
+yarn why @luma.gl/core
+```
+
+Follow the package's `types` or conditional `exports.types` entry to the declaration
+file. Search declarations for exact props and overloads before writing a constructor or
+method call.
+
+## Select the API level
+
+### Engine API
+
+Start with `@luma.gl/engine` for models, geometry, animation, picking, scenegraph
+helpers, dynamic resources, and common transforms. Most rendered applications should
+begin here.
+
+Use Engine when it can own the routine pipeline and redraw work. Do not drop to lower
+levels merely because the underlying operation is GPU-backed.
+
+### Core GPU API
+
+Use `@luma.gl/core` when the application needs explicit devices, canvas contexts,
+buffers, textures, samplers, shader layouts, bindings, pipelines, command encoders, or
+render/compute passes. A concrete adapter from `@luma.gl/webgpu` or `@luma.gl/webgl`
+must be available.
+
+Core is portable at the luma.gl abstraction boundary, but individual capabilities are
+not automatically portable. Check device features and limits before choosing a path.
+
+### Shader API
+
+Use `@luma.gl/shadertools` to assemble shader modules, plugins, hooks, defines, and
+typed shader inputs. Shadertools composes shader source and contracts; Engine or Core
+still creates resources and executes work.
+
+## Design rules
+
+- Prefer the highest level that exposes the control the task needs.
+- Keep application-facing data and binding names stable across backends.
+- Keep resource ownership explicit. Destroy objects created and owned by the feature;
+ do not destroy borrowed device, buffer, texture, or model resources.
+- Prefer current `RenderPipeline` and resource APIs when declarations do not contain an
+ older `Program`-centric example.
+- Request the narrowest device feature level and optional features that satisfy the
+ use case.
+- If deck.gl already provides the required visualization abstraction, recommend it
+ rather than rebuilding layers, cameras, and interaction directly in luma.gl.
+
+## Primary documentation
+
+- `https://luma.gl/docs/api-guide.md`
+- `https://luma.gl/docs/api-guide/gpu/gpu-initialization.md`
+- `https://luma.gl/docs/api-guide/gpu/gpu-resources.md`
+- `https://luma.gl/docs/api-guide/shaders/shader-assembly.md`
+- `https://luma.gl/docs/api-reference.md`
diff --git a/skills/lumagl/references/contributing.md b/skills/lumagl/references/contributing.md
new file mode 100644
index 0000000000..03ae377b58
--- /dev/null
+++ b/skills/lumagl/references/contributing.md
@@ -0,0 +1,66 @@
+# Contributing in visgl/luma.gl
+
+## Read repository instructions
+
+Read the root `AGENTS.md` before editing, then check for more specific instructions in
+the affected tree. Repository instructions are authoritative for setup, style,
+ownership boundaries, validation, and merge preparation.
+
+Preserve unrelated working-tree changes. Follow established TypeScript and
+documentation conventions, and keep new agent support on the existing contributor
+command surface.
+
+## Stable root commands
+
+Use root scripts rather than reaching into test-runner internals:
+
+```bash
+yarn test-node
+yarn test-browser
+yarn test-headless
+yarn test-coverage
+yarn website-debug
+```
+
+`yarn test-node` is a focused check. It does not replace the repository-wide build or
+combined test gate. Follow the final command sequence in `AGENTS.md`, including
+formatting after changes and the required `yarn build` and `yarn test`.
+
+Reusable Vitest and Playwright wiring lives in
+`dev-modules/devtools-extensions/`. Repository-specific overrides live in
+`.ocularrc.js`. Change the reusable workspace for shared runner behavior and the root
+configuration for luma.gl-specific policy.
+
+## Browser diagnosis
+
+Run an example with an explicit backend:
+
+```bash
+yarn website-debug --example hello-triangle --backend webgpu-core
+yarn website-debug --example hello-triangle --backend webgl2
+```
+
+The runner writes:
+
+- `.playwright-artifacts/website-playwright.png`;
+- `.playwright-artifacts/webgpu-probe.json`;
+- `.playwright-artifacts/page-diagnostics.json`;
+- `.playwright-artifacts/last-url.txt`.
+
+Inspect the artifact contents. A zero exit code is not evidence that the expected
+pixels rendered or that the intended backend was selected.
+
+## Scope and policy
+
+Use the same contribution standards for human- and agent-authored changes. This skill
+does not create disclosure requirements or other contribution policy. It only makes
+the existing instructions, verification loops, and architecture boundaries easier for
+an agent to follow.
+
+## Repository sources
+
+- `AGENTS.md`
+- `dev-modules/devtools-extensions/docs/llm-friendly-test-setup.md`
+- `dev-modules/devtools-extensions/docs/vitest.md`
+- `dev-modules/devtools-extensions/docs/playwright.md`
+- `.ocularrc.js`
diff --git a/skills/lumagl/references/debugging.md b/skills/lumagl/references/debugging.md
new file mode 100644
index 0000000000..1cf8a696d8
--- /dev/null
+++ b/skills/lumagl/references/debugging.md
@@ -0,0 +1,74 @@
+# GPU Debugging
+
+## Preserve evidence
+
+Before changing code, reproduce the failure and record:
+
+- selected backend, adapter, device info, features, and limits;
+- browser console messages, page errors, and failed requests;
+- shader compiler errors and the assembled shader when available;
+- a screenshot after the failing frame;
+- resource identifiers, layouts, bindings, and a small sample of uploaded data.
+
+Give important resources stable `id` values when supported. Increase luma.gl logging
+only for diagnosis; high-volume and synchronous WebGL debug modes are not production
+settings.
+
+## Debug in dependency order
+
+### 1. Device and adapters
+
+Confirm an adapter is installed and imported, device creation completed, the expected
+backend was actually selected, required features and limits exist, and the device is
+not lost.
+
+### 2. Canvas context
+
+Confirm the canvas is present, sized, visible, and associated with the device's canvas
+or presentation context. Check CSS dimensions and backing-store dimensions. Verify
+that the frame is presented and not immediately cleared or covered.
+
+### 3. Shader compilation
+
+Read compiler errors before editing shader logic. Inspect the assembled source, entry
+points, shader language, defined modules, injected hooks, and declared resource names.
+Use forced shader display or debug output when the installed Engine API supports it.
+
+### 4. Layouts and bindings
+
+Compare vertex buffer layouts, shader layouts, attribute names and formats, bind-group
+or logical binding names, uniform types, texture/sample types, and resource usage
+flags. Verify every required resource is bound and has the expected lifetime.
+
+### 5. Data
+
+Check buffer byte lengths, offsets, strides, draw ranges, indices, texture dimensions,
+formats, upload timing, and a small sample of values. Reject `NaN`, `undefined`, wrong
+component counts, and stale CPU debug copies.
+
+### 6. Render pass and draw
+
+Confirm attachment formats and sample counts match the pipeline, viewport and scissor
+are nonempty, clear/depth/blend/cull state is intentional, the draw count is nonzero,
+and the pass ends before presentation. Check that redraw scheduling reaches this path.
+
+### 7. Backend comparison
+
+Run the same minimal case through WebGPU and WebGL 2 when supported. A working backend
+can reveal a shader-contract, format, feature, or state assumption in the failing one.
+Do not hide a backend failure by allowing automatic fallback during this comparison.
+
+## Escalation tools
+
+Use a browser GPU inspector or frame debugger after the ordered checks. On WebGL,
+Spector.js and the optional synchronous debug context can expose calls and state. Use
+browser-native WebGPU diagnostics where available. Keep the failing screenshot and
+logs alongside the diagnosis.
+
+## Primary documentation
+
+- `https://luma.gl/docs/developer-guide/debugging.md`
+- `https://luma.gl/docs/api-guide/gpu/gpu-rendering.md`
+- `https://luma.gl/docs/api-guide/gpu/gpu-bindings.md`
+- `https://luma.gl/docs/api-reference/core/shader-logs.md`
+- `https://luma.gl/docs/developer-guide/profiling.md`
diff --git a/skills/lumagl/references/portability.md b/skills/lumagl/references/portability.md
new file mode 100644
index 0000000000..1240770c37
--- /dev/null
+++ b/skills/lumagl/references/portability.md
@@ -0,0 +1,60 @@
+# Backend Portability
+
+## Decide the support contract first
+
+Identify whether the feature is:
+
+- portable across WebGPU and WebGL 2;
+- WebGPU-first with a reduced WebGL 2 path; or
+- intentionally WebGPU-only.
+
+Do not promise WebGL 2 support for compute shaders, storage textures, storage buffers,
+or other missing capabilities. Make the unsupported behavior explicit and testable.
+
+## Device and adapter boundary
+
+Supply the adapters the application supports and select them at device creation. Keep
+backend selection near startup rather than spreading raw backend checks across the
+render path.
+
+Use the default core WebGPU feature level for a portable baseline. Request maximum or
+optional WebGPU features only when the feature needs them, and feature-detect before
+creating dependent resources.
+
+## Portable shader contract
+
+luma.gl does not translate application WGSL into GLSL or GLSL into WGSL. Portable
+rendering normally uses matching sources with one application-facing contract:
+
+- WGSL source for WebGPU;
+- GLSL ES 3.00 vertex and fragment sources for WebGL 2;
+- aligned attribute names and formats;
+- aligned logical binding names;
+- aligned module props, defaults, and uniform types;
+- aligned varyings and observable rendering behavior.
+
+When shadertools assembles WGSL, prefer named `@binding(auto)` resources and bind them
+by name. Raw WebGPU code that bypasses shadertools still needs concrete binding
+numbers. Keep buffer layout, shader layout, and runtime bindings consistent.
+
+## Backend comparison
+
+For portable work:
+
+1. Force WebGPU and record the actual device, errors, and screenshot.
+2. Force WebGL 2 and record the same evidence.
+3. Compare more than pixels: initialization, warnings, feature choices, resource
+ formats, binding names, draw counts, blending, depth state, and cleanup.
+4. If output differs, reduce to the smallest resource and draw path that reproduces
+ the difference.
+
+Do not treat a fallback from a failed WebGPU attempt as a WebGPU pass. Report the
+selected backend explicitly.
+
+## Primary documentation
+
+- `https://luma.gl/docs/api-guide/background/webgpu-vs-webgl.md`
+- `https://luma.gl/docs/api-guide/shaders/writing-portable-shaders.md`
+- `https://luma.gl/docs/api-guide/gpu/gpu-bindings.md`
+- `https://luma.gl/docs/api-reference/core/device-features.md`
+- `https://luma.gl/docs/api-reference/core/shader-layout.md`
diff --git a/test/llm/README.md b/test/llm/README.md
new file mode 100644
index 0000000000..7852cc510b
--- /dev/null
+++ b/test/llm/README.md
@@ -0,0 +1,52 @@
+# lumagl Agent Skill Evaluation
+
+`lumagl-skill-evals.json` is an offline corpus for manually comparing agent behavior with
+and without the `lumagl` skill. CI does not invoke a model, grade responses, or require
+model API credentials.
+
+## Validate the corpus
+
+From the repository root, run:
+
+```bash
+yarn test-node test/llm/lumagl-skill.node.spec.ts
+```
+
+This checks the skill frontmatter and references, corpus structure, unique case IDs,
+expected categories, and canonical source paths. A passing test means that those static
+checks succeeded; it does not mean that the behavioral cases passed. The same test also
+runs as part of the full `yarn test-node` and `yarn test` suites.
+
+## Compare agent behavior manually
+
+1. Record the repository commit, model and agent versions, date, tool permissions,
+ settings, and number of trials per case.
+2. Use clean, equivalent workspaces and fresh sessions for every trial. Keep the model,
+ prompt, context, tools, and settings identical between runs.
+3. For the baseline, ensure that no luma.gl skill is installed or discoverable. For the
+ treatment, load `skills/lumagl` from the same checkout through the runner's local-skill
+ mechanism, and record that mechanism with the results.
+4. Run the exact case prompt once in each workspace. Do not give the agent the scoring
+ criteria or canonical sources.
+5. Preserve the transcript, tool output, logs, and visual artifacts needed to verify the
+ response.
+6. Score each run from the preserved evidence. Repeat with the same trial count for every
+ case when more than one trial is used.
+
+For each run, count the expected behaviors that are clearly supported by evidence and
+the forbidden mistakes that occurred. Unclear or unsupported behavior does not count as
+satisfied. A case passes only when every expected behavior is satisfied and no forbidden
+mistake occurs. Use `canonicalSources` to judge technical correctness; citing every source
+is not itself required unless a criterion says so.
+
+## Share results
+
+Include the run metadata above and a table like this in the pull request or linked result
+artifact:
+
+| Case | Baseline expected | Baseline forbidden | Baseline pass | With skill expected | With skill forbidden | With skill pass | Evidence |
+| --- | ---: | ---: | --- | ---: | ---: | --- | --- |
+| `case-id` | 2/4 | 1 | No | 4/4 | 0 | Yes | Transcript and artifact links |
+
+Report the baseline and with-skill pass totals separately. Do not describe the behavioral
+corpus as passing based only on the static validation command.
diff --git a/test/llm/lumagl-skill-evals.json b/test/llm/lumagl-skill-evals.json
new file mode 100644
index 0000000000..81b192ea44
--- /dev/null
+++ b/test/llm/lumagl-skill-evals.json
@@ -0,0 +1,195 @@
+{
+ "version": 1,
+ "description": "Offline prompts and scoring criteria for manually comparing luma.gl agent behavior before and after loading the lumagl skill.",
+ "cases": [
+ {
+ "id": "application-setup-current-packages",
+ "category": "application-setup",
+ "prompt": "Add luma.gl to this TypeScript application and render a first frame with WebGPU preferred and WebGL 2 fallback.",
+ "expectedBehaviors": [
+ "Inspect the package manager, lockfile, installed @luma.gl package versions, and exported declarations before choosing APIs.",
+ "Choose Engine for the application loop and model-level work, with concrete WebGPU and WebGL adapters.",
+ "Keep backend selection at device or animation-loop startup.",
+ "Run the application typecheck and observe the frame in a real browser."
+ ],
+ "forbiddenMistakes": [
+ "Assume an API signature from model memory without checking the installed declarations.",
+ "Install or use @luma.gl/core without importing a concrete adapter."
+ ],
+ "canonicalSources": [
+ "docs/getting-started.mdx",
+ "docs/api-guide/README.md",
+ "docs/developer-guide/installing.md",
+ "skills/lumagl/references/architecture.md"
+ ]
+ },
+ {
+ "id": "outdated-program-api",
+ "category": "outdated-apis",
+ "prompt": "Fix this luma.gl v9 application that copied an old Program-based rendering example and no longer typechecks.",
+ "expectedBehaviors": [
+ "Confirm the installed luma.gl major and exact package versions.",
+ "Inspect the declarations to determine the current RenderPipeline, Model, or resource API that matches the task.",
+ "Use matching release documentation and explain the narrow migration being made.",
+ "Typecheck the migrated path and exercise it in the browser."
+ ],
+ "forbiddenMistakes": [
+ "Invent a compatibility shim for a removed API without evidence that the project needs one.",
+ "Upgrade packages or rewrite unrelated rendering code merely to make the old sample compile."
+ ],
+ "canonicalSources": [
+ "docs/developer-guide/installing.md",
+ "docs/api-guide/gpu/gpu-resources.md",
+ "docs/api-reference/core/resources/render-pipeline.md",
+ "skills/lumagl/references/architecture.md"
+ ]
+ },
+ {
+ "id": "webgpu-only-storage-feature",
+ "category": "webgpu-only-features",
+ "prompt": "Implement a compute feature backed by storage buffers, but keep the existing WebGL 2 rendering application usable.",
+ "expectedBehaviors": [
+ "Identify compute and storage buffers as WebGPU-only capabilities.",
+ "Put the feature behind an explicit backend or feature check.",
+ "Keep the existing WebGL 2 render path usable without pretending it executes the compute feature.",
+ "Test the supported WebGPU path and the clear unsupported WebGL 2 behavior."
+ ],
+ "forbiddenMistakes": [
+ "Claim that luma.gl automatically translates the compute path to WebGL 2.",
+ "Request maximum device capabilities without checking the feature actually needed."
+ ],
+ "canonicalSources": [
+ "docs/api-guide/background/webgpu-vs-webgl.md",
+ "docs/api-guide/gpu/gpu-storage-buffers.md",
+ "docs/api-reference/core/device-features.mdx",
+ "skills/lumagl/references/portability.md"
+ ]
+ },
+ {
+ "id": "blank-canvas-debugging",
+ "category": "blank-canvases",
+ "prompt": "The application typechecks and starts without throwing, but the luma.gl canvas stays blank. Diagnose it.",
+ "expectedBehaviors": [
+ "Reproduce the issue and collect the selected backend, console output, page errors, failed requests, and a screenshot.",
+ "Check device and adapters, canvas context, shader compilation, layouts and bindings, data, then render pass and draw in that order.",
+ "Confirm that draw counts, viewport, attachments, pass completion, and presentation are valid.",
+ "Force each supported backend separately to distinguish a portable bug from a backend-specific bug."
+ ],
+ "forbiddenMistakes": [
+ "Declare success because typechecking or the development server process passed.",
+ "Change shader math at random before confirming device, context, and binding state."
+ ],
+ "canonicalSources": [
+ "docs/developer-guide/debugging.md",
+ "docs/api-guide/gpu/gpu-rendering.md",
+ "dev-modules/devtools-extensions/docs/playwright.md",
+ "skills/lumagl/references/debugging.md"
+ ]
+ },
+ {
+ "id": "binding-layout-mismatch",
+ "category": "bindings-and-layouts",
+ "prompt": "A model compiles, but WebGPU reports a binding or layout mismatch and WebGL 2 renders corrupted attributes.",
+ "expectedBehaviors": [
+ "Compare the shader layout, vertex buffer layout, logical binding names, resource types, offsets, strides, and formats.",
+ "Inspect the assembled WGSL bindings and matching GLSL contract.",
+ "Inspect a small sample of uploaded data and reject undefined or non-finite values.",
+ "Keep the fix localized at the layout, adapter, or model boundary that owns the mismatch."
+ ],
+ "forbiddenMistakes": [
+ "Silence TypeScript with broad casts instead of reconciling layouts and resource types.",
+ "Rename shader inputs on one backend without updating the shared application-facing contract."
+ ],
+ "canonicalSources": [
+ "docs/api-guide/gpu/gpu-bindings.md",
+ "docs/api-guide/gpu/gpu-memory-layouts.md",
+ "docs/api-reference/core/shader-layout.md",
+ "skills/lumagl/references/debugging.md"
+ ]
+ },
+ {
+ "id": "owned-resource-cleanup",
+ "category": "resource-cleanup",
+ "prompt": "A component repeatedly mounts and unmounts a luma.gl model and GPU buffers. Stop its GPU memory growth.",
+ "expectedBehaviors": [
+ "Identify which models, buffers, textures, animation loops, or helpers the component owns.",
+ "Destroy owned resources at the matching unmount or lifecycle boundary.",
+ "Avoid destroying devices or resources borrowed from the application.",
+ "Use resource identifiers and profiling evidence to verify that repeated mounts no longer grow the live resource set."
+ ],
+ "forbiddenMistakes": [
+ "Rely on JavaScript garbage collection to release GPU resources promptly.",
+ "Destroy shared or borrowed resources to make the local leak counter fall."
+ ],
+ "canonicalSources": [
+ "docs/api-reference/core/resources/resource.md",
+ "docs/api-reference/engine/model.md",
+ "docs/developer-guide/profiling.md",
+ "skills/lumagl/references/architecture.md"
+ ]
+ },
+ {
+ "id": "dual-backend-shader-portability",
+ "category": "backend-portability",
+ "prompt": "Add a reusable rendering effect that must behave the same on WebGPU and WebGL 2.",
+ "expectedBehaviors": [
+ "Maintain matching WGSL and GLSL ES 3.00 implementations with one logical input contract.",
+ "Align attributes, varyings, bindings, uniforms, module props, and observable output.",
+ "Use named auto bindings for WGSL assembled by shadertools when supported by the installed version.",
+ "Force and compare both backends with logs and screenshots."
+ ],
+ "forbiddenMistakes": [
+ "Claim that luma.gl transpiles the application shader between WGSL and GLSL.",
+ "Allow automatic fallback to count as verification of both backends."
+ ],
+ "canonicalSources": [
+ "docs/api-guide/shaders/writing-portable-shaders.md",
+ "docs/api-reference/shadertools/wgsl-support.md",
+ "docs/api-guide/gpu/gpu-bindings.md",
+ "skills/lumagl/references/portability.md"
+ ]
+ },
+ {
+ "id": "repository-contributor-tests",
+ "category": "contributor-testing",
+ "prompt": "Make a shared luma.gl package API change and get it ready for merge.",
+ "expectedBehaviors": [
+ "Read AGENTS.md and any scoped repository instructions before editing.",
+ "Use the stable root test scripts and run focused tests while iterating.",
+ "Run yarn lint fix after changes.",
+ "Run the final yarn build and yarn test gates rather than substituting yarn test-node."
+ ],
+ "forbiddenMistakes": [
+ "Call internal Vitest or Playwright wiring directly when a stable root command exists.",
+ "Present a focused Node test as sufficient merge verification for a shared API change."
+ ],
+ "canonicalSources": [
+ "AGENTS.md",
+ "dev-modules/devtools-extensions/docs/llm-friendly-test-setup.md",
+ "docs/developer-guide/testing.md",
+ "skills/lumagl/references/contributing.md"
+ ]
+ },
+ {
+ "id": "website-debug-artifacts",
+ "category": "website-debugging",
+ "prompt": "Debug a luma.gl website example that works in WebGL 2 but fails in WebGPU and report evidence.",
+ "expectedBehaviors": [
+ "Run yarn website-debug from the repository root with the example and explicit WebGPU backend.",
+ "Inspect the final URL, screenshot, WebGPU probe, and page diagnostics artifacts.",
+ "Compare against an explicit WebGL 2 run without automatic fallback.",
+ "Report console errors, selected backend, screenshot evidence, and the smallest isolated difference."
+ ],
+ "forbiddenMistakes": [
+ "Claim the visual task passed only because website-debug exited successfully.",
+ "Ignore the saved page diagnostics or screenshot while guessing at a shader fix."
+ ],
+ "canonicalSources": [
+ "AGENTS.md",
+ "dev-modules/devtools-extensions/docs/playwright.md",
+ "dev-modules/devtools-extensions/docs/browser-debug.md",
+ "skills/lumagl/references/contributing.md"
+ ]
+ }
+ ]
+}
diff --git a/test/llm/lumagl-skill.node.spec.ts b/test/llm/lumagl-skill.node.spec.ts
new file mode 100644
index 0000000000..65e66b44be
--- /dev/null
+++ b/test/llm/lumagl-skill.node.spec.ts
@@ -0,0 +1,99 @@
+import {existsSync, readFileSync} from 'node:fs';
+import path from 'node:path';
+import {describe, expect, test} from 'vitest';
+import {parse} from 'yaml';
+
+type EvalCase = {
+ id: string;
+ category: string;
+ prompt: string;
+ expectedBehaviors: string[];
+ forbiddenMistakes: string[];
+ canonicalSources: string[];
+};
+
+type EvalCorpus = {
+ version: number;
+ description: string;
+ cases: EvalCase[];
+};
+
+const repositoryDirectory = process.cwd();
+const skillDirectory = path.join(repositoryDirectory, 'skills/lumagl');
+const skillPath = path.join(skillDirectory, 'SKILL.md');
+const evalPath = path.join(repositoryDirectory, 'test/llm/lumagl-skill-evals.json');
+
+function readSkillFrontmatter(): {frontmatter: Record; body: string} {
+ const skill = readFileSync(skillPath, 'utf8');
+ const match = skill.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
+ expect(match, 'SKILL.md must have YAML frontmatter').not.toBeNull();
+
+ return {
+ frontmatter: parse(match?.[1] || ''),
+ body: match?.[2] || ''
+ };
+}
+
+describe('lumagl Agent Skill', () => {
+ test('uses valid portable frontmatter and local references', () => {
+ const {frontmatter, body} = readSkillFrontmatter();
+
+ expect(Object.keys(frontmatter).sort()).toEqual(['description', 'name']);
+ expect(frontmatter.name).toBe('lumagl');
+ expect(typeof frontmatter.description).toBe('string');
+ expect((frontmatter.description as string).length).toBeGreaterThan(80);
+
+ const referenceLinks = [...body.matchAll(/\]\((references\/[^)]+\.md)\)/g)].map(
+ match => match[1]
+ );
+ expect(new Set(referenceLinks).size).toBe(4);
+ for (const referenceLink of referenceLinks) {
+ expect(existsSync(path.join(skillDirectory, referenceLink))).toBe(true);
+ }
+
+ expect(existsSync(path.join(skillDirectory, 'agents/openai.yaml'))).toBe(false);
+ expect(existsSync(path.join(skillDirectory, 'scripts'))).toBe(false);
+ });
+
+ test('has a valid offline eval corpus with resolvable canonical sources', () => {
+ const corpus = JSON.parse(readFileSync(evalPath, 'utf8')) as EvalCorpus;
+
+ expect(corpus.version).toBe(1);
+ expect(corpus.description.length).toBeGreaterThan(40);
+ expect(corpus.cases.length).toBeGreaterThanOrEqual(9);
+
+ const caseIds = new Set();
+ const categories = new Set();
+ for (const evalCase of corpus.cases) {
+ expect(evalCase.id).toMatch(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
+ expect(caseIds.has(evalCase.id), `duplicate eval id: ${evalCase.id}`).toBe(false);
+ caseIds.add(evalCase.id);
+ categories.add(evalCase.category);
+
+ expect(evalCase.prompt.length).toBeGreaterThan(30);
+ expect(evalCase.expectedBehaviors.length).toBeGreaterThanOrEqual(3);
+ expect(evalCase.forbiddenMistakes.length).toBeGreaterThanOrEqual(2);
+ expect(evalCase.canonicalSources.length).toBeGreaterThanOrEqual(2);
+
+ for (const source of evalCase.canonicalSources) {
+ expect(path.isAbsolute(source), `${source} must be repository-relative`).toBe(false);
+ expect(source.split('/')).not.toContain('..');
+ expect(existsSync(path.join(repositoryDirectory, source)), `missing ${source}`).toBe(true);
+ }
+ }
+
+ expect(categories).toEqual(
+ new Set([
+ 'application-setup',
+ 'outdated-apis',
+ 'webgpu-only-features',
+ 'blank-canvases',
+ 'bindings-and-layouts',
+ 'resource-cleanup',
+ 'backend-portability',
+ 'contributor-testing',
+ 'website-debugging'
+ ])
+ );
+ });
+});
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index ee92a367a1..7e7bc19467 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -3,6 +3,13 @@ const {OptionDefaults: typedocOptionDefaults} = require('typedoc');
const path = require('path');
const websiteBaseUrl = process.env.WEBSITE_BASE_URL || '/';
+const websiteBasePathSegments = websiteBaseUrl.split('/').filter(Boolean);
+const websiteRoutePrefix =
+ websiteBasePathSegments.length === 0 ? '' : `/${websiteBasePathSegments.join('/')}`;
+
+function prefixWebsiteRoute(route) {
+ return `${websiteRoutePrefix}${route}`;
+}
const config = getDocusaurusConfig({
projectName: 'luma.gl',
@@ -106,6 +113,38 @@ module.exports = {
}
return plugin;
}),
+ [
+ '@signalwire/docusaurus-plugin-llms-txt',
+ {
+ siteTitle: 'luma.gl',
+ siteDescription:
+ 'WebGPU and WebGL2 framework documentation for visualization and compute.',
+ // Plugin 1.x builds its route tree from base-prefixed Docusaurus paths.
+ // Preserve the configured three levels after the post-build base-path normalization.
+ depth: Math.min(5, 3 + websiteBasePathSegments.length),
+ enableDescriptions: true,
+ includeOrder: [
+ '/docs/getting-started',
+ '/docs/tutorials/**',
+ '/docs/api-guide/**',
+ '/docs/api-reference/**',
+ '/docs/developer-guide/**'
+ ].map(prefixWebsiteRoute),
+ onRouteError: 'throw',
+ content: {
+ enableMarkdownFiles: true,
+ enableLlmsFullTxt: false,
+ relativePaths: false,
+ includeBlog: false,
+ includePages: false,
+ includeDocs: true,
+ includeVersionedDocs: false,
+ includeGeneratedIndex: true,
+ // Plugin 1.x matches these globs against base-prefixed Docusaurus routes.
+ excludeRoutes: ['/docs/legacy/**', '/examples/**'].map(prefixWebsiteRoute)
+ }
+ }
+ ],
[
'docusaurus-plugin-typedoc',
{
diff --git a/website/package.json b/website/package.json
index 503d7ad92a..c17240875d 100644
--- a/website/package.json
+++ b/website/package.json
@@ -5,7 +5,7 @@
"scripts": {
"docusaurus": "docusaurus",
"start": "node ./scripts/sync-example-assets.mjs && docusaurus start",
- "build": "node ./scripts/sync-example-assets.mjs && docusaurus build",
+ "build": "node ./scripts/sync-example-assets.mjs && docusaurus build && node ./scripts/normalize-llm-output.mjs && node ./scripts/check-llm-output.mjs",
"build-staging": "WEBSITE_BASE_URL=/luma.gl/ yarn build",
"lint": "npx tsc --noEmit",
"swizzle": "docusaurus swizzle",
@@ -25,6 +25,7 @@
"@docusaurus/core": "^3.9.2",
"@probe.gl/stats": "^4.1.1",
"@probe.gl/stats-widget": "^4.1.1",
+ "@signalwire/docusaurus-plugin-llms-txt": "^1.2.2",
"@stackblitz/sdk": "^1.11.0",
"@vis.gl/docusaurus-website": "1.0.0-alpha.21",
"clsx": "^1.1.1",
diff --git a/website/scripts/check-llm-output.mjs b/website/scripts/check-llm-output.mjs
new file mode 100644
index 0000000000..70bfe0c61c
--- /dev/null
+++ b/website/scripts/check-llm-output.mjs
@@ -0,0 +1,199 @@
+import {existsSync, readdirSync, readFileSync, statSync} from 'node:fs';
+import path from 'node:path';
+import {fileURLToPath} from 'node:url';
+
+const websiteDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const buildDirectory = path.join(websiteDirectory, 'build');
+const llmsTxtPath = path.join(buildDirectory, 'llms.txt');
+const llmsFullTxtPath = path.join(buildDirectory, 'llms-full.txt');
+const websiteOrigin = 'https://luma.gl';
+const websiteBasePath = normalizeWebsiteBasePath(process.env.WEBSITE_BASE_URL || '/');
+const websiteUrl = new URL(websiteBasePath, websiteOrigin);
+const duplicatedWebsiteBaseUrl =
+ websiteBasePath === '/' ? null : new URL(websiteBasePath.slice(1), websiteUrl).href;
+
+function normalizeWebsiteBasePath(basePath) {
+ const pathSegments = basePath.split('/').filter(Boolean);
+ return pathSegments.length === 0 ? '/' : `/${pathSegments.join('/')}/`;
+}
+
+function stripWebsiteBasePath(pathname) {
+ if (websiteBasePath === '/' || !pathname.startsWith(websiteBasePath)) {
+ return pathname;
+ }
+ return `/${pathname.slice(websiteBasePath.length)}`;
+}
+
+function fail(message) {
+ throw new Error(`llms.txt output check failed: ${message}`);
+}
+
+function requireFile(relativePath) {
+ const filePath = path.join(buildDirectory, relativePath);
+ if (!existsSync(filePath) || !statSync(filePath).isFile()) {
+ fail(`missing ${relativePath}`);
+ }
+ return filePath;
+}
+
+function findFiles(directory, extension) {
+ if (!existsSync(directory)) {
+ return [];
+ }
+
+ const filePaths = [];
+ for (const entry of readdirSync(directory, {withFileTypes: true})) {
+ const entryPath = path.join(directory, entry.name);
+ if (entry.isDirectory()) {
+ filePaths.push(...findFiles(entryPath, extension));
+ } else if (entry.isFile() && entryPath.endsWith(extension)) {
+ filePaths.push(entryPath);
+ }
+ }
+ return filePaths;
+}
+
+function extractMarkdownLinks(markdown) {
+ const links = [];
+ const markdownLinkPattern = /!?\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g;
+ for (const match of markdown.matchAll(markdownLinkPattern)) {
+ links.push(match[1].replace(/^<|>$/g, ''));
+ }
+ return links;
+}
+
+function resolveGeneratedMarkdownLink(sourcePath, link) {
+ if (link.startsWith('#') || link.startsWith('mailto:')) {
+ return null;
+ }
+
+ let pathname;
+ try {
+ const url = new URL(link);
+ if (url.origin !== websiteUrl.origin) {
+ return null;
+ }
+ pathname = decodeURIComponent(url.pathname);
+ } catch {
+ const linkWithoutFragment = link.split('#', 1)[0].split('?', 1)[0];
+ if (!linkWithoutFragment.endsWith('.md')) {
+ return null;
+ }
+ if (linkWithoutFragment.startsWith('/')) {
+ pathname = decodeURIComponent(linkWithoutFragment);
+ } else {
+ return path.resolve(path.dirname(sourcePath), decodeURIComponent(linkWithoutFragment));
+ }
+ }
+
+ if (!pathname.endsWith('.md')) {
+ return null;
+ }
+ pathname = stripWebsiteBasePath(pathname);
+ if (path.isAbsolute(pathname) && pathname.startsWith(buildDirectory)) {
+ return pathname;
+ }
+ return path.join(buildDirectory, pathname.replace(/^\/+/, ''));
+}
+
+if (!existsSync(llmsTxtPath)) {
+ fail('missing llms.txt');
+}
+if (existsSync(llmsFullTxtPath)) {
+ fail('llms-full.txt must not be generated');
+}
+
+const llmsTxt = readFileSync(llmsTxtPath, 'utf8');
+if (!llmsTxt.startsWith('# luma.gl\n')) {
+ fail('llms.txt has an unexpected title');
+}
+const firstSectionHeading = llmsTxt.match(/^## .+$/m)?.[0];
+if (firstSectionHeading !== '## docs') {
+ fail(`llms.txt has an unexpected first section: ${firstSectionHeading || 'none'}`);
+}
+if (llmsTxt.includes('/docs/legacy/')) {
+ fail('llms.txt contains a legacy guide');
+}
+if (llmsTxt.includes('/examples/')) {
+ fail('llms.txt contains a standalone example page');
+}
+
+const requiredIndexLinks = [
+ 'docs/getting-started.md',
+ 'docs/tutorials/hello-triangle.md',
+ 'docs/api-guide.md',
+ 'docs/api-reference.md',
+ 'docs/developer-guide/working-with-ai.md'
+].map((relativePath) => new URL(relativePath, websiteUrl).href);
+for (const link of requiredIndexLinks) {
+ if (!llmsTxt.includes(link)) {
+ fail(`llms.txt is missing ${link}`);
+ }
+}
+
+const gettingStartedPath = requireFile('docs/getting-started.md');
+const workingWithAiPath = requireFile('docs/developer-guide/working-with-ai.md');
+requireFile('docs.md');
+requireFile('docs/api-guide.md');
+requireFile('docs/api-reference.md');
+
+const typedocMarkdownFiles = findFiles(
+ path.join(buildDirectory, 'docs/api-reference/generated/core'),
+ '.md'
+);
+if (typedocMarkdownFiles.length === 0) {
+ fail('generated TypeDoc Markdown references are missing');
+}
+
+const markdownFiles = findFiles(buildDirectory, '.md');
+if (markdownFiles.length < 100) {
+ fail(`only ${markdownFiles.length} documentation Markdown files were generated`);
+}
+
+const gettingStarted = readFileSync(gettingStartedPath, 'utf8');
+for (const expectedText of ['npm create vite', 'yarn create vite', 'pnpm create vite']) {
+ if (!gettingStarted.includes(expectedText)) {
+ fail(`rendered Getting Started Markdown is missing "${expectedText}"`);
+ }
+}
+if (gettingStarted.includes(' ${path.relative(
+ buildDirectory,
+ targetPath
+ )}`
+ );
+ }
+ }
+}
+
+if (brokenLinks.length > 0) {
+ fail(`broken Markdown links:\n${brokenLinks.slice(0, 20).join('\n')}`);
+}
+
+console.log(`Validated llms.txt and ${markdownFiles.length} raw documentation pages.`);
diff --git a/website/scripts/normalize-llm-output.mjs b/website/scripts/normalize-llm-output.mjs
new file mode 100644
index 0000000000..50a73879ec
--- /dev/null
+++ b/website/scripts/normalize-llm-output.mjs
@@ -0,0 +1,94 @@
+import {existsSync, readdirSync, readFileSync, statSync, writeFileSync} from 'node:fs';
+import path from 'node:path';
+import {fileURLToPath} from 'node:url';
+
+const websiteDirectory = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
+const buildDirectory = path.join(websiteDirectory, 'build');
+const llmsTxtPath = path.join(buildDirectory, 'llms.txt');
+const websiteOrigin = 'https://luma.gl';
+const websiteBasePathSegments = (process.env.WEBSITE_BASE_URL || '/').split('/').filter(Boolean);
+
+function fail(message) {
+ throw new Error(`llms.txt output normalization failed: ${message}`);
+}
+
+function findFiles(directory, extension) {
+ if (!existsSync(directory)) {
+ return [];
+ }
+
+ const filePaths = [];
+ for (const entry of readdirSync(directory, {withFileTypes: true})) {
+ const entryPath = path.join(directory, entry.name);
+ if (entry.isDirectory()) {
+ filePaths.push(...findFiles(entryPath, extension));
+ } else if (entry.isFile() && entryPath.endsWith(extension)) {
+ filePaths.push(entryPath);
+ }
+ }
+ return filePaths;
+}
+
+function normalizeIndexHierarchy(markdown) {
+ const lines = markdown.split('\n');
+ const firstSectionIndex = lines.findIndex((line) => line.startsWith('## '));
+ if (firstSectionIndex === -1) {
+ fail('llms.txt does not contain a section heading');
+ }
+ if (lines[firstSectionIndex] === '## docs') {
+ return markdown;
+ }
+
+ let wrapperIndex = firstSectionIndex;
+ for (const [segmentIndex, segment] of websiteBasePathSegments.entries()) {
+ const expectedHeading = `${'#'.repeat(segmentIndex + 2)} ${segment}`;
+ if (lines[wrapperIndex] !== expectedHeading) {
+ fail(`expected base-path heading "${expectedHeading}"`);
+ }
+ lines.splice(wrapperIndex, 1);
+ while (lines[wrapperIndex] === '') {
+ lines.splice(wrapperIndex, 1);
+ }
+ }
+
+ const basePathDepth = websiteBasePathSegments.length;
+ const firstContentHeadingDepth = basePathDepth + 2;
+ const normalizedLines = lines.map((line) => {
+ const headingMatch = line.match(/^(#+) (.+)$/);
+ if (!headingMatch || headingMatch[1].length < firstContentHeadingDepth) {
+ return line;
+ }
+ return `${headingMatch[1].slice(basePathDepth)} ${headingMatch[2]}`;
+ });
+ return normalizedLines.join('\n');
+}
+
+if (websiteBasePathSegments.length === 0) {
+ console.log('No llms.txt base-path normalization needed.');
+ process.exit(0);
+}
+if (!existsSync(llmsTxtPath) || !statSync(llmsTxtPath).isFile()) {
+ fail('missing llms.txt');
+}
+
+const websiteBasePath = `/${websiteBasePathSegments.join('/')}/`;
+const websiteUrl = new URL(websiteBasePath, websiteOrigin);
+const duplicatedWebsiteBaseUrl = new URL(websiteBasePath.slice(1), websiteUrl).href;
+const markdownPaths = [llmsTxtPath, ...findFiles(buildDirectory, '.md')];
+let normalizedFileCount = 0;
+
+for (const markdownPath of markdownPaths) {
+ const markdown = readFileSync(markdownPath, 'utf8');
+ // Stable plugin 1.2.2 prepends the already-based site URL to base-prefixed links.
+ let normalizedMarkdown = markdown.split(duplicatedWebsiteBaseUrl).join(websiteUrl.href);
+ if (markdownPath === llmsTxtPath) {
+ // The same plugin version exposes each base-path segment as an index category.
+ normalizedMarkdown = normalizeIndexHierarchy(normalizedMarkdown);
+ }
+ if (normalizedMarkdown !== markdown) {
+ writeFileSync(markdownPath, normalizedMarkdown);
+ normalizedFileCount++;
+ }
+}
+
+console.log(`Normalized ${normalizedFileCount} LLM documentation files for ${websiteBasePath}.`);
diff --git a/website/src/components/docs/developer-docs-tabs.tsx b/website/src/components/docs/developer-docs-tabs.tsx
index 8c27045845..5b2015020c 100644
--- a/website/src/components/docs/developer-docs-tabs.tsx
+++ b/website/src/components/docs/developer-docs-tabs.tsx
@@ -10,6 +10,7 @@ type DeveloperDocsTab = {
/** Developer documentation tab identifiers. */
export type DeveloperDocsTabId =
| 'overview'
+ | 'ai'
| 'contributing'
| 'editing'
| 'testing'
@@ -19,6 +20,11 @@ export type DeveloperDocsTabId =
const DEVELOPER_DOCS_TABS: DeveloperDocsTab[] = [
{id: 'overview', label: 'Overview', href: '/docs/developer-guide'},
+ {
+ id: 'ai',
+ label: 'AI Agents',
+ href: '/docs/developer-guide/working-with-ai'
+ },
{id: 'contributing', label: 'Contributing', href: '/docs/developer-guide/contributing'},
{id: 'editing', label: 'Editing', href: '/docs/developer-guide/editing'},
{id: 'testing', label: 'Testing', href: '/docs/developer-guide/testing'},
diff --git a/yarn.lock b/yarn.lock
index 3c9b7111af..b67e0b48e9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6175,6 +6175,28 @@ __metadata:
languageName: node
linkType: hard
+"@signalwire/docusaurus-plugin-llms-txt@npm:^1.2.2":
+ version: 1.2.2
+ resolution: "@signalwire/docusaurus-plugin-llms-txt@npm:1.2.2"
+ dependencies:
+ fs-extra: "npm:^11.0.0"
+ hast-util-select: "npm:^6.0.4"
+ hast-util-to-html: "npm:^9.0.5"
+ hast-util-to-string: "npm:^3.0.1"
+ p-map: "npm:^7.0.2"
+ rehype-parse: "npm:^9"
+ rehype-remark: "npm:^10"
+ remark-gfm: "npm:^4"
+ remark-stringify: "npm:^11"
+ string-width: "npm:^5.0.0"
+ unified: "npm:^11"
+ unist-util-visit: "npm:^5"
+ peerDependencies:
+ "@docusaurus/core": ^3.0.0
+ checksum: 10c0/f9c47749357fd781b092705860367482383e8d8a92e76d9d77f95f38ed7041a4b3fc5e177ef3dc97423637101dc1a24ae63aafed89a22a0de27acb74ae3552b5
+ languageName: node
+ linkType: hard
+
"@sigstore/bundle@npm:^2.3.2":
version: 2.3.2
resolution: "@sigstore/bundle@npm:2.3.2"
@@ -8696,6 +8718,13 @@ __metadata:
languageName: node
linkType: hard
+"bcp-47-match@npm:^2.0.0":
+ version: 2.0.3
+ resolution: "bcp-47-match@npm:2.0.3"
+ checksum: 10c0/ae5c202854df8a9ad4777dc3b49562578495a69164869f365a88c1a089837a9fbbce4c0c44f6f1a5e44c7841f47e91fe6fea00306ca49ce5ec95a7eb71f839c4
+ languageName: node
+ linkType: hard
+
"bcrypt-pbkdf@npm:^1.0.0":
version: 1.0.2
resolution: "bcrypt-pbkdf@npm:1.0.2"
@@ -10234,6 +10263,13 @@ __metadata:
languageName: node
linkType: hard
+"css-selector-parser@npm:^3.0.0":
+ version: 3.3.0
+ resolution: "css-selector-parser@npm:3.3.0"
+ checksum: 10c0/7ec2c19800ce52591cf32d3d3745db5a8715b40dbd01057c9b799577c47b0ce5e29c19369a50bf3d6f8990fc3278544f1f73d5c03646c0fe752ce83330eff608
+ languageName: node
+ linkType: hard
+
"css-to-react-native@npm:^3.0.0":
version: 3.2.0
resolution: "css-to-react-native@npm:3.2.0"
@@ -10809,6 +10845,15 @@ __metadata:
languageName: node
linkType: hard
+"direction@npm:^2.0.0":
+ version: 2.0.1
+ resolution: "direction@npm:2.0.1"
+ bin:
+ direction: cli.js
+ checksum: 10c0/dce809431cad978e0778769a3818ea797ebe0bd542c85032ad9ad98971e2021a146be62feb259d7ffe4b76739e07b23e861b29c3f184ac8d38cc6ba956d5c586
+ languageName: node
+ linkType: hard
+
"dns-packet@npm:^5.2.2":
version: 5.6.1
resolution: "dns-packet@npm:5.6.1"
@@ -12623,6 +12668,17 @@ __metadata:
languageName: node
linkType: hard
+"fs-extra@npm:^11.0.0":
+ version: 11.4.0
+ resolution: "fs-extra@npm:11.4.0"
+ dependencies:
+ graceful-fs: "npm:^4.2.0"
+ jsonfile: "npm:^6.0.1"
+ universalify: "npm:^2.0.0"
+ checksum: 10c0/1e311eca820a12d3d795f2043dcd5628d017f4d21e634f3f9ef314ae68bee9979758262fb9cb35ffa6f26454c3fffe8b15558733e91e8fc729b07b10bb98d8b6
+ languageName: node
+ linkType: hard
+
"fs-extra@npm:^11.1.1":
version: 11.3.4
resolution: "fs-extra@npm:11.3.4"
@@ -13350,6 +13406,30 @@ __metadata:
languageName: node
linkType: hard
+"hast-util-embedded@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "hast-util-embedded@npm:3.0.0"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ hast-util-is-element: "npm:^3.0.0"
+ checksum: 10c0/054c3d3b96fcd5c1d1c6f8d38ce1f7f33022ba6362129a022673d0b539f876acdcababbb9df29812fb927294f98ef7a2f44519a80d637fe3eea1819c9e69eeac
+ languageName: node
+ linkType: hard
+
+"hast-util-from-html@npm:^2.0.0":
+ version: 2.0.3
+ resolution: "hast-util-from-html@npm:2.0.3"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ devlop: "npm:^1.1.0"
+ hast-util-from-parse5: "npm:^8.0.0"
+ parse5: "npm:^7.0.0"
+ vfile: "npm:^6.0.0"
+ vfile-message: "npm:^4.0.0"
+ checksum: 10c0/993ef707c1a12474c8d4094fc9706a72826c660a7e308ea54c50ad893353d32e139b7cbc67510c2e82feac572b320e3b05aeb13d0f9c6302d61261f337b46764
+ languageName: node
+ linkType: hard
+
"hast-util-from-parse5@npm:^8.0.0":
version: 8.0.3
resolution: "hast-util-from-parse5@npm:8.0.3"
@@ -13366,6 +13446,46 @@ __metadata:
languageName: node
linkType: hard
+"hast-util-has-property@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "hast-util-has-property@npm:3.0.0"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ checksum: 10c0/6e2c0e22ca893c6ebb60f8390e184c4deb041c36d09796756f02cd121c1789c0f5c862ed06caea8f1a80ea8c0ef6a7854dd57946c2eebb76488727bd4a1c952e
+ languageName: node
+ linkType: hard
+
+"hast-util-is-body-ok-link@npm:^3.0.0":
+ version: 3.0.1
+ resolution: "hast-util-is-body-ok-link@npm:3.0.1"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ checksum: 10c0/c320cbd9a9a834b007a6f2f8c271e98b8331c0193adf06e0a7c5ea0acae664e97ce28eb4436e0658bc5cdb8f47390ec1c6cba7c4fe1ded10951fcdd1432f60bf
+ languageName: node
+ linkType: hard
+
+"hast-util-is-element@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "hast-util-is-element@npm:3.0.0"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ checksum: 10c0/f5361e4c9859c587ca8eb0d8343492f3077ccaa0f58a44cd09f35d5038f94d65152288dcd0c19336ef2c9491ec4d4e45fde2176b05293437021570aa0bc3613b
+ languageName: node
+ linkType: hard
+
+"hast-util-minify-whitespace@npm:^1.0.0":
+ version: 1.0.1
+ resolution: "hast-util-minify-whitespace@npm:1.0.1"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ hast-util-embedded: "npm:^3.0.0"
+ hast-util-is-element: "npm:^3.0.0"
+ hast-util-whitespace: "npm:^3.0.0"
+ unist-util-is: "npm:^6.0.0"
+ checksum: 10c0/20a7d64947e080463084f444ad09c7f28c40e7648ca2d9c6c036e42a67f8e945d352560ff599304c988257c1e477abcf6a1f508c0900211fa58ec1ba21b36533
+ languageName: node
+ linkType: hard
+
"hast-util-parse-selector@npm:^4.0.0":
version: 4.0.0
resolution: "hast-util-parse-selector@npm:4.0.0"
@@ -13375,6 +13495,19 @@ __metadata:
languageName: node
linkType: hard
+"hast-util-phrasing@npm:^3.0.0":
+ version: 3.0.1
+ resolution: "hast-util-phrasing@npm:3.0.1"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ hast-util-embedded: "npm:^3.0.0"
+ hast-util-has-property: "npm:^3.0.0"
+ hast-util-is-body-ok-link: "npm:^3.0.0"
+ hast-util-is-element: "npm:^3.0.0"
+ checksum: 10c0/d77e186ea3d7d62f6db9c4a55c3e6d9f1f6affd5f40250e8de9d73f167ae19fcc02fafe1601dfbe36e90f76ed5013ac004f0b6b398aee3a04a7a81de12788600
+ languageName: node
+ linkType: hard
+
"hast-util-raw@npm:^9.0.0":
version: 9.1.0
resolution: "hast-util-raw@npm:9.1.0"
@@ -13396,6 +13529,29 @@ __metadata:
languageName: node
linkType: hard
+"hast-util-select@npm:^6.0.4":
+ version: 6.0.4
+ resolution: "hast-util-select@npm:6.0.4"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ "@types/unist": "npm:^3.0.0"
+ bcp-47-match: "npm:^2.0.0"
+ comma-separated-tokens: "npm:^2.0.0"
+ css-selector-parser: "npm:^3.0.0"
+ devlop: "npm:^1.0.0"
+ direction: "npm:^2.0.0"
+ hast-util-has-property: "npm:^3.0.0"
+ hast-util-to-string: "npm:^3.0.0"
+ hast-util-whitespace: "npm:^3.0.0"
+ nth-check: "npm:^2.0.0"
+ property-information: "npm:^7.0.0"
+ space-separated-tokens: "npm:^2.0.0"
+ unist-util-visit: "npm:^5.0.0"
+ zwitch: "npm:^2.0.0"
+ checksum: 10c0/d6829953f829c24ffe465c2b156f6a7cd352f7d9b4d601e0e6ca38b85cc4a720bb9f027d34881c3b2a05f4b55c9375e256dbf43ca88604230da784e1c9c7d03f
+ languageName: node
+ linkType: hard
+
"hast-util-to-estree@npm:^3.0.0":
version: 3.1.3
resolution: "hast-util-to-estree@npm:3.1.3"
@@ -13420,6 +13576,25 @@ __metadata:
languageName: node
linkType: hard
+"hast-util-to-html@npm:^9.0.0, hast-util-to-html@npm:^9.0.5":
+ version: 9.0.5
+ resolution: "hast-util-to-html@npm:9.0.5"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ "@types/unist": "npm:^3.0.0"
+ ccount: "npm:^2.0.0"
+ comma-separated-tokens: "npm:^2.0.0"
+ hast-util-whitespace: "npm:^3.0.0"
+ html-void-elements: "npm:^3.0.0"
+ mdast-util-to-hast: "npm:^13.0.0"
+ property-information: "npm:^7.0.0"
+ space-separated-tokens: "npm:^2.0.0"
+ stringify-entities: "npm:^4.0.0"
+ zwitch: "npm:^2.0.4"
+ checksum: 10c0/b7a08c30bab4371fc9b4a620965c40b270e5ae7a8e94cf885f43b21705179e28c8e43b39c72885d1647965fb3738654e6962eb8b58b0c2a84271655b4d748836
+ languageName: node
+ linkType: hard
+
"hast-util-to-jsx-runtime@npm:^2.0.0":
version: 2.3.6
resolution: "hast-util-to-jsx-runtime@npm:2.3.6"
@@ -13443,6 +13618,28 @@ __metadata:
languageName: node
linkType: hard
+"hast-util-to-mdast@npm:^10.0.0":
+ version: 10.1.2
+ resolution: "hast-util-to-mdast@npm:10.1.2"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ "@types/mdast": "npm:^4.0.0"
+ "@ungap/structured-clone": "npm:^1.0.0"
+ hast-util-phrasing: "npm:^3.0.0"
+ hast-util-to-html: "npm:^9.0.0"
+ hast-util-to-text: "npm:^4.0.0"
+ hast-util-whitespace: "npm:^3.0.0"
+ mdast-util-phrasing: "npm:^4.0.0"
+ mdast-util-to-hast: "npm:^13.0.0"
+ mdast-util-to-string: "npm:^4.0.0"
+ rehype-minify-whitespace: "npm:^6.0.0"
+ trim-trailing-lines: "npm:^2.0.0"
+ unist-util-position: "npm:^5.0.0"
+ unist-util-visit: "npm:^5.0.0"
+ checksum: 10c0/2edd4521b147734078d66e03cd43c571a0a3aeefd3fcc34659c783b25e9222ddb5c8c759b12a86ebc70a25b3888505dc59b913ff36ae17cca04d52050592a963
+ languageName: node
+ linkType: hard
+
"hast-util-to-parse5@npm:^8.0.0":
version: 8.0.1
resolution: "hast-util-to-parse5@npm:8.0.1"
@@ -13458,6 +13655,27 @@ __metadata:
languageName: node
linkType: hard
+"hast-util-to-string@npm:^3.0.0, hast-util-to-string@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "hast-util-to-string@npm:3.0.1"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ checksum: 10c0/b5fa1912a6ba6131affae52a0f4394406c4c0d23c2b0307f1d69988f1030c7bb830289303e67c5ad8f674f5f23a454c1dcd492c39e45a22c1f46d3c9bce5bd0c
+ languageName: node
+ linkType: hard
+
+"hast-util-to-text@npm:^4.0.0":
+ version: 4.0.2
+ resolution: "hast-util-to-text@npm:4.0.2"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ "@types/unist": "npm:^3.0.0"
+ hast-util-is-element: "npm:^3.0.0"
+ unist-util-find-after: "npm:^5.0.0"
+ checksum: 10c0/93ecc10e68fe5391c6e634140eb330942e71dea2724c8e0c647c73ed74a8ec930a4b77043b5081284808c96f73f2bee64ee416038ece75a63a467e8d14f09946
+ languageName: node
+ linkType: hard
+
"hast-util-whitespace@npm:^3.0.0":
version: 3.0.0
resolution: "hast-util-whitespace@npm:3.0.0"
@@ -16852,6 +17070,7 @@ __metadata:
preact: "npm:^10.17.0"
vite: "npm:^8.0.0"
vitest: "npm:^4.0.18"
+ yaml: "npm:^2.8.1"
languageName: unknown
linkType: soft
@@ -18818,7 +19037,7 @@ __metadata:
languageName: node
linkType: hard
-"nth-check@npm:^2.0.1":
+"nth-check@npm:^2.0.0, nth-check@npm:^2.0.1":
version: 2.1.1
resolution: "nth-check@npm:2.1.1"
dependencies:
@@ -21607,6 +21826,27 @@ __metadata:
languageName: node
linkType: hard
+"rehype-minify-whitespace@npm:^6.0.0":
+ version: 6.0.2
+ resolution: "rehype-minify-whitespace@npm:6.0.2"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ hast-util-minify-whitespace: "npm:^1.0.0"
+ checksum: 10c0/e808a452068392070dcba4ea0fdc24c783e21ddc9c70008f90827ddd29afa6fb82f77473bba91e06b48cef8575553f906fa8ab44ae59700f945eb0910927acd9
+ languageName: node
+ linkType: hard
+
+"rehype-parse@npm:^9":
+ version: 9.0.1
+ resolution: "rehype-parse@npm:9.0.1"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ hast-util-from-html: "npm:^2.0.0"
+ unified: "npm:^11.0.0"
+ checksum: 10c0/efa9ca17673fe70e2d322a1d262796bbed5f6a89382f8f8393352bbd6f6bbf1d4d1d050984b86ff9cb6c0fa2535175ab0829e53c94b1e38fc3c158e6c0ad90bc
+ languageName: node
+ linkType: hard
+
"rehype-raw@npm:^7.0.0":
version: 7.0.0
resolution: "rehype-raw@npm:7.0.0"
@@ -21629,6 +21869,19 @@ __metadata:
languageName: node
linkType: hard
+"rehype-remark@npm:^10":
+ version: 10.0.1
+ resolution: "rehype-remark@npm:10.0.1"
+ dependencies:
+ "@types/hast": "npm:^3.0.0"
+ "@types/mdast": "npm:^4.0.0"
+ hast-util-to-mdast: "npm:^10.0.0"
+ unified: "npm:^11.0.0"
+ vfile: "npm:^6.0.0"
+ checksum: 10c0/e013fad22dd7b3bf653a79cf3dc4fecd434c5eb5f89f41e1932ae12f592b3a83c980f759d4a6ae764a61a6ea7f08330f9908c235c11510d3be731e80290aa0ba
+ languageName: node
+ linkType: hard
+
"relateurl@npm:^0.2.7":
version: 0.2.7
resolution: "relateurl@npm:0.2.7"
@@ -21673,7 +21926,7 @@ __metadata:
languageName: node
linkType: hard
-"remark-gfm@npm:^4.0.0":
+"remark-gfm@npm:^4, remark-gfm@npm:^4.0.0":
version: 4.0.1
resolution: "remark-gfm@npm:4.0.1"
dependencies:
@@ -21734,7 +21987,7 @@ __metadata:
languageName: node
linkType: hard
-"remark-stringify@npm:^11.0.0":
+"remark-stringify@npm:^11, remark-stringify@npm:^11.0.0":
version: 11.0.0
resolution: "remark-stringify@npm:11.0.0"
dependencies:
@@ -23093,7 +23346,7 @@ __metadata:
languageName: node
linkType: hard
-"string-width@npm:^5.0.1, string-width@npm:^5.1.2":
+"string-width@npm:^5.0.0, string-width@npm:^5.0.1, string-width@npm:^5.1.2":
version: 5.1.2
resolution: "string-width@npm:5.1.2"
dependencies:
@@ -23862,6 +24115,13 @@ __metadata:
languageName: node
linkType: hard
+"trim-trailing-lines@npm:^2.0.0":
+ version: 2.1.0
+ resolution: "trim-trailing-lines@npm:2.1.0"
+ checksum: 10c0/9b010d16b191422d08678f5a4988213dffd8ae9445e1b0f7f7b3e5b28ffdb062a8465a7988b66999b90589b386ddc93b56d23545ba75a74ebaf5838b30594cb9
+ languageName: node
+ linkType: hard
+
"trim@npm:0.0.1":
version: 0.0.1
resolution: "trim@npm:0.0.1"
@@ -24389,7 +24649,7 @@ __metadata:
languageName: node
linkType: hard
-"unified@npm:^11.0.0, unified@npm:^11.0.3, unified@npm:^11.0.4":
+"unified@npm:^11, unified@npm:^11.0.0, unified@npm:^11.0.3, unified@npm:^11.0.4":
version: 11.0.5
resolution: "unified@npm:11.0.5"
dependencies:
@@ -24456,6 +24716,16 @@ __metadata:
languageName: node
linkType: hard
+"unist-util-find-after@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "unist-util-find-after@npm:5.0.0"
+ dependencies:
+ "@types/unist": "npm:^3.0.0"
+ unist-util-is: "npm:^6.0.0"
+ checksum: 10c0/a7cea473c4384df8de867c456b797ff1221b20f822e1af673ff5812ed505358b36f47f3b084ac14c3622cb879ed833b71b288e8aa71025352a2aab4c2925a6eb
+ languageName: node
+ linkType: hard
+
"unist-util-is@npm:^6.0.0":
version: 6.0.1
resolution: "unist-util-is@npm:6.0.1"
@@ -24521,7 +24791,7 @@ __metadata:
languageName: node
linkType: hard
-"unist-util-visit@npm:^5.0.0":
+"unist-util-visit@npm:^5, unist-util-visit@npm:^5.0.0":
version: 5.1.0
resolution: "unist-util-visit@npm:5.1.0"
dependencies:
@@ -25244,6 +25514,7 @@ __metadata:
"@docusaurus/tsconfig": "npm:^3.9.2"
"@probe.gl/stats": "npm:^4.1.1"
"@probe.gl/stats-widget": "npm:^4.1.1"
+ "@signalwire/docusaurus-plugin-llms-txt": "npm:^1.2.2"
"@stackblitz/sdk": "npm:^1.11.0"
"@vis.gl/docusaurus-website": "npm:1.0.0-alpha.21"
clsx: "npm:^1.1.1"
@@ -25715,7 +25986,7 @@ __metadata:
languageName: node
linkType: hard
-"yaml@npm:^2.8.3":
+"yaml@npm:^2.8.1, yaml@npm:^2.8.3":
version: 2.9.0
resolution: "yaml@npm:2.9.0"
bin:
@@ -25809,7 +26080,7 @@ __metadata:
languageName: node
linkType: hard
-"zwitch@npm:^2.0.0":
+"zwitch@npm:^2.0.0, zwitch@npm:^2.0.4":
version: 2.0.4
resolution: "zwitch@npm:2.0.4"
checksum: 10c0/3c7830cdd3378667e058ffdb4cf2bb78ac5711214e2725900873accb23f3dfe5f9e7e5a06dcdc5f29605da976fc45c26d9a13ca334d6eea2245a15e77b8fc06e