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