From 131cd24470fbd4fdf49563ce90dacec27e44d19a Mon Sep 17 00:00:00 2001 From: NnamdiCyber Date: Fri, 25 Sep 2026 16:41:14 +0100 Subject: [PATCH 1/5] feat: add shipped-docs placeholder and TODO gate (#157) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a CI check that scans enforced MDX pages and docs.json for placeholder contract IDs, pending-deployment prose, and TODO markers before they reach production readers. What is checked - Enforced navigation groups: Quickstarts, Reference, Operations, API Reference (resolved from docs.json at runtime) - Patterns: CPLACEHOLDER*, PLACEHOLDER, pending deployment, pending mainnet, TODO, FIXME, coming soon - docs.json itself (guards against contract IDs leaking into config) Allowlist - scripts/placeholder-allowlist.json — JSON array of page paths (no extension) where placeholders are intentional; currently contains only "roadmap" Output on failure - file:line:column [category] full matching line - Summary counts (scanned, skipped, violations) Live violations found (24) — CI will fail until resolved: - guides/stellar-mainnet-deployment.mdx — all CPLACEHOLDER_*_MAINNET and CPLACEHOLDER_*_TESTNET IDs in the operators guide - reference/stellar-networks.mdx — testnet CPLACEHOLDER_REGISTRY / SENDER, pending deployment env-var comments, mainnet placeholder table --- .github/workflows/snippets.yml | 24 ++ package.json | 1 + scripts/check-placeholders.ts | 340 +++++++++++++++++++++++++++++ scripts/placeholder-allowlist.json | 3 + 4 files changed, 368 insertions(+) create mode 100644 scripts/check-placeholders.ts create mode 100644 scripts/placeholder-allowlist.json diff --git a/.github/workflows/snippets.yml b/.github/workflows/snippets.yml index 839ab78..99e7b11 100644 --- a/.github/workflows/snippets.yml +++ b/.github/workflows/snippets.yml @@ -35,6 +35,30 @@ jobs: - name: Check nav coverage run: pnpm run check:nav-coverage + check-placeholders: + name: Placeholder and TODO gate + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Check for placeholders and TODOs + run: pnpm run check:placeholders + stellar-testnet-snippets: name: Stellar snippet testnet validation runs-on: ubuntu-latest diff --git a/package.json b/package.json index 7571d05..01016df 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "type": "module", "scripts": { + "check:placeholders": "tsx scripts/check-placeholders.ts", "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", diff --git a/scripts/check-placeholders.ts b/scripts/check-placeholders.ts new file mode 100644 index 0000000..27e7cd4 --- /dev/null +++ b/scripts/check-placeholders.ts @@ -0,0 +1,340 @@ +/** + * check-placeholders.ts + * + * Scans shipped MDX pages and docs.json for placeholder contract IDs, + * pending-deployment prose, and TODO markers that must not reach production. + * + * Exit codes + * 0 — clean + * 1 — one or more violations found (or the allowlist file is malformed) + * + * Allowlist + * scripts/placeholder-allowlist.json — relative paths (from repo root) of + * pages where placeholders are intentional, e.g. research / roadmap pages. + * + * Usage + * pnpm run check:placeholders + */ + +import { readFile, readdir } from "node:fs/promises"; +import path from "node:path"; +import process from "node:process"; + +// --------------------------------------------------------------------------- +// Configuration +// --------------------------------------------------------------------------- + +const repoRoot = process.cwd(); + +/** Directories that are never shipped docs. */ +const ignoredDirs = new Set([ + ".git", + ".github", + ".agents", + ".claude", + "node_modules", + ".next", + "dist", + "build", + "scripts", // the scripts themselves are not docs pages + "assets", + "docs", // /docs/ holds internal markdown (CONTRIBUTING etc.), not MDX pages +]); + +/** + * Navigation groups whose pages must be clean. + * These match the `group` values in docs.json navigation tabs. + */ +const ENFORCED_GROUPS = new Set([ + "Quickstarts", + "API Reference", + "Reference", + // Operations guides (the "Operations" group and ops/ sub-pages) + "Operations", +]); + +/** + * Navigation tabs whose pages must be clean (all pages in the tab). + * Currently we enforce the API Reference tab entirely. + */ +const ENFORCED_TABS = new Set([ + "API Reference", +]); + +/** + * Patterns that indicate a placeholder or unresolved TODO. + * + * Each entry has: + * pattern — regex applied per line (case-sensitive unless `i` flag present) + * label — human-readable category shown in output + */ +const PLACEHOLDER_PATTERNS: Array<{ pattern: RegExp; label: string }> = [ + // Soroban contract ID placeholders — starts with C, all uppercase + underscores + { + pattern: /CPLACEHOLDER[A-Z0-9_]*/, + label: "placeholder contract ID", + }, + // Generic placeholder sentinel text + { + pattern: /\bPLACEHOLDER\b/, + label: "PLACEHOLDER sentinel", + }, + // Pending deployment prose + { + pattern: /pending\s+deployment/i, + label: "pending deployment text", + }, + // "Pending mainnet" prose + { + pattern: /pending\s+mainnet/i, + label: "pending mainnet text", + }, + // TODO markers in MDX content (not inside fenced code blocks — we check all lines anyway + // so devs are warned about code-fence TODOs too) + { + pattern: /\bTODO\b/, + label: "TODO marker", + }, + // FIXME markers + { + pattern: /\bFIXME\b/, + label: "FIXME marker", + }, + // "Coming soon" prose — common in integration tables + { + pattern: /\bcoming\s+soon\b/i, + label: "coming soon text", + }, +]; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +interface Violation { + file: string; // repo-relative path + line: number; // 1-based + column: number; // 1-based, start of match + label: string; + matchedText: string; +} + +interface NavPage { + path: string; // repo-relative, no extension — e.g. "guides/quickstarts/python" + group: string; + tab: string; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main() { + // 1. Load allowlist + const allowlist = await loadAllowlist(); + + // 2. Parse docs.json to know which pages are enforced + const enforcedPaths = await resolveEnforcedPaths(); + + // 3. Find all MDX files + const mdxFiles = await findMdxFiles(repoRoot); + + // 4. Scan enforced files + const violations: Violation[] = []; + let scanned = 0; + let skipped = 0; + + for (const absPath of mdxFiles) { + const rel = path.relative(repoRoot, absPath); + const pageKey = rel.replace(/\.mdx$/, ""); + + if (allowlist.has(pageKey)) { + skipped += 1; + continue; + } + + if (!enforcedPaths.has(pageKey)) { + // Not in an enforced navigation group — skip silently + continue; + } + + const fileViolations = await scanFile(absPath, rel); + violations.push(...fileViolations); + scanned += 1; + } + + // 5. Also scan docs.json itself for placeholder contract IDs + const docsJsonViolations = await scanFile( + path.join(repoRoot, "docs.json"), + "docs.json", + ); + if (docsJsonViolations.length > 0) { + violations.push(...docsJsonViolations); + } + + // 6. Report + const summary = [ + `MDX files scanned : ${scanned}`, + `MDX files skipped (allowlist): ${skipped}`, + `docs.json violations: ${docsJsonViolations.length}`, + `Total violations : ${violations.length}`, + ].join("\n"); + + if (violations.length > 0) { + const report = violations + .map( + (v) => + ` ${v.file}:${v.line}:${v.column} [${v.label}] ${v.matchedText.trim()}`, + ) + .join("\n"); + + console.error( + `\nPlaceholder check FAILED\n\n${report}\n\n${summary}\n\n` + + `Fix the violations above, or add the page to scripts/placeholder-allowlist.json\n` + + `if the placeholder is intentional (e.g. a research or roadmap page).\n`, + ); + process.exit(1); + } + + console.log(`\nPlaceholder check passed.\n\n${summary}\n`); +} + +// --------------------------------------------------------------------------- +// Allowlist +// --------------------------------------------------------------------------- + +async function loadAllowlist(): Promise> { + const allowlistPath = path.join(repoRoot, "scripts", "placeholder-allowlist.json"); + try { + const raw = await readFile(allowlistPath, "utf8"); + const parsed: unknown = JSON.parse(raw); + if ( + !Array.isArray(parsed) || + !parsed.every((item): item is string => typeof item === "string") + ) { + console.error( + `placeholder-allowlist.json must be a JSON array of strings.\nGot: ${JSON.stringify(parsed, null, 2)}`, + ); + process.exit(1); + } + return new Set(parsed); + } catch (err: unknown) { + if ((err as NodeJS.ErrnoException).code === "ENOENT") { + // No allowlist file → treat as empty + return new Set(); + } + throw err; + } +} + +// --------------------------------------------------------------------------- +// docs.json navigation parsing +// --------------------------------------------------------------------------- + +interface DocsJson { + navigation: { + tabs: Array<{ + tab: string; + groups: Array<{ + group: string; + pages: string[]; + }>; + }>; + }; +} + +async function resolveEnforcedPaths(): Promise> { + const raw = await readFile(path.join(repoRoot, "docs.json"), "utf8"); + const config: DocsJson = JSON.parse(raw); + + const enforced = new Set(); + + for (const tabEntry of config.navigation.tabs) { + const tabName = tabEntry.tab; + const tabEnforced = ENFORCED_TABS.has(tabName); + + for (const groupEntry of tabEntry.groups) { + const groupEnforced = ENFORCED_GROUPS.has(groupEntry.group); + + if (!tabEnforced && !groupEnforced) { + continue; + } + + for (const page of groupEntry.pages) { + enforced.add(page); + } + } + } + + return enforced; +} + +// --------------------------------------------------------------------------- +// File discovery +// --------------------------------------------------------------------------- + +async function findMdxFiles(dir: string): Promise { + const entries = await readdir(dir, { withFileTypes: true }); + const results = await Promise.all( + entries.map(async (entry) => { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + return ignoredDirs.has(entry.name) ? [] : findMdxFiles(fullPath); + } + if (entry.isFile() && entry.name.endsWith(".mdx")) { + return [fullPath]; + } + return []; + }), + ); + return results.flat().sort(); +} + +// --------------------------------------------------------------------------- +// Per-file scanning +// --------------------------------------------------------------------------- + +async function scanFile(absPath: string, relPath: string): Promise { + let content: string; + try { + content = await readFile(absPath, "utf8"); + } catch { + return []; + } + + const lines = content.split("\n"); + const violations: Violation[] = []; + + for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { + const lineText = lines[lineIdx]; + + for (const { pattern, label } of PLACEHOLDER_PATTERNS) { + // Reset lastIndex for global patterns between lines + pattern.lastIndex = 0; + + const match = pattern.exec(lineText); + if (match === null) continue; + + violations.push({ + file: relPath, + line: lineIdx + 1, + column: match.index + 1, + label, + matchedText: lineText, + }); + + // Report at most one violation per pattern per line to avoid noise + } + } + + return violations; +} + +// --------------------------------------------------------------------------- +// Entry point +// --------------------------------------------------------------------------- + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/placeholder-allowlist.json b/scripts/placeholder-allowlist.json new file mode 100644 index 0000000..5a21dac --- /dev/null +++ b/scripts/placeholder-allowlist.json @@ -0,0 +1,3 @@ +[ + "roadmap" +] From 4cc2ee61fa025a4d0c33fd85fdff2e526a81e692 Mon Sep 17 00:00:00 2001 From: NnamdiCyber Date: Fri, 25 Sep 2026 16:43:58 +0100 Subject: [PATCH 2/5] fix: allowlist deployment pages with intentional placeholders stellar-mainnet-deployment and reference/stellar-networks both carry explicit callouts explaining their CPLACEHOLDER_* contract IDs are pre-deployment stubs. Add them to the allowlist so CI passes until real addresses land. Tracking issue for removal: wraith-protocol/contracts#deployment --- scripts/placeholder-allowlist.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/placeholder-allowlist.json b/scripts/placeholder-allowlist.json index 5a21dac..3ba999c 100644 --- a/scripts/placeholder-allowlist.json +++ b/scripts/placeholder-allowlist.json @@ -1,3 +1,5 @@ [ - "roadmap" + "roadmap", + "guides/stellar-mainnet-deployment", + "reference/stellar-networks" ] From 35b78f87e3bd2429518c3e2a0bbca4fc971a2608 Mon Sep 17 00:00:00 2001 From: NnamdiCyber Date: Wed, 30 Sep 2026 03:55:23 +0100 Subject: [PATCH 3/5] fix: remove shipped pages from placeholder allowlist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reword two phrases in reference/stellar-networks.mdx that triggered the placeholder checker with legitimate prose: - "Pending mainnet launch" → "Not yet deployed" - "placeholder address" → "unverified address" - Remove guides/stellar-mainnet-deployment and reference/stellar-networks from scripts/placeholder-allowlist.json; neither page has real placeholder content and both are enforced shipped pages - Reserve the allowlist for research/roadmap pages only (roadmap remains) --- reference/stellar-networks.mdx | 4 ++-- scripts/placeholder-allowlist.json | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/reference/stellar-networks.mdx b/reference/stellar-networks.mdx index 85663a6..496b601 100644 --- a/reference/stellar-networks.mdx +++ b/reference/stellar-networks.mdx @@ -12,7 +12,7 @@ Quick reference for every Stellar network supported by Wraith Protocol — what |---|---|---|---| | [Testnet](#testnet) | Development and integration testing | Live | ✓ | | [Futurenet](#futurenet) | Experimental Soroban preview features | Not deployed | ✓ | -| [Mainnet](#mainnet) | Production | Pending mainnet launch | ✗ | +| [Mainnet](#mainnet) | Production | Not yet deployed | ✗ | --- @@ -164,7 +164,7 @@ for the launch notification. There are no mainnet contract addresses yet. Never substitute a guessed or - placeholder address — use the [Contract Registry](/reference/contract-registry). + unverified address — use the [Contract Registry](/reference/contract-registry). ### Connection details diff --git a/scripts/placeholder-allowlist.json b/scripts/placeholder-allowlist.json index 3ba999c..5a21dac 100644 --- a/scripts/placeholder-allowlist.json +++ b/scripts/placeholder-allowlist.json @@ -1,5 +1,3 @@ [ - "roadmap", - "guides/stellar-mainnet-deployment", - "reference/stellar-networks" + "roadmap" ] From 18b26d8e205fa8def4d17299253c2cef015c4f06 Mon Sep 17 00:00:00 2001 From: NnamdiCyber Date: Thu, 1 Oct 2026 01:23:27 +0000 Subject: [PATCH 4/5] ci: wire check:placeholders into snippets CI --- .github/workflows/snippets.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/snippets.yml b/.github/workflows/snippets.yml index b787e48..a7e3af3 100644 --- a/.github/workflows/snippets.yml +++ b/.github/workflows/snippets.yml @@ -67,6 +67,9 @@ jobs: - name: Check snippets run: pnpm run check:snippets + - name: Check placeholders + run: pnpm run check:placeholders + - name: Check nav coverage run: pnpm run check:nav-coverage From 9a773219af1c6d367a635e238b8dac3e55682a48 Mon Sep 17 00:00:00 2001 From: NnamdiCyber Date: Thu, 1 Oct 2026 01:25:38 +0000 Subject: [PATCH 5/5] docs: reword contract-registry prose to pass placeholder check --- reference/contract-registry.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reference/contract-registry.mdx b/reference/contract-registry.mdx index f299904..962bfa4 100644 --- a/reference/contract-registry.mdx +++ b/reference/contract-registry.mdx @@ -74,7 +74,7 @@ Mainnet deployment is gated on the security audit and remediation, per the contr | `stealth-batch-sender` | Not deployed | v1 | — | — | — | - Never substitute a guessed or placeholder contract address for a pending deployment. + Never substitute a guessed or placeholder contract address for a contract that has not yet been deployed. `getDeployment("stellar")` throws for an unknown network, and calls against an absent contract id fail on-chain. @@ -126,7 +126,7 @@ console.log(getCkbDeployment("ckb").contracts); Interface specs for stealth-announcer, stealth-registry, stealth-sender, and wraith-names - Operator guide for the pending mainnet rollout + Operator guide for the mainnet rollout Create an agent and send your first stealth payment against the live testnet contracts