diff --git a/.github/workflows/snippets.yml b/.github/workflows/snippets.yml index f33cde5..c6ee1ed 100644 --- a/.github/workflows/snippets.yml +++ b/.github/workflows/snippets.yml @@ -32,6 +32,9 @@ jobs: - name: Check snippets run: pnpm run check:snippets + - name: Check nav coverage + run: pnpm run check:nav-coverage + stellar-testnet-snippets: name: Stellar snippet testnet validation runs-on: ubuntu-latest diff --git a/docs.json b/docs.json index 268920e..4bd9a68 100644 --- a/docs.json +++ b/docs.json @@ -82,6 +82,7 @@ "group": "Architecture", "pages": [ "architecture/overview", + "architecture/announcement-format", "architecture/chain-connectors", "architecture/tee", "architecture/stellar-cryptography", @@ -90,11 +91,16 @@ }, { "group": "Contracts", - "pages": ["contracts/evm", "contracts/stellar", "contracts/solana", "contracts/ckb", "reference/stellar-event-schemas"] + "pages": ["contracts/evm", "contracts/stellar", "contracts/solana", "contracts/ckb"] }, { "group": "Reference", - "pages": ["reference/security-disclosure", "reference/threat-model", "reference/audits"] + "pages": [ + "reference/audits", + "reference/security-disclosure", + "reference/stellar-networks", + "reference/threat-model" + ] } ] }, @@ -118,19 +124,27 @@ "group": "Guides", "pages": [ "guides/stealth-payments", + "guides/stellar-quickstart", + "guides/stellar-quickstart.es", "guides/single-chain-agent", "guides/multichain-agent", "guides/bring-your-own-model", "guides/privacy-best-practices", "guides/stellar-fees", "guides/stellar-troubleshooting", - "guides/spectre-stellar-cookbook" + "guides/spectre-stellar-cookbook", + "guides/stellar-wallet-integration" ] }, { "group": "Integrations", "pages": [ - "guides/integrations/aquarius" + "guides/integrations/aquarius", + "guides/integrations/blend", + "guides/integrations/nuxt", + "guides/integrations/react-native", + "guides/integrations/reflector", + "guides/integrations/soroswap" ] }, { @@ -146,13 +160,12 @@ "guides/stellar-custom-assets", "guides/stellar/stellar-liquidity-pool-swap", "guides/stellar/stellar-path-payment", + "guides/stellar/passkey-signing", + "guides/stellar/stellar-quickstart", + "guides/stellar/wraith-names-lifecycle", "guides/wraith-names-stellar", "guides/ops/self-hosted-deployment" ] - }, - { - "group": "Integrations", - "pages": ["guides/integrations/nuxt"] } ] }, @@ -164,6 +177,7 @@ "pages": [ "api-reference/endpoints", "api-reference/fetch-announcements-stream", + "api-reference/stealth-keys", "api-reference/types" ] } diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 597665e..67593e2 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -27,8 +27,30 @@ Use `no-check` only for intentionally illustrative pseudocode: Prefer making snippets compile over opting them out. +## Navigation coverage + +Every `.mdx` page in the shipped taxonomy (root pages plus `architecture/`, +`api-reference/`, `contracts/`, `guides/`, `reference/`, and `sdk/`) must be +registered in the `docs.json` navigation tree, and every navigation entry must +resolve to a real file. This is enforced by: + +```bash +npm run check:nav-coverage +``` + +The checker (`scripts/check-nav-coverage.mjs`) scans for `.mdx` files that are +missing from `docs.json` and for nav entries that point at files that no longer +exist. Run it after adding, renaming, or removing a page: + +```bash +node scripts/check-nav-coverage.mjs +``` + +Note: `docs.json` is strict JSON — do not add `//` comments to it, the Mintlify +CLI rejects them. Keep this file comment-free. + ## CI -Every pull request runs the snippet checker through GitHub Actions. A separate -non-blocking Stellar testnet job is reserved for end-to-end snippet validation -that depends on network availability. +Every pull request runs the snippet checker and the nav coverage check through +GitHub Actions. A separate non-blocking Stellar testnet job is reserved for +end-to-end snippet validation that depends on network availability. diff --git a/package.json b/package.json index e364273..1acf7bb 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,13 @@ "type": "module", "scripts": { "check:snippets": "tsx scripts/check-snippets.ts", + "check:nav-coverage": "node scripts/check-nav-coverage.mjs", "check:stellar-testnet": "tsx scripts/check-stellar-testnet-snippets.ts", "generate:stellar-reference": "tsx scripts/generate-stellar-reference.ts", "check:stellar-reference": "tsx scripts/generate-stellar-reference.ts --check --allow-missing", "mint:validate": "mint validate", "mint:broken-links": "mint broken-links", - "test": "npm run check:snippets" + "test": "npm run check:snippets && npm run check:nav-coverage" }, "dependencies": { "@solana/web3.js": "^1.95.0", diff --git a/scripts/check-nav-coverage.mjs b/scripts/check-nav-coverage.mjs new file mode 100644 index 0000000..4cb26eb --- /dev/null +++ b/scripts/check-nav-coverage.mjs @@ -0,0 +1,211 @@ +#!/usr/bin/env node +/** + * check-nav-coverage.mjs + * + * Verifies that every .mdx page in the shipped taxonomy is registered in the + * docs.json navigation tree, and that every docs.json navigation entry + * resolves to a real file. Run it after adding, renaming, or removing pages: + * + * node scripts/check-nav-coverage.mjs + * + * Wired into CI via the "Compile docs snippets" job in + * .github/workflows/snippets.yml, so a PR that adds an .mdx page without + * registering it in docs.json fails the build. + */ +import { readFile, readdir } from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; + +const repoRoot = process.cwd(); +const docsJsonPath = path.join(repoRoot, "docs.json"); + +/** Top-level directories whose pages ship in the navigation. */ +const shippedDirs = [ + "api-reference", + "architecture", + "concepts", + "contracts", + "guides", + "reference", + "sdk", +]; + +async function main() { + const docsJson = JSON.parse(stripComments(await readFile(docsJsonPath, "utf8"))); + const navEntries = collectNavEntries(docsJson.navigation); + + const pagePaths = await collectPagePaths(); + const missingFromNav = [...pagePaths] + .filter((page) => !navEntries.has(page)) + .sort(); + const missingOnDisk = [...navEntries] + .filter((entry) => !isExternal(entry) && !pagePaths.has(entry)) + .sort(); + + const failures = []; + if (missingFromNav.length > 0) { + failures.push( + [ + "Pages exist on disk but are missing from docs.json navigation:", + ...missingFromNav.map((page) => ` - ${page}`), + ].join("\n"), + ); + } + if (missingOnDisk.length > 0) { + failures.push( + [ + "docs.json navigation entries with no matching .mdx file on disk:", + ...missingOnDisk.map((entry) => ` - ${entry}`), + ].join("\n"), + ); + } + + if (failures.length > 0) { + console.error( + [ + "Nav coverage check failed.", + "", + ...failures, + "", + "Register new pages in their natural group in docs.json, and remove nav", + "entries that point at files that no longer exist.", + ].join("\n"), + ); + process.exit(1); + } + + console.log( + `Nav coverage passed: ${pagePaths.size} pages checked, ` + + `${navEntries.size} nav entries verified.`, + ); +} + +/** + * Collect every page path referenced anywhere in the navigation tree. + * Handles nested groups, per-locale variants (e.g. guides/foo.es), and + * object entries with a `page` field. External links are collected too and + * filtered out by the on-disk check. + */ +function collectNavEntries(navigation) { + const entries = new Set(); + const walk = (value, inPages) => { + if (typeof value === "string") { + if (inPages) entries.add(value); + return; + } + if (Array.isArray(value)) { + value.forEach((item) => walk(item, inPages)); + return; + } + if (value && typeof value === "object") { + if (typeof value.page === "string") entries.add(value.page); + for (const [key, child] of Object.entries(value)) { + walk(child, inPages || key === "pages"); + } + } + }; + walk(navigation, false); + return entries; +} + +/** Collect the nav path of every .mdx page in the shipped taxonomy. */ +async function collectPagePaths() { + const pages = new Set(); + + const walk = async (dir) => { + let entries; + try { + entries = await readdir(dir, { withFileTypes: true }); + } catch { + return; // Directory does not exist (e.g. concepts/). + } + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + await walk(fullPath); + } else if (entry.isFile() && entry.name.endsWith(".mdx")) { + pages.add(toNavPath(fullPath)); + } + } + }; + + for (const dir of shippedDirs) { + await walk(path.join(repoRoot, dir)); + } + + // Root-level pages live next to docs.json, outside the shipped dirs. + const rootEntries = await readdir(repoRoot, { withFileTypes: true }); + for (const entry of rootEntries) { + if (entry.isFile() && entry.name.endsWith(".mdx")) { + pages.add(entry.name.replace(/\.mdx$/, "")); + } + } + + return pages; +} + +/** Absolute path -> navigation path (relative, forward slashes, no .mdx). */ +function toNavPath(filePath) { + return path.relative(repoRoot, filePath).split(path.sep).join("/").replace(/\.mdx$/, ""); +} + +function isExternal(entry) { + return /^[a-z][a-z0-9+.-]*:\/\//i.test(entry) || entry.startsWith("//"); +} + +/** + * Strip line comments (//) and block comments (slash-star ... star-slash) + * from JSONC so docs.json can carry the audit-script comment header. + * Respects string literals so URLs like "https://..." are untouched. + */ +function stripComments(source) { + let result = ""; + let inString = false; + let i = 0; + + while (i < source.length) { + const char = source[i]; + const next = source[i + 1]; + + if (inString) { + result += char; + if (char === "\\" && next !== undefined) { + result += next; + i += 2; + continue; + } + if (char === '"') inString = false; + i += 1; + continue; + } + + if (char === '"') { + inString = true; + result += char; + i += 1; + continue; + } + + if (char === "/" && next === "/") { + while (i < source.length && source[i] !== "\n") i += 1; + continue; + } + + if (char === "/" && next === "*") { + i += 2; + while (i < source.length && !(source[i] === "*" && source[i + 1] === "/")) i += 1; + i += 2; + continue; + } + + result += char; + i += 1; + } + + return result; +} + +main().catch((error) => { + console.error(error); + process.exit(1); +});