diff --git a/docs.json b/docs.json index 2162368..e69de29 100644 --- a/docs.json +++ b/docs.json @@ -1,207 +0,0 @@ -{ - "$schema": "https://mintlify.com/docs.json", - "name": "Wraith Protocol", - "theme": "maple", - "logo": { - "dark": "/assets/images/logo-white.png", - "light": "/assets/images/logo-black.png" - }, - "favicon": "/assets/images/logo-black.png", - "colors": { - "primary": "#c6c6c7", - "light": "#e6e1e5", - "dark": "#767575" - }, - "search": { - "analytics": true, - "synonyms": [ - ["stealth", "unlinkable"], - ["meta-address", "metaaddress", "meta address"], - ["Soroban", "Stellar smart contract"], - ["friendbot", "testnet faucet"], - ["XLM", "lumen", "reflector"], - ["Freighter", "Stellar wallet"] - ], - "boosts": [ - { "page": "getting-started", "weight": 3 }, - { "group": "Guides", "weight": 2 }, - { "group": "API Reference", "weight": 1 } - ] - }, - "font": { - "headings": { - "family": "Space Grotesk", - "weight": 600 - }, - "body": { - "family": "Inter" - } - }, - "layout": "sidenav", - "sidebar": { - "items": "border" - }, - "topbar": { - "style": "default" - }, - "rounded": "none", - "topbarLinks": [ - { - "name": "GitHub", - "url": "https://github.com/wraith-protocol" - } - ], - "topbarCtaButton": { - "name": "Get API Key", - "url": "https://usewraith.xyz" - }, - "tabs": [ - { - "name": "SDK", - "url": "sdk" - }, - { - "name": "Guides", - "url": "guides" - }, - { - "name": "API Reference", - "url": "api-reference" - } - ], - "navigation": { - "tabs": [ - { - "tab": "Documentation", - "groups": [ - { - "group": "Overview", - "pages": ["introduction", "getting-started", "roadmap"] - }, - { - "group": "Architecture", - "pages": [ - "architecture/overview", - "architecture/announcement-format", - "architecture/chain-connectors", - "architecture/tee", - "architecture/stellar-cryptography", - "architecture/view-tag-scanner" - ] - }, - { - "group": "Contracts", - "pages": ["contracts/evm", "contracts/stellar", "contracts/solana", "contracts/ckb"] - }, - { - "group": "Reference", - "pages": [ - "reference/audits", - "reference/auditor-guide", - "reference/error-codes", - "reference/security-disclosure", - "reference/sep-compatibility", - "reference/stellar-networks", - "reference/threat-model" - ] - } - ] - }, - { - "tab": "SDK", - "groups": [ - { - "group": "SDK", - "pages": ["sdk/overview", "sdk/agent-client"] - }, - { - "group": "Chain Primitives", - "pages": ["sdk/chains/evm", "sdk/chains/stellar", "sdk/chains/solana", "sdk/chains/ckb"] - } - ] - }, - { - "tab": "Guides", - "groups": [ - { - "group": "Quickstarts", - "pages": [ - "guides/quickstarts/python", - "guides/quickstarts/rust" - ] - }, - { - "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/stellar-wallet-integration" - ] - }, - { - "group": "Integrations", - "pages": [ - "guides/integrations/aquarius", - "guides/integrations/blend", - "guides/integrations/fxdao", - "guides/integrations/nuxt", - "guides/integrations/phoenix", - "guides/integrations/react-native", - "guides/integrations/reflector", - "guides/integrations/soroswap" - ] - }, - { - "group": "Operations", - "pages": [ - "guides/stellar-mainnet-deployment", - "guides/stellar-payment-links", - "guides/stellar-multisig-withdrawal", - "guides/stellar/multisig-authority-rotation", - "guides/stellar-offline-signing", - "guides/stellar-tx-simulation", - "guides/stellar-explorer-recipes", - "guides/stellar-federation", - "guides/stellar-custom-assets", - "guides/stellar/stellar-liquidity-pool-swap", - "guides/stellar/stellar-path-payment", - "guides/stellar/sponsored-batch-send", - "guides/stellar/passkey-signing", - "guides/stellar/stellar-quickstart", - "guides/stellar/wraith-names-lifecycle", - "guides/stellar/subscriptions-with-wraith-names", - "guides/wraith-names-stellar", - "guides/ops/self-hosted-deployment", - "guides/ops/monitoring-and-on-call" - ] - } - ] - }, - { - "tab": "API Reference", - "groups": [ - { - "group": "API Reference", - "pages": [ - "api-reference/endpoints", - "api-reference/fetch-announcements-stream", - "api-reference/stealth-keys", - "api-reference/types" - ] - } - ] - } - ] - }, - "footerSocials": { - "github": "https://github.com/wraith-protocol" - } -} diff --git a/localization-qa.js b/localization-qa.js new file mode 100644 index 0000000..e662b1a --- /dev/null +++ b/localization-qa.js @@ -0,0 +1,458 @@ +/** + * Localization QA for Wraith Protocol documentation + * + * Checks translated documentation pages for: + * - Stale links + * - Missing sections + * - Code drift + * - Language metadata + */ + +const fs = require('fs'); +const path = require('path'); + +const DOCS_DIR = path.join(__dirname); +const MARKDOWN_EXT = ['.md', '.mdx']; + +const SKIP_DIRS = new Set(['.git', '.agents', 'node_modules']); +const SKIP_FILES = new Set(['docs.json', 'CLAUDE.md', 'PR_DESCRIPTION.md']); + +function parseFrontMatter(content) { + let attrs = {}; + let body = content; + + const fmMatch = content.match(/^---[\s\S]*?^---/m); + if (fmMatch) { + const fmContent = content.substring(3, fmMatch[0].lastIndexOf('---')).trim(); + const attrsObj = {}; + for (const line of fmContent.split('\n')) { + const match = line.match(/^(\w+):\s*(.*)$/); + if (match) { + let value = match[2].trim(); + if (value === 'true') value = true; + else if (value === 'false') value = false; + else if (!isNaN(value)) value = Number(value); + attrsObj[match[1]] = value; + } + } + attrs = attrsObj; + + const fmEndIndex = content.indexOf('---', 3); + if (fmEndIndex !== -1) { + body = content.substring(fmEndIndex + 3).trim(); + } + } + + return { attrs, body }; +} + +function extractHeadings(body) { + const headings = []; + const lines = body.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const match = line.match(/^(#{1,6})\s+(.*)$/); + if (match) { + headings.push({ + level: match[1].length, + text: match[2].trim(), + line: i + 1, + }); + } + } + return headings; +} + +function countCodeFences(body) { + const lines = body.split('\n'); + let count = 0; + let inFence = false; + for (const line of lines) { + if (line.trim().startsWith('```')) { + inFence = !inFence; + count++; + } + } + return count; +} + +function extractLinks(body) { + const links = []; + const regex = /\[([^\]]+)\]\(([^)]+)\)/g; + let match; + while ((match = regex.exec(body)) !== null) { + links.push({ + text: match[1], + url: match[2], + line: match.index, + }); + } + return links; +} + +function classifyLink(url) { + if (!url) return 'external'; + if (url.startsWith('/') || url.startsWith('#')) return 'internal'; + if (url.startsWith('http://') || url.startsWith('https://')) return 'external'; + return 'internal'; +} + +function normalizePagePath(page) { + if (!page) return ''; + let p = page.replace(/^\/|\/$/g, ''); + return p.toLowerCase(); +} + +function getNavPages(navigation) { + const pages = []; + + if (!navigation || !navigation.tabs || !Array.isArray(navigation.tabs)) return pages; + + for (const tab of navigation.tabs) { + const groups = tab.groups; + if (!groups || !Array.isArray(groups)) continue; + + for (const group of groups) { + const groupPages = group.pages; + if (groupPages && Array.isArray(groupPages)) { + for (const page of groupPages) { + pages.push(normalizePagePath(page)); + } + } + if (group.items && Array.isArray(group.items)) { + for (const subItem of group.items) { + const subPages = subItem.pages; + if (subPages && Array.isArray(subPages)) { + for (const page of subPages) { + pages.push(normalizePagePath(page)); + } + } + const subGroups = subItem.groups; + if (subGroups && Array.isArray(subGroups)) { + for (const subGroup of subGroups) { + const sgPages = subGroup.pages; + if (sgPages && Array.isArray(sgPages)) { + for (const page of sgPages) { + pages.push(normalizePagePath(page)); + } + } + } + } + } + } + } + } + + return pages; +} + +function getAllMDXFiles() { + const files = []; + + function walk(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + if (SKIP_DIRS.has(entry.name)) continue; + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(fullPath); + } else if (MARKDOWN_EXT.some(ext => entry.name.endsWith(ext))) { + // Skip non-doc files + const basename = entry.name.replace(path.extname(entry.name), ''); + if (SKIP_FILES.has(basename)) continue; + files.push(fullPath); + } + } + } + + walk(DOCS_DIR); + return files; +} + +function getPageKey(filePath) { + const rel = path.relative(DOCS_DIR, filePath); + const withoutExt = path.extname(rel) ? rel.replace(path.extname(rel), '') : rel; + return withoutExt.replace(/\\/g, '/'); +} + +function hasLocaleMetadata(attrs) { + return attrs.locale !== undefined && attrs.locale !== null; +} + +function runQA() { + console.log('=== Wraith Protocol Localization QA ===\n'); + + // 1. Read docs.json + const docsJSONPath = path.join(DOCS_DIR, 'docs.json'); + let navigation; + if (fs.existsSync(docsJSONPath)) { + const jsonContent = fs.readFileSync(docsJSONPath, 'utf-8'); + const docsConfig = JSON.parse(jsonContent); + navigation = docsConfig.navigation; + console.log('Loaded navigation from docs.json\n'); + } else { + console.warn('docs.json not found\n'); + navigation = null; + } + + // Get canonical page paths from navigation + const canonicalPages = navigation ? getNavPages(navigation) : []; + console.log(`Canonical pages from navigation: ${canonicalPages.length}\n`); + console.log(' ', canonicalPages.join(', '), '\n'); + + // 2. Get all markdown files + const allFiles = getAllMDXFiles(); + console.log(`Total markdown files found: ${allFiles.length}\n`); + + // 3. Parse all files + const fileData = []; + + for (const filePath of allFiles) { + try { + const content = fs.readFileSync(filePath, 'utf-8'); + const parsed = parseFrontMatter(content); + const pageKey = getPageKey(filePath); + const hasLocale = hasLocaleMetadata(parsed.attrs); + const locale = hasLocale ? parsed.attrs.locale : null; + + const headings = extractHeadings(parsed.body); + const codeFenceCount = countCodeFences(parsed.body); + const links = extractLinks(parsed.body); + const classifiedLinks = links.map(l => ({ + ...l, + type: classifyLink(l.url), + })); + + fileData.push({ + filePath, + pageKey, + title: parsed.attrs.title || 'Untitled', + description: parsed.attrs.description || '', + locale, + hasLocaleMetadata: hasLocale, + headings, + codeFenceCount, + links: classifiedLinks, + rawBody: parsed.body, + }); + } catch (err) { + console.error(`Error parsing ${filePath}:`, err.message); + } + } + + // Build map of page keys + const pageMap = new Map(); + for (const fd of fileData) { + pageMap.set(fd.pageKey, fd); + } + + // 4. Identify source vs translated pages + const translatedPages = new Set(); + const sourceLanguagePages = new Set(); + + for (const [key, fd] of pageMap) { + if (fd.hasLocaleMetadata) { + translatedPages.add(key); + } else { + sourceLanguagePages.add(key); + } + } + + console.log(`Source language (English) pages: ${sourceLanguagePages.size}`); + console.log(`Translated pages: ${translatedPages.size}\n`); + + // 5. Define supported translated pages (from navigation) + const supportedTranslated = new Set(); + if (navigation) { + for (const pageKey of canonicalPages) { + if (!pageMap.has(pageKey)) { + console.log(` Warning: Page in navigation but not found in docs: ${pageKey}`); + } else { + supportedTranslated.add(pageKey); + } + } + } + + console.log(`Supported pages for translation: ${supportedTranslated.size}\n`); + + // 6. Compare structure between source and translated pages + const issues = []; + + for (const pageKey of supportedTranslated) { + const fd = pageMap.get(pageKey); + if (!fd) { + issues.push({ + type: 'missing_page', + page: pageKey, + message: 'Page defined in navigation but not found in docs directory', + }); + continue; + } + + // Check if translated + if (!fd.hasLocaleMetadata) { + issues.push({ + type: 'untranslated', + page: pageKey, + message: 'Page is missing locale metadata - not marked as translated', + }); + } + + // Compare with source version (same key in English) + const sourceFd = pageMap.get(pageKey); + + if (sourceFd) { + // Compare headings + const sourceHeadingTexts = sourceFd.headings.map(h => h.text); + const translatedHeadingTexts = fd.headings.map(h => h.text); + + const missingInTranslation = sourceHeadingTexts.filter(h => !translatedHeadingTexts.includes(h)); + const extraInTranslation = translatedHeadingTexts.filter(h => !sourceHeadingTexts.includes(h)); + + if (missingInTranslation.length > 0) { + issues.push({ + type: 'heading_drift', + page: pageKey, + message: `Missing headings in translation: ${missingInTranslation.join(', ')}`, + details: { + sourceHeadings: sourceHeadingTexts, + translatedHeadings: translatedHeadingTexts, + }, + }); + } + + if (extraInTranslation.length > 0) { + issues.push({ + type: 'heading_drift', + page: pageKey, + message: `Extra headings in translation not in source: ${extraInTranslation.join(', ')}`, + details: { + sourceHeadings: sourceHeadingTexts, + translatedHeadings: translatedHeadingTexts, + }, + }); + } + + // Compare code fence counts + if (sourceFd.codeFenceCount !== fd.codeFenceCount) { + issues.push({ + type: 'code_drift', + page: pageKey, + message: `Code fence count mismatch: source=${sourceFd.codeFenceCount}, translation=${fd.codeFenceCount}`, + details: { + sourceFenceCount: sourceFd.codeFenceCount, + translatedFenceCount: fd.codeFenceCount, + }, + }); + } + + // Compare link structure + const sourceLinks = sourceFd.links.map(l => ({ ...l, type: classifyLink(l.url) })); + const translatedLinks = fd.links.map(l => ({ ...l, type: classifyLink(l.url) })); + + // Links in translation not in source + for (const translatedLink of translatedLinks) { + const matchingSourceLink = sourceLinks.find( + sl => sl.url === translatedLink.url && sl.text === translatedLink.text + ); + + if (!matchingSourceLink) { + if (translatedLink.type === 'external') { + issues.push({ + type: 'link_structural', + page: pageKey, + message: `Link in translation not in source: ${translatedLink.text} (${translatedLink.url})`, + details: { type: translatedLink.type }, + }); + } else { + issues.push({ + type: 'link_structural', + page: pageKey, + message: `Internal link in translation not in source: ${translatedLink.text} (${translatedLink.url})`, + details: { type: translatedLink.type }, + }); + } + } + } + + // Links in source missing from translation + for (const sourceLink of sourceLinks) { + const hasMatching = translatedLinks.some( + tl => tl.url === sourceLink.url && tl.text === sourceLink.text + ); + if (!hasMatching && sourceLink.type === 'internal') { + issues.push({ + type: 'link_missing', + page: pageKey, + message: `Internal link missing from translation: ${sourceLink.text} (${sourceLink.url})`, + }); + } + } + } + } + + // 7. Check locale metadata + console.log('\n=== Locale Metadata Check ==='); + for (const [key, fd] of pageMap) { + if (fd.hasLocaleMetadata) { + console.log(` ✓ ${key}: locale="${fd.locale}"`); + } else { + console.log(` ✗ ${key}: missing locale metadata`); + } + } + +// 8. Check canonical links +console.log('\n=== Canonical Links Check ==='); + for (const fd of fileData) { + // Check for canonical link by looking for tag with rel=canonical + const hasCanonicalLink = /"], + "topics": [ + ["announce"], + ["1"], + ["*"], + ["*"] + ] + } + ], + "pagination": { "limit": 100 } +} +``` + +### 2. Filter by `view_tag_bucket` (e.g., Bucket 42) +This is the recommended query for clients looking for their own transactions. + +```json +{ + "startLedger": 123456, + "filters": [ + { + "type": "contract", + "contractIds": [""], + "topics": [ + ["announce"], + ["1"], + ["42"], + ["0"] + ] + } + ], + "pagination": { "limit": 100 } +} +``` + +## Migration & Indexer Recommendations + +The transition from v1 to v2 involves a new deployment of the `stealth-announcer` contract. + +- **Migration Timing**: v1 events remain readable and will not be deleted. v2 is a strictly new deployment with a new contract ID. +- **Indexer Recommendations**: During the transition period, indexers and wallets *must* listen to both the v1 and v2 contract IDs to ensure no announcements are missed. + +You can query both simultaneously by including both contract IDs in your `getEvents` filter, or by executing parallel queries for the different topic structures.