Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/snippets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 22 additions & 8 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
"group": "Architecture",
"pages": [
"architecture/overview",
"architecture/announcement-format",
"architecture/chain-connectors",
"architecture/tee",
"architecture/stellar-cryptography",
Expand All @@ -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"
]
}
]
},
Expand All @@ -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"
]
},
{
Expand All @@ -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"]
}
]
},
Expand All @@ -164,6 +177,7 @@
"pages": [
"api-reference/endpoints",
"api-reference/fetch-announcements-stream",
"api-reference/stealth-keys",
"api-reference/types"
]
}
Expand Down
28 changes: 25 additions & 3 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
211 changes: 211 additions & 0 deletions scripts/check-nav-coverage.mjs
Original file line number Diff line number Diff line change
@@ -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);
});
Loading