{"owner":"maplibre","repo":"maplibre-gl-js","hasSkills":true,"totalSkillsCount":4,"totalTokensCount":2889,"categories":["root-instruction","mcp-config","marketplace","plugin-manifest"],"hasMcp":true,"mcpConfig":{"mcpServers":{"maplibre-gl-js":{"command":"npx","args":["-y","@modelcontextprotocol/server-maplibre-gl-js"]}}},"found":["AGENTS.md",".cursor/mcp.json",".claude-plugin/marketplace.json",".claude-plugin/plugin.json"],"skills":{"AGENTS.md":"# Agent conventions\n\n## Comments\n\nExplain code in **TSDoc comments on the declaration**, not in comments inside the body.\n\nA reader meets a function through its signature and its doc block, and so does the generated API\ndocumentation. A comment buried three lines into a method is invisible to both. Anything worth\nsaying about *what* a function does, *why* it exists, or what a caller has to know belongs above it:\n\n```ts\n/**\n * Returns the offsets, counted in UTF-16 code units, at which a word begins.\n *\n * Leaving this to the segmenter rather than to a table of punctuation brings word wrapping to\n * writing systems that do not put spaces between words, because the browser has the dictionaries.\n */\nexport function wordBoundaries(text: string): Set<number> {\n```\n\nThe same goes for a type, a field, or a constant: document it where it is declared.\n\n```ts\ntype Entry = {\n    /** One TinySDF per `font-faces` file this stack draws with, keyed by the file's CSS family. */\n    fontFaceTinySDFs?: {[family: string]: Promise<TinySDF>};\n};\n```\n\nAn in-body comment is a last resort, for a single step whose reason cannot be seen from the code and\ndoes not generalise to the whole function — a workaround for a specific browser bug, say. If you\nfind yourself writing several, the explanation belongs in the doc block, or the body wants splitting\ninto named functions that can each carry one.\n\nDo not restate the code (`// increment i`), and do not narrate the change you are making\n(`// now uses the segmenter`) — the diff already says that, and it stops being true immediately.\n\n**Describe what the code does, not how it came to be that way.** Two things read as documentation\nbut are not.\n\nThe first is the change itself. A comment that frames the present approach against an earlier one —\n\"reads the derived table rather than `ArabicShaping.txt`\", \"no longer needs the plugin\", \"this used\nto be done per codepoint\" — is a changelog entry to everyone who arrives after the change, and what\nit contrasts with is not in the file for them to compare against.\n\nThe second is the decision behind it. A comment that argues the choice — \"the package that carries\nthis unpacks to more than 250 MB\", \"we went with this because the alternative was too slow\" — is the\ncase for a pull request, written for reviewers who are weighing the options. Whoever reads the merged\ncode is not weighing anything, and the rationale belongs in the PR or the issue that tracks it.\n\nWrite as though the code had always looked this way and no other way was ever on the table. Where a\nfuture maintainer would otherwise walk into a trap, state the trap as a fact about the problem — \"a\nglyphs URL serves codepoints, so it has no way to serve a cluster\" — rather than as the story of how\nit came to be avoided.\n\n**No file-level preamble.** A comment belongs to the thing below it, not to the file. A block at the\ntop of a module explaining the subject in general has nothing to attach to: it is not in the API\ndocs, nobody scrolls back up to it from the function they are reading, and it drifts once the file\ngains a function the preamble never anticipated.\n\nEverything such a preamble wants to say belongs to one of the declarations underneath it. Why a\ngrapheme cluster is the unit of layout belongs on the function that produces them. What\n`Intl.Segmenter` does belongs on the constant that holds it. Split it up and put each part where a\nreader meets the code it explains — if a part fits nowhere, it was background rather than\ndocumentation, and the PR description is where it goes.\n\n## Nesting\n\n**Keep indentation shallow.** Every level a reader descends is another condition they have to hold\nin their head to know whether the line in front of them runs at all. Three levels inside a function\nis usually the point to stop and restructure.\n\nTwo moves cover most of it. **Hoist what does not vary** — a condition that depends on none of the\nloop variables is computed once, above the loop, under a name:\n\n```ts\nconst needsVerticalForms = (textAlongLine || allowVerticalPlacement) && doesAllowVerticalWritingMode;\n\nfor (const grapheme of toGraphemes(text)) {\n```\n\n**Turn a wrapping `if` into a guard.** `if (!ready) continue;` in a loop, or an early `return` in a\nfunction, puts the exceptional case out of the way and lets the work that matters sit at the\nouter level instead of inside a block:\n\n```ts\nfor (const char of grapheme) {\n    stack[char] = true;\n    if (!needsVerticalForms) continue;\n\n    const verticalChar = verticalizedCharacterMap[char];\n    if (verticalChar) stack[verticalChar] = true;\n}\n```\n\nExtracting a named function is the third move, and the right one when a block has grown its own\nsubject rather than merely its own indentation.\n\nFlattening must not change behaviour. Splitting one pass into two reorders the work, which is fine\nfor a pure computation and not fine where something downstream can observe the order — insertion\norder into an object used as a set, for one. If you cannot see that it is safe, leave it nested.\n\n## Types\n\n**Spell a map as `Record<K, V>`, not as an index signature.** `Record<string, Promise<TinySDF>>`\nreads as one thing; `{[family: string]: Promise<TinySDF>}` makes the reader parse a type literal to\nfind out it is a map, and nests badly — a map of maps is four lines as an index signature and one as\na `Record`.\n\n```ts\nglyphs: Record<string, StyleGlyph | null>;\nexport type GetGlyphsResponse = Record<string, Record<string, StyleGlyph>>;\n```\n\nKeep an index signature only where it carries something a `Record` cannot: a named key that\ndocuments itself in a type with other members, or a numeric key alongside declared properties.\n\n## Changelog\n\n**One line per entry.** Say what changed and what it means for someone using the library, then stop.\nThe changelog is skimmed, not read, and a paragraph buries the one clause a reader was looking for.\n\n```md\n- Support `font-faces` style spec property and improve text rendering for complex script languages ([#6637](https://github.com/maplibre/maplibre-gl-js/issues/6637))\n```\n\nResist explaining the mechanism, listing every affected script, or narrating the design decision —\nthat belongs in the PR description and the code's own doc comments. If a change genuinely needs two\nsentences to be understood, it is usually two entries, or one entry and a link.\n\n## Tests\n\n**Declare test helpers with `function`, not with a lambda assigned to a `const`.** A hoisted,\nnamed function reads as part of the file's vocabulary and can be defined below the tests that use it;\n`const helper = () => {...}` has to come first and puts the name and the arguments further apart.\n\n```ts\nfunction createGlyphManager(remoteEnabled: boolean, font?: string | false): GlyphManager {\n```\n\n**Cover the code, then stop.** Every branch worth a name deserves a test, but two tests that differ\nonly in an input value are one test — consolidate them and pick the input that says the most. What\nyou are guarding against is a suite where a single change reddens fifteen tests that were all making\nthe same point.\n\n**Keep tests DAMP rather than DRY.** A test should read top to bottom without the reader having to go\nand find what a shared fixture contains. Repeating three lines of setup in each test is cheaper for\nthe reader than one clever builder that every test calls with different arguments. Factor out only\nwhat is genuinely incidental — a stub of an unrelated collaborator, say — and leave the parts the\ntest is actually about inline.\n\n**Test through the public API.** Do not reach for `_`-prefixed fields or methods. If a behaviour can\nonly be observed through internals, that is a sign the class is missing an accessor or the test is\nasserting on the wrong thing — assert on what a caller can see.\n\n**Fake the network with a fake server, not by mocking the request functions.** `nise`'s\n`fakeServer` is what the rest of the suite uses. Mocking `getArrayBuffer` or `getJSON` skips the\nrequest-building the code under test does — the URL it composed, the headers it set, the\n`transformRequest` it went through — which is usually the part worth asserting on.\n\n```ts\nlet server: FakeServer;\n\nbeforeEach(() => {\n    global.fetch = null;   // sends the request down the XHR path the fake server intercepts\n    server = fakeServer.create();\n});\n\nafterEach(() => {\n    server.restore();\n});\n```\n\n**Stub collaborators, never the unit under test.** Replacing a method on the class you are testing\nmeans the test no longer exercises the thing it names. Replace what that class *uses* — its\ndependencies, the network, the clock — and let the unit itself run.\n",".cursor/mcp.json":"# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.cursor/mcp.json)",".claude-plugin/marketplace.json":"# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.claude-plugin/marketplace.json)",".claude-plugin/plugin.json":"# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.claude-plugin/plugin.json)"},"files":{"AGENTS.md":"# Agent conventions\n\n## Comments\n\nExplain code in **TSDoc comments on the declaration**, not in comments inside the body.\n\nA reader meets a function through its signature and its doc block, and so does the generated API\ndocumentation. A comment buried three lines into a method is invisible to both. Anything worth\nsaying about *what* a function does, *why* it exists, or what a caller has to know belongs above it:\n\n```ts\n/**\n * Returns the offsets, counted in UTF-16 code units, at which a word begins.\n *\n * Leaving this to the segmenter rather than to a table of punctuation brings word wrapping to\n * writing systems that do not put spaces between words, because the browser has the dictionaries.\n */\nexport function wordBoundaries(text: string): Set<number> {\n```\n\nThe same goes for a type, a field, or a constant: document it where it is declared.\n\n```ts\ntype Entry = {\n    /** One TinySDF per `font-faces` file this stack draws with, keyed by the file's CSS family. */\n    fontFaceTinySDFs?: {[family: string]: Promise<TinySDF>};\n};\n```\n\nAn in-body comment is a last resort, for a single step whose reason cannot be seen from the code and\ndoes not generalise to the whole function — a workaround for a specific browser bug, say. If you\nfind yourself writing several, the explanation belongs in the doc block, or the body wants splitting\ninto named functions that can each carry one.\n\nDo not restate the code (`// increment i`), and do not narrate the change you are making\n(`// now uses the segmenter`) — the diff already says that, and it stops being true immediately.\n\n**Describe what the code does, not how it came to be that way.** Two things read as documentation\nbut are not.\n\nThe first is the change itself. A comment that frames the present approach against an earlier one —\n\"reads the derived table rather than `ArabicShaping.txt`\", \"no longer needs the plugin\", \"this used\nto be done per codepoint\" — is a changelog entry to everyone who arrives after the change, and what\nit contrasts with is not in the file for them to compare against.\n\nThe second is the decision behind it. A comment that argues the choice — \"the package that carries\nthis unpacks to more than 250 MB\", \"we went with this because the alternative was too slow\" — is the\ncase for a pull request, written for reviewers who are weighing the options. Whoever reads the merged\ncode is not weighing anything, and the rationale belongs in the PR or the issue that tracks it.\n\nWrite as though the code had always looked this way and no other way was ever on the table. Where a\nfuture maintainer would otherwise walk into a trap, state the trap as a fact about the problem — \"a\nglyphs URL serves codepoints, so it has no way to serve a cluster\" — rather than as the story of how\nit came to be avoided.\n\n**No file-level preamble.** A comment belongs to the thing below it, not to the file. A block at the\ntop of a module explaining the subject in general has nothing to attach to: it is not in the API\ndocs, nobody scrolls back up to it from the function they are reading, and it drifts once the file\ngains a function the preamble never anticipated.\n\nEverything such a preamble wants to say belongs to one of the declarations underneath it. Why a\ngrapheme cluster is the unit of layout belongs on the function that produces them. What\n`Intl.Segmenter` does belongs on the constant that holds it. Split it up and put each part where a\nreader meets the code it explains — if a part fits nowhere, it was background rather than\ndocumentation, and the PR description is where it goes.\n\n## Nesting\n\n**Keep indentation shallow.** Every level a reader descends is another condition they have to hold\nin their head to know whether the line in front of them runs at all. Three levels inside a function\nis usually the point to stop and restructure.\n\nTwo moves cover most of it. **Hoist what does not vary** — a condition that depends on none of the\nloop variables is computed once, above the loop, under a name:\n\n```ts\nconst needsVerticalForms = (textAlongLine || allowVerticalPlacement) && doesAllowVerticalWritingMode;\n\nfor (const grapheme of toGraphemes(text)) {\n```\n\n**Turn a wrapping `if` into a guard.** `if (!ready) continue;` in a loop, or an early `return` in a\nfunction, puts the exceptional case out of the way and lets the work that matters sit at the\nouter level instead of inside a block:\n\n```ts\nfor (const char of grapheme) {\n    stack[char] = true;\n    if (!needsVerticalForms) continue;\n\n    const verticalChar = verticalizedCharacterMap[char];\n    if (verticalChar) stack[verticalChar] = true;\n}\n```\n\nExtracting a named function is the third move, and the right one when a block has grown its own\nsubject rather than merely its own indentation.\n\nFlattening must not change behaviour. Splitting one pass into two reorders the work, which is fine\nfor a pure computation and not fine where something downstream can observe the order — insertion\norder into an object used as a set, for one. If you cannot see that it is safe, leave it nested.\n\n## Types\n\n**Spell a map as `Record<K, V>`, not as an index signature.** `Record<string, Promise<TinySDF>>`\nreads as one thing; `{[family: string]: Promise<TinySDF>}` makes the reader parse a type literal to\nfind out it is a map, and nests badly — a map of maps is four lines as an index signature and one as\na `Record`.\n\n```ts\nglyphs: Record<string, StyleGlyph | null>;\nexport type GetGlyphsResponse = Record<string, Record<string, StyleGlyph>>;\n```\n\nKeep an index signature only where it carries something a `Record` cannot: a named key that\ndocuments itself in a type with other members, or a numeric key alongside declared properties.\n\n## Changelog\n\n**One line per entry.** Say what changed and what it means for someone using the library, then stop.\nThe changelog is skimmed, not read, and a paragraph buries the one clause a reader was looking for.\n\n```md\n- Support `font-faces` style spec property and improve text rendering for complex script languages ([#6637](https://github.com/maplibre/maplibre-gl-js/issues/6637))\n```\n\nResist explaining the mechanism, listing every affected script, or narrating the design decision —\nthat belongs in the PR description and the code's own doc comments. If a change genuinely needs two\nsentences to be understood, it is usually two entries, or one entry and a link.\n\n## Tests\n\n**Declare test helpers with `function`, not with a lambda assigned to a `const`.** A hoisted,\nnamed function reads as part of the file's vocabulary and can be defined below the tests that use it;\n`const helper = () => {...}` has to come first and puts the name and the arguments further apart.\n\n```ts\nfunction createGlyphManager(remoteEnabled: boolean, font?: string | false): GlyphManager {\n```\n\n**Cover the code, then stop.** Every branch worth a name deserves a test, but two tests that differ\nonly in an input value are one test — consolidate them and pick the input that says the most. What\nyou are guarding against is a suite where a single change reddens fifteen tests that were all making\nthe same point.\n\n**Keep tests DAMP rather than DRY.** A test should read top to bottom without the reader having to go\nand find what a shared fixture contains. Repeating three lines of setup in each test is cheaper for\nthe reader than one clever builder that every test calls with different arguments. Factor out only\nwhat is genuinely incidental — a stub of an unrelated collaborator, say — and leave the parts the\ntest is actually about inline.\n\n**Test through the public API.** Do not reach for `_`-prefixed fields or methods. If a behaviour can\nonly be observed through internals, that is a sign the class is missing an accessor or the test is\nasserting on the wrong thing — assert on what a caller can see.\n\n**Fake the network with a fake server, not by mocking the request functions.** `nise`'s\n`fakeServer` is what the rest of the suite uses. Mocking `getArrayBuffer` or `getJSON` skips the\nrequest-building the code under test does — the URL it composed, the headers it set, the\n`transformRequest` it went through — which is usually the part worth asserting on.\n\n```ts\nlet server: FakeServer;\n\nbeforeEach(() => {\n    global.fetch = null;   // sends the request down the XHR path the fake server intercepts\n    server = fakeServer.create();\n});\n\nafterEach(() => {\n    server.restore();\n});\n```\n\n**Stub collaborators, never the unit under test.** Replacing a method on the class you are testing\nmeans the test no longer exercises the thing it names. Replace what that class *uses* — its\ndependencies, the network, the clock — and let the unit itself run.\n",".cursor/mcp.json":"# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.cursor/mcp.json)",".claude-plugin/marketplace.json":"# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.claude-plugin/marketplace.json)",".claude-plugin/plugin.json":"# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.claude-plugin/plugin.json)"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Agent conventions\n\n## Comments\n\nExplain code in **TSDoc comments on the declaration**, not in comments inside the body.\n\nA reader meets a function through its signature and its doc block, and so does the generated API\ndocumentation. A comment buried three lines into a method is invisible to both. Anything worth\nsaying about *what* a function does, *why* it exists, or what a caller has to know belongs above it:\n\n```ts\n/**\n * Returns the offsets, counted in UTF-16 code units, at which a word begins.\n *\n * Leaving this to the segmenter rather than to a table of punctuation brings word wrapping to\n * writing systems that do not put spaces between words, because the browser has the dictionaries.\n */\nexport function wordBoundaries(text: string): Set<number> {\n```\n\nThe same goes for a type, a field, or a constant: document it where it is declared.\n\n```ts\ntype Entry = {\n    /** One TinySDF per `font-faces` file this stack draws with, keyed by the file's CSS family. */\n    fontFaceTinySDFs?: {[family: string]: Promise<TinySDF>};\n};\n```\n\nAn in-body comment is a last resort, for a single step whose reason cannot be seen from the code and\ndoes not generalise to the whole function — a workaround for a specific browser bug, say. If you\nfind yourself writing several, the explanation belongs in the doc block, or the body wants splitting\ninto named functions that can each carry one.\n\nDo not restate the code (`// increment i`), and do not narrate the change you are making\n(`// now uses the segmenter`) — the diff already says that, and it stops being true immediately.\n\n**Describe what the code does, not how it came to be that way.** Two things read as documentation\nbut are not.\n\nThe first is the change itself. A comment that frames the present approach against an earlier one —\n\"reads the derived table rather than `ArabicShaping.txt`\", \"no longer needs the plugin\", \"this used\nto be done per codepoint\" — is a changelog entry to everyone who arrives after the change, and what\nit contrasts with is not in the file for them to compare against.\n\nThe second is the decision behind it. A comment that argues the choice — \"the package that carries\nthis unpacks to more than 250 MB\", \"we went with this because the alternative was too slow\" — is the\ncase for a pull request, written for reviewers who are weighing the options. Whoever reads the merged\ncode is not weighing anything, and the rationale belongs in the PR or the issue that tracks it.\n\nWrite as though the code had always looked this way and no other way was ever on the table. Where a\nfuture maintainer would otherwise walk into a trap, state the trap as a fact about the problem — \"a\nglyphs URL serves codepoints, so it has no way to serve a cluster\" — rather than as the story of how\nit came to be avoided.\n\n**No file-level preamble.** A comment belongs to the thing below it, not to the file. A block at the\ntop of a module explaining the subject in general has nothing to attach to: it is not in the API\ndocs, nobody scrolls back up to it from the function they are reading, and it drifts once the file\ngains a function the preamble never anticipated.\n\nEverything such a preamble wants to say belongs to one of the declarations underneath it. Why a\ngrapheme cluster is the unit of layout belongs on the function that produces them. What\n`Intl.Segmenter` does belongs on the constant that holds it. Split it up and put each part where a\nreader meets the code it explains — if a part fits nowhere, it was background rather than\ndocumentation, and the PR description is where it goes.\n\n## Nesting\n\n**Keep indentation shallow.** Every level a reader descends is another condition they have to hold\nin their head to know whether the line in front of them runs at all. Three levels inside a function\nis usually the point to stop and restructure.\n\nTwo moves cover most of it. **Hoist what does not vary** — a condition that depends on none of the\nloop variables is computed once, above the loop, under a name:\n\n```ts\nconst needsVerticalForms = (textAlongLine || allowVerticalPlacement) && doesAllowVerticalWritingMode;\n\nfor (const grapheme of toGraphemes(text)) {\n```\n\n**Turn a wrapping `if` into a guard.** `if (!ready) continue;` in a loop, or an early `return` in a\nfunction, puts the exceptional case out of the way and lets the work that matters sit at the\nouter level instead of inside a block:\n\n```ts\nfor (const char of grapheme) {\n    stack[char] = true;\n    if (!needsVerticalForms) continue;\n\n    const verticalChar = verticalizedCharacterMap[char];\n    if (verticalChar) stack[verticalChar] = true;\n}\n```\n\nExtracting a named function is the third move, and the right one when a block has grown its own\nsubject rather than merely its own indentation.\n\nFlattening must not change behaviour. Splitting one pass into two reorders the work, which is fine\nfor a pure computation and not fine where something downstream can observe the order — insertion\norder into an object used as a set, for one. If you cannot see that it is safe, leave it nested.\n\n## Types\n\n**Spell a map as `Record<K, V>`, not as an index signature.** `Record<string, Promise<TinySDF>>`\nreads as one thing; `{[family: string]: Promise<TinySDF>}` makes the reader parse a type literal to\nfind out it is a map, and nests badly — a map of maps is four lines as an index signature and one as\na `Record`.\n\n```ts\nglyphs: Record<string, StyleGlyph | null>;\nexport type GetGlyphsResponse = Record<string, Record<string, StyleGlyph>>;\n```\n\nKeep an index signature only where it carries something a `Record` cannot: a named key that\ndocuments itself in a type with other members, or a numeric key alongside declared properties.\n\n## Changelog\n\n**One line per entry.** Say what changed and what it means for someone using the library, then stop.\nThe changelog is skimmed, not read, and a paragraph buries the one clause a reader was looking for.\n\n```md\n- Support `font-faces` style spec property and improve text rendering for complex script languages ([#6637](https://github.com/maplibre/maplibre-gl-js/issues/6637))\n```\n\nResist explaining the mechanism, listing every affected script, or narrating the design decision —\nthat belongs in the PR description and the code's own doc comments. If a change genuinely needs two\nsentences to be understood, it is usually two entries, or one entry and a link.\n\n## Tests\n\n**Declare test helpers with `function`, not with a lambda assigned to a `const`.** A hoisted,\nnamed function reads as part of the file's vocabulary and can be defined below the tests that use it;\n`const helper = () => {...}` has to come first and puts the name and the arguments further apart.\n\n```ts\nfunction createGlyphManager(remoteEnabled: boolean, font?: string | false): GlyphManager {\n```\n\n**Cover the code, then stop.** Every branch worth a name deserves a test, but two tests that differ\nonly in an input value are one test — consolidate them and pick the input that says the most. What\nyou are guarding against is a suite where a single change reddens fifteen tests that were all making\nthe same point.\n\n**Keep tests DAMP rather than DRY.** A test should read top to bottom without the reader having to go\nand find what a shared fixture contains. Repeating three lines of setup in each test is cheaper for\nthe reader than one clever builder that every test calls with different arguments. Factor out only\nwhat is genuinely incidental — a stub of an unrelated collaborator, say — and leave the parts the\ntest is actually about inline.\n\n**Test through the public API.** Do not reach for `_`-prefixed fields or methods. If a behaviour can\nonly be observed through internals, that is a sign the class is missing an accessor or the test is\nasserting on the wrong thing — assert on what a caller can see.\n\n**Fake the network with a fake server, not by mocking the request functions.** `nise`'s\n`fakeServer` is what the rest of the suite uses. Mocking `getArrayBuffer` or `getJSON` skips the\nrequest-building the code under test does — the URL it composed, the headers it set, the\n`transformRequest` it went through — which is usually the part worth asserting on.\n\n```ts\nlet server: FakeServer;\n\nbeforeEach(() => {\n    global.fetch = null;   // sends the request down the XHR path the fake server intercepts\n    server = fakeServer.create();\n});\n\nafterEach(() => {\n    server.restore();\n});\n```\n\n**Stub collaborators, never the unit under test.** Replacing a method on the class you are testing\nmeans the test no longer exercises the thing it names. Replace what that class *uses* — its\ndependencies, the network, the clock — and let the unit itself run.\n","isInternal":false,"tokens":1989,"sizeBytes":8646},{"name":"marketplace.json","path":".claude-plugin/marketplace.json","rawUrl":"https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.claude-plugin/marketplace.json","title":"Claude Plugin Marketplace Catalog","category":"marketplace","format":"json","content":"# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.claude-plugin/marketplace.json)","tokens":300,"sizeBytes":202},{"name":"plugin.json","path":".claude-plugin/plugin.json","rawUrl":"https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.claude-plugin/plugin.json","title":"claude-plugin Plugin Manifest","category":"plugin-manifest","format":"json","content":"# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.claude-plugin/plugin.json)","tokens":300,"sizeBytes":188},{"name":"mcp.json","path":".cursor/mcp.json","rawUrl":"https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.cursor/mcp.json","title":"Model Context Protocol (MCP) Configuration","category":"mcp-config","format":"json","content":"# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.cursor/mcp.json)","tokens":300,"sizeBytes":181}],"systemPromptSnippet":"<yakaai_skills repo=\"maplibre/maplibre-gl-js\">\n<!-- File: AGENTS.md (Tokens: ~1989 | Category: root-instruction) -->\n# Agent conventions\n\n## Comments\n\nExplain code in **TSDoc comments on the declaration**, not in comments inside the body.\n\nA reader meets a function through its signature and its doc block, and so does the generated API\ndocumentation. A comment buried three lines into a method is invisible to both. Anything worth\nsaying about *what* a function does, *why* it exists, or what a caller has to know belongs above it:\n\n```ts\n/**\n * Returns the offsets, counted in UTF-16 code units, at which a word begins.\n *\n * Leaving this to the segmenter rather than to a table of punctuation brings word wrapping to\n * writing systems that do not put spaces between words, because the browser has the dictionaries.\n */\nexport function wordBoundaries(text: string): Set<number> {\n```\n\nThe same goes for a type, a field, or a constant: document it where it is declared.\n\n```ts\ntype Entry = {\n    /** One TinySDF per `font-faces` file this stack draws with, keyed by the file's CSS family. */\n    fontFaceTinySDFs?: {[family: string]: Promise<TinySDF>};\n};\n```\n\nAn in-body comment is a last resort, for a single step whose reason cannot be seen from the code and\ndoes not generalise to the whole function — a workaround for a specific browser bug, say. If you\nfind yourself writing several, the explanation belongs in the doc block, or the body wants splitting\ninto named functions that can each carry one.\n\nDo not restate the code (`// increment i`), and do not narrate the change you are making\n(`// now uses the segmenter`) — the diff already says that, and it stops being true immediately.\n\n**Describe what the code does, not how it came to be that way.** Two things read as documentation\nbut are not.\n\nThe first is the change itself. A comment that frames the present approach against an earlier one —\n\"reads the derived table rather than `ArabicShaping.txt`\", \"no longer needs the plugin\", \"this used\nto be done per codepoint\" — is a changelog entry to everyone who arrives after the change, and what\nit contrasts with is not in the file for them to compare against.\n\nThe second is the decision behind it. A comment that argues the choice — \"the package that carries\nthis unpacks to more than 250 MB\", \"we went with this because the alternative was too slow\" — is the\ncase for a pull request, written for reviewers who are weighing the options. Whoever reads the merged\ncode is not weighing anything, and the rationale belongs in the PR or the issue that tracks it.\n\nWrite as though the code had always looked this way and no other way was ever on the table. Where a\nfuture maintainer would otherwise walk into a trap, state the trap as a fact about the problem — \"a\nglyphs URL serves codepoints, so it has no way to serve a cluster\" — rather than as the story of how\nit came to be avoided.\n\n**No file-level preamble.** A comment belongs to the thing below it, not to the file. A block at the\ntop of a module explaining the subject in general has nothing to attach to: it is not in the API\ndocs, nobody scrolls back up to it from the function they are reading, and it drifts once the file\ngains a function the preamble never anticipated.\n\nEverything such a preamble wants to say belongs to one of the declarations underneath it. Why a\ngrapheme cluster is the unit of layout belongs on the function that produces them. What\n`Intl.Segmenter` does belongs on the constant that holds it. Split it up and put each part where a\nreader meets the code it explains — if a part fits nowhere, it was background rather than\ndocumentation, and the PR description is where it goes.\n\n## Nesting\n\n**Keep indentation shallow.** Every level a reader descends is another condition they have to hold\nin their head to know whether the line in front of them runs at all. Three levels inside a function\nis usually the point to stop and restructure.\n\nTwo moves cover most of it. **Hoist what does not vary** — a condition that depends on none of the\nloop variables is computed once, above the loop, under a name:\n\n```ts\nconst needsVerticalForms = (textAlongLine || allowVerticalPlacement) && doesAllowVerticalWritingMode;\n\nfor (const grapheme of toGraphemes(text)) {\n```\n\n**Turn a wrapping `if` into a guard.** `if (!ready) continue;` in a loop, or an early `return` in a\nfunction, puts the exceptional case out of the way and lets the work that matters sit at the\nouter level instead of inside a block:\n\n```ts\nfor (const char of grapheme) {\n    stack[char] = true;\n    if (!needsVerticalForms) continue;\n\n    const verticalChar = verticalizedCharacterMap[char];\n    if (verticalChar) stack[verticalChar] = true;\n}\n```\n\nExtracting a named function is the third move, and the right one when a block has grown its own\nsubject rather than merely its own indentation.\n\nFlattening must not change behaviour. Splitting one pass into two reorders the work, which is fine\nfor a pure computation and not fine where something downstream can observe the order — insertion\norder into an object used as a set, for one. If you cannot see that it is safe, leave it nested.\n\n## Types\n\n**Spell a map as `Record<K, V>`, not as an index signature.** `Record<string, Promise<TinySDF>>`\nreads as one thing; `{[family: string]: Promise<TinySDF>}` makes the reader parse a type literal to\nfind out it is a map, and nests badly — a map of maps is four lines as an index signature and one as\na `Record`.\n\n```ts\nglyphs: Record<string, StyleGlyph | null>;\nexport type GetGlyphsResponse = Record<string, Record<string, StyleGlyph>>;\n```\n\nKeep an index signature only where it carries something a `Record` cannot: a named key that\ndocuments itself in a type with other members, or a numeric key alongside declared properties.\n\n## Changelog\n\n**One line per entry.** Say what changed and what it means for someone using the library, then stop.\nThe changelog is skimmed, not read, and a paragraph buries the one clause a reader was looking for.\n\n```md\n- Support `font-faces` style spec property and improve text rendering for complex script languages ([#6637](https://github.com/maplibre/maplibre-gl-js/issues/6637))\n```\n\nResist explaining the mechanism, listing every affected script, or narrating the design decision —\nthat belongs in the PR description and the code's own doc comments. If a change genuinely needs two\nsentences to be understood, it is usually two entries, or one entry and a link.\n\n## Tests\n\n**Declare test helpers with `function`, not with a lambda assigned to a `const`.** A hoisted,\nnamed function reads as part of the file's vocabulary and can be defined below the tests that use it;\n`const helper = () => {...}` has to come first and puts the name and the arguments further apart.\n\n```ts\nfunction createGlyphManager(remoteEnabled: boolean, font?: string | false): GlyphManager {\n```\n\n**Cover the code, then stop.** Every branch worth a name deserves a test, but two tests that differ\nonly in an input value are one test — consolidate them and pick the input that says the most. What\nyou are guarding against is a suite where a single change reddens fifteen tests that were all making\nthe same point.\n\n**Keep tests DAMP rather than DRY.** A test should read top to bottom without the reader having to go\nand find what a shared fixture contains. Repeating three lines of setup in each test is cheaper for\nthe reader than one clever builder that every test calls with different arguments. Factor out only\nwhat is genuinely incidental — a stub of an unrelated collaborator, say — and leave the parts the\ntest is actually about inline.\n\n**Test through the public API.** Do not reach for `_`-prefixed fields or methods. If a behaviour can\nonly be observed through internals, that is a sign the class is missing an accessor or the test is\nasserting on the wrong thing — assert on what a caller can see.\n\n**Fake the network with a fake server, not by mocking the request functions.** `nise`'s\n`fakeServer` is what the rest of the suite uses. Mocking `getArrayBuffer` or `getJSON` skips the\nrequest-building the code under test does — the URL it composed, the headers it set, the\n`transformRequest` it went through — which is usually the part worth asserting on.\n\n```ts\nlet server: FakeServer;\n\nbeforeEach(() => {\n    global.fetch = null;   // sends the request down the XHR path the fake server intercepts\n    server = fakeServer.create();\n});\n\nafterEach(() => {\n    server.restore();\n});\n```\n\n**Stub collaborators, never the unit under test.** Replacing a method on the class you are testing\nmeans the test no longer exercises the thing it names. Replace what that class *uses* — its\ndependencies, the network, the clock — and let the unit itself run.\n\n<!-- File: .claude-plugin/marketplace.json (Tokens: ~300 | Category: marketplace) -->\n# Claude Plugin Marketplace Catalog\n\nPath: `.claude-plugin/marketplace.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.claude-plugin/marketplace.json)\n\n<!-- File: .claude-plugin/plugin.json (Tokens: ~300 | Category: plugin-manifest) -->\n# claude-plugin Plugin Manifest\n\nPath: `.claude-plugin/plugin.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.claude-plugin/plugin.json)\n\n<!-- File: .cursor/mcp.json (Tokens: ~300 | Category: mcp-config) -->\n# Model Context Protocol (MCP) Configuration\n\nPath: `.cursor/mcp.json`\n\n[View Raw Content on GitHub](https://raw.githubusercontent.com/maplibre/maplibre-gl-js/HEAD/.cursor/mcp.json)\n</yakaai_skills>"}