Fixture
\n
\n 🔗\n tools/scripts/validators/content/check-mdx-safe-markdown.js | Validates first-party markdown and MDX content for repo-wide MDX-safe syntax, including parse failures and deterministic unsafe patterns. | node tools/scripts/validators/content/check-mdx-safe-markdown.js [--staged|--files a,b] [--json] | docs |
| tools/scripts/validators/content/check-page-endings.js | Validates that English v2 MDX pages end with an approved navigational or closing element | node tools/scripts/validators/content/check-page-endings.js [--fix] [--json] | docs |
| tools/scripts/validators/content/check-proper-nouns.js | Detects and fixes incorrect proper noun capitalisation in prose while skipping code, frontmatter, URLs, and path-like tokens. | node tools/scripts/validators/content/check-proper-nouns.js [--file <path[,path...]>] [--fix] | docs |
+| tools/scripts/validators/governance/audit-script-inventory.js | Deep inventory audit of every script in the repo. Traces triggers, outputs, downstream chains, and governance compliance. Produces reports grouped by trigger category. | node tools/scripts/validators/governance/audit-script-inventory.js [--json] [--md] [--output <dir>] [--verbose] | docs |
| tools/scripts/validators/governance/check-agent-docs-freshness.js | Validates that required agent governance docs exist and have been touched within a freshness threshold | node tools/scripts/validators/governance/check-agent-docs-freshness.js [--threshold <days>] [--json] | docs |
| tools/scripts/verify-all-pages.js | Utility script for tools/scripts/verify-all-pages.js. | node tools/scripts/verify-all-pages.js | docs |
| tools/scripts/verify-pay-orc-gate-finalize.sh | Payment/orchestrator gate verifier — checks payment and orchestrator documentation gate conditions | bash tools/scripts/verify-pay-orc-gate-finalize.sh [flags] | docs |
| tools/scripts/verify/.verify-large-change.sh | Large change verifier — blocks or warns when a commit touches an unusually large number of files | bash tools/scripts/verify/.verify-large-change.sh [flags] | docs |
| tools/scripts/wcag-repair-common.js | WCAG repair shared logic — common repair functions used by WCAG audit fix mode | node tools/scripts/wcag-repair-common.js [flags] | docs |
+
+## tools/lib
+
+| Script | Summary | Usage | Owner |
+|---|---|---|---|
+| tools/lib/component-governance-utils.js | Shared parsing and validation utilities for component governance scripts. | const utils = require('../lib/component-governance-utils'); | docs |
+| tools/lib/docs-index-utils.js | Shared utilities for docs-index.json generation — path resolution, frontmatter extraction, index merging | node tools/lib/docs-index-utils.js [flags] | docs |
+| tools/lib/docs-usefulness/config-validator.js | Validates docs-usefulness config structure and field completeness. | const { validateConfig } = require('../lib/docs-usefulness/config-validator'); | docs |
+| tools/lib/docs-usefulness/journey-check.js | Evaluates docs pages against user journey completeness criteria. | const { checkJourney } = require('../lib/docs-usefulness/journey-check'); | docs |
+| tools/lib/docs-usefulness/llm-evaluator.js | Wraps LLM API calls for rubric-based page quality evaluation. | const { evaluate } = require('../lib/docs-usefulness/llm-evaluator'); | docs |
+| tools/lib/docs-usefulness/prompts/changelog.js | LLM prompt template for changelog page-type usefulness evaluation. | const { getPrompt } = require('../lib/docs-usefulness/prompts/changelog'); | docs |
+| tools/lib/docs-usefulness/prompts/concept.js | LLM prompt template for concept page-type usefulness evaluation. | const { getPrompt } = require('../lib/docs-usefulness/prompts/concept'); | docs |
+| tools/lib/docs-usefulness/prompts/faq.js | LLM prompt template for faq page-type usefulness evaluation. | const { getPrompt } = require('../lib/docs-usefulness/prompts/faq'); | docs |
+| tools/lib/docs-usefulness/prompts/glossary.js | LLM prompt template for glossary page-type usefulness evaluation. | const { getPrompt } = require('../lib/docs-usefulness/prompts/glossary'); | docs |
+| tools/lib/docs-usefulness/prompts/how_to.js | LLM prompt template for how_to page-type usefulness evaluation. | const { getPrompt } = require('../lib/docs-usefulness/prompts/how_to'); | docs |
+| tools/lib/docs-usefulness/prompts/index.js | LLM prompt template for index page-type usefulness evaluation. | const { getPrompt } = require('../lib/docs-usefulness/prompts/index'); | docs |
+| tools/lib/docs-usefulness/prompts/landing.js | LLM prompt template for landing page-type usefulness evaluation. | const { getPrompt } = require('../lib/docs-usefulness/prompts/landing'); | docs |
+| tools/lib/docs-usefulness/prompts/overview.js | LLM prompt template for overview page-type usefulness evaluation. | const { getPrompt } = require('../lib/docs-usefulness/prompts/overview'); | docs |
+| tools/lib/docs-usefulness/prompts/reference.js | LLM prompt template for reference page-type usefulness evaluation. | const { getPrompt } = require('../lib/docs-usefulness/prompts/reference'); | docs |
+| tools/lib/docs-usefulness/prompts/troubleshooting.js | LLM prompt template for troubleshooting page-type usefulness evaluation. | const { getPrompt } = require('../lib/docs-usefulness/prompts/troubleshooting'); | docs |
+| tools/lib/docs-usefulness/prompts/tutorial.js | LLM prompt template for tutorial page-type usefulness evaluation. | const { getPrompt } = require('../lib/docs-usefulness/prompts/tutorial'); | docs |
+| tools/lib/docs-usefulness/quality-gate.js | Applies pass/fail thresholds to usefulness scores. | const { applyGate } = require('../lib/docs-usefulness/quality-gate'); | docs |
+| tools/lib/docs-usefulness/rubric-loader.js | Loads and parses rubric YAML/JSON for page-type scoring rules. | const { loadRubric } = require('../lib/docs-usefulness/rubric-loader'); | docs |
+| tools/lib/docs-usefulness/rule-evaluators.js | Evaluates individual rubric rules against page content. | const { evaluateRule } = require('../lib/docs-usefulness/rule-evaluators'); | docs |
+| tools/lib/docs-usefulness/scoring.js | Aggregates rule scores into a final usefulness score per page. | const { score } = require('../lib/docs-usefulness/scoring'); | docs |
+| tools/lib/generated-file-banners.js | Generated file banner template — provides standard banner text for auto-generated files | node tools/lib/generated-file-banners.js [flags] | docs |
+| tools/lib/mdx-safe-markdown.js | Shared MDX-safe markdown helpers that collect first-party markdown files, detect unsafe patterns, and apply deterministic repairs. | node tools/lib/mdx-safe-markdown.js [flags] | docs |
+| tools/lib/script-governance-config.js | Shared governance constants for script discovery, indexing, classification, and pipeline normalisation across the repo. | const config = require('../lib/script-governance-config'); | docs |
+
+## tools/notion
+
+| Script | Summary | Usage | Owner |
+|---|---|---|---|
+| tools/notion/1-read-notion-to-csv.js | Reads the Notion pages database, filters v2 rows, and writes CSV/JSON exports for downstream sync steps. | node tools/notion/1-read-notion-to-csv.js [flags] | docs |
+| tools/notion/2-read-docs-to-csv.js | Parses docs.json v2 navigation and writes CSV/JSON exports with section-group metadata for Notion sync. | node tools/notion/2-read-docs-to-csv.js [flags] | docs |
+| tools/notion/3-check-duplicates.js | Analyzes the exported Notion snapshot for duplicate page keys and writes JSON and Markdown reports. | node tools/notion/3-check-duplicates.js [flags] | docs |
+| tools/notion/4-remove-duplicates.js | Archives duplicate Notion pages from the duplicate report while keeping the first record in each group. | node tools/notion/4-remove-duplicates.js [flags] | docs |
+| tools/notion/5-export-to-notion.js | Updates existing Notion page grouping fields from the exported docs navigation snapshot. | node tools/notion/5-export-to-notion.js [flags] | docs |
+| tools/notion/backup-notion-table.js | Backs up the current Notion data source rows and metadata into timestamped JSON and CSV artifacts with a manifest. | node tools/notion/backup-notion-table.js [flags] | docs |
+| tools/notion/install-local-sync-hook.sh | Installs the managed local post-commit hook that invokes the Notion sync runner and preserves any prior hook as a backup. | bash tools/notion/install-local-sync-hook.sh [flags] | docs |
+| tools/notion/local-post-commit-sync.sh | Detects docs.json or v2 content changes in the latest commit and runs the canonical Notion sync locally when enabled. | bash tools/notion/local-post-commit-sync.sh [flags] | docs |
+| tools/notion/remove-local-sync-hook.sh | Removes the managed local Notion post-commit hook when it is present. | bash tools/notion/remove-local-sync-hook.sh [flags] | docs |
+| tools/notion/sync-v2-en-canonical.js | Builds canonical v2 English page metadata and syncs Notion schema, row metadata, and optional page-body blocks to match docs. | node tools/notion/sync-v2-en-canonical.js [flags] | docs |
+
+## tools/config
+
+| Script | Summary | Usage | Owner |
+|---|---|---|---|
+| tools/config/v2-internal-report-pages.js | Configuration data — list of internal report page paths for publish-v2-internal-reports.js | node tools/config/v2-internal-report-pages.js [flags] | docs |
+
+## tasks/scripts
+
+| Script | Summary | Usage | Owner |
+|---|---|---|---|
+| tasks/scripts/audit-python.py | Python audit utility — runs Python-based audit checks (alternative to Node auditors) | python3 tasks/scripts/audit-python.py [flags] | docs |
+
+## snippets/automations
+
+| Script | Summary | Usage | Owner |
+|---|---|---|---|
+| snippets/automations/youtube/filterVideos.js | YouTube video filter — post-processes fetched YouTube data to filter/sort videos for display | node snippets/automations/youtube/filterVideos.js [flags] | docs |
diff --git a/snippets/automations/script-index.md b/snippets/automations/script-index.md
new file mode 100644
index 000000000..b7f44655b
--- /dev/null
+++ b/snippets/automations/script-index.md
@@ -0,0 +1,9 @@
+# automations Script Index
+
+{/* SCRIPT-INDEX:START */}
+## Script Index
+
+| Script | Summary | Usage | Owner |
+|---|---|---|---|
+| `snippets/automations/youtube/filterVideos.js` | YouTube video filter — post-processes fetched YouTube data to filter/sort videos for display | `node snippets/automations/youtube/filterVideos.js [flags]` | docs |
+{/* SCRIPT-INDEX:END */}
diff --git a/tasks/reports/repo-ops/SCRIPT_INVENTORY_FULL.json b/tasks/reports/repo-ops/SCRIPT_INVENTORY_FULL.json
new file mode 100644
index 000000000..9eb95e674
--- /dev/null
+++ b/tasks/reports/repo-ops/SCRIPT_INVENTORY_FULL.json
@@ -0,0 +1,21921 @@
+{
+ "generated_at": "2026-03-09T16:58:20.262Z",
+ "output_dir": "tasks/reports/repo-ops",
+ "summary": {
+ "total_scripts": 200,
+ "by_trigger": {
+ "Unmanaged": 0,
+ "Orphaned": 0,
+ "P1": 28,
+ "P2": 8,
+ "P3": 3,
+ "P5": 7,
+ "P6": 5,
+ "Indirect": 60,
+ "Manual": 89
+ },
+ "grade_distribution": {
+ "A": 0,
+ "B": 48,
+ "C": 152,
+ "F": 0
+ },
+ "pipeline_verification": {
+ "MATCH": 200,
+ "MISMATCH": 0,
+ "MISSING": 0
+ },
+ "classification_sync": {
+ "in_json": 200,
+ "not_in_json": 0,
+ "phantom": 0
+ },
+ "output_chain_count": 1
+ },
+ "groups": {
+ "Unmanaged": {
+ "label": "Unmanaged",
+ "scripts": [],
+ "count": 0
+ },
+ "Orphaned": {
+ "label": "Orphaned",
+ "scripts": [],
+ "count": 0
+ },
+ "P1": {
+ "label": "P1 - Commit gate",
+ "scripts": [
+ {
+ "path": "tools/scripts/check-no-ai-stash.sh",
+ "script": "check-no-ai-stash",
+ "category": "enforcer",
+ "purpose": "governance:agent-governance",
+ "scope": "tools/scripts, .githooks/pre-commit",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "AI stash enforcer — blocks push if AI temporary/stash files are present in working tree",
+ "pipeline_declared": "P1, P2",
+ "usage": "bash tools/scripts/check-no-ai-stash.sh [flags]",
+ "header": "#!/usr/bin/env bash\n# @script check-no-ai-stash\n# @category enforcer\n# @purpose governance:agent-governance\n# @scope tools/scripts, .githooks/pre-commit\n# @owner docs\n# @needs R-R27, R-R30\n# @purpose-statement AI stash enforcer — blocks push if AI temporary/stash files are present in working tree\n# @pipeline P1, P2\n# @usage bash tools/scripts/check-no-ai-stash.sh [flags]\nset -euo pipefail\n\nbranch=\"\"\nquiet=0\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --branch)\n branch=\"${2:-}\"\n shift 2\n ;;\n --quiet)\n quiet=1\n shift\n ;;\n *)\n echo \"Unknown argument: $1\" >&2\n exit 1\n ;;\n esac\ndone\n\nif [[ -z \"$branch\" ]]; then\n branch=\"$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)\"\nfi\n\nstash_lines=\"$(git stash list --date=iso 2>/dev/null || true)\"\nif [[ -z \"$stash_lines\" ]]; then\n exit 0\nfi\n\nviolations=()\n\nwhile IFS= read -r line; do\n [[ -z \"$line\" ]] && continue\n\n # Direct marker used by previous AI stash workflows.\n if [[ \"$line\" == *\"stash-non-codex\"* ]]; then\n violations+=(\"$line\")\n continue\n fi\n\n # Only stashes tied to the current branch should block this worktree.\n if [[ -n \"$branch\" ]] && [[ \"$line\" == *\"On ${branch}:\"* ]]; then\n violations+=(\"$line\")\n continue\n fi\ndone <<< \"$stash_lines\"\n\nif [[ ${#violations[@]} -eq 0 ]]; then\n exit 0\nfi\n\nif [[ \"$quiet\" -eq 0 ]]; then\n echo \"AI stash policy violation: stash-based isolation is forbidden.\"\n echo \"Use branch + WIP commit checkpoints instead.\"\nfi\nfor line in \"${violations[@]}\"; do\n echo \"$line\"\ndone\n\nexit 1\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "pre-commit",
+ "caller": ".githooks/pre-commit",
+ "pipeline": "P1"
+ },
+ {
+ "type": "pre-push",
+ "caller": ".githooks/pre-push",
+ "pipeline": "P2"
+ },
+ {
+ "type": "script",
+ "caller": ".githooks/pre-push",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 (pre-commit); P2 (pre-push); indirect via .githooks/pre-push",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tools/scripts/codex/validate-locks.js",
+ "script": "codex/validate-locks",
+ "category": "enforcer",
+ "purpose": "governance:agent-governance",
+ "scope": "tools/scripts/codex, .codex/locks-local, .codex/task-contract.yaml",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Codex lock validator — checks for stale or conflicting lock files before push",
+ "pipeline_declared": "P1 (commit), P2 (push)",
+ "usage": "node tools/scripts/codex/validate-locks.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script codex/validate-locks\n * @category enforcer\n * @purpose governance:agent-governance\n * @scope tools/scripts/codex, .codex/locks-local, .codex/task-contract.yaml\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Codex lock validator — checks for stale or conflicting lock files before push\n * @pipeline P1 (commit), P2 (push)\n * @usage node tools/scripts/codex/validate-locks.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\nconst yaml = require('js-yaml');\n\nconst CODEX_BRANCH_RE = /^codex\\/(\\d+)-[a-z0-9][a-z0-9-]*$/;\nconst DEFAULT_CONTRACT = '.codex/task-contract.yaml';\nconst LOCK_DIR_REL = '.codex/locks-local';\n\nconst REPO_ROOT = getRepoRoot();\n\nfunction getRepoRoot() {\n const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n if (result.status === 0 && String(result.stdout || '').trim()) {\n return String(result.stdout || '').trim();\n }\n return process.cwd();\n}\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction runGit(args) {\n const result = spawnSync('git', args, { cwd: REPO_ROOT, encoding: 'utf8' });\n if (result.status !== 0) {\n const stderr = String(result.stderr || '').trim();\n const stdout = String(result.stdout || '').trim();\n throw new Error(stderr || stdout || `git ${args.join(' ')} failed`);\n }\n return String(result.stdout || '').trim();\n}\n\nfunction tryRunGit(args) {\n try {\n return runGit(args);\n } catch (_error) {\n return '';\n }\n}\n\nfunction parseArgs(argv) {\n const args = {\n contractPath: DEFAULT_CONTRACT,\n branch: '',\n files: null,\n staged: false,\n quiet: false,\n json: false\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--contract') {\n args.contractPath = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--contract=')) {\n args.contractPath = token.slice('--contract='.length).trim();\n continue;\n }\n if (token === '--branch') {\n args.branch = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--branch=')) {\n args.branch = token.slice('--branch='.length).trim();\n continue;\n }\n if (token === '--files') {\n const raw = String(argv[i + 1] || '').trim();\n args.files = raw\n ? [...new Set(raw.split(',').map((entry) => toPosix(entry.trim())).filter(Boolean))]\n : [];\n i += 1;\n continue;\n }\n if (token.startsWith('--files=')) {\n const raw = token.slice('--files='.length).trim();\n args.files = raw\n ? [...new Set(raw.split(',').map((entry) => toPosix(entry.trim())).filter(Boolean))]\n : [];\n continue;\n }\n if (token === '--staged') {\n args.staged = true;\n continue;\n }\n if (token === '--quiet') {\n args.quiet = true;\n continue;\n }\n if (token === '--json') {\n args.json = true;\n continue;\n }\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n return args;\n}\n\nfunction usage() {\n console.log('Usage: node tools/scripts/codex/validate-locks.js [--contract /g, \"
\");\n cleanHTML = cleanHTML.replace(/<\\/p>/g, \"
\");\n cleanHTML = cleanHTML.replace(/(.*?)<\\/code>/g, \"$1\");\n\n // Simplify links\n cleanHTML = cleanHTML.replace(\n /]*href=\"([^\"]*)\"[^>]*>(.*?)<\\/a>/g,\n '$2'\n );\n\n // Decode HTML entities\n cleanHTML = cleanHTML.replace(/&/g, \"&\");\n cleanHTML = cleanHTML.replace(/</g, \"<\");\n cleanHTML = cleanHTML.replace(/>/g, \">\");\n cleanHTML = cleanHTML.replace(/"/g, '\"');\n cleanHTML = cleanHTML.replace(/'/g, \"'\");\n cleanHTML = cleanHTML.replace(/ /g, \" \");\n\n // Clean up whitespace\n cleanHTML = cleanHTML.replace(/\\s+/g, \" \");\n cleanHTML = cleanHTML.replace(/\\s*<\\/p>/g, \"\");\n\n return cleanHTML.trim();\n}\n\nasync function main() {\n console.log(\"Fetching latest topics...\");\n const latestData = await fetchJSON(\"https://forum.livepeer.org/latest.json\");\n\n const topics = latestData.topic_list?.topics || [];\n console.log(`Found ${topics.length} topics`);\n\n // Filter out old pinned topics\n const filteredTopics = topics.filter((t) => !isOldPinned(t));\n console.log(`After filtering: ${filteredTopics.length} topics`);\n\n // Get top 4\n const top4 = filteredTopics.slice(0, 4);\n console.log(`Processing top 4 topics...`);\n\n const processedTopics = [];\n\n for (const topic of top4) {\n console.log(`Processing topic ${topic.id}: ${topic.title}`);\n\n // Fetch full topic data\n const topicData = await fetchJSON(\n `https://forum.livepeer.org/t/${topic.id}.json`\n );\n\n // Extract first post\n const firstPost = topicData.post_stream?.posts?.find(\n (p) => p.post_number === 1\n );\n\n if (!firstPost) {\n console.log(` No first post found, skipping`);\n continue;\n }\n\n const htmlContent = cleanAndFormatHTML(firstPost.cooked || \"\");\n const datePosted = topic.created_at\n ? new Date(topic.created_at).toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n })\n : \"\";\n\n processedTopics.push({\n title: topic.title,\n href: `https://forum.livepeer.org/t/${topic.id}`,\n author: `By ${firstPost.name || firstPost.username || \"Unknown\"} (@${\n firstPost.username || \"unknown\"\n })`,\n content: htmlContent,\n replyCount: (topic.posts_count || 1) - 1,\n datePosted: datePosted,\n });\n }\n\n console.log(`Processed ${processedTopics.length} topics`);\n\n // Generate JavaScript export with exact formatting\n let jsExport = \"export const forumData = [\\n\";\n\n processedTopics.forEach((item, index) => {\n jsExport += \" {\\n\";\n jsExport += ` title: \"${item.title\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')}\",\\n`;\n jsExport += ` href: \"${item.href}\",\\n`;\n jsExport += ` author: \"${item.author\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')}\",\\n`;\n\n // Content with proper escaping and indentation\n const escapedContent = item.content\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \" \");\n\n jsExport += ` content:\\n \"${escapedContent}\",\\n`;\n jsExport += ` replyCount: ${item.replyCount},\\n`;\n jsExport += ` datePosted: \"${item.datePosted}\",\\n`;\n jsExport += \" }\";\n\n if (index < processedTopics.length - 1) {\n jsExport += \",\";\n }\n jsExport += \"\\n\";\n });\n\n jsExport += \"];\\n\";\n\n // Write to file\n const outputPath = \"snippets/automations/forum/forumData.jsx\";\n fs.mkdirSync(\"snippets/automations/forum\", { recursive: true });\n fs.writeFileSync(outputPath, jsExport);\n console.log(`Written to ${outputPath}`);\n}\n\nmain().catch((err) => {\n console.error(\"Error:\", err);\n process.exit(1);\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-schedule",
+ "caller": ".github/workflows/update-forum-data.yml",
+ "workflow": "Update Forum Data",
+ "pipeline": "P5",
+ "cron": "0 0 * * *"
+ },
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/update-forum-data.yml",
+ "workflow": "Update Forum Data",
+ "pipeline": "P6"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": ".github/scripts/snippets/automations/forum",
+ "type": "directory",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": ".github/scripts/snippets/automations/forum/forumData.jsx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": ".github/scripts/snippets/automations/forum, .github/scripts/snippets/automations/forum/forumData.jsx",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P5 (Update Forum Data cron 0 0 * * *); P6 (Update Forum Data)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P5"
+ },
+ {
+ "path": ".github/scripts/fetch-ghost-blog-data.js",
+ "script": "fetch-ghost-blog-data",
+ "category": "automation",
+ "purpose": "infrastructure:data-feeds",
+ "scope": ".github/scripts",
+ "owner": "docs",
+ "needs": "F-R1",
+ "purpose_statement": "Fetches blog posts from Ghost CMS API, writes to snippets/automations/blog/",
+ "pipeline_declared": "P5, P6",
+ "usage": "node .github/scripts/fetch-ghost-blog-data.js [flags]",
+ "header": "/**\n * @script fetch-ghost-blog-data\n * @category automation\n * @purpose infrastructure:data-feeds\n * @scope .github/scripts\n * @owner docs\n * @needs F-R1\n * @purpose-statement Fetches blog posts from Ghost CMS API, writes to snippets/automations/blog/\n * @pipeline P5, P6\n * @usage node .github/scripts/fetch-ghost-blog-data.js [flags]\n */\nconst https = require(\"https\");\nconst fs = require(\"fs\");\n\n// Fetch JSON from URL\nfunction fetchJSON(url) {\n return new Promise((resolve, reject) => {\n https\n .get(url, (res) => {\n let data = \"\";\n res.on(\"data\", (chunk) => {\n data += chunk;\n });\n res.on(\"end\", () => {\n try {\n resolve(JSON.parse(data));\n } catch (e) {\n reject(e);\n }\n });\n })\n .on(\"error\", reject);\n });\n}\n\n// Safe HTML escape - only escape backticks for template literals\nfunction safeHTML(html) {\n return (html || \"\").replace(/`/g, \"\\\\`\");\n}\n\n// Format date\nfunction formatDate(iso) {\n return new Date(iso).toLocaleDateString(\"en-US\", {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n });\n}\n\nasync function main() {\n console.log(\"Fetching Ghost blog posts...\");\n\n const apiKey = process.env.GHOST_CONTENT_API_KEY;\n if (!apiKey) {\n throw new Error(\n \"GHOST_CONTENT_API_KEY environment variable is not set. Please configure your Ghost Content API key.\"\n );\n }\n\n const apiUrl =\n `https://livepeer-studio.ghost.io/ghost/api/content/posts/?key=${apiKey}&limit=4&include=tags,authors`;\n\n const response = await fetchJSON(apiUrl);\n\n if (!response.posts || response.posts.length === 0) {\n console.log(\"No posts found\");\n return;\n }\n\n console.log(`Found ${response.posts.length} posts`);\n\n // Process posts\n const posts = response.posts.map((p) => ({\n title: p.title,\n href: p.url,\n author: p.primary_author?.name\n ? `By ${p.primary_author.name}`\n : \"By Livepeer Team\",\n content: safeHTML(p.html),\n datePosted: formatDate(p.published_at),\n img: p.feature_image || \"\",\n excerpt: safeHTML(p.excerpt),\n readingTime: p.reading_time || 0,\n }));\n\n // Generate JavaScript export with template literals\n let jsExport = \"export const ghostData = [\\n\";\n\n posts.forEach((post, index) => {\n jsExport += \"{\\n\";\n jsExport += ` title: \\`${post.title}\\`,\\n`;\n jsExport += ` href: \\`${post.href}\\`,\\n`;\n jsExport += ` author: \\`${post.author}\\`,\\n`;\n jsExport += ` content: \\`${post.content}\\`,\\n`;\n jsExport += ` datePosted: \\`${post.datePosted}\\`,\\n`;\n jsExport += ` img: \\`${post.img}\\`,\\n`;\n jsExport += ` excerpt: \\`${post.excerpt}\\`,\\n`;\n jsExport += ` readingTime: ${post.readingTime}\\n`;\n jsExport += \"}\";\n\n if (index < posts.length - 1) {\n jsExport += \",\";\n }\n jsExport += \"\\n\";\n });\n\n jsExport += \"];\\n\";\n\n // Write to file\n const outputPath = \"snippets/automations/blog/ghostBlogData.jsx\";\n fs.mkdirSync(\"snippets/automations/blog\", { recursive: true });\n fs.writeFileSync(outputPath, jsExport);\n console.log(`Written to ${outputPath}`);\n}\n\nmain().catch((err) => {\n console.error(\"Error:\", err);\n process.exit(1);\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-schedule",
+ "caller": ".github/workflows/update-ghost-blog-data.yml",
+ "workflow": "Update Ghost Blog Data",
+ "pipeline": "P5",
+ "cron": "0 0 * * *"
+ },
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/update-ghost-blog-data.yml",
+ "workflow": "Update Ghost Blog Data",
+ "pipeline": "P6"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": ".github/scripts/snippets/automations/blog",
+ "type": "directory",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": ".github/scripts/snippets/automations/blog/ghostBlogData.jsx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": ".github/scripts/snippets/automations/blog, .github/scripts/snippets/automations/blog/ghostBlogData.jsx",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P5 (Update Ghost Blog Data cron 0 0 * * *); P6 (Update Ghost Blog Data)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P5"
+ },
+ {
+ "path": ".github/scripts/fetch-youtube-data.js",
+ "script": "fetch-youtube-data",
+ "category": "automation",
+ "purpose": "infrastructure:data-feeds",
+ "scope": ".github/scripts",
+ "owner": "docs",
+ "needs": "F-R1",
+ "purpose_statement": "Fetches video data from YouTube Data API, writes to snippets/automations/youtube/",
+ "pipeline_declared": "P5, P6",
+ "usage": "node .github/scripts/fetch-youtube-data.js [flags]",
+ "header": "/**\n * @script fetch-youtube-data\n * @category automation\n * @purpose infrastructure:data-feeds\n * @scope .github/scripts\n * @owner docs\n * @needs F-R1\n * @purpose-statement Fetches video data from YouTube Data API, writes to snippets/automations/youtube/\n * @pipeline P5, P6\n * @usage node .github/scripts/fetch-youtube-data.js [flags]\n */\nconst https = require(\"https\");\nconst fs = require(\"fs\");\n\nconst YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY;\nconst CHANNEL_ID = process.env.CHANNEL_ID || \"UCzfHtZnmUzMbJDxGCwIgY2g\";\n\nfunction httpsGet(url) {\n return new Promise((resolve, reject) => {\n https\n .get(url, (res) => {\n let data = \"\";\n res.on(\"data\", (chunk) => (data += chunk));\n res.on(\"end\", () => resolve(JSON.parse(data)));\n })\n .on(\"error\", reject);\n });\n}\n\nfunction parseDuration(duration) {\n const match = duration.match(/PT(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?/);\n if (!match) return 0;\n\n const hours = parseInt(match[1] || 0);\n const minutes = parseInt(match[2] || 0);\n const seconds = parseInt(match[3] || 0);\n\n return hours * 3600 + minutes * 60 + seconds;\n}\n\nfunction escapeForJSX(str) {\n return str\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/'/g, \"\\\\'\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \" \")\n .replace(/\\r/g, \"\")\n .replace(/\\t/g, \" \");\n}\n\nasync function main() {\n // Step 1: Get recent videos\n console.log(\"Fetching recent videos...\");\n const searchUrl = `https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=${CHANNEL_ID}&maxResults=50&order=date&type=video&key=${YOUTUBE_API_KEY}`;\n const searchResults = await httpsGet(searchUrl);\n\n if (!searchResults.items || searchResults.items.length === 0) {\n console.log(\"No videos found\");\n return;\n }\n\n // Step 2: Get video details for each video\n console.log(\n `Found ${searchResults.items.length} videos, fetching details...`\n );\n const videoIds = searchResults.items.map((item) => item.id.videoId).join(\",\");\n const detailsUrl = `https://www.googleapis.com/youtube/v3/videos?part=contentDetails,snippet&id=${videoIds}&key=${YOUTUBE_API_KEY}`;\n const detailsResults = await httpsGet(detailsUrl);\n\n // Step 3: Process and filter videos\n const videos = [];\n for (const video of detailsResults.items) {\n const duration = video.contentDetails.duration;\n const durationSeconds = parseDuration(duration);\n const snippet = video.snippet;\n\n // Check if it's a livestream\n const isLivestream =\n snippet.liveBroadcastContent === \"live\" ||\n snippet.liveBroadcastContent === \"upcoming\" ||\n duration === \"PT0S\" ||\n snippet.title.toLowerCase().includes(\"watercooler\") ||\n snippet.title.toLowerCase().includes(\"fireside\");\n\n // Filter out Shorts (≤60 seconds and not livestreams)\n const isShort =\n durationSeconds <= 60 && durationSeconds > 0 && !isLivestream;\n\n if (!isShort) {\n videos.push({\n title: snippet.title,\n href: `https://www.youtube.com/watch?v=${video.id}`,\n author: `By ${snippet.channelTitle || \"Livepeer\"}`,\n content: (snippet.description || \"\").substring(0, 500),\n publishedDate: new Date(snippet.publishedAt).toLocaleDateString(\n \"en-US\",\n { month: \"short\", day: \"numeric\", year: \"numeric\" }\n ),\n duration: duration,\n thumbnailUrl: snippet.thumbnails.high.url,\n });\n }\n }\n\n console.log(`Filtered to ${videos.length} non-Short videos`);\n\n // Step 4: Generate JSX content\n const jsxContent = `export const youtubeData = [\n${videos\n .map(\n (v) => ` {\n title: '${escapeForJSX(v.title)}',\n href: '${v.href}',\n author: '${v.author}',\n content: '${escapeForJSX(v.content)}...',\n publishedDate: '${v.publishedDate}',\n duration: '${v.duration}',\n thumbnailUrl: '${v.thumbnailUrl}'\n }`\n )\n .join(\",\\n\")}\n];\n`;\n\n // Step 5: Write to file\n fs.writeFileSync(\"snippets/automations/youtube/youtubeData.jsx\", jsxContent);\n console.log(\"Successfully wrote youtubeData.jsx\");\n}\n\nmain().catch((err) => {\n console.error(\"Error:\", err);\n process.exit(1);\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-schedule",
+ "caller": ".github/workflows/update-youtube-data.yml",
+ "workflow": "Update YouTube Data",
+ "pipeline": "P5",
+ "cron": "0 0 * * 0"
+ },
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/update-youtube-data.yml",
+ "workflow": "Update YouTube Data",
+ "pipeline": "P6"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": ".github/scripts/snippets/automations/youtube/youtubeData.jsx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": ".github/scripts/snippets/automations/youtube/youtubeData.jsx",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P5 (Update YouTube Data cron 0 0 * * 0); P6 (Update YouTube Data)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P5"
+ },
+ {
+ "path": ".github/scripts/project-showcase-sync.js",
+ "script": "project-showcase-sync",
+ "category": "automation",
+ "purpose": "infrastructure:data-feeds",
+ "scope": ".github/scripts",
+ "owner": "docs",
+ "needs": "F-R1",
+ "purpose_statement": "Fetches project showcase data from external source, writes to snippets/automations/showcase/",
+ "pipeline_declared": "P5, P6",
+ "usage": "node .github/scripts/project-showcase-sync.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script project-showcase-sync\n * @category automation\n * @purpose infrastructure:data-feeds\n * @scope .github/scripts\n * @owner docs\n * @needs F-R1\n * @purpose-statement Fetches project showcase data from external source, writes to snippets/automations/showcase/\n * @pipeline P5, P6\n * @usage node .github/scripts/project-showcase-sync.js [flags]\n */\n/*\n * Project Showcase sync job for GitHub Actions.\n *\n * Modes:\n * - poll: process new submissions + pending review decisions from Sheets\n * - dispatch: process one decision from repository_dispatch payload\n */\n\nconst crypto = require('crypto');\nconst fs = require('fs');\n\nconst REQUIRED_ENVS = [\n 'GOOGLE_SERVICE_ACCOUNT_JSON',\n 'GOOGLE_SHEET_ID',\n 'DISCORD_BOT_TOKEN',\n 'DISCORD_REVIEWER_USER_ID'\n];\n\nconst cfg = {\n mode: process.env.SHOWCASE_SYNC_MODE || 'poll',\n googleSheetId: process.env.GOOGLE_SHEET_ID,\n submissionsSheetName: process.env.SUBMISSIONS_SHEET_NAME || 'Form Responses 1',\n submissionsSheetUrl: process.env.SUBMISSIONS_SHEET_URL || '',\n reviewSheetName: process.env.REVIEW_SHEET_NAME || 'Review Responses',\n transformedSheetName: process.env.TRANSFORMED_SHEET_NAME || 'Transformed Responses',\n reviewProcessedColumn: process.env.REVIEW_PROCESSED_COLUMN || 'Processed',\n approvalFormBaseUrl: process.env.APPROVAL_FORM_BASE_URL || '',\n githubOwner: process.env.GITHUB_OWNER || process.env.GITHUB_REPOSITORY?.split('/')[0],\n githubRepo: process.env.GITHUB_REPO || process.env.GITHUB_REPOSITORY?.split('/')[1],\n githubDataBranch: process.env.GITHUB_DATA_BRANCH || 'docs-v2-preview',\n githubAssetsBranch: process.env.GITHUB_ASSETS_BRANCH || 'docs-v2-assets',\n showcaseDataPath:\n process.env.SHOWCASE_DATA_FILE_PATH || 'snippets/automations/showcase/showcaseData.jsx',\n showcaseAssetsBasePath:\n process.env.SHOWCASE_ASSETS_BASE_PATH || 'snippets/assets/domain/00_HOME/showcase',\n discordApiBaseUrl: process.env.DISCORD_API_BASE_URL || 'https://discord.com/api/v10',\n discordReviewerUserId: process.env.DISCORD_REVIEWER_USER_ID,\n githubToken: process.env.GITHUB_TOKEN,\n maxRowsPerRun: Number(process.env.MAX_ROWS_PER_RUN || 10)\n};\n\nconst serviceAccount = JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_JSON || '{}');\n\nfunction log(msg, obj) {\n if (obj !== undefined) {\n console.log(msg, JSON.stringify(obj));\n } else {\n console.log(msg);\n }\n}\n\nfunction assertEnv() {\n const missing = REQUIRED_ENVS.filter((key) => !process.env[key]);\n if (missing.length > 0) {\n throw new Error(`Missing required env vars: ${missing.join(', ')}`);\n }\n if (!cfg.githubOwner || !cfg.githubRepo || !cfg.githubToken) {\n throw new Error('Missing GitHub config: GITHUB_OWNER/GITHUB_REPO/GITHUB_TOKEN');\n }\n}\n\nfunction base64url(input) {\n return Buffer.from(input)\n .toString('base64')\n .replace(/=/g, '')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_');\n}\n\nfunction sanitizePathSegment(input) {\n return String(input || '')\n .trim()\n .replace(/[^a-zA-Z0-9._-]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 120) || 'untitled-project';\n}\n\nfunction getDriveFileId(url) {\n if (!url) return null;\n const patterns = [\n /\\/file\\/d\\/([a-zA-Z0-9_-]+)/,\n /[?&]id=([a-zA-Z0-9_-]+)/,\n /\\/uc\\?export=download&id=([a-zA-Z0-9_-]+)/\n ];\n for (const p of patterns) {\n const m = String(url).match(p);\n if (m?.[1]) return m[1];\n }\n return null;\n}\n\nfunction isValidUrl(value) {\n try {\n new URL(String(value));\n return true;\n } catch {\n return false;\n }\n}\n\nfunction findHeader(headers, candidates) {\n const map = new Map(headers.map((h) => [String(h).toLowerCase().trim(), h]));\n for (const c of candidates) {\n const hit = map.get(String(c).toLowerCase().trim());\n if (hit) return hit;\n }\n return null;\n}\n\nfunction getCell(row, headers, candidates) {\n const header = findHeader(headers, candidates);\n if (!header) return '';\n return row[header] || '';\n}\n\nfunction toObjects(values) {\n if (!values.length) return { headers: [], rows: [] };\n const headers = values[0];\n const rows = values.slice(1).map((raw, i) => {\n const json = {};\n headers.forEach((h, idx) => {\n json[h] = raw[idx] || '';\n });\n json.__rowIndex = i + 2;\n return json;\n });\n return { headers, rows };\n}\n\nfunction colToA1(colIndexZeroBased) {\n let n = colIndexZeroBased + 1;\n let result = '';\n while (n > 0) {\n const rem = (n - 1) % 26;\n result = String.fromCharCode(65 + rem) + result;\n n = Math.floor((n - 1) / 26);\n }\n return result;\n}\n\nfunction parseDecision(raw) {\n const value = String(raw || '').trim().toLowerCase();\n if (['yes', 'approve', 'approved'].includes(value)) return 'yes';\n if (['no', 'deny', 'denied'].includes(value)) return 'no';\n return null;\n}\n\nasync function getGoogleAccessToken() {\n const now = Math.floor(Date.now() / 1000);\n const header = { alg: 'RS256', typ: 'JWT' };\n const payload = {\n iss: serviceAccount.client_email,\n scope: 'https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.readonly',\n aud: 'https://oauth2.googleapis.com/token',\n exp: now + 3600,\n iat: now\n };\n\n const unsignedToken = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(payload))}`;\n const signer = crypto.createSign('RSA-SHA256');\n signer.update(unsignedToken);\n signer.end();\n const signature = signer.sign(serviceAccount.private_key, 'base64url');\n const assertion = `${unsignedToken}.${signature}`;\n\n const body = new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion\n });\n\n const res = await fetch('https://oauth2.googleapis.com/token', {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body\n });\n\n if (!res.ok) {\n throw new Error(`Failed to get Google access token: ${res.status} ${await res.text()}`);\n }\n\n return (await res.json()).access_token;\n}\n\nasync function sheetsGetValues(accessToken, range) {\n const url = `https://sheets.googleapis.com/v4/spreadsheets/${cfg.googleSheetId}/values/${encodeURIComponent(\n range\n )}`;\n const res = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } });\n if (!res.ok) {\n throw new Error(`Sheets read failed for ${range}: ${res.status} ${await res.text()}`);\n }\n return (await res.json()).values || [];\n}\n\nasync function sheetsUpdateValue(accessToken, range, value) {\n const url = `https://sheets.googleapis.com/v4/spreadsheets/${cfg.googleSheetId}/values/${encodeURIComponent(\n range\n )}?valueInputOption=RAW`;\n const res = await fetch(url, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ values: [[value]] })\n });\n if (!res.ok) {\n throw new Error(`Sheets update failed for ${range}: ${res.status} ${await res.text()}`);",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-schedule",
+ "caller": ".github/workflows/project-showcase-sync.yml",
+ "workflow": "Project Showcase Sync",
+ "pipeline": "P5",
+ "cron": "*/15 * * * *"
+ },
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/project-showcase-sync.yml",
+ "workflow": "Project Showcase Sync",
+ "pipeline": "P6"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P5 (Project Showcase Sync cron */15 * * * *); P6 (Project Showcase Sync)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P5"
+ },
+ {
+ "path": "tools/scripts/docs-quality-and-freshness-audit.js",
+ "script": "docs-quality-and-freshness-audit",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tools/scripts, v2, tasks/reports/quality-accessibility",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Content freshness audit — checks for TODO/TBD/Coming Soon markers, thin pages, stale content",
+ "pipeline_declared": "P5, P6",
+ "usage": "node tools/scripts/docs-quality-and-freshness-audit.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script docs-quality-and-freshness-audit\n * @category validator\n * @purpose qa:content-quality\n * @scope tools/scripts, v2, tasks/reports/quality-accessibility\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Content freshness audit — checks for TODO/TBD/Coming Soon markers, thin pages, stale content\n * @pipeline P5, P6\n * @usage node tools/scripts/docs-quality-and-freshness-audit.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\n\nconst STAGE_ID = 'docs-quality-and-freshness-audit';\nconst REPO_ROOT = process.cwd();\nconst DEFAULT_OUTPUT_DIR = 'tasks/reports/repo-ops';\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction parseArgs(argv) {\n const out = { scope: 'full', outputDir: DEFAULT_OUTPUT_DIR };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--scope') {\n out.scope = String(argv[i + 1] || out.scope).trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--scope=')) {\n out.scope = token.slice('--scope='.length).trim() || out.scope;\n continue;\n }\n if (token === '--output-dir') {\n out.outputDir = String(argv[i + 1] || out.outputDir).trim() || out.outputDir;\n i += 1;\n continue;\n }\n if (token.startsWith('--output-dir=')) {\n out.outputDir = token.slice('--output-dir='.length).trim() || out.outputDir;\n continue;\n }\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (!['changed', 'full'].includes(out.scope)) {\n throw new Error(`Invalid --scope: ${out.scope}`);\n }\n\n return out;\n}\n\nfunction addIssue(issues, issue) {\n issues.push({\n id: issue.id,\n title: issue.title,\n severity: issue.severity,\n evidence: issue.evidence,\n recommendation: issue.recommendation,\n path: issue.path || '',\n line: Number.isFinite(Number(issue.line)) ? Number(issue.line) : 1\n });\n}\n\nfunction summarize(issues) {\n const summary = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n total: issues.length\n };\n\n for (const issue of issues) {\n if (Object.prototype.hasOwnProperty.call(summary, issue.severity)) {\n summary[issue.severity] += 1;\n }\n }\n\n return summary;\n}\n\nfunction shouldExclude(relPath) {\n const rel = toPosix(relPath);\n if (!rel.startsWith('v2/')) return true;\n if (rel.startsWith('v2/es/') || rel.startsWith('v2/fr/') || rel.startsWith('v2/cn/')) return true;\n if (rel.startsWith('v2/internal/')) return true;\n if (rel.includes('/x-')) return true;\n if (rel.includes('/_contextData_/') || rel.includes('/_context_data_/')) return true;\n if (rel.includes('/_move_me/') || rel.includes('/_tests-to-delete/')) return true;\n if (rel.endsWith('todo.txt') || rel.endsWith('todo.mdx') || rel.endsWith('NOTES_V2.md')) return true;\n return false;\n}\n\nfunction walkFiles(dirPath, out = []) {\n if (!fs.existsSync(dirPath)) return out;\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n for (const entry of entries) {\n const full = path.join(dirPath, entry.name);\n const rel = toPosix(path.relative(REPO_ROOT, full));\n\n if (entry.isDirectory()) {\n if (entry.name === '.git' || entry.name === 'node_modules') continue;\n walkFiles(full, out);\n continue;\n }\n\n if (!/\\.mdx$/i.test(entry.name)) continue;\n if (shouldExclude(rel)) continue;\n out.push({ absPath: full, relPath: rel });\n }\n return out;\n}\n\nfunction getChangedEnglishFiles() {\n try {\n const output = execSync('git diff --name-only --diff-filter=ACMR HEAD', {\n cwd: REPO_ROOT,\n encoding: 'utf8'\n });\n\n return output\n .split('\\n')\n .map((line) => toPosix(line.trim()))\n .filter(Boolean)\n .filter((line) => line.startsWith('v2/'))\n .filter((line) => /\\.mdx$/i.test(line))\n .filter((line) => !shouldExclude(line))\n .map((line) => ({ relPath: line, absPath: path.join(REPO_ROOT, line) }));\n } catch (_error) {\n return [];\n }\n}\n\nfunction countWords(content) {\n return String(content || '')\n .replace(/^---[\\s\\S]*?---\\s*/m, ' ')\n .replace(/```[\\s\\S]*?```/g, ' ')\n .replace(/`[^`]+`/g, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\[[^\\]]+\\]\\([^)]+\\)/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim()\n .split(' ')\n .filter(Boolean).length;\n}\n\nfunction loadUsefulnessSignal(issues) {\n const metadataPath = path.join(\n REPO_ROOT,\n 'tasks/reports/quality-accessibility/docs-usefulness/latest/run-metadata.json'\n );\n\n if (!fs.existsSync(metadataPath)) {\n addIssue(issues, {\n id: 'usefulness-metadata-missing',\n title: 'Docs usefulness metadata not found',\n severity: 'low',\n path: 'tasks/reports/quality-accessibility/docs-usefulness/latest/run-metadata.json',\n evidence: 'No usefulness metadata snapshot found for cross-check.',\n recommendation: 'Run `node tools/scripts/audit-v2-usefulness.js --mode full` for a fresh baseline.'\n });\n return;\n }\n\n try {\n const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));\n const pages = Number(metadata?.counts?.processed_pages || metadata?.processed_pages || 0);\n if (pages === 0) {\n addIssue(issues, {\n id: 'usefulness-empty-run',\n title: 'Usefulness metadata reports zero processed pages',\n severity: 'low',\n path: 'tasks/reports/quality-accessibility/docs-usefulness/latest/run-metadata.json',\n evidence: 'Processed pages count is zero in latest usefulness metadata.',\n recommendation: 'Verify usefulness run mode and regenerate metadata with a full run.'\n });\n }\n } catch (_error) {\n addIssue(issues, {\n id: 'usefulness-metadata-parse-failed',\n title: 'Unable to parse usefulness metadata',\n severity: 'low',\n path: 'tasks/reports/quality-accessibility/docs-usefulness/latest/run-metadata.json',\n evidence: 'Metadata JSON could not be parsed.',\n recommendation: 'Regenerate usefulness outputs to restore machine-readable state.'\n });\n }\n}\n\nfunction analyzeFileMarkers(issues, file) {\n const content = fs.readFileSync(file.absPath, 'utf8');\n const lines = content.split('\\n');\n const markerRules = [\n { id: 'todo-marker', label: 'TODO marker detected', regex: /\\bTODO\\b/i, severity: 'medium' },\n { id: 'tbd-marker', label: 'TBD marker detected', regex: /\\bTBD\\b/i, severity: 'medium' },\n { id: 'coming-soon-marker', label: 'Coming Soon marker detected', regex: /Coming Soon/i, severity: 'high' },\n { id: 'preview-callout-marker', label: 'PreviewCallout marker detected', regex: / {\n let count = 0;\n lines.forEach((line, index) => {\n if (!rule.regex.test(line)) return;\n count += 1;\n addIssue(issues, {\n id: rule.id,\n title: rule.label,\n severity: rule.severity,",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-schedule",
+ "caller": ".github/workflows/content-health.yml",
+ "workflow": "Content Health Check",
+ "pipeline": "P5",
+ "cron": "0 6 * * 1"
+ },
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/content-health.yml",
+ "workflow": "Content Health Check",
+ "pipeline": "P6"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "audit:docs-quality"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/${STAGE_ID}.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/scripts/${STAGE_ID}.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/${STAGE_ID}.json, tools/scripts/${STAGE_ID}.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P5 (Content Health Check cron 0 6 * * 1); P6 (Content Health Check); manual (npm script: audit:docs-quality)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P5"
+ },
+ {
+ "path": "tools/scripts/audit-component-usage.js",
+ "script": "audit-component-usage",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Component usage auditor — scans pages for component usage patterns and reports statistics",
+ "pipeline_declared": "P5, P6",
+ "usage": "node tools/scripts/audit-component-usage.js [flags]",
+ "header": "/**\n * @script audit-component-usage\n * @category validator\n * @purpose qa:repo-health\n * @scope tools/scripts\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Component usage auditor — scans pages for component usage patterns and reports statistics\n * @pipeline P5, P6\n * @usage node tools/scripts/audit-component-usage.js [flags]\n */\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\n\nconst REPORT_PATH = path.join(__dirname, '..', '..', 'tasks', 'reports', 'repo-ops', 'component-usage-audit.json');\n\nconst V2_DOC_ROOTS = [\n 'v2/pages',\n 'v2/home',\n 'v2/solutions',\n 'v2/about',\n 'v2/community',\n 'v2/developers',\n 'v2/gateways',\n 'v2/orchestrators',\n 'v2/lpt',\n 'v2/resources',\n 'v2/internal',\n 'v2/deprecated',\n 'v2/experimental',\n 'v2/notes'\n].filter((root) => fs.existsSync(root));\n\nfunction resolveFirstExistingPath(candidates) {\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) return candidate;\n }\n return candidates[0];\n}\n\n// Get all exported components from snippets/components\nfunction getAllExportedComponents() {\n const components = new Map();\n \n // Read all .jsx files in snippets/components\n const componentFiles = execSync('find snippets/components -name \"*.jsx\" -o -name \"*.mdx\" | grep -E \"(components|\\.jsx|\\.mdx)$\"', { encoding: 'utf8' })\n .trim()\n .split('\\n')\n .filter(f => f);\n \n componentFiles.forEach(file => {\n try {\n const content = fs.readFileSync(file, 'utf8');\n \n // Find inline exports: export const ComponentName\n const inlineExports = content.matchAll(/^export (const|function|default) (\\w+)/gm);\n for (const match of inlineExports) {\n const componentName = match[2];\n const relativePath = path.relative('snippets/components', file);\n components.set(componentName, {\n name: componentName,\n file: file,\n relativePath: relativePath,\n category: getCategory(relativePath)\n });\n }\n \n // Find export statements at end: export { Component1, Component2 }\n const exportStatements = content.matchAll(/^export\\s*{([^}]+)}/gm);\n for (const match of exportStatements) {\n const exports = match[1].split(',').map(e => e.trim());\n exports.forEach(exp => {\n const componentName = exp.trim();\n if (componentName) {\n const relativePath = path.relative('snippets/components', file);\n components.set(componentName, {\n name: componentName,\n file: file,\n relativePath: relativePath,\n category: getCategory(relativePath)\n });\n }\n });\n }\n } catch (e) {\n // Skip files that can't be read\n }\n });\n \n return components;\n}\n\nfunction getCategory(filePath) {\n if (filePath.includes('primitives/')) return 'primitives';\n if (filePath.includes('display/')) return 'display';\n if (filePath.includes('content/')) return 'content';\n if (filePath.includes('layout/')) return 'layout';\n if (filePath.includes('integrations/')) return 'integrations';\n if (filePath.includes('domain/')) return 'domain';\n return 'unknown';\n}\n\n// Get components used in component library pages\nfunction getComponentLibraryComponents() {\n const used = new Set();\n const commented = new Set();\n \n const libFiles = [\n resolveFirstExistingPath([\n 'v2/resources/documentation-guide/component-library.mdx',\n 'v2/resources/documentation-guide/component-library.mdx'\n ]),\n resolveFirstExistingPath([\n 'v2/resources/documentation-guide/component-library/primitives.mdx',\n 'v2/resources/documentation-guide/component-library/primitives.mdx'\n ]),\n resolveFirstExistingPath([\n 'v2/resources/documentation-guide/component-library/display.mdx',\n 'v2/resources/documentation-guide/component-library/display.mdx'\n ]),\n resolveFirstExistingPath([\n 'v2/resources/documentation-guide/component-library/content.mdx',\n 'v2/resources/documentation-guide/component-library/content.mdx'\n ]),\n resolveFirstExistingPath([\n 'v2/resources/documentation-guide/component-library/layout.mdx',\n 'v2/resources/documentation-guide/component-library/layout.mdx'\n ]),\n resolveFirstExistingPath([\n 'v2/resources/documentation-guide/component-library/integrations.mdx',\n 'v2/resources/documentation-guide/component-library/integrations.mdx'\n ]),\n resolveFirstExistingPath([\n 'v2/resources/documentation-guide/component-library/domain.mdx',\n 'v2/resources/documentation-guide/component-library/domain.mdx'\n ])\n ];\n \n libFiles.forEach(file => {\n if (!fs.existsSync(file)) return;\n const content = fs.readFileSync(file, 'utf8');\n \n // Find imports\n const importMatches = content.matchAll(/import\\s+{([^}]+)}\\s+from\\s+['\"]\\/snippets\\/components\\/([^'\"]+)['\"]/g);\n for (const match of importMatches) {\n const imports = match[1].split(',').map(i => i.trim());\n imports.forEach(imp => {\n const name = imp.replace(/\\/\\*.*?\\*\\//g, '').trim();\n if (name && !name.startsWith('//')) {\n used.add(name);\n }\n });\n }\n \n // Find commented out imports\n const commentedMatches = content.matchAll(/\\/\\/\\s*import\\s+{([^}]+)}\\s+from/g);\n for (const match of commentedMatches) {\n const imports = match[1].split(',').map(i => i.trim());\n imports.forEach(imp => {\n const name = imp.replace(/\\/\\*.*?\\*\\//g, '').trim();\n if (name) {\n commented.add(name);\n }\n });\n }\n \n // Find JSX comments\n const jsxCommentMatches = content.matchAll(/{\\/\\*\\s*([^*]+)\\s*\\*\\/}/g);\n for (const match of jsxCommentMatches) {\n const comment = match[1];\n // Extract component names from comments\n const nameMatches = comment.matchAll(/([A-Z][a-zA-Z0-9]+)/g);\n for (const nameMatch of nameMatches) {\n const name = nameMatch[1];\n if (name.length > 3 && !['This', 'These', 'They', 'That'].includes(name)) {\n commented.add(name);\n }\n }\n }\n });\n \n return { used, commented };\n}\n\n// Get components used in v2 pages\nfunction getV2PageComponents() {\n const used = new Map(); // component -> [files]\n const searchRoots = V2_DOC_ROOTS.length > 0 ? V2_DOC_ROOTS : ['v2/pages'];\n const findCommand = `find ${searchRoots.join(' ')} -name \"*.mdx\" -type f`;\n const v2Files = execSync(findCommand, { encoding: 'utf8' })\n .trim()\n .split('\\n')\n .filter((f) => f && !f.includes('component-library'));\n \n v2Files.forEach(file => {\n try {\n const content = fs.readFileSync(file, 'utf8');\n const importMatches = content.matchAll(/import\\s+{([^}]+)}\\s+from\\s+['\"]\\/snippets\\/components\\/([^'\"]+)['\"]/g);\n \n for (const match of importMatches) {\n const imports = match[1].split(',').map(i => i.trim().replace(/\\/\\*.*?\\*\\//g, '').trim());\n imports.forEach(imp => {\n if (imp && !imp.startsWith('//')) {\n if (!used.has(imp)) {\n used.set(imp, []);\n }\n used.get(imp).push(file);\n }\n });\n }\n } catch (e) {\n // Skip\n }\n });\n \n return used;\n}\n\n// Get components used in snippets",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-schedule",
+ "caller": ".github/workflows/content-health.yml",
+ "workflow": "Content Health Check",
+ "pipeline": "P5",
+ "cron": "0 6 * * 1"
+ },
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/content-health.yml",
+ "workflow": "Content Health Check",
+ "pipeline": "P6"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/component-usage-audit.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/component-usage-audit.json",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P5 (Content Health Check cron 0 6 * * 1); P6 (Content Health Check)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P5"
+ },
+ {
+ "path": "tools/scripts/component-layout-governance.js",
+ "script": "component-layout-governance",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tools/scripts, v2, tools/config/component-layout-profile.json",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Component layout governance validator — checks v2 page layouts against approved component contracts",
+ "pipeline_declared": "P5, P6",
+ "usage": "node tools/scripts/component-layout-governance.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script component-layout-governance\n * @category validator\n * @purpose qa:repo-health\n * @scope tools/scripts, v2, tools/config/component-layout-profile.json\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Component layout governance validator — checks v2 page layouts against approved component contracts\n * @pipeline P5, P6\n * @usage node tools/scripts/component-layout-governance.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\n\nconst STAGE_ID = 'component-layout-governance';\nconst REPO_ROOT = process.cwd();\nconst DEFAULT_OUTPUT_DIR = 'tasks/reports/repo-ops';\nconst DEFAULT_PROFILE_PATH = 'tools/config/component-layout-profile.json';\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction parseArgs(argv) {\n const out = {\n scope: 'full',\n outputDir: DEFAULT_OUTPUT_DIR,\n profilePath: DEFAULT_PROFILE_PATH\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n\n if (token === '--scope') {\n out.scope = String(argv[i + 1] || out.scope).trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--scope=')) {\n out.scope = token.slice('--scope='.length).trim() || out.scope;\n continue;\n }\n if (token === '--output-dir') {\n out.outputDir = String(argv[i + 1] || out.outputDir).trim() || out.outputDir;\n i += 1;\n continue;\n }\n if (token.startsWith('--output-dir=')) {\n out.outputDir = token.slice('--output-dir='.length).trim() || out.outputDir;\n continue;\n }\n if (token === '--profile') {\n out.profilePath = String(argv[i + 1] || out.profilePath).trim() || out.profilePath;\n i += 1;\n continue;\n }\n if (token.startsWith('--profile=')) {\n out.profilePath = token.slice('--profile='.length).trim() || out.profilePath;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (!['changed', 'full'].includes(out.scope)) {\n throw new Error(`Invalid --scope: ${out.scope}`);\n }\n\n return out;\n}\n\nfunction readJsonFile(repoPath) {\n return JSON.parse(fs.readFileSync(path.join(REPO_ROOT, repoPath), 'utf8'));\n}\n\nfunction shouldExclude(relPath) {\n const rel = toPosix(relPath);\n if (!rel.startsWith('v2/')) return true;\n if (!/\\.mdx$/i.test(rel)) return true;\n if (rel.startsWith('v2/es/') || rel.startsWith('v2/fr/') || rel.startsWith('v2/cn/')) return true;\n if (rel.startsWith('v2/internal/')) return true;\n if (rel.includes('/x-')) return true;\n if (rel.includes('/_contextData_/') || rel.includes('/_context_data_/')) return true;\n if (rel.includes('/_tests-to-delete/') || rel.includes('/_move_me/')) return true;\n if (rel.endsWith('todo.mdx') || rel.endsWith('NOTES_V2.md')) return true;\n return false;\n}\n\nfunction walkFiles(dirPath, out = []) {\n if (!fs.existsSync(dirPath)) return out;\n\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = path.join(dirPath, entry.name);\n const relPath = toPosix(path.relative(REPO_ROOT, fullPath));\n\n if (entry.isDirectory()) {\n if (entry.name === '.git' || entry.name === 'node_modules') continue;\n walkFiles(fullPath, out);\n continue;\n }\n\n if (shouldExclude(relPath)) continue;\n out.push({ absPath: fullPath, relPath });\n }\n\n return out;\n}\n\nfunction getChangedFiles() {\n try {\n const output = execSync('git diff --name-only --diff-filter=ACMR HEAD', {\n cwd: REPO_ROOT,\n encoding: 'utf8'\n });\n\n return output\n .split('\\n')\n .map((line) => toPosix(line.trim()))\n .filter(Boolean)\n .filter((line) => !shouldExclude(line))\n .map((line) => ({ relPath: line, absPath: path.join(REPO_ROOT, line) }));\n } catch (_error) {\n return [];\n }\n}\n\nfunction normalizeHeading(value) {\n return String(value || '')\n .replace(/[`*_~]/g, '')\n .replace(/<[^>]+>/g, '')\n .replace(/\\s+/g, ' ')\n .trim()\n .toLowerCase();\n}\n\nfunction extractHeadings(content) {\n const lines = String(content || '').split('\\n');\n const headings = [];\n\n lines.forEach((line) => {\n const match = line.match(/^#{1,6}\\s+(.+)$/);\n if (!match) return;\n headings.push(normalizeHeading(match[1]));\n });\n\n return headings;\n}\n\nfunction extractComponents(content) {\n const names = new Set();\n const regex = /<([A-Z][A-Za-z0-9]*)\\b/g;\n let match = regex.exec(content);\n\n while (match) {\n names.add(match[1]);\n match = regex.exec(content);\n }\n\n return [...names].sort();\n}\n\nfunction findPageType(relPath, pageTypes) {\n const lowerPath = String(relPath || '').toLowerCase();\n\n for (const pageType of pageTypes) {\n const patterns = Array.isArray(pageType?.match?.path_contains) ? pageType.match.path_contains : [];\n const matched = patterns.some((pattern) => lowerPath.includes(String(pattern || '').toLowerCase()));\n if (matched) return pageType;\n }\n\n return null;\n}\n\nfunction addIssue(issues, issue) {\n issues.push({\n id: issue.id,\n title: issue.title,\n severity: issue.severity,\n evidence: issue.evidence,\n recommendation: issue.recommendation,\n path: issue.path || '',\n line: Number.isFinite(Number(issue.line)) ? Number(issue.line) : 1\n });\n}\n\nfunction summarize(issues) {\n const summary = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n total: issues.length\n };\n\n for (const issue of issues) {\n if (Object.prototype.hasOwnProperty.call(summary, issue.severity)) {\n summary[issue.severity] += 1;\n }\n }\n\n return summary;\n}\n\nfunction analyzeFile(issues, file, pageType, governedComponents) {\n const content = fs.readFileSync(file.absPath, 'utf8');\n const headings = extractHeadings(content);\n const components = extractComponents(content);\n\n const requiredSections = Array.isArray(pageType.required_sections) ? pageType.required_sections : [];\n requiredSections.forEach((section) => {\n const needle = normalizeHeading(section);",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-schedule",
+ "caller": ".github/workflows/content-health.yml",
+ "workflow": "Content Health Check",
+ "pipeline": "P5",
+ "cron": "0 6 * * 1"
+ },
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/content-health.yml",
+ "workflow": "Content Health Check",
+ "pipeline": "P6"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "audit:component-layout"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/${STAGE_ID}.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/scripts/${STAGE_ID}.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/${STAGE_ID}.json, tools/scripts/${STAGE_ID}.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P5 (Content Health Check cron 0 6 * * 1); P6 (Content Health Check); manual (npm script: audit:component-layout)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P5"
+ }
+ ],
+ "count": 7
+ },
+ "P6": {
+ "label": "P6 - On-demand",
+ "scripts": [
+ {
+ "path": "tools/scripts/snippets/generate-seo.js",
+ "script": "generate-seo",
+ "category": "generator",
+ "purpose": "feature:seo",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-R19, F-R7",
+ "purpose_statement": "SEO generator — generates SEO metadata (title, description, keywords) for v2 pages from content analysis",
+ "pipeline_declared": "P6 (on-demand, SEO refresh)",
+ "usage": "node tools/scripts/snippets/generate-seo.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script generate-seo\n * @category generator\n * @purpose feature:seo\n * @scope tools/scripts\n * @owner docs\n * @needs E-R19, F-R7\n * @purpose-statement SEO generator — generates SEO metadata (title, description, keywords) for v2 pages from content analysis\n * @pipeline P6 (on-demand, SEO refresh)\n * @usage node tools/scripts/snippets/generate-seo.js [flags]\n */\n/**\n * SEO Generator for Livepeer Documentation\n *\n * Automatically generates and updates SEO and AEO (Answer Engine Optimization)\n * metadata for MDX documentation pages:\n * - keywords: Generated from file path, title, and content\n * - description: Generated from first paragraph when missing (AEO: helps LLMs/snippets)\n * - og:image / twitter:image: Uses default or domain-specific images\n *\n * AEO = optimizing for AI/answer engines (structured metadata, clear summaries).\n *\n * Usage: node tools/scripts/snippets/generate-seo.js [--dry-run] [--file=path/to/file.mdx]\n */\n\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\n// Configuration\nconst PAGES_DIRS = [\n path.join(__dirname, \"../../v2/pages\"),\n path.join(__dirname, \"../../v2\"),\n].filter((candidate) => fs.existsSync(candidate));\nconst DEFAULT_SOCIAL_IMAGE =\n \"/snippets/assets/social/livepeer-social-preview.jpg\";\n\n// Domain-specific images (can be expanded)\nconst DOMAIN_IMAGES = {\n home: \"/snippets/assets/domain/00_HOME/social-preview-home.jpg\",\n about: \"/snippets/assets/domain/01_ABOUT/social-preview-about.jpg\",\n community: \"/snippets/assets/domain/03_COMMUNITY/social-preview-community.jpg\",\n developers:\n \"/snippets/assets/domain/02_DEVELOPERS/social-preview-developers.jpg\",\n gateways:\n \"/snippets/assets/domain/04_GATEWAYS/social-preview-gateways.jpg\",\n orchestrators:\n \"/snippets/assets/domain/05_ORCHESTRATORS/social-preview-orchestrators.jpg\",\n lpt: \"/snippets/assets/domain/06_DELEGATORS/social-preview-delegators.jpg\",\n resources: \"/snippets/assets/domain/07_RESOURCES/social-preview-resources.jpg\",\n internal: \"/snippets/assets/domain/SHARED/LivepeerDocsLogo.svg\",\n solutions: \"/snippets/assets/domain/SHARED/LivepeerDocsLogo.svg\",\n deprecated: \"/snippets/assets/domain/SHARED/LivepeerDocsLogo.svg\",\n experimental: \"/snippets/assets/domain/SHARED/LivepeerDocsLogo.svg\",\n notes: \"/snippets/assets/domain/SHARED/LivepeerDocsLogo.svg\",\n \"00_home\": \"/snippets/assets/domain/00_HOME/social-preview-home.jpg\",\n \"01_about\": \"/snippets/assets/domain/01_ABOUT/social-preview-about.jpg\",\n \"02_community\":\n \"/snippets/assets/domain/03_COMMUNITY/social-preview-community.jpg\",\n \"03_developers\":\n \"/snippets/assets/domain/02_DEVELOPERS/social-preview-developers.jpg\",\n \"04_gateways\":\n \"/snippets/assets/domain/04_GATEWAYS/social-preview-gateways.jpg\",\n \"05_orchestrators\":\n \"/snippets/assets/domain/05_ORCHESTRATORS/social-preview-orchestrators.jpg\",\n \"06_lptoken\":\n \"/snippets/assets/domain/06_DELEGATORS/social-preview-delegators.jpg\",\n \"07_resources\":\n \"/snippets/assets/domain/07_RESOURCES/social-preview-resources.jpg\",\n};\n\n// Parse command line arguments\nconst args = process.argv.slice(2);\nconst isDryRun = args.includes(\"--dry-run\");\nconst specificFile = args\n .find((arg) => arg.startsWith(\"--file=\"))\n ?.split(\"=\")[1];\n\n/**\n * Extract frontmatter from MDX content\n * Handles broken YAML like og: 'image': '' by skipping invalid lines\n */\nfunction extractFrontmatter(content) {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);\n if (!match) return { frontmatter: {}, content, hasYaml: false };\n\n const yamlContent = match[1];\n const restContent = content.slice(match[0].length);\n\n // Parse YAML-like frontmatter\n const frontmatter = {};\n const lines = yamlContent.split(\"\\n\");\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i].trim();\n if (!line) continue;\n\n // Skip broken YAML lines like: og: 'image': '' or 'og:image': ''\n // Check for multiple colons that aren't part of a valid quoted key\n const colonCount = (line.match(/:/g) || []).length;\n if (colonCount > 1) {\n // Valid format: \"og:image\": \"value\" or \"twitter:image\": \"value\"\n const validQuotedKey = line.match(/^\"([^\"]+)\":\\s*\"([^\"]*)\"/);\n if (!validQuotedKey) {\n console.log(`⚠️ Skipping invalid YAML line: ${line}`);\n continue; // Skip this broken line\n }\n // Parse the valid quoted key\n const [, key, value] = validQuotedKey;\n frontmatter[key] = value;\n continue;\n }\n\n // Handle key: value or key: [array]\n const colonIndex = line.indexOf(\":\");\n if (colonIndex === -1) continue;\n\n const key = line\n .slice(0, colonIndex)\n .trim()\n .replace(/^[\"']|[\"']$/g, \"\");\n let value = line.slice(colonIndex + 1).trim();\n\n // Handle arrays\n if (value.startsWith(\"[\") && value.endsWith(\"]\")) {\n value = value\n .slice(1, -1)\n .split(\",\")\n .map((v) => v.trim().replace(/^[\"']|[\"']$/g, \"\"));\n } else {\n value = value.replace(/^[\"']|[\"']$/g, \"\");\n }\n\n frontmatter[key] = value;\n }\n\n return { frontmatter, content: restContent, hasYaml: true, yamlContent };\n}\n\n/**\n * Generate keywords from file path and content\n */\nfunction generateKeywords(filePath, frontmatter, content) {\n const keywords = new Set();\n\n // Add 'livepeer' as base keyword\n keywords.add(\"livepeer\");\n\n // Extract from path - only use meaningful parts\n const pathParts = filePath.split(\"/\").filter((p) => {\n // Filter out system paths and keep only meaningful directory/file names\n const lower = p.toLowerCase();\n return (\n p &&\n !lower.includes(\"users\") &&\n !lower.includes(\"documents\") &&\n !lower.includes(\"livepeer-docs\") &&\n !lower.includes(\"current\") &&\n p !== \"v2\" &&\n p !== \"pages\" &&\n p !== \"tests\"\n );\n });\n pathParts.forEach((part) => {\n // Remove number prefixes and file extensions\n const cleaned = part\n .replace(/^\\d+_/, \"\")\n .replace(/\\.mdx?$/, \"\")\n .replace(/-/g, \" \")\n .toLowerCase();\n // Skip common words and user-specific paths\n if (\n cleaned &&\n cleaned.length > 2 &&\n ![\"readme\", \"summary\"].includes(cleaned)\n ) {\n keywords.add(cleaned);\n }\n });\n\n // Extract from title\n if (frontmatter.title) {\n const titleWords = frontmatter.title\n .toLowerCase()\n .replace(/[^\\w\\s]/g, \" \")\n .split(/\\s+/)\n .filter(\n (w) =>\n w.length > 3 && ![\"this\", \"that\", \"with\", \"from\", \"have\"].includes(w),\n );\n titleWords.forEach((w) => keywords.add(w));\n }\n\n // Extract from description\n if (frontmatter.description) {\n const descWords = frontmatter.description\n .toLowerCase()\n .replace(/[^\\w\\s]/g, \" \")\n .split(/\\s+/)\n .filter(\n (w) =>\n w.length > 4 &&\n ![\"this\", \"that\", \"with\", \"from\", \"have\", \"about\"].includes(w),\n );\n descWords.slice(0, 3).forEach((w) => keywords.add(w));\n }\n\n // Limit to 10 keywords\n return Array.from(keywords).slice(0, 10);\n}\n\n/**\n * Generate AEO-friendly description from content when missing.\n * Uses first paragraph or first heading + following text; keeps to 50–160 chars for SEO/AEO.\n */\nfunction generateDescription(content) {\n if (!content || typeof content !== \"string\") return null;\n // Strip frontmatter\n const withoutFrontmatter = content.replace(/^---\\n[\\s\\S]*?\\n---\\n?/, \"\");\n // First block of text: lines that are not empty and not starting with # or < or import",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/seo-refresh.yml",
+ "workflow": "SEO Metadata Refresh",
+ "pipeline": "P6"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "generate-seo"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "generate-seo:dry-run"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P6 (SEO Metadata Refresh); manual (npm script: generate-seo); manual (npm script: generate-seo:dry-run)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P6"
+ },
+ {
+ "path": "tools/scripts/i18n/generate-localized-docs-json.js",
+ "script": "generate-localized-docs-json",
+ "category": "generator",
+ "purpose": "feature:translation",
+ "scope": "docs.json, tools/scripts/i18n",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Locale docs.json generator — produces localised docs.json variants from route-map and source docs.json",
+ "pipeline_declared": "P6 (on-demand, translation pipeline)",
+ "usage": "node tools/scripts/i18n/generate-localized-docs-json.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script generate-localized-docs-json\n * @category generator\n * @purpose feature:translation\n * @scope docs.json, tools/scripts/i18n\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Locale docs.json generator — produces localised docs.json variants from route-map and source docs.json\n * @pipeline P6 (on-demand, translation pipeline)\n * @usage node tools/scripts/i18n/generate-localized-docs-json.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { parseCommonCliArgs, loadI18nConfig, buildRuntimeOptions } = require('./lib/config');\nconst { getRepoRoot, readJson, writeJson, writeTextIfChanged } = require('./lib/common');\nconst { createTranslator } = require('./lib/providers');\nconst { generateLocalizedDocsJson } = require('./lib/docs-json-localizer');\nconst { collectExistingLocalizedRouteMapEntries } = require('./lib/docs-routes');\n\nfunction printHelp() {\n console.log(\n [\n 'Usage: node tools/scripts/i18n/generate-localized-docs-json.js [options]',\n '',\n 'Options:',\n ' --languages Target languages (default from config)',\n ' --route-map Route-map artifact from translate-docs.js',\n ' --provider openrouter | mock',\n ' --write Update docs.json in place',\n ' --dry-run Compute report only (no write)',\n ' --allow-mock-write Allow mock provider in write mode (dangerous; testing only)',\n ' --report-json Write docs.json generation report',\n ' --config Alternate i18n config JSON',\n ' --help, -h Show this help'\n ].join('\\n')\n );\n}\n\nfunction assertMockWriteAllowed({ translator, runtime, config, cliArgs }) {\n const writeMode = Boolean(cliArgs.write && !runtime.dryRun);\n if (!writeMode) return;\n if (translator?.name !== 'mock') return;\n if (runtime.allowMockWrite) return;\n if (config?.qualityGates?.blockMockProviderWrites === false) return;\n throw new Error(\n 'Refusing to write docs.json with mock provider. Use --dry-run for mock testing or pass --allow-mock-write only for deliberate fixture generation.'\n );\n}\n\nfunction loadRouteMapEntries(repoRoot, routeMapPath, config, runtime) {\n if (routeMapPath) {\n const abs = path.resolve(repoRoot, routeMapPath);\n const parsed = readJson(abs);\n return {\n path: abs,\n entries: Array.isArray(parsed.entries) ? parsed.entries : [],\n metadata: {\n provider: parsed.provider || null,\n runtime: parsed.runtime || null,\n scope: parsed.scope || null,\n warnings: Array.isArray(parsed.warnings) ? parsed.warnings : []\n }\n };\n }\n return {\n path: '',\n entries: collectExistingLocalizedRouteMapEntries(repoRoot, config.generatedRoot, {\n generatedPathStyle: config.generatedPathStyle,\n sourceRoot: config.sourceRoot,\n knownLanguages: runtime.languages\n }),\n metadata: {\n provider: null,\n runtime: null,\n scope: null,\n warnings: []\n }\n };\n}\n\nasync function run(argv = process.argv.slice(2)) {\n const cliArgs = parseCommonCliArgs(argv);\n if (cliArgs.help || argv.includes('--help') || argv.includes('-h')) {\n printHelp();\n return { help: true };\n }\n const { repoRoot, configPath, config } = loadI18nConfig({\n repoRoot: getRepoRoot(),\n configPath: cliArgs.configPath || undefined\n });\n const runtime = buildRuntimeOptions(cliArgs, config);\n const translator = createTranslator({ config, providerNameOverride: runtime.providerNameOverride });\n assertMockWriteAllowed({ translator, runtime, config, cliArgs });\n const docsJsonPath = path.join(repoRoot, 'docs.json');\n const docsJson = readJson(docsJsonPath);\n const routeMap = loadRouteMapEntries(repoRoot, cliArgs.routeMapPath, config, runtime);\n const result = await generateLocalizedDocsJson({\n docsJson,\n repoRoot,\n targetLanguages: runtime.languages,\n translator,\n translationRules: config.translationRules || {},\n routeMapEntries: routeMap.entries,\n routeMapMetadata: routeMap.metadata || {},\n qualityGates: config.qualityGates || {},\n reporting: config.reporting || {},\n runtime,\n languageCodeMap: config.languageCodeMap || {},\n writeMode: Boolean(cliArgs.write && !runtime.dryRun),\n allowMockWrite: runtime.allowMockWrite\n });\n\n const nextText = `${JSON.stringify(result.docsJson, null, 2)}\\n`;\n let changed = false;\n if (cliArgs.write && !runtime.dryRun) {\n changed = writeTextIfChanged(docsJsonPath, nextText);\n }\n\n const report = {\n generatedAt: new Date().toISOString(),\n repoRoot,\n configPath,\n docsJsonPath,\n routeMapPath: routeMap.path,\n dryRun: runtime.dryRun || !cliArgs.write,\n wroteDocsJson: Boolean(cliArgs.write && !runtime.dryRun && changed),\n provider: {\n name: translator.name,\n mode: translator.name\n },\n warnings: result.report.warnings || [],\n perLanguage: result.report.perLanguage\n };\n\n if (runtime.reportJsonPath) {\n writeJson(path.resolve(repoRoot, runtime.reportJsonPath), report);\n }\n\n console.log(\n [\n 'i18n generate-localized-docs-json summary',\n `- languages: ${runtime.languages.join(', ')}`,\n `- provider: ${translator.name}`,\n `- route-map entries: ${routeMap.entries.length}`,\n `- write mode: ${cliArgs.write && !runtime.dryRun ? 'enabled' : 'disabled'}`,\n `- docs.json changed: ${changed ? 'yes' : 'no'}`\n ].join('\\n')\n );\n\n return report;\n}\n\nif (require.main === module) {\n run().catch((error) => {\n console.error(`❌ ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { run };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/translate-docs.yml",
+ "workflow": "Docs Translation Pipeline",
+ "pipeline": "P6"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/test/cli-guardrails.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "i18n:docs-json"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P6 (Docs Translation Pipeline); indirect via tools/scripts/i18n/test/cli-guardrails.test.js; manual (npm script: i18n:docs-json)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P6"
+ },
+ {
+ "path": "tools/scripts/i18n/translate-docs.js",
+ "script": "translate-docs",
+ "category": "generator",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts/i18n, docs.json, v2",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Translation generator — translates v2 MDX pages to target languages via OpenRouter API",
+ "pipeline_declared": "P6 (on-demand, translation pipeline)",
+ "usage": "node tools/scripts/i18n/translate-docs.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script translate-docs\n * @category generator\n * @purpose feature:translation\n * @scope tools/scripts/i18n, docs.json, v2\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Translation generator — translates v2 MDX pages to target languages via OpenRouter API\n * @pipeline P6 (on-demand, translation pipeline)\n * @usage node tools/scripts/i18n/translate-docs.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst { buildRuntimeOptions, loadI18nConfig, parseCommonCliArgs } = require('./lib/config');\nconst { buildV2EnglishRouteInventory, collectExistingLocalizedRouteMapEntries, selectScopeItems } = require('./lib/docs-routes');\nconst { parseMdxFileWithFrontmatter, translateFrontmatterFields } = require('./lib/frontmatter');\nconst { rewriteInternalLinksInBody, rewriteInternalLinksInJsx, translateMdxBody } = require('./lib/mdx-translate');\nconst { repoFileRelToLocalizedFileRel, normalizeRouteKey } = require('./lib/path-utils');\nconst {\n buildProvenanceComment,\n classifyLocalizedArtifactProvenance,\n injectOrReplaceProvenanceComment,\n parseProvenanceComment,\n sha256\n} = require('./lib/provenance');\nconst { hasGeneratedNote, removeGeneratedNotes } = require('../../lib/generated-file-banners');\nconst { createTranslator } = require('./lib/providers');\nconst { getRepoRoot, normalizeRepoRel, writeJson, writeTextIfChanged } = require('./lib/common');\n\nfunction printHelp() {\n console.log(\n [\n 'Usage: node tools/scripts/i18n/translate-docs.js [options]',\n '',\n 'Options:',\n ' --languages Target languages (default from tools/i18n/config.json)',\n ' --scope-mode changed_since_ref | prefixes | paths_file | full_v2_nav',\n ' --base-ref Base ref for changed_since_ref mode',\n ' --prefixes Repo prefixes for prefixes mode',\n ' --paths-file File containing explicit routes/files (paths_file mode)',\n ' --max-pages Max selected pages to process',\n ' --max-concurrency Max concurrent source files to translate (capped at 5)',\n ' --provider openrouter | mock',\n ' --dry-run Do not write localized files',\n ' --force Retranslate even when source hash matches',\n ' --allow-mock-write Allow mock provider in non-dry runs (dangerous; testing only)',\n ' --route-map Write route-map artifact JSON',\n ' --report-json Write run report JSON',\n ' --cleanup-only Skip translation and only run cleanup/link rewrite',\n ' --rewrite-links Rewrite internal links for localized files (default true in cleanup-only)',\n ' --rewrite-scope all | moved (default all for cleanup-only)',\n ' --config Alternate i18n config JSON',\n ' --help, -h Show this help'\n ].join('\\n')\n );\n}\n\nfunction isMockWriteBlocked({ translator, runtime, config }) {\n if (translator?.name !== 'mock') return false;\n if (runtime.dryRun) return false;\n if (runtime.allowMockWrite) return false;\n if (config?.qualityGates?.blockMockProviderWrites === false) return false;\n return true;\n}\n\nfunction assertMockWriteAllowed({ translator, runtime, config, action }) {\n if (!isMockWriteBlocked({ translator, runtime, config })) return;\n throw new Error(\n [\n `Refusing to run ${action} with mock provider in write mode.`,\n 'The mock provider prefixes content (for smoke tests) and can contaminate localized MDX/docs.json.',\n 'Use --dry-run for mock provider smoke tests, or pass --allow-mock-write only for deliberate fixture generation.'\n ].join(' ')\n );\n}\n\nfunction artifactClassForProvider(providerName) {\n return String(providerName || '').trim().toLowerCase() === 'mock' ? 'translated_mock' : 'translated_real';\n}\n\nfunction requestedLanguageForEffective(runtime, effectiveLanguage) {\n const hit = (runtime.languageAliasesApplied || []).find((entry) => entry.effectiveLanguage === effectiveLanguage);\n return hit?.requestedLanguage || effectiveLanguage;\n}\n\nfunction buildRouteMapIndex(entries) {\n const index = new Map();\n for (const entry of entries) {\n const sourceRoute = normalizeRouteKey(entry.sourceRoute);\n const language = String(entry.effectiveLanguage || entry.language || '').trim();\n const localizedRoute = normalizeRouteKey(entry.localizedRoute);\n if (!sourceRoute || !language || !localizedRoute) continue;\n if (!index.has(sourceRoute)) index.set(sourceRoute, new Map());\n index.get(sourceRoute).set(language, localizedRoute);\n }\n return index;\n}\n\nfunction normalizeFileKey(value) {\n return normalizeRepoRel(String(value || '').replace(/\\\\/g, '/'));\n}\n\nfunction normalizeMaxConcurrency(value, fallback = 5) {\n const parsed = Number(value);\n if (!Number.isFinite(parsed) || parsed <= 0) return fallback;\n return Math.min(parsed, fallback);\n}\n\nfunction chunkArray(items, size) {\n if (!Array.isArray(items) || items.length === 0) return [];\n const normalized = normalizeMaxConcurrency(size);\n const batches = [];\n for (let i = 0; i < items.length; i += normalized) {\n batches.push(items.slice(i, i + normalized));\n }\n return batches;\n}\n\nfunction isQuarantinedLocalizedFile(localizedFile) {\n const normalized = normalizeFileKey(localizedFile);\n return /^v2\\/[^/]+\\/group\\/x-orphaned\\//.test(normalized);\n}\n\nfunction resolveSolutionsEquivalent(localizedFile) {\n const normalized = normalizeFileKey(localizedFile);\n if (!normalized.includes('/platforms/')) return '';\n let next = normalized.replace('/platforms/', '/solutions/');\n if (next.endsWith('/ecosystem-products.mdx')) {\n next = next.replace('/ecosystem-products.mdx', '/solution-providers.mdx');\n }\n if (next.endsWith('/ecosystem-products.md')) {\n next = next.replace('/ecosystem-products.md', '/solution-providers.md');\n }\n return next;\n}\n\nfunction resolveOrphanQuarantinePath(localizedFile, language) {\n const normalized = normalizeFileKey(localizedFile);\n if (!normalized.startsWith('v2/')) return '';\n const parts = normalized.split('/');\n const lang = parts[1] || String(language || '').trim();\n const suffix = parts.slice(2).join('/');\n if (!lang || !suffix) return '';\n return `v2/${lang}/group/x-orphaned/${suffix}`;\n}\n\nfunction resolveQuarantineBasePath(localizedFile, language) {\n const normalized = normalizeFileKey(localizedFile);\n if (isQuarantinedLocalizedFile(normalized)) return normalized;\n return resolveOrphanQuarantinePath(normalized, language);\n}\n\nfunction moveLocalizedToQuarantine({ repoRoot, localizedFile, language, reason }) {\n const normalized = normalizeFileKey(localizedFile);\n const absPath = path.join(repoRoot, normalized);\n if (!fs.existsSync(absPath)) {\n return { movedTo: '', warning: `${reason} (missing file): ${localizedFile}` };\n }\n const basePath = resolveQuarantineBasePath(normalized, language);\n if (!basePath) {\n return { movedTo: '', warning: `${reason} (invalid quarantine path): ${localizedFile}` };\n }\n const raw = fs.readFileSync(absPath, 'utf8');\n const hash = sha256(`${normalized}:${raw}`).slice(0, 8);\n let target = basePath;\n const targetAbs = path.join(repoRoot, target);\n const hadCollision = fs.existsSync(targetAbs);\n fs.mkdirSync(path.dirname(targetAbs), { recursive: true });\n const forceFlag = hadCollision ? '-f ' : '';\n execSync(`git -C ${JSON.stringify(repoRoot)} mv ${forceFlag}${JSON.stringify(normalized)} ${JSON.stringify(target)}`, {\n stdio: 'ignore'\n });\n const collisionNote = hadCollision ? ` (replaced existing ${hash})` : '';\n return { movedTo: target, warning: `${reason}: ${localizedFile} -> ${target}${collisionNote}` };\n}\n\nfunction findRenameTarget(repoRoot, sourcePath) {\n if (!sourcePath) return '';\n try {\n const output = execSync(\n `git -C ${JSON.stringify(repoRoot)} log --follow --name-status --diff-filter=R -- ${JSON.stringify(sourcePath)}`,\n { encoding: 'utf8' }\n );\n for (const line of output.split('\\n')) {\n if (!line.startsWith('R')) continue;\n const parts = line.split('\\t');\n if (parts.length >= 3) {\n return normalizeRepoRel(parts[2]);\n }\n }\n } catch (_err) {\n return '';\n }\n return '';\n}\n\nconst STATIC_RENAME_MAP = new Map(\n [\n [\n 'v2/gateways/run-a-gateway/quickstart/get-AI-to-setup-the-gateway.mdx',\n 'v2/gateways/quickstart/AI-prompt.mdx'\n ],\n [\n 'v2/gateways/run-a-gateway/quickstart/quickstart-a-gateway.mdx',\n 'v2/gateways/quickstart/gateway-setup.mdx'\n ],\n [\n 'v2/gateways/references/artibtrum-exchanges.mdx',\n 'v2/gateways/references/arbitrum-exchanges.mdx'\n ],\n [\n 'v2/developers/livepeer-real-time-video/video-streaming-on-livepeer/README.mdx',\n 'v2/developers/quickstart/video/video-streaming.mdx'\n ]\n ].map(([from, to]) => [normalizeRepoRel(from), normalizeRepoRel(to)])\n);\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/translate-docs.yml",
+ "workflow": "Docs Translation Pipeline",
+ "pipeline": "P6"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/test/cli-guardrails.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "i18n:translate"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeJson"
+ }
+ ],
+ "outputs_display": "",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P6 (Docs Translation Pipeline); indirect via tools/scripts/i18n/test/cli-guardrails.test.js; manual (npm script: i18n:translate)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P6"
+ },
+ {
+ "path": "tools/scripts/i18n/validate-generated.js",
+ "script": "validate-generated",
+ "category": "validator",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts/i18n, v2",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Generated localisation validator — checks generated translated MDX files and route-map outputs for integrity",
+ "pipeline_declared": "P6",
+ "usage": "node tools/scripts/i18n/validate-generated.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script validate-generated\n * @category validator\n * @purpose feature:translation\n * @scope tools/scripts/i18n, v2\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Generated localisation validator — checks generated translated MDX files and route-map outputs for integrity\n * @pipeline P6\n * @usage node tools/scripts/i18n/validate-generated.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { parseCommonCliArgs, loadI18nConfig, buildRuntimeOptions } = require('./lib/config');\nconst { getRepoRoot, readJson, writeJson } = require('./lib/common');\nconst { collectExistingLocalizedRouteMapEntries } = require('./lib/docs-routes');\nconst { parseMdxFileWithFrontmatter } = require('./lib/frontmatter');\nconst { parseMdx } = require('./lib/mdx-parser');\nconst { parseProvenanceComment, classifyLocalizedArtifactProvenance } = require('./lib/provenance');\n\nfunction printHelp() {\n console.log(\n [\n 'Usage: node tools/scripts/i18n/validate-generated.js [options]',\n '',\n 'Options:',\n ' --languages Languages to validate (default from config)',\n ' --route-map Validate only entries from route-map artifact',\n ' --report-json Write validation report JSON',\n ' --fail-on-mock-artifacts Exit non-zero if generated files contain mock provenance',\n ' --fail-on-missing-provenance Exit non-zero if generated files are missing codex-i18n provenance',\n ' --config Alternate i18n config JSON',\n ' --help, -h Show this help'\n ].join('\\n')\n );\n}\n\nfunction loadEntries(repoRoot, routeMapPath, config, runtime) {\n if (routeMapPath) {\n const abs = path.resolve(repoRoot, routeMapPath);\n const parsed = readJson(abs);\n return { path: abs, entries: Array.isArray(parsed.entries) ? parsed.entries : [] };\n }\n return {\n path: '',\n entries: collectExistingLocalizedRouteMapEntries(repoRoot, config.generatedRoot, {\n generatedPathStyle: config.generatedPathStyle,\n sourceRoot: config.sourceRoot,\n knownLanguages: runtime.languages\n })\n };\n}\n\nasync function run(argv = process.argv.slice(2)) {\n const cliArgs = parseCommonCliArgs(argv);\n if (cliArgs.help || argv.includes('--help') || argv.includes('-h')) {\n printHelp();\n return { help: true };\n }\n const { repoRoot, config } = loadI18nConfig({\n repoRoot: getRepoRoot(),\n configPath: cliArgs.configPath || undefined\n });\n const runtime = buildRuntimeOptions(cliArgs, config);\n const routeMap = loadEntries(repoRoot, cliArgs.routeMapPath, config, runtime);\n const languageSet = new Set(runtime.languages.length ? runtime.languages : config.targetLanguages);\n\n const entries = routeMap.entries.filter(\n (entry) =>\n languageSet.has(entry.language) &&\n ['translated', 'unchanged_write', 'skipped_up_to_date', 'existing'].includes(entry.status)\n );\n\n const report = {\n generatedAt: new Date().toISOString(),\n checked: 0,\n failures: [],\n mockArtifactsDetected: 0,\n artifactsMissingProvenance: 0,\n provenanceParseFailures: 0,\n samples: []\n };\n\n function pushSample(type, payload) {\n if (report.samples.length >= 20) return;\n report.samples.push({ type, ...payload });\n }\n\n for (const entry of entries) {\n const rel = entry.localizedFile || `${entry.localizedRoute}.mdx`;\n const abs = path.join(repoRoot, rel);\n report.checked += 1;\n try {\n const raw = fs.readFileSync(abs, 'utf8');\n const provenance = parseProvenanceComment(raw);\n const hasProvenanceMarker = raw.includes('codex-i18n:');\n if (!provenance) {\n if (hasProvenanceMarker) {\n report.provenanceParseFailures += 1;\n pushSample('provenance_parse_failure', { localizedFile: rel, language: entry.language });\n } else {\n report.artifactsMissingProvenance += 1;\n pushSample('missing_provenance', { localizedFile: rel, language: entry.language });\n }\n } else {\n const meta = classifyLocalizedArtifactProvenance(provenance);\n if (meta.artifactClass === 'translated_mock') {\n report.mockArtifactsDetected += 1;\n pushSample('mock_artifact', { localizedFile: rel, language: entry.language, provider: meta.provider });\n }\n }\n const parsed = parseMdxFileWithFrontmatter(raw);\n await parseMdx(parsed.body);\n } catch (error) {\n report.failures.push({\n localizedFile: rel,\n language: entry.language,\n sourceRoute: entry.sourceRoute,\n error: error.message\n });\n }\n }\n\n if (runtime.reportJsonPath) {\n writeJson(path.resolve(repoRoot, runtime.reportJsonPath), report);\n }\n\n console.log(\n [\n 'i18n validate-generated summary',\n `- checked: ${report.checked}`,\n `- failures: ${report.failures.length}`,\n `- mock artifacts: ${report.mockArtifactsDetected}`,\n `- missing provenance: ${report.artifactsMissingProvenance}`,\n `- provenance parse failures: ${report.provenanceParseFailures}`\n ].join('\\n')\n );\n\n if (report.failures.length > 0) {\n throw new Error(`Generated MDX validation failed for ${report.failures.length} file(s)`);\n }\n if (runtime.failOnMockArtifacts && report.mockArtifactsDetected > 0) {\n throw new Error(`Generated validation found ${report.mockArtifactsDetected} mock artifact(s)`);\n }\n if (runtime.failOnMissingProvenance && report.artifactsMissingProvenance > 0) {\n throw new Error(`Generated validation found ${report.artifactsMissingProvenance} file(s) missing provenance`);\n }\n\n return report;\n}\n\nif (require.main === module) {\n run().catch((error) => {\n console.error(`❌ ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { run };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/translate-docs.yml",
+ "workflow": "Docs Translation Pipeline",
+ "pipeline": "P6"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "i18n:validate"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P6 (Docs Translation Pipeline); manual (npm script: i18n:validate)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P6"
+ },
+ {
+ "path": "tools/scripts/style-and-language-homogenizer-en-gb.js",
+ "script": "style-and-language-homogenizer-en-gb",
+ "category": "remediator",
+ "purpose": "tooling:dev-tools",
+ "scope": "tools/scripts, v2, tools/config/style-language-profile-en-gb.json",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "EN-GB style homogeniser — finds and fixes American English spellings, style guide violations, and formatting inconsistencies across v2 content",
+ "pipeline_declared": "P6 (on-demand, repair)",
+ "usage": "node tools/scripts/style-and-language-homogenizer-en-gb.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script style-and-language-homogenizer-en-gb\n * @category remediator\n * @purpose tooling:dev-tools\n * @scope tools/scripts, v2, tools/config/style-language-profile-en-gb.json\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement EN-GB style homogeniser — finds and fixes American English spellings, style guide violations, and formatting inconsistencies across v2 content\n * @pipeline P6 (on-demand, repair)\n * @usage node tools/scripts/style-and-language-homogenizer-en-gb.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\n\nconst STAGE_ID = 'style-and-language-homogenizer-en-gb';\nconst REPO_ROOT = process.cwd();\nconst DEFAULT_OUTPUT_DIR = 'tasks/reports/repo-ops';\nconst DEFAULT_PROFILE_PATH = 'tools/config/style-language-profile-en-gb.json';\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction parseArgs(argv) {\n const out = {\n scope: 'full',\n outputDir: DEFAULT_OUTPUT_DIR,\n profilePath: DEFAULT_PROFILE_PATH\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--scope') {\n out.scope = String(argv[i + 1] || out.scope).trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--scope=')) {\n out.scope = token.slice('--scope='.length).trim() || out.scope;\n continue;\n }\n if (token === '--output-dir') {\n out.outputDir = String(argv[i + 1] || out.outputDir).trim() || out.outputDir;\n i += 1;\n continue;\n }\n if (token.startsWith('--output-dir=')) {\n out.outputDir = token.slice('--output-dir='.length).trim() || out.outputDir;\n continue;\n }\n if (token === '--profile') {\n out.profilePath = String(argv[i + 1] || out.profilePath).trim() || out.profilePath;\n i += 1;\n continue;\n }\n if (token.startsWith('--profile=')) {\n out.profilePath = token.slice('--profile='.length).trim() || out.profilePath;\n continue;\n }\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (!['changed', 'full'].includes(out.scope)) {\n throw new Error(`Invalid --scope: ${out.scope}`);\n }\n\n return out;\n}\n\nfunction loadProfile(profilePath) {\n const absPath = path.resolve(REPO_ROOT, profilePath);\n return JSON.parse(fs.readFileSync(absPath, 'utf8'));\n}\n\nfunction addIssue(issues, issue) {\n issues.push({\n id: issue.id,\n title: issue.title,\n severity: issue.severity,\n evidence: issue.evidence,\n recommendation: issue.recommendation,\n path: issue.path || '',\n line: Number.isFinite(Number(issue.line)) ? Number(issue.line) : 1\n });\n}\n\nfunction summarize(issues) {\n const summary = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n total: issues.length\n };\n\n for (const issue of issues) {\n if (Object.prototype.hasOwnProperty.call(summary, issue.severity)) {\n summary[issue.severity] += 1;\n }\n }\n\n return summary;\n}\n\nfunction shouldExclude(relPath, profile) {\n const rel = toPosix(relPath);\n if (!rel.startsWith('v2/')) return true;\n\n const defaults = ['v2/es/', 'v2/fr/', 'v2/cn/', 'v2/internal/'];\n if (defaults.some((prefix) => rel.startsWith(prefix))) return true;\n if (rel.includes('/x-')) return true;\n\n const extraExcludes = Array.isArray(profile.exclude_globs) ? profile.exclude_globs : [];\n if (extraExcludes.some((glob) => glob.endsWith('/**') && rel.startsWith(glob.slice(0, -3)))) return true;\n\n return false;\n}\n\nfunction walkEnglishV2Files(profile, out = []) {\n const root = path.join(REPO_ROOT, 'v2');\n if (!fs.existsSync(root)) return out;\n\n function walk(dirPath) {\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = path.join(dirPath, entry.name);\n const relPath = toPosix(path.relative(REPO_ROOT, fullPath));\n\n if (entry.isDirectory()) {\n if (entry.name === '.git' || entry.name === 'node_modules') continue;\n walk(fullPath);\n continue;\n }\n\n if (!/\\.mdx$/i.test(entry.name)) continue;\n if (shouldExclude(relPath, profile)) continue;\n\n out.push({ absPath: fullPath, relPath });\n }\n }\n\n walk(root);\n return out;\n}\n\nfunction getChangedEnglishFiles(profile) {\n try {\n const output = execSync('git diff --name-only --diff-filter=ACMR HEAD', {\n cwd: REPO_ROOT,\n encoding: 'utf8'\n });\n\n return output\n .split('\\n')\n .map((line) => toPosix(line.trim()))\n .filter(Boolean)\n .filter((line) => line.startsWith('v2/'))\n .filter((line) => /\\.mdx$/i.test(line))\n .filter((line) => !shouldExclude(line, profile))\n .map((line) => ({ relPath: line, absPath: path.join(REPO_ROOT, line) }));\n } catch (_error) {\n return [];\n }\n}\n\nfunction getScannableLines(content) {\n const lines = String(content || '').split('\\n');\n const out = [];\n\n let inFrontmatter = lines[0] && lines[0].trim() === '---';\n let inCodeFence = false;\n\n lines.forEach((lineText, index) => {\n const trimmed = String(lineText || '').trim();\n\n if (inFrontmatter) {\n if (index > 0 && trimmed === '---') {\n inFrontmatter = false;\n }\n return;\n }\n\n if (/^```/.test(trimmed)) {\n inCodeFence = !inCodeFence;\n return;\n }\n\n if (inCodeFence) return;\n\n out.push({\n lineText,\n line: index + 1\n });\n });\n\n return out;\n}\n\nfunction shouldSkipLanguageLine(lineText) {\n const trimmed = String(lineText || '').trim();\n if (!trimmed) return true;\n if (trimmed.startsWith('import ') || trimmed.startsWith('export ')) return true;\n if (trimmed.startsWith('//')) return true;\n if (trimmed.includes('`')) return true;\n if (/\\[[^\\]]+\\]\\([^)]+\\)/.test(trimmed)) return true;\n if (/^<[^>]+>$/.test(trimmed)) return true;\n if (/^\\|.*\\|$/.test(trimmed)) return true;\n return false;\n}\n\nfunction scanForbiddenTerms(issues, file, terms) {\n if (!Array.isArray(terms) || terms.length === 0) return;",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/style-homogenise.yml",
+ "workflow": "EN-GB Style Homogenisation",
+ "pipeline": "P6"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "audit:language-en-gb"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/${STAGE_ID}.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/scripts/${STAGE_ID}.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/${STAGE_ID}.json, tools/scripts/${STAGE_ID}.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P6 (EN-GB Style Homogenisation); manual (npm script: audit:language-en-gb)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P6"
+ }
+ ],
+ "count": 5
+ },
+ "Indirect": {
+ "label": "Indirect - Library",
+ "scripts": [
+ {
+ "path": "tools/scripts/dev/seo-generator-safe.js",
+ "script": "seo-generator-safe",
+ "category": "generator",
+ "purpose": "feature:seo",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-R19, F-R7",
+ "purpose_statement": "Safe SEO generator — generates SEO metadata with dry-run and rollback safety",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/dev/seo-generator-safe.js [flags]",
+ "header": "/**\n * @script seo-generator-safe\n * @category generator\n * @purpose feature:seo\n * @scope tools/scripts\n * @owner docs\n * @needs E-R19, F-R7\n * @purpose-statement Safe SEO generator — generates SEO metadata with dry-run and rollback safety\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/dev/seo-generator-safe.js [flags]\n */\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst { execSync } = require(\"child_process\");\n\n/**\n * SAFE SEO Generator Script\n *\n * This script ONLY modifies:\n * 1. keywords field - adds if missing, updates if present\n * 2. og:image field - fixes broken syntax, adds if missing\n *\n * It PRESERVES:\n * - All other frontmatter fields (title, description, sidebarTitle, etc.)\n * - All content below frontmatter\n * - Original formatting and structure\n */\n\n// Configuration\nconst DRY_RUN = true; // Set to false to actually write files\nconst VERBOSE = true;\n\nfunction extractFrontmatter(content) {\n const match = content.match(/^---\\n([\\s\\S]*?)\\n---\\n([\\s\\S]*)$/);\n if (!match) {\n return null;\n }\n return {\n frontmatter: match[1],\n body: match[2],\n fullMatch: match[0],\n };\n}\n\nfunction generateKeywords(filePath, currentKeywords) {\n // Extract meaningful keywords from file path and name\n const parts = filePath.split(\"/\").filter((p) => p && !p.match(/^\\d+_/));\n const fileName = path.basename(filePath, \".mdx\");\n\n const keywords = new Set();\n\n // Add existing keywords if any\n if (currentKeywords && Array.isArray(currentKeywords)) {\n currentKeywords.forEach((k) => keywords.add(k));\n }\n\n // Add path-based keywords\n parts.forEach((part) => {\n const cleaned = part.replace(/[-_]/g, \" \").toLowerCase();\n if (cleaned && cleaned !== \"pages\" && cleaned !== \"mdx\") {\n keywords.add(cleaned);\n }\n });\n\n // Add filename keywords\n const fileKeywords = fileName.replace(/[-_]/g, \" \").toLowerCase();\n if (fileKeywords && fileKeywords !== \"readme\" && fileKeywords !== \"index\") {\n keywords.add(fileKeywords);\n }\n\n return Array.from(keywords).slice(0, 10); // Limit to 10 keywords\n}\n\nfunction getOgImagePath(filePath) {\n // Determine og:image based on directory structure\n const pathParts = filePath.split(\"/\");\n\n if (pathParts.includes(\"00_home\") || pathParts.includes(\"home\")) {\n return \"/snippets/assets/domain/00_HOME/social-preview-home.jpg\";\n } else if (pathParts.includes(\"01_about\") || pathParts.includes(\"about\")) {\n return \"/snippets/assets/domain/01_ABOUT/social-preview-about.jpg\";\n } else if (pathParts.includes(\"02_community\") || pathParts.includes(\"community\")) {\n return \"/snippets/assets/domain/02_COMMUNITY/social-preview-community.jpg\";\n } else if (pathParts.includes(\"03_developers\") || pathParts.includes(\"developers\")) {\n return \"/snippets/assets/domain/03_DEVELOPERS/social-preview-developers.jpg\";\n } else if (pathParts.includes(\"04_gateways\") || pathParts.includes(\"gateways\")) {\n return \"/snippets/assets/domain/04_GATEWAYS/social-preview-gateways.jpg\";\n } else if (pathParts.includes(\"05_orchestrators\") || pathParts.includes(\"orchestrators\")) {\n return \"/snippets/assets/domain/05_ORCHESTRATORS/social-preview-orchestrators.jpg\";\n } else if (pathParts.includes(\"06_lptoken\") || pathParts.includes(\"lpt\")) {\n return \"/snippets/assets/domain/06_DELEGATORS/social-preview-delegators.jpg\";\n } else if (pathParts.includes(\"07_resources\") || pathParts.includes(\"resources\")) {\n return \"/snippets/assets/domain/07_RESOURCES/social-preview-resources.jpg\";\n }\n\n return \"/snippets/assets/domain/social-preview-default.jpg\";\n}\n\nfunction parseFrontmatterFields(frontmatter) {\n const fields = {};\n const lines = frontmatter.split(\"\\n\");\n let currentField = null;\n let currentValue = [];\n let inMultiLine = false;\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n\n // Check if this is a new field\n const fieldMatch = line.match(/^([a-zA-Z_-]+|[\"']?og:image[\"']?):\\s*(.*)$/);\n\n if (fieldMatch && !inMultiLine) {\n // Save previous field\n if (currentField) {\n fields[currentField] = currentValue.join(\"\\n\");\n }\n\n currentField = fieldMatch[1].replace(/[\"']/g, \"\");\n const value = fieldMatch[2];\n\n // Check if it's a multi-line value\n if (value.includes(\"[\") && !value.includes(\"]\")) {\n inMultiLine = true;\n currentValue = [value];\n } else {\n currentValue = [value];\n inMultiLine = false;\n }\n } else if (currentField) {\n currentValue.push(line);\n if (line.includes(\"]\")) {\n inMultiLine = false;\n }\n }\n }\n\n // Save last field\n if (currentField) {\n fields[currentField] = currentValue.join(\"\\n\");\n }\n\n return fields;\n}\n\nfunction rebuildFrontmatter(fields, newKeywords, newOgImage) {\n const lines = [];\n\n // Add all fields EXCEPT keywords and og:image variants\n for (const [key, value] of Object.entries(fields)) {\n if (key === \"keywords\" || key === \"og:image\" || key === \"og\") {\n continue; // Skip, we'll add these at the end\n }\n lines.push(`${key}: ${value}`);\n }\n\n // Add keywords in proper format\n if (newKeywords && newKeywords.length > 0) {\n const keywordsJson = JSON.stringify(newKeywords);\n lines.push(`keywords: ${keywordsJson}`);\n }\n\n // Add og:image in proper format (no quotes on key)\n if (newOgImage) {\n lines.push(`og:image: '${newOgImage}'`);\n }\n\n return lines.join(\"\\n\");\n}\n\nfunction processFile(filePath) {\n const fullPath = path.join(process.cwd(), filePath);\n\n if (!fs.existsSync(fullPath)) {\n return { success: false, error: \"File not found\" };\n }\n\n const content = fs.readFileSync(fullPath, \"utf8\");\n const extracted = extractFrontmatter(content);\n\n if (!extracted) {\n return { success: false, error: \"No frontmatter found\" };\n }\n\n // Parse existing frontmatter\n const fields = parseFrontmatterFields(extracted.frontmatter);\n\n // Extract current keywords if they exist\n let currentKeywords = [];\n if (fields.keywords) {\n try {\n const keywordsStr = fields.keywords.trim();\n if (keywordsStr.startsWith(\"[\")) {\n currentKeywords = JSON.parse(keywordsStr);\n }\n } catch (e) {\n // Ignore parse errors\n }\n }\n\n // Generate new keywords\n const newKeywords = generateKeywords(filePath, currentKeywords);\n\n // Get og:image path\n const newOgImage = getOgImagePath(filePath);\n\n // Rebuild frontmatter\n const newFrontmatter = rebuildFrontmatter(fields, newKeywords, newOgImage);\n const newContent = `---\\n${newFrontmatter}\\n---\\n${extracted.body}`;\n\n // Write file if not dry run\n if (!DRY_RUN) {\n fs.writeFileSync(fullPath, newContent, \"utf8\");\n }\n\n return {\n success: true,\n filePath,\n oldKeywords: currentKeywords,\n newKeywords,\n ogImage: newOgImage,",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/dev/test-seo-generator.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/dev/test-seo-generator.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/dev/update-og-image.js",
+ "script": "update-og-image",
+ "category": "remediator",
+ "purpose": "feature:seo",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-R19, F-R7",
+ "purpose_statement": "Single OG image updater — updates og:image for one page",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/dev/update-og-image.js [flags]",
+ "header": "/**\n * @script update-og-image\n * @category remediator\n * @purpose feature:seo\n * @scope tools/scripts\n * @owner docs\n * @needs E-R19, F-R7\n * @purpose-statement Single OG image updater — updates og:image for one page\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/dev/update-og-image.js [flags]\n */\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\n\nconst NEW_OG_IMAGE = '/snippets/assets/domain/SHARED/LivepeerDocsHero.svg';\nconst EXCLUDE_FILES = ['mission-control.mdx'];\nconst V2_DOC_ROOTS = [\n 'v2/pages',\n 'v2/home',\n 'v2/solutions',\n 'v2/about',\n 'v2/community',\n 'v2/developers',\n 'v2/gateways',\n 'v2/orchestrators',\n 'v2/lpt',\n 'v2/resources',\n 'v2/internal',\n 'v2/deprecated',\n 'v2/experimental',\n 'v2/notes'\n].filter((root) => fs.existsSync(root));\n\n// Get all MDX files\nconst files = execSync(`find ${V2_DOC_ROOTS.join(' ')} -name \"*.mdx\" -type f`, { encoding: 'utf8' })\n .trim()\n .split('\\n');\n\nconsole.log(`Found ${files.length} MDX files`);\nconsole.log(`New og:image: ${NEW_OG_IMAGE}`);\nconsole.log(`Excluded: ${EXCLUDE_FILES.join(', ')}\\n`);\n\nlet changed = 0;\nlet skipped = 0;\nlet errors = 0;\n\nfiles.forEach(filePath => {\n try {\n const fileName = path.basename(filePath);\n \n // Skip excluded files\n if (EXCLUDE_FILES.includes(fileName)) {\n console.log(`⊘ ${filePath} - Excluded`);\n skipped++;\n return;\n }\n \n const content = fs.readFileSync(filePath, 'utf8');\n \n // Check if file has frontmatter with og:image\n if (!content.match(/^---\\n[\\s\\S]*?\\nog:image:/m)) {\n skipped++;\n return;\n }\n \n // Replace og:image value\n const newContent = content.replace(\n /(og:image:\\s*)[\"'].*?[\"']/g,\n `$1\"${NEW_OG_IMAGE}\"`\n );\n \n if (newContent !== content) {\n fs.writeFileSync(filePath, newContent, 'utf8');\n console.log(`✓ ${filePath}`);\n changed++;\n } else {\n skipped++;\n }\n } catch (error) {\n console.error(`✗ ${filePath}: ${error.message}`);\n errors++;\n }\n});\n\nconsole.log(`\\n========== SUMMARY ==========`);\nconsole.log(`Changed: ${changed}`);\nconsole.log(`Skipped: ${skipped}`);\nconsole.log(`Errors: ${errors}`);\nconsole.log(`=============================`);\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/dev/batch-update-og-image.sh",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/dev/batch-update-og-image.sh",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/i18n/lib/common.js",
+ "script": "common",
+ "category": "utility",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "i18n shared utilities — common helper functions for translation pipeline",
+ "pipeline_declared": "indirect — library module imported by other scripts, not invoked directly",
+ "usage": "node tools/scripts/i18n/lib/common.js [flags]",
+ "header": "/**\n * @script common\n * @category utility\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement i18n shared utilities — common helper functions for translation pipeline\n * @pipeline indirect — library module imported by other scripts, not invoked directly\n * @usage node tools/scripts/i18n/lib/common.js [flags]\n */\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction normalizeRepoRel(repoRelPath) {\n return toPosix(String(repoRelPath || '').replace(/^\\.\\//, '').replace(/^\\/+/, ''));\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction ensureDirForFile(filePath) {\n ensureDir(path.dirname(filePath));\n}\n\nfunction readJson(filePath) {\n return JSON.parse(fs.readFileSync(filePath, 'utf8'));\n}\n\nfunction writeJson(filePath, value) {\n ensureDirForFile(filePath);\n fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\\n`, 'utf8');\n}\n\nfunction writeTextIfChanged(filePath, nextContent) {\n let prev = null;\n try {\n prev = fs.readFileSync(filePath, 'utf8');\n } catch (_err) {\n prev = null;\n }\n if (prev === nextContent) return false;\n ensureDirForFile(filePath);\n fs.writeFileSync(filePath, nextContent, 'utf8');\n return true;\n}\n\nfunction getRepoRoot(cwd = process.cwd()) {\n try {\n return execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf8' }).trim();\n } catch (_err) {\n return cwd;\n }\n}\n\nfunction chunkArray(items, maxItems, maxChars = Infinity, getSize = (item) => String(item || '').length) {\n const chunks = [];\n let current = [];\n let currentChars = 0;\n\n for (const item of items) {\n const size = getSize(item);\n const wouldOverflowItems = current.length >= maxItems;\n const wouldOverflowChars = current.length > 0 && currentChars + size > maxChars;\n if (wouldOverflowItems || wouldOverflowChars) {\n chunks.push(current);\n current = [];\n currentChars = 0;\n }\n current.push(item);\n currentChars += size;\n }\n if (current.length > 0) chunks.push(current);\n return chunks;\n}\n\nfunction parseCsv(value) {\n return String(value || '')\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\nfunction isExternalHref(href) {\n return /^(https?:\\/\\/|mailto:|#)/i.test(String(href || '').trim());\n}\n\nmodule.exports = {\n chunkArray,\n ensureDir,\n ensureDirForFile,\n getRepoRoot,\n isExternalHref,\n normalizeRepoRel,\n parseCsv,\n readJson,\n toPosix,\n writeJson,\n writeTextIfChanged\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/generate-localized-docs-json.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/lib/config.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/lib/docs-json-localizer.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/lib/docs-routes.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/lib/path-utils.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/lib/provider-openrouter.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/translate-docs.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/validate-generated.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/i18n/generate-localized-docs-json.js; indirect via tools/scripts/i18n/lib/config.js; indirect via tools/scripts/i18n/lib/docs-json-localizer.js; indirect via tools/scripts/i18n/lib/docs-routes.js; indirect via tools/scripts/i18n/lib/path-utils.js; indirect via tools/scripts/i18n/lib/provider-openrouter.js; indirect via tools/scripts/i18n/translate-docs.js; indirect via tools/scripts/i18n/validate-generated.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/i18n/lib/config.js",
+ "script": "config",
+ "category": "utility",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "i18n configuration — language codes, locale paths, translation settings",
+ "pipeline_declared": "indirect — library module imported by other scripts, not invoked directly",
+ "usage": "node tools/scripts/i18n/lib/config.js [flags]",
+ "header": "/**\n * @script config\n * @category utility\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement i18n configuration — language codes, locale paths, translation settings\n * @pipeline indirect — library module imported by other scripts, not invoked directly\n * @usage node tools/scripts/i18n/lib/config.js [flags]\n */\nconst fs = require('fs');\nconst path = require('path');\nconst { getRepoRoot, parseCsv, readJson } = require('./common');\n\nconst DEFAULT_CONFIG_REPO_REL = 'tools/i18n/config.json';\n\nfunction loadI18nConfig(options = {}) {\n const repoRoot = options.repoRoot || getRepoRoot();\n const configPath = options.configPath\n ? path.resolve(repoRoot, options.configPath)\n : path.join(repoRoot, DEFAULT_CONFIG_REPO_REL);\n\n if (!fs.existsSync(configPath)) {\n throw new Error(`i18n config not found: ${configPath}`);\n }\n\n const config = readJson(configPath);\n validateI18nConfig(config, { configPath });\n return { repoRoot, configPath, config };\n}\n\nfunction validateI18nConfig(config, context = {}) {\n const configPath = context.configPath || DEFAULT_CONFIG_REPO_REL;\n if (!config || typeof config !== 'object') {\n throw new Error(`Invalid i18n config at ${configPath}: expected object`);\n }\n\n const requiredStringKeys = ['sourceLanguage', 'sourceRoot', 'generatedRoot'];\n for (const key of requiredStringKeys) {\n if (typeof config[key] !== 'string' || !config[key].trim()) {\n throw new Error(`Invalid i18n config at ${configPath}: missing \"${key}\"`);\n }\n }\n\n if (!Array.isArray(config.targetLanguages) || config.targetLanguages.length === 0) {\n throw new Error(`Invalid i18n config at ${configPath}: targetLanguages must be a non-empty array`);\n }\n\n if (config.languageCodeMap != null && (typeof config.languageCodeMap !== 'object' || Array.isArray(config.languageCodeMap))) {\n throw new Error(`Invalid i18n config at ${configPath}: languageCodeMap must be an object when provided`);\n }\n\n if (config.generatedPathStyle != null) {\n const style = String(config.generatedPathStyle || '').trim();\n if (!['v2_i18n_legacy', 'v2_language_prefix'].includes(style)) {\n throw new Error(\n `Invalid i18n config at ${configPath}: generatedPathStyle must be \"v2_i18n_legacy\" or \"v2_language_prefix\"`\n );\n }\n }\n\n if (!config.provider || typeof config.provider !== 'object') {\n throw new Error(`Invalid i18n config at ${configPath}: missing provider`);\n }\n\n const models = config.provider.modelCandidates;\n if (!Array.isArray(models) || models.length === 0 || models.some((m) => !String(m || '').trim())) {\n throw new Error(`Invalid i18n config at ${configPath}: provider.modelCandidates must be a non-empty array`);\n }\n}\n\nfunction normalizeLanguageCode(language, config = {}) {\n const raw = String(language || '').trim();\n if (!raw) return '';\n const map = config.languageCodeMap || {};\n if (typeof map[raw] === 'string' && map[raw].trim()) return map[raw].trim();\n const lowerMap = new Map(Object.entries(map).map(([k, v]) => [String(k).toLowerCase(), String(v).trim()]));\n return lowerMap.get(raw.toLowerCase()) || raw;\n}\n\nfunction normalizeLanguageList(languages, config = {}) {\n const out = [];\n const seen = new Set();\n const aliasesApplied = [];\n for (const value of languages || []) {\n const requestedLanguage = String(value || '').trim();\n if (!requestedLanguage) continue;\n const effectiveLanguage = normalizeLanguageCode(requestedLanguage, config);\n if (!effectiveLanguage) continue;\n if (requestedLanguage !== effectiveLanguage) {\n aliasesApplied.push({ requestedLanguage, effectiveLanguage });\n }\n if (seen.has(effectiveLanguage)) continue;\n seen.add(effectiveLanguage);\n out.push(effectiveLanguage);\n }\n return { languages: out, aliasesApplied };\n}\n\nfunction buildRuntimeOptions(cliArgs = {}, config = {}) {\n const defaults = config.scopeDefaults || {};\n const requestedLanguages =\n cliArgs.languages && cliArgs.languages.length ? [...cliArgs.languages] : [...(config.targetLanguages || [])];\n const normalizedLanguages = normalizeLanguageList(requestedLanguages, config);\n const runtime = {\n requestedLanguages,\n languages: normalizedLanguages.languages,\n languageAliasesApplied: normalizedLanguages.aliasesApplied,\n scopeMode: cliArgs.scopeMode || defaults.defaultMode || 'changed_since_ref',\n baseRef: cliArgs.baseRef || defaults.defaultBaseRef || 'docs-v2',\n prefixes: cliArgs.prefixes || [],\n pathsFile: cliArgs.pathsFile || '',\n maxPages: Number.isFinite(cliArgs.maxPages) ? cliArgs.maxPages : Number(defaults.maxPagesPerRun) || 50,\n dryRun: Boolean(cliArgs.dryRun),\n force: Boolean(cliArgs.force),\n routeMapPath: cliArgs.routeMapPath || '',\n reportJsonPath: cliArgs.reportJsonPath || '',\n configPath: cliArgs.configPath || '',\n providerNameOverride: cliArgs.providerName || '',\n allowMockWrite: Boolean(cliArgs.allowMockWrite),\n failOnMockArtifacts: Boolean(cliArgs.failOnMockArtifacts),\n failOnMissingProvenance: Boolean(cliArgs.failOnMissingProvenance),\n cleanupOnly: Boolean(cliArgs.cleanupOnly),\n rewriteLinks: typeof cliArgs.rewriteLinks === 'boolean' ? cliArgs.rewriteLinks : null,\n rewriteScope: cliArgs.rewriteScope || '',\n help: Boolean(cliArgs.help)\n };\n return runtime;\n}\n\nfunction parseCommonCliArgs(argv) {\n const args = {\n languages: [],\n prefixes: [],\n dryRun: false,\n force: false,\n maxPages: null,\n scopeMode: '',\n baseRef: '',\n pathsFile: '',\n routeMapPath: '',\n reportJsonPath: '',\n configPath: '',\n providerName: '',\n write: false,\n allowMockWrite: false,\n failOnMockArtifacts: false,\n failOnMissingProvenance: false,\n cleanupOnly: false,\n rewriteLinks: null,\n rewriteScope: '',\n help: false\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n const next = argv[i + 1];\n switch (token) {\n case '--languages':\n args.languages = parseCsv(next);\n i += 1;\n break;\n case '--prefixes':\n args.prefixes = parseCsv(next);\n i += 1;\n break;\n case '--scope-mode':\n args.scopeMode = String(next || '').trim();\n i += 1;\n break;\n case '--base-ref':\n args.baseRef = String(next || '').trim();\n i += 1;\n break;\n case '--paths-file':\n args.pathsFile = String(next || '').trim();\n i += 1;\n break;\n case '--max-pages': {\n const parsed = Number(next);\n if (Number.isFinite(parsed) && parsed > 0) args.maxPages = parsed;\n i += 1;\n break;\n }\n case '--route-map':\n args.routeMapPath = String(next || '').trim();\n i += 1;\n break;\n case '--report-json':\n args.reportJsonPath = String(next || '').trim();\n i += 1;\n break;\n case '--config':\n args.configPath = String(next || '').trim();\n i += 1;\n break;\n case '--provider':\n args.providerName = String(next || '').trim();\n i += 1;\n break;\n case '--dry-run':\n args.dryRun = true;\n break;\n case '--force':\n case '--force-retranslate':\n args.force = true;\n break;\n case '--write':\n args.write = true;\n break;\n case '--allow-mock-write':\n args.allowMockWrite = true;\n break;\n case '--cleanup-only':\n args.cleanupOnly = true;\n break;\n case '--rewrite-links':\n args.rewriteLinks = true;\n break;",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/enforce-generated-file-banners.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/generate-localized-docs-json.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/translate-docs.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/validate-generated.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/enforce-generated-file-banners.js; indirect via tools/scripts/i18n/generate-localized-docs-json.js; indirect via tools/scripts/i18n/translate-docs.js; indirect via tools/scripts/i18n/validate-generated.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/i18n/lib/docs-json-localizer.js",
+ "script": "docs-json-localizer",
+ "category": "utility",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "docs.json localiser engine — transforms docs.json navigation for locale variants",
+ "pipeline_declared": "indirect — library module imported by other scripts, not invoked directly",
+ "usage": "node tools/scripts/i18n/lib/docs-json-localizer.js [flags]",
+ "header": "/**\n * @script docs-json-localizer\n * @category utility\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement docs.json localiser engine — transforms docs.json navigation for locale variants\n * @pipeline indirect — library module imported by other scripts, not invoked directly\n * @usage node tools/scripts/i18n/lib/docs-json-localizer.js [flags]\n */\nconst fs = require('fs');\nconst path = require('path');\nconst { chunkArray } = require('./common');\nconst { normalizeRouteKey } = require('./path-utils');\nconst { protectText, restoreProtectedText } = require('./mdx-translate');\n\nconst MOCK_LABEL_PREFIX_RE = /^\\[[a-z]{2,5}(?:-[A-Z]{2})?\\]\\s+/;\n\nfunction deepClone(value) {\n return JSON.parse(JSON.stringify(value));\n}\n\nfunction normalizeLanguageAlias(language, languageCodeMap = {}) {\n const raw = String(language || '').trim();\n if (!raw) return '';\n if (typeof languageCodeMap[raw] === 'string' && languageCodeMap[raw].trim()) return languageCodeMap[raw].trim();\n const lower = raw.toLowerCase();\n for (const [key, value] of Object.entries(languageCodeMap || {})) {\n if (String(key).toLowerCase() === lower && String(value || '').trim()) return String(value).trim();\n }\n return raw;\n}\n\nfunction collectLabelFields(node, labelKeys, out = []) {\n if (Array.isArray(node)) {\n node.forEach((item) => collectLabelFields(item, labelKeys, out));\n return out;\n }\n if (!node || typeof node !== 'object') return out;\n\n for (const key of Object.keys(node)) {\n if (labelKeys.has(key) && typeof node[key] === 'string' && node[key].trim() && node[key].trim() !== ' ') {\n out.push({ target: node, key, value: node[key] });\n }\n collectLabelFields(node[key], labelKeys, out);\n }\n return out;\n}\n\nfunction detectMockPrefixedLabels(node, labelKeys) {\n const labels = collectLabelFields(node, labelKeys);\n const matches = [];\n for (const label of labels) {\n const value = String(label.target?.[label.key] || '');\n if (MOCK_LABEL_PREFIX_RE.test(value)) {\n matches.push({\n key: label.key,\n value\n });\n }\n }\n return matches;\n}\n\nfunction normalizeArtifactClass(entry) {\n const explicit = String(entry?.artifactClass || '').trim();\n if (explicit) return explicit;\n const provider = String(entry?.provider || '').trim().toLowerCase();\n if (provider === 'mock') return 'translated_mock';\n if (provider) return 'translated_real';\n return 'existing_unknown';\n}\n\nfunction buildRouteMapIndex(routeMapEntries) {\n const index = new Map();\n for (const rawEntry of routeMapEntries || []) {\n const status = String(rawEntry.status || '').trim();\n if (status && !['translated', 'unchanged_write', 'skipped_up_to_date', 'existing', 'translated_dry_run'].includes(status)) {\n continue;\n }\n const sourceRoute = normalizeRouteKey(rawEntry.sourceRoute);\n const language = String(rawEntry.effectiveLanguage || rawEntry.language || '').trim();\n const localizedRoute = normalizeRouteKey(rawEntry.localizedRoute);\n if (!sourceRoute || !language || !localizedRoute) continue;\n const entry = {\n ...rawEntry,\n sourceRoute,\n language,\n localizedRoute,\n effectiveLanguage: String(rawEntry.effectiveLanguage || rawEntry.language || '').trim(),\n requestedLanguage: String(rawEntry.requestedLanguage || ''),\n localizedRouteStyle: String(rawEntry.localizedRouteStyle || ''),\n provider: String(rawEntry.provider || '').trim().toLowerCase(),\n modelUsed: String(rawEntry.modelUsed || ''),\n provenanceKind: String(rawEntry.provenanceKind || ''),\n artifactClass: normalizeArtifactClass(rawEntry)\n };\n if (!index.has(sourceRoute)) index.set(sourceRoute, new Map());\n index.get(sourceRoute).set(language, entry);\n }\n return index;\n}\n\nfunction shouldPreservePageEntry(value) {\n const trimmed = String(value || '').trim();\n if (!trimmed || trimmed === ' ') return true;\n if (/^(https?:\\/\\/|mailto:|#)/i.test(trimmed)) return true;\n return false;\n}\n\nfunction buildStats() {\n return {\n rewrittenRoutes: 0,\n fallbackRoutes: 0,\n fallbackRoutesWithinSelectedScope: 0,\n fallbackRoutesOutsideSelectedScope: 0,\n rewrittenRoutesFromMockArtifacts: 0,\n blockedMockArtifactRoutes: 0,\n fallbackByLocation: new Map()\n };\n}\n\nfunction trimLabel(value) {\n const text = String(value || '').trim();\n return text || '';\n}\n\nfunction locationKeyFromTrail(trail = {}) {\n const tab = trimLabel(trail.tab) || '(tab:unknown)';\n const anchor = trimLabel(trail.anchor) || '(anchor:unknown)';\n const group = trimLabel(trail.group) || '(group:ungrouped)';\n return `${tab} > ${anchor} > ${group}`;\n}\n\nfunction recordFallback(stats, context, trail, sourceRoute) {\n stats.fallbackRoutes += 1;\n const selectedSet = context.selectedScopeRoutes;\n if (selectedSet && selectedSet.size > 0) {\n if (selectedSet.has(sourceRoute)) stats.fallbackRoutesWithinSelectedScope += 1;\n else stats.fallbackRoutesOutsideSelectedScope += 1;\n } else {\n stats.fallbackRoutesOutsideSelectedScope += 1;\n }\n\n const key = locationKeyFromTrail(trail);\n const nextCount = (stats.fallbackByLocation.get(key) || 0) + 1;\n stats.fallbackByLocation.set(key, nextCount);\n}\n\nfunction maybeRewriteRoute(entry, sourceRoute, context, stats, trail) {\n if (!entry) {\n recordFallback(stats, context, trail, sourceRoute);\n return { rewritten: false, route: null };\n }\n\n if (context.rejectMockArtifactsForRouteRewrite && entry.artifactClass === 'translated_mock') {\n stats.blockedMockArtifactRoutes += 1;\n recordFallback(stats, context, trail, sourceRoute);\n return { rewritten: false, route: null };\n }\n\n const localizedRoute = normalizeRouteKey(entry.localizedRoute);\n if (!localizedRoute) {\n recordFallback(stats, context, trail, sourceRoute);\n return { rewritten: false, route: null };\n }\n\n if (context.requireLocalizedFileForRouteRewrite) {\n const abs = path.join(context.repoRoot, `${localizedRoute}.mdx`);\n const absAlt = path.join(context.repoRoot, `${localizedRoute}.md`);\n if (!fs.existsSync(abs) && !fs.existsSync(absAlt)) {\n recordFallback(stats, context, trail, sourceRoute);\n return { rewritten: false, route: null };\n }\n }\n\n stats.rewrittenRoutes += 1;\n if (entry.artifactClass === 'translated_mock') {\n stats.rewrittenRoutesFromMockArtifacts += 1;\n }\n return { rewritten: true, route: localizedRoute };\n}\n\nfunction localizePagesInNode(node, context, stats, trail = {}) {\n if (Array.isArray(node)) {\n node.forEach((item) => localizePagesInNode(item, context, stats, trail));\n return;\n }\n if (!node || typeof node !== 'object') return;\n\n const nextTrail = { ...trail };\n if (typeof node.tab === 'string' && node.tab.trim() && node.tab.trim() !== ' ') nextTrail.tab = node.tab;\n if (typeof node.anchor === 'string' && node.anchor.trim() && node.anchor.trim() !== ' ') nextTrail.anchor = node.anchor;\n if (typeof node.group === 'string' && node.group.trim() && node.group.trim() !== ' ') nextTrail.group = node.group;\n\n if (Array.isArray(node.pages)) {\n node.pages = node.pages.map((entry) => {\n if (typeof entry !== 'string') {\n localizePagesInNode(entry, context, stats, nextTrail);\n return entry;\n }\n if (shouldPreservePageEntry(entry)) return entry;\n\n const normalized = normalizeRouteKey(entry);\n if (!normalized.startsWith('v2/')) return entry;\n\n const mapped = context.routeMapIndex.get(normalized)?.get(context.language) || null;\n const rewrite = maybeRewriteRoute(mapped, normalized, context, stats, nextTrail);\n if (!rewrite.rewritten || !rewrite.route) return entry;\n\n if (String(entry).trim().startsWith('/')) {\n return `/${rewrite.route}`;\n }\n return rewrite.route;\n });\n }\n\n Object.keys(node).forEach((key) => {\n if (key === 'pages') return;",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/generate-localized-docs-json.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/test/docs-json-localizer.test.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/i18n/generate-localized-docs-json.js; indirect via tools/scripts/i18n/test/docs-json-localizer.test.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/i18n/lib/docs-routes.js",
+ "script": "docs-routes",
+ "category": "utility",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "docs route resolver — maps page paths to locale-aware routes",
+ "pipeline_declared": "indirect — library module imported by other scripts, not invoked directly",
+ "usage": "node tools/scripts/i18n/lib/docs-routes.js [flags]",
+ "header": "/**\n * @script docs-routes\n * @category utility\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement docs route resolver — maps page paths to locale-aware routes\n * @pipeline indirect — library module imported by other scripts, not invoked directly\n * @usage node tools/scripts/i18n/lib/docs-routes.js [flags]\n */\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst { normalizeRepoRel, readJson, toPosix } = require('./common');\nconst { fileToRouteKey, normalizeRouteKey, resolveLocalizedPathStyle, resolveRouteToFile } = require('./path-utils');\nconst { parseProvenanceComment, classifyLocalizedArtifactProvenance } = require('./provenance');\n\nfunction collectPageStrings(node, out = []) {\n if (typeof node === 'string') {\n out.push(node);\n return out;\n }\n if (Array.isArray(node)) {\n node.forEach((item) => collectPageStrings(item, out));\n return out;\n }\n if (!node || typeof node !== 'object') return out;\n if (Array.isArray(node.pages)) {\n node.pages.forEach((item) => collectPageStrings(item, out));\n }\n Object.values(node).forEach((value) => collectPageStrings(value, out));\n return out;\n}\n\nfunction getDocsJson(repoRoot) {\n return readJson(path.join(repoRoot, 'docs.json'));\n}\n\nfunction getV2EnglishLanguageNode(docsJson) {\n const versions = docsJson?.navigation?.versions || [];\n const v2 = versions.find((versionNode) => versionNode?.version === 'v2');\n if (!v2) throw new Error('v2 version not found in docs.json');\n const en = (v2.languages || []).find((langNode) => langNode?.language === 'en');\n if (!en) throw new Error('v2 English language node not found in docs.json');\n return { v2, en };\n}\n\nfunction getV2EnglishRoutes(repoRoot) {\n const docsJson = getDocsJson(repoRoot);\n const { en } = getV2EnglishLanguageNode(docsJson);\n const rawEntries = collectPageStrings(en);\n const seen = new Set();\n const routes = [];\n for (const raw of rawEntries) {\n const trimmed = String(raw || '').trim();\n if (!trimmed || trimmed === ' ') continue;\n if (!trimmed.startsWith('v2/')) continue;\n const normalized = normalizeRouteKey(trimmed);\n if (!normalized || seen.has(normalized)) continue;\n seen.add(normalized);\n routes.push(normalized);\n }\n return routes;\n}\n\nfunction buildV2EnglishRouteInventory(repoRoot) {\n const routes = getV2EnglishRoutes(repoRoot);\n const items = [];\n const unresolved = [];\n\n for (const route of routes) {\n const fileRel = resolveRouteToFile(repoRoot, route);\n if (!fileRel) {\n unresolved.push(route);\n continue;\n }\n if (!fileRel.startsWith('v2/')) continue;\n if (fileRel.startsWith('v2/x-')) continue;\n items.push({\n route,\n fileRel,\n fileAbs: path.join(repoRoot, fileRel)\n });\n }\n\n return { items, unresolved };\n}\n\nfunction getChangedFilesSinceRef(repoRoot, baseRef) {\n const candidates = [`origin/${baseRef}...HEAD`, `${baseRef}...HEAD`];\n for (const range of candidates) {\n try {\n const output = execSync(`git diff --name-only ${range}`, { cwd: repoRoot, encoding: 'utf8' });\n return output\n .split('\\n')\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => normalizeRepoRel(line));\n } catch (_err) {\n // Try next range.\n }\n }\n return [];\n}\n\nfunction loadPathsFile(repoRoot, filePath) {\n const abs = path.resolve(repoRoot, filePath);\n const content = fs.readFileSync(abs, 'utf8');\n return content\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter((line) => line && !line.startsWith('#'))\n .map((line) => normalizeRepoRel(line));\n}\n\nfunction toInventoryItemFromFile(repoRoot, fileRel) {\n const normalizedFile = normalizeRepoRel(fileRel);\n const fileAbs = path.join(repoRoot, normalizedFile);\n if (!fs.existsSync(fileAbs)) return null;\n const stat = fs.statSync(fileAbs);\n if (!stat.isFile()) return null;\n if (!/\\.(md|mdx)$/i.test(normalizedFile)) return null;\n\n let route = fileToRouteKey(repoRoot, fileAbs);\n route = route.replace(/\\/README$/i, '');\n route = normalizeRouteKey(route);\n if (!route) return null;\n\n return {\n route,\n fileRel: normalizedFile,\n fileAbs\n };\n}\n\nfunction selectScopeItems(options) {\n const {\n repoRoot,\n inventory,\n scopeMode,\n baseRef,\n prefixes = [],\n pathsFile = '',\n maxPages = 50\n } = options;\n\n const items = inventory.items || [];\n let selected = [];\n\n if (scopeMode === 'full_v2_nav') {\n selected = items;\n } else if (scopeMode === 'prefixes') {\n const normalizedPrefixes = prefixes.map((p) => normalizeRepoRel(p)).filter(Boolean);\n selected = items.filter((item) =>\n normalizedPrefixes.some((prefix) => item.fileRel.startsWith(prefix) || item.route.startsWith(normalizeRouteKey(prefix)))\n );\n } else if (scopeMode === 'paths_file') {\n const entries = loadPathsFile(repoRoot, pathsFile);\n const routeSet = new Set(entries.map((entry) => normalizeRouteKey(entry)));\n const fileSet = new Set(entries.map((entry) => normalizeRepoRel(entry)));\n const selectedByFile = new Map();\n selected = items.filter((item) => routeSet.has(item.route) || fileSet.has(item.fileRel));\n for (const item of selected) selectedByFile.set(item.fileRel, item);\n\n for (const entry of entries) {\n if (!entry) continue;\n\n // Explicit file path, including non-v2 docs such as docs-guide/* or contribute/*.\n let candidate = toInventoryItemFromFile(repoRoot, entry);\n\n // Route entry outside the existing v2 inventory.\n if (!candidate) {\n const resolvedFile = resolveRouteToFile(repoRoot, entry);\n if (resolvedFile) candidate = toInventoryItemFromFile(repoRoot, resolvedFile);\n }\n\n if (!candidate) continue;\n if (selectedByFile.has(candidate.fileRel)) continue;\n selectedByFile.set(candidate.fileRel, candidate);\n selected.push(candidate);\n }\n } else {\n const changedFiles = new Set(getChangedFilesSinceRef(repoRoot, baseRef));\n selected = items.filter((item) => changedFiles.has(item.fileRel));\n }\n\n return {\n selected: selected.slice(0, Math.max(1, Number(maxPages) || 50)),\n truncated: selected.length > (Math.max(1, Number(maxPages) || 50)),\n totalCandidateCount: selected.length\n };\n}\n\nfunction collectExistingLocalizedRouteMapEntries(repoRoot, generatedRoot = 'v2/i18n', options = {}) {\n const out = [];\n const pathStyle = resolveLocalizedPathStyle(generatedRoot, options.generatedPathStyle || options.pathStyle || '');\n const sourceRoot = normalizeRepoRel(options.sourceRoot || 'v2');\n const knownLanguages = new Set((options.knownLanguages || []).map((lang) => String(lang || '').trim()).filter(Boolean));\n\n function walk(dirAbs) {\n const entries = fs.readdirSync(dirAbs, { withFileTypes: true });\n for (const entry of entries) {\n const childAbs = path.join(dirAbs, entry.name);\n if (entry.isDirectory()) {\n walk(childAbs);\n continue;\n }\n if (!/\\.(md|mdx)$/i.test(entry.name)) continue;\n const rel = normalizeRepoRel(path.relative(repoRoot, childAbs));\n const parts = rel.split('/');\n let language = '';\n let suffix = '';\n if (pathStyle === 'v2_language_prefix') {\n if (parts.length < 3 || parts[0] !== sourceRoot) continue;\n if (knownLanguages.size > 0 && !knownLanguages.has(parts[1])) continue;\n language = parts[1];\n suffix = parts.slice(2).join('/');\n } else {\n if (parts.length < 4 || parts[0] !== sourceRoot || parts[1] !== 'i18n') continue;",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/generate-localized-docs-json.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/translate-docs.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/validate-generated.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/i18n/generate-localized-docs-json.js; indirect via tools/scripts/i18n/translate-docs.js; indirect via tools/scripts/i18n/validate-generated.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/i18n/lib/frontmatter.js",
+ "script": "frontmatter",
+ "category": "utility",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Frontmatter parser/writer — reads and writes MDX frontmatter for translation",
+ "pipeline_declared": "indirect — library module imported by other scripts, not invoked directly",
+ "usage": "node tools/scripts/i18n/lib/frontmatter.js [flags]",
+ "header": "/**\n * @script frontmatter\n * @category utility\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Frontmatter parser/writer — reads and writes MDX frontmatter for translation\n * @pipeline indirect — library module imported by other scripts, not invoked directly\n * @usage node tools/scripts/i18n/lib/frontmatter.js [flags]\n */\nconst matter = require('gray-matter');\nconst { protectText, restoreProtectedText } = require('./mdx-translate');\n\nfunction parseMdxFileWithFrontmatter(rawContent) {\n const parsed = matter(String(rawContent || ''));\n const hasFrontmatter = String(rawContent || '').startsWith('---');\n return {\n data: parsed.data || {},\n body: parsed.content || '',\n hasFrontmatter,\n stringify(nextBody, nextData) {\n const body = String(nextBody || '');\n const data = nextData || {};\n if (!hasFrontmatter && Object.keys(data).length === 0) {\n return body;\n }\n return matter.stringify(body, data);\n }\n };\n}\n\nasync function translateFrontmatterFields({ data, language, translator, rules, translateKeywords = false }) {\n const next = { ...data };\n const keys = Array.isArray(rules?.translateFrontmatterKeys) ? rules.translateFrontmatterKeys : [];\n const stringTargets = [];\n const placements = [];\n\n for (const key of keys) {\n if (typeof next[key] !== 'string' || !next[key].trim()) continue;\n const protectedValue = protectText(next[key], rules);\n if (protectedValue.skip) continue;\n stringTargets.push(protectedValue.text);\n placements.push({ key, placeholders: protectedValue.placeholders });\n }\n\n if (translateKeywords && Array.isArray(next.keywords)) {\n for (let i = 0; i < next.keywords.length; i += 1) {\n if (typeof next.keywords[i] !== 'string' || !next.keywords[i].trim()) continue;\n const protectedValue = protectText(next.keywords[i], rules);\n if (protectedValue.skip) continue;\n stringTargets.push(protectedValue.text);\n placements.push({ key: 'keywords', index: i, placeholders: protectedValue.placeholders });\n }\n }\n\n if (stringTargets.length === 0) {\n return { data: next, translatedCount: 0, modelUsed: '' };\n }\n\n const result = await translator.translateStrings({\n language,\n strings: stringTargets,\n kind: 'frontmatter'\n });\n\n placements.forEach((placement, idx) => {\n const restored = restoreProtectedText(result.strings[idx], placement.placeholders);\n if (placement.key === 'keywords') {\n next.keywords[placement.index] = restored;\n } else {\n next[placement.key] = restored;\n }\n });\n\n return {\n data: next,\n translatedCount: stringTargets.length,\n modelUsed: result.modelUsed || ''\n };\n}\n\nmodule.exports = {\n parseMdxFileWithFrontmatter,\n translateFrontmatterFields\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/test/frontmatter.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/test/provenance.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/translate-docs.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/validate-generated.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/i18n/test/frontmatter.test.js; indirect via tools/scripts/i18n/test/provenance.test.js; indirect via tools/scripts/i18n/translate-docs.js; indirect via tools/scripts/i18n/validate-generated.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/i18n/lib/mdx-parser.js",
+ "script": "mdx-parser",
+ "category": "utility",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "MDX parser for i18n — extracts translatable content blocks from MDX",
+ "pipeline_declared": "indirect — library module imported by other scripts, not invoked directly",
+ "usage": "node tools/scripts/i18n/lib/mdx-parser.js [flags]",
+ "header": "/**\n * @script mdx-parser\n * @category utility\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement MDX parser for i18n — extracts translatable content blocks from MDX\n * @pipeline indirect — library module imported by other scripts, not invoked directly\n * @usage node tools/scripts/i18n/lib/mdx-parser.js [flags]\n */\nlet parserPromise = null;\n\nasync function createParser() {\n const [{ unified }, { default: remarkParse }, { default: remarkMdx }, { default: remarkGfm }] =\n await Promise.all([import('unified'), import('remark-parse'), import('remark-mdx'), import('remark-gfm')]);\n return unified().use(remarkParse).use(remarkGfm).use(remarkMdx);\n}\n\nasync function getParser() {\n if (!parserPromise) {\n parserPromise = createParser();\n }\n return parserPromise;\n}\n\nasync function parseMdx(content) {\n const parser = await getParser();\n return parser.parse(String(content || ''));\n}\n\nmodule.exports = { parseMdx };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/lib/mdx-translate.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/validate-generated.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/remediators/content/repair-spelling.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/i18n/lib/mdx-translate.js; indirect via tools/scripts/i18n/validate-generated.js; indirect via tools/scripts/remediators/content/repair-spelling.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/i18n/lib/mdx-translate.js",
+ "script": "mdx-translate",
+ "category": "utility",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "MDX translation engine — applies translations to MDX content blocks",
+ "pipeline_declared": "indirect — library module imported by other scripts, not invoked directly",
+ "usage": "node tools/scripts/i18n/lib/mdx-translate.js [flags]",
+ "header": "/**\n * @script mdx-translate\n * @category utility\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement MDX translation engine — applies translations to MDX content blocks\n * @pipeline indirect — library module imported by other scripts, not invoked directly\n * @usage node tools/scripts/i18n/lib/mdx-translate.js [flags]\n */\nconst path = require('path');\nconst { parseMdx } = require('./mdx-parser');\nconst { normalizeRouteKey } = require('./path-utils');\n\nconst BLOCKED_TYPES = new Set([\n 'code',\n 'inlineCode',\n 'html',\n 'yaml',\n 'definition',\n 'mdxjsEsm',\n 'mdxFlowExpression',\n 'mdxTextExpression',\n 'mdxJsxFlowElement',\n 'mdxJsxTextElement'\n]);\n\nfunction escapeRegExp(value) {\n return String(value || '').replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction walk(node, parents, visitor) {\n if (!node || typeof node !== 'object') return;\n visitor(node, parents);\n if (BLOCKED_TYPES.has(node.type)) return;\n\n if (Array.isArray(node.children)) {\n const nextParents = parents.concat(node);\n node.children.forEach((child) => walk(child, nextParents, visitor));\n }\n}\n\nfunction canTranslateTextNode(node, parents) {\n if (!node || node.type !== 'text') return false;\n if (!node.position || typeof node.position.start?.offset !== 'number' || typeof node.position.end?.offset !== 'number') {\n return false;\n }\n if (!String(node.value || '').trim()) return false;\n if (parents.some((p) => BLOCKED_TYPES.has(p.type))) return false;\n return true;\n}\n\nasync function extractTranslatableTextSegments(body) {\n const tree = await parseMdx(body);\n const segments = [];\n let counter = 0;\n\n walk(tree, [], (node, parents) => {\n if (!canTranslateTextNode(node, parents)) return;\n const start = node.position.start.offset;\n const end = node.position.end.offset;\n if (typeof start !== 'number' || typeof end !== 'number' || end <= start) return;\n segments.push({\n id: `seg_${counter++}`,\n start,\n end,\n text: String(node.value || '')\n });\n });\n\n segments.sort((a, b) => a.start - b.start || a.end - b.end);\n return segments.filter((segment, index) => {\n if (index === 0) return true;\n return segment.start >= segments[index - 1].end;\n });\n}\n\nfunction applySegmentTranslations(body, translatedSegments) {\n let next = String(body || '');\n const ordered = [...translatedSegments].sort((a, b) => b.start - a.start);\n for (const segment of ordered) {\n next = `${next.slice(0, segment.start)}${segment.text}${next.slice(segment.end)}`;\n }\n return next;\n}\n\nfunction protectText(text, rules = {}) {\n const source = String(text ?? '');\n const preserveRegexes = Array.isArray(rules.preserveRegexes) ? rules.preserveRegexes : [];\n for (const pattern of preserveRegexes) {\n try {\n const regex = new RegExp(pattern);\n if (regex.test(source)) {\n return { text: source, placeholders: [], skip: true };\n }\n } catch (_err) {\n // Ignore invalid regex in config.\n }\n }\n\n const placeholders = [];\n let next = source;\n let index = 0;\n\n const replaceWithPlaceholder = (regex) => {\n next = next.replace(regex, (match) => {\n const token = `__I18N_PH_${index++}__`;\n placeholders.push({ token, value: match });\n return token;\n });\n };\n\n replaceWithPlaceholder(/https?:\\/\\/[^\\s)]+/g);\n replaceWithPlaceholder(/mailto:[^\\s)]+/g);\n replaceWithPlaceholder(/\\/snippets\\/[^\\s)]+/g);\n replaceWithPlaceholder(/\\bv\\d+\\/[^\\s)]+/g);\n replaceWithPlaceholder(/`[^`]+`/g);\n\n const brandTerms = Array.isArray(rules.preserveBrandTerms) ? rules.preserveBrandTerms : [];\n for (const term of brandTerms) {\n const escaped = escapeRegExp(term);\n next = next.replace(new RegExp(`\\\\b${escaped}\\\\b`, 'g'), (match) => {\n const token = `__I18N_PH_${index++}__`;\n placeholders.push({ token, value: match });\n return token;\n });\n }\n\n return { text: next, placeholders, skip: false };\n}\n\nfunction restoreProtectedText(text, placeholders = []) {\n let next = String(text ?? '');\n for (const placeholder of placeholders) {\n if (!next.includes(placeholder.token)) {\n throw new Error(`Missing placeholder in translation output: ${placeholder.token}`);\n }\n next = next.split(placeholder.token).join(placeholder.value);\n }\n return next;\n}\n\nfunction resolveLinkUrlToRoute(sourceRoute, url) {\n const raw = String(url || '').trim();\n if (!raw || raw === ' ' || raw.startsWith('#')) return '';\n if (/^(https?:\\/\\/|mailto:)/i.test(raw)) return '';\n if (raw.startsWith('/snippets/')) return '';\n\n let target = raw;\n if (target.startsWith('/')) {\n target = target.slice(1);\n } else if (!/^v\\d+\\//i.test(target)) {\n const baseDir = path.posix.dirname(normalizeRouteKey(sourceRoute));\n target = path.posix.join(baseDir, target);\n }\n return normalizeRouteKey(target);\n}\n\nfunction buildRelativeLink(fromRoute, toRoute) {\n const fromDir = path.posix.dirname(normalizeRouteKey(fromRoute));\n const rel = path.posix.relative(fromDir, normalizeRouteKey(toRoute));\n if (!rel) return './';\n if (rel.startsWith('.')) return rel;\n return `./${rel}`;\n}\n\nfunction rewriteUrlForLanguage({ sourceRoute, sourceLocalizedRoute, url, localizedTargetRoute }) {\n const raw = String(url || '');\n const trimmed = raw.trim();\n if (!trimmed) return raw;\n if (trimmed.startsWith('/')) {\n return `/${normalizeRouteKey(localizedTargetRoute)}`;\n }\n if (/^v\\d+\\//i.test(trimmed)) {\n return normalizeRouteKey(localizedTargetRoute);\n }\n const rel = buildRelativeLink(sourceLocalizedRoute || sourceRoute, localizedTargetRoute);\n if (trimmed.startsWith('./')) return rel;\n if (trimmed.startsWith('../')) return rel;\n return rel.replace(/^\\.\\//, '');\n}\n\nfunction rewriteMarkdownLinks(body, rewriter) {\n return String(body || '').replace(/\\[([^\\]]*)\\]\\(([^)]+)\\)/g, (full, label, destination) => {\n const inner = String(destination);\n const trimmed = inner.trim();\n if (!trimmed) return full;\n\n let urlPart = trimmed;\n let remainder = '';\n const titleMatch = trimmed.match(/^(\\S+)(\\s+[\"'][\\s\\S]*[\"'])$/);\n if (titleMatch) {\n urlPart = titleMatch[1];\n remainder = titleMatch[2];\n }\n\n const rewritten = rewriter(urlPart);\n if (!rewritten || rewritten === urlPart) return full;\n return `[${label}](${rewritten}${remainder})`;\n });\n}\n\nfunction rewriteJsxHrefAttributes(body, rewriter) {\n return String(body || '').replace(/\\bhref=([\"'])([^\"']+)\\1/g, (full, quote, url) => {\n const trimmed = String(url || '').trim();\n if (!trimmed) return full;\n const rewritten = rewriter(trimmed);\n if (!rewritten || rewritten === trimmed) return full;\n return `href=${quote}${rewritten}${quote}`;\n });\n}\n\nfunction rewriteInternalLinksInBody(body, options) {\n const {\n sourceRoute,\n language,\n sourceLocalizedRoute,\n routeMapBySourceRoute\n } = options;",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/lib/docs-json-localizer.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/lib/frontmatter.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/test/mdx-translate.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/translate-docs.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/i18n/lib/docs-json-localizer.js; indirect via tools/scripts/i18n/lib/frontmatter.js; indirect via tools/scripts/i18n/test/mdx-translate.test.js; indirect via tools/scripts/i18n/translate-docs.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/i18n/lib/path-utils.js",
+ "script": "path-utils",
+ "category": "utility",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Path utilities for i18n — locale-aware path resolution and mapping",
+ "pipeline_declared": "indirect — library module imported by other scripts, not invoked directly",
+ "usage": "node tools/scripts/i18n/lib/path-utils.js [flags]",
+ "header": "/**\n * @script path-utils\n * @category utility\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Path utilities for i18n — locale-aware path resolution and mapping\n * @pipeline indirect — library module imported by other scripts, not invoked directly\n * @usage node tools/scripts/i18n/lib/path-utils.js [flags]\n */\nconst fs = require('fs');\nconst path = require('path');\nconst { normalizeRepoRel, toPosix, isExternalHref } = require('./common');\n\nfunction normalizeRouteKey(value) {\n let normalized = String(value || '').trim();\n normalized = normalized.replace(/^\\/+/, '');\n normalized = normalized.replace(/\\.(md|mdx)$/i, '');\n normalized = normalized.replace(/\\/index$/i, '');\n normalized = normalized.replace(/\\/+$/, '');\n return normalized;\n}\n\nfunction routeToFileCandidates(routeValue) {\n const route = normalizeRouteKey(routeValue);\n if (!route) return [];\n return [\n `${route}.mdx`,\n `${route}.md`,\n `${route}/index.mdx`,\n `${route}/index.md`,\n `${route}/README.mdx`,\n `${route}/README.md`\n ];\n}\n\nfunction resolveRouteToFile(repoRoot, routeValue) {\n for (const candidate of routeToFileCandidates(routeValue)) {\n const abs = path.join(repoRoot, candidate);\n if (fs.existsSync(abs)) return normalizeRepoRel(candidate);\n }\n return '';\n}\n\nfunction fileToRouteKey(repoRoot, filePath) {\n const rel = normalizeRepoRel(path.relative(repoRoot, filePath));\n return normalizeRouteKey(rel);\n}\n\nfunction resolveLocalizedPathStyle(generatedRoot, generatedPathStyle) {\n const explicit = String(generatedPathStyle || '').trim();\n if (explicit) return explicit;\n return normalizeRepoRel(generatedRoot) === 'v2' ? 'v2_language_prefix' : 'v2_i18n_legacy';\n}\n\nfunction repoFileRelToLocalizedFileRel(sourceRepoFileRel, language, generatedRoot = 'v2/i18n', generatedPathStyle = '') {\n const normalized = normalizeRepoRel(sourceRepoFileRel);\n const suffix = normalized.startsWith('v2/') ? normalized.slice('v2/'.length) : normalized;\n const style = resolveLocalizedPathStyle(generatedRoot, generatedPathStyle);\n if (style === 'v2_language_prefix') {\n return normalizeRepoRel(path.posix.join('v2', language, suffix));\n }\n return normalizeRepoRel(path.posix.join(generatedRoot, language, suffix));\n}\n\nfunction repoRouteToLocalizedRoute(sourceRoute, language, generatedRoot = 'v2/i18n', generatedPathStyle = '') {\n const route = normalizeRouteKey(sourceRoute);\n if (!route.startsWith('v2/')) return route;\n const suffix = route.slice('v2/'.length);\n const style = resolveLocalizedPathStyle(generatedRoot, generatedPathStyle);\n if (style === 'v2_language_prefix') {\n return normalizeRepoRel(path.posix.join('v2', language, suffix));\n }\n return normalizeRepoRel(path.posix.join(generatedRoot, language, suffix));\n}\n\nfunction routeStringLooksInternalDocs(routeValue) {\n const trimmed = String(routeValue || '').trim();\n if (!trimmed) return false;\n if (trimmed === ' ') return false;\n if (isExternalHref(trimmed)) return false;\n return /^v\\d+\\//i.test(trimmed) || trimmed.startsWith('/');\n}\n\nmodule.exports = {\n fileToRouteKey,\n normalizeRouteKey,\n repoFileRelToLocalizedFileRel,\n repoRouteToLocalizedRoute,\n resolveLocalizedPathStyle,\n resolveRouteToFile,\n routeStringLooksInternalDocs\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/lib/docs-json-localizer.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/lib/docs-routes.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/lib/mdx-translate.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/translate-docs.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/i18n/lib/docs-json-localizer.js; indirect via tools/scripts/i18n/lib/docs-routes.js; indirect via tools/scripts/i18n/lib/mdx-translate.js; indirect via tools/scripts/i18n/translate-docs.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/i18n/lib/provenance.js",
+ "script": "provenance",
+ "category": "utility",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Translation provenance tracker — records source, timestamp, and provider for each translated segment",
+ "pipeline_declared": "indirect — library module imported by other scripts, not invoked directly",
+ "usage": "node tools/scripts/i18n/lib/provenance.js [flags]",
+ "header": "/**\n * @script provenance\n * @category utility\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Translation provenance tracker — records source, timestamp, and provider for each translated segment\n * @pipeline indirect — library module imported by other scripts, not invoked directly\n * @usage node tools/scripts/i18n/lib/provenance.js [flags]\n */\nconst crypto = require('crypto');\n\nconst PROVENANCE_PREFIX = 'codex-i18n';\n\nfunction sha256(value) {\n return crypto.createHash('sha256').update(String(value || ''), 'utf8').digest('hex');\n}\n\nfunction buildProvenanceComment(metadata) {\n const payload = {\n kind: PROVENANCE_PREFIX,\n version: 1,\n ...metadata\n };\n const encoded = Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');\n return `{/* ${PROVENANCE_PREFIX}: ${encoded} */}`;\n}\n\nfunction parseProvenanceComment(content) {\n const source = String(content || '');\n const jsxMatch = source.match(new RegExp(`\\\\{\\\\/\\\\*\\\\s*${PROVENANCE_PREFIX}:\\\\s*([A-Za-z0-9+/=]+)\\\\s*\\\\*\\\\/\\\\}`));\n if (jsxMatch) {\n try {\n const decoded = Buffer.from(jsxMatch[1], 'base64').toString('utf8');\n const parsed = JSON.parse(decoded);\n if (parsed && parsed.kind === PROVENANCE_PREFIX) return parsed;\n } catch (_err) {\n return null;\n }\n }\n\n const legacyHtmlMatch = source.match(new RegExp(``));\n if (legacyHtmlMatch) {\n try {\n const parsed = JSON.parse(legacyHtmlMatch[1]);\n if (parsed && parsed.kind === PROVENANCE_PREFIX) return parsed;\n } catch (_err) {\n return null;\n }\n }\n\n return null;\n}\n\nfunction classifyLocalizedArtifactProvenance(provenance) {\n if (!provenance || typeof provenance !== 'object') {\n return {\n provenancePresent: false,\n provenanceKind: '',\n provider: '',\n modelUsed: '',\n sourceHash: '',\n artifactClass: 'existing_unknown'\n };\n }\n\n const provider = String(provenance.provider || '').trim().toLowerCase();\n let artifactClass = 'translated_real';\n if (provider === 'mock') artifactClass = 'translated_mock';\n if (!provider) artifactClass = 'existing_unknown';\n\n return {\n provenancePresent: true,\n provenanceKind: String(provenance.kind || ''),\n provider,\n modelUsed: String(provenance.model || ''),\n sourceHash: String(provenance.sourceHash || ''),\n artifactClass\n };\n}\n\nfunction injectOrReplaceProvenanceComment(content, comment) {\n const source = String(content || '');\n const hasFrontmatter = source.startsWith('---\\n') || source.startsWith('---\\r\\n');\n const provenancePattern = new RegExp(\n `(?:|\\\\{\\\\/\\\\*\\\\s*${PROVENANCE_PREFIX}:\\\\s*[A-Za-z0-9+/=]+\\\\s*\\\\*\\\\/\\\\})`\n );\n if (!hasFrontmatter) {\n if (provenancePattern.test(source)) {\n return source.replace(provenancePattern, comment);\n }\n return `${comment}\\n${source}`;\n }\n\n const end = findFrontmatterEndOffset(source);\n if (end < 0) {\n return `${comment}\\n${source}`;\n }\n\n const before = source.slice(0, end);\n const after = source.slice(end);\n const replacedAfter = after.replace(\n new RegExp(\n `^\\\\s*(?:|\\\\{\\\\/\\\\*\\\\s*${PROVENANCE_PREFIX}:\\\\s*[A-Za-z0-9+/=]+\\\\s*\\\\*\\\\/\\\\})\\\\s*\\\\n?`\n ),\n ''\n );\n const prefix = before.endsWith('\\n') ? before : `${before}\\n`;\n return `${prefix}${comment}\\n${replacedAfter.replace(/^\\n*/, '')}`;\n}\n\nfunction findFrontmatterEndOffset(content) {\n const text = String(content || '');\n if (!text.startsWith('---')) return -1;\n const lineBreak = text.includes('\\r\\n') ? '\\r\\n' : '\\n';\n const marker = `${lineBreak}---`;\n const markerIndex = text.indexOf(marker, 3);\n if (markerIndex < 0) return -1;\n const afterMarker = markerIndex + marker.length;\n if (text.startsWith(lineBreak, afterMarker)) return afterMarker + lineBreak.length;\n return afterMarker;\n}\n\nmodule.exports = {\n classifyLocalizedArtifactProvenance,\n buildProvenanceComment,\n injectOrReplaceProvenanceComment,\n parseProvenanceComment,\n sha256\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/enforce-generated-file-banners.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/lib/docs-routes.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/test/provenance.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/translate-docs.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/validate-generated.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/enforce-generated-file-banners.js; indirect via tools/scripts/i18n/lib/docs-routes.js; indirect via tools/scripts/i18n/test/provenance.test.js; indirect via tools/scripts/i18n/translate-docs.js; indirect via tools/scripts/i18n/validate-generated.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/i18n/lib/provider-mock.js",
+ "script": "provider-mock",
+ "category": "utility",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Mock translation provider — returns placeholder translations for testing without API calls",
+ "pipeline_declared": "indirect — library module imported by other scripts, not invoked directly",
+ "usage": "node tools/scripts/i18n/lib/provider-mock.js [flags]",
+ "header": "/**\n * @script provider-mock\n * @category utility\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Mock translation provider — returns placeholder translations for testing without API calls\n * @pipeline indirect — library module imported by other scripts, not invoked directly\n * @usage node tools/scripts/i18n/lib/provider-mock.js [flags]\n */\nfunction createMockTranslator() {\n return {\n name: 'mock',\n async translateStrings({ language, strings }) {\n return {\n strings: (strings || []).map((value) => `[${language}] ${String(value ?? '')}`),\n modelUsed: 'mock',\n attempts: 0\n };\n }\n };\n}\n\nmodule.exports = { createMockTranslator };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/lib/providers.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/i18n/lib/providers.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/i18n/lib/provider-openrouter.js",
+ "script": "provider-openrouter",
+ "category": "utility",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "OpenRouter translation provider — calls OpenRouter API for actual translations",
+ "pipeline_declared": "indirect — library module imported by other scripts, not invoked directly",
+ "usage": "node tools/scripts/i18n/lib/provider-openrouter.js [flags]",
+ "header": "/**\n * @script provider-openrouter\n * @category utility\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement OpenRouter translation provider — calls OpenRouter API for actual translations\n * @pipeline indirect — library module imported by other scripts, not invoked directly\n * @usage node tools/scripts/i18n/lib/provider-openrouter.js [flags]\n */\nconst { chunkArray } = require('./common');\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction assertOpenRouterConfig(config) {\n if (!config || typeof config !== 'object') throw new Error('Missing OpenRouter config');\n const models = config.modelCandidates || [];\n if (!Array.isArray(models) || models.length === 0) {\n throw new Error('OpenRouter modelCandidates must be a non-empty array');\n }\n}\n\nfunction getOpenRouterAuthHeaders(config = {}) {\n const apiKey = process.env.OPENROUTER_API_KEY || '';\n if (!apiKey) {\n throw new Error('OPENROUTER_API_KEY is required');\n }\n const headers = {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json'\n };\n const referer = process.env.OPENROUTER_HTTP_REFERER || process.env.GITHUB_SERVER_URL || '';\n if (referer) headers['HTTP-Referer'] = referer;\n const title = process.env.OPENROUTER_APP_NAME || 'livepeer-docs-i18n';\n if (title) headers['X-Title'] = title;\n return headers;\n}\n\nfunction buildPromptMessages({ language, kind, segments, rules }) {\n const preserveTerms = (rules?.preserveBrandTerms || []).join(', ');\n const system = [\n 'You translate documentation text while preserving Markdown/MDX syntax and placeholders.',\n 'Return strict JSON only.',\n 'Do not translate URLs, routes, code syntax, placeholders, or brand/product names that must be preserved.',\n preserveTerms ? `Preserve these exact terms: ${preserveTerms}` : ''\n ]\n .filter(Boolean)\n .join(' ');\n\n const userPayload = {\n task: 'translate_segments',\n language,\n kind,\n instructions: [\n 'Return a JSON object with key \"segments\".',\n 'segments must be an array of objects with keys: id, text.',\n 'Preserve ids exactly and keep segment count unchanged.',\n 'Preserve placeholders like __I18N_PH_#__ exactly.'\n ],\n segments: segments.map((segment) => ({ id: segment.id, text: segment.text }))\n };\n\n return [\n { role: 'system', content: system },\n { role: 'user', content: JSON.stringify(userPayload) }\n ];\n}\n\nfunction parseJsonFromModelContent(content) {\n const raw = String(content || '').trim();\n if (!raw) throw new Error('OpenRouter response content was empty');\n try {\n return JSON.parse(raw);\n } catch (_err) {\n const fenced = raw.match(/```(?:json)?\\s*([\\s\\S]*?)```/i);\n if (fenced) {\n return JSON.parse(fenced[1]);\n }\n const objectStart = raw.indexOf('{');\n const objectEnd = raw.lastIndexOf('}');\n if (objectStart >= 0 && objectEnd > objectStart) {\n return JSON.parse(raw.slice(objectStart, objectEnd + 1));\n }\n throw new Error('OpenRouter response was not valid JSON');\n }\n}\n\nfunction validateSegmentResponse(payload, requestSegments) {\n const responseSegments = payload?.segments;\n if (!Array.isArray(responseSegments)) {\n throw new Error('OpenRouter JSON response missing segments array');\n }\n if (responseSegments.length !== requestSegments.length) {\n throw new Error(`Segment count mismatch: expected ${requestSegments.length}, received ${responseSegments.length}`);\n }\n\n const byId = new Map();\n responseSegments.forEach((segment) => {\n byId.set(String(segment?.id), String(segment?.text ?? ''));\n });\n\n return requestSegments.map((segment) => {\n const key = String(segment.id);\n if (!byId.has(key)) {\n throw new Error(`Missing translated segment id \"${key}\"`);\n }\n return { id: segment.id, text: byId.get(key) };\n });\n}\n\nasync function requestCompletion({ config, model, messages }) {\n const controller = new AbortController();\n const timeoutMs = Number(config.requestTimeoutMs) || 60000;\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n try {\n const response = await fetch(`${config.baseUrl || 'https://openrouter.ai/api/v1'}/chat/completions`, {\n method: 'POST',\n headers: getOpenRouterAuthHeaders(config),\n signal: controller.signal,\n body: JSON.stringify({\n model,\n temperature: Number.isFinite(config.temperature) ? config.temperature : 0.1,\n messages,\n response_format: { type: 'json_object' }\n })\n });\n\n if (!response.ok) {\n const body = await response.text();\n const error = new Error(`OpenRouter request failed (${response.status}): ${body.slice(0, 500)}`);\n error.status = response.status;\n error.headers =\n response.headers && typeof response.headers.entries === 'function'\n ? Object.fromEntries(response.headers.entries())\n : {};\n throw error;\n }\n\n const data = await response.json();\n const content = data?.choices?.[0]?.message?.content;\n return { data, content };\n } finally {\n clearTimeout(timer);\n }\n}\n\nfunction parseResetEpochMsFromError(error) {\n if (!error) return 0;\n const headers = error.headers || {};\n const headerReset = Number(headers['x-ratelimit-reset'] || headers['X-RateLimit-Reset'] || 0);\n if (Number.isFinite(headerReset) && headerReset > 0) {\n return headerReset > 1e12 ? headerReset : headerReset * 1000;\n }\n\n const message = String(error.message || '');\n const headerMatch = message.match(/X-RateLimit-Reset\\\\?\":\\\\?\"?(\\d{10,13})\"?/i);\n if (headerMatch) {\n const parsed = Number(headerMatch[1]);\n if (Number.isFinite(parsed) && parsed > 0) {\n return parsed > 1e12 ? parsed : parsed * 1000;\n }\n }\n return 0;\n}\n\nfunction getRetryDelayMs(error, attempt) {\n const status = Number(error?.status);\n const baseDelay = Math.min(30000, 2000 * 2 ** Math.max(0, attempt - 1));\n if (status !== 429) return baseDelay;\n\n const resetEpochMs = parseResetEpochMsFromError(error);\n if (resetEpochMs > 0) {\n const now = Date.now();\n const untilReset = resetEpochMs - now;\n if (untilReset > 0) {\n return Math.min(70000, untilReset + 750);\n }\n }\n return Math.min(60000, Math.max(baseDelay, 10000));\n}\n\nfunction isRetryableError(error) {\n if (!error) return false;\n if (error.name === 'AbortError') return true;\n const status = Number(error.status);\n const message = String(error.message || '');\n if (/OpenRouter response (content was empty|was not valid JSON)/i.test(message)) {\n return true;\n }\n if (/Segment count mismatch|Missing translated segment id/i.test(message)) {\n return true;\n }\n return status === 429 || (status >= 500 && status < 600);\n}\n\nfunction isModelFailoverError(error) {\n if (!error) return false;\n const status = Number(error.status);\n const message = String(error.message || '');\n if (/OpenRouter response (content was empty|was not valid JSON)/i.test(message)) {\n return true;\n }\n if (/Segment count mismatch|Missing translated segment id/i.test(message)) {\n return true;\n }\n if (\n status === 400 &&\n /(Developer instruction is not enabled|INVALID_ARGUMENT|response_format)/i.test(message)\n ) {\n return true;\n }\n // OpenRouter/provider-specific capacity or billing responses can be model/provider dependent.\n return status === 402 || status === 403 || status === 429 || (status >= 500 && status < 600);\n}\n\nfunction createOpenRouterTranslator(providerConfig, translationRules = {}) {\n assertOpenRouterConfig(providerConfig);",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/lib/providers.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/test/provider-openrouter.test.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/i18n/lib/providers.js; indirect via tools/scripts/i18n/test/provider-openrouter.test.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/i18n/lib/providers.js",
+ "script": "providers",
+ "category": "utility",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Provider registry — selects translation provider (OpenRouter or mock) based on configuration",
+ "pipeline_declared": "indirect — library module imported by other scripts, not invoked directly",
+ "usage": "node tools/scripts/i18n/lib/providers.js [flags]",
+ "header": "/**\n * @script providers\n * @category utility\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Provider registry — selects translation provider (OpenRouter or mock) based on configuration\n * @pipeline indirect — library module imported by other scripts, not invoked directly\n * @usage node tools/scripts/i18n/lib/providers.js [flags]\n */\nconst { createOpenRouterTranslator } = require('./provider-openrouter');\nconst { createMockTranslator } = require('./provider-mock');\n\nfunction createTranslator({ config, providerNameOverride = '' }) {\n const providerConfig = { ...(config.provider || {}) };\n const name = String(providerNameOverride || providerConfig.name || '').trim().toLowerCase();\n\n if (name === 'mock') {\n return createMockTranslator();\n }\n if (name === 'openrouter' || !name) {\n return createOpenRouterTranslator(providerConfig, config.translationRules || {});\n }\n\n throw new Error(`Unsupported i18n provider: ${name}`);\n}\n\nmodule.exports = { createTranslator };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/generate-localized-docs-json.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/test/provider-openrouter.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/translate-docs.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/i18n/generate-localized-docs-json.js; indirect via tools/scripts/i18n/test/provider-openrouter.test.js; indirect via tools/scripts/i18n/translate-docs.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/codex/task-preflight.js",
+ "script": "codex/task-preflight",
+ "category": "generator",
+ "purpose": "governance:agent-governance",
+ "scope": "tools/scripts/codex, .codex/task-contract.yaml, .codex/locks-local",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Codex task preflight — generates task setup files and validates preconditions",
+ "pipeline_declared": "manual — codex setup tool referenced by .githooks/pre-commit guidance, not auto-executed",
+ "usage": "node tools/scripts/codex/task-preflight.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script codex/task-preflight\n * @category generator\n * @purpose governance:agent-governance\n * @scope tools/scripts/codex, .codex/task-contract.yaml, .codex/locks-local\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Codex task preflight — generates task setup files and validates preconditions\n * @pipeline manual — codex setup tool referenced by .githooks/pre-commit guidance, not auto-executed\n * @usage node tools/scripts/codex/task-preflight.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst { spawnSync } = require('child_process');\n\nconst DEFAULT_BASE = 'docs-v2';\nconst DEFAULT_CONTRACT = '.codex/task-contract.yaml';\nconst LOCK_DIR_REL = '.codex/locks-local';\n\nconst REPO_ROOT = getRepoRoot();\n\nfunction getRepoRoot() {\n const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n if (result.status === 0 && String(result.stdout || '').trim()) {\n return String(result.stdout || '').trim();\n }\n return process.cwd();\n}\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction slugify(value) {\n return String(value || '')\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, '-')\n .replace(/-{2,}/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\nfunction runGit(args, options = {}) {\n const result = spawnSync('git', args, { cwd: REPO_ROOT, encoding: 'utf8', ...options });\n if (result.status !== 0) {\n const stderr = String(result.stderr || '').trim();\n const stdout = String(result.stdout || '').trim();\n throw new Error(stderr || stdout || `git ${args.join(' ')} failed`);\n }\n return String(result.stdout || '').trim();\n}\n\nfunction tryRunGit(args) {\n try {\n return runGit(args);\n } catch (_error) {\n return '';\n }\n}\n\nfunction parseArgs(argv) {\n const args = {\n taskId: null,\n slug: '',\n base: DEFAULT_BASE,\n scope: [],\n scopeFile: '',\n owner: `${process.env.USER || 'unknown'}@${os.hostname()}`,\n worktree: '',\n expiresHours: 8,\n contractPath: DEFAULT_CONTRACT,\n dryRun: false\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n\n if (token === '--task') {\n args.taskId = Number(argv[i + 1]);\n i += 1;\n continue;\n }\n if (token.startsWith('--task=')) {\n args.taskId = Number(token.slice('--task='.length));\n continue;\n }\n if (token === '--slug') {\n args.slug = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--slug=')) {\n args.slug = token.slice('--slug='.length).trim();\n continue;\n }\n if (token === '--base') {\n args.base = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--base=')) {\n args.base = token.slice('--base='.length).trim();\n continue;\n }\n if (token === '--scope') {\n args.scope = String(argv[i + 1] || '')\n .split(',')\n .map((entry) => toPosix(entry.trim()))\n .filter(Boolean);\n i += 1;\n continue;\n }\n if (token.startsWith('--scope=')) {\n args.scope = token\n .slice('--scope='.length)\n .split(',')\n .map((entry) => toPosix(entry.trim()))\n .filter(Boolean);\n continue;\n }\n if (token === '--scope-file') {\n args.scopeFile = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--scope-file=')) {\n args.scopeFile = token.slice('--scope-file='.length).trim();\n continue;\n }\n if (token === '--owner') {\n args.owner = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--owner=')) {\n args.owner = token.slice('--owner='.length).trim();\n continue;\n }\n if (token === '--worktree') {\n args.worktree = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--worktree=')) {\n args.worktree = token.slice('--worktree='.length).trim();\n continue;\n }\n if (token === '--expires-hours') {\n args.expiresHours = Number(argv[i + 1]);\n i += 1;\n continue;\n }\n if (token.startsWith('--expires-hours=')) {\n args.expiresHours = Number(token.slice('--expires-hours='.length));\n continue;\n }\n if (token === '--contract') {\n args.contractPath = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--contract=')) {\n args.contractPath = token.slice('--contract='.length).trim();\n continue;\n }\n if (token === '--dry-run') {\n args.dryRun = true;\n continue;\n }\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n return args;\n}\n\nfunction usage() {\n console.log('Usage: node tools/scripts/codex/task-preflight.js --task --slug (--scope | --scope-file ) [--base docs-v2] [--worktree ] [--dry-run]');\n}\n\nfunction readScopeFromFile(scopeFile) {\n if (!scopeFile) return [];\n const abs = path.resolve(REPO_ROOT, scopeFile);\n if (!fs.existsSync(abs)) {\n throw new Error(`Scope file not found: ${toPosix(path.relative(REPO_ROOT, abs))}`);\n }\n return fs\n .readFileSync(abs, 'utf8')\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter((line) => line && !line.startsWith('#'))\n .map((line) => toPosix(line));\n}\n\nfunction writeYamlContract(contractAbs, data) {\n const content = [\n `task_id: ${data.taskId}`,\n `branch: ${data.branch}`,\n `base_branch: ${data.base}`,\n 'scope_in:',\n ...data.scope.map((entry) => ` - ${entry}`),\n 'scope_out: []',\n 'allowed_generated:',\n ' - .codex/pr-body.generated.md',\n 'acceptance_checks:',\n ` - node tests/run-pr-checks.js --base-ref ${data.base}`,\n 'risk_flags:',\n ' - workflow-governance',\n 'follow_up_issues: []',\n ''\n ].join('\\n');\n\n fs.mkdirSync(path.dirname(contractAbs), { recursive: true });",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": ".githooks/pre-commit",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/scripts/codex/.codex/locks-local",
+ "type": "directory",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": "tools/scripts/codex/${lock.lock_id}.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": ", tools/scripts/codex/.codex/locks-local, tools/scripts/codex/${lock.lock_id}.json",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via .githooks/pre-commit",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-index-utils.js",
+ "script": "docs-index-utils",
+ "category": "utility",
+ "purpose": "governance:index-management",
+ "scope": "tools/lib, tools/scripts, v2",
+ "owner": "docs",
+ "needs": "R-R16, R-R17",
+ "purpose_statement": "Shared utilities for docs-index.json generation — path resolution, frontmatter extraction, index merging",
+ "pipeline_declared": "indirect — library module imported by other scripts, not invoked directly",
+ "usage": "node tools/lib/docs-index-utils.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script docs-index-utils\n * @category utility\n * @purpose governance:index-management\n * @scope tools/lib, tools/scripts, v2\n * @owner docs\n * @needs R-R16, R-R17\n * @purpose-statement Shared utilities for docs-index.json generation — path resolution, frontmatter extraction, index merging\n * @pipeline indirect — library module imported by other scripts, not invoked directly\n * @usage node tools/lib/docs-index-utils.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst yaml = require('js-yaml');\n\nconst DOMAIN_RENAME_MAP = {\n '00_home': 'home',\n '010_products': 'solutions',\n '01_about': 'about',\n '02_community': 'community',\n '03_developers': 'developers',\n '04_gateways': 'gateways',\n '05_orchestrators': 'orchestrators',\n '06_lptoken': 'lpt',\n '07_resources': 'resources',\n '09_internal': 'internal',\n deprecated: 'deprecated',\n experimental: 'experimental',\n notes: 'notes'\n};\n\nconst DOMAIN_REVERSE_MAP = Object.fromEntries(\n Object.entries(DOMAIN_RENAME_MAP).map(([legacy, modern]) => [modern, legacy])\n);\n\nconst DIFFICULTY_BEGINNER = ['quickstart', 'getting-started', 'overview', 'intro', 'primer'];\nconst DIFFICULTY_ADVANCED = ['advanced', 'architecture', 'reference', 'api-reference', 'cli', 'sdk'];\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_err) {\n return process.cwd();\n }\n}\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction normalizeRel(relPath) {\n return toPosix(relPath).replace(/^\\.\\//, '').replace(/^\\//, '');\n}\n\nfunction normalizeDocPathForUrl(relPath) {\n let normalized = normalizeRel(relPath);\n normalized = normalized.replace(/\\.(md|mdx)$/i, '');\n normalized = normalized.replace(/\\/index$/i, '');\n normalized = normalized.replace(/\\/+$/, '');\n return normalized;\n}\n\nfunction mapLegacyRepoPathToModern(repoRelPath) {\n const normalized = normalizeRel(repoRelPath);\n if (!normalized.startsWith('v2/pages/')) return normalized;\n const rest = normalized.slice('v2/pages/'.length);\n const [legacyDomain, ...tail] = rest.split('/').filter(Boolean);\n const modernDomain = DOMAIN_RENAME_MAP[legacyDomain] || legacyDomain;\n return normalizeRel(path.posix.join('v2', modernDomain, ...tail));\n}\n\nfunction mapModernRepoPathToLegacy(repoRelPath) {\n const normalized = normalizeRel(repoRelPath);\n if (!normalized.startsWith('v2/') || normalized.startsWith('v2/pages/')) return normalized;\n const rest = normalized.slice('v2/'.length);\n const [modernDomain, ...tail] = rest.split('/').filter(Boolean);\n const legacyDomain = DOMAIN_REVERSE_MAP[modernDomain];\n if (!legacyDomain) return normalized;\n return normalizeRel(path.posix.join('v2/pages', legacyDomain, ...tail));\n}\n\nfunction resolveDocPathCandidates(pagePath) {\n const normalized = normalizeRel(pagePath);\n const candidates = [];\n\n function pushVariants(p) {\n candidates.push(`${p}.mdx`);\n candidates.push(`${p}.md`);\n candidates.push(path.posix.join(p, 'index.mdx'));\n candidates.push(path.posix.join(p, 'index.md'));\n }\n\n pushVariants(normalized);\n const modern = mapLegacyRepoPathToModern(normalized);\n if (modern !== normalized) pushVariants(modern);\n const legacy = mapModernRepoPathToLegacy(normalized);\n if (legacy !== normalized) pushVariants(legacy);\n\n return [...new Set(candidates.map((candidate) => normalizeRel(candidate)))];\n}\n\nfunction resolveDocPath(pagePath, repoRoot) {\n const root = repoRoot || getRepoRoot();\n const candidates = resolveDocPathCandidates(pagePath);\n for (const candidate of candidates) {\n const absPath = path.join(root, candidate);\n if (fs.existsSync(absPath) && fs.statSync(absPath).isFile()) {\n return candidate;\n }\n }\n return null;\n}\n\nfunction extractFrontmatter(content) {\n const rawContent = String(content || '');\n const match = rawContent.match(/^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n?/);\n if (!match) {\n return { exists: false, data: null, raw: null, body: rawContent };\n }\n try {\n return {\n exists: true,\n data: yaml.load(match[1]) || {},\n raw: match[1],\n body: rawContent.slice(match[0].length)\n };\n } catch (error) {\n return {\n exists: true,\n data: null,\n raw: match[1],\n error: error.message,\n body: rawContent.slice(match[0].length)\n };\n }\n}\n\nfunction stripCodeFences(lines) {\n const result = [];\n let inFence = false;\n let fenceToken = null;\n for (const line of lines) {\n const fenceMatch = line.match(/^(```|~~~)/);\n if (fenceMatch) {\n const token = fenceMatch[1];\n if (!inFence) {\n inFence = true;\n fenceToken = token;\n } else if (token === fenceToken) {\n inFence = false;\n fenceToken = null;\n }\n continue;\n }\n if (!inFence) result.push(line);\n }\n return result;\n}\n\nfunction extractHeadings(body) {\n const lines = stripCodeFences(String(body || '').split('\\n'));\n return lines\n .map((line) => line.match(/^(#{1,6})\\s+(.+)$/))\n .filter(Boolean)\n .map((match) => ({ level: match[1].length, text: match[2].trim() }));\n}\n\nfunction extractCodeLanguages(content) {\n const lines = String(content || '').split('\\n');\n const languages = new Set();\n let inFence = false;\n let fenceToken = null;\n\n for (const line of lines) {\n const fenceMatch = line.match(/^(\\s*)(```|~~~)([^\\s]*)/);\n if (fenceMatch) {\n const token = fenceMatch[2];\n const lang = fenceMatch[3] ? fenceMatch[3].trim() : '';\n if (!inFence) {\n inFence = true;\n fenceToken = token;\n if (lang) languages.add(lang);\n } else if (token === fenceToken) {\n inFence = false;\n fenceToken = null;\n }\n }\n }\n\n return [...languages];\n}\n\nfunction stripForWordCount(body) {\n return String(body || '')\n .replace(/^import\\s.+$/gm, ' ')\n .replace(/```[\\s\\S]*?```/g, ' ')\n .replace(/~~~[\\s\\S]*?~~~/g, ' ')\n .replace(/`[^`]+`/g, ' ')\n .replace(/\\{\\/\\*[\\s\\S]*?\\*\\/\\}/g, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\[[^\\]]+\\]\\([^)]+\\)/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction countWords(text) {\n return String(text || '')\n .split(/\\s+/)\n .filter(Boolean).length;\n}\n\nfunction buildGitLastModifiedMap(repoRoot) {\n const root = repoRoot || getRepoRoot();\n const trackedRoots = ['v2/', 'docs-guide/', 'contribute/'];\n try {\n const output = execSync('git log --name-only --format=%cI -- v2 docs-guide contribute', {\n cwd: root,",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/notion/sync-v2-en-canonical.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/generate-ai-sitemap.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/generate-content-gap-reconciliation.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/generate-docs-index.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/notion/sync-v2-en-canonical.js; indirect via tools/scripts/generate-ai-sitemap.js; indirect via tools/scripts/generate-content-gap-reconciliation.js; indirect via tools/scripts/generate-docs-index.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/generated-file-banners.js",
+ "script": "generated-file-banners",
+ "category": "utility",
+ "purpose": "governance:index-management",
+ "scope": "full-repo",
+ "owner": "docs",
+ "needs": "R-R16, R-R17",
+ "purpose_statement": "Generated file banner template — provides standard banner text for auto-generated files",
+ "pipeline_declared": "indirect — library module imported by other scripts, not invoked directly",
+ "usage": "node tools/lib/generated-file-banners.js [flags]",
+ "header": "/**\n * @script generated-file-banners\n * @category utility\n * @purpose governance:index-management\n * @scope full-repo\n * @owner docs\n * @needs R-R16, R-R17\n * @purpose-statement Generated file banner template — provides standard banner text for auto-generated files\n * @pipeline indirect — library module imported by other scripts, not invoked directly\n * @usage node tools/lib/generated-file-banners.js [flags]\n */\nconst GENERATED_HIDDEN_MARKER = 'generated-file-banner:v1';\n\nfunction normalizeInline(value) {\n return String(value || '')\n .replace(/\\r?\\n/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction escapeYamlSingle(value) {\n return String(value || '').replace(/'/g, \"''\");\n}\n\nfunction asQuotedYaml(value) {\n return `'${escapeYamlSingle(value)}'`;\n}\n\nfunction parseFrontmatter(content) {\n const raw = String(content || '');\n if (!raw.startsWith('---')) {\n return {\n hasFrontmatter: false,\n frontmatter: '',\n body: raw\n };\n }\n\n const lineBreak = raw.includes('\\r\\n') ? '\\r\\n' : '\\n';\n const marker = `${lineBreak}---`;\n const markerIndex = raw.indexOf(marker, 3);\n if (markerIndex < 0) {\n return {\n hasFrontmatter: false,\n frontmatter: '',\n body: raw\n };\n }\n\n const frontmatter = raw.slice(0, markerIndex + marker.length);\n let body = raw.slice(markerIndex + marker.length);\n if (body.startsWith(lineBreak)) body = body.slice(lineBreak.length);\n\n return {\n hasFrontmatter: true,\n frontmatter,\n body\n };\n}\n\nfunction buildGeneratedFrontmatterLines(options = {}) {\n const title = normalizeInline(options.title);\n const sidebarTitle = normalizeInline(options.sidebarTitle);\n const description = normalizeInline(options.description);\n const pageType = normalizeInline(options.pageType);\n const keywords = Array.isArray(options.keywords) ? options.keywords.map((item) => normalizeInline(item)).filter(Boolean) : [];\n const keywordsStyle = options.keywordsStyle === 'multiline' ? 'multiline' : 'inline';\n const lines = ['---'];\n\n if (title) lines.push(`title: ${asQuotedYaml(title)}`);\n if (sidebarTitle) lines.push(`sidebarTitle: ${asQuotedYaml(sidebarTitle)}`);\n if (description) lines.push(`description: ${asQuotedYaml(description)}`);\n if (pageType) lines.push(`pageType: ${pageType}`);\n\n if (keywords.length > 0) {\n if (keywordsStyle === 'multiline') {\n lines.push('keywords:');\n lines.push(' [');\n keywords.forEach((keyword) => {\n lines.push(` ${asQuotedYaml(keyword)},`);\n });\n lines.push(' ]');\n } else {\n lines.push(`keywords: [${keywords.map((keyword) => asQuotedYaml(keyword)).join(', ')}]`);\n }\n }\n\n lines.push('---');\n return lines;\n}\n\nfunction buildGeneratedHiddenBannerLines(details = {}) {\n const marker = normalizeInline(details.marker) || GENERATED_HIDDEN_MARKER;\n return [\n '{/*',\n `generated-file-banner: ${marker}`,\n `Generation Script: ${normalizeInline(details.script)}`,\n `Purpose: ${normalizeInline(details.purpose)}`,\n `Run when: ${normalizeInline(details.runWhen)}`,\n `Run command: ${normalizeInline(details.runCommand)}`,\n '*/}'\n ];\n}\n\nfunction buildGeneratedNoteLines(details = {}) {\n return [\n '',\n `**Generation Script**: This file is generated from script(s): \\`${normalizeInline(details.script)}\\`.
`,\n `**Purpose**: ${normalizeInline(details.purpose)}
`,\n `**Run when**: ${normalizeInline(details.runWhen)}
`,\n `**Important**: Do not manually edit this file; run \\`${normalizeInline(details.runCommand)}\\`.
`,\n ' '\n ];\n}\n\nfunction parseGeneratedHiddenBanner(content) {\n const parsed = parseFrontmatter(content);\n const body = String(parsed.body || '');\n const commentRe = /\\{\\/\\*([\\s\\S]*?)\\*\\/\\}/g;\n let match;\n while ((match = commentRe.exec(body)) !== null) {\n const lines = match[1]\n .split('\\n')\n .map((line) => line.trim())\n .filter(Boolean);\n const map = new Map();\n lines.forEach((line) => {\n const idx = line.indexOf(':');\n if (idx < 0) return;\n const key = line.slice(0, idx).trim().toLowerCase();\n const value = line.slice(idx + 1).trim();\n map.set(key, value);\n });\n\n if (!map.has('generated-file-banner')) continue;\n\n return {\n found: true,\n marker: map.get('generated-file-banner') || '',\n script: map.get('generation script') || '',\n purpose: map.get('purpose') || '',\n runWhen: map.get('run when') || '',\n runCommand: map.get('run command') || '',\n raw: match[0]\n };\n }\n\n return {\n found: false,\n marker: '',\n script: '',\n purpose: '',\n runWhen: '',\n runCommand: '',\n raw: ''\n };\n}\n\nfunction hasGeneratedNote(content) {\n return /[\\s\\S]*?\\*\\*Generation Script\\*\\*:[\\s\\S]*?<\\/Note>/m.test(String(content || ''));\n}\n\nfunction extractGeneratedNote(content) {\n const match = String(content || '').match(/[\\s\\S]*?\\*\\*Generation Script\\*\\*:[\\s\\S]*?<\\/Note>/m);\n return match ? match[0] : '';\n}\n\nfunction removeGeneratedNotes(content) {\n return String(content || '').replace(/[\\s\\S]*?\\*\\*Generation Script\\*\\*:[\\s\\S]*?<\\/Note>\\s*/gm, '');\n}\n\nfunction hasFrontmatterKey(content, key) {\n const parsed = parseFrontmatter(content);\n if (!parsed.hasFrontmatter) return false;\n const re = new RegExp(`^\\\\s*${key}\\\\s*:`, 'm');\n return re.test(parsed.frontmatter);\n}\n\nmodule.exports = {\n GENERATED_HIDDEN_MARKER,\n buildGeneratedFrontmatterLines,\n buildGeneratedHiddenBannerLines,\n buildGeneratedNoteLines,\n parseFrontmatter,\n parseGeneratedHiddenBanner,\n hasGeneratedNote,\n extractGeneratedNote,\n removeGeneratedNotes,\n hasFrontmatterKey\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/script-docs.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/enforce-generated-file-banners.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/generate-component-docs.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/generate-docs-guide-components-index.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/generate-docs-guide-indexes.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/generate-docs-guide-pages-index.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/generate-pages-index.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/i18n/translate-docs.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/script-docs.test.js; indirect via tools/scripts/enforce-generated-file-banners.js; indirect via tools/scripts/generate-component-docs.js; indirect via tools/scripts/generate-docs-guide-components-index.js; indirect via tools/scripts/generate-docs-guide-indexes.js; indirect via tools/scripts/generate-docs-guide-pages-index.js; indirect via tools/scripts/generate-pages-index.js; indirect via tools/scripts/i18n/translate-docs.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/generate-component-docs.js",
+ "script": "generate-component-docs",
+ "category": "generator",
+ "purpose": "governance:index-management",
+ "scope": "generated-output",
+ "owner": "docs",
+ "needs": "R-R10",
+ "purpose_statement": "Generates published component library MDX pages from the registry.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/scripts/generate-component-docs.js [--template-only] [--category ]",
+ "header": "#!/usr/bin/env node\n/**\n * @script generate-component-docs\n * @category generator\n * @purpose governance:index-management\n * @scope generated-output\n * @owner docs\n * @needs R-R10\n * @purpose-statement Generates published component library MDX pages from the registry.\n * @pipeline manual\n * @usage node tools/scripts/generate-component-docs.js [--template-only] [--category ]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { VALID_CATEGORIES } = require('../lib/component-governance-utils');\nconst {\n buildGeneratedHiddenBannerLines,\n buildGeneratedNoteLines\n} = require('../lib/generated-file-banners');\n\nconst REPO_ROOT = path.resolve(__dirname, '..', '..');\nconst REGISTRY_PATH = path.join(REPO_ROOT, 'docs-guide', 'component-registry.json');\nconst ENGLISH_OUTPUT_DIR = path.join(\n REPO_ROOT,\n 'v2',\n 'resources',\n 'documentation-guide',\n 'component-library'\n);\nconst LOCALE_DIRS = [\n path.join(REPO_ROOT, 'v2', 'es', 'resources', 'documentation-guide', 'component-library'),\n path.join(REPO_ROOT, 'v2', 'fr', 'resources', 'documentation-guide', 'component-library'),\n path.join(REPO_ROOT, 'v2', 'cn', 'resources', 'documentation-guide', 'component-library')\n];\n\nconst CATEGORY_LABELS = {\n primitives: 'Primitives',\n layout: 'Layout',\n content: 'Content',\n data: 'Data',\n 'page-structure': 'Page Structure'\n};\n\nconst CATEGORY_DESCRIPTIONS = {\n primitives: 'Standalone visual atoms used across authored documentation.',\n layout: 'Containers and arrangements that define spacing, grouping, and page flow.',\n content: 'Renderers for reader-facing media, code, quotations, and structured content.',\n data: 'Components that are coupled to feeds, release data, or other external sources.',\n 'page-structure': 'Portal and frame-mode scaffolding components that shape whole sections.'\n};\n\nfunction usage() {\n console.log(\n [\n 'Usage: node tools/scripts/generate-component-docs.js [options]',\n '',\n 'Options:',\n ' --template-only Use template prose only (default for this phase)',\n ' --category Regenerate one category page only',\n ' --help, -h Show this help message'\n ].join('\\n')\n );\n}\n\nfunction parseArgs(argv) {\n const args = {\n templateOnly: true,\n category: '',\n help: false\n };\n\n for (let index = 0; index < argv.length; index += 1) {\n const token = argv[index];\n if (token === '--template-only') {\n args.templateOnly = true;\n continue;\n }\n if (token === '--category') {\n args.category = String(argv[index + 1] || '').trim();\n index += 1;\n continue;\n }\n if (token.startsWith('--category=')) {\n args.category = token.slice('--category='.length).trim();\n continue;\n }\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (args.category && !VALID_CATEGORIES.includes(args.category)) {\n throw new Error(`Unknown category: ${args.category}`);\n }\n\n return args;\n}\n\nfunction loadRegistry() {\n if (!fs.existsSync(REGISTRY_PATH)) {\n throw new Error('Missing docs-guide/component-registry.json. Run generate-component-registry first.');\n }\n\n return JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));\n}\n\nfunction escapePipes(value) {\n return String(value || '').replace(/\\|/g, '\\\\|');\n}\n\nfunction renderGeneratedNote(scriptPath, runCommand) {\n const details = {\n script: scriptPath,\n purpose: 'Generated component-library MDX pages derived from docs-guide/component-registry.json.',\n runWhen: 'Component governance metadata, registry outputs, or published component-library templates change.',\n runCommand\n };\n\n return [...buildGeneratedHiddenBannerLines(details), '', ...buildGeneratedNoteLines(details), ''].join('\\n');\n}\n\nfunction renderDecisionTree() {\n return [\n '1. Bound to an external data source or automation pipeline? `data/`',\n '2. Only makes sense on frame-mode or portal pages? `page-structure/`',\n '3. Accepts children and arranges them spatially? `layout/`',\n '4. Formats or renders content for the reader? `content/`',\n '5. Otherwise: `primitives/`'\n ].join('\\n');\n}\n\nfunction renderPropsTable(params) {\n if (!params.length) {\n return 'No documented props.\\n';\n }\n\n const lines = [\n '| Prop | Type | Required | Default | Description |',\n '| --- | --- | --- | --- | --- |'\n ];\n\n params.forEach((param) => {\n lines.push(\n `| \\`${escapePipes(param.name)}\\` | \\`${escapePipes(param.type || 'any')}\\` | ${\n param.required ? 'Yes' : 'No'\n } | \\`${escapePipes(param.defaultValue || '')}\\` | ${escapePipes(param.description || '')} |`\n );\n });\n\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction renderExample(example) {\n if (!example) return '';\n return ['```jsx', example.trim(), '```', ''].join('\\n');\n}\n\nfunction renderUsageSummary(component) {\n if (!component.usedIn.length) {\n return '`@usedIn`: none';\n }\n\n const preview = component.usedIn.slice(0, 6).map((entry) => `\\`${entry}\\``).join(', ');\n if (component.usedIn.length <= 6) {\n return `\\`@usedIn\\`: ${preview}`;\n }\n\n return `\\`@usedIn\\`: ${preview}, and ${component.usedIn.length - 6} more page(s)`;\n}\n\nfunction renderComponentSection(component) {\n return [\n `### ${component.name}`,\n '',\n component.description,\n '',\n `- File: \\`/${component.file}\\``,\n `- Tier: \\`${component.tier}\\``,\n `- Status: \\`${component.status}\\``,\n `- Risk: \\`${component.breakingChangeRisk}\\``,\n `- Decision: \\`${component.decision}\\``,\n `- Owner: \\`${component.owner}\\``,\n `- Content affinity: ${\n component.contentAffinity.length ? component.contentAffinity.map((value) => `\\`${value}\\``).join(', ') : '`none`'\n }`,\n `- Dependencies: ${\n component.dependencies.length ? component.dependencies.map((value) => `\\`${value}\\``).join(', ') : '`none`'\n }`,\n `- Data source: \\`${component.dataSource || 'none'}\\``,\n `- Duplicates: ${\n component.duplicates.length ? component.duplicates.map((value) => `\\`${value}\\``).join(', ') : '`none`'\n }`,\n `- Last meaningful change: \\`${component.lastMeaningfulChange}\\``,\n `- ${renderUsageSummary(component)}`,\n component.status === 'deprecated' && component.deprecated\n ? `- Deprecation note: ${component.deprecated}${component.see ? ` See \\`${component.see}\\`.` : ''}`\n : null,\n '',\n '**Import**',\n '',\n '```jsx',\n `import { ${component.name} } from '/${component.file}'`,\n '```',\n '',\n '**Props**',\n '',\n renderPropsTable(component.params),\n component.examples.length\n ? ['**Example**', '', renderExample(component.examples[0])].join('\\n')\n : ''\n ]\n .filter(Boolean)\n .join('\\n');\n}\n\nfunction renderCategoryPage(category, components) {\n const label = CATEGORY_LABELS[category];",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/component-governance-generators.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/generate-docs-guide-components-index.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/component-governance-generators.test.js; indirect via tools/scripts/generate-docs-guide-components-index.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/scan-component-imports.js",
+ "script": "scan-component-imports",
+ "category": "generator",
+ "purpose": "governance:index-management",
+ "scope": "full-repo",
+ "owner": "docs",
+ "needs": "R-R10",
+ "purpose_statement": "Scans MDX imports to produce component-usage-map.json and detect @usedIn drift.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/scripts/scan-component-imports.js [--verify]",
+ "header": "#!/usr/bin/env node\n/**\n * @script scan-component-imports\n * @category generator\n * @purpose governance:index-management\n * @scope full-repo\n * @owner docs\n * @needs R-R10\n * @purpose-statement Scans MDX imports to produce component-usage-map.json and detect @usedIn drift.\n * @pipeline manual\n * @usage node tools/scripts/scan-component-imports.js [--verify]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst {\n extractExports,\n getComponentFiles,\n parseJSDocBlock,\n scanMDXImports\n} = require('../lib/component-governance-utils');\nconst { buildRegistry } = require('./generate-component-registry');\n\nconst REPO_ROOT = path.resolve(__dirname, '..', '..');\nconst OUTPUT_PATH = path.join(REPO_ROOT, 'docs-guide', 'component-usage-map.json');\nconst REGISTRY_PATH = path.join(REPO_ROOT, 'docs-guide', 'component-registry.json');\n\nfunction isPublishedDocsPath(filePath) {\n return !/^v2(?:\\/(?:cn|es|fr))?\\/x-(archived|deprecated|experimental|notes)\\//.test(\n String(filePath || '').trim()\n );\n}\n\nfunction usage() {\n console.log(\n [\n 'Usage: node tools/scripts/scan-component-imports.js [options]',\n '',\n 'Options:',\n ' --verify Compare live imports with @usedIn declarations and fail on drift',\n ' --help, -h Show this help message'\n ].join('\\n')\n );\n}\n\nfunction parseArgs(argv) {\n const args = {\n verify: false,\n help: false\n };\n\n argv.forEach((token) => {\n if (token === '--verify') {\n args.verify = true;\n return;\n }\n if (token === '--help' || token === '-h') {\n args.help = true;\n return;\n }\n throw new Error(`Unknown argument: ${token}`);\n });\n\n return args;\n}\n\nfunction sortStrings(values) {\n return [...new Set(values)].sort((left, right) =>\n left.localeCompare(right, 'en', { sensitivity: 'base' })\n );\n}\n\nfunction readRegistry() {\n if (fs.existsSync(REGISTRY_PATH)) {\n return JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));\n }\n\n return buildRegistry().registry;\n}\n\nfunction collectDeclaredUsage() {\n const usage = new Map();\n\n getComponentFiles().forEach((file) => {\n extractExports(file.displayPath).forEach((entry) => {\n if (!entry.jsDocBlock) return;\n const parsed = parseJSDocBlock(entry.jsDocBlock);\n usage.set(\n entry.name,\n sortStrings(\n String(parsed.usedIn || '')\n .split(',')\n .map((value) => value.trim())\n .filter((value) => value && value.toLowerCase() !== 'none' && isPublishedDocsPath(value))\n )\n );\n });\n });\n\n return usage;\n}\n\nfunction buildUsageMap() {\n const registry = readRegistry();\n const liveImports = scanMDXImports('v2/**/*.mdx');\n const declaredUsage = collectDeclaredUsage();\n\n const components = registry.components.map((component) => {\n const actual = liveImports.get(component.name) || {\n pages: [],\n count: 0,\n localeBreakdown: { en: 0, es: 0, fr: 0, cn: 0, other: 0 }\n };\n const actualPages = sortStrings(actual.pages);\n const declaredPages = declaredUsage.get(component.name) || [];\n const missingFromJsDoc = actualPages.filter((page) => !declaredPages.includes(page));\n const staleInJsDoc = declaredPages.filter((page) => !actualPages.includes(page));\n\n return {\n name: component.name,\n file: component.file,\n category: component.category,\n count: actual.count,\n pages: actualPages,\n localeBreakdown: actual.localeBreakdown,\n declaredUsedIn: declaredPages,\n drift: {\n missingFromJsDoc,\n staleInJsDoc\n }\n };\n });\n\n const orphaned = components\n .filter((component) => component.pages.length === 0)\n .map((component) => ({\n name: component.name,\n file: component.file,\n category: component.category\n }));\n\n const mostImported = [...components]\n .sort((left, right) => {\n if (right.count !== left.count) return right.count - left.count;\n return left.name.localeCompare(right.name, 'en', { sensitivity: 'base' });\n })\n .slice(0, 25)\n .map((component, index) => ({\n rank: index + 1,\n name: component.name,\n category: component.category,\n count: component.count\n }));\n\n const drift = components\n .filter((component) => component.drift.missingFromJsDoc.length || component.drift.staleInJsDoc.length)\n .map((component) => ({\n name: component.name,\n file: component.file,\n missingFromJsDoc: component.drift.missingFromJsDoc,\n staleInJsDoc: component.drift.staleInJsDoc\n }));\n\n return {\n usageMap: {\n _meta: {\n generated: new Date().toISOString(),\n generator: 'tools/scripts/scan-component-imports.js',\n componentCount: components.length\n },\n components,\n orphaned,\n mostImported\n },\n drift\n };\n}\n\nfunction run(argv = process.argv.slice(2)) {\n let args;\n try {\n args = parseArgs(argv);\n } catch (error) {\n console.error(`❌ ${error.message}`);\n usage();\n return 1;\n }\n\n if (args.help) {\n usage();\n return 0;\n }\n\n const { usageMap, drift } = buildUsageMap();\n fs.mkdirSync(path.dirname(OUTPUT_PATH), { recursive: true });\n fs.writeFileSync(OUTPUT_PATH, `${JSON.stringify(usageMap, null, 2)}\\n`, 'utf8');\n console.log(`Wrote ${path.relative(REPO_ROOT, OUTPUT_PATH)}`);\n\n if (args.verify && drift.length > 0) {\n console.error('❌ @usedIn drift detected:');\n drift.forEach((entry) => {\n if (entry.missingFromJsDoc.length > 0) {\n console.error(`- ${entry.file} :: ${entry.name} missing from @usedIn: ${entry.missingFromJsDoc.join(', ')}`);\n }\n if (entry.staleInJsDoc.length > 0) {\n console.error(`- ${entry.file} :: ${entry.name} stale @usedIn entries: ${entry.staleInJsDoc.join(', ')}`);\n }\n });\n return 1;\n }\n\n if (args.verify) {\n console.log('No @usedIn drift detected.');\n }\n\n return 0;\n}\n\nif (require.main === module) {\n process.exit(run());",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/component-governance-generators.test.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/component-usage-map.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/component-usage-map.json",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/component-governance-generators.test.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/component-governance-utils.js",
+ "script": "component-governance-utils",
+ "category": "utility",
+ "purpose": "governance:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R10",
+ "purpose_statement": "Shared parsing and validation utilities for component governance scripts.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const utils = require('../lib/component-governance-utils');",
+ "header": "/**\n * @script component-governance-utils\n * @category utility\n * @purpose governance:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R10\n * @purpose-statement Shared parsing and validation utilities for component governance scripts.\n * @pipeline indirect -- library module\n * @usage const utils = require('../lib/component-governance-utils');\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst VALID_CATEGORIES = ['primitives', 'layout', 'content', 'data', 'page-structure'];\nconst VALID_STATUSES = ['stable', 'experimental', 'deprecated', 'broken', 'placeholder'];\nconst VALID_TIERS = ['primitive', 'composite', 'pattern'];\nconst VALID_RISKS = ['low', 'medium', 'high'];\nconst VALID_DECISIONS = ['KEEP', 'MOVE', 'SPLIT', 'MERGE', 'REMOVE'];\nconst VALID_CONTENT_AFFINITY = [\n 'landing',\n 'overview',\n 'tutorial',\n 'how_to',\n 'concept',\n 'reference',\n 'faq',\n 'troubleshooting',\n 'changelog',\n 'glossary',\n 'universal'\n];\nconst GOVERNANCE_FIELDS = [\n 'component',\n 'category',\n 'tier',\n 'status',\n 'description',\n 'contentAffinity',\n 'owner',\n 'dependencies',\n 'usedIn',\n 'breakingChangeRisk',\n 'decision',\n 'dataSource',\n 'duplicates',\n 'lastMeaningfulChange'\n];\nconst COMPONENT_IMPORT_RE = /import\\s*\\{([\\s\\S]*?)\\}\\s*from\\s*['\"]([^'\"]+)['\"]/g;\nconst COLOR_LITERAL_RE = /#[0-9a-fA-F]{3,8}\\b|\\brgba?\\([^)]*\\)|\\bhsla?\\([^)]*\\)/g;\nconst COLOR_CONTEXT_RE = /\\b(?:accentcolor|background(?:color)?|border(?:color)?|caretcolor|color|fill|floodcolor|icon|lightingcolor|outlinecolor|stopcolor|stroke|textdecorationcolor)\\b/;\n\nfunction getRepoRoot() {\n const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n if (result.status === 0 && String(result.stdout || '').trim()) {\n return String(result.stdout || '').trim();\n }\n return process.cwd();\n}\n\nconst REPO_ROOT = getRepoRoot();\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction compactWhitespace(value) {\n return String(value || '').replace(/\\s+/g, ' ').trim();\n}\n\nfunction lineNumberAt(content, index) {\n return String(content || '').slice(0, Math.max(0, index)).split('\\n').length;\n}\n\nfunction normalizeRepoPath(inputPath) {\n const raw = String(inputPath || '').trim();\n const absolute = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(REPO_ROOT, raw);\n return toPosix(path.relative(REPO_ROOT, absolute));\n}\n\nfunction isArchivePath(filePath) {\n return toPosix(filePath).split('/').includes('_archive');\n}\n\nfunction getCategoryFromPath(filePath) {\n const parts = toPosix(filePath).split('/');\n const index = parts.findIndex((part) => part === 'components');\n if (index === -1) return '';\n const candidate = parts[index + 1] || '';\n return VALID_CATEGORIES.includes(candidate) ? candidate : '';\n}\n\nfunction walkFiles(targetPath, predicate, files = []) {\n if (!fs.existsSync(targetPath)) return files;\n const stat = fs.statSync(targetPath);\n if (stat.isFile()) {\n if (predicate(targetPath)) files.push(targetPath);\n return files;\n }\n\n const entries = fs.readdirSync(targetPath, { withFileTypes: true });\n entries.forEach((entry) => {\n if (entry.name === '.git' || entry.name === 'node_modules') return;\n walkFiles(path.join(targetPath, entry.name), predicate, files);\n });\n return files;\n}\n\nfunction getComponentFiles(baseDir = 'snippets/components') {\n const absoluteBase = path.isAbsolute(baseDir) ? baseDir : path.join(REPO_ROOT, baseDir);\n return walkFiles(\n absoluteBase,\n (absolutePath) =>\n absolutePath.endsWith('.jsx') &&\n !isArchivePath(absolutePath) &&\n VALID_CATEGORIES.includes(getCategoryFromPath(absolutePath))\n )\n .map((absolutePath) => ({\n absolutePath,\n displayPath: normalizeRepoPath(absolutePath)\n }))\n .sort((a, b) => a.displayPath.localeCompare(b.displayPath, 'en', { sensitivity: 'base' }));\n}\n\nfunction stripComments(value) {\n return String(value || '')\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .replace(/\\/\\/.*$/gm, '')\n .trim();\n}\n\nfunction splitTopLevel(input, separator = ',') {\n const parts = [];\n let current = '';\n let depthParen = 0;\n let depthBrace = 0;\n let depthBracket = 0;\n let quote = null;\n let escaped = false;\n\n for (let index = 0; index < input.length; index += 1) {\n const char = input[index];\n\n if (quote) {\n current += char;\n if (!escaped && char === '\\\\') {\n escaped = true;\n } else if (!escaped && char === quote) {\n quote = null;\n } else {\n escaped = false;\n }\n continue;\n }\n\n if (char === '\"' || char === \"'\" || char === '`') {\n quote = char;\n current += char;\n continue;\n }\n\n if (char === '(') depthParen += 1;\n else if (char === ')') depthParen = Math.max(0, depthParen - 1);\n else if (char === '{') depthBrace += 1;\n else if (char === '}') depthBrace = Math.max(0, depthBrace - 1);\n else if (char === '[') depthBracket += 1;\n else if (char === ']') depthBracket = Math.max(0, depthBracket - 1);\n\n if (\n char === separator &&\n depthParen === 0 &&\n depthBrace === 0 &&\n depthBracket === 0\n ) {\n parts.push(current.trim());\n current = '';\n continue;\n }\n\n current += char;\n }\n\n if (current.trim()) {\n parts.push(current.trim());\n }\n\n return parts;\n}\n\nfunction splitTopLevelOnce(input, separator) {\n const parts = splitTopLevel(input, separator);\n if (parts.length <= 1) return [input.trim(), ''];\n const first = parts.shift();\n return [first, parts.join(separator).trim()];\n}\n\nfunction findMatchingBracket(input, startIndex, openChar, closeChar) {\n let depth = 0;\n let quote = null;\n let escaped = false;\n\n for (let index = startIndex; index < input.length; index += 1) {\n const char = input[index];\n\n if (quote) {\n if (!escaped && char === '\\\\') {\n escaped = true;\n } else if (!escaped && char === quote) {\n quote = null;\n } else {\n escaped = false;\n }\n continue;\n }\n\n if (char === '\"' || char === \"'\" || char === '`') {\n quote = char;\n continue;",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/component-governance-utils.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/generate-component-docs.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/generate-component-registry.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/generate-docs-guide-components-index.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/scan-component-imports.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/validators/components/check-component-css.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/validators/components/check-component-docs.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/component-governance-utils.test.js; indirect via tools/scripts/generate-component-docs.js; indirect via tools/scripts/generate-component-registry.js; indirect via tools/scripts/generate-docs-guide-components-index.js; indirect via tools/scripts/scan-component-imports.js; indirect via tools/scripts/validators/components/check-component-css.js; indirect via tools/scripts/validators/components/check-component-docs.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/script-governance-config.js",
+ "script": "script-governance-config",
+ "category": "utility",
+ "purpose": "governance:repo-health",
+ "scope": "full-repo",
+ "owner": "docs",
+ "needs": "R-R14, R-R18, R-C6",
+ "purpose_statement": "Shared governance constants for script discovery, indexing, classification, and pipeline normalisation across the repo.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const config = require('../lib/script-governance-config');",
+ "header": "/**\n * @script script-governance-config\n * @category utility\n * @purpose governance:repo-health\n * @scope full-repo\n * @owner docs\n * @needs R-R14, R-R18, R-C6\n * @purpose-statement Shared governance constants for script discovery, indexing, classification, and pipeline normalisation across the repo.\n * @pipeline indirect -- library module\n * @usage const config = require('../lib/script-governance-config');\n */\n\nconst path = require('path');\n\nconst DISCOVERY_ROOTS = [\n '.githooks',\n '.github/scripts',\n 'tasks/scripts',\n 'tests/unit',\n 'tests/integration',\n 'tests/utils',\n 'tests',\n 'tools/scripts',\n 'tools/lib',\n 'tools/notion',\n 'tools/config',\n 'snippets/automations'\n];\n\nconst GOVERNED_ROOTS = [\n '.githooks',\n '.github/scripts',\n 'tests',\n 'tools/scripts',\n 'tools/lib',\n 'tools/notion',\n 'tools/config',\n 'tasks/scripts',\n 'snippets/automations'\n];\n\nconst INDEXED_ROOTS = [\n '.githooks',\n '.github/scripts',\n 'tests',\n 'tools/scripts',\n 'tools/lib',\n 'tools/notion',\n 'tools/config',\n 'tasks/scripts',\n 'snippets/automations'\n];\n\nconst GROUP_INDEX_MAP = [\n { root: '.githooks', index: '.githooks/script-index.md' },\n { root: '.github/scripts', index: '.github/script-index.md' },\n { root: 'tests', index: 'tests/script-index.md' },\n { root: 'tools/scripts', index: 'tools/script-index.md' },\n { root: 'tools/lib', index: 'tools/lib/script-index.md' },\n { root: 'tools/notion', index: 'tools/notion/script-index.md' },\n { root: 'tools/config', index: 'tools/config/script-index.md' },\n { root: 'tasks/scripts', index: 'tasks/scripts/script-index.md' },\n { root: 'snippets/automations', index: 'snippets/automations/script-index.md' }\n];\nconst GROUP_INDEX_PATHS = GROUP_INDEX_MAP.map((entry) => entry.index);\n\nconst AGGREGATE_INDEX_PATH = 'docs-guide/indexes/scripts-index.mdx';\nconst LEGACY_AGGREGATE_INDEX_PATH = 'docs-guide/indexes/scripts-index.md';\nconst CLASSIFICATION_DATA_PATH = 'tasks/reports/script-classifications.json';\n\nconst SCRIPT_EXTENSIONS = ['.js', '.sh', '.py'];\nconst SCRIPT_EXTENSIONS_SET = new Set(SCRIPT_EXTENSIONS);\n\nconst EXCLUDED_PREFIXES = ['node_modules/', '.git/', 'tools/scripts/archive/'];\nconst EXCLUDED_SEGMENTS = ['node_modules', '.git'];\n\nconst FRAMEWORK_FIELDS = [\n { key: 'script', tag: '@script' },\n { key: 'category', tag: '@category' },\n { key: 'purpose', tag: '@purpose' },\n { key: 'scope', tag: '@scope' },\n { key: 'owner', tag: '@owner' },\n { key: 'needs', tag: '@needs' },\n { key: 'purpose_statement', tag: '@purpose-statement' },\n { key: 'pipeline', tag: '@pipeline' },\n { key: 'usage', tag: '@usage' }\n];\n\nconst REQUIRED_FRAMEWORK_KEYS = ['script', 'category', 'purpose', 'scope', 'owner', 'usage'];\n\nconst CATEGORY_ENUM = [\n 'validator',\n 'enforcer',\n 'remediator',\n 'generator',\n 'automation',\n 'utility',\n 'orchestrator'\n];\n\nconst PURPOSE_ENUM = [\n 'qa:content-quality',\n 'qa:link-integrity',\n 'qa:repo-health',\n 'governance:index-management',\n 'governance:agent-governance',\n 'governance:repo-health',\n 'feature:translation',\n 'feature:seo',\n 'infrastructure:data-feeds',\n 'infrastructure:pipeline-orchestration',\n 'tooling:api-spec',\n 'tooling:dev-tools'\n];\n\nconst SCOPE_ENUM = [\n 'staged',\n 'changed',\n 'full-repo',\n 'v2-content',\n 'single-domain',\n 'single-file',\n 'external',\n 'generated-output'\n];\n\nconst GROUP_LABELS = {\n Unmanaged: 'Unmanaged',\n Orphaned: 'Orphaned',\n P1: 'P1 - Commit gate',\n P2: 'P2 - Push gate',\n P3: 'P3 - PR gate',\n P5: 'P5 - Scheduled',\n P6: 'P6 - On-demand',\n Indirect: 'Indirect - Library',\n Manual: 'Manual - CLI only'\n};\n\nconst GROUP_ORDER = ['Unmanaged', 'Orphaned', 'P1', 'P2', 'P3', 'P5', 'P6', 'Indirect', 'Manual'];\nconst PIPELINE_ORDER = ['P1', 'P2', 'P3', 'P5', 'P6', 'indirect', 'manual'];\n\nfunction normalizeRepoPath(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction pathHasExcludedSegment(repoPath) {\n const parts = normalizeRepoPath(repoPath).split('/');\n return parts.some((part) => EXCLUDED_SEGMENTS.includes(part));\n}\n\nfunction shouldExcludeScriptPath(repoPath) {\n const normalized = normalizeRepoPath(repoPath);\n if (!normalized) return true;\n if (EXCLUDED_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return true;\n if (pathHasExcludedSegment(normalized)) return true;\n return normalized.includes('.bak') || normalized.endsWith('.disabled');\n}\n\nfunction isWithinRoots(repoPath, roots) {\n const normalized = normalizeRepoPath(repoPath);\n return roots.some((root) => normalized === root || normalized.startsWith(`${root}/`));\n}\n\nfunction isHookScriptPath(repoPath) {\n const normalized = normalizeRepoPath(repoPath);\n return normalized.startsWith('.githooks/') && path.extname(normalized) === '';\n}\n\nfunction isDiscoveredScriptPath(repoPath) {\n const normalized = normalizeRepoPath(repoPath);\n if (shouldExcludeScriptPath(normalized)) return false;\n if (!isWithinRoots(normalized, DISCOVERY_ROOTS)) return false;\n const ext = path.extname(normalized).toLowerCase();\n return SCRIPT_EXTENSIONS_SET.has(ext) || isHookScriptPath(normalized);\n}\n\nfunction parseDeclaredPipelines(rawValue) {\n const raw = String(rawValue || '').trim();\n const found = new Set();\n if (!raw) return found;\n for (const match of raw.match(/\\bP[1-6]\\b/g) || []) {\n found.add(match);\n }\n if (/manual/i.test(raw)) found.add('manual');\n if (/indirect/i.test(raw)) found.add('indirect');\n return found;\n}\n\nmodule.exports = {\n AGGREGATE_INDEX_PATH,\n CATEGORY_ENUM,\n CLASSIFICATION_DATA_PATH,\n DISCOVERY_ROOTS,\n EXCLUDED_PREFIXES,\n FRAMEWORK_FIELDS,\n GOVERNED_ROOTS,\n GROUP_INDEX_MAP,\n GROUP_INDEX_PATHS,\n GROUP_LABELS,\n GROUP_ORDER,\n INDEXED_ROOTS,\n LEGACY_AGGREGATE_INDEX_PATH,\n PIPELINE_ORDER,\n PURPOSE_ENUM,\n REQUIRED_FRAMEWORK_KEYS,\n SCOPE_ENUM,\n SCRIPT_EXTENSIONS,\n isDiscoveredScriptPath,\n isHookScriptPath,\n isWithinRoots,\n normalizeRepoPath,\n parseDeclaredPipelines,\n shouldExcludeScriptPath\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/script-docs.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/validators/governance/audit-script-inventory.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/run-pr-checks.js; indirect via tests/unit/script-docs.test.js; indirect via tools/scripts/validators/governance/audit-script-inventory.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/audit-media-assets.js",
+ "script": "audit-media-assets",
+ "category": "utility",
+ "purpose": "governance:repo-health",
+ "scope": "tools/scripts, tasks/reports/media-audit",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "Audits repo media assets, references, ignore leakage, and externalized asset branch inventory.",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/audit-media-assets.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script audit-media-assets\n * @category utility\n * @purpose governance:repo-health\n * @scope tools/scripts, tasks/reports/media-audit\n * @owner docs\n * @needs R-R14\n * @purpose-statement Audits repo media assets, references, ignore leakage, and externalized asset branch inventory.\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/audit-media-assets.js [flags]\n */\n\nconst fs = require('fs')\nconst path = require('path')\nconst { execSync, spawnSync } = require('child_process')\n\nconst REPO_ROOT = process.cwd()\nconst DEFAULT_MANIFEST_PATH = 'tasks/reports/media-audit/media-audit-manifest.json'\nconst DEFAULT_SUMMARY_PATH = 'tasks/reports/media-audit/media-audit-summary.md'\nconst DEFAULT_ASSETS_BRANCH_REF = 'origin/docs-v2-assets'\nconst ONE_MB = 1024 * 1024\nconst FIVE_MB = 5 * ONE_MB\nconst TARGET_EXTENSIONS = new Set([\n '.mp4',\n '.mov',\n '.webm',\n '.gif',\n '.png',\n '.jpg',\n '.jpeg',\n '.webp',\n '.pdf',\n])\nconst SOURCE_EXTENSIONS = new Set(['.mdx', '.jsx'])\nconst RAW_ASSET_BRANCH_NAMES = ['assets', 'docs-v2-assets']\n\nfunction usage() {\n console.log(\n 'Usage: node tools/scripts/audit-media-assets.js [--manifest ] [--summary ] [--assets-branch-ref ] [--check-staged]'\n )\n}\n\nfunction parseArgs(argv) {\n const options = {\n manifestPath: DEFAULT_MANIFEST_PATH,\n summaryPath: DEFAULT_SUMMARY_PATH,\n assetsBranchRef: DEFAULT_ASSETS_BRANCH_REF,\n checkStaged: false,\n }\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i]\n\n if (token === '--help' || token === '-h') {\n usage()\n process.exit(0)\n }\n\n if (token === '--check-staged') {\n options.checkStaged = true\n continue\n }\n\n if (token === '--manifest') {\n options.manifestPath = String(argv[i + 1] || '').trim()\n i += 1\n continue\n }\n\n if (token.startsWith('--manifest=')) {\n options.manifestPath = token.slice('--manifest='.length).trim()\n continue\n }\n\n if (token === '--summary') {\n options.summaryPath = String(argv[i + 1] || '').trim()\n i += 1\n continue\n }\n\n if (token.startsWith('--summary=')) {\n options.summaryPath = token.slice('--summary='.length).trim()\n continue\n }\n\n if (token === '--assets-branch-ref') {\n options.assetsBranchRef = String(argv[i + 1] || '').trim()\n i += 1\n continue\n }\n\n if (token.startsWith('--assets-branch-ref=')) {\n options.assetsBranchRef = token.slice('--assets-branch-ref='.length).trim()\n continue\n }\n\n console.error(`Unknown argument: ${token}`)\n usage()\n process.exit(1)\n }\n\n if (!options.manifestPath) {\n console.error('Missing --manifest value')\n usage()\n process.exit(1)\n }\n\n if (!options.summaryPath) {\n console.error('Missing --summary value')\n usage()\n process.exit(1)\n }\n\n if (!options.assetsBranchRef) {\n console.error('Missing --assets-branch-ref value')\n usage()\n process.exit(1)\n }\n\n return options\n}\n\nfunction normalizeRepoPath(value) {\n return String(value || '')\n .replace(/^\\.\\//, '')\n .split(path.sep)\n .join('/')\n}\n\nfunction absoluteFromRepoPath(repoPath) {\n return path.join(REPO_ROOT, normalizeRepoPath(repoPath))\n}\n\nfunction ensureDirForFile(repoPath) {\n fs.mkdirSync(path.dirname(absoluteFromRepoPath(repoPath)), { recursive: true })\n}\n\nfunction fileExists(repoPath) {\n try {\n return fs.statSync(absoluteFromRepoPath(repoPath)).isFile()\n } catch (_err) {\n return false\n }\n}\n\nfunction readFileUtf8(repoPath) {\n return fs.readFileSync(absoluteFromRepoPath(repoPath), 'utf8')\n}\n\nfunction escapeRegExp(value) {\n return String(value || '').replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\nfunction uniqueSorted(values) {\n return [...new Set(values.map((value) => normalizeRepoPath(value)).filter(Boolean))].sort()\n}\n\nfunction runShell(command, options = {}) {\n const allowFailure = Boolean(options.allowFailure)\n try {\n return execSync(command, {\n cwd: REPO_ROOT,\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'pipe'],\n })\n } catch (error) {\n if (allowFailure) return ''\n const stderr = String(error.stderr || '').trim()\n const stdout = String(error.stdout || '').trim()\n const detail = stderr || stdout || error.message\n throw new Error(`Command failed: ${command}\\n${detail}`)\n }\n}\n\nfunction runCommand(command, args, options = {}) {\n const allowFailure = Boolean(options.allowFailure)\n const result = spawnSync(command, args, {\n cwd: REPO_ROOT,\n encoding: options.encoding === 'buffer' ? null : 'utf8',\n stdio: ['ignore', 'pipe', 'pipe'],\n })\n\n if (result.status === 0) {\n return options.encoding === 'buffer' ? result.stdout : String(result.stdout || '')\n }\n\n if (allowFailure) {\n return options.encoding === 'buffer' ? Buffer.alloc(0) : ''\n }\n\n const stderr = options.encoding === 'buffer'\n ? String(result.stderr || '')\n : String(result.stderr || '').trim()\n const stdout = options.encoding === 'buffer'\n ? String(result.stdout || '')\n : String(result.stdout || '').trim()\n const detail = stderr || stdout || `exit ${result.status}`\n throw new Error(`Command failed: ${command} ${args.join(' ')}\\n${detail}`)\n}\n\nfunction splitNullDelimited(value) {\n return String(value || '')\n .split('\\0')\n .map((entry) => normalizeRepoPath(entry.trim()))\n .filter(Boolean)\n}\n\nfunction getTrackedFiles() {\n return splitNullDelimited(runCommand('git', ['ls-files', '-z']))\n}\n\nfunction getStagedFiles() {\n return splitNullDelimited(\n runCommand('git', ['diff', '--cached', '--name-only', '-z', '--diff-filter=ACMR'])\n )\n}\n\nfunction collectUniverseFiles(checkStaged) {\n return checkStaged ? getStagedFiles() : getTrackedFiles()",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/remediators/assets/migrate-assets-to-branch.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": false,
+ "purpose_match": false,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/remediators/assets/migrate-assets-to-branch.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope",
+ "header-json-category-mismatch",
+ "header-json-purpose-mismatch"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/remediators/assets/migrate-assets-to-branch.js",
+ "script": "migrate-assets-to-branch",
+ "category": "remediator",
+ "purpose": "governance:repo-health",
+ "scope": "full-repo",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "Reads the media-audit manifest and migrates flagged assets to the",
+ "pipeline_declared": "manual",
+ "usage": "node tools/scripts/remediators/assets/migrate-assets-to-branch.js \\",
+ "header": "#!/usr/bin/env node\n/**\n * @script migrate-assets-to-branch\n * @category remediator\n * @purpose governance:repo-health\n * @scope full-repo\n * @owner docs\n * @needs R-R14\n * @purpose-statement Reads the media-audit manifest and migrates flagged assets to the\n * docs-v2-assets branch, then rewrites MDX/JSX references to raw\n * GitHub URLs. Repair-lifecycle script: run after audit-media-assets.js\n * flags violations.\n * @pipeline manual\n * @dualmode --dry-run (show what would change) | --write (execute migration)\n * @usage node tools/scripts/remediators/assets/migrate-assets-to-branch.js \\\n * --manifest tasks/reports/media-audit/media-audit-manifest.json \\\n * --target migrate_r2,migrate_cloudinary \\\n * --dry-run\n */\n\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst auditMediaAssets = require('../../audit-media-assets');\n\nconst REPO_ROOT = process.cwd();\nconst DEFAULT_TARGETS = ['migrate_r2', 'migrate_cloudinary'];\nconst DEFAULT_MANIFEST_PATH = auditMediaAssets.DEFAULT_MANIFEST_PATH;\nconst DEFAULT_ASSETS_BRANCH = 'docs-v2-assets';\nconst DEFAULT_ASSETS_BRANCH_REF = `origin/${DEFAULT_ASSETS_BRANCH}`;\nconst RAW_GITHUB_BASE = `https://raw.githubusercontent.com/livepeer/docs/${DEFAULT_ASSETS_BRANCH}`;\nconst LOG_DIR = 'tasks/reports/media-audit';\n\nfunction printHelp() {\n console.log(\n [\n 'Usage:',\n ' node tools/scripts/remediators/assets/migrate-assets-to-branch.js [options]',\n '',\n 'Options:',\n ` --manifest Path to manifest (default: ${DEFAULT_MANIFEST_PATH})`,\n ` --target Comma-separated migration_target values (default: ${DEFAULT_TARGETS.join(',')})`,\n ' --dry-run Show planned changes without executing (default).',\n ' --write Execute copy, ref rewrite, test, and delete phases.',\n ' --skip-copy Skip copying files to docs-v2-assets.',\n ' --skip-refs Skip MDX/JSX reference rewrites.',\n ' --file Restrict processing to a single repo-relative asset path.',\n ' --help Show this help output.',\n '',\n 'Notes:',\n ' - GIF references are rewritten to raw GitHub URLs in-place; they are not transcoded to video.',\n ' - Bare filename-only references are treated as ambiguous and skipped for manual review.'\n ].join('\\n')\n );\n}\n\nfunction parseArgs(argv) {\n const args = {\n manifestPath: DEFAULT_MANIFEST_PATH,\n targets: [...DEFAULT_TARGETS],\n mode: 'dry-run',\n skipCopy: false,\n skipRefs: false,\n file: '',\n help: false\n };\n\n let explicitModeCount = 0;\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n if (token === '--dry-run') {\n args.mode = 'dry-run';\n explicitModeCount += 1;\n continue;\n }\n if (token === '--write') {\n args.mode = 'write';\n explicitModeCount += 1;\n continue;\n }\n if (token === '--skip-copy') {\n args.skipCopy = true;\n continue;\n }\n if (token === '--skip-refs') {\n args.skipRefs = true;\n continue;\n }\n if (token === '--manifest') {\n args.manifestPath = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--manifest=')) {\n args.manifestPath = token.slice('--manifest='.length).trim();\n continue;\n }\n if (token === '--target') {\n args.targets = parseCsvList(argv[i + 1] || '');\n i += 1;\n continue;\n }\n if (token.startsWith('--target=')) {\n args.targets = parseCsvList(token.slice('--target='.length));\n continue;\n }\n if (token === '--file') {\n args.file = normalizeOptionalRepoPath(argv[i + 1] || '');\n i += 1;\n continue;\n }\n if (token.startsWith('--file=')) {\n args.file = normalizeOptionalRepoPath(token.slice('--file='.length));\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (explicitModeCount > 1) {\n throw new Error('Choose only one mode: --dry-run or --write');\n }\n if (!args.manifestPath) {\n throw new Error('Missing --manifest value');\n }\n if (!Array.isArray(args.targets) || args.targets.length === 0) {\n throw new Error('At least one --target value is required');\n }\n\n args.manifestPath = auditMediaAssets.normalizeRepoPath(args.manifestPath);\n return args;\n}\n\nfunction parseCsvList(raw) {\n return [...new Set(\n String(raw || '')\n .split(',')\n .map((entry) => String(entry || '').trim())\n .filter(Boolean)\n )];\n}\n\nfunction normalizeOptionalRepoPath(value) {\n const trimmed = String(value || '').trim();\n return trimmed ? auditMediaAssets.normalizeRepoPath(trimmed) : '';\n}\n\nfunction absoluteRepoPath(repoPath) {\n return path.join(REPO_ROOT, auditMediaAssets.normalizeRepoPath(repoPath));\n}\n\nfunction ensureDirForFile(repoPath) {\n fs.mkdirSync(path.dirname(absoluteRepoPath(repoPath)), { recursive: true });\n}\n\nfunction readJson(repoPath) {\n return JSON.parse(fs.readFileSync(absoluteRepoPath(repoPath), 'utf8'));\n}\n\nfunction writeJson(repoPath, value) {\n ensureDirForFile(repoPath);\n fs.writeFileSync(absoluteRepoPath(repoPath), `${JSON.stringify(value, null, 2)}\\n`);\n}\n\nfunction writeText(repoPath, value) {\n ensureDirForFile(repoPath);\n fs.writeFileSync(absoluteRepoPath(repoPath), value);\n}\n\nfunction runCommand(command, args, options = {}) {\n const result = spawnSync(command, args, {\n cwd: options.cwd || REPO_ROOT,\n encoding: options.encoding === 'buffer' ? null : 'utf8',\n stdio: options.stdio || ['ignore', 'pipe', 'pipe']\n });\n\n const stdout = options.encoding === 'buffer' ? result.stdout : String(result.stdout || '');\n const stderr = options.encoding === 'buffer' ? result.stderr : String(result.stderr || '');\n\n if (result.status !== 0 && !options.allowFailure) {\n const detail = String(stderr || stdout || `exit ${result.status}`).trim();\n throw new Error(`Command failed: ${command} ${args.join(' ')}\\n${detail}`);\n }\n\n return {\n status: result.status,\n stdout,\n stderr\n };\n}\n\nfunction runGit(args, options = {}) {\n return runCommand('git', args, options);\n}\n\nfunction hasCachedChanges(cwd) {\n const result = spawnSync('git', ['diff', '--cached', '--quiet'], {\n cwd,\n stdio: 'ignore'\n });\n return result.status === 1;\n}\n\nfunction buildCanonicalAssetUrl(assetPath) {\n return `${RAW_GITHUB_BASE}/${encodeURI(auditMediaAssets.normalizeRepoPath(assetPath))}`;\n}\n\nfunction buildRawUrlRegex(assetPath) {\n return auditMediaAssets.buildRawGitHubRegex(assetPath, \"[^/\\\"'\\\\s)]+\");\n}\n\nfunction buildDirectTokens(sourcePath, assetPath) {",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/migrate-assets-to-branch.test.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/migrate-assets-to-branch.test.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tests/integration/mdx-component-runtime-smoke.js",
+ "script": "mdx-component-runtime-smoke",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests/integration, .githooks/server-manager.js, tests/run-pr-checks.js",
+ "owner": "docs",
+ "needs": "E-R1, R-R29",
+ "purpose_statement": "Smoke-tests sentinel MDX routes for runtime component failures, focused on page-killing render errors from MDX-imported JSX modules.",
+ "pipeline_declared": "manual",
+ "usage": "node tests/integration/mdx-component-runtime-smoke.js [--routes route[,route...]] [--base-url http://localhost:3000]",
+ "header": "#!/usr/bin/env node\n/**\n * @script mdx-component-runtime-smoke\n * @category validator\n * @purpose qa:content-quality\n * @scope tests/integration, .githooks/server-manager.js, tests/run-pr-checks.js\n * @owner docs\n * @needs E-R1, R-R29\n * @purpose-statement Smoke-tests sentinel MDX routes for runtime component failures, focused on page-killing render errors from MDX-imported JSX modules.\n * @pipeline manual\n * @usage node tests/integration/mdx-component-runtime-smoke.js [--routes route[,route...]] [--base-url http://localhost:3000]\n */\n\nconst path = require('path');\n\nlet puppeteer;\ntry {\n puppeteer = require('puppeteer');\n} catch (_error) {\n puppeteer = require(path.join(process.cwd(), 'tools', 'node_modules', 'puppeteer'));\n}\nconst {\n ensureServerRunning,\n stopServer,\n getServerUrl\n} = require('../../.githooks/server-manager');\n\nconst DEFAULT_TIMEOUT_MS = 30000;\nconst SENTINEL_ROUTES = [\n '/v2/home/mission-control',\n '/v2/about/portal',\n '/v2/developers/portal',\n '/v2/gateways/gateways-portal',\n '/v2/resources/documentation-guide/component-library/primitives'\n];\nconst SENTINEL_ROUTE_FILES = new Set([\n 'v2/home/mission-control.mdx',\n 'v2/about/portal.mdx',\n 'v2/developers/portal.mdx',\n 'v2/gateways/gateways-portal.mdx',\n 'v2/resources/documentation-guide/component-library/primitives.mdx'\n]);\nconst BLOCKING_CONSOLE_PATTERNS = [\n /ReferenceError/i,\n /Cannot access/i,\n /is not defined/i,\n /BlinkingIcon/i,\n /normalizeIconName/i,\n /normalizeIconSize/i\n];\nconst BLOCKING_BODY_PATTERNS = [\n /Failed to render/i,\n /ReferenceError/i,\n /normalizeIconName/i,\n /normalizeIconSize/i\n];\nconst COMPONENT_CHANGE_RE = /^snippets\\/components\\/.+\\.jsx$/;\n\nfunction toPosix(value) {\n return String(value || '').replace(/\\\\/g, '/');\n}\n\nfunction usage() {\n console.log(\n [\n 'Usage: node tests/integration/mdx-component-runtime-smoke.js [options]',\n '',\n 'Options:',\n ` --routes Override sentinel routes (default: ${SENTINEL_ROUTES.join(', ')})`,\n ' --base-url Use an existing Mintlify base URL instead of server-manager discovery',\n ' --help, -h Show this message'\n ].join('\\n')\n );\n}\n\nfunction parseArgs(argv) {\n const args = {\n routes: [],\n baseUrl: String(process.env.MINT_BASE_URL || '').trim(),\n help: false\n };\n\n for (let index = 0; index < argv.length; index += 1) {\n const token = argv[index];\n\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n\n if (token === '--routes') {\n const raw = String(argv[index + 1] || '').trim();\n if (raw) {\n raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((route) => args.routes.push(route));\n }\n index += 1;\n continue;\n }\n\n if (token.startsWith('--routes=')) {\n token\n .slice('--routes='.length)\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((route) => args.routes.push(route));\n continue;\n }\n\n if (token === '--base-url') {\n args.baseUrl = String(argv[index + 1] || '').trim();\n index += 1;\n continue;\n }\n\n if (token.startsWith('--base-url=')) {\n args.baseUrl = token.slice('--base-url='.length).trim();\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n args.routes = [...new Set(args.routes)];\n return args;\n}\n\nfunction normalizeRoute(route) {\n const raw = String(route || '').trim();\n if (!raw) return '';\n return raw.startsWith('/') ? raw : `/${raw}`;\n}\n\nfunction getRoutes(args = {}) {\n const routes = Array.isArray(args.routes) && args.routes.length > 0 ? args.routes : SENTINEL_ROUTES;\n return [...new Set(routes.map(normalizeRoute).filter(Boolean))];\n}\n\nfunction isBlockingConsoleMessage(type, text) {\n if (String(type || '').toLowerCase() !== 'error') return false;\n return BLOCKING_CONSOLE_PATTERNS.some((pattern) => pattern.test(String(text || '')));\n}\n\nfunction isBlockingPageError(message) {\n return BLOCKING_CONSOLE_PATTERNS.some((pattern) => pattern.test(String(message || '')));\n}\n\nfunction classifyDomFailure(snapshot) {\n const status = Number(snapshot?.status || 0);\n const bodyText = String(snapshot?.bodyText || '');\n const bodyLength = Number(snapshot?.bodyLength || 0);\n\n if (status === 404) {\n return `Route returned HTTP 404`;\n }\n\n if (snapshot?.is404) {\n return 'Page rendered a 404 state';\n }\n\n if (snapshot?.hasErrorBoundary) {\n return 'Page rendered an error boundary';\n }\n\n if (bodyLength < 500) {\n return `Page appears empty or failed to render (${bodyLength} chars)`;\n }\n\n const blockingBodyPattern = BLOCKING_BODY_PATTERNS.find((pattern) => pattern.test(bodyText));\n if (blockingBodyPattern) {\n return `Page body includes blocking runtime error text (${blockingBodyPattern})`;\n }\n\n return '';\n}\n\nfunction shouldRunForChangedFiles(changedFiles) {\n return changedFiles.some((filePath) => {\n const relPath = toPosix(filePath);\n return (\n COMPONENT_CHANGE_RE.test(relPath) ||\n relPath === 'tools/scripts/validators/components/check-mdx-component-scope.js' ||\n relPath === 'tests/integration/mdx-component-runtime-smoke.js' ||\n SENTINEL_ROUTE_FILES.has(relPath)\n );\n });\n}\n\nasync function testRoute(browser, route, baseUrl) {\n const page = await browser.newPage();\n const consoleErrors = [];\n const pageErrors = [];\n\n await page.setViewport({ width: 1440, height: 900 });\n\n page.on('console', (msg) => {\n const type = msg.type();\n const text = msg.text();\n if (isBlockingConsoleMessage(type, text)) {\n consoleErrors.push(`Console ${type}: ${text}`);\n }\n });\n\n page.on('pageerror', (error) => {\n if (isBlockingPageError(error.message)) {\n pageErrors.push(`Page error: ${error.message}`);\n }\n });\n\n let response;\n try {\n response = await page.goto(`${baseUrl}${route}`, {\n waitUntil: 'networkidle2',\n timeout: DEFAULT_TIMEOUT_MS\n });\n await new Promise((resolve) => setTimeout(resolve, 1500));",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/mdx-component-runtime-smoke.test.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/mdx-component-runtime-smoke.test.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tests/utils/spell-checker.js",
+ "script": "spell-checker",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Spell checker utility — checks text against custom dictionary with en-GB locale support",
+ "pipeline_declared": "indirect — library module",
+ "usage": "node tests/utils/spell-checker.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script spell-checker\n * @category validator\n * @purpose qa:content-quality\n * @scope tests\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Spell checker utility — checks text against custom dictionary with en-GB locale support\n * @pipeline indirect — library module\n * @dualmode dual-mode (document flags)\n * @usage node tests/utils/spell-checker.js [flags]\n */\n/**\n * Spell checking utilities using cspell\n */\n\nconst { execSync, execFileSync } = require('child_process');\nconst path = require('path');\nconst fs = require('fs');\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nfunction resolveCspellConfig(configPath = null) {\n if (configPath && fs.existsSync(configPath)) {\n return configPath;\n }\n\n const repoRoot = getRepoRoot();\n const candidates = [\n path.join(process.cwd(), 'cspell.json'),\n path.join(repoRoot, 'cspell.json'),\n path.join(repoRoot, 'tools', 'config', 'cspell.json')\n ];\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) {\n return candidate;\n }\n }\n\n return path.join(repoRoot, 'tools', 'config', 'cspell.json');\n}\n\nfunction resolveCspellBinary() {\n if (process.env.CSPELL_BIN) {\n return { bin: process.env.CSPELL_BIN, viaNpx: false };\n }\n\n const repoRoot = getRepoRoot();\n const candidates = [\n path.join(repoRoot, 'tools', 'node_modules', '.bin', 'cspell'),\n path.join(repoRoot, 'tests', 'node_modules', '.bin', 'cspell')\n ];\n\n for (const candidate of candidates) {\n if (fs.existsSync(candidate)) {\n return { bin: candidate, viaNpx: false };\n }\n }\n\n return { bin: 'npx', viaNpx: true };\n}\n\n/**\n * Check spelling in a file\n */\nfunction checkSpelling(filePath, configPath = null) {\n const cspellConfig = resolveCspellConfig(configPath);\n const cspell = resolveCspellBinary();\n \n try {\n let result;\n if (cspell.viaNpx) {\n result = execSync(\n `npx cspell --no-progress --config \"${cspellConfig}\" \"${filePath}\"`,\n { encoding: 'utf8', stdio: 'pipe' }\n );\n } else {\n result = execFileSync(\n cspell.bin,\n ['--no-progress', '--config', cspellConfig, filePath],\n { encoding: 'utf8', stdio: 'pipe' }\n );\n }\n return { errors: [], output: result };\n } catch (error) {\n // Parse cspell output\n const output = error.stdout || error.stderr || error.message;\n const errors = parseCspellOutput(output, filePath);\n return { errors, output };\n }\n}\n\n/**\n * Parse cspell output to extract errors\n */\nfunction parseCspellOutput(output, filePath) {\n const errors = [];\n const lines = output.split('\\n');\n \n for (const line of lines) {\n // cspell output format: filePath:line:col - Unknown word: \"word\"\n const match = line.match(/:(\\d+):(\\d+)\\s*-\\s*Unknown word:\\s*\"([^\"]+)\"/);\n if (match) {\n errors.push({\n line: parseInt(match[1]),\n column: parseInt(match[2]),\n word: match[3],\n file: filePath\n });\n }\n }\n \n return errors;\n}\n\n/**\n * Check multiple files\n */\nfunction checkMultipleFiles(filePaths, configPath = null) {\n const results = [];\n \n for (const filePath of filePaths) {\n if (fs.existsSync(filePath)) {\n const result = checkSpelling(filePath, configPath);\n results.push({\n file: filePath,\n ...result\n });\n }\n }\n \n return results;\n}\n\nmodule.exports = {\n checkSpelling,\n checkMultipleFiles,\n parseCspellOutput,\n resolveCspellBinary,\n resolveCspellConfig\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/spelling.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/remediators/content/repair-spelling.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/spelling.test.js; indirect via tools/scripts/remediators/content/repair-spelling.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/scoring.js",
+ "script": "scoring",
+ "category": "utility",
+ "purpose": "qa:content-quality",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "Aggregates rule scores into a final usefulness score per page.",
+ "pipeline_declared": "indirect — library module",
+ "usage": "const { score } = require('../lib/docs-usefulness/scoring');",
+ "header": "'use strict';\n/**\n * @script scoring\n * @category utility\n * @purpose qa:content-quality\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement Aggregates rule scores into a final usefulness score per page.\n * @pipeline indirect — library module\n * @usage const { score } = require('../lib/docs-usefulness/scoring');\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst {\n extractFrontmatter,\n extractImports,\n validateMdx\n} = require('../../../tests/utils/mdx-parser');\nconst {\n loadRubric,\n getRulesForPage\n} = require('./rubric-loader');\nconst { EVALUATORS } = require('./rule-evaluators');\nconst { runQualityGate } = require('./quality-gate');\n\nfunction toPosix(filePath) {\n return String(filePath || '').split(path.sep).join('/');\n}\n\nfunction firstOf(...values) {\n for (const value of values) {\n if (value !== undefined && value !== null) return value;\n }\n return null;\n}\n\nfunction getFrontmatterValues(page) {\n if (!page) return {};\n if (page.frontmatter && page.frontmatter.data && typeof page.frontmatter.data === 'object') {\n return page.frontmatter.data;\n }\n if (page.frontmatter && typeof page.frontmatter === 'object') {\n return page.frontmatter;\n }\n if (page.frontmatterData && typeof page.frontmatterData === 'object') {\n return page.frontmatterData;\n }\n return {};\n}\n\nfunction stripFrontmatter(content) {\n return String(content || '').replace(/^---\\s*\\n[\\s\\S]*?\\n---\\s*\\n?/, '');\n}\n\nfunction stripMdxToText(content) {\n return String(content || '')\n .replace(/```[\\s\\S]*?```/g, ' ')\n .replace(/`[^`]+`/g, ' ')\n .replace(/^import\\s.+$/gm, ' ')\n .replace(/\\{\\/\\*[\\s\\S]*?\\*\\/\\}/g, ' ')\n .replace(/<[^>]+>/g, ' ')\n .replace(/\\[[^\\]]+\\]\\(([^)]+)\\)/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction parseHeadings(content) {\n const headings = [];\n String(content || '')\n .split('\\n')\n .forEach((line) => {\n const match = line.match(/^(#{1,6})\\s+(.+)$/);\n if (match) {\n headings.push({ level: match[1].length, text: match[2].trim() });\n }\n });\n return headings;\n}\n\nfunction parseCodeBlocks(content) {\n const blocks = [];\n const regex = /```([A-Za-z0-9_-]+)?\\n([\\s\\S]*?)```/g;\n let match;\n while ((match = regex.exec(String(content || ''))) !== null) {\n blocks.push({\n language: String(match[1] || '').toLowerCase(),\n content: String(match[2] || '')\n });\n }\n return blocks;\n}\n\nfunction parseTables(content) {\n const tables = [];\n const lines = String(content || '').split('\\n');\n for (let i = 0; i < lines.length - 1; i += 1) {\n const line = lines[i].trim();\n const next = lines[i + 1].trim();\n if (!line.includes('|') || !next.includes('|')) continue;\n if (!/^\\|?\\s*:?-{3,}/.test(next)) continue;\n tables.push({\n startLine: i + 1,\n header: line,\n separator: next\n });\n }\n return tables;\n}\n\nfunction parseMarkdownLinks(content) {\n const links = [];\n const regex = /\\[[^\\]]+\\]\\(([^)]+)\\)/g;\n let match;\n while ((match = regex.exec(String(content || ''))) !== null) {\n links.push(match[1].trim());\n }\n return links;\n}\n\nfunction parseHrefLinks(content) {\n const links = [];\n const regex = /\\bhref\\s*=\\s*[\"']([^\"']+)[\"']/gi;\n let match;\n while ((match = regex.exec(String(content || ''))) !== null) {\n links.push(match[1].trim());\n }\n return links;\n}\n\nfunction parseImages(content) {\n const images = [];\n\n const markdown = /!\\[([^\\]]*)\\]\\(([^)]+)\\)/g;\n let match;\n while ((match = markdown.exec(String(content || ''))) !== null) {\n images.push({ alt: String(match[1] || '').trim(), src: String(match[2] || '').trim() });\n }\n\n const jsxImg = /<(?:img|Image)\\b[^>]*>/g;\n while ((match = jsxImg.exec(String(content || ''))) !== null) {\n const tag = match[0];\n const src = (tag.match(/\\bsrc\\s*=\\s*[\"']([^\"']+)[\"']/i) || [])[1] || '';\n const alt = (tag.match(/\\balt\\s*=\\s*[\"']([^\"']+)[\"']/i) || [])[1] || '';\n if (src) {\n images.push({ alt: String(alt || '').trim(), src: String(src || '').trim() });\n }\n }\n\n return images;\n}\n\nfunction parseComponents(content) {\n const components = new Set();\n const regex = /<([A-Z][A-Za-z0-9]*)\\b/g;\n let match;\n while ((match = regex.exec(String(content || ''))) !== null) {\n components.add(match[1]);\n }\n return [...components];\n}\n\nfunction parseSectionFromPath(filePath) {\n const rel = toPosix(filePath).replace(/^\\/+/, '');\n const parts = rel.split('/');\n if (parts[0] === 'v2' && parts[1]) return parts[1];\n return parts[0] || 'unknown';\n}\n\nfunction resolveImportPath(repoRoot, currentFile, importPath) {\n const relImport = String(importPath || '');\n if (!relImport) return null;\n\n const candidates = [];\n if (relImport.startsWith('/')) {\n candidates.push(path.join(repoRoot, relImport));\n } else {\n const dir = path.dirname(path.join(repoRoot, currentFile));\n candidates.push(path.join(dir, relImport));\n }\n\n return candidates.some((candidate) => {\n const withExt = [candidate, `${candidate}.js`, `${candidate}.jsx`, `${candidate}.ts`, `${candidate}.tsx`, `${candidate}.mdx`, `${candidate}.md`, path.join(candidate, 'index.js'), path.join(candidate, 'index.mdx')];\n return withExt.some((target) => fs.existsSync(target));\n });\n}\n\nfunction analyzeMdxPage({ content, filePath, routePath, repoRoot = process.cwd() }) {\n const rawContent = String(content || '');\n const frontmatterMeta = extractFrontmatter(rawContent);\n const frontmatterValues = frontmatterMeta?.data && typeof frontmatterMeta.data === 'object' ? frontmatterMeta.data : {};\n const body = stripFrontmatter(rawContent);\n const textContent = stripMdxToText(body);\n const headings = parseHeadings(body);\n const codeBlocks = parseCodeBlocks(rawContent);\n const tables = parseTables(rawContent);\n const images = parseImages(rawContent);\n const links = [...parseMarkdownLinks(rawContent), ...parseHrefLinks(rawContent)];\n const internalLinks = [...new Set(links.filter((link) => !/^https?:\\/\\//i.test(link) && !/^mailto:/i.test(link)))];\n const externalLinks = [...new Set(links.filter((link) => /^https?:\\/\\//i.test(link)))];\n const components = parseComponents(rawContent);\n const imports = extractImports(rawContent);\n const parseValidation = validateMdx(rawContent, filePath);\n\n const flags = [];\n if (!rawContent.trim()) flags.push('empty');\n if (!frontmatterMeta.exists) flags.push('missing_frontmatter');\n if (frontmatterMeta.exists && frontmatterMeta.data === null) flags.push('invalid_frontmatter');\n if (frontmatterMeta.exists && frontmatterValues && !frontmatterValues.title) flags.push('missing_title');\n if (frontmatterMeta.exists && frontmatterValues && !frontmatterValues.description) flags.push('missing_description');\n if (/\\bTODO\\b|\\bTBD\\b/i.test(rawContent)) flags.push('todo_marker');\n if (/Coming Soon/i.test(rawContent)) flags.push('coming_soon');\n if (/\\/v2\\/pages\\//i.test(rawContent)) flags.push('legacy_v2_pages_link');\n\n const unresolvedImports = imports.filter((imp) => {\n const impPath = String(imp.path || '');\n if (impPath.startsWith('http')) return false;\n if (/^(react|next|fs|path|os|crypto|assert|child_process|node:)/.test(impPath)) return false;\n return !resolveImportPath(repoRoot, filePath, impPath);",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/usefulness-rubric.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/notion/sync-v2-en-canonical.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/assign-purpose-metadata.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/audit-v2-usefulness.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/usefulness-rubric.test.js; indirect via tools/notion/sync-v2-en-canonical.js; indirect via tools/scripts/assign-purpose-metadata.js; indirect via tools/scripts/audit-v2-usefulness.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/mdx-safe-markdown.js",
+ "script": "mdx-safe-markdown",
+ "category": "utility",
+ "purpose": "qa:content-quality",
+ "scope": "full-repo",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Shared MDX-safe markdown helpers that collect first-party markdown files, detect unsafe patterns, and apply deterministic repairs.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "node tools/lib/mdx-safe-markdown.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script mdx-safe-markdown\n * @category utility\n * @purpose qa:content-quality\n * @scope full-repo\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Shared MDX-safe markdown helpers that collect first-party markdown files, detect unsafe patterns, and apply deterministic repairs.\n * @pipeline indirect -- library module\n * @usage node tools/lib/mdx-safe-markdown.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst { unified } = require('unified');\n\nfunction loadPlugin(name) {\n const plugin = require(name);\n return plugin.default || plugin;\n}\n\nconst remarkParse = loadPlugin('remark-parse');\nconst remarkGfm = loadPlugin('remark-gfm');\nconst remarkMath = loadPlugin('remark-math');\nconst remarkMdx = loadPlugin('remark-mdx');\n\nconst REPO_ROOT = path.resolve(__dirname, '..', '..');\nconst STAGED_SNAPSHOT_ENV = 'LPD_STAGED_FILES_SNAPSHOT';\nconst MARKDOWN_EXTENSIONS = new Set(['.md', '.mdx']);\nconst PLACEHOLDER_TAG_RE = /<([a-z][a-z0-9-]{1,})>/g;\nconst RAW_BR_RE = /
(?!\\s*\\/>)/g;\nconst COMPARISON_RE = /(^|[\\s([{])(<={0,1}|>={0,1})(?=\\s*\\d)/g;\nconst HTML_TAG_ALLOWLIST = new Set([\n 'a',\n 'abbr',\n 'b',\n 'blockquote',\n 'body',\n 'br',\n 'caption',\n 'code',\n 'dd',\n 'del',\n 'details',\n 'div',\n 'dl',\n 'dt',\n 'em',\n 'figcaption',\n 'figure',\n 'h1',\n 'h2',\n 'h3',\n 'h4',\n 'h5',\n 'h6',\n 'head',\n 'hr',\n 'html',\n 'i',\n 'img',\n 'kbd',\n 'li',\n 'main',\n 'mark',\n 'ol',\n 'p',\n 'picture',\n 'pre',\n 'q',\n 's',\n 'section',\n 'select',\n 'small',\n 'source',\n 'span',\n 'strong',\n 'style',\n 'sub',\n 'summary',\n 'sup',\n 'table',\n 'tbody',\n 'td',\n 'tfoot',\n 'th',\n 'thead',\n 'time',\n 'tr',\n 'u',\n 'ul',\n 'option'\n]);\nconst PLACEHOLDER_ALIASES = {\n issue: 'ISSUE_ID',\n 'issue-id': 'ISSUE_ID',\n slug: 'TASK_SLUG',\n task: 'TASK_ID',\n 'task-id': 'TASK_ID',\n id: 'ID',\n path: 'TARGET_PATH',\n 'file-path': 'FILE_PATH',\n file: 'FILE_PATH',\n branch: 'BRANCH_NAME',\n scope: 'PATH_SCOPE'\n};\nconst MDX_PROCESSOR = unified()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkMath)\n .use(remarkMdx);\n\nfunction normalizeRepoPath(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction normalizeInline(value) {\n return String(value || '')\n .replace(/\\r?\\n/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction escapeRegExp(value) {\n return String(value || '').replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction isMarkdownFile(repoPath) {\n return MARKDOWN_EXTENSIONS.has(path.extname(String(repoPath || '')).toLowerCase());\n}\n\nfunction shouldExcludeMarkdownPath(repoPath) {\n const relPath = normalizeRepoPath(repoPath).replace(/^\\/+/, '');\n if (!relPath) return true;\n if (!isMarkdownFile(relPath)) return true;\n\n return (\n relPath.startsWith('.git/') ||\n relPath.startsWith('node_modules/') ||\n relPath.includes('/.git/') ||\n relPath.includes('/node_modules/') ||\n relPath.startsWith('.venv/') ||\n relPath.includes('/.venv/') ||\n relPath.startsWith('tmp/') ||\n relPath.includes('/tmp/') ||\n relPath.includes('/__pycache__/') ||\n relPath.includes('/.next/') ||\n relPath.endsWith('.bak') ||\n relPath.endsWith('.disabled')\n );\n}\n\nfunction isEligibleRepoMarkdownPath(repoPath) {\n return !shouldExcludeMarkdownPath(repoPath);\n}\n\nfunction getRepoRoot(rootDir = REPO_ROOT) {\n try {\n return execSync('git rev-parse --show-toplevel', {\n cwd: rootDir,\n encoding: 'utf8'\n }).trim();\n } catch (_error) {\n return rootDir;\n }\n}\n\nfunction readFileSafe(filePath) {\n try {\n return fs.readFileSync(filePath, 'utf8');\n } catch (_error) {\n return '';\n }\n}\n\nfunction walkMarkdownFiles(dirPath, repoRoot, out = []) {\n if (!fs.existsSync(dirPath)) return out;\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.name === '.git' || entry.name === 'node_modules' || entry.name === '.venv' || entry.name === '__pycache__' || entry.name === '.next') {\n continue;\n }\n\n const absPath = path.join(dirPath, entry.name);\n if (entry.isDirectory()) {\n walkMarkdownFiles(absPath, repoRoot, out);\n continue;\n }\n\n const relPath = normalizeRepoPath(path.relative(repoRoot, absPath));\n if (isEligibleRepoMarkdownPath(relPath)) {\n out.push(absPath);\n }\n }\n\n return out;\n}\n\nfunction getAllRepoMarkdownFiles(rootDir = REPO_ROOT) {\n const repoRoot = getRepoRoot(rootDir);\n return walkMarkdownFiles(repoRoot, repoRoot).sort((left, right) =>\n normalizeRepoPath(path.relative(repoRoot, left)).localeCompare(normalizeRepoPath(path.relative(repoRoot, right)))\n );\n}\n\nfunction getStagedRepoMarkdownFiles(rootDir = REPO_ROOT) {\n const repoRoot = getRepoRoot(rootDir);\n const snapshot = String(process.env[STAGED_SNAPSHOT_ENV] || '')\n .split(/\\r?\\n/)\n .map((entry) => entry.trim())\n .filter(Boolean);\n\n const relFiles = snapshot.length > 0\n ? snapshot\n : String(\n execSync('git diff --cached --name-only --diff-filter=ACMR', {\n cwd: repoRoot,",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/mdx-safe-markdown.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/generate-component-governance-remediation-reports.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/remediators/content/repair-mdx-safe-markdown.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/validators/content/check-mdx-safe-markdown.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/run-pr-checks.js; indirect via tests/unit/mdx-safe-markdown.test.js; indirect via tools/scripts/generate-component-governance-remediation-reports.js; indirect via tools/scripts/remediators/content/repair-mdx-safe-markdown.js; indirect via tools/scripts/validators/content/check-mdx-safe-markdown.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/audit-v2-usefulness.js",
+ "script": "audit-v2-usefulness",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tools/scripts, v2, tasks/reports, tools/config",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Usefulness auditor — scores v2 MDX pages on human and agent usefulness with source-weighted 2026 accuracy verification",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/audit-v2-usefulness.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script audit-v2-usefulness\n * @category validator\n * @purpose qa:content-quality\n * @scope tools/scripts, v2, tasks/reports, tools/config\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Usefulness auditor — scores v2 MDX pages on human and agent usefulness with source-weighted 2026 accuracy verification\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/audit-v2-usefulness.js [flags]\n */\n\n'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst { execSync } = require('child_process');\nconst {\n getDocsJsonRouteKeys,\n toDocsRouteKeyFromFileV2Aware\n} = require('../../tests/utils/file-walker');\nconst { analyzeMdxPage, scorePage, computeBand } = require('../lib/docs-usefulness/scoring');\nconst { getRulesForPage } = require('../lib/docs-usefulness/rubric-loader');\nconst { checkJourneys } = require('../lib/docs-usefulness/journey-check');\nconst { LlmEvaluator } = require('../lib/docs-usefulness/llm-evaluator');\nconst prompts = require('../lib/docs-usefulness/prompts');\nconst { loadAndValidateUsefulnessConfig } = require('../lib/docs-usefulness/config-validator');\n\nconst REMOVED_FLAGS = new Set([\n '--accuracy-mode',\n '--as-of',\n '--verify-sources',\n '--github-repos',\n '--deepwiki-enabled',\n '--deepwiki-base-url',\n '--official-docs-base-url',\n '--github-results-per-repo',\n '--verification-cache-dir',\n '--verification-max-requests',\n '--verification-timeout-ms',\n '--verification-fixture',\n '--scoring-engine'\n]);\n\nconst BASELINE_DEFAULT_REL = path.join(\n 'tools',\n 'config',\n 'usefulness-baselines',\n 'full-run-2026-02-23-page-matrix.csv'\n);\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nfunction toPosix(filePath) {\n return String(filePath || '').split(path.sep).join('/');\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction parseCsv(raw, fallback = []) {\n if (!raw) return [...fallback];\n return String(raw)\n .split(',')\n .map((value) => value.trim())\n .filter(Boolean);\n}\n\nfunction parseBoolean(raw, fallback) {\n if (raw === undefined || raw === null) return fallback;\n const value = String(raw).trim().toLowerCase();\n if (['1', 'true', 'yes', 'on'].includes(value)) return true;\n if (['0', 'false', 'no', 'off'].includes(value)) return false;\n return fallback;\n}\n\nfunction parseArgs(argv, repoRoot) {\n for (const token of argv) {\n if (REMOVED_FLAGS.has(token)) {\n throw new Error(`Deprecated flag ${token} is removed. Run without accuracy-verification options; usefulness audit is now purpose-aware by default.`);\n }\n }\n\n const args = {\n mode: 'full',\n files: [],\n outDir: path.join(repoRoot, 'tasks', 'reports', 'quality-accessibility', 'docs-usefulness', 'latest'),\n format: ['jsonl', 'csv', 'json', 'md'],\n maxPages: null,\n llm: false,\n llmTier: 'free',\n llmBudget: null,\n llmSample: 100,\n llmResume: true,\n journey: true,\n purposeAware: true,\n legacyRubric: false\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--mode') {\n args.mode = String(argv[i + 1] || args.mode).trim();\n i += 1;\n continue;\n }\n if (token === '--files' || token === '--file') {\n args.files.push(...parseCsv(argv[i + 1], []));\n i += 1;\n continue;\n }\n if (token === '--out-dir') {\n const raw = String(argv[i + 1] || '').trim();\n if (raw) args.outDir = path.resolve(repoRoot, raw);\n i += 1;\n continue;\n }\n if (token === '--format') {\n args.format = parseCsv(argv[i + 1], args.format);\n i += 1;\n continue;\n }\n if (token === '--max-pages') {\n const parsed = Number(argv[i + 1]);\n if (Number.isFinite(parsed) && parsed > 0) {\n args.maxPages = parsed;\n }\n i += 1;\n continue;\n }\n if (token === '--llm') {\n args.llm = true;\n continue;\n }\n if (token === '--llm-tier') {\n args.llmTier = String(argv[i + 1] || args.llmTier).trim();\n i += 1;\n continue;\n }\n if (token === '--llm-budget') {\n const parsed = Number(argv[i + 1]);\n if (Number.isFinite(parsed) && parsed >= 0) args.llmBudget = parsed;\n i += 1;\n continue;\n }\n if (token === '--llm-sample') {\n const parsed = Number(argv[i + 1]);\n if (Number.isFinite(parsed) && parsed >= 1 && parsed <= 100) args.llmSample = parsed;\n i += 1;\n continue;\n }\n if (token === '--llm-resume') {\n const next = argv[i + 1];\n if (next && !next.startsWith('--')) {\n args.llmResume = parseBoolean(next, true);\n i += 1;\n } else {\n args.llmResume = true;\n }\n continue;\n }\n if (token === '--journey') {\n args.journey = true;\n continue;\n }\n if (token === '--no-journey') {\n args.journey = false;\n continue;\n }\n if (token === '--purpose-aware') {\n args.purposeAware = true;\n continue;\n }\n if (token === '--legacy-rubric') {\n args.legacyRubric = true;\n continue;\n }\n }\n\n args.files = [...new Set(args.files)];\n if (args.files.length > 0) {\n args.mode = 'files';\n }\n if (args.mode === 'files') {\n args.journey = false;\n }\n if (args.legacyRubric) {\n args.purposeAware = false;\n }\n\n return args;\n}\n\nfunction normalizeRouteKey(value) {\n let route = toPosix(value || '').trim();\n route = route.replace(/^\\/+/, '');\n route = route.replace(/\\.(md|mdx)$/i, '');\n route = route.replace(/\\/index$/i, '');\n route = route.replace(/\\/+/g, '/');\n route = route.replace(/\\s+$/g, '');\n return route;\n}\n\nfunction collectPagesFromNode(node, out = []) {\n if (typeof node === 'string') {\n const value = normalizeRouteKey(node);\n if (value.startsWith('v2/')) out.push(value);\n return out;\n }\n\n if (Array.isArray(node)) {",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/usefulness-rubric.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/docs-quality-and-freshness-audit.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/usefulness-rubric.test.js; indirect via tools/scripts/docs-quality-and-freshness-audit.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/dev/add-callouts.js",
+ "script": "add-callouts",
+ "category": "remediator",
+ "purpose": "qa:content-quality",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Callout inserter — adds Note/Tip/Warning callout components to MDX files based on content patterns",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/dev/add-callouts.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script add-callouts\n * @category remediator\n * @purpose qa:content-quality\n * @scope tools/scripts\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Callout inserter — adds Note/Tip/Warning callout components to MDX files based on content patterns\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/dev/add-callouts.js [flags]\n */\n\n/**\n * Script to add callouts to MDX pages\n * \n * This script processes all MDX files in v2 docs folders and adds appropriate callouts:\n * - ComingSoonCallout for pages with no content (only metadata/title)\n * - PreviewCallout for pages with content\n * \n * Usage: node add-callouts.js [--dry-run] [--remove]\n */\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst REPO_ROOT = path.join(__dirname, '../../../');\nconst DOCS_JSON_PATH = path.join(REPO_ROOT, 'docs.json');\n\nconst COMING_SOON_IMPORT = \"import {ComingSoonCallout} from '/snippets/components/primitives/previewCallouts.jsx'\";\nconst COMING_SOON_COMPONENT = ' ';\nconst PREVIEW_IMPORT = \"import { PreviewCallout } from '/snippets/components/primitives/previewCallouts.jsx'\";\nconst PREVIEW_COMPONENT = ' ';\n\n/**\n * Check if a page has substantial content beyond metadata\n * @param {string} content - The file content\n * @returns {boolean} - True if page has content, false otherwise\n */\nfunction hasContent(content) {\n // Split by metadata delimiter\n const parts = content.split('---');\n \n if (parts.length < 3) {\n return false; // No proper metadata structure\n }\n \n // Get content after metadata (parts[2] onwards)\n const afterMetadata = parts.slice(2).join('---').trim();\n \n // Remove imports\n const withoutImports = afterMetadata.replace(/^import\\s+.*$/gm, '').trim();\n \n // Remove existing callouts\n const withoutCallouts = withoutImports\n .replace(/]*\\/>/g, '')\n .replace(/]*\\/>/g, '')\n .trim();\n \n // Check if there's meaningful content (more than just whitespace or a single heading)\n const lines = withoutCallouts.split('\\n').filter(line => line.trim().length > 0);\n \n // If only one line and it's a heading, consider it as no content\n if (lines.length === 0 || (lines.length === 1 && lines[0].trim().startsWith('#'))) {\n return false;\n }\n \n return true;\n}\n\n/**\n * Collect v2 English nav page paths from docs.json.\n * @returns {string[]} - Array of page paths like \"v2/...\"\n */\nfunction getV2EnglishNavPages() {\n if (!fs.existsSync(DOCS_JSON_PATH)) {\n throw new Error(`docs.json not found at ${DOCS_JSON_PATH}`);\n }\n\n const docsJson = JSON.parse(fs.readFileSync(DOCS_JSON_PATH, 'utf8'));\n const versions = (docsJson.navigation && docsJson.navigation.versions) || [];\n const v2 = versions.find((version) => version.version === 'v2');\n if (!v2) return [];\n\n const languages = v2.languages || [];\n const en = languages.find((lang) => lang.language === 'en');\n if (!en) return [];\n\n const pages = new Set();\n\n function collect(node) {\n if (!node) return;\n if (Array.isArray(node)) {\n node.forEach(collect);\n return;\n }\n if (typeof node === 'string') {\n const value = node.trim();\n if (value && value.startsWith('v2/')) {\n pages.add(value);\n }\n return;\n }\n if (typeof node === 'object') {\n collect(node.pages);\n collect(node.groups);\n collect(node.anchors);\n collect(node.tabs);\n }\n }\n\n collect(en.tabs);\n\n return Array.from(pages);\n}\n\n/**\n * Check if file already has a callout\n * @param {string} content - The file content\n * @returns {boolean} - True if callout exists\n */\nfunction hasCallout(content) {\n return content.includes(' {\n const match = line.match(importRegex);\n if (!match) return line;\n\n const quote = match[2];\n const hasSemicolon = line.trim().endsWith(';');\n const items = match[1]\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean);\n\n const filtered = items.filter((item) => {\n if (item === 'PreviewCallout') return usage.keepPreview;\n if (item === 'ComingSoonCallout') return usage.keepComing;\n return true;\n });\n\n if (filtered.length === 0) return null;\n\n return `import { ${filtered.join(', ')} } from ${quote}/snippets/components/primitives/previewCallouts.jsx${quote}${hasSemicolon ? ';' : ''}`;\n })\n .filter((line) => line !== null)\n .join('\\n');\n}\n\n/**\n * Remove top-level Preview/ComingSoon callout block (import + first callout tag).\n * @param {string} content - The file content\n * @returns {{ content: string, removed: boolean }}\n */\nfunction removeTopLevelCallout(content) {\n const parts = content.split('---');\n if (parts.length < 3) return { content, removed: false };\n\n const beforeMetadata = parts[0];\n const metadata = parts[1];\n const afterMetadata = parts.slice(2).join('---');\n\n const lines = afterMetadata.split('\\n');\n const importRegex = /^\\s*import\\s+\\{[^}]*\\}\\s+from\\s+['\"]\\/snippets\\/components\\/primitives\\/previewCallouts\\.jsx['\"];?\\s*$/;\n const calloutRegex = /^\\s*<\\s*(PreviewCallout|ComingSoonCallout)\\b[^>]*\\/>\\s*$/;\n\n let firstNonImportIndex = -1;\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i].trim();\n if (!trimmed) continue;\n if (/^\\s*import\\s+/.test(lines[i])) continue;\n firstNonImportIndex = i;\n break;\n }\n\n let calloutIndex = -1;\n if (firstNonImportIndex !== -1 && calloutRegex.test(lines[firstNonImportIndex])) {\n calloutIndex = firstNonImportIndex;\n }\n\n let importIndex = -1;\n if (calloutIndex !== -1) {\n for (let i = 0; i < calloutIndex; i++) {\n if (importRegex.test(lines[i]) && /(PreviewCallout|ComingSoonCallout)/.test(lines[i])) {\n importIndex = i;\n break;\n }\n }\n }\n\n if (calloutIndex === -1 && importIndex === -1) {\n return { content, removed: false };\n }\n\n const filteredLines = lines.filter((_, index) => index !== calloutIndex && index !== importIndex);\n let newAfter = filteredLines.join('\\n');\n\n const usage = {\n keepPreview: /]',\n '',\n 'Modes:',\n ' --dry-run Show deterministic spelling repairs without modifying files (default).',\n ' --write Apply deterministic spelling repairs and print per-file diffs.',\n '',\n 'Scope:',\n ' --staged Limit to staged routable v2 MDX files.',\n ' --files Limit to explicit MDX files (comma-separated, repeatable).',\n '',\n 'Safety:',\n ' - Repairs only prose ranges parsed from MDX text nodes.',\n ' - Never edits frontmatter, inline code, fenced code, or URL substrings.',\n ' - Applies a correction only when cspell gives a deterministic, dictionary-backed choice.'\n ].join('\\n')\n );\n}\n\nfunction parseArgs(argv) {\n const args = {\n mode: 'dry-run',\n stagedOnly: false,\n files: [],\n help: false\n };\n\n let explicitModeCount = 0;\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n if (token === '--dry-run') {\n args.mode = 'dry-run';\n explicitModeCount += 1;\n continue;\n }\n if (token === '--write') {\n args.mode = 'write';\n explicitModeCount += 1;\n continue;\n }\n if (token === '--staged') {\n args.stagedOnly = true;\n continue;\n }\n if (token === '--files') {\n const raw = String(argv[i + 1] || '').trim();\n if (!raw) throw new Error('Missing value for --files');\n raw\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean)\n .forEach((item) => args.files.push(item));\n i += 1;\n continue;\n }\n if (token.startsWith('--files=')) {\n token\n .slice('--files='.length)\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean)\n .forEach((item) => args.files.push(item));\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (explicitModeCount > 1) {\n throw new Error('Choose only one mode: --dry-run or --write');\n }\n\n if (args.stagedOnly && args.files.length > 0) {\n throw new Error('Use either --staged or --files, not both');\n }\n\n args.files = [...new Set(args.files)];\n return args;\n}\n\nfunction splitLeadingFrontmatter(content) {\n const text = String(content || '');\n const match = text.match(/^---\\r?\\n[\\s\\S]*?\\r?\\n---(?:\\r?\\n|$)/);\n if (!match) {\n return {\n frontmatter: '',\n body: text,\n bodyOffset: 0\n };\n }\n\n return {\n frontmatter: match[0],\n body: text.slice(match[0].length),\n bodyOffset: match[0].length\n };\n}\n\nfunction mergeRanges(ranges) {\n if (!Array.isArray(ranges) || ranges.length === 0) return [];\n const ordered = ranges\n .filter((range) => Number.isFinite(range.start) && Number.isFinite(range.end) && range.end > range.start)\n .sort((a, b) => a.start - b.start || a.end - b.end);\n\n const out = [ordered[0]];\n for (let i = 1; i < ordered.length; i += 1) {\n const current = ordered[i];\n const previous = out[out.length - 1];\n if (current.start <= previous.end) {\n previous.end = Math.max(previous.end, current.end);\n continue;\n }\n out.push({ start: current.start, end: current.end });\n }\n return out;\n}\n\nfunction splitTextRangeByUrls(baseStart, value) {\n const text = String(value || '');\n const ranges = [];\n let cursor = 0;\n let match;\n let foundUrl = false;\n\n URL_REGEX.lastIndex = 0;\n while ((match = URL_REGEX.exec(text)) !== null) {\n foundUrl = true;\n if (match.index > cursor) {\n ranges.push({\n start: baseStart + cursor,\n end: baseStart + match.index\n });\n }\n cursor = match.index + match[0].length;\n }\n\n if (cursor < text.length) {\n ranges.push({\n start: baseStart + cursor,\n end: baseStart + text.length\n });\n }\n\n if (!foundUrl && ranges.length === 0 && text.length > 0) {\n ranges.push({\n start: baseStart,\n end: baseStart + text.length\n });\n }\n\n return ranges;\n}\n\nfunction walkTextNodes(node, visitor) {\n if (!node || typeof node !== 'object') return;\n\n if (node.type === 'text') {\n visitor(node);\n }\n\n if (Array.isArray(node.children)) {\n node.children.forEach((child) => walkTextNodes(child, visitor));\n }\n}\n\nasync function collectEligibleRanges(content) {\n const { body, bodyOffset } = splitLeadingFrontmatter(content);\n if (!body.trim()) return [];\n\n const tree = await parseMdx(body);\n const ranges = [];\n\n walkTextNodes(tree, (node) => {\n const startOffset = node?.position?.start?.offset;\n const endOffset = node?.position?.end?.offset;\n if (!Number.isFinite(startOffset) || !Number.isFinite(endOffset) || endOffset <= startOffset) {\n return;\n }\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/repair-spelling.test.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/remediators/content/before.mdx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/scripts/remediators/content/after.mdx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/remediators/content/before.mdx, tools/scripts/remediators/content/after.mdx",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/repair-spelling.test.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/config-validator.js",
+ "script": "config-validator",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14, R-C6",
+ "purpose_statement": "Validates docs-usefulness config structure and field completeness.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { validateConfig } = require('../lib/docs-usefulness/config-validator');",
+ "header": "'use strict';\n/**\n * @script config-validator\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14, R-C6\n * @purpose-statement Validates docs-usefulness config structure and field completeness.\n * @pipeline indirect -- library module\n * @usage const { validateConfig } = require('../lib/docs-usefulness/config-validator');\n */\n\nconst {\n PURPOSE_ENUM,\n AUDIENCE_ENUM,\n loadRubric,\n loadJourneys,\n loadAudienceNormalization,\n loadLlmTiers\n} = require('./rubric-loader');\n\nfunction validateRubricPromptKeys(rubric, prompts) {\n const referenced = new Set();\n Object.entries(rubric).forEach(([purpose, rules]) => {\n if (!PURPOSE_ENUM.includes(purpose)) {\n throw new Error(`Invalid rubric purpose key: ${purpose}`);\n }\n\n const tier1 = rules?.tier1_rules || {};\n Object.entries(tier1).forEach(([ruleName, rule]) => {\n if (typeof rule.weight !== 'number') {\n throw new Error(`Rubric rule missing numeric weight: ${purpose}.${ruleName}`);\n }\n if (!rule.type) {\n throw new Error(`Rubric rule missing type: ${purpose}.${ruleName}`);\n }\n });\n\n const tier2 = rules?.tier2_llm || {};\n Object.entries(tier2).forEach(([criterion, config]) => {\n if (typeof config.weight !== 'number') {\n throw new Error(`Tier2 criterion missing numeric weight: ${purpose}.${criterion}`);\n }\n if (!config.prompt_key) {\n throw new Error(`Tier2 criterion missing prompt_key: ${purpose}.${criterion}`);\n }\n referenced.add(config.prompt_key);\n });\n });\n\n referenced.forEach((promptKey) => {\n if (!prompts[promptKey]) {\n throw new Error(`Rubric references unknown prompt key: ${promptKey}`);\n }\n });\n}\n\nfunction validateJourneyConfig(journeys) {\n Object.entries(journeys).forEach(([personaKey, journey]) => {\n if (!Array.isArray(journey.steps) || journey.steps.length === 0) {\n throw new Error(`Journey ${personaKey} has no steps`);\n }\n\n journey.steps.forEach((step) => {\n if (!Array.isArray(step.path_patterns) || step.path_patterns.length === 0) {\n throw new Error(`Journey ${personaKey} step ${step.position} missing path_patterns`);\n }\n step.path_patterns.forEach((pattern) => {\n if (String(pattern).includes('v2/platforms/')) {\n throw new Error(`Journey ${personaKey} step ${step.position} uses forbidden pattern: ${pattern}`);\n }\n });\n });\n });\n}\n\nfunction validateAudienceConfig(config) {\n const canonical = config.canonical_audiences || [];\n AUDIENCE_ENUM.forEach((audience) => {\n if (!canonical.includes(audience)) {\n throw new Error(`Audience normalization missing canonical value: ${audience}`);\n }\n });\n\n if (!config.section_defaults || !config.section_defaults.solutions) {\n throw new Error('Audience normalization missing section default for solutions');\n }\n\n if (config.section_defaults.platforms) {\n throw new Error('Audience normalization must not define deprecated platforms section');\n }\n}\n\nfunction validateLlmTiers(config) {\n if (!config.default_tier) {\n throw new Error('LLM tiers config missing default_tier');\n }\n if (!config.tiers || !config.tiers[config.default_tier]) {\n throw new Error(`LLM tiers missing default tier definition: ${config.default_tier}`);\n }\n\n ['free', 'good', 'optimal'].forEach((tier) => {\n if (!config.tiers[tier]) {\n throw new Error(`LLM tiers missing required tier: ${tier}`);\n }\n if (!Array.isArray(config.tiers[tier].models) || config.tiers[tier].models.length === 0) {\n throw new Error(`LLM tier has no models: ${tier}`);\n }\n });\n}\n\nfunction validateUsefulnessConfig({ rubric, journeys, audience, llmTiers, prompts }) {\n validateRubricPromptKeys(rubric, prompts);\n validateJourneyConfig(journeys);\n validateAudienceConfig(audience);\n validateLlmTiers(llmTiers);\n}\n\nfunction loadAndValidateUsefulnessConfig(prompts) {\n const rubric = loadRubric();\n const journeys = loadJourneys();\n const audience = loadAudienceNormalization();\n const llmTiers = loadLlmTiers();\n validateUsefulnessConfig({ rubric, journeys, audience, llmTiers, prompts });\n return { rubric, journeys, audience, llmTiers };\n}\n\nmodule.exports = {\n validateUsefulnessConfig,\n loadAndValidateUsefulnessConfig\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/usefulness-journey.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/usefulness-rubric.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/assign-purpose-metadata.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/audit-v2-usefulness.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/usefulness-journey.test.js; indirect via tests/unit/usefulness-rubric.test.js; indirect via tools/scripts/assign-purpose-metadata.js; indirect via tools/scripts/audit-v2-usefulness.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/journey-check.js",
+ "script": "journey-check",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14, R-C6",
+ "purpose_statement": "Evaluates docs pages against user journey completeness criteria.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { checkJourney } = require('../lib/docs-usefulness/journey-check');",
+ "header": "'use strict';\n/**\n * @script journey-check\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14, R-C6\n * @purpose-statement Evaluates docs pages against user journey completeness criteria.\n * @pipeline indirect -- library module\n * @usage const { checkJourney } = require('../lib/docs-usefulness/journey-check');\n */\n\nconst { minimatch } = require('minimatch');\nconst { loadJourneys } = require('./rubric-loader');\n\nfunction chooseBestPage(pages) {\n if (!Array.isArray(pages) || pages.length === 0) return null;\n return [...pages].sort((a, b) => {\n const scoreA = Number(a.combined_score || 0);\n const scoreB = Number(b.combined_score || 0);\n if (scoreA !== scoreB) return scoreB - scoreA;\n return String(a.path || '').localeCompare(String(b.path || ''));\n })[0];\n}\n\nfunction hasNavLinkToNext(currentPage, nextPagePath) {\n if (!currentPage || !Array.isArray(currentPage.internalLinks) || !nextPagePath) return false;\n const normalizedNext = String(nextPagePath).replace(/\\.mdx$/i, '').replace(/\\/index$/i, '');\n return currentPage.internalLinks.some((link) => {\n const normalizedLink = String(link || '').replace(/^\\/+/, '').replace(/\\.mdx$/i, '').replace(/\\/index$/i, '');\n return normalizedNext.includes(normalizedLink) || normalizedLink.includes(normalizedNext);\n });\n}\n\nfunction checkJourneys(pageScores, journeysConfig = loadJourneys()) {\n const reports = [];\n\n Object.entries(journeysConfig).forEach(([persona, journey]) => {\n const stepResults = (journey.steps || []).map((step) => {\n const patterns = Array.isArray(step.path_patterns) ? step.path_patterns : [];\n const matching = pageScores.filter((page) =>\n patterns.some((pattern) => minimatch(String(page.path || ''), pattern, { nocase: true }))\n );\n\n if (matching.length === 0) {\n return {\n ...step,\n status: 'missing',\n reason: 'no_page',\n page: null,\n score: null,\n has_link_to_next: false\n };\n }\n\n const allowedPurposes = Array.isArray(step.purpose) ? step.purpose : [step.purpose];\n const purposeMatched = matching.filter((page) => allowedPurposes.includes(page.purpose));\n\n if (purposeMatched.length === 0) {\n const best = chooseBestPage(matching);\n return {\n ...step,\n status: 'weak',\n reason: 'wrong_purpose',\n page: best?.path || null,\n score: best?.combined_score || 0,\n has_link_to_next: false\n };\n }\n\n const best = chooseBestPage(purposeMatched);\n const score = Number(best?.combined_score || 0);\n return {\n ...step,\n status: score >= 50 ? 'complete' : 'weak',\n reason: score >= 50 ? null : 'low_score',\n page: best?.path || null,\n score,\n has_link_to_next: false\n };\n });\n\n for (let i = 0; i < stepResults.length - 1; i += 1) {\n const current = stepResults[i];\n const next = stepResults[i + 1];\n if (!current.page || !next.page) continue;\n const currentPage = pageScores.find((page) => page.path === current.page);\n current.has_link_to_next = hasNavLinkToNext(currentPage, next.page);\n }\n\n const complete = stepResults.filter((step) => step.status === 'complete').length;\n const weak = stepResults.filter((step) => step.status === 'weak').length;\n const missing = stepResults.filter((step) => step.status === 'missing').length;\n\n const blockers = stepResults\n .filter((step) => step.status === 'missing')\n .map((step) => {\n const purpose = Array.isArray(step.purpose) ? step.purpose.join('/') : step.purpose;\n return `Step ${step.position} (${purpose}): NO PAGE FOUND`;\n });\n\n const verdict = missing === 0 && weak === 0 ? 'COMPLETE' : missing === 0 ? 'MOSTLY_COMPLETE' : 'BLOCKED';\n\n reports.push({\n persona,\n label: journey.label,\n maps_to: journey.maps_to || null,\n success_criteria: journey.success_criteria,\n priority: Number(journey.priority || 999),\n steps_total: stepResults.length,\n steps_complete: complete,\n steps_weak: weak,\n steps_missing: missing,\n verdict,\n blockers,\n steps: stepResults\n });\n });\n\n return reports.sort((a, b) => a.priority - b.priority || String(a.persona).localeCompare(String(b.persona)));\n}\n\nmodule.exports = {\n checkJourneys\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/usefulness-journey.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/audit-v2-usefulness.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/usefulness-journey.test.js; indirect via tools/scripts/audit-v2-usefulness.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/llm-evaluator.js",
+ "script": "llm-evaluator",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "Wraps LLM API calls for rubric-based page quality evaluation.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { evaluate } = require('../lib/docs-usefulness/llm-evaluator');",
+ "header": "'use strict';\n/**\n * @script llm-evaluator\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement Wraps LLM API calls for rubric-based page quality evaluation.\n * @pipeline indirect -- library module\n * @usage const { evaluate } = require('../lib/docs-usefulness/llm-evaluator');\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst { loadLlmTiers } = require('./rubric-loader');\n\nconst OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';\nconst CACHE_DIR = path.join(__dirname, '../../../.cache/llm-usefulness');\n\nfunction ensureCacheDir() {\n fs.mkdirSync(CACHE_DIR, { recursive: true });\n}\n\nfunction clampScore(value) {\n const numeric = Number(value);\n if (!Number.isFinite(numeric)) return 0;\n if (numeric < 0) return 0;\n if (numeric > 100) return 100;\n return Math.round(numeric);\n}\n\nclass LlmEvaluator {\n constructor(apiKey, options = {}) {\n if (!apiKey) throw new Error('OPENROUTER_API_KEY is required when --llm is enabled.');\n ensureCacheDir();\n\n const tierConfig = loadLlmTiers();\n const tierName = options.tier || tierConfig.default_tier || 'free';\n const tier = tierConfig.tiers?.[tierName];\n if (!tier) {\n throw new Error(`Unknown llm tier: ${tierName}`);\n }\n\n this.apiKey = apiKey;\n this.tierName = tierName;\n this.tier = tier;\n this.maxRetries = Number(options.maxRetries || 2);\n this.batchInterval = this.tierName === 'free' ? 3200 : 1000;\n this.lastCallMs = 0;\n this.callCount = 0;\n this.spentUsd = 0;\n\n const defaultBudget = Number(this.tier.default_budget_usd ?? 0);\n const budgetInput = Number(options.budget);\n this.budget = this.tierName === 'free' ? 0 : Number.isFinite(budgetInput) && budgetInput >= 0 ? budgetInput : defaultBudget;\n\n this.modelIndex = 0;\n this.modelDailyCounts = {};\n this.modelMinuteCounts = {};\n this.minuteWindowStart = Date.now();\n\n this._loadDailyCounts();\n }\n\n _dailyFile() {\n const date = new Date().toISOString().slice(0, 10);\n return path.join(CACHE_DIR, `daily-counts-${date}.json`);\n }\n\n _loadDailyCounts() {\n const file = this._dailyFile();\n if (!fs.existsSync(file)) return;\n try {\n this.modelDailyCounts = JSON.parse(fs.readFileSync(file, 'utf8')) || {};\n } catch (_error) {\n this.modelDailyCounts = {};\n }\n }\n\n _saveDailyCounts() {\n fs.writeFileSync(this._dailyFile(), JSON.stringify(this.modelDailyCounts, null, 2));\n }\n\n _cacheKey(page, promptKey) {\n const body = String(page.content || page.textContent || '');\n const hash = crypto.createHash('md5').update(body).digest('hex').slice(0, 12);\n const safePath = String(page.path || 'unknown').replace(/[\\\\/]/g, '_');\n return `${safePath}__${hash}__${promptKey}.json`;\n }\n\n _cacheFile(key) {\n return path.join(CACHE_DIR, key);\n }\n\n _readCache(key) {\n const file = this._cacheFile(key);\n if (!fs.existsSync(file)) return null;\n try {\n return JSON.parse(fs.readFileSync(file, 'utf8'));\n } catch (_error) {\n return null;\n }\n }\n\n _writeCache(key, data) {\n fs.writeFileSync(this._cacheFile(key), JSON.stringify(data, null, 2));\n }\n\n _minuteLimit() {\n return Number(this.tier.rate_limit?.per_minute || 60);\n }\n\n _dailyLimit() {\n const limit = this.tier.rate_limit?.per_day;\n return limit === null || limit === undefined ? null : Number(limit);\n }\n\n _resetMinuteWindowIfNeeded() {\n if (Date.now() - this.minuteWindowStart > 60000) {\n this.minuteWindowStart = Date.now();\n this.modelMinuteCounts = {};\n }\n }\n\n _modelMinuteCount(model) {\n this._resetMinuteWindowIfNeeded();\n return Number(this.modelMinuteCounts[model] || 0);\n }\n\n _modelDailyCount(model) {\n return Number(this.modelDailyCounts[model] || 0);\n }\n\n _recordCall(model) {\n this.callCount += 1;\n this.modelDailyCounts[model] = this._modelDailyCount(model) + 1;\n this.modelMinuteCounts[model] = this._modelMinuteCount(model) + 1;\n this._saveDailyCounts();\n }\n\n _remainingCapacity() {\n const perDay = this._dailyLimit();\n if (perDay === null) return null;\n return (this.tier.models || []).reduce((sum, model) => sum + Math.max(0, perDay - this._modelDailyCount(model)), 0);\n }\n\n _selectModel() {\n const models = this.tier.models || [];\n if (models.length === 0) return null;\n\n const perDay = this._dailyLimit();\n for (let offset = 0; offset < models.length; offset += 1) {\n const idx = (this.modelIndex + offset) % models.length;\n const model = models[idx];\n\n if (perDay !== null && this._modelDailyCount(model) >= perDay) {\n continue;\n }\n\n const minuteLimit = this._minuteLimit();\n if (this._modelMinuteCount(model) >= minuteLimit) {\n continue;\n }\n\n this.modelIndex = idx;\n return model;\n }\n\n return null;\n }\n\n async _respectRateLimit() {\n const elapsed = Date.now() - this.lastCallMs;\n if (elapsed < this.batchInterval) {\n await this._sleep(this.batchInterval - elapsed);\n }\n this.lastCallMs = Date.now();\n }\n\n _interpolate(template, page, audience, purpose) {\n return String(template || '')\n .replace('{content}', String(page.textContent || '').slice(0, 4000))\n .replace('{frontmatter}', JSON.stringify(page.frontmatter || {}, null, 2))\n .replace('{audience}', String(audience || page.frontmatter?.audience || page.section || 'unknown'))\n .replace('{purpose}', String(purpose || page.frontmatter?.purpose || 'unknown'));\n }\n\n _extractJson(text) {\n const raw = String(text || '').trim();\n const cleaned = raw.replace(/^```json\\s*/i, '').replace(/```$/i, '').trim();\n try {\n return JSON.parse(cleaned);\n } catch (_error) {\n const match = cleaned.match(/\\{[\\s\\S]*\\}/);\n if (match) {\n try {\n return JSON.parse(match[0]);\n } catch (_inner) {\n return null;\n }\n }\n return null;\n }\n }\n\n async evaluate(page, criterion, promptTemplate, context = {}) {\n const cacheKey = this._cacheKey(page, criterion);\n if (context.resume !== false) {\n const cached = this._readCache(cacheKey);\n if (cached) return { ...cached, cached: true };\n }\n\n if (this.tierName !== 'free' && this.spentUsd >= this.budget) {\n return { status: 'budget_exceeded' };\n }\n\n let model = this._selectModel();\n if (!model) {",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/usefulness-rubric.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/audit-v2-usefulness.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": ".cache/llm-usefulness",
+ "type": "directory",
+ "call": "mkdirSync"
+ }
+ ],
+ "outputs_display": ".cache/llm-usefulness",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/usefulness-rubric.test.js; indirect via tools/scripts/audit-v2-usefulness.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/prompts/changelog.js",
+ "script": "prompts/changelog",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "LLM prompt template for changelog page-type usefulness evaluation.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { getPrompt } = require('../lib/docs-usefulness/prompts/changelog');",
+ "header": "'use strict';\n/**\n * @script prompts/changelog\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement LLM prompt template for changelog page-type usefulness evaluation.\n * @pipeline indirect -- library module\n * @usage const { getPrompt } = require('../lib/docs-usefulness/prompts/changelog');\n */\n\nconst SYSTEM_BASE = 'You are a documentation quality evaluator for Livepeer docs. Return ONLY JSON: {\"score\":0-100,\"pass\":true/false,\"reasoning\":\"one sentence\"}.';\n\nmodule.exports = {\n changelog_clear: {\n system: `${SYSTEM_BASE} Evaluate changelog summary clarity.`,\n user: `Evaluate this changelog page.\n\nCriteria:\n- Entries clearly describe what changed.\n- Time/version context is clear.\n\nPAGE CONTENT:\n{content}`\n },\n changelog_actionable: {\n system: `${SYSTEM_BASE} Evaluate changelog actionability for implementers.`,\n user: `Evaluate whether this changelog tells developers what action they should take.\n\nPAGE CONTENT:\n{content}`\n }\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/prompts/index.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/lib/docs-usefulness/prompts/index.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/prompts/concept.js",
+ "script": "prompts/concept",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "LLM prompt template for concept page-type usefulness evaluation.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { getPrompt } = require('../lib/docs-usefulness/prompts/concept');",
+ "header": "'use strict';\n/**\n * @script prompts/concept\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement LLM prompt template for concept page-type usefulness evaluation.\n * @pipeline indirect -- library module\n * @usage const { getPrompt } = require('../lib/docs-usefulness/prompts/concept');\n */\n\nconst SYSTEM_BASE = 'You are a documentation quality evaluator for Livepeer docs. Return ONLY JSON: {\"score\":0-100,\"pass\":true/false,\"reasoning\":\"one sentence\"}.';\n\nmodule.exports = {\n concept_focus: {\n system: `${SYSTEM_BASE} Evaluate concept-page focus.`,\n user: `Evaluate if this page stays focused on one primary concept.\n\nPAGE CONTENT:\n{content}`\n },\n concept_jargon: {\n system: `${SYSTEM_BASE} Evaluate jargon definition quality.`,\n user: `Evaluate if jargon is defined on first use or clearly linked.\n\nDeclared audience: {audience}\n\nPAGE CONTENT:\n{content}`\n },\n concept_practical: {\n system: `${SYSTEM_BASE} Evaluate practical relevance of conceptual explanation.`,\n user: `Evaluate if this concept page connects ideas to practical usage.\n\nPAGE CONTENT:\n{content}`\n },\n concept_clarity: {\n system: `${SYSTEM_BASE} Evaluate conceptual clarity.`,\n user: `Evaluate explanation quality, coherence, and readability.\n\nPAGE CONTENT:\n{content}`\n }\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/prompts/index.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/lib/docs-usefulness/prompts/index.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/prompts/faq.js",
+ "script": "prompts/faq",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "LLM prompt template for faq page-type usefulness evaluation.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { getPrompt } = require('../lib/docs-usefulness/prompts/faq');",
+ "header": "'use strict';\n/**\n * @script prompts/faq\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement LLM prompt template for faq page-type usefulness evaluation.\n * @pipeline indirect -- library module\n * @usage const { getPrompt } = require('../lib/docs-usefulness/prompts/faq');\n */\n\nconst SYSTEM_BASE = 'You are a documentation quality evaluator for Livepeer docs. Return ONLY JSON: {\"score\":0-100,\"pass\":true/false,\"reasoning\":\"one sentence\"}.';\n\nmodule.exports = {\n faq_real_questions: {\n system: `${SYSTEM_BASE} Evaluate realism of FAQ questions.`,\n user: `Evaluate this FAQ page.\n\nCriteria:\n- Questions reflect real user concerns.\n- Questions are not placeholder or invented fluff.\n\nPAGE CONTENT:\n{content}`\n },\n faq_direct_answers: {\n system: `${SYSTEM_BASE} Evaluate directness of FAQ answers.`,\n user: `Evaluate this FAQ page.\n\nCriteria:\n- Answers address the question directly first.\n- Avoids vague or circular responses.\n\nPAGE CONTENT:\n{content}`\n },\n faq_organised: {\n system: `${SYSTEM_BASE} Evaluate thematic organization in FAQ pages.`,\n user: `Evaluate this FAQ page for thematic grouping and scanability.\n\nPAGE CONTENT:\n{content}`\n }\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/prompts/index.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/lib/docs-usefulness/prompts/index.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/prompts/glossary.js",
+ "script": "prompts/glossary",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "LLM prompt template for glossary page-type usefulness evaluation.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { getPrompt } = require('../lib/docs-usefulness/prompts/glossary');",
+ "header": "'use strict';\n/**\n * @script prompts/glossary\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement LLM prompt template for glossary page-type usefulness evaluation.\n * @pipeline indirect -- library module\n * @usage const { getPrompt } = require('../lib/docs-usefulness/prompts/glossary');\n */\n\nconst SYSTEM_BASE = 'You are a documentation quality evaluator for Livepeer docs. Return ONLY JSON: {\"score\":0-100,\"pass\":true/false,\"reasoning\":\"one sentence\"}.';\n\nmodule.exports = {\n glossary_no_marketing: {\n system: `${SYSTEM_BASE} Evaluate glossary neutrality.`,\n user: `Evaluate this glossary page.\n\nCriteria:\n- Definitions are neutral and non-marketing.\n- Terms are concise and factual.\n\nPAGE CONTENT:\n{content}`\n },\n glossary_newcomer: {\n system: `${SYSTEM_BASE} Evaluate glossary coverage for newcomers.`,\n user: `Evaluate whether this glossary helps newcomers understand essential terms.\n\nPAGE CONTENT:\n{content}`\n }\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/prompts/index.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/lib/docs-usefulness/prompts/index.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/prompts/how_to.js",
+ "script": "prompts/how_to",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "LLM prompt template for how_to page-type usefulness evaluation.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { getPrompt } = require('../lib/docs-usefulness/prompts/how_to');",
+ "header": "'use strict';\n/**\n * @script prompts/how_to\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement LLM prompt template for how_to page-type usefulness evaluation.\n * @pipeline indirect -- library module\n * @usage const { getPrompt } = require('../lib/docs-usefulness/prompts/how_to');\n */\n\nconst SYSTEM_BASE = 'You are a documentation quality evaluator for Livepeer docs. Return ONLY JSON: {\"score\":0-100,\"pass\":true/false,\"reasoning\":\"one sentence\"}.';\n\nmodule.exports = {\n howto_action_result: {\n system: `${SYSTEM_BASE} Evaluate action/result pairing for how-to pages.`,\n user: `Evaluate this how-to page.\n\nCriteria:\n- Steps specify concrete actions.\n- Steps include expected outputs/results.\n- User can verify success after each stage.\n\nPAGE CONTENT:\n{content}`\n },\n audience_knowledge: {\n system: `${SYSTEM_BASE} Evaluate audience knowledge fit.`,\n user: `Evaluate whether this page matches declared audience knowledge.\n\nDeclared audience: {audience}\nDeclared purpose: {purpose}\n\nPAGE CONTENT:\n{content}`\n },\n howto_troubleshooting: {\n system: `${SYSTEM_BASE} Evaluate troubleshooting coverage for how-to pages.`,\n user: `Evaluate this how-to page.\n\nCriteria:\n- Mentions common failures.\n- Provides recovery guidance or links.\n\nPAGE CONTENT:\n{content}`\n }\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/prompts/index.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/lib/docs-usefulness/prompts/index.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/prompts/landing.js",
+ "script": "prompts/landing",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "LLM prompt template for landing page-type usefulness evaluation.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { getPrompt } = require('../lib/docs-usefulness/prompts/landing');",
+ "header": "'use strict';\n/**\n * @script prompts/landing\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement LLM prompt template for landing page-type usefulness evaluation.\n * @pipeline indirect -- library module\n * @usage const { getPrompt } = require('../lib/docs-usefulness/prompts/landing');\n */\n\nconst SYSTEM_BASE = 'You are a documentation quality evaluator for Livepeer docs. Return ONLY JSON: {\"score\":0-100,\"pass\":true/false,\"reasoning\":\"one sentence\"}.';\n\nmodule.exports = {\n landing_routing_clarity: {\n system: `${SYSTEM_BASE} Evaluate routing clarity for landing pages.`,\n user: `Evaluate if this landing page routes users clearly.\n\nCriteria:\n- Users can choose where to go next.\n- Navigation options are explicit.\n- No ambiguity about recommended next clicks.\n\nPAGE CONTENT:\n{content}`\n },\n audience_signal: {\n system: `${SYSTEM_BASE} Evaluate audience signaling strength.`,\n user: `Evaluate whether this page clearly states who it is for.\n\nDeclared audience: {audience}\n\nCriteria:\n- Audience cues are visible in title/opening copy.\n- Terminology matches the audience.\n\nPAGE CONTENT:\n{content}`\n },\n landing_no_teaching: {\n system: `${SYSTEM_BASE} Evaluate that landing pages avoid deep instructional teaching.`,\n user: `Evaluate whether this landing page avoids deep tutorial content.\n\nCriteria:\n- Mostly orientation/routing.\n- Limited procedural depth.\n- No long step-by-step instruction blocks.\n\nPAGE CONTENT:\n{content}`\n }\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/prompts/index.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/lib/docs-usefulness/prompts/index.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/prompts/overview.js",
+ "script": "prompts/overview",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "LLM prompt template for overview page-type usefulness evaluation.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { getPrompt } = require('../lib/docs-usefulness/prompts/overview');",
+ "header": "'use strict';\n/**\n * @script prompts/overview\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement LLM prompt template for overview page-type usefulness evaluation.\n * @pipeline indirect -- library module\n * @usage const { getPrompt } = require('../lib/docs-usefulness/prompts/overview');\n */\n\nconst SYSTEM_BASE = 'You are a documentation quality evaluator for Livepeer docs. Return ONLY JSON: {\"score\":0-100,\"pass\":true/false,\"reasoning\":\"one sentence\"}.';\n\nmodule.exports = {\n overview_benefit: {\n system: `${SYSTEM_BASE} Evaluate whether overview pages explain reader benefit.`,\n user: `Evaluate this overview page.\n\nCriteria:\n- Why this topic matters is clear early.\n- Reader benefit is concrete.\n\nPAGE CONTENT:\n{content}`\n },\n overview_next_context: {\n system: `${SYSTEM_BASE} Evaluate whether overview pages provide context for next steps.`,\n user: `Evaluate this overview page.\n\nCriteria:\n- Mentions what to read/do next.\n- Links to next resources are contextualized.\n\nPAGE CONTENT:\n{content}`\n },\n overview_no_detail: {\n system: `${SYSTEM_BASE} Evaluate that overview pages avoid exhaustive implementation detail.`,\n user: `Evaluate this overview page.\n\nCriteria:\n- Stays high-level.\n- Avoids deep exhaustive procedures/reference dumps.\n\nPAGE CONTENT:\n{content}`\n }\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/prompts/index.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/lib/docs-usefulness/prompts/index.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/prompts/reference.js",
+ "script": "prompts/reference",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "LLM prompt template for reference page-type usefulness evaluation.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { getPrompt } = require('../lib/docs-usefulness/prompts/reference');",
+ "header": "'use strict';\n/**\n * @script prompts/reference\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement LLM prompt template for reference page-type usefulness evaluation.\n * @pipeline indirect -- library module\n * @usage const { getPrompt } = require('../lib/docs-usefulness/prompts/reference');\n */\n\nconst SYSTEM_BASE = 'You are a documentation quality evaluator for Livepeer docs. Return ONLY JSON: {\"score\":0-100,\"pass\":true/false,\"reasoning\":\"one sentence\"}.';\n\nmodule.exports = {\n reference_params: {\n system: `${SYSTEM_BASE} Evaluate parameter completeness for reference pages.`,\n user: `Evaluate this reference page.\n\nCriteria:\n- Parameters/fields are complete and clearly named.\n- Required/optional/default/type details are present where expected.\n\nPAGE CONTENT:\n{content}`\n },\n reference_examples: {\n system: `${SYSTEM_BASE} Evaluate example quality for reference pages.`,\n user: `Evaluate this reference page.\n\nCriteria:\n- Includes realistic examples.\n- Examples align with documented parameters/fields.\n\nPAGE CONTENT:\n{content}`\n },\n reference_no_prose: {\n system: `${SYSTEM_BASE} Evaluate whether the page remains lookup-oriented and avoids narrative drift.`,\n user: `Evaluate this reference page for lookup orientation.\n\nPAGE CONTENT:\n{content}`\n }\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/prompts/index.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/lib/docs-usefulness/prompts/index.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/prompts/troubleshooting.js",
+ "script": "prompts/troubleshooting",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "LLM prompt template for troubleshooting page-type usefulness evaluation.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { getPrompt } = require('../lib/docs-usefulness/prompts/troubleshooting');",
+ "header": "'use strict';\n/**\n * @script prompts/troubleshooting\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement LLM prompt template for troubleshooting page-type usefulness evaluation.\n * @pipeline indirect -- library module\n * @usage const { getPrompt } = require('../lib/docs-usefulness/prompts/troubleshooting');\n */\n\nconst SYSTEM_BASE = 'You are a documentation quality evaluator for Livepeer docs. Return ONLY JSON: {\"score\":0-100,\"pass\":true/false,\"reasoning\":\"one sentence\"}.';\n\nmodule.exports = {\n troubleshooting_symptom_first: {\n system: `${SYSTEM_BASE} Evaluate symptom-first troubleshooting structure.`,\n user: `Evaluate this troubleshooting page.\n\nCriteria:\n- Entries are structured by symptom/error users observe.\n- Cause-only structure without symptom entrypoints scores low.\n\nPAGE CONTENT:\n{content}`\n },\n troubleshooting_actionable: {\n system: `${SYSTEM_BASE} Evaluate actionability of troubleshooting fixes.`,\n user: `Evaluate this troubleshooting page.\n\nCriteria:\n- Fixes are explicit and executable.\n- Steps are concrete enough to attempt immediately.\n\nPAGE CONTENT:\n{content}`\n },\n troubleshooting_searchable: {\n system: `${SYSTEM_BASE} Evaluate searchability of errors and symptoms.`,\n user: `Evaluate if errors/symptoms are represented in searchable phrasing.\n\nPAGE CONTENT:\n{content}`\n }\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/prompts/index.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/lib/docs-usefulness/prompts/index.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/prompts/tutorial.js",
+ "script": "prompts/tutorial",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "LLM prompt template for tutorial page-type usefulness evaluation.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { getPrompt } = require('../lib/docs-usefulness/prompts/tutorial');",
+ "header": "'use strict';\n/**\n * @script prompts/tutorial\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement LLM prompt template for tutorial page-type usefulness evaluation.\n * @pipeline indirect -- library module\n * @usage const { getPrompt } = require('../lib/docs-usefulness/prompts/tutorial');\n */\n\nconst SYSTEM_BASE = 'You are a documentation quality evaluator for Livepeer docs. Return ONLY JSON: {\"score\":0-100,\"pass\":true/false,\"reasoning\":\"one sentence\"}.';\n\nmodule.exports = {\n tutorial_steps_complete: {\n system: `${SYSTEM_BASE} Evaluate tutorial step completeness.`,\n user: `Evaluate if this tutorial can be completed end-to-end by following only this page.\n\nPAGE CONTENT:\n{content}`\n },\n tutorial_newcomer: {\n system: `${SYSTEM_BASE} Evaluate newcomer accessibility.`,\n user: `Evaluate if this tutorial is accessible to newcomers to the domain.\n\nPAGE CONTENT:\n{content}`\n },\n tutorial_error_recovery: {\n system: `${SYSTEM_BASE} Evaluate error recovery guidance in tutorials.`,\n user: `Evaluate whether this tutorial includes recovery paths when steps fail.\n\nPAGE CONTENT:\n{content}`\n },\n tutorial_context: {\n system: `${SYSTEM_BASE} Evaluate contextual explanations for why actions are performed.`,\n user: `Evaluate whether this tutorial explains why each major step matters.\n\nPAGE CONTENT:\n{content}`\n }\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/prompts/index.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/lib/docs-usefulness/prompts/index.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/quality-gate.js",
+ "script": "quality-gate",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14, R-C6",
+ "purpose_statement": "Applies pass/fail thresholds to usefulness scores.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { applyGate } = require('../lib/docs-usefulness/quality-gate');",
+ "header": "'use strict';\n/**\n * @script quality-gate\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14, R-C6\n * @purpose-statement Applies pass/fail thresholds to usefulness scores.\n * @pipeline indirect -- library module\n * @usage const { applyGate } = require('../lib/docs-usefulness/quality-gate');\n */\n\nfunction runQualityGate(page) {\n const details = {};\n let fails = 0;\n let warns = 0;\n\n details.mdx_parses = !(page.flags || []).includes('mdx_parse_error');\n if (!details.mdx_parses) fails += 1;\n\n details.frontmatter_exists = !(page.flags || []).includes('missing_frontmatter');\n if (!details.frontmatter_exists) fails += 1;\n\n details.frontmatter_valid = !(page.flags || []).includes('invalid_frontmatter');\n if (!details.frontmatter_valid) fails += 1;\n\n details.has_title = Boolean(page.frontmatter?.title);\n if (!details.has_title) fails += 1;\n\n details.has_description = Boolean(page.frontmatter?.description);\n if (!details.has_description) fails += 1;\n\n details.imports_resolve = !(page.flags || []).includes('broken_import');\n if (!details.imports_resolve) fails += 1;\n\n details.no_broken_links = !(page.flags || []).includes('broken_links');\n if (!details.no_broken_links) fails += 1;\n\n details.no_todo_markers = !(page.flags || []).includes('todo_marker');\n if (!details.no_todo_markers) warns += 1;\n\n details.no_coming_soon = !(page.flags || []).includes('coming_soon');\n if (!details.no_coming_soon) warns += 1;\n\n details.no_legacy_links = !(page.flags || []).includes('legacy_v2_pages_link');\n if (!details.no_legacy_links) warns += 1;\n\n details.images_have_alt =\n !Array.isArray(page.images) ||\n page.images.length === 0 ||\n page.images.every((image) => Boolean(String(image.alt || '').trim()));\n if (!details.images_have_alt) warns += 1;\n\n const status = fails > 0 ? 'fail' : warns > 0 ? 'warn' : 'pass';\n return {\n status,\n errors: fails + warns,\n fails,\n warns,\n details\n };\n}\n\nmodule.exports = {\n runQualityGate\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/scoring.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/lib/docs-usefulness/scoring.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/rubric-loader.js",
+ "script": "rubric-loader",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "Loads and parses rubric YAML/JSON for page-type scoring rules.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { loadRubric } = require('../lib/docs-usefulness/rubric-loader');",
+ "header": "'use strict';\n/**\n * @script rubric-loader\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement Loads and parses rubric YAML/JSON for page-type scoring rules.\n * @pipeline indirect -- library module\n * @usage const { loadRubric } = require('../lib/docs-usefulness/rubric-loader');\n */\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst PURPOSE_ENUM = [\n 'landing',\n 'overview',\n 'concept',\n 'how_to',\n 'tutorial',\n 'reference',\n 'faq',\n 'glossary',\n 'changelog',\n 'troubleshooting'\n];\n\nconst AUDIENCE_ENUM = [\n 'developer',\n 'gateway-operator',\n 'orchestrator',\n 'delegator',\n 'platform-builder',\n 'community',\n 'internal',\n 'general',\n 'everyone'\n];\n\nconst SECTION_DEFAULT_AUDIENCE = {\n home: 'everyone',\n about: 'general',\n developers: 'developer',\n gateways: 'gateway-operator',\n orchestrators: 'orchestrator',\n lpt: 'delegator',\n community: 'community',\n solutions: 'platform-builder',\n internal: 'internal',\n resources: 'everyone'\n};\n\nconst cache = new Map();\n\nfunction loadJson(filePath) {\n const abs = path.resolve(filePath);\n if (cache.has(abs)) return cache.get(abs);\n const parsed = JSON.parse(fs.readFileSync(abs, 'utf8'));\n cache.set(abs, parsed);\n return parsed;\n}\n\nfunction getFrontmatterValues(page) {\n if (!page) return {};\n if (page.frontmatter && page.frontmatter.data && typeof page.frontmatter.data === 'object') {\n return page.frontmatter.data;\n }\n if (page.frontmatter && typeof page.frontmatter === 'object') {\n return page.frontmatter;\n }\n if (page.frontmatterData && typeof page.frontmatterData === 'object') {\n return page.frontmatterData;\n }\n return {};\n}\n\nfunction configPath(relativePath) {\n return path.join(__dirname, '../../config', relativePath);\n}\n\nfunction loadRubric(filePath = configPath('usefulness-rubric.json')) {\n return loadJson(filePath);\n}\n\nfunction loadJourneys(filePath = configPath('usefulness-journeys.json')) {\n return loadJson(filePath);\n}\n\nfunction loadAudienceNormalization(filePath = configPath('usefulness-audience-normalization.json')) {\n return loadJson(filePath);\n}\n\nfunction loadLlmTiers(filePath = configPath('usefulness-llm-tiers.json')) {\n return loadJson(filePath);\n}\n\nfunction normalizeAudienceToken(token, normalization) {\n const value = String(token || '').trim().toLowerCase();\n if (!value) return null;\n if (AUDIENCE_ENUM.includes(value)) return value;\n if (normalization?.synonyms && normalization.synonyms[value]) {\n return normalization.synonyms[value];\n }\n return null;\n}\n\nfunction audienceTokensFromRaw(raw, normalization) {\n if (!raw && raw !== 0) return [];\n if (Array.isArray(raw)) {\n return raw\n .map((item) => normalizeAudienceToken(item, normalization))\n .filter(Boolean);\n }\n\n const text = String(raw).trim();\n if (!text) return [];\n\n const separators = normalization?.token_separators || [',', ';', '|'];\n let tokens = [text];\n separators.forEach((sep) => {\n tokens = tokens.flatMap((token) => token.split(sep));\n });\n\n return tokens\n .map((token) => normalizeAudienceToken(token, normalization))\n .filter(Boolean);\n}\n\nfunction selectAudience(tokens, page, normalization) {\n if (!tokens.length) return null;\n if (tokens.length === 1) return tokens[0];\n\n const sectionDefault =\n (normalization?.section_defaults && normalization.section_defaults[page.section]) ||\n SECTION_DEFAULT_AUDIENCE[page.section];\n if (sectionDefault && tokens.includes(sectionDefault)) return sectionDefault;\n\n const precedence = normalization?.deterministic_precedence || AUDIENCE_ENUM;\n for (const candidate of precedence) {\n if (tokens.includes(candidate)) return candidate;\n }\n return tokens[0] || null;\n}\n\nfunction resolveAudience(page, normalization = loadAudienceNormalization()) {\n const frontmatter = getFrontmatterValues(page);\n const rawAudience = frontmatter.audience;\n const tokens = audienceTokensFromRaw(rawAudience, normalization);\n const selected = selectAudience(tokens, page, normalization);\n\n if (selected && AUDIENCE_ENUM.includes(selected)) {\n return {\n audience: selected,\n source: 'frontmatter',\n audienceRaw: rawAudience,\n audienceCandidates: tokens\n };\n }\n\n const fallback =\n (normalization?.section_defaults && normalization.section_defaults[page.section]) ||\n SECTION_DEFAULT_AUDIENCE[page.section] ||\n 'everyone';\n return {\n audience: fallback,\n source: 'inferred',\n audienceRaw: rawAudience,\n audienceCandidates: tokens\n };\n}\n\nfunction resolvePurpose(page) {\n const frontmatter = getFrontmatterValues(page);\n const frontmatterPurpose = String(frontmatter.purpose || '').trim();\n if (frontmatterPurpose) {\n if (PURPOSE_ENUM.includes(frontmatterPurpose)) {\n return { purpose: frontmatterPurpose, source: 'frontmatter', invalid: false };\n }\n return { purpose: null, source: 'none', invalid: true };\n }\n\n const base = path.basename(String(page.path || ''), path.extname(String(page.path || '')));\n const route = String(page.path || '').toLowerCase();\n\n if (/portal|mission-control/i.test(base) || base === 'index') return { purpose: 'landing', source: 'inferred', invalid: false };\n if (/quickstart|get-started|primer|^first-/i.test(base)) return { purpose: 'tutorial', source: 'inferred', invalid: false };\n if (/^faq/i.test(base)) return { purpose: 'faq', source: 'inferred', invalid: false };\n if (/troubleshoot/i.test(base)) return { purpose: 'troubleshooting', source: 'inferred', invalid: false };\n if (/glossary/i.test(base)) return { purpose: 'glossary', source: 'inferred', invalid: false };\n if (/changelog|release-notes/i.test(base)) return { purpose: 'changelog', source: 'inferred', invalid: false };\n if (/api-reference|config-flags/i.test(base)) return { purpose: 'reference', source: 'inferred', invalid: false };\n if (/overview/i.test(base)) return { purpose: 'overview', source: 'inferred', invalid: false };\n if (/\\/references?\\//i.test(route)) return { purpose: 'reference', source: 'inferred', invalid: false };\n\n if ((page.components || []).includes('Steps')) return { purpose: 'how_to', source: 'inferred', invalid: false };\n\n if ((page.wordCount || 0) < 150 && (page.components || []).some((component) => ['Card', 'CardGroup', 'GotoCard', 'DisplayCard'].includes(component))) {\n return { purpose: 'landing', source: 'inferred', invalid: false };\n }\n\n const accordionCount = (String(page.content || '').match(/]/g) || []).length;\n if (accordionCount >= 5) return { purpose: 'faq', source: 'inferred', invalid: false };\n\n if ((page.wordCount || 0) > 300 && (page.headings || []).length >= 3 && !(page.components || []).includes('Steps')) {\n return { purpose: 'concept', source: 'inferred', invalid: false };\n }\n\n return { purpose: null, source: 'none', invalid: false };\n}\n\nfunction getRulesForPage(rubric, page, normalization = loadAudienceNormalization()) {\n const purposeResolution = resolvePurpose(page);\n const audienceResolution = resolveAudience(page, normalization);\n\n if (!purposeResolution.purpose || !rubric[purposeResolution.purpose]) {\n return {\n purpose: null,\n purposeSource: purposeResolution.source,",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/usefulness-rubric.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/config-validator.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/journey-check.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/llm-evaluator.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/scoring.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/assign-purpose-metadata.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/audit-v2-usefulness.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/usefulness-rubric.test.js; indirect via tools/lib/docs-usefulness/config-validator.js; indirect via tools/lib/docs-usefulness/journey-check.js; indirect via tools/lib/docs-usefulness/llm-evaluator.js; indirect via tools/lib/docs-usefulness/scoring.js; indirect via tools/scripts/assign-purpose-metadata.js; indirect via tools/scripts/audit-v2-usefulness.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/rule-evaluators.js",
+ "script": "rule-evaluators",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "Evaluates individual rubric rules against page content.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { evaluateRule } = require('../lib/docs-usefulness/rule-evaluators');",
+ "header": "'use strict';\n/**\n * @script rule-evaluators\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement Evaluates individual rubric rules against page content.\n * @pipeline indirect -- library module\n * @usage const { evaluateRule } = require('../lib/docs-usefulness/rule-evaluators');\n */\n\nfunction toText(value) {\n return String(value || '').toLowerCase();\n}\n\nfunction countMatches(content, re) {\n return (String(content || '').match(re) || []).length;\n}\n\nfunction getSignalValue(page, signal) {\n const extractors = {\n internal_link_count: () => (page.internalLinks || []).length,\n code_block_count: () => (page.codeBlocks || []).length,\n word_count: () => Number(page.wordCount || 0),\n heading_count: () => (page.headings || []).length,\n max_heading_level: () => {\n const levels = (page.headings || []).map((heading) => Number(heading.level || 0)).filter(Boolean);\n if (levels.length === 0) return 0;\n return Math.max(...levels);\n },\n steps_component: () => ((page.components || []).includes('Steps') ? 1 : 0),\n h2_or_accordion_count: () => {\n const h2Count = (page.headings || []).filter((heading) => Number(heading.level) === 2).length;\n const accordionCount = countMatches(page.content, /]/g);\n return Math.max(h2Count, accordionCount);\n },\n code_inline_dense: () => countMatches(page.content, /`[^`]+`/g),\n error_keyword: () => (/\\b(error|fail|exception|timeout|refused|denied|cannot|unable)\\b/i.test(page.textContent || '') ? 1 : 0)\n };\n\n const extractor = extractors[signal];\n if (!extractor) {\n throw new Error(`Unknown signal: ${signal}`);\n }\n return extractor();\n}\n\nconst JARGON_TERMS = [\n 'orchestrator',\n 'delegator',\n 'transcoding',\n 'transcode',\n 'lpt',\n 'arbitrum',\n 'clearinghouse',\n 'byoc',\n 'go-livepeer',\n 'aiserviceregistry',\n 'pipeline',\n 'epoch',\n 'round',\n 'bonding',\n 'unbonding',\n 'slashing',\n 'erc-20',\n 'wei',\n 'gwei',\n 'mainnet',\n 'testnet'\n];\n\nconst DEFINITION_SIGNALS = ['is a', 'refers to', 'means', 'defined as', 'also known as', 'i.e.'];\n\nconst BOOLEAN_CHECKS = {\n first_para_has_value_prop(page) {\n const firstPara = String(page.textContent || '').split(/\\n\\n/)[0] || '';\n const hasAudienceSignal = /you|developer|operator|delegat|build|run|stake|community/i.test(firstPara);\n const hasActionSignal = /learn|find|get started|discover|explore|choose|set up|start/i.test(firstPara);\n return hasAudienceSignal && hasActionSignal;\n },\n\n all_internal_links_resolve(page) {\n return !(page.flags || []).includes('broken_links');\n }\n};\n\nfunction splitByH2(page) {\n const text = String(page.textContent || '');\n const parts = text.split(/(?=^## )/m);\n return parts\n .map((part) => part.trim())\n .filter(Boolean)\n .map((part) => {\n const lines = part.split('\\n');\n const heading = lines[0].replace(/^##\\s*/, '').trim();\n const body = lines.slice(1).join('\\n').trim();\n const wordCount = body ? body.split(/\\s+/).filter(Boolean).length : 0;\n return { heading, content: body, wordCount };\n });\n}\n\nconst EVALUATORS = {\n boolean(page, rule) {\n const fn = BOOLEAN_CHECKS[rule.check];\n if (!fn) throw new Error(`Unknown boolean check: ${rule.check}`);\n return Boolean(fn(page));\n },\n\n threshold(page, rule) {\n const value = getSignalValue(page, rule.signal);\n if (rule.min !== undefined && value < rule.min) return false;\n if (rule.max !== undefined && value > rule.max) return false;\n return true;\n },\n\n absence(page, rule) {\n return getSignalValue(page, rule.signal) === 0;\n },\n\n presence(page, rule) {\n const components = Array.isArray(rule.components) ? rule.components : [];\n return components.some((component) => (page.components || []).includes(component));\n },\n\n heading_keyword(page, rule) {\n const keywords = (rule.keywords || []).map((keyword) => toText(keyword));\n return (page.headings || []).some((heading) => {\n const text = toText(heading.text);\n return keywords.some((keyword) => text.includes(keyword));\n });\n },\n\n content_keyword(page, rule) {\n const text = toText(page.textContent);\n return (rule.keywords || []).some((keyword) => text.includes(toText(keyword)));\n },\n\n presence_any(page, rule) {\n const signals = Array.isArray(rule.signals) ? rule.signals : [];\n return signals.some((signal) => {\n switch (signal) {\n case 'mermaid':\n return String(page.content || '').includes('```mermaid') || (page.components || []).includes('Mermaid');\n case 'image':\n return (page.images || []).length > 0;\n case 'table':\n return (page.tables || []).length > 0;\n case 'parameter_list':\n return /\\|\\s*(parameter|name|field|flag|option)\\s*\\|/i.test(page.content || '');\n case 'openapi_marker':\n return Boolean(page.frontmatter?.openapi) || /openapi/i.test(String(page.content || ''));\n case 'code_inline_dense':\n return countMatches(page.content, /`[^`]+`/g) > 5;\n case 'error_keyword':\n return /\\b(error|fail|exception|timeout|cannot|unable)\\b/i.test(page.textContent || '');\n default:\n return false;\n }\n });\n },\n\n content_check(page, rule) {\n if (rule.check !== 'first_200_words_no_jargon') {\n return true;\n }\n\n const words = String(page.textContent || '').split(/\\s+/).slice(0, 200);\n const first200 = words.join(' ').toLowerCase();\n let undefinedJargon = 0;\n for (const term of JARGON_TERMS) {\n const idx = first200.indexOf(term);\n if (idx === -1) continue;\n const window = first200.slice(Math.max(0, idx - 80), idx + term.length + 80);\n const defined = DEFINITION_SIGNALS.some((signal) => window.includes(signal));\n if (!defined) undefinedJargon += 1;\n }\n return undefinedJargon <= 1;\n },\n\n heading_pattern(page, rule) {\n const headings = (page.headings || []).filter((heading) => Number(heading.level) <= 3);\n if (headings.length < 2) return false;\n\n switch (rule.pattern) {\n case 'question':\n return (\n headings.filter((heading) => {\n const text = String(heading.text || '').trim();\n return /\\?$/.test(text) || /^(how|what|why|when|where|can|do|is|should|will)\\b/i.test(text);\n }).length >= 3\n );\n case 'error_or_symptom':\n return (\n headings.filter((heading) => {\n const text = String(heading.text || '');\n return (\n /\\b(error|fail|cannot|unable|timeout|crash|not working|issue|warning)\\b/i.test(text) ||\n /^`[^`]+`/.test(text)\n );\n }).length >= 2\n );\n case 'alphabetical': {\n const h2 = (page.headings || [])\n .filter((heading) => Number(heading.level) === 2)\n .map((heading) => toText(heading.text));\n if (h2.length < 5) return true;\n let ordered = 0;\n for (let i = 1; i < h2.length; i += 1) {\n if (h2[i] >= h2[i - 1]) ordered += 1;\n }\n return ordered / (h2.length - 1) >= 0.7;\n }\n default:\n return true;\n }\n },\n\n avg_section_words(page, rule) {",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/usefulness-rubric.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/scoring.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/usefulness-rubric.test.js; indirect via tools/lib/docs-usefulness/scoring.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/audit-scripts.js",
+ "script": "audit-scripts",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tools/scripts, tasks/README.md, tasks/reports, tests/unit/script-docs.test.js, tests/README.md",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Script auditor — analyses all repo scripts, categorises usage/overlap, generates SCRIPT_AUDIT reports",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/audit-scripts.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script audit-scripts\n * @category validator\n * @purpose qa:repo-health\n * @scope tools/scripts, tasks/README.md, tasks/reports, tests/unit/script-docs.test.js, tests/README.md\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Script auditor — analyses all repo scripts, categorises usage/overlap, generates SCRIPT_AUDIT reports\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/audit-scripts.js [flags]\n */\n\nconst fs = require('fs')\nconst path = require('path')\nconst crypto = require('crypto')\nconst { execSync } = require('child_process')\n\nlet yaml = null\ntry {\n // Optional dependency; parser falls back to text heuristics if unavailable.\n yaml = require('js-yaml')\n} catch (_err) {\n yaml = null\n}\n\nconst REPO_ROOT = process.cwd()\nconst DEFAULT_FORMAT = 'both'\nconst DEFAULT_OUTPUT_DIR = 'tasks/reports/repo-ops'\nconst REPORTS_INDEX_PATH = path.join('tasks', 'reports', 'INDEX.md')\n\nconst RULES_SOURCE = ['tests/unit/script-docs.test.js', 'tests/README.md']\n\nconst LEGACY_REQUIRED_TAGS = [\n '@script',\n '@summary',\n '@owner',\n '@scope',\n '@usage',\n '@inputs',\n '@outputs',\n '@exit-codes',\n '@examples',\n '@notes',\n]\n\nconst FRAMEWORK_REQUIRED_TAGS = [\n '@script',\n '@category',\n '@purpose',\n '@scope',\n '@owner',\n '@needs',\n '@purpose-statement',\n '@pipeline',\n]\n\nconst LEGACY_INLINE_REQUIRED_TAGS = ['@script', '@summary', '@owner', '@scope']\nconst FRAMEWORK_INLINE_REQUIRED_TAGS = FRAMEWORK_REQUIRED_TAGS\nconst LEGACY_BLOCK_REQUIRED_TAGS = [\n '@usage',\n '@inputs',\n '@outputs',\n '@exit-codes',\n '@examples',\n '@notes',\n]\nconst PLACEHOLDER_PATTERNS = [\n /^<.*>$/,\n /^todo\\b/i,\n /^tbd\\b/i,\n /^fill\\b/i,\n /^replace$/i,\n /^replace me$/i,\n /^n\\/a$/i,\n /^none$/i,\n /^placeholder$/i,\n]\n\nconst SCRIPT_EXTENSIONS = new Set([\n '.js',\n '.cjs',\n '.mjs',\n '.ts',\n '.tsx',\n '.sh',\n '.bash',\n '.py',\n])\nconst PACKAGE_JSON_PATHS = [\n 'tests/package.json',\n 'tools/package.json',\n\n 'tools/scripts/snippets/generate-data/scripts/package.json',\n]\nconst WORKFLOW_DIR = '.github/workflows'\nconst HOOK_FILES = ['.githooks/pre-commit', '.githooks/verify.sh']\n\nconst IGNORED_DIR_SEGMENTS = new Set([\n 'node_modules',\n '.git',\n '.venv',\n 'tmp',\n 'notion',\n])\nconst SCRIPT_COMMAND_NAMES = new Set([\n 'node',\n 'bash',\n 'sh',\n 'python',\n 'python3',\n 'npm',\n 'npx',\n 'pnpm',\n 'yarn',\n])\n\nfunction usage() {\n console.log(\n 'Usage: node tools/scripts/audit-scripts.js [--format both|md|json] [--output-dir ] [--strict]'\n )\n}\n\nfunction parseArgs(argv) {\n const out = {\n format: DEFAULT_FORMAT,\n outputDir: DEFAULT_OUTPUT_DIR,\n strict: false,\n }\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i]\n if (token === '--strict') {\n out.strict = true\n continue\n }\n\n if (token === '--format') {\n out.format = String(argv[i + 1] || '').trim()\n i += 1\n continue\n }\n\n if (token.startsWith('--format=')) {\n out.format = token.slice('--format='.length).trim()\n continue\n }\n\n if (token === '--output-dir') {\n out.outputDir = String(argv[i + 1] || '').trim()\n i += 1\n continue\n }\n\n if (token.startsWith('--output-dir=')) {\n out.outputDir = token.slice('--output-dir='.length).trim()\n continue\n }\n\n console.error(`Unknown argument: ${token}`)\n usage()\n process.exit(1)\n }\n\n if (!['both', 'md', 'json'].includes(out.format)) {\n console.error(`Invalid --format value: ${out.format}`)\n usage()\n process.exit(1)\n }\n\n if (!out.outputDir) {\n console.error('Missing --output-dir value')\n usage()\n process.exit(1)\n }\n\n return out\n}\n\nfunction normalizeRepoPath(value) {\n return String(value || '')\n .split(path.sep)\n .join('/')\n}\n\nfunction ensureWithinRepo(absPath) {\n const rel = normalizeRepoPath(path.relative(REPO_ROOT, absPath))\n if (!rel || rel === '.' || rel.startsWith('..')) return ''\n return rel\n}\n\nfunction fileExists(repoPath) {\n const full = path.join(REPO_ROOT, repoPath)\n return fs.existsSync(full) && fs.statSync(full).isFile()\n}\n\nfunction readFileSafe(repoPath) {\n try {\n return fs.readFileSync(path.join(REPO_ROOT, repoPath), 'utf8')\n } catch (_err) {\n return ''\n }\n}\n\nfunction shouldExclude(repoPath) {\n const p = normalizeRepoPath(repoPath)\n if (p.startsWith('tools/scripts/archive/')) return true\n const parts = p.split('/')\n if (parts.some((part) => IGNORED_DIR_SEGMENTS.has(part))) return true\n if (p.endsWith('.disabled')) return true\n if (path.basename(p).includes('.bak')) return true\n return false\n}\n\nfunction hasShebang(repoPath) {\n try {\n const content = fs.readFileSync(path.join(REPO_ROOT, repoPath), 'utf8')\n return content.startsWith('#!')\n } catch (_err) {\n return false",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/script-footprint-and-usage-audit.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/script-footprint-and-usage-audit.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/validators/components/check-mdx-component-scope.js",
+ "script": "check-mdx-component-scope",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tools/scripts/validators/components, tests/run-all.js, tests/run-pr-checks.js, snippets/components, tests/utils",
+ "owner": "docs",
+ "needs": "R-R10, R-R29",
+ "purpose_statement": "Validates MDX-facing component modules do not depend on private file-scope helper bindings from exported components.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/scripts/validators/components/check-mdx-component-scope.js [--files path[,path...]] [--staged]",
+ "header": "#!/usr/bin/env node\n/**\n * @script check-mdx-component-scope\n * @category validator\n * @purpose qa:repo-health\n * @scope tools/scripts/validators/components, tests/run-all.js, tests/run-pr-checks.js, snippets/components, tests/utils\n * @owner docs\n * @needs R-R10, R-R29\n * @purpose-statement Validates MDX-facing component modules do not depend on private file-scope helper bindings from exported components.\n * @pipeline manual\n * @usage node tools/scripts/validators/components/check-mdx-component-scope.js [--files path[,path...]] [--staged]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\nconst {\n getMdxFiles,\n getStagedDocsPageFiles,\n getStagedFiles\n} = require('../../../../tests/utils/file-walker');\nconst { extractImports } = require('../../../../tests/utils/mdx-parser');\n\nconst RULE_LABEL = '[4.12]';\nconst DEFAULT_COMPONENT_ROOT = 'snippets/components';\nconst COMPONENT_EXPORT_NAME_RE = /^[A-Z][A-Za-z0-9]*$/;\nconst KNOWN_GLOBALS = new Set([\n 'Array',\n 'Boolean',\n 'Date',\n 'Error',\n 'Function',\n 'JSON',\n 'Map',\n 'Math',\n 'Number',\n 'Object',\n 'Promise',\n 'Reflect',\n 'RegExp',\n 'Set',\n 'String',\n 'Symbol',\n 'URL',\n 'URLSearchParams',\n 'WeakMap',\n 'WeakSet',\n 'BigInt',\n 'Infinity',\n 'NaN',\n 'undefined',\n 'console',\n 'document',\n 'window',\n 'navigator',\n 'location',\n 'history',\n 'localStorage',\n 'sessionStorage',\n 'requestAnimationFrame',\n 'cancelAnimationFrame',\n 'setTimeout',\n 'clearTimeout',\n 'setInterval',\n 'clearInterval',\n 'fetch',\n 'Headers',\n 'Request',\n 'Response',\n 'AbortController',\n 'IntersectionObserver',\n 'MutationObserver',\n 'ResizeObserver',\n 'HTMLElement',\n 'Node',\n 'process',\n '__dirname',\n '__filename',\n 'require',\n 'module',\n 'exports',\n 'global',\n 'globalThis',\n 'Intl'\n]);\n\nfunction getRepoRoot() {\n const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n if (result.status === 0 && String(result.stdout || '').trim()) {\n return String(result.stdout || '').trim();\n }\n return process.cwd();\n}\n\nconst REPO_ROOT = getRepoRoot();\nif (process.cwd() !== REPO_ROOT) {\n process.chdir(REPO_ROOT);\n}\n\nfunction loadBabelParser() {\n try {\n return require('@babel/parser');\n } catch (_error) {\n return require(path.join(REPO_ROOT, 'tools', 'node_modules', '@babel', 'parser'));\n }\n}\n\nconst { parse } = loadBabelParser();\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction usage() {\n console.log(\n [\n 'Usage: node tools/scripts/validators/components/check-mdx-component-scope.js [options]',\n '',\n 'Options:',\n ` --files Limit validation to specific component files under ${DEFAULT_COMPONENT_ROOT}`,\n ' --staged Validate changed component files plus components imported by staged routable MDX pages',\n ' --help, -h Show this message'\n ].join('\\n')\n );\n}\n\nfunction parseArgs(argv) {\n const args = {\n files: [],\n staged: false,\n help: false\n };\n\n for (let index = 0; index < argv.length; index += 1) {\n const token = argv[index];\n\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n\n if (token === '--staged') {\n args.staged = true;\n continue;\n }\n\n if (token === '--files' || token === '--file') {\n const raw = String(argv[index + 1] || '').trim();\n if (raw) {\n raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((part) => args.files.push(part));\n }\n index += 1;\n continue;\n }\n\n if (token.startsWith('--files=')) {\n token\n .slice('--files='.length)\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((part) => args.files.push(part));\n continue;\n }\n\n if (token.startsWith('--file=')) {\n token\n .slice('--file='.length)\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((part) => args.files.push(part));\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n args.files = [...new Set(args.files)];\n return args;\n}\n\nfunction normalizeRepoPath(inputPath) {\n const raw = String(inputPath || '').trim();\n if (!raw) return '';\n const absolute = path.isAbsolute(raw) ? path.resolve(raw) : path.resolve(REPO_ROOT, raw);\n return toPosix(path.relative(REPO_ROOT, absolute));\n}\n\nfunction isSafeRepoRelative(relPath) {\n return relPath && relPath !== '..' && !relPath.startsWith('../');\n}\n\nfunction resolveImportTarget(importPath, importerFile) {\n const raw = String(importPath || '').trim();\n if (!raw) return '';\n\n let basePath = '';\n if (raw.startsWith('/')) {\n basePath = path.resolve(REPO_ROOT, raw.replace(/^\\/+/, ''));\n } else if (raw.startsWith('.')) {\n basePath = path.resolve(path.dirname(importerFile), raw);\n } else {\n return '';\n }\n\n const candidates = [];\n if (path.extname(basePath)) {\n candidates.push(basePath);\n } else {\n candidates.push(basePath);\n candidates.push(`${basePath}.jsx`);\n candidates.push(`${basePath}.js`);\n candidates.push(path.join(basePath, 'index.jsx'));\n candidates.push(path.join(basePath, 'index.js'));\n }",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/mdx-component-scope.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/generate-component-governance-remediation-reports.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/mdx-component-scope.test.js; indirect via tools/scripts/generate-component-governance-remediation-reports.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tests/utils/openapi-rolling-issue.js",
+ "script": "openapi-rolling-issue",
+ "category": "validator",
+ "purpose": "tooling:api-spec",
+ "scope": "tests/utils, tests/unit, .github/workflows/openapi-reference-validation.yml",
+ "owner": "docs",
+ "needs": "F-R17",
+ "purpose_statement": "OpenAPI rolling issue utility — creates/updates GitHub issues for persistent OpenAPI audit findings",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "node tests/utils/openapi-rolling-issue.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script openapi-rolling-issue\n * @category validator\n * @purpose tooling:api-spec\n * @scope tests/utils, tests/unit, .github/workflows/openapi-reference-validation.yml\n * @owner docs\n * @needs F-R17\n * @purpose-statement OpenAPI rolling issue utility — creates/updates GitHub issues for persistent OpenAPI audit findings\n * @pipeline indirect -- library module\n * @usage node tests/utils/openapi-rolling-issue.js [flags]\n */\n\nconst ROLLING_ISSUE_MARKER = '';\nconst ROLLING_ISSUE_TITLE = '[tooling]: OpenAPI reference validation failures';\nconst ROLLING_ISSUE_LABELS = ['docs-v2', 'help wanted', 'status: needs-triage', 'type: bug', 'area: ci-cd'];\n\nfunction compareFindings(a, b) {\n const fileA = String(a?.file || '');\n const fileB = String(b?.file || '');\n if (fileA !== fileB) return fileA.localeCompare(fileB);\n\n const lineA = Number(a?.line || 0);\n const lineB = Number(b?.line || 0);\n if (lineA !== lineB) return lineA - lineB;\n\n const typeA = String(a?.type || '');\n const typeB = String(b?.type || '');\n if (typeA !== typeB) return typeA.localeCompare(typeB);\n\n const refA = String(a?.reference || '');\n const refB = String(b?.reference || '');\n if (refA !== refB) return refA.localeCompare(refB);\n\n const specA = String(a?.resolvedSpec || '');\n const specB = String(b?.resolvedSpec || '');\n return specA.localeCompare(specB);\n}\n\nfunction formatFindingLine(finding) {\n const ref = finding?.reference ? ` (${finding.reference})` : '';\n const spec = finding?.resolvedSpec ? ` [${finding.resolvedSpec}]` : '';\n return `- ${finding.type}: ${finding.file}:${finding.line}${ref}${spec}`;\n}\n\nfunction buildTopFindings(findings, limit = 30) {\n const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 30;\n const sorted = Array.isArray(findings)\n ? findings.slice().sort(compareFindings)\n : [];\n const lines = sorted.slice(0, safeLimit).map(formatFindingLine);\n return lines.length > 0 ? lines.join('\\n') : '- none';\n}\n\nfunction findMarkerIssue(items, marker = ROLLING_ISSUE_MARKER) {\n if (!Array.isArray(items) || items.length === 0) return null;\n\n const matches = items.filter((item) => String(item?.body || '').includes(marker));\n if (matches.length === 0) return null;\n\n matches.sort((a, b) => {\n const aOpen = a?.state === 'open' ? 1 : 0;\n const bOpen = b?.state === 'open' ? 1 : 0;\n if (aOpen !== bOpen) return bOpen - aOpen;\n return Number(b?.number || 0) - Number(a?.number || 0);\n });\n\n return matches[0] || null;\n}\n\nfunction buildIssueBody({\n runUrl,\n topFindings,\n totalFailures,\n totalFiles,\n totalReferences,\n marker = ROLLING_ISSUE_MARKER\n}) {\n return [\n marker,\n '',\n '### Area',\n 'area: ci-cd',\n '',\n '### Failing command or workflow',\n '.github/workflows/openapi-reference-validation.yml',\n '',\n '### Script or workflow path',\n 'tests/integration/openapi-reference-audit.js and .github/workflows/openapi-reference-validation.yml',\n '',\n '### Full error output',\n '```text',\n topFindings || '- none',\n '```',\n '',\n '### Reproduction conditions',\n [\n '- Triggered by CI OpenAPI Reference Validation workflow.',\n '- Scope: v2 + locales (`v2/es`, `v2/fr`, `v2/cn`).',\n `- Workflow run: ${runUrl}`\n ].join('\\n'),\n '',\n '### Expected behavior',\n 'All OpenAPI endpoint references should resolve to the canonical mapped spec and pass strict validation.',\n '',\n '### Action requested from maintainers',\n 'Review reported references, apply required content corrections, and re-run the OpenAPI reference audit workflow.',\n '',\n '### Classification',\n 'classification: high',\n '',\n '### Priority',\n 'priority: high',\n '',\n '### Additional context',\n [\n `- Total failures: ${totalFailures}`,\n `- Files analyzed: ${totalFiles}`,\n `- References analyzed: ${totalReferences}`\n ].join('\\n')\n ].join('\\n');\n}\n\nfunction buildResolutionComment(runUrl) {\n return [\n 'OpenAPI reference validation is passing again.',\n '',\n `Resolved in run: ${runUrl}`\n ].join('\\n');\n}\n\nfunction getIssueAction({ existingIssue, totalFailures }) {\n const failures = Number(totalFailures || 0);\n const hasExisting = Boolean(existingIssue);\n const existingOpen = hasExisting && existingIssue.state === 'open';\n\n if (failures === 0) {\n return existingOpen ? 'close' : 'noop';\n }\n\n return hasExisting ? 'update' : 'create';\n}\n\nmodule.exports = {\n ROLLING_ISSUE_MARKER,\n ROLLING_ISSUE_TITLE,\n ROLLING_ISSUE_LABELS,\n buildTopFindings,\n findMarkerIssue,\n buildIssueBody,\n buildResolutionComment,\n getIssueAction\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/openapi-rolling-issue.test.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/openapi-rolling-issue.test.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": ".githooks/server-manager.js",
+ "script": "server-manager",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": ".githooks",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Manages Mintlify dev server lifecycle for browser tests (start/stop/health-check)",
+ "pipeline_declared": "indirect — legacy browser-validation module imported by .githooks/verify-browser.js",
+ "usage": "node .githooks/server-manager.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script server-manager\n * @category utility\n * @purpose tooling:dev-tools\n * @scope .githooks\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement Manages Mintlify dev server lifecycle for browser tests (start/stop/health-check)\n * @pipeline indirect — legacy browser-validation module imported by .githooks/verify-browser.js\n * @usage node .githooks/server-manager.js [flags]\n */\n/**\n * Server management utility for browser tests\n * Automatically starts mint dev if not running and manages the process lifecycle\n */\n\nconst { spawn, execSync } = require('child_process');\nconst http = require('http');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\n\n// Use a dedicated port for browser validation tests (unlikely to be in use)\nconst TEST_PORT = 3145;\nconst BASE_URL = process.env.MINT_BASE_URL || `http://localhost:${TEST_PORT}`;\nconst PORT = new URL(BASE_URL).port || TEST_PORT;\nconst PID_FILE = path.join(os.tmpdir(), 'mint-dev-test.pid');\nconst LOG_FILE = path.join(os.tmpdir(), 'mint-dev-test.log');\n\nlet serverProcess = null;\nlet serverStartedByUs = false;\nlet actualServerUrl = BASE_URL; // Will be updated if port is detected from log\nlet detectedServerPort = null; // Port where server was actually found\n\n/**\n * Check if server is already running (on expected port, detected port, or common ports)\n */\nasync function isServerRunning(options = {}) {\n const { probePath, allowCommonPorts = true } = options;\n // Check expected port first (3145)\n if (await isServerRunningOnPort(PORT, probePath)) {\n return true;\n }\n \n // Check common mint dev ports (3000, 3001, 3002, etc.)\n // Mint dev often uses these ports if 3000 is in use\n if (allowCommonPorts) {\n for (let commonPort = 3000; commonPort <= 3010; commonPort++) {\n if (await isServerRunningOnPort(commonPort, probePath)) {\n // Found server on common port - store it for getServerUrl()\n detectedServerPort = commonPort;\n console.log(` Found existing server on port ${commonPort}, using it`);\n return true;\n }\n }\n }\n \n // Check if log shows server on different port\n if (allowCommonPorts) {\n const detectedPort = detectPortFromLog();\n if (detectedPort && detectedPort !== PORT) {\n return await isServerRunningOnPort(detectedPort, probePath);\n }\n }\n \n return false;\n}\n\n/**\n * Parse log file to detect actual port mint dev is using\n * Looks for patterns like \"local → http://localhost:3001\" or \"port 3000 is already in use. trying 3001 instead\"\n */\nfunction detectPortFromLog() {\n if (!fs.existsSync(LOG_FILE)) {\n return null;\n }\n \n try {\n const logContent = fs.readFileSync(LOG_FILE, 'utf8');\n \n const lastMatch = (regex) => {\n const matches = [...logContent.matchAll(regex)];\n if (!matches.length) return null;\n const last = matches[matches.length - 1];\n return last && last[1] ? parseInt(last[1], 10) : null;\n };\n \n // Pattern 1: \"local → http://localhost:XXXX\"\n const localPort = lastMatch(/local\\s*→\\s*http:\\/\\/localhost:(\\d+)/gi);\n if (localPort) {\n return localPort;\n }\n \n // Pattern 2: \"port XXXX is already in use. trying YYYY instead\"\n const portPort = lastMatch(/port\\s+\\d+\\s+is\\s+already\\s+in\\s+use\\.\\s+trying\\s+(\\d+)\\s+instead/gi);\n if (portPort) {\n return portPort;\n }\n \n // Pattern 3: \"preview ready\" followed by port info\n const previewPort = lastMatch(/preview\\s+ready[^\\n]*localhost:(\\d+)/gi);\n if (previewPort) {\n return previewPort;\n }\n } catch (e) {\n // Ignore errors reading log\n }\n \n return null;\n}\n\n/**\n * Check if server is running on a specific port\n */\nfunction normalizeProbePath(probePath) {\n if (!probePath) {\n return '';\n }\n return probePath.startsWith('/') ? probePath : `/${probePath}`;\n}\n\nasync function isServerRunningOnPort(port, probePath) {\n const pathSuffix = normalizeProbePath(probePath);\n const url = `http://localhost:${port}${pathSuffix}`;\n return new Promise((resolve) => {\n const req = http.get(url, { timeout: 2000 }, (res) => {\n if (!Number.isInteger(res.statusCode)) {\n resolve(false);\n return;\n }\n if (pathSuffix) {\n // Treat 404 on probe paths as a mismatch (not the expected server).\n resolve(res.statusCode !== 404);\n return;\n }\n // Any HTTP response means the server is up (Mint may return redirects during startup).\n resolve(res.statusCode > 0);\n });\n \n req.on('error', () => resolve(false));\n req.on('timeout', () => {\n req.destroy();\n resolve(false);\n });\n });\n}\n\n/**\n * Wait for server to be ready, checking expected port, common ports, and detected port from log\n */\nasync function waitForServer(maxAttempts = 60, interval = 2000, options = {}) {\n const { probePath, allowCommonPorts = true } = options;\n for (let i = 0; i < maxAttempts; i++) {\n // First check expected port (3145)\n if (await isServerRunningOnPort(PORT, probePath)) {\n return true;\n }\n \n // Check common ports (3000-3010) - mint dev often uses these if 3145 is unavailable\n if (allowCommonPorts) {\n for (let commonPort = 3000; commonPort <= 3010; commonPort++) {\n if (await isServerRunningOnPort(commonPort, probePath)) {\n detectedServerPort = commonPort;\n console.log(` Server started on port ${commonPort} (expected ${PORT})`);\n return true;\n }\n }\n }\n \n // If not on expected or common ports, try to detect from log (after a few attempts to let log populate)\n if (allowCommonPorts && i >= 3) {\n const detectedPort = detectPortFromLog();\n if (detectedPort && detectedPort !== PORT) {\n // Check detected port\n if (await isServerRunningOnPort(detectedPort, probePath)) {\n detectedServerPort = detectedPort;\n console.log(` Server detected on port ${detectedPort} from log (expected ${PORT})`);\n return true;\n }\n }\n }\n \n if (i < maxAttempts - 1) {\n await new Promise(resolve => setTimeout(resolve, interval));\n }\n }\n return false;\n}\n\n/**\n * Start mint dev server\n */\nfunction startServer() {\n // Check if already running from a previous test\n if (fs.existsSync(PID_FILE)) {\n try {\n const existingPid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim());\n // Check if process is still running\n try {\n process.kill(existingPid, 0); // Signal 0 just checks if process exists\n console.log(`⚠️ Found existing mint dev process (PID: ${existingPid}), reusing...`);\n serverStartedByUs = false;\n return existingPid;\n } catch (e) {\n // Process doesn't exist, remove stale PID file\n fs.unlinkSync(PID_FILE);\n }\n } catch (e) {\n // Ignore errors reading PID file\n }\n }\n\n console.log(`🚀 Starting mint dev server on port ${PORT}...`);\n \n // Start mint dev in background with specific port via environment variable\n // Use 'pipe' instead of WriteStream directly to avoid stdio issues\n serverProcess = spawn('mint', ['dev', '--port', PORT.toString(), '--no-open'], {\n stdio: ['ignore', 'pipe', 'pipe'],\n detached: true,",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": ".githooks/verify-browser.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/integration/browser.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/integration/mdx-component-runtime-smoke.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/integration/v2-wcag-audit.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": ".githooks/mint-dev-test.log",
+ "type": "generated-output",
+ "call": "createWriteStream"
+ },
+ {
+ "output_path": ".githooks/mint-dev-test.pid",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": ".githooks/mint-dev-test.log, .githooks/mint-dev-test.pid",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via .githooks/verify-browser.js; indirect via tests/integration/browser.test.js; indirect via tests/integration/mdx-component-runtime-smoke.js; indirect via tests/integration/v2-wcag-audit.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tests/utils/file-walker.js",
+ "script": "file-walker",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "File tree walker — recursively finds files matching patterns. Used by pre-commit hook and validators.",
+ "pipeline_declared": "indirect — library module",
+ "usage": "node tests/utils/file-walker.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script file-walker\n * @category utility\n * @purpose tooling:dev-tools\n * @scope tests\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement File tree walker — recursively finds files matching patterns. Used by pre-commit hook and validators.\n * @pipeline indirect — library module\n * @dualmode dual-mode (document flags)\n * @usage node tests/utils/file-walker.js [flags]\n */\n/**\n * File traversal utilities for testing\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst { filterPathsByMintIgnore } = require('./mintignore');\n\nfunction toPosix(filePath) {\n return String(filePath || '').split(path.sep).join('/');\n}\n\nfunction getRepoRoot(rootDir = process.cwd()) {\n try {\n return execSync('git rev-parse --show-toplevel', {\n encoding: 'utf8',\n cwd: rootDir\n }).trim();\n } catch (_error) {\n return rootDir;\n }\n}\n\nfunction resolveRepoRoot(rootDir = null) {\n return getRepoRoot(rootDir || process.cwd());\n}\n\nfunction normalizeDocsRouteKey(routePath) {\n let normalized = toPosix(routePath).trim();\n normalized = normalized.replace(/^\\/+/, '');\n normalized = normalized.replace(/\\.(md|mdx)$/i, '');\n normalized = normalized.replace(/\\/index$/i, '');\n normalized = normalized.replace(/\\/+$/, '');\n return normalized;\n}\n\nfunction isExcludedV2ExperimentalPath(relPath) {\n const rel = toPosix(relPath).replace(/^\\/+/, '');\n if (!rel.startsWith('v2/')) return false;\n return rel\n .split('/')\n .some((segment) => segment.toLowerCase().startsWith('x-'));\n}\n\nfunction collectDocsPageEntries(node, out = []) {\n if (typeof node === 'string') {\n const value = node.trim();\n const normalizedValue = value.replace(/^\\/+/, '');\n if (normalizedValue.startsWith('v1/')) {\n out.push(normalizedValue);\n } else if (\n normalizedValue.startsWith('v2/') &&\n !isExcludedV2ExperimentalPath(normalizedValue)\n ) {\n out.push(normalizedValue);\n }\n return out;\n }\n\n if (Array.isArray(node)) {\n node.forEach((item) => collectDocsPageEntries(item, out));\n return out;\n }\n\n if (!node || typeof node !== 'object') {\n return out;\n }\n\n if (Array.isArray(node.pages)) {\n node.pages.forEach((item) => collectDocsPageEntries(item, out));\n }\n\n Object.values(node).forEach((value) => collectDocsPageEntries(value, out));\n return out;\n}\n\nfunction getDocsJsonRouteKeys(rootDir = null) {\n const repoRoot = resolveRepoRoot(rootDir);\n const docsJsonPath = path.join(repoRoot, 'docs.json');\n if (!fs.existsSync(docsJsonPath)) {\n return new Set();\n }\n\n const docsJson = JSON.parse(fs.readFileSync(docsJsonPath, 'utf8'));\n const versions = docsJson?.navigation?.versions || [];\n const entries = [];\n\n versions.forEach((versionNode) => {\n if (versionNode?.languages) {\n collectDocsPageEntries(versionNode.languages, entries);\n }\n });\n\n const keys = new Set();\n entries.forEach((entry) => {\n const key = normalizeDocsRouteKey(entry);\n if (key) {\n keys.add(key);\n }\n });\n return keys;\n}\n\nfunction toDocsRouteKeyFromFile(filePath, rootDir = null) {\n const repoRoot = resolveRepoRoot(rootDir);\n const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(repoRoot, filePath);\n const relPath = toPosix(path.relative(repoRoot, absPath));\n if (!(relPath.startsWith('v1/') || relPath.startsWith('v2/'))) {\n return '';\n }\n if (relPath.startsWith('v2/') && isExcludedV2ExperimentalPath(relPath)) {\n return '';\n }\n return normalizeDocsRouteKey(relPath);\n}\n\nfunction toDocsRouteKeyFromFileV2Aware(filePath, rootDir = null) {\n const repoRoot = resolveRepoRoot(rootDir);\n const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(repoRoot, filePath);\n const relPath = toPosix(path.relative(repoRoot, absPath));\n if (!(relPath.startsWith('v1/') || relPath.startsWith('v2/'))) {\n return '';\n }\n if (relPath.startsWith('v2/') && isExcludedV2ExperimentalPath(relPath)) {\n return '';\n }\n return normalizeDocsRouteKey(relPath);\n}\n\nfunction collectFiles(dir, pattern, fileList = []) {\n const files = fs.readdirSync(dir);\n\n files.forEach((file) => {\n const filePath = path.join(dir, file);\n const stat = fs.statSync(filePath);\n\n if (stat.isDirectory()) {\n // Skip node_modules and .git\n if (!file.startsWith('.') && file !== 'node_modules') {\n collectFiles(filePath, pattern, fileList);\n }\n } else if (pattern.test(file)) {\n fileList.push(filePath);\n }\n });\n\n return fileList;\n}\n\n/**\n * Recursively get all files matching a pattern\n */\nfunction getFiles(dir, pattern, options = {}) {\n const { rootDir = null, respectMintIgnore = true } = options;\n const repoRoot = resolveRepoRoot(rootDir || dir);\n const files = collectFiles(dir, pattern, []);\n return filterPathsByMintIgnore(files, { rootDir: repoRoot, respectMintIgnore });\n}\n\n/**\n * Get all routable MDX files in v2\n */\nfunction getMdxFiles(rootDir = null, options = {}) {\n const { respectMintIgnore = true } = options;\n const repoRoot = resolveRepoRoot(rootDir);\n const docsRouteKeys = getDocsJsonRouteKeys(repoRoot);\n const v2DocsFiles = getV2DocsFiles({ rootDir: repoRoot, respectMintIgnore });\n const mdxFiles = v2DocsFiles.filter((filePath) => filePath.endsWith('.mdx'));\n\n if (docsRouteKeys.size === 0) {\n return mdxFiles;\n }\n\n return mdxFiles.filter((filePath) => {\n const key = toDocsRouteKeyFromFileV2Aware(filePath, repoRoot);\n return key && docsRouteKeys.has(key);\n });\n}\n\n/**\n * Get all JSX files in snippets/components\n */\nfunction getJsxFiles(rootDir = null, options = {}) {\n const { respectMintIgnore = true } = options;\n const repoRoot = resolveRepoRoot(rootDir);\n const componentsDir = path.join(repoRoot, 'snippets', 'components');\n if (!fs.existsSync(componentsDir)) {\n return [];\n }\n return getFiles(componentsDir, /\\.jsx$/, { rootDir: repoRoot, respectMintIgnore });\n}\n\n/**\n * Get staged files from git\n * Returns absolute paths relative to repo root (not cwd)\n */\nfunction getStagedFiles(rootDir = null) {\n try {\n // Get repo root directory (where .git is)\n const repoRoot = resolveRepoRoot(rootDir);\n \n const output = execSync('git diff --cached --name-only --diff-filter=ACMR', {\n encoding: 'utf8',\n cwd: repoRoot\n });\n return output",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": ".githooks/verify-browser.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/integration/browser.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/integration/domain-pages-audit.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/integration/v2-link-audit.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/integration/v2-wcag-audit.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/links-imports.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/mdx.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/quality.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/spelling.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/style-guide.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/v2-wcag-audit.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/assign-purpose-metadata.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/audit-v2-usefulness.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/remediators/content/repair-spelling.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/validators/components/check-mdx-component-scope.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/validators/content/check-double-headers.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/validators/content/check-proper-nouns.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via .githooks/verify-browser.js; indirect via tests/integration/browser.test.js; indirect via tests/integration/domain-pages-audit.js; indirect via tests/integration/v2-link-audit.js; indirect via tests/integration/v2-wcag-audit.js; indirect via tests/run-pr-checks.js; indirect via tests/unit/links-imports.test.js; indirect via tests/unit/mdx.test.js; indirect via tests/unit/quality.test.js; indirect via tests/unit/spelling.test.js; indirect via tests/unit/style-guide.test.js; indirect via tests/unit/v2-wcag-audit.test.js; indirect via tools/scripts/assign-purpose-metadata.js; indirect via tools/scripts/audit-v2-usefulness.js; indirect via tools/scripts/remediators/content/repair-spelling.js; indirect via tools/scripts/validators/components/check-mdx-component-scope.js; indirect via tools/scripts/validators/content/check-double-headers.js; indirect via tools/scripts/validators/content/check-proper-nouns.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tests/utils/mdx-parser.js",
+ "script": "mdx-parser",
+ "category": "validator",
+ "purpose": "tooling:dev-tools",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "MDX parser utility — extracts frontmatter, components, content blocks from MDX files",
+ "pipeline_declared": "indirect — library module",
+ "usage": "node tests/utils/mdx-parser.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script mdx-parser\n * @category validator\n * @purpose tooling:dev-tools\n * @scope tests\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement MDX parser utility — extracts frontmatter, components, content blocks from MDX files\n * @pipeline indirect — library module\n * @usage node tests/utils/mdx-parser.js [flags]\n */\n/**\n * MDX parsing utilities for validation\n */\n\nconst path = require('path');\n\nlet yaml;\ntry {\n yaml = require('js-yaml');\n} catch (_error) {\n yaml = require(path.join(process.cwd(), 'tools', 'node_modules', 'js-yaml'));\n}\n\nfunction getIgnoredRanges(content) {\n const ignoredRanges = [];\n const ignoreRegexes = [\n /```[\\s\\S]*?```/g,\n /~~~[\\s\\S]*?~~~/g,\n /\\{\\/\\*[\\s\\S]*?\\*\\/\\}/g,\n //g\n ];\n\n ignoreRegexes.forEach((regex) => {\n let match;\n\n while ((match = regex.exec(content)) !== null) {\n ignoredRanges.push({\n start: match.index,\n end: match.index + match[0].length\n });\n }\n });\n\n return ignoredRanges;\n}\n\nfunction isIgnoredIndex(index, ignoredRanges) {\n return ignoredRanges.some((range) => index >= range.start && index < range.end);\n}\n\n/**\n * Extract frontmatter from MDX file\n */\nfunction extractFrontmatter(content) {\n const frontmatterRegex = /^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n/;\n const match = content.match(frontmatterRegex);\n \n if (!match) {\n return { exists: false, data: null, raw: null };\n }\n \n try {\n const data = yaml.load(match[1]);\n return { exists: true, data, raw: match[1] };\n } catch (error) {\n return { exists: true, data: null, raw: match[1], error: error.message };\n }\n}\n\n/**\n * Extract imports from MDX file\n */\nfunction extractImports(content) {\n const importRegex = /^import\\s+(?:(?:\\{[^}]*\\}|\\*\\s+as\\s+\\w+|\\w+)(?:\\s*,\\s*(?:\\{[^}]*\\}|\\*\\s+as\\s+\\w+|\\w+))*\\s+from\\s+)?['\"]([^'\"]+)['\"];?/gm;\n const imports = [];\n const ignoredRanges = getIgnoredRanges(content);\n let match;\n \n while ((match = importRegex.exec(content)) !== null) {\n if (isIgnoredIndex(match.index, ignoredRanges)) {\n continue;\n }\n\n imports.push({\n full: match[0],\n path: match[1],\n line: content.substring(0, match.index).split('\\n').length\n });\n }\n \n return imports;\n}\n\n/**\n * Check for unclosed JSX tags\n */\nfunction checkUnclosedTags(content) {\n const errors = [];\n const tagStack = [];\n const ignoreRanges = [];\n const ignoreRegexes = [\n /```[\\s\\S]*?```/g, // markdown code blocks\n /`[^`\\n]*`/g, // inline code spans\n /\\{\\/\\*[\\s\\S]*?\\*\\/\\}/g, // JSX comments\n //g // HTML comments\n ];\n\n ignoreRegexes.forEach((regex) => {\n let match;\n while ((match = regex.exec(content)) !== null) {\n ignoreRanges.push({\n start: match.index,\n end: match.index + match[0].length\n });\n }\n });\n\n function isInIgnoredRange(pos) {\n return ignoreRanges.some((range) => pos >= range.start && pos < range.end);\n }\n\n function getLineNumber(pos) {\n return content.slice(0, pos).split('\\n').length;\n }\n\n function findTagEnd(startIndex) {\n let quote = null;\n let braceDepth = 0;\n\n for (let i = startIndex + 1; i < content.length; i++) {\n const ch = content[i];\n const prev = content[i - 1];\n\n // Inside JSX expression braces, only track nested braces.\n // This avoids treating apostrophes in JSX text (e.g. Livepeer's)\n // as string delimiters, which can hide the real closing '>'.\n if (braceDepth > 0) {\n if (ch === '{') {\n braceDepth += 1;\n continue;\n }\n\n if (ch === '}') {\n braceDepth -= 1;\n continue;\n }\n\n continue;\n }\n\n if (quote) {\n if (ch === quote && prev !== '\\\\') {\n quote = null;\n }\n continue;\n }\n\n if (ch === '\"' || ch === '\\'' || ch === '`') {\n quote = ch;\n continue;\n }\n\n if (ch === '{') {\n braceDepth += 1;\n continue;\n }\n\n if (ch === '>' && braceDepth === 0) {\n return i;\n }\n }\n\n return -1;\n }\n\n for (let cursor = 0; cursor < content.length; ) {\n const tagStart = content.indexOf('<', cursor);\n if (tagStart === -1) {\n break;\n }\n\n if (isInIgnoredRange(tagStart)) {\n cursor = tagStart + 1;\n continue;\n }\n\n let nameStart = tagStart + 1;\n let isClosing = false;\n\n if (content[nameStart] === '/') {\n isClosing = true;\n nameStart += 1;\n }\n\n if (!/[A-Z]/.test(content[nameStart] || '')) {\n cursor = tagStart + 1;\n continue;\n }\n\n let nameEnd = nameStart + 1;\n while (/[A-Za-z0-9]/.test(content[nameEnd] || '')) {\n nameEnd += 1;\n }\n\n const tagName = content.slice(nameStart, nameEnd);\n const lineNumber = getLineNumber(tagStart);\n const tagEnd = findTagEnd(tagStart);\n\n if (tagEnd === -1) {\n errors.push({\n line: lineNumber,\n message: `Unclosed tag <${tagName}>`,\n tag: tagName\n });\n break;\n }\n\n const fullTag = content.slice(tagStart, tagEnd + 1);",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/integration/v2-link-audit.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/links-imports.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/mdx.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/quality.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/lib/docs-usefulness/scoring.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/validators/components/check-mdx-component-scope.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/integration/v2-link-audit.js; indirect via tests/unit/links-imports.test.js; indirect via tests/unit/mdx.test.js; indirect via tests/unit/quality.test.js; indirect via tools/lib/docs-usefulness/scoring.js; indirect via tools/scripts/validators/components/check-mdx-component-scope.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tests/utils/mintignore.js",
+ "script": "mintignore",
+ "category": "validator",
+ "purpose": "tooling:dev-tools",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Mintignore utility — reads .mintignore patterns and filters file lists",
+ "pipeline_declared": "indirect — library module",
+ "usage": "node tests/utils/mintignore.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script mintignore\n * @category validator\n * @purpose tooling:dev-tools\n * @scope tests\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement Mintignore utility — reads .mintignore patterns and filters file lists\n * @pipeline indirect — library module\n * @usage node tests/utils/mintignore.js [flags]\n */\n\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { execFileSync } = require('child_process');\n\nconst tempRepoCache = new Map();\nconst trackedTempRepos = new Set();\nconst GIT_ENV_STRIP_KEYS = ['GIT_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_PREFIX'];\n\nfunction getSanitizedGitEnv(overrides = {}) {\n const env = { ...process.env, ...overrides };\n GIT_ENV_STRIP_KEYS.forEach((key) => {\n delete env[key];\n });\n return env;\n}\n\nfunction toPosix(filePath) {\n return String(filePath || '').split(path.sep).join('/');\n}\n\nfunction getRepoRoot(rootDir = process.cwd()) {\n try {\n return execFileSync('git', ['rev-parse', '--show-toplevel'], {\n cwd: rootDir,\n encoding: 'utf8',\n env: getSanitizedGitEnv()\n }).trim();\n } catch (_error) {\n return rootDir;\n }\n}\n\nfunction normalizeRepoRelativePath(filePath, repoRoot) {\n if (!filePath) return '';\n const raw = String(filePath).trim();\n if (!raw) return '';\n\n const absolutePath = path.isAbsolute(raw)\n ? path.resolve(raw)\n : path.resolve(repoRoot, raw);\n\n let rel = toPosix(path.relative(repoRoot, absolutePath));\n if (!rel) return '';\n rel = rel.replace(/^\\.\\//, '');\n rel = rel.replace(/^\\/+/, '');\n return rel;\n}\n\nfunction getMintIgnorePath(rootDir = null) {\n const repoRoot = getRepoRoot(rootDir || process.cwd());\n const mintIgnorePath = path.join(repoRoot, '.mintignore');\n return fs.existsSync(mintIgnorePath) ? mintIgnorePath : '';\n}\n\nfunction ensureTempGitRepo(repoRoot) {\n const cached = tempRepoCache.get(repoRoot);\n if (cached && fs.existsSync(cached)) {\n return cached;\n }\n\n const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mintignore-check-'));\n execFileSync('git', ['init', '-q'], { cwd: tempDir, env: getSanitizedGitEnv() });\n tempRepoCache.set(repoRoot, tempDir);\n trackedTempRepos.add(tempDir);\n return tempDir;\n}\n\nfunction parseCheckIgnoreOutput(output, mintIgnorePath) {\n const ignored = new Set();\n if (!output) return ignored;\n\n const mintIgnoreAbs = path.resolve(mintIgnorePath);\n const lines = output.split('\\n').filter(Boolean);\n\n lines.forEach((line) => {\n const tabIndex = line.indexOf('\\t');\n if (tabIndex < 0) return;\n\n const meta = line.slice(0, tabIndex);\n const relPath = toPosix(line.slice(tabIndex + 1).trim())\n .replace(/^\\.\\//, '')\n .replace(/^\\/+/, '');\n\n if (!relPath || meta === '::') return;\n\n const sourceMatch = meta.match(/^(.*):(\\d+):([^\\t]*)$/);\n if (!sourceMatch) return;\n\n const sourceFile = path.resolve(sourceMatch[1]);\n const pattern = String(sourceMatch[3] || '').trim();\n\n if (sourceFile === mintIgnoreAbs && !pattern.startsWith('!')) {\n ignored.add(relPath);\n } else {\n ignored.delete(relPath);\n }\n });\n\n return ignored;\n}\n\nfunction listMintIgnoredRepoPaths(paths, options = {}) {\n const { rootDir = null } = options;\n const repoRoot = getRepoRoot(rootDir || process.cwd());\n const mintIgnorePath = getMintIgnorePath(repoRoot);\n if (!mintIgnorePath) return new Set();\n\n const uniqueRelPaths = [...new Set(\n (paths || [])\n .map((entry) => normalizeRepoRelativePath(entry, repoRoot))\n .filter(Boolean)\n )];\n\n if (!uniqueRelPaths.length) {\n return new Set();\n }\n\n try {\n const checkerRepo = ensureTempGitRepo(repoRoot);\n const output = execFileSync(\n 'git',\n [\n '-c',\n `core.excludesfile=${mintIgnorePath}`,\n 'check-ignore',\n '--verbose',\n '--non-matching',\n '--no-index',\n '--stdin'\n ],\n {\n cwd: checkerRepo,\n encoding: 'utf8',\n input: `${uniqueRelPaths.join('\\n')}\\n`,\n maxBuffer: 64 * 1024 * 1024,\n env: getSanitizedGitEnv()\n }\n );\n\n return parseCheckIgnoreOutput(output, mintIgnorePath);\n } catch (_error) {\n return new Set();\n }\n}\n\nfunction filterPathsByMintIgnore(paths, options = {}) {\n const { rootDir = null, respectMintIgnore = true } = options;\n if (!respectMintIgnore) {\n return [...(paths || [])];\n }\n\n const repoRoot = getRepoRoot(rootDir || process.cwd());\n const rows = (paths || []).map((entry) => ({\n original: entry,\n rel: normalizeRepoRelativePath(entry, repoRoot)\n }));\n\n const ignored = listMintIgnoredRepoPaths(rows.map((row) => row.rel), {\n rootDir: repoRoot\n });\n\n return rows\n .filter((row) => !row.rel || !ignored.has(row.rel))\n .map((row) => row.original);\n}\n\nprocess.on('exit', () => {\n trackedTempRepos.forEach((tempDir) => {\n try {\n fs.rmSync(tempDir, { recursive: true, force: true });\n } catch (_error) {\n // Best-effort cleanup only.\n }\n });\n});\n\nmodule.exports = {\n toPosix,\n getRepoRoot,\n normalizeRepoRelativePath,\n getMintIgnorePath,\n listMintIgnoredRepoPaths,\n filterPathsByMintIgnore\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/docs-navigation.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/utils/file-walker.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/docs-navigation.test.js; indirect via tests/utils/file-walker.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/config/v2-internal-report-pages.js",
+ "script": "v2-internal-report-pages",
+ "category": "config",
+ "purpose": "tooling:dev-tools",
+ "scope": "full-repo",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Configuration data — list of internal report page paths for publish-v2-internal-reports.js",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/config/v2-internal-report-pages.js [flags]",
+ "header": "/**\n * @script v2-internal-report-pages\n * @category config\n * @purpose tooling:dev-tools\n * @scope full-repo\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement Configuration data — list of internal report page paths for publish-v2-internal-reports.js\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/config/v2-internal-report-pages.js [flags]\n */\nmodule.exports = {\n categories: [\n {\n slug: 'navigation-links',\n groupTitle: 'Navigation & Links',\n },\n {\n slug: 'quality-accessibility',\n groupTitle: 'Quality & Accessibility',\n },\n {\n slug: 'page-audits',\n groupTitle: 'Page Audits',\n },\n {\n slug: 'repo-ops',\n groupTitle: 'Repo Ops',\n },\n ],\n docsGroups: [\n {\n slug: 'navigation-links',\n groupTitle: 'Navigation & Links',\n },\n {\n slug: 'quality-accessibility',\n groupTitle: 'Quality & Accessibility',\n },\n {\n slug: 'page-audits',\n groupTitle: 'Page Audits',\n },\n {\n slug: 'repo-ops',\n groupTitle: 'Repo Ops',\n },\n {\n slug: 'tests',\n groupTitle: 'Tests',\n },\n ],\n entries: [\n {\n publish: true,\n categorySlug: 'navigation-links',\n docsGroupSlugs: ['navigation-links', 'tests'],\n scriptId: 'docs-navigation.test',\n scriptPath: 'tests/unit/docs-navigation.test.js',\n title: 'Docs Navigation Route Report',\n sidebarTitle: 'Docs Navigation',\n description:\n 'Generated docs.json route validation report from tests/unit/docs-navigation.test.js.',\n sourceType: 'file',\n sourcePath: 'tasks/reports/navigation-links/navigation-report.md',\n targetSlug: 'docs-navigation',\n },\n {\n publish: true,\n categorySlug: 'navigation-links',\n docsGroupSlugs: ['navigation-links', 'tests'],\n scriptId: 'v2-link-audit',\n scriptPath: 'tests/integration/v2-link-audit.js',\n title: 'V2 Link Audit Report',\n sidebarTitle: 'Link Audit',\n description:\n 'Generated V2 MDX link audit report from tests/integration/v2-link-audit.js.',\n sourceType: 'file',\n sourcePath: 'tasks/reports/navigation-links/LINK_TEST_REPORT.md',\n targetSlug: 'v2-link-audit',\n },\n {\n publish: true,\n categorySlug: 'quality-accessibility',\n docsGroupSlugs: ['quality-accessibility', 'tests'],\n scriptId: 'v2-wcag-audit',\n scriptPath: 'tests/integration/v2-wcag-audit.js',\n title: 'V2 WCAG Audit Report',\n sidebarTitle: 'WCAG Audit',\n description:\n 'Generated WCAG audit report from tests/integration/v2-wcag-audit.js.',\n sourceType: 'file',\n sourcePath: 'tasks/reports/quality-accessibility/v2-wcag-audit-report.md',\n targetSlug: 'v2-wcag-audit',\n },\n {\n publish: true,\n categorySlug: 'quality-accessibility',\n scriptId: 'wcag-repair-common',\n scriptPath: 'tools/scripts/wcag-repair-common.js',\n title: 'WCAG Repair Common Report',\n sidebarTitle: 'WCAG Repair',\n description:\n 'Generated WCAG repair report from tools/scripts/wcag-repair-common.js.',\n sourceType: 'file',\n sourcePath: 'tasks/reports/quality-accessibility/v2-wcag-repair-common-report.md',\n targetSlug: 'wcag-repair-common',\n },\n {\n publish: true,\n categorySlug: 'quality-accessibility',\n scriptId: 'audit-v2-usefulness',\n scriptPath: 'tools/scripts/audit-v2-usefulness.js',\n title: 'V2 Usefulness Audit Summary',\n sidebarTitle: 'Usefulness Audit',\n description:\n 'Generated usefulness audit summary from tools/scripts/audit-v2-usefulness.js.',\n sourceType: 'file',\n sourcePath: 'tasks/reports/quality-accessibility/docs-usefulness/latest/summary.md',\n sourceFallbackGlobs: [\n 'tasks/reports/quality-accessibility/docs-usefulness/*/summary.md',\n ],\n targetSlug: 'audit-v2-usefulness',\n },\n {\n publish: true,\n categorySlug: 'page-audits',\n scriptId: 'test-all-pages-comprehensive',\n scriptPath: 'tools/scripts/archive/legacy/test-all-pages-comprehensive.js',\n title: 'All Pages Comprehensive Browser Report',\n sidebarTitle: 'Browser Report',\n description:\n 'Generated browser report from tools/scripts/archive/legacy/test-all-pages-comprehensive.js.',\n sourceType: 'file',\n sourcePath: 'tasks/reports/page-audits/browser-test-report.md',\n targetSlug: 'test-all-pages-comprehensive',\n },\n {\n publish: true,\n categorySlug: 'page-audits',\n scriptId: 'audit-all-pages',\n scriptPath: 'tools/scripts/archive/legacy/audit-all-pages.js',\n title: 'All Pages Audit (Legacy Browser Script)',\n sidebarTitle: 'Audit All Pages',\n description:\n 'Generated audit report from tools/scripts/archive/legacy/audit-all-pages.js.',\n sourceType: 'file',\n sourcePath: 'tasks/reports/page-audits/page-audit-latest.md',\n targetSlug: 'audit-all-pages',\n },\n {\n publish: true,\n categorySlug: 'page-audits',\n scriptId: 'audit-all-pages-simple',\n scriptPath: 'tools/scripts/archive/legacy/audit-all-pages-simple.js',\n title: 'All Pages Audit Simple (File Checks)',\n sidebarTitle: 'Audit Simple',\n description:\n 'Generated file-check audit report from tools/scripts/archive/legacy/audit-all-pages-simple.js.',\n sourceType: 'file',\n sourcePath: 'tasks/reports/page-audits/page-audit-simple-latest.md',\n targetSlug: 'audit-all-pages-simple',\n },\n {\n publish: true,\n categorySlug: 'page-audits',\n scriptId: 'audit-python',\n scriptPath: 'tasks/scripts/audit-python.py',\n title: 'All Pages Audit (Python)',\n sidebarTitle: 'Audit Python',\n description:\n 'Generated Python audit report from tasks/scripts/audit-python.py.',\n sourceType: 'file',\n sourcePath: 'tasks/reports/page-audits/page-audit-python-latest.md',\n targetSlug: 'audit-python',\n },\n {\n publish: true,\n categorySlug: 'page-audits',\n docsGroupSlugs: ['page-audits', 'tests'],\n scriptId: 'domain-pages-audit',\n scriptPath: 'tests/integration/domain-pages-audit.js',\n title: 'Domain Page Load Audit Report',\n sidebarTitle: 'Domain Pages',\n description:\n 'Generated deployed page load audit report from tests/integration/domain-pages-audit.js.',\n sourceType: 'file',\n sourcePath: 'tests/reports/domain-page-load-report.md',\n targetSlug: 'domain-pages-audit',\n },\n {\n publish: true,\n categorySlug: 'repo-ops',\n scriptId: 'audit-scripts',\n scriptPath: 'tools/scripts/audit-scripts.js',\n title: 'Script Audit Report',\n sidebarTitle: 'Script Audit',\n description:\n 'Generated script inventory audit report from tools/scripts/audit-scripts.js.',\n sourceType: 'file',\n sourcePath: 'tasks/reports/repo-ops/SCRIPT_AUDIT.md',\n targetSlug: 'audit-scripts',\n },\n {\n publish: true,\n categorySlug: 'repo-ops',\n scriptId: 'audit-tasks-folders',\n scriptPath: 'tools/scripts/audit-tasks-folders.js',\n title: 'Tasks Folder Audit Reports',\n sidebarTitle: 'Tasks Folder Audits',\n description:\n 'Generated tasks folder audit reports from tools/scripts/audit-tasks-folders.js.',\n sourceType: 'glob',\n sourceGlob: 'tasks/reports/repo-ops/*_audit.md',\n sourceBasenameAllowList: [\n 'errors_audit.md',\n 'reports_navigation-links_audit.md',\n 'reports_page-audits_audit.md',\n 'reports_quality-accessibility_audit.md',\n 'reports_quality-accessibility_docs-usefulness_audit.md',",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": false,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/publish-v2-internal-reports.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/publish-v2-internal-reports.js",
+ "grade": "C",
+ "flags": [
+ "invalid-category"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/notion/1-read-notion-to-csv.js",
+ "script": "1-read-notion-to-csv",
+ "category": "automation",
+ "purpose": "tooling:dev-tools",
+ "scope": "external",
+ "owner": "docs",
+ "needs": "node, @notionhq/client, dotenv, NOTION_API_KEY, NOTION_DATABASE_ID",
+ "purpose_statement": "Reads the Notion pages database, filters v2 rows, and writes CSV/JSON exports for downstream sync steps.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/notion/1-read-notion-to-csv.js [flags]",
+ "header": "/**\n * @script 1-read-notion-to-csv\n * @category automation\n * @purpose tooling:dev-tools\n * @scope external\n * @owner docs\n * @needs node, @notionhq/client, dotenv, NOTION_API_KEY, NOTION_DATABASE_ID\n * @purpose-statement Reads the Notion pages database, filters v2 rows, and writes CSV/JSON exports for downstream sync steps.\n * @pipeline manual\n * @usage node tools/notion/1-read-notion-to-csv.js [flags]\n */\n\nrequire(\"dotenv\").config();\nconst { Client } = require(\"@notionhq/client\");\nconst fs = require(\"fs\");\n\nasync function readNotionToCSV() {\n const notion = new Client({ auth: process.env.NOTION_API_KEY });\n const databaseId = process.env.NOTION_DATABASE_ID;\n\n console.log(\"Fetching all pages from Notion database...\");\n\n const allPages = [];\n let hasMore = true;\n let startCursor = undefined;\n\n while (hasMore) {\n const response = await notion.databases.query({\n database_id: databaseId,\n start_cursor: startCursor,\n });\n\n response.results.forEach((page) => {\n const pageName =\n page.properties[\"Page Name\"]?.title?.[0]?.plain_text || \"\";\n const tabGroup = page.properties[\"Tab Group\"]?.select?.name || \"\";\n const sectionGroup = page.properties[\"Section Group\"]?.select?.name || \"\";\n const subGroup = page.properties[\"Sub Section\"]?.select?.name || \"\";\n const pageStatus =\n page.properties[\"Page Status\"]?.multi_select?.[0]?.plain_text || \"\";\n const notes = page.properties[\"Notes\"]?.rich_text?.[0]?.plain_text || \"\";\n const relativePath = page.properties[\"Relative path URL\"]?.url || \"\";\n\n // ONLY INCLUDE V2 PAGES\n if (relativePath && relativePath.startsWith(\"v2/\")) {\n allPages.push({\n id: page.id,\n pageName,\n tabGroup,\n sectionGroup,\n subGroup,\n pageStatus,\n notes,\n relativePath,\n });\n }\n });\n\n hasMore = response.has_more;\n startCursor = response.next_cursor;\n console.log(`Fetched ${allPages.length} pages...`);\n }\n\n console.log(`\\nTotal pages fetched: ${allPages.length}`);\n\n // Write to CSV\n const csvHeader =\n \"ID,Page Name,Tab Group,Section Group,Sub Group,Page Status,Notes,Relative Path URL\\n\";\n const csvRows = allPages.map(\n (p) =>\n `\"${p.id}\",\"${p.pageName}\",\"${p.tabGroup}\",\"${p.sectionGroup}\",\"${p.subGroup}\",\"${p.pageStatus}\",\"${p.notes}\",\"${p.relativePath}\"`\n );\n const csvContent = csvHeader + csvRows.join(\"\\n\");\n\n // Create data directory if it doesn't exist\n const dataDir = __dirname + \"/data\";\n if (!fs.existsSync(dataDir)) {\n fs.mkdirSync(dataDir, { recursive: true });\n }\n\n const outputPath = dataDir + \"/notion-read.csv\";\n fs.writeFileSync(outputPath, csvContent);\n console.log(`\\nCSV exported to: ${outputPath}`);\n\n // Also write JSON for easier processing\n const jsonPath = dataDir + \"/notion-read.json\";\n fs.writeFileSync(jsonPath, JSON.stringify(allPages, null, 2));\n console.log(`JSON exported to: ${jsonPath}`);\n}\n\nreadNotionToCSV().catch(console.error);\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/notion/3-check-duplicates.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/notion/5-export-to-notion.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/notion/3-check-duplicates.js; indirect via tools/notion/5-export-to-notion.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/notion/2-read-docs-to-csv.js",
+ "script": "2-read-docs-to-csv",
+ "category": "generator",
+ "purpose": "tooling:dev-tools",
+ "scope": "external",
+ "owner": "docs",
+ "needs": "node",
+ "purpose_statement": "Parses docs.json v2 navigation and writes CSV/JSON exports with section-group metadata for Notion sync.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/notion/2-read-docs-to-csv.js [flags]",
+ "header": "/**\n * @script 2-read-docs-to-csv\n * @category generator\n * @purpose tooling:dev-tools\n * @scope external\n * @owner docs\n * @needs node\n * @purpose-statement Parses docs.json v2 navigation and writes CSV/JSON exports with section-group metadata for Notion sync.\n * @pipeline manual\n * @usage node tools/notion/2-read-docs-to-csv.js [flags]\n */\n\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\nfunction readDocsToCSV() {\n console.log(\"Starting docs.json export...\");\n\n const docsJsonPath = path.join(__dirname, \"../docs.json\");\n console.log(`Reading docs.json from: ${docsJsonPath}`);\n\n const docsJson = JSON.parse(fs.readFileSync(docsJsonPath, \"utf8\"));\n console.log(\"docs.json loaded successfully\");\n\n const pages = [];\n const sectionGroupColors = new Map();\n\n function processItem(\n item,\n tabGroup = \"\",\n sectionGroup = \"\",\n subGroup = \"\",\n depth = 0\n ) {\n // Handle string pages (direct page paths)\n if (typeof item === \"string\") {\n const pageName = path.basename(item, \".mdx\");\n const relativePath = item;\n const pageStatus = \"Active\";\n const notes = \"\";\n\n // ONLY INCLUDE V2 PAGES\n // EXCLUDE redirect pages (these are navigation hacks, not real content)\n if (\n relativePath &&\n relativePath.startsWith(\"v2/\") &&\n !relativePath.includes(\"/redirect\")\n ) {\n pages.push({\n pageName,\n tabGroup,\n sectionGroup,\n subGroup,\n pageStatus,\n notes,\n relativePath,\n });\n }\n return;\n }\n\n // Handle arrays\n if (Array.isArray(item)) {\n item.forEach((subItem) =>\n processItem(subItem, tabGroup, sectionGroup, subGroup, depth)\n );\n return;\n }\n\n // Handle groups\n if (item.group) {\n // depth 0 = top-level group under anchor (Section Group)\n // depth 1+ = nested group (Sub Group)\n if (depth === 0) {\n // This is a Section Group (top-level)\n const newSectionGroup = item.group;\n if (!sectionGroupColors.has(newSectionGroup)) {\n sectionGroupColors.set(\n newSectionGroup,\n getColorForIndex(sectionGroupColors.size)\n );\n }\n item.pages?.forEach((page) =>\n processItem(page, tabGroup, newSectionGroup, \"\", depth + 1)\n );\n } else {\n // This is a Sub Group (nested)\n const newSubGroup = item.group;\n item.pages?.forEach((page) =>\n processItem(page, tabGroup, sectionGroup, newSubGroup, depth + 1)\n );\n }\n return;\n }\n\n // Handle tabs\n if (item.tab) {\n const newTabGroup = item.tab;\n item.anchors?.forEach((anchor) =>\n processItem(anchor, newTabGroup, sectionGroup)\n );\n }\n\n // Handle anchors\n if (item.anchor) {\n // If anchor has groups, process groups (which will handle pages)\n // Groups under anchors are depth 0 (Section Groups)\n if (item.groups && item.groups.length > 0) {\n item.groups.forEach((group) => processItem(group, tabGroup, \"\", \"\", 0));\n } else {\n // Only process pages directly if there are no groups\n item.pages?.forEach((page) => processItem(page, tabGroup, \"\", \"\", 0));\n }\n }\n }\n\n // Navigate through the nested structure\n if (docsJson.navigation?.versions) {\n docsJson.navigation.versions.forEach((version) => {\n if (version.languages) {\n version.languages.forEach((language) => {\n if (language.tabs) {\n language.tabs.forEach((tab) => processItem(tab));\n }\n });\n }\n });\n }\n\n console.log(`Found ${pages.length} pages`);\n console.log(`Assigned colors to ${sectionGroupColors.size} section groups`);\n\n // Create data directory if it doesn't exist\n const dataDir = path.join(__dirname, \"data\");\n if (!fs.existsSync(dataDir)) {\n fs.mkdirSync(dataDir, { recursive: true });\n }\n\n // Write to CSV\n const csvHeader =\n \"Page Name,Tab Group,Section Group,Sub Group,Page Status,Notes,Relative Path URL\\n\";\n const csvRows = pages.map(\n (p) =>\n `\"${p.pageName}\",\"${p.tabGroup}\",\"${p.sectionGroup}\",\"${p.subGroup}\",\"${p.pageStatus}\",\"${p.notes}\",\"${p.relativePath}\"`\n );\n const csvContent = csvHeader + csvRows.join(\"\\n\");\n\n const csvPath = path.join(dataDir, \"docs-read.csv\");\n fs.writeFileSync(csvPath, csvContent);\n console.log(`CSV exported to: ${csvPath}`);\n\n // Write JSON with colors\n const jsonPath = path.join(dataDir, \"docs-read.json\");\n const jsonData = {\n pages,\n sectionGroupColors: Object.fromEntries(sectionGroupColors),\n };\n fs.writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2));\n console.log(`JSON exported to: ${jsonPath}`);\n}\n\nfunction getColorForIndex(index) {\n const colors = [\n \"blue\",\n \"brown\",\n \"default\",\n \"gray\",\n \"green\",\n \"orange\",\n \"pink\",\n \"purple\",\n \"red\",\n \"yellow\",\n ];\n return colors[index % colors.length];\n}\n\nreadDocsToCSV();\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/notion/5-export-to-notion.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/notion/data",
+ "type": "directory",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": "tools/notion/docs-read.csv",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/notion/docs-read.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/notion/data, tools/notion/docs-read.csv, tools/notion/docs-read.json",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/notion/5-export-to-notion.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/notion/3-check-duplicates.js",
+ "script": "3-check-duplicates",
+ "category": "validator",
+ "purpose": "tooling:dev-tools",
+ "scope": "external",
+ "owner": "docs",
+ "needs": "node",
+ "purpose_statement": "Analyzes the exported Notion snapshot for duplicate page keys and writes JSON and Markdown reports.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/notion/3-check-duplicates.js [flags]",
+ "header": "/**\n * @script 3-check-duplicates\n * @category validator\n * @purpose tooling:dev-tools\n * @scope external\n * @owner docs\n * @needs node\n * @purpose-statement Analyzes the exported Notion snapshot for duplicate page keys and writes JSON and Markdown reports.\n * @pipeline manual\n * @usage node tools/notion/3-check-duplicates.js [flags]\n */\n\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\nfunction checkDuplicates() {\n console.log(\"Checking for duplicates in Notion database...\\n\");\n\n const notionJsonPath = path.join(__dirname, \"data\", \"notion-read.json\");\n\n if (!fs.existsSync(notionJsonPath)) {\n console.error(\"ERROR: data/notion-read.json not found!\");\n console.error(\"Please run: node 1-read-notion-to-csv.js first\");\n process.exit(1);\n }\n\n const notionPages = JSON.parse(fs.readFileSync(notionJsonPath, \"utf8\"));\n\n console.log(`Total pages in Notion: ${notionPages.length}`);\n\n // Group by unique key: Page Name + Tab Group + Section Group\n const pageGroups = new Map();\n\n notionPages.forEach((page) => {\n const uniqueKey = `${page.pageName}|||${page.tabGroup}|||${page.sectionGroup}`;\n\n if (!pageGroups.has(uniqueKey)) {\n pageGroups.set(uniqueKey, []);\n }\n pageGroups.get(uniqueKey).push(page);\n });\n\n // Find duplicates\n const duplicates = [];\n pageGroups.forEach((pages, key) => {\n if (pages.length > 1) {\n duplicates.push({ key, pages });\n }\n });\n\n console.log(`\\nUnique page combinations: ${pageGroups.size}`);\n console.log(`Duplicate groups found: ${duplicates.length}\\n`);\n\n if (duplicates.length > 0) {\n // Report duplicates\n console.log(\"DUPLICATES FOUND:\\n\");\n duplicates.forEach((dup, index) => {\n const [pageName, tabGroup, sectionGroup] = dup.key.split(\"|||\");\n console.log(\n `${\n index + 1\n }. \"${pageName}\" | Tab: \"${tabGroup}\" | Section: \"${sectionGroup}\"`\n );\n console.log(` Found ${dup.pages.length} copies:`);\n dup.pages.forEach((page, i) => {\n console.log(` ${i + 1}. ID: ${page.id} | Path: ${page.relativePath}`);\n });\n console.log();\n });\n } else {\n console.log(\"✓ No duplicates found!\");\n }\n\n // Create reports directory if it doesn't exist\n const reportsDir = path.join(__dirname, \"reports\");\n if (!fs.existsSync(reportsDir)) {\n fs.mkdirSync(reportsDir, { recursive: true });\n }\n\n // Write duplicates report as JSON\n const jsonReportPath = path.join(reportsDir, \"duplicates-report.json\");\n fs.writeFileSync(jsonReportPath, JSON.stringify(duplicates, null, 2));\n\n // Write duplicates report as Markdown\n let mdReport = `# Duplicates Report\\n\\n`;\n mdReport += `**Generated:** ${new Date().toISOString()}\\n\\n`;\n mdReport += `**Total pages in Notion:** ${notionPages.length}\\n`;\n mdReport += `**Unique page combinations:** ${pageGroups.size}\\n`;\n mdReport += `**Duplicate groups found:** ${duplicates.length}\\n\\n`;\n\n if (duplicates.length === 0) {\n mdReport += `✓ **No duplicates found!**\\n`;\n } else {\n mdReport += `## Duplicates Found\\n\\n`;\n duplicates.forEach((dup, index) => {\n const [pageName, tabGroup, sectionGroup] = dup.key.split(\"|||\");\n mdReport += `### ${index + 1}. \"${pageName}\"\\n\\n`;\n mdReport += `- **Tab Group:** \"${tabGroup}\"\\n`;\n mdReport += `- **Section Group:** \"${sectionGroup}\"\\n`;\n mdReport += `- **Copies found:** ${dup.pages.length}\\n\\n`;\n mdReport += `| # | Notion ID | Relative Path |\\n`;\n mdReport += `|---|-----------|---------------|\\n`;\n dup.pages.forEach((page, i) => {\n mdReport += `| ${i + 1} | \\`${page.id}\\` | ${\n page.relativePath || \"(empty)\"\n } |\\n`;\n });\n mdReport += `\\n`;\n });\n }\n\n const mdReportPath = path.join(reportsDir, \"Duplicates.md\");\n fs.writeFileSync(mdReportPath, mdReport);\n\n console.log(`\\nJSON report saved to: ${jsonReportPath}`);\n console.log(`Markdown report saved to: ${mdReportPath}`);\n\n if (duplicates.length > 0) {\n console.log(`\\nTo remove duplicates, run: node 4-remove-duplicates.js`);\n }\n}\n\ncheckDuplicates();\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/notion/4-remove-duplicates.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/notion/reports",
+ "type": "directory",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": "tools/notion/duplicates-report.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/notion/Duplicates.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/notion/reports, tools/notion/duplicates-report.json, tools/notion/Duplicates.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/notion/4-remove-duplicates.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/notion/4-remove-duplicates.js",
+ "script": "4-remove-duplicates",
+ "category": "remediator",
+ "purpose": "tooling:dev-tools",
+ "scope": "external",
+ "owner": "docs",
+ "needs": "node, @notionhq/client, dotenv, NOTION_API_KEY",
+ "purpose_statement": "Archives duplicate Notion pages from the duplicate report while keeping the first record in each group.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/notion/4-remove-duplicates.js [flags]",
+ "header": "/**\n * @script 4-remove-duplicates\n * @category remediator\n * @purpose tooling:dev-tools\n * @scope external\n * @owner docs\n * @needs node, @notionhq/client, dotenv, NOTION_API_KEY\n * @purpose-statement Archives duplicate Notion pages from the duplicate report while keeping the first record in each group.\n * @pipeline manual\n * @usage node tools/notion/4-remove-duplicates.js [flags]\n */\n\nrequire(\"dotenv\").config();\nconst { Client } = require(\"@notionhq/client\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\nasync function removeDuplicates() {\n const reportPath = path.join(__dirname, \"reports\", \"duplicates-report.json\");\n\n if (!fs.existsSync(reportPath)) {\n console.error(\"ERROR: reports/duplicates-report.json not found!\");\n console.error(\"Please run: node 3-check-duplicates.js first\");\n process.exit(1);\n }\n\n const duplicates = JSON.parse(fs.readFileSync(reportPath, \"utf8\"));\n\n if (duplicates.length === 0) {\n console.log(\"No duplicates to remove!\");\n return;\n }\n\n console.log(`Found ${duplicates.length} duplicate groups`);\n console.log(\"Strategy: Keep the FIRST entry, delete the rest\\n\");\n\n const notion = new Client({ auth: process.env.NOTION_API_KEY });\n\n let deletedCount = 0;\n let errorCount = 0;\n\n for (const dup of duplicates) {\n const [pageName, tabGroup, sectionGroup] = dup.key.split(\"|||\");\n console.log(\n `Processing: \"${pageName}\" | Tab: \"${tabGroup}\" | Section: \"${sectionGroup}\"`\n );\n console.log(` Keeping: ${dup.pages[0].id}`);\n\n // Delete all except the first one\n for (let i = 1; i < dup.pages.length; i++) {\n const pageToDelete = dup.pages[i];\n try {\n await notion.pages.update({\n page_id: pageToDelete.id,\n archived: true,\n });\n console.log(` ✓ Deleted: ${pageToDelete.id}`);\n deletedCount++;\n } catch (error) {\n console.error(\n ` ✗ Failed to delete ${pageToDelete.id}:`,\n error.message\n );\n errorCount++;\n }\n }\n console.log();\n }\n\n console.log(`\\nDeletion complete:`);\n console.log(` Deleted: ${deletedCount}`);\n console.log(` Errors: ${errorCount}`);\n\n // Write cleanup report\n const reportsDir = path.join(__dirname, \"reports\");\n if (!fs.existsSync(reportsDir)) {\n fs.mkdirSync(reportsDir, { recursive: true });\n }\n\n const cleanupReport = {\n timestamp: new Date().toISOString(),\n duplicateGroupsProcessed: duplicates.length,\n pagesDeleted: deletedCount,\n errors: errorCount,\n };\n\n const cleanupReportPath = path.join(reportsDir, \"notion-clean.json\");\n fs.writeFileSync(cleanupReportPath, JSON.stringify(cleanupReport, null, 2));\n console.log(`\\nCleanup report saved to: ${cleanupReportPath}`);\n console.log(`\\nRun 'node 1-read-notion-to-csv.js' to verify the cleanup.`);\n}\n\nremoveDuplicates().catch(console.error);\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/notion/5-export-to-notion.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/notion/reports",
+ "type": "directory",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": "tools/notion/notion-clean.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/notion/reports, tools/notion/notion-clean.json",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/notion/5-export-to-notion.js",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/dev/ensure-mint-watcher-patch.sh",
+ "script": "ensure-mint-watcher-patch",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Mint watcher patcher — applies patch to fix Mintlify file watcher issues in dev mode",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "bash tools/scripts/dev/ensure-mint-watcher-patch.sh [flags]",
+ "header": "#!/usr/bin/env bash\n# @script ensure-mint-watcher-patch\n# @category utility\n# @purpose tooling:dev-tools\n# @scope tools/scripts\n# @owner docs\n# @needs E-C6, F-C1\n# @purpose-statement Mint watcher patcher — applies patch to fix Mintlify file watcher issues in dev mode\n# @pipeline manual — interactive developer tool, not suited for automated pipelines\n# @usage bash tools/scripts/dev/ensure-mint-watcher-patch.sh [flags]\nset -euo pipefail\n\nMODE=\"apply\"\nif [ \"${1:-}\" = \"--check\" ]; then\n MODE=\"check\"\nelif [ \"${1:-}\" = \"--apply\" ] || [ \"${1:-}\" = \"\" ]; then\n MODE=\"apply\"\nelif [ \"${1:-}\" = \"--help\" ] || [ \"${1:-}\" = \"-h\" ]; then\n cat <<'EOF'\nUsage:\n bash tools/scripts/dev/ensure-mint-watcher-patch.sh --check\n bash tools/scripts/dev/ensure-mint-watcher-patch.sh --apply\nEOF\n exit 0\nelse\n echo \"Error: Unknown option: $1\" >&2\n exit 2\nfi\n\nif ! command -v mint >/dev/null 2>&1; then\n echo \"Error: mint CLI not found in PATH.\" >&2\n exit 2\nfi\n\nMINT_BIN=\"$(command -v mint)\"\nMINT_ENTRY_REALPATH=\"$(node -e 'const fs=require(\"fs\"); console.log(fs.realpathSync(process.argv[1]));' \"$MINT_BIN\")\"\nMINT_ROOT=\"$(cd \"$(dirname \"$MINT_ENTRY_REALPATH\")\" && pwd)\"\nLISTENER_PATH=\"$MINT_ROOT/node_modules/@mintlify/previewing/dist/local-preview/listener/index.js\"\n\nif [ ! -f \"$LISTENER_PATH\" ]; then\n echo \"Error: Mint listener file not found at: $LISTENER_PATH\" >&2\n exit 2\nfi\n\nif rg -q \"disableGlobbing:\\\\s*true\" \"$LISTENER_PATH\"; then\n echo \"Mint watcher patch present: $LISTENER_PATH\"\n exit 0\nfi\n\nif [ \"$MODE\" = \"check\" ]; then\n echo \"Mint watcher patch missing: $LISTENER_PATH\" >&2\n exit 1\nfi\n\nif ! rg -q \"^\\\\s*ignoreInitial:\\\\s*true,\" \"$LISTENER_PATH\"; then\n echo \"Error: Could not find expected watcher option in listener file.\" >&2\n exit 2\nfi\n\nif ! node -e '\nconst fs = require(\"fs\");\nconst path = process.argv[1];\nconst src = fs.readFileSync(path, \"utf8\");\nif (src.includes(\"disableGlobbing: true\")) {\n process.exit(0);\n}\nconst needle = \" ignoreInitial: true,\\n\";\nconst injected = \" ignoreInitial: true,\\n disableGlobbing: true,\\n\";\nif (!src.includes(needle)) {\n console.error(\"Unsupported listener format: ignoreInitial block not found.\");\n process.exit(2);\n}\nconst next = src.replace(needle, injected);\nif (next === src) {\n console.error(\"Patch was not applied.\");\n process.exit(2);\n}\ntry {\n fs.writeFileSync(path, next, \"utf8\");\n} catch (err) {\n console.error(err && err.message ? err.message : String(err));\n process.exit(3);\n}\n' \"$LISTENER_PATH\"; then\n echo \"Error: Failed to apply Mint watcher patch at: $LISTENER_PATH\" >&2\n exit 3\nfi\n\necho \"Mint watcher patch applied: $LISTENER_PATH\"\nexit 0\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tools/scripts/mint-dev.sh",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tools/scripts/mint-dev.sh",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tools/scripts/dev/generate-mint-dev-scope.js",
+ "script": "generate-mint-dev-scope",
+ "category": "generator",
+ "purpose": "tooling:dev-tools",
+ "scope": "tools/scripts/dev, docs.json, .mintignore",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Mint dev scope generator — creates a scoped docs.json for running mint dev on a subset of pages",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/dev/generate-mint-dev-scope.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script generate-mint-dev-scope\n * @category generator\n * @purpose tooling:dev-tools\n * @scope tools/scripts/dev, docs.json, .mintignore\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement Mint dev scope generator — creates a scoped docs.json for running mint dev on a subset of pages\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/dev/generate-mint-dev-scope.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\nconst crypto = require('crypto');\nconst readline = require('readline');\n\nconst STRUCTURAL_ARRAY_KEYS = ['versions', 'languages', 'tabs', 'anchors', 'groups', 'pages'];\n\nfunction printUsage() {\n console.log(\n [\n 'Usage: node tools/scripts/dev/generate-mint-dev-scope.js [options]',\n '',\n 'Options:',\n ' --repo-root ',\n ' --workspace-base ',\n ' --scope-file ',\n ' --versions (repeatable)',\n ' --languages (repeatable)',\n ' --tabs (repeatable)',\n ' --prefixes (repeatable)',\n ' --interactive',\n ' --disable-openapi',\n ' --print-only',\n ' --help'\n ].join('\\n')\n );\n}\n\nfunction splitCsv(value) {\n return String(value || '')\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\nfunction uniqStrings(values) {\n return [...new Set((values || []).map((entry) => String(entry || '').trim()).filter(Boolean))];\n}\n\nfunction normalizeRoute(value) {\n return String(value || '')\n .trim()\n .replace(/^\\/+/, '')\n .replace(/\\/+$/, '');\n}\n\nfunction normalizeForCompare(value) {\n return String(value || '').trim().toLowerCase();\n}\n\nfunction routeMatchesPrefix(route, prefix) {\n const normalizedRoute = normalizeRoute(route);\n const normalizedPrefix = normalizeRoute(prefix);\n if (!normalizedRoute || !normalizedPrefix) return false;\n return normalizedRoute === normalizedPrefix || normalizedRoute.startsWith(`${normalizedPrefix}/`);\n}\n\nfunction isLanguageSegment(value) {\n const segment = String(value || '').trim();\n return /^[a-z]{2}(?:-[A-Za-z]{2})?$/.test(segment);\n}\n\nfunction isLikelyRouteEntry(value) {\n const route = normalizeRoute(value);\n if (!route) return false;\n if (route.startsWith('http://') || route.startsWith('https://')) return false;\n if (route === '-') return false;\n return route.includes('/');\n}\n\nfunction isOpenapiRoute(route) {\n const normalized = normalizeRoute(route);\n return normalized.includes('/api-reference/') || normalized.endsWith('/api-reference');\n}\n\nfunction shouldKeepRouteEntry(routeValue, context) {\n const route = normalizeRoute(routeValue);\n if (!route) return false;\n if (!isLikelyRouteEntry(route)) return false;\n if (context.disableOpenapi && isOpenapiRoute(route)) return false;\n if (context.prefixes.length === 0) return true;\n return context.prefixes.some((prefix) => routeMatchesPrefix(route, prefix));\n}\n\nfunction deriveSectionPrefix(routeValue) {\n const route = normalizeRoute(routeValue);\n if (!route) return '';\n const segments = route.split('/').filter(Boolean);\n if (segments.length < 2) return route;\n if (/^v\\d+$/.test(segments[0])) {\n if (segments.length >= 3 && isLanguageSegment(segments[1])) {\n return segments.slice(0, 3).join('/');\n }\n return segments.slice(0, 2).join('/');\n }\n return segments.slice(0, 2).join('/');\n}\n\nfunction collectRoutesFromPages(pages, outSet) {\n if (!Array.isArray(pages)) return;\n for (const entry of pages) {\n if (typeof entry === 'string') {\n const route = normalizeRoute(entry);\n if (isLikelyRouteEntry(route)) outSet.add(route);\n continue;\n }\n if (entry && typeof entry === 'object') {\n visitNavigationNode(entry, outSet);\n }\n }\n}\n\nfunction visitNavigationNode(node, outSet) {\n if (!node || typeof node !== 'object') return;\n if (Array.isArray(node.pages)) collectRoutesFromPages(node.pages, outSet);\n if (Array.isArray(node.groups)) node.groups.forEach((group) => visitNavigationNode(group, outSet));\n if (Array.isArray(node.anchors)) node.anchors.forEach((anchor) => visitNavigationNode(anchor, outSet));\n if (Array.isArray(node.tabs)) node.tabs.forEach((tab) => visitNavigationNode(tab, outSet));\n if (Array.isArray(node.languages)) node.languages.forEach((language) => visitNavigationNode(language, outSet));\n if (Array.isArray(node.versions)) node.versions.forEach((version) => visitNavigationNode(version, outSet));\n}\n\nfunction collectRoutesFromNavigation(navigation) {\n const routes = new Set();\n visitNavigationNode(navigation, routes);\n return [...routes].sort();\n}\n\nfunction parseArgs(argv) {\n const out = {\n repoRoot: process.cwd(),\n workspaceBase: path.join(os.tmpdir(), 'lpd-mint-dev'),\n scopeFile: '',\n versions: [],\n languages: [],\n tabs: [],\n prefixes: [],\n interactive: false,\n disableOpenapi: false,\n printOnly: false,\n help: false\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--help' || token === '-h') {\n out.help = true;\n continue;\n }\n if (token === '--repo-root') {\n out.repoRoot = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--repo-root=')) {\n out.repoRoot = token.slice('--repo-root='.length).trim();\n continue;\n }\n if (token === '--workspace-base') {\n out.workspaceBase = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--workspace-base=')) {\n out.workspaceBase = token.slice('--workspace-base='.length).trim();\n continue;\n }\n if (token === '--scope-file') {\n out.scopeFile = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--scope-file=')) {\n out.scopeFile = token.slice('--scope-file='.length).trim();\n continue;\n }\n if (token === '--versions' || token === '--scope-version') {\n out.versions.push(...splitCsv(argv[i + 1] || ''));\n i += 1;\n continue;\n }\n if (token.startsWith('--versions=') || token.startsWith('--scope-version=')) {\n out.versions.push(...splitCsv(token.split('=').slice(1).join('=')));\n continue;\n }\n if (token === '--languages' || token === '--scope-language') {\n out.languages.push(...splitCsv(argv[i + 1] || ''));\n i += 1;\n continue;\n }\n if (token.startsWith('--languages=') || token.startsWith('--scope-language=')) {\n out.languages.push(...splitCsv(token.split('=').slice(1).join('=')));\n continue;\n }\n if (token === '--tabs' || token === '--scope-tab') {\n out.tabs.push(...splitCsv(argv[i + 1] || ''));\n i += 1;\n continue;\n }\n if (token.startsWith('--tabs=') || token.startsWith('--scope-tab=')) {\n out.tabs.push(...splitCsv(token.split('=').slice(1).join('=')));\n continue;\n }\n if (token === '--prefixes' || token === '--scope-prefix') {\n out.prefixes.push(...splitCsv(argv[i + 1] || ''));\n i += 1;",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/lpd-scoped-mint-dev.test.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/lpd-scoped-mint-dev.test.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ }
+ ],
+ "count": 60
+ },
+ "Manual": {
+ "label": "Manual - CLI only",
+ "scripts": [
+ {
+ "path": "tools/scripts/dev/batch-update-og-image.sh",
+ "script": "batch-update-og-image",
+ "category": "remediator",
+ "purpose": "feature:seo",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-R19, F-R7",
+ "purpose_statement": "Batch OG image updater — updates og:image meta tags across multiple pages",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "bash tools/scripts/dev/batch-update-og-image.sh [flags]",
+ "header": "#!/bin/bash\n# @script batch-update-og-image\n# @category remediator\n# @purpose feature:seo\n# @scope tools/scripts\n# @owner docs\n# @needs E-R19, F-R7\n# @purpose-statement Batch OG image updater — updates og:image meta tags across multiple pages\n# @pipeline manual — interactive developer tool, not suited for automated pipelines\n# @usage bash tools/scripts/dev/batch-update-og-image.sh [flags]\nOLD_IMAGE='og:image: \"/snippets/assets/domain/SHARED/LivepeerDocsLogo.svg\"'\nNEW_IMAGE='og:image: \"/snippets/assets/domain/SHARED/LivepeerDocsHero.svg\"'\n\necho \"Finding files with old og:image...\"\nroots=(v2/pages v2/home v2/solutions v2/about v2/community v2/developers v2/gateways v2/orchestrators v2/lpt v2/resources v2/internal v2/deprecated v2/experimental v2/notes)\nexisting_roots=()\nfor root in \"${roots[@]}\"; do\n if [ -d \"$root\" ]; then\n existing_roots+=(\"$root\")\n fi\ndone\n\nfiles=$(grep -rl \"$OLD_IMAGE\" \"${existing_roots[@]}\" --include=\"*.mdx\" 2>/dev/null | grep -v \"mission-control.mdx\" || true)\n\ncount=0\nfor file in $files; do\n echo \"Updating: $file\"\n # Use perl for in-place replacement\n perl -pi -e \"s|og:image: \\\"/snippets/assets/domain/SHARED/LivepeerDocsLogo.svg\\\"|og:image: \\\"/snippets/assets/domain/SHARED/LivepeerDocsHero.svg\\\"|g\" \"$file\"\n ((count++))\ndone\n\necho \"\"\necho \"========== SUMMARY ==========\"\necho \"Updated: $count files\"\necho \"=============================\"\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/dev/replace-og-image.py",
+ "script": "replace-og-image",
+ "category": "remediator",
+ "purpose": "feature:seo",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-R19, F-R7",
+ "purpose_statement": "OG image replacer — replaces og:image path in a single page frontmatter",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "python3 tools/scripts/dev/replace-og-image.py [flags]",
+ "header": "#!/usr/bin/env python3\n# @script replace-og-image\n# @category remediator\n# @purpose feature:seo\n# @scope tools/scripts\n# @owner docs\n# @needs E-R19, F-R7\n# @purpose-statement OG image replacer — replaces og:image path in a single page frontmatter\n# @pipeline manual — interactive developer tool, not suited for automated pipelines\n# @usage python3 tools/scripts/dev/replace-og-image.py [flags]\nimport os\nimport sys\n\nOLD_IMAGE = 'og:image: \"/snippets/assets/domain/SHARED/LivepeerDocsLogo.svg\"'\nNEW_IMAGE = 'og:image: \"/snippets/assets/domain/SHARED/LivepeerDocsHero.svg\"'\nEXCLUDE_FILES = ['mission-control.mdx']\n\nchanged = 0\nskipped = 0\n\nroots = [\n 'v2/pages',\n 'v2/home',\n 'v2/solutions',\n 'v2/about',\n 'v2/community',\n 'v2/developers',\n 'v2/gateways',\n 'v2/orchestrators',\n 'v2/lpt',\n 'v2/resources',\n 'v2/internal',\n 'v2/deprecated',\n 'v2/experimental',\n 'v2/notes',\n]\n\nfor doc_root in roots:\n if not os.path.isdir(doc_root):\n continue\n for root, dirs, files in os.walk(doc_root):\n for file in files:\n if file.endswith('.mdx'):\n filepath = os.path.join(root, file)\n\n # Skip excluded files\n if file in EXCLUDE_FILES:\n print(f\"⊘ {filepath} - Excluded\")\n skipped += 1\n continue\n\n try:\n with open(filepath, 'r', encoding='utf-8') as f:\n content = f.read()\n\n if OLD_IMAGE in content:\n new_content = content.replace(OLD_IMAGE, NEW_IMAGE)\n\n with open(filepath, 'w', encoding='utf-8') as f:\n f.write(new_content)\n\n print(f\"✓ {filepath}\")\n changed += 1\n else:\n skipped += 1\n\n except Exception as e:\n print(f\"✗ {filepath}: {e}\")\n\nprint(f\"\\n========== SUMMARY ==========\")\nprint(f\"Changed: {changed}\")\nprint(f\"Skipped: {skipped}\")\nprint(f\"=============================\")\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/dev/test-seo-generator.js",
+ "script": "test-seo-generator",
+ "category": "generator",
+ "purpose": "feature:seo",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-R19, F-R7",
+ "purpose_statement": "Test for seo-generator — validates SEO generation logic",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/dev/test-seo-generator.js [flags]",
+ "header": "/**\n * @script test-seo-generator\n * @category generator\n * @purpose feature:seo\n * @scope tools/scripts\n * @owner docs\n * @needs E-R19, F-R7\n * @purpose-statement Test for seo-generator — validates SEO generation logic\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/dev/test-seo-generator.js [flags]\n */\nconst fs = require('fs')\nconst path = require('path')\nconst {\n extractFrontmatter,\n generateKeywords,\n getOgImagePath,\n parseFrontmatterFields,\n rebuildFrontmatter,\n processFile,\n} = require('./seo-generator-safe.js')\n\nconsole.log('Testing SEO Generator Script\\n')\n\n// Test files\nconst testFiles = [\n fs.existsSync('v2/home/mission-control.mdx')\n ? 'v2/home/mission-control.mdx'\n : 'v2/home/mission-control.mdx',\n fs.existsSync('v2/about/portal.mdx')\n ? 'v2/about/portal.mdx'\n : 'v2/about/portal.mdx',\n fs.existsSync('v2/gateways/gateways-portal.mdx')\n ? 'v2/gateways/gateways-portal.mdx'\n : 'v2/gateways/gateways-portal.mdx',\n]\n\nlet allTestsPassed = true\n\ntestFiles.forEach((testFile) => {\n console.log(`\\n========== Testing: ${testFile} ==========`)\n\n const fullPath = path.join(process.cwd(), testFile)\n const originalContent = fs.readFileSync(fullPath, 'utf8')\n const originalExtracted = extractFrontmatter(originalContent)\n\n if (!originalExtracted) {\n console.error('❌ Failed to extract frontmatter')\n allTestsPassed = false\n return\n }\n\n // Parse original frontmatter\n const originalFields = parseFrontmatterFields(originalExtracted.frontmatter)\n\n // Process file (dry run)\n const result = processFile(testFile)\n\n if (!result.success) {\n console.error(`❌ Processing failed: ${result.error}`)\n allTestsPassed = false\n return\n }\n\n // Simulate what the new content would be\n const newKeywords = result.newKeywords\n const newOgImage = result.ogImage\n const newFrontmatter = rebuildFrontmatter(\n originalFields,\n newKeywords,\n newOgImage\n )\n const newContent = `---\\n${newFrontmatter}\\n---\\n${originalExtracted.body}`\n\n // Parse new frontmatter\n const newExtracted = extractFrontmatter(newContent)\n const newFields = parseFrontmatterFields(newExtracted.frontmatter)\n\n // Verify all fields except keywords and og:image are preserved\n let fieldsPreserved = true\n for (const key in originalFields) {\n if (key === 'keywords' || key === 'og:image' || key === 'og') {\n continue // These are expected to change\n }\n\n if (originalFields[key] !== newFields[key]) {\n console.error(`❌ Field \"${key}\" was modified!`)\n console.error(` Original: ${originalFields[key]}`)\n console.error(` New: ${newFields[key]}`)\n fieldsPreserved = false\n allTestsPassed = false\n }\n }\n\n // Verify body content is unchanged\n if (originalExtracted.body !== newExtracted.body) {\n console.error('❌ Body content was modified!')\n console.error(` Original length: ${originalExtracted.body.length}`)\n console.error(` New length: ${newExtracted.body.length}`)\n allTestsPassed = false\n } else {\n console.log('✓ Body content preserved')\n }\n\n if (fieldsPreserved) {\n console.log(\n '✓ All frontmatter fields preserved (except keywords and og:image)'\n )\n }\n\n // Show what changed\n console.log('\\nChanges:')\n console.log(\n ` Keywords: ${JSON.stringify(result.oldKeywords)} → ${JSON.stringify(result.newKeywords)}`\n )\n console.log(` og:image: ${newOgImage}`)\n\n // Show preserved fields\n console.log('\\nPreserved fields:')\n for (const key in originalFields) {\n if (key !== 'keywords' && key !== 'og:image' && key !== 'og') {\n const value =\n originalFields[key].length > 50\n ? originalFields[key].substring(0, 50) + '...'\n : originalFields[key]\n console.log(` ${key}: ${value}`)\n }\n }\n})\n\nconsole.log('\\n\\n========== TEST SUMMARY ==========')\nif (allTestsPassed) {\n console.log('✅ ALL TESTS PASSED')\n console.log('The script correctly:')\n console.log(\n ' - Preserves all frontmatter fields except keywords and og:image'\n )\n console.log(' - Preserves all body content')\n console.log(' - Only modifies keywords and og:image fields')\n} else {\n console.log('❌ SOME TESTS FAILED')\n console.log('DO NOT USE THIS SCRIPT until all tests pass!')\n}\nconsole.log('==================================')\n\nprocess.exit(allTestsPassed ? 0 : 1)\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/dev/update-all-og-images.js",
+ "script": "update-all-og-images",
+ "category": "remediator",
+ "purpose": "feature:seo",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-R19, F-R7",
+ "purpose_statement": "Bulk OG image updater — updates og:image across all v2 pages",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/dev/update-all-og-images.js [flags]",
+ "header": "/**\n * @script update-all-og-images\n * @category remediator\n * @purpose feature:seo\n * @scope tools/scripts\n * @owner docs\n * @needs E-R19, F-R7\n * @purpose-statement Bulk OG image updater — updates og:image across all v2 pages\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/dev/update-all-og-images.js [flags]\n */\nconst fs = require('fs');\nconst path = require('path');\n\nconst OLD_IMAGE = 'og:image: \"/snippets/assets/domain/SHARED/LivepeerDocsLogo.svg\"';\nconst NEW_IMAGE = 'og:image: \"/snippets/assets/domain/SHARED/LivepeerDocsHero.svg\"';\nconst EXCLUDE_FILES = ['mission-control.mdx'];\nconst V2_DOC_ROOTS = [\n 'v2/pages',\n 'v2/home',\n 'v2/solutions',\n 'v2/about',\n 'v2/community',\n 'v2/developers',\n 'v2/gateways',\n 'v2/orchestrators',\n 'v2/lpt',\n 'v2/resources',\n 'v2/internal',\n 'v2/deprecated',\n 'v2/experimental',\n 'v2/notes'\n].filter((root) => fs.existsSync(root));\n\nfunction getAllMdxFiles(dir, fileList = []) {\n const files = fs.readdirSync(dir);\n \n files.forEach(file => {\n const filePath = path.join(dir, file);\n const stat = fs.statSync(filePath);\n \n if (stat.isDirectory()) {\n getAllMdxFiles(filePath, fileList);\n } else if (file.endsWith('.mdx')) {\n fileList.push(filePath);\n }\n });\n \n return fileList;\n}\n\nconst allFiles = V2_DOC_ROOTS.flatMap((root) => getAllMdxFiles(root));\nlet changed = 0;\nlet skipped = 0;\nlet errors = 0;\n\nconsole.log(`Found ${allFiles.length} MDX files\\n`);\n\nallFiles.forEach(filePath => {\n try {\n const fileName = path.basename(filePath);\n \n // Skip excluded files\n if (EXCLUDE_FILES.includes(fileName)) {\n console.log(`⊘ ${filePath} - Excluded`);\n skipped++;\n return;\n }\n \n const content = fs.readFileSync(filePath, 'utf8');\n \n if (content.includes(OLD_IMAGE)) {\n const newContent = content.replace(OLD_IMAGE, NEW_IMAGE);\n fs.writeFileSync(filePath, newContent, 'utf8');\n console.log(`✓ ${filePath}`);\n changed++;\n } else {\n skipped++;\n }\n } catch (error) {\n console.error(`✗ ${filePath}: ${error.message}`);\n errors++;\n }\n});\n\nconsole.log(`\\n========== SUMMARY ==========`);\nconsole.log(`Changed: ${changed}`);\nconsole.log(`Skipped: ${skipped}`);\nconsole.log(`Errors: ${errors}`);\nconsole.log(`=============================`);\n\n// Write summary to file\nfs.writeFileSync('og-image-update-summary.txt', `Changed: ${changed}\\nSkipped: ${skipped}\\nErrors: ${errors}\\n`);\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/dev/og-image-update-summary.txt",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/dev/og-image-update-summary.txt",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/i18n/test-mintlify-version-language-toggle.js",
+ "script": "test-mintlify-version-language-toggle",
+ "category": "enforcer",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts/i18n, docs.json, v2",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Mintlify language toggle checker — validates Mintlify version supports language toggle feature",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/i18n/test-mintlify-version-language-toggle.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script test-mintlify-version-language-toggle\n * @category enforcer\n * @purpose feature:translation\n * @scope tools/scripts/i18n, docs.json, v2\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Mintlify language toggle checker — validates Mintlify version supports language toggle feature\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/i18n/test-mintlify-version-language-toggle.js [flags]\n */\n\nconst puppeteer = require('puppeteer');\n\nfunction parseArgs(argv) {\n const args = {\n baseUrl: 'http://localhost:3000',\n timeoutMs: 120000,\n strictMenus: false\n };\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n const next = argv[i + 1];\n if (token === '--base-url') {\n args.baseUrl = String(next || args.baseUrl).trim();\n i += 1;\n } else if (token === '--timeout-ms') {\n const parsed = Number(next);\n if (Number.isFinite(parsed) && parsed > 0) args.timeoutMs = parsed;\n i += 1;\n } else if (token === '--strict-menus') {\n args.strictMenus = true;\n } else if (token === '--help' || token === '-h') {\n console.log(\n [\n 'Usage: node tools/scripts/i18n/test-mintlify-version-language-toggle.js [options]',\n '',\n 'Options:',\n ' --base-url Mint preview base URL (default http://localhost:3000)',\n ' --timeout-ms Navigation timeout (default 120000)',\n ' --strict-menus Fail if menu contents cannot be confirmed in headless mode',\n ' --help, -h Show help'\n ].join('\\n')\n );\n process.exit(0);\n }\n }\n return args;\n}\n\nfunction buildTextMatchers(values) {\n return values.map((value) => String(value || '').trim()).filter(Boolean);\n}\n\nfunction textListContains(texts, expected) {\n const needle = String(expected || '').trim();\n if (!needle) return false;\n return texts.some((text) => {\n const value = String(text || '').trim();\n if (!value) return false;\n return value === needle || value.includes(needle) || value.startsWith(`${needle}\\n`) || value.endsWith(`\\n${needle}`);\n });\n}\n\nasync function collectTexts(page, { visibleOnly = false } = {}) {\n return page.evaluate((opts) => {\n const els = Array.from(document.querySelectorAll('button,a,[role=\"menuitem\"]'));\n const isVisible = (el) => {\n const rect = el.getBoundingClientRect();\n const styles = window.getComputedStyle(el);\n return rect.width > 0 && rect.height > 0 && styles.display !== 'none' && styles.visibility !== 'hidden';\n };\n return els\n .filter((el) => (opts.visibleOnly ? isVisible(el) : true))\n .map((el) => (el.innerText || el.textContent || '').trim())\n .filter(Boolean);\n }, { visibleOnly });\n}\n\nasync function findVersionTriggerText(page) {\n return page.evaluate(() => {\n const btns = Array.from(document.querySelectorAll('button'));\n const isVisible = (el) => {\n const rect = el.getBoundingClientRect();\n const styles = window.getComputedStyle(el);\n return rect.width > 0 && rect.height > 0 && styles.display !== 'none' && styles.visibility !== 'hidden';\n };\n const trigger =\n btns.find((b) => isVisible(b) && /^(v1|v2)$/i.test((b.innerText || b.textContent || '').trim())) ||\n btns.find((b) => /^(v1|v2)$/i.test((b.innerText || b.textContent || '').trim()));\n return trigger ? (trigger.innerText || trigger.textContent || '').trim() : '';\n });\n}\n\nasync function clickTrigger(page, patternSource) {\n return page.evaluate((source) => {\n const pattern = new RegExp(source, 'i');\n const btns = Array.from(document.querySelectorAll('button'));\n const isVisible = (el) => {\n const rect = el.getBoundingClientRect();\n const styles = window.getComputedStyle(el);\n return rect.width > 0 && rect.height > 0 && styles.display !== 'none' && styles.visibility !== 'hidden';\n };\n const btn =\n btns.find((b) => isVisible(b) && pattern.test((b.innerText || b.textContent || '').trim())) ||\n btns.find((b) => pattern.test((b.innerText || b.textContent || '').trim()));\n if (!btn) return { ok: false, text: '' };\n btn.click();\n return { ok: true, text: (btn.innerText || btn.textContent || '').trim() };\n }, patternSource);\n}\n\nasync function clickVersionTrigger(page) {\n return clickTrigger(page, '^(v1|v2)$');\n}\n\nasync function clickLanguageTrigger(page) {\n return clickTrigger(page, '(English|Espa\\u00f1ol|Fran\\u00e7ais|\\u4e2d\\u6587|Chinese|Spanish|French)');\n}\n\nfunction assertCondition(condition, message, failures) {\n if (!condition) failures.push(message);\n}\n\nfunction normalizeUrl(url) {\n return String(url || '').replace(/\\/+$/, '');\n}\n\nfunction expectedTargetVersion(currentVersion) {\n return currentVersion === 'v2' ? 'v1' : 'v2';\n}\n\nasync function maybeAssertMenuItems({ scenario, page, failures, warnings, strictMenus }) {\n const versionClick = await clickVersionTrigger(page);\n if (!versionClick.ok) {\n if (strictMenus) failures.push(`${scenario.name}: version trigger not found for menu check`);\n else warnings.push(`${scenario.name}: version trigger menu could not be opened in headless preview`);\n return;\n }\n await new Promise((r) => setTimeout(r, 500));\n const menuTexts = await collectTexts(page);\n const expectedItems = buildTextMatchers(scenario.requiredVersionMenuItems || []);\n for (const item of expectedItems) {\n if (!textListContains(menuTexts, item)) {\n if (strictMenus) failures.push(`${scenario.name}: version menu missing \"${item}\"`);\n else warnings.push(`${scenario.name}: could not confirm version menu item \"${item}\" in headless preview`);\n }\n }\n}\n\nasync function maybeAssertLanguageItems({ scenario, page, failures, warnings, strictMenus }) {\n const languageClick = await clickLanguageTrigger(page);\n if (!languageClick.ok) {\n if (strictMenus) failures.push(`${scenario.name}: language trigger not found`);\n else warnings.push(`${scenario.name}: language trigger could not be opened in headless preview`);\n return;\n }\n await new Promise((r) => setTimeout(r, 500));\n const menuTexts = await collectTexts(page);\n const expectedItems = buildTextMatchers(scenario.requiredLanguageMenuItems || []);\n for (const item of expectedItems) {\n if (!textListContains(menuTexts, item)) {\n if (strictMenus) failures.push(`${scenario.name}: language menu missing \"${item}\"`);\n else warnings.push(`${scenario.name}: could not confirm language menu item \"${item}\" in headless preview`);\n }\n }\n}\n\nasync function tryVersionSwitch(page, scenario, failures, warnings, timeoutMs, strictMenus) {\n const currentVersion = scenario.expectedVersionTrigger;\n const targetVersion = expectedTargetVersion(currentVersion);\n const versionClick = await clickVersionTrigger(page);\n if (!versionClick.ok) {\n if (strictMenus) failures.push(`${scenario.name}: version trigger not found for switch attempt`);\n else warnings.push(`${scenario.name}: version switch not attempted (trigger not found in headless preview)`);\n return null;\n }\n await new Promise((r) => setTimeout(r, 500));\n const targetClicked = await page.evaluate((target) => {\n const nodes = Array.from(document.querySelectorAll('button,a,[role=\"menuitem\"]'));\n const isVisible = (el) => {\n const rect = el.getBoundingClientRect();\n const styles = window.getComputedStyle(el);\n return rect.width > 0 && rect.height > 0 && styles.display !== 'none' && styles.visibility !== 'hidden';\n };\n const pick =\n nodes.find((node) => isVisible(node) && new RegExp(`^${target}$`, 'i').test((node.innerText || node.textContent || '').trim())) ||\n nodes.find((node) => new RegExp(`^${target}$`, 'i').test((node.innerText || node.textContent || '').trim()));\n if (!pick) return false;\n pick.click();\n return true;\n }, targetVersion);\n\n if (!targetClicked) {\n if (strictMenus) failures.push(`${scenario.name}: could not click version menu item ${targetVersion}`);\n else warnings.push(`${scenario.name}: could not click version menu item ${targetVersion} in headless preview`);\n return null;\n }\n\n try {\n await page.waitForNavigation({ waitUntil: 'networkidle2', timeout: timeoutMs });\n } catch {\n await new Promise((r) => setTimeout(r, 1200));\n }\n\n const switchedUrl = page.url();\n if (!String(switchedUrl).includes(`/${targetVersion}/`)) {\n failures.push(`${scenario.name}: version switch did not navigate to /${targetVersion}/ (got ${switchedUrl})`);\n }\n return switchedUrl;\n}\n\nasync function runScenario(page, scenario, failures, warnings, timeoutMs, strictMenus) {\n const pageErrors = [];\n page.removeAllListeners('pageerror');\n page.on('pageerror', (err) => pageErrors.push(err.message));\n\n await page.goto(scenario.url, { waitUntil: 'networkidle2', timeout: timeoutMs });\n await new Promise((r) => setTimeout(r, 800));",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/i18n/test/cli-guardrails.test.js",
+ "script": "cli-guardrails.test",
+ "category": "validator",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Tests i18n CLI guardrails — validates argument validation and safety checks",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/i18n/test/cli-guardrails.test.js [flags]",
+ "header": "/**\n * @script cli-guardrails.test\n * @category validator\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Tests i18n CLI guardrails — validates argument validation and safety checks\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @dualmode dual-mode (document flags)\n * @usage node tools/scripts/i18n/test/cli-guardrails.test.js [flags]\n */\nconst test = require('node:test');\nconst assert = require('node:assert/strict');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\n\nconst { run: runTranslateDocs } = require('../translate-docs');\nconst { run: runGenerateDocsJson } = require('../generate-localized-docs-json');\n\ntest('translate-docs blocks mock provider non-dry runs without --allow-mock-write', async () => {\n await assert.rejects(\n () => runTranslateDocs(['--provider', 'mock']),\n /mock provider.*write mode|Refusing to run translate-docs with mock provider/i\n );\n});\n\ntest('translate-docs allows mock provider non-dry runs with --allow-mock-write (guard path)', async () => {\n const tmpFile = path.join(os.tmpdir(), `i18n-empty-paths-${Date.now()}.txt`);\n fs.writeFileSync(tmpFile, 'v2/does-not-exist\\n', 'utf8');\n\n const report = await runTranslateDocs([\n '--provider',\n 'mock',\n '--allow-mock-write',\n '--languages',\n 'es',\n '--scope-mode',\n 'paths_file',\n '--paths-file',\n tmpFile,\n '--max-pages',\n '1'\n ]);\n\n assert.equal(report.counts.failed, 0);\n assert.equal(report.counts.successfulPageLanguagePairs, 0);\n});\n\ntest('generate-localized-docs-json blocks mock provider write runs without --allow-mock-write', async () => {\n await assert.rejects(\n () => runGenerateDocsJson(['--provider', 'mock', '--write', '--languages', 'es']),\n /Refusing to write docs\\.json with mock provider/i\n );\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/i18n/test/i18n-empty-paths-${Date.now()}.txt",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/i18n/test/i18n-empty-paths-${Date.now()}.txt",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/i18n/test/docs-json-localizer.test.js",
+ "script": "docs-json-localizer.test",
+ "category": "validator",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Tests docs-json-localizer — validates locale docs.json transformation logic",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/i18n/test/docs-json-localizer.test.js [flags]",
+ "header": "/**\n * @script docs-json-localizer.test\n * @category validator\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Tests docs-json-localizer — validates locale docs.json transformation logic\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/i18n/test/docs-json-localizer.test.js [flags]\n */\nconst test = require('node:test');\nconst assert = require('node:assert/strict');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { generateLocalizedDocsJson } = require('../lib/docs-json-localizer');\n\nfunction fixtureDocsJson() {\n return {\n navigation: {\n versions: [\n {\n version: 'v1',\n languages: [\n {\n language: 'en',\n dropdowns: [\n {\n dropdown: 'Developers',\n anchors: [{ anchor: 'Docs', groups: [{ group: 'Start', pages: ['v1/developers/introduction'] }] }]\n }\n ]\n }\n ]\n },\n {\n version: 'v2',\n languages: [\n {\n language: 'en',\n tabs: [\n {\n tab: 'About',\n anchors: [\n {\n anchor: 'Livepeer Network',\n groups: [\n {\n group: 'Pages',\n pages: [\n 'v2/about/livepeer-network/actors',\n 'v2/about/livepeer-network/job-lifecycle',\n ' '\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n };\n}\n\ntest('generateLocalizedDocsJson clones v2 english node, translates labels, and rewrites localized routes', async () => {\n const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'i18n-docs-json-'));\n fs.mkdirSync(path.join(tmp, 'v2', 'es', 'about', 'livepeer-network'), { recursive: true });\n fs.writeFileSync(path.join(tmp, 'v2', 'es', 'about', 'livepeer-network', 'actors.mdx'), '# actors');\n fs.writeFileSync(path.join(tmp, 'v2', 'es', 'about', 'livepeer-network', 'job-lifecycle.mdx'), '# job');\n\n const translator = {\n name: 'openrouter',\n async translateStrings({ language, strings }) {\n return { strings: strings.map((s) => `${language.toUpperCase()}: ${s}`), modelUsed: 'stub-openrouter' };\n }\n };\n\n const routeMapEntries = [\n {\n sourceRoute: 'v2/about/livepeer-network/actors',\n localizedRoute: 'v2/es/about/livepeer-network/actors',\n localizedFile: 'v2/es/about/livepeer-network/actors.mdx',\n language: 'es',\n status: 'translated',\n provider: 'openrouter',\n artifactClass: 'translated_real'\n },\n {\n sourceRoute: 'v2/about/livepeer-network/job-lifecycle',\n localizedRoute: 'v2/es/about/livepeer-network/job-lifecycle',\n localizedFile: 'v2/es/about/livepeer-network/job-lifecycle.mdx',\n language: 'es',\n status: 'translated',\n provider: 'openrouter',\n artifactClass: 'translated_real'\n }\n ];\n\n const result = await generateLocalizedDocsJson({\n docsJson: fixtureDocsJson(),\n repoRoot: tmp,\n targetLanguages: ['es'],\n translator,\n translationRules: {\n translateDocsJsonLabelKeys: ['tab', 'anchor', 'group'],\n preserveBrandTerms: [],\n preserveRegexes: []\n },\n routeMapEntries,\n routeMapMetadata: {\n runtime: { scopeMode: 'full_v2_nav' },\n scope: {\n scopeMode: 'full_v2_nav',\n selectedRoutes: [\n 'v2/about/livepeer-network/actors',\n 'v2/about/livepeer-network/job-lifecycle'\n ]\n }\n },\n qualityGates: {\n requireLocalizedFileForRouteRewrite: true\n },\n reporting: {\n warnRouteRewriteCoverageBelowPct: 80,\n warnOnMixedLanguageLocaleUi: true\n },\n writeMode: false,\n allowMockWrite: false\n });\n\n const v2 = result.docsJson.navigation.versions.find((v) => v.version === 'v2');\n assert.deepEqual(\n v2.languages.map((l) => l.language),\n ['en', 'es']\n );\n\n const es = v2.languages.find((l) => l.language === 'es');\n assert.match(es.tabs[0].tab, /^ES:/);\n assert.doesNotMatch(es.tabs[0].tab, /^\\[es\\]/);\n assert.equal(es.tabs[0].anchors[0].groups[0].pages[0], 'v2/es/about/livepeer-network/actors');\n assert.equal(es.tabs[0].anchors[0].groups[0].pages[1], 'v2/es/about/livepeer-network/job-lifecycle');\n assert.equal(es.tabs[0].anchors[0].groups[0].pages[2], ' ');\n assert.equal(result.report.perLanguage[0].rewrittenRoutes, 2);\n assert.equal(result.report.perLanguage[0].fallbackRoutes, 0);\n assert.equal(result.report.perLanguage[0].routeCoverageOverallPct, 100);\n assert.equal(result.report.perLanguage[0].routeCoverageScope, 'full_v2_nav');\n assert.deepEqual(result.report.perLanguage[0].warnings, []);\n const v1 = result.docsJson.navigation.versions.find((v) => v.version === 'v1');\n assert.deepEqual(\n v1.languages.map((l) => l.language),\n ['en']\n );\n assert.equal(v1.languages[0].dropdowns[0].dropdown, 'Developers');\n});\n\ntest('generateLocalizedDocsJson rejects mock provenance routes for rewrite and reports fallback diagnostics', async () => {\n const translator = {\n name: 'openrouter',\n async translateStrings({ language, strings }) {\n return { strings: strings.map((s) => `${language}:${s}`), modelUsed: 'stub' };\n }\n };\n\n const routeMapEntries = [\n {\n sourceRoute: 'v2/about/livepeer-network/actors',\n localizedRoute: 'v2/es/about/livepeer-network/actors',\n localizedFile: 'v2/es/about/livepeer-network/actors.mdx',\n language: 'es',\n status: 'existing',\n provider: 'mock',\n artifactClass: 'translated_mock'\n }\n ];\n\n const result = await generateLocalizedDocsJson({\n docsJson: fixtureDocsJson(),\n repoRoot: process.cwd(),\n targetLanguages: ['es'],\n translator,\n translationRules: {\n translateDocsJsonLabelKeys: ['tab', 'anchor', 'group'],\n preserveBrandTerms: [],\n preserveRegexes: []\n },\n routeMapEntries,\n routeMapMetadata: {\n runtime: { scopeMode: 'prefixes' },\n scope: {\n scopeMode: 'prefixes',\n selectedRoutes: [\n 'v2/about/livepeer-network/actors',\n 'v2/about/livepeer-network/job-lifecycle'\n ]\n }\n },\n qualityGates: {\n requireLocalizedFileForRouteRewrite: false,\n rejectMockArtifactsForRouteRewrite: true\n },\n reporting: {\n warnRouteRewriteCoverageBelowPct: 80,\n warnOnMixedLanguageLocaleUi: true\n },\n writeMode: false,\n allowMockWrite: false\n });\n\n const v2 = result.docsJson.navigation.versions.find((v) => v.version === 'v2');\n const es = v2.languages.find((l) => l.language === 'es');\n assert.equal(es.tabs[0].anchors[0].groups[0].pages[0], 'v2/about/livepeer-network/actors');\n\n const report = result.report.perLanguage[0];\n assert.equal(report.rewrittenRoutes, 0);\n assert.equal(report.fallbackRoutes, 2);",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/i18n/test/frontmatter.test.js",
+ "script": "frontmatter.test",
+ "category": "validator",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Tests frontmatter parser — validates frontmatter read/write roundtrip",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/i18n/test/frontmatter.test.js [flags]",
+ "header": "/**\n * @script frontmatter.test\n * @category validator\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Tests frontmatter parser — validates frontmatter read/write roundtrip\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/i18n/test/frontmatter.test.js [flags]\n */\nconst test = require('node:test');\nconst assert = require('node:assert/strict');\nconst { parseMdxFileWithFrontmatter, translateFrontmatterFields } = require('../lib/frontmatter');\n\ntest('translateFrontmatterFields only translates allowlisted string keys', async () => {\n const translator = {\n name: 'mock',\n async translateStrings({ language, strings }) {\n return { strings: strings.map((s) => `[${language}] ${s}`), modelUsed: 'mock' };\n }\n };\n\n const raw = [\n '---',\n 'title: Actors Overview',\n 'sidebarTitle: Actors',\n 'description: Who participates',\n 'keywords:',\n ' - livepeer',\n 'og:image: /snippets/assets/logo.svg',\n '---',\n '',\n 'Body'\n ].join('\\n');\n\n const parsed = parseMdxFileWithFrontmatter(raw);\n const result = await translateFrontmatterFields({\n data: parsed.data,\n language: 'fr',\n translator,\n rules: {\n translateFrontmatterKeys: ['title', 'sidebarTitle', 'description'],\n preserveBrandTerms: [],\n preserveRegexes: ['^/snippets/']\n },\n translateKeywords: false\n });\n\n assert.match(result.data.title, /^\\[fr\\]/);\n assert.match(result.data.sidebarTitle, /^\\[fr\\]/);\n assert.match(result.data.description, /^\\[fr\\]/);\n assert.deepEqual(result.data.keywords, ['livepeer']);\n assert.equal(result.data['og:image'], '/snippets/assets/logo.svg');\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/i18n/test/mdx-translate.test.js",
+ "script": "mdx-translate.test",
+ "category": "validator",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Tests MDX translation — validates content block translation logic",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/i18n/test/mdx-translate.test.js [flags]",
+ "header": "/**\n * @script mdx-translate.test\n * @category validator\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Tests MDX translation — validates content block translation logic\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/i18n/test/mdx-translate.test.js [flags]\n */\nconst test = require('node:test');\nconst assert = require('node:assert/strict');\nconst {\n extractTranslatableTextSegments,\n protectText,\n restoreProtectedText,\n translateMdxBody\n} = require('../lib/mdx-translate');\n\ntest('extractTranslatableTextSegments skips code and MDX JSX text', async () => {\n const mdx = [\n '# Heading',\n '',\n 'Paragraph with `inline` code and [link](./target).',\n '',\n '```js',\n 'const value = \"do not translate\";',\n '```',\n '',\n 'Do not translate JSX children in v1 '\n ].join('\\n');\n\n const segments = await extractTranslatableTextSegments(mdx);\n const combined = segments.map((s) => s.text).join(' | ');\n\n assert.match(combined, /Heading/);\n assert.match(combined, /Paragraph with /);\n assert.match(combined, /link/);\n assert.doesNotMatch(combined, /do not translate/);\n assert.doesNotMatch(combined, /JSX children/);\n});\n\ntest('protectText preserves URLs/routes/brand terms and restores placeholders', () => {\n const rules = {\n preserveBrandTerms: ['Livepeer'],\n preserveRegexes: []\n };\n const input = 'Use Livepeer at https://example.com and route v2/about/livepeer-network/actors';\n const protectedValue = protectText(input, rules);\n assert.equal(protectedValue.skip, false);\n assert.match(protectedValue.text, /__I18N_PH_/);\n const restored = restoreProtectedText(protectedValue.text, protectedValue.placeholders);\n assert.equal(restored, input);\n});\n\ntest('translateMdxBody rewrites internal markdown links when localized targets exist', async () => {\n const translator = {\n name: 'mock',\n async translateStrings({ language, strings }) {\n return { strings: strings.map((s) => `${language.toUpperCase()}: ${s}`), modelUsed: 'mock' };\n }\n };\n\n const routeMap = new Map([\n ['v2/about/livepeer-network/actors', new Map([['es', 'v2/es/about/livepeer-network/actors']])],\n ['v2/about/livepeer-network/job-lifecycle', new Map([['es', 'v2/es/about/livepeer-network/job-lifecycle']])]\n ]);\n\n const body = 'See [Job Lifecycle](./job-lifecycle) and [External](https://example.com).';\n const result = await translateMdxBody({\n body,\n language: 'es',\n translator,\n rules: { preserveBrandTerms: [], preserveRegexes: [] },\n routeContext: {\n sourceRoute: 'v2/about/livepeer-network/actors',\n language: 'es',\n sourceLocalizedRoute: 'v2/es/about/livepeer-network/actors',\n routeMapBySourceRoute: routeMap\n }\n });\n\n assert.match(result.body, /\\[[^\\]]*Job Lifecycle[^\\]]*\\]\\(\\.\\/job-lifecycle\\)/);\n assert.match(result.body, /\\[[^\\]]*External[^\\]]*\\]\\(https:\\/\\/example\\.com\\)/);\n assert.equal(result.linkRewrite.rewrittenCount, 1);\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/i18n/test/provenance.test.js",
+ "script": "provenance.test",
+ "category": "validator",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Tests provenance tracker — validates translation provenance recording",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/i18n/test/provenance.test.js [flags]",
+ "header": "/**\n * @script provenance.test\n * @category validator\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Tests provenance tracker — validates translation provenance recording\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/i18n/test/provenance.test.js [flags]\n */\nconst test = require('node:test');\nconst assert = require('node:assert/strict');\nconst { parseMdxFileWithFrontmatter } = require('../lib/frontmatter');\nconst {\n buildProvenanceComment,\n injectOrReplaceProvenanceComment,\n parseProvenanceComment\n} = require('../lib/provenance');\n\ntest('provenance comment is MDX-safe, replaceable, and parseable after frontmatter', () => {\n const raw = [\n '---',\n 'title: Example',\n 'description: Example',\n '---',\n '',\n '# Heading',\n '',\n 'Body'\n ].join('\\n');\n\n const first = buildProvenanceComment({\n sourcePath: 'v2/example.mdx',\n sourceRoute: 'v2/example',\n sourceHash: 'abc',\n language: 'es',\n provider: 'openrouter',\n model: 'model-a',\n generatedAt: '2026-02-24T00:00:00.000Z'\n });\n const second = buildProvenanceComment({\n sourcePath: 'v2/example.mdx',\n sourceRoute: 'v2/example',\n sourceHash: 'def',\n language: 'es',\n provider: 'openrouter',\n model: 'model-b',\n generatedAt: '2026-02-24T01:00:00.000Z'\n });\n\n const withFirst = injectOrReplaceProvenanceComment(raw, first);\n const withSecond = injectOrReplaceProvenanceComment(withFirst, second);\n\n const parsedDoc = parseMdxFileWithFrontmatter(withSecond);\n assert.match(parsedDoc.body, /\\{\\/\\* codex-i18n:/);\n const provenance = parseProvenanceComment(withSecond);\n assert.equal(provenance.sourceHash, 'def');\n assert.equal(provenance.model, 'model-b');\n assert.equal((withSecond.match(/codex-i18n:/g) || []).length, 1);\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/i18n/test/provider-openrouter.test.js",
+ "script": "provider-openrouter.test",
+ "category": "validator",
+ "purpose": "feature:translation",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R6, F-R7",
+ "purpose_statement": "Tests OpenRouter provider — validates API call logic and response parsing",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/i18n/test/provider-openrouter.test.js [flags]",
+ "header": "/**\n * @script provider-openrouter.test\n * @category validator\n * @purpose feature:translation\n * @scope tools/scripts\n * @owner docs\n * @needs F-R6, F-R7\n * @purpose-statement Tests OpenRouter provider — validates API call logic and response parsing\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/i18n/test/provider-openrouter.test.js [flags]\n */\nconst test = require('node:test');\nconst assert = require('node:assert/strict');\nconst {\n createOpenRouterTranslator,\n isModelFailoverError,\n parseJsonFromModelContent,\n validateSegmentResponse\n} = require('../lib/provider-openrouter');\nconst { createTranslator } = require('../lib/providers');\n\ntest('parseJsonFromModelContent parses plain JSON and fenced JSON', () => {\n const plain = parseJsonFromModelContent('{\"segments\":[{\"id\":0,\"text\":\"hola\"}]}');\n assert.equal(plain.segments[0].text, 'hola');\n\n const fenced = parseJsonFromModelContent('```json\\n{\"segments\":[{\"id\":\"1\",\"text\":\"bonjour\"}]}\\n```');\n assert.equal(fenced.segments[0].text, 'bonjour');\n});\n\ntest('validateSegmentResponse preserves ordering by request ids and rejects count mismatch', () => {\n const request = [\n { id: 'b', text: 'beta' },\n { id: 'a', text: 'alpha' }\n ];\n const response = {\n segments: [\n { id: 'a', text: 'A' },\n { id: 'b', text: 'B' }\n ]\n };\n const normalized = validateSegmentResponse(response, request);\n assert.deepEqual(normalized, [\n { id: 'b', text: 'B' },\n { id: 'a', text: 'A' }\n ]);\n\n assert.throws(\n () => validateSegmentResponse({ segments: [{ id: 'a', text: 'A' }] }, request),\n /Segment count mismatch/\n );\n});\n\ntest('isModelFailoverError treats provider incompatibility and capacity errors as failover eligible', () => {\n assert.equal(\n isModelFailoverError({ status: 400, message: 'Developer instruction is not enabled for this model' }),\n true\n );\n assert.equal(\n isModelFailoverError({ status: 400, message: 'totally unrelated validation error' }),\n false\n );\n assert.equal(isModelFailoverError({ status: 429, message: 'rate limited' }), true);\n assert.equal(isModelFailoverError({ status: 503, message: 'provider down' }), true);\n});\n\ntest('createTranslator defaults to openrouter and reports provider name', () => {\n const translator = createTranslator({\n config: {\n provider: { name: 'openrouter', modelCandidates: ['test/model:free'] },\n translationRules: {}\n }\n });\n assert.equal(translator.name, 'openrouter');\n});\n\ntest('createOpenRouterTranslator fails over across models and returns final modelUsed', async () => {\n const originalFetch = global.fetch;\n const originalApiKey = process.env.OPENROUTER_API_KEY;\n let callCount = 0;\n global.fetch = async (_url, options) => {\n callCount += 1;\n const body = JSON.parse(String(options.body || '{}'));\n if (body.model === 'bad/model:free') {\n return {\n ok: false,\n status: 400,\n text: async () => '{\"error\":{\"message\":\"Developer instruction is not enabled\"}}'\n };\n }\n return {\n ok: true,\n json: async () => ({\n choices: [\n {\n message: {\n content: JSON.stringify({\n segments: [{ id: 0, text: 'hola' }]\n })\n }\n }\n ]\n })\n };\n };\n\n process.env.OPENROUTER_API_KEY = process.env.OPENROUTER_API_KEY || 'test-key';\n\n try {\n const translator = createOpenRouterTranslator(\n {\n modelCandidates: ['bad/model:free', 'good/model:free'],\n baseUrl: 'https://example.invalid',\n maxRetriesPerRequest: 1,\n requestTimeoutMs: 1000,\n temperature: 0.1\n },\n {}\n );\n const result = await translator.translateStrings({ language: 'es', strings: ['hello'] });\n assert.deepEqual(result.strings, ['hola']);\n assert.equal(result.modelUsed, 'good/model:free');\n assert.equal(callCount, 2);\n } finally {\n global.fetch = originalFetch;\n if (originalApiKey === undefined) delete process.env.OPENROUTER_API_KEY;\n else process.env.OPENROUTER_API_KEY = originalApiKey;\n }\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/codex-commit.test.js",
+ "script": "codex-commit.test",
+ "category": "validator",
+ "purpose": "governance:agent-governance",
+ "scope": "tests/unit, tools/scripts/codex-commit.js",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Tests codex-commit.js — validates commit message generation and contract compliance",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/codex-commit.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script codex-commit.test\n * @category validator\n * @purpose governance:agent-governance\n * @scope tests/unit, tools/scripts/codex-commit.js\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Tests codex-commit.js — validates commit message generation and contract compliance\n * @pipeline manual — not yet in pipeline\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/codex-commit.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst REPO_ROOT = process.cwd();\nconst SCRIPT_PATH = path.join(REPO_ROOT, 'tools/scripts/codex-commit.js');\n\nfunction run(cmd, args, cwd) {\n return spawnSync(cmd, args, { cwd, encoding: 'utf8' });\n}\n\nfunction runGit(args, cwd) {\n const out = run('git', args, cwd);\n if (out.status !== 0) {\n throw new Error(`git ${args.join(' ')} failed: ${out.stderr || out.stdout}`);\n }\n return (out.stdout || '').trim();\n}\n\nfunction writeFile(absPath, content) {\n fs.mkdirSync(path.dirname(absPath), { recursive: true });\n fs.writeFileSync(absPath, content, 'utf8');\n}\n\nfunction mkRepo(prefix) {\n const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));\n runGit(['init', '-b', 'main'], dir);\n runGit(['config', 'user.email', 'tests@example.com'], dir);\n runGit(['config', 'user.name', 'test-runner'], dir);\n writeFile(path.join(dir, 'file.txt'), 'init\\n');\n runGit(['add', 'file.txt'], dir);\n runGit(['commit', '-m', 'init'], dir);\n return dir;\n}\n\nfunction runScript(args, cwd) {\n return run('node', [SCRIPT_PATH, ...args], cwd);\n}\n\nasync function runTests() {\n const failures = [];\n const cases = [];\n\n cases.push(async () => {\n const repo = mkRepo('codex-commit-normal-');\n writeFile(path.join(repo, 'file.txt'), 'normal\\n');\n runGit(['add', 'file.txt'], repo);\n\n const exec = runScript(['--message', 'chore: normal commit'], repo);\n assert.strictEqual(exec.status, 0, exec.stderr || exec.stdout);\n const body = runGit(['log', '-1', '--pretty=%B'], repo);\n assert.match(body, /chore: normal commit/);\n });\n\n cases.push(async () => {\n const repo = mkRepo('codex-commit-reject-');\n writeFile(path.join(repo, 'file.txt'), 'reject\\n');\n runGit(['add', 'file.txt'], repo);\n\n const exec = runScript(['--message', 'chore: should fail', '--no-verify'], repo);\n assert.strictEqual(exec.status, 1, 'no-verify without override must fail');\n const output = `${exec.stdout}\\n${exec.stderr}`;\n assert.match(output, /requires --human-override true/i);\n });\n\n cases.push(async () => {\n const repo = mkRepo('codex-commit-override-');\n writeFile(path.join(repo, 'file.txt'), 'override\\n');\n runGit(['add', 'file.txt'], repo);\n\n const exec = runScript(\n [\n '--message',\n 'chore: override commit',\n '--no-verify',\n '--human-override',\n 'true',\n '--override-note',\n 'User said no-verify is allowed for this commit.'\n ],\n repo\n );\n assert.strictEqual(exec.status, 0, exec.stderr || exec.stdout);\n\n const body = runGit(['log', '-1', '--pretty=%B'], repo);\n assert.match(body, /\\[override-audit\\]/);\n assert.match(body, /override_type: no-verify/);\n assert.match(body, /requested_by: human/);\n assert.match(body, /User said no-verify is allowed/);\n });\n\n for (let i = 0; i < cases.length; i += 1) {\n const name = `case-${i + 1}`;\n try {\n // eslint-disable-next-line no-await-in-loop\n await cases[i]();\n console.log(` ✓ ${name}`);\n } catch (error) {\n failures.push(`${name}: ${error.message}`);\n }\n }\n\n return {\n passed: failures.length === 0,\n total: cases.length,\n errors: failures\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ codex-commit tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} codex-commit test failure(s)`);\n result.errors.forEach((entry) => console.error(` - ${entry}`));\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ codex-commit tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/codex-safe-merge-with-stash.test.js",
+ "script": "codex-safe-merge-with-stash.test",
+ "category": "utility",
+ "purpose": "governance:agent-governance",
+ "scope": "tests/unit, tools/scripts/codex-safe-merge-with-stash.js",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Tests codex-safe-merge-with-stash.js — validates safe merge logic with stash handling",
+ "pipeline_declared": "manual — developer tool",
+ "usage": "node tests/unit/codex-safe-merge-with-stash.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script codex-safe-merge-with-stash.test\n * @category utility\n * @purpose governance:agent-governance\n * @scope tests/unit, tools/scripts/codex-safe-merge-with-stash.js\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Tests codex-safe-merge-with-stash.js — validates safe merge logic with stash handling\n * @pipeline manual — developer tool\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/codex-safe-merge-with-stash.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst REPO_ROOT = process.cwd();\nconst SCRIPT_PATH = path.join(REPO_ROOT, 'tools/scripts/codex-safe-merge-with-stash.js');\n\nfunction run(cmd, args, cwd) {\n return spawnSync(cmd, args, { cwd, encoding: 'utf8' });\n}\n\nfunction runGit(args, cwd) {\n const out = run('git', args, cwd);\n if (out.status !== 0) {\n throw new Error(`git ${args.join(' ')} failed: ${out.stderr || out.stdout}`);\n }\n return (out.stdout || '').trim();\n}\n\nfunction writeFile(absPath, content) {\n fs.mkdirSync(path.dirname(absPath), { recursive: true });\n fs.writeFileSync(absPath, content, 'utf8');\n}\n\nfunction mkRepo(prefix) {\n const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));\n runGit(['init', '-b', 'main'], dir);\n runGit(['config', 'user.email', 'tests@example.com'], dir);\n runGit(['config', 'user.name', 'test-runner'], dir);\n writeFile(path.join(dir, 'app.txt'), 'base\\n');\n runGit(['add', 'app.txt'], dir);\n runGit(['commit', '-m', 'init'], dir);\n runGit(['checkout', '-b', 'feature'], dir);\n return dir;\n}\n\nfunction runScript(args, cwd) {\n return run('node', [SCRIPT_PATH, ...args], cwd);\n}\n\nasync function runTests() {\n const failures = [];\n const cases = [];\n\n cases.push(async () => {\n const repo = mkRepo('codex-safe-merge-clean-');\n runGit(['checkout', 'main'], repo);\n writeFile(path.join(repo, 'app.txt'), 'base\\nmain-change\\n');\n runGit(['add', 'app.txt'], repo);\n runGit(['commit', '-m', 'main change'], repo);\n runGit(['checkout', 'feature'], repo);\n\n const exec = runScript(['--target', 'main'], repo);\n assert.strictEqual(exec.status, 0, exec.stderr || exec.stdout);\n const merged = fs.readFileSync(path.join(repo, 'app.txt'), 'utf8');\n assert.match(merged, /main-change/, 'feature should include merged change');\n });\n\n cases.push(async () => {\n const repo = mkRepo('codex-safe-merge-dirty-');\n runGit(['checkout', 'main'], repo);\n writeFile(path.join(repo, 'app.txt'), 'base\\nmain-change\\n');\n runGit(['add', 'app.txt'], repo);\n runGit(['commit', '-m', 'main change'], repo);\n runGit(['checkout', 'feature'], repo);\n\n writeFile(path.join(repo, 'local.txt'), 'local-dirty\\n');\n writeFile(path.join(repo, 'temp-untracked.txt'), 'temp\\n');\n runGit(['add', 'local.txt'], repo);\n\n const exec = runScript(['--target', 'main'], repo);\n assert.strictEqual(exec.status, 0, exec.stderr || exec.stdout);\n\n const localTracked = fs.readFileSync(path.join(repo, 'local.txt'), 'utf8');\n const localUntracked = fs.readFileSync(path.join(repo, 'temp-untracked.txt'), 'utf8');\n assert.match(localTracked, /local-dirty/, 'tracked local file should be restored');\n assert.match(localUntracked, /temp/, 'untracked local file should be restored');\n });\n\n cases.push(async () => {\n const repo = mkRepo('codex-safe-merge-conflict-');\n runGit(['checkout', 'main'], repo);\n writeFile(path.join(repo, 'app.txt'), 'main-version\\n');\n runGit(['add', 'app.txt'], repo);\n runGit(['commit', '-m', 'main update'], repo);\n runGit(['checkout', 'feature'], repo);\n writeFile(path.join(repo, 'app.txt'), 'feature-version\\n');\n runGit(['add', 'app.txt'], repo);\n runGit(['commit', '-m', 'feature update'], repo);\n\n writeFile(path.join(repo, 'dirty.txt'), 'dirty\\n');\n runGit(['add', 'dirty.txt'], repo);\n\n const exec = runScript(['--target', 'main'], repo);\n assert.strictEqual(exec.status, 1, 'merge conflict should fail');\n\n const stashList = runGit(['stash', 'list'], repo);\n assert.match(stashList, /codex-safe-merge:/, 'stash should be preserved when merge fails');\n });\n\n for (let i = 0; i < cases.length; i += 1) {\n const name = `case-${i + 1}`;\n try {\n // eslint-disable-next-line no-await-in-loop\n await cases[i]();\n console.log(` ✓ ${name}`);\n } catch (error) {\n failures.push(`${name}: ${error.message}`);\n }\n }\n\n return {\n passed: failures.length === 0,\n total: cases.length,\n errors: failures\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ codex-safe-merge-with-stash tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} codex-safe-merge-with-stash test failure(s)`);\n result.errors.forEach((entry) => console.error(` - ${entry}`));\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ codex-safe-merge-with-stash tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/codex-skill-sync.test.js",
+ "script": "codex-skill-sync.test",
+ "category": "validator",
+ "purpose": "governance:agent-governance",
+ "scope": "tests/unit, tools/scripts/sync-codex-skills.js, ai-tools/ai-skills/templates",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Tests sync-codex-skills.js — validates skill file synchronisation between sources",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/codex-skill-sync.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script codex-skill-sync.test\n * @category validator\n * @purpose governance:agent-governance\n * @scope tests/unit, tools/scripts/sync-codex-skills.js, ai-tools/ai-skills/templates\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Tests sync-codex-skills.js — validates skill file synchronisation between sources\n * @pipeline manual — not yet in pipeline\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/codex-skill-sync.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst REPO_ROOT = process.cwd();\nconst SYNC_SCRIPT = path.join(REPO_ROOT, 'tools/scripts/sync-codex-skills.js');\n\nlet errors = [];\n\nfunction runNode(args, options = {}) {\n return spawnSync('node', [SYNC_SCRIPT, ...args], {\n cwd: REPO_ROOT,\n encoding: 'utf8',\n env: {\n ...process.env,\n ...(options.env || {})\n }\n });\n}\n\nfunction mkTmpDir(prefix) {\n return fs.mkdtempSync(path.join(os.tmpdir(), prefix));\n}\n\nfunction writeFile(absPath, content) {\n fs.mkdirSync(path.dirname(absPath), { recursive: true });\n fs.writeFileSync(absPath, content, 'utf8');\n}\n\nfunction createTemplate(sourceDir, number, name, description) {\n const fileName = `${String(number).padStart(2, '0')}-${name}.template.md`;\n const absPath = path.join(sourceDir, fileName);\n const content = [\n '---',\n `name: ${name}`,\n `description: ${description}`,\n 'tier: 1',\n 'triggers:',\n ' - \"trigger one\"',\n ' - \"trigger two\"',\n ' - \"trigger three\"',\n 'primary_paths:',\n ' - \"README.md\"',\n ' - \"tools/scripts\"',\n 'primary_commands:',\n ' - \"bash lpd setup --yes\"',\n ' - \"bash lpd doctor --strict\"',\n '---',\n '',\n `SKILL: ${name}`,\n '',\n 'Goal',\n 'Execute the workflow safely.',\n '',\n 'Constraints',\n '- Keep v1 immutable.',\n '',\n 'Workflow',\n '1. Run command one.',\n '2. Run command two.',\n '',\n 'Deliverable Format',\n '- Provide concise status and next actions.',\n '',\n 'Failure Modes / Fallback',\n '- If command fails, report exact remediation.',\n '',\n 'Validation Checklist',\n '- [ ] Required checks were run.',\n ''\n ].join('\\n');\n writeFile(absPath, content);\n return { absPath, content };\n}\n\nfunction readUtf8(absPath) {\n return fs.readFileSync(absPath, 'utf8');\n}\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n name,\n message: error.message\n });\n }\n}\n\nasync function runTests() {\n errors = [];\n console.log('🧪 Codex Skill Sync Unit Tests');\n\n await runCase('Sync installs templates and generates openai.yaml', async () => {\n const root = mkTmpDir('codex-skill-sync-install-');\n const sourceDir = path.join(root, 'source');\n const destDir = path.join(root, 'dest');\n\n const t1 = createTemplate(sourceDir, 1, 'alpha-skill', 'Alpha skill synchronization workflow.');\n createTemplate(sourceDir, 2, 'beta-skill', 'Beta skill synchronization workflow.');\n\n const result = runNode(['--source-dir', sourceDir, '--dest', destDir]);\n assert.strictEqual(result.status, 0, `sync exited non-zero: ${result.stderr || result.stdout}`);\n\n const skillPath = path.join(destDir, 'alpha-skill', 'SKILL.md');\n const openaiPath = path.join(destDir, 'alpha-skill', 'agents', 'openai.yaml');\n assert.ok(fs.existsSync(skillPath), 'alpha SKILL.md should exist');\n assert.ok(fs.existsSync(openaiPath), 'alpha openai.yaml should exist');\n assert.strictEqual(readUtf8(skillPath), t1.content, 'SKILL.md should match canonical template exactly');\n\n const openai = readUtf8(openaiPath);\n assert.ok(openai.includes('display_name: \"Alpha Skill\"'), 'openai.yaml should include deterministic display_name');\n assert.ok(openai.includes('short_description: \"'), 'openai.yaml should include short_description');\n assert.ok(openai.includes('$alpha-skill'), 'openai.yaml default_prompt should include explicit $skill reference');\n });\n\n await runCase('Check mode fails on drift', async () => {\n const root = mkTmpDir('codex-skill-sync-check-');\n const sourceDir = path.join(root, 'source');\n const destDir = path.join(root, 'dest');\n\n createTemplate(sourceDir, 1, 'drift-skill', 'Detect drift in check mode.');\n const syncResult = runNode(['--source-dir', sourceDir, '--dest', destDir]);\n assert.strictEqual(syncResult.status, 0, `initial sync failed: ${syncResult.stderr || syncResult.stdout}`);\n\n writeFile(path.join(destDir, 'drift-skill', 'SKILL.md'), 'tampered');\n const checkResult = runNode(['--source-dir', sourceDir, '--dest', destDir, '--check']);\n assert.strictEqual(checkResult.status, 1, 'check mode should fail on drift');\n assert.ok((checkResult.stdout + checkResult.stderr).includes('drift'), 'check output should include drift signal');\n });\n\n await runCase('Safe upsert preserves unmanaged skills', async () => {\n const root = mkTmpDir('codex-skill-sync-upsert-');\n const sourceDir = path.join(root, 'source');\n const destDir = path.join(root, 'dest');\n createTemplate(sourceDir, 1, 'managed-skill', 'Managed skill from templates.');\n\n const unmanagedPath = path.join(destDir, 'custom-local-skill', 'SKILL.md');\n writeFile(unmanagedPath, '---\\nname: custom-local-skill\\ndescription: local\\n---\\n\\nSKILL: Custom\\n');\n\n const result = runNode(['--source-dir', sourceDir, '--dest', destDir]);\n assert.strictEqual(result.status, 0, `sync failed: ${result.stderr || result.stdout}`);\n assert.ok(fs.existsSync(unmanagedPath), 'unmanaged skill should remain');\n assert.strictEqual(readUtf8(unmanagedPath).includes('custom-local-skill'), true, 'unmanaged content should remain intact');\n });\n\n await runCase('Subset install writes only selected skills', async () => {\n const root = mkTmpDir('codex-skill-sync-subset-');\n const sourceDir = path.join(root, 'source');\n const destDir = path.join(root, 'dest');\n createTemplate(sourceDir, 1, 'subset-one', 'Subset one.');\n createTemplate(sourceDir, 2, 'subset-two', 'Subset two.');\n\n const result = runNode(['--source-dir', sourceDir, '--dest', destDir, '--skills', 'subset-two']);\n assert.strictEqual(result.status, 0, `subset sync failed: ${result.stderr || result.stdout}`);\n assert.ok(!fs.existsSync(path.join(destDir, 'subset-one')), 'subset-one should not be installed');\n assert.ok(fs.existsSync(path.join(destDir, 'subset-two', 'SKILL.md')), 'subset-two should be installed');\n });\n\n await runCase('Dry-run does not mutate destination', async () => {\n const root = mkTmpDir('codex-skill-sync-dryrun-');\n const sourceDir = path.join(root, 'source');\n const destDir = path.join(root, 'dest');\n createTemplate(sourceDir, 1, 'dry-run-skill', 'Dry-run should not write files.');\n\n const result = runNode(['--source-dir', sourceDir, '--dest', destDir, '--dry-run']);\n assert.strictEqual(result.status, 0, `dry-run failed: ${result.stderr || result.stdout}`);\n assert.ok(!fs.existsSync(path.join(destDir, 'dry-run-skill')), 'dry-run should not create skill files');\n });\n\n return {\n passed: errors.length === 0,\n total: 5,\n errors\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ Codex skill sync unit tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} codex skill sync unit test failure(s)`);\n result.errors.forEach((entry) => {\n console.error(` - ${entry.name}: ${entry.message}`);\n });\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ Codex skill sync unit tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:codex-skills-sync"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tests/unit/${String(number).padStart(2, '0')}-${name}.template.md",
+ "type": "generated-output",
+ "call": "writeFile"
+ },
+ {
+ "output_path": "tests/unit/${String(number).padStart(2, '0')}-${name}.template.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tests/unit/SKILL.md",
+ "type": "generated-output",
+ "call": "writeFile"
+ }
+ ],
+ "outputs_display": "tests/unit/${String(number).padStart(2, '0')}-${name}.template.md, tests/unit/${String(number).padStart(2, '0')}-${name}.template.md, tests/unit/SKILL.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:codex-skills-sync)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/create-codex-pr.test.js",
+ "script": "create-codex-pr.test",
+ "category": "generator",
+ "purpose": "governance:agent-governance",
+ "scope": "tests/unit, tools/scripts/create-codex-pr.js",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Tests create-codex-pr.js — validates PR creation logic and branch naming",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/create-codex-pr.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script create-codex-pr.test\n * @category generator\n * @purpose governance:agent-governance\n * @scope tests/unit, tools/scripts/create-codex-pr.js\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Tests create-codex-pr.js — validates PR creation logic and branch naming\n * @pipeline manual — not yet in pipeline\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/create-codex-pr.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst REPO_ROOT = process.cwd();\nconst SCRIPT_PATH = path.join(REPO_ROOT, 'tools/scripts/create-codex-pr.js');\n\nfunction runScript(args) {\n return spawnSync('node', [SCRIPT_PATH, ...args], {\n cwd: REPO_ROOT,\n encoding: 'utf8'\n });\n}\n\nfunction mkTmpDir(prefix) {\n return fs.mkdtempSync(path.join(os.tmpdir(), prefix));\n}\n\nfunction writeFile(absPath, content) {\n fs.mkdirSync(path.dirname(absPath), { recursive: true });\n fs.writeFileSync(absPath, content, 'utf8');\n}\n\nfunction makeContract(absPath, taskId = 3456) {\n const content = [\n `task_id: ${taskId}`,\n 'base_branch: docs-v2',\n `branch: codex/${taskId}-auto-pr-body`,\n 'scope_in:',\n ' - v2/community/',\n ' - docs.json',\n 'scope_out:',\n ' - v1/',\n 'allowed_generated:',\n ' - docs-index.json',\n 'acceptance_checks:',\n ' - node tests/run-pr-checks.js --base-ref docs-v2',\n ' - node tests/integration/v2-link-audit.js --files v2/community/faq.mdx --strict',\n 'follow_up_issues:',\n ` - ${taskId + 1}`,\n ''\n ].join('\\n');\n writeFile(absPath, content);\n}\n\nasync function runTests() {\n const failures = [];\n const cases = [];\n\n cases.push(async () => {\n const tmp = mkTmpDir('codex-pr-body-');\n const contractPath = path.join(tmp, 'task-contract.yaml');\n const outputPath = path.join(tmp, 'pr-body.md');\n makeContract(contractPath);\n\n const run = runScript([\n '--contract',\n contractPath,\n '--output',\n outputPath,\n '--head',\n 'codex/3456-auto-pr-body',\n '--base',\n 'docs-v2',\n '--changed-files',\n 'v2/community/faq.mdx,docs.json'\n ]);\n\n assert.strictEqual(run.status, 0, `generator failed: ${run.stderr || run.stdout}`);\n const body = fs.readFileSync(outputPath, 'utf8');\n assert.match(body, /codex-pr-body-generated:/, 'body should include generated marker');\n assert.match(body, /^Fixes #3456$/m, 'body should include closing keyword for task issue');\n assert.match(body, /^## Scope/m, 'body should include Scope section');\n assert.match(body, /^## Validation/m, 'body should include Validation section');\n assert.match(body, /^## Follow-up Tasks/m, 'body should include Follow-up section');\n assert.match(body, /`v2\\/community\\/faq\\.mdx`/, 'body should include changed file list');\n assert.match(body, /#3457/, 'body should include follow-up issue id');\n });\n\n cases.push(async () => {\n const tmp = mkTmpDir('codex-pr-dry-run-');\n const contractPath = path.join(tmp, 'task-contract.yaml');\n const outputPath = path.join(tmp, 'pr-body.md');\n makeContract(contractPath, 4567);\n\n const run = runScript([\n '--contract',\n contractPath,\n '--output',\n outputPath,\n '--head',\n 'codex/4567-auto-pr-body',\n '--base',\n 'docs-v2',\n '--changed-files',\n 'docs.json',\n '--create',\n '--dry-run'\n ]);\n\n assert.strictEqual(run.status, 0, `dry-run create failed: ${run.stderr || run.stdout}`);\n const output = `${run.stdout}\\n${run.stderr}`;\n assert.match(output, /DRY RUN: gh pr create/, 'dry-run should print gh create command');\n });\n\n for (let index = 0; index < cases.length; index += 1) {\n const name = `case-${index + 1}`;\n try {\n // eslint-disable-next-line no-await-in-loop\n await cases[index]();\n console.log(` ✓ ${name}`);\n } catch (error) {\n failures.push(`${name}: ${error.message}`);\n }\n }\n\n return {\n passed: failures.length === 0,\n total: cases.length,\n errors: failures\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ create-codex-pr tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} create-codex-pr test failure(s)`);\n result.errors.forEach((entry) => console.error(` - ${entry}`));\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ create-codex-pr tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:codex-pr-create"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:codex-pr-create)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/validate-codex-task-contract.test.js",
+ "script": "validate-codex-task-contract.test",
+ "category": "enforcer",
+ "purpose": "governance:agent-governance",
+ "scope": "tests/unit, tools/scripts/validate-codex-task-contract.js",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Tests validate-codex-task-contract.js — validates contract checking logic",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/validate-codex-task-contract.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script validate-codex-task-contract.test\n * @category enforcer\n * @purpose governance:agent-governance\n * @scope tests/unit, tools/scripts/validate-codex-task-contract.js\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Tests validate-codex-task-contract.js — validates contract checking logic\n * @pipeline manual — not yet in pipeline\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/validate-codex-task-contract.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst REPO_ROOT = process.cwd();\nconst SCRIPT_PATH = path.join(REPO_ROOT, 'tools/scripts/validate-codex-task-contract.js');\n\nfunction mkTmpDir(prefix) {\n return fs.mkdtempSync(path.join(os.tmpdir(), prefix));\n}\n\nfunction writeFile(absPath, content, mode = 0o644) {\n fs.mkdirSync(path.dirname(absPath), { recursive: true });\n fs.writeFileSync(absPath, content, { encoding: 'utf8', mode });\n}\n\nfunction runScript(args, env = {}) {\n return spawnSync('node', [SCRIPT_PATH, ...args], {\n cwd: REPO_ROOT,\n encoding: 'utf8',\n env: {\n ...process.env,\n ...env\n }\n });\n}\n\nfunction writeContract(absPath, taskId, branchName) {\n const content = [\n `task_id: ${taskId}`,\n 'base_branch: docs-v2',\n `branch: ${branchName}`,\n 'scope_in:',\n ' - .codex/task-contract.yaml',\n 'scope_out:',\n ' - v1/',\n 'allowed_generated: []',\n 'acceptance_checks:',\n ' - node tests/run-pr-checks.js --base-ref docs-v2',\n 'risk_flags: []',\n 'follow_up_issues: []',\n ''\n ].join('\\n');\n writeFile(absPath, content);\n}\n\nfunction writeIssuePolicy(absPath) {\n const policy = {\n required_labels: ['docs-v2'],\n required_label_prefixes: ['type:', 'area:', 'classification:', 'priority:'],\n forbidden_labels: ['status: needs-info'],\n required_state: 'open'\n };\n writeFile(absPath, `${JSON.stringify(policy, null, 2)}\\n`);\n}\n\nfunction buildIssuePayload({ state = 'open', labels = [] }) {\n return JSON.stringify({\n state,\n labels: labels.map((name) => ({ name }))\n });\n}\n\nfunction assertFailedWith(result, regex, msg) {\n assert.strictEqual(result.status, 1, msg || 'expected command to fail');\n const combined = `${result.stdout}\\n${result.stderr}`;\n assert.match(combined, regex, `expected output to match ${regex}, got:\\n${combined}`);\n}\n\nasync function runTests() {\n const failures = [];\n\n try {\n const tmp = mkTmpDir('validate-codex-contract-');\n const taskId = 5678;\n const branchName = 'codex/5678-marker-enforcement';\n const contractPath = path.join(tmp, 'task-contract.yaml');\n const prBodyOk = path.join(tmp, 'pr-ok.md');\n const prBodyMissing = path.join(tmp, 'pr-missing.md');\n const prBodyNoClosing = path.join(tmp, 'pr-no-closing.md');\n const prBodyWrongClosing = path.join(tmp, 'pr-wrong-closing.md');\n const issuePolicy = path.join(tmp, 'issue-policy.json');\n\n writeContract(contractPath, taskId, branchName);\n writeIssuePolicy(issuePolicy);\n\n writeFile(\n prBodyOk,\n [\n ``,\n '',\n `Fixes #${taskId}`,\n '',\n '## Scope',\n '',\n '- scope details',\n '',\n '## Validation',\n '',\n '- validation details',\n '',\n '## Follow-up Tasks',\n '',\n '- none',\n ''\n ].join('\\n')\n );\n\n writeFile(\n prBodyNoClosing,\n [\n ``,\n '',\n '## Scope',\n '',\n '- scope details',\n '',\n '## Validation',\n '',\n '- validation details',\n '',\n '## Follow-up Tasks',\n '',\n '- none',\n ''\n ].join('\\n')\n );\n\n writeFile(\n prBodyWrongClosing,\n [\n ``,\n '',\n 'Fixes #9999',\n '',\n '## Scope',\n '',\n '- scope details',\n '',\n '## Validation',\n '',\n '- validation details',\n '',\n '## Follow-up Tasks',\n '',\n '- none',\n ''\n ].join('\\n')\n );\n\n writeFile(\n prBodyMissing,\n [\n '## Scope',\n '',\n '- scope details',\n '',\n '## Validation',\n '',\n '- validation details',\n '',\n '## Follow-up Tasks',\n '',\n '- none',\n ''\n ].join('\\n')\n );\n\n const baseArgs = [\n '--branch',\n branchName,\n '--contract',\n contractPath,\n '--issue-source',\n 'gh',\n '--issue-policy',\n issuePolicy,\n '--issue-number',\n String(taskId),\n '--issue-repo',\n 'livepeer/docs',\n '--validate-contract-only',\n '--require-issue-state'\n ];\n\n const baseEnv = {\n PATH: process.env.PATH\n };\n\n // Marker success\n const ok = runScript([\n '--branch',\n branchName,\n '--contract',\n contractPath,\n '--require-pr-body',\n '--pr-body-file',\n prBodyOk,\n '--files',\n '.codex/task-contract.yaml'\n ]);\n assert.strictEqual(ok.status, 0, `expected marker-valid run to pass: ${ok.stderr || ok.stdout}`);\n console.log(' ✓ marker-valid case passed');\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:codex-task-contract"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:codex-task-contract)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/codex-commit.js",
+ "script": "codex-commit",
+ "category": "generator",
+ "purpose": "governance:agent-governance",
+ "scope": "tools/scripts, .githooks, ai-tools/ai-rules",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Codex commit helper — audits codex branch state and generates compliant commit messages",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/codex-commit.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script codex-commit\n * @category generator\n * @purpose governance:agent-governance\n * @scope tools/scripts, .githooks, ai-tools/ai-rules\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Codex commit helper — audits codex branch state and generates compliant commit messages\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @dualmode dual-mode (document flags)\n * @usage node tools/scripts/codex-commit.js [flags]\n */\n\nconst { spawnSync } = require('child_process')\n\nfunction parseArgs(argv) {\n const args = {\n message: '',\n noVerify: false,\n humanOverride: '',\n overrideNote: '',\n trailers: [],\n }\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i]\n if (token === '--message') {\n args.message = String(argv[i + 1] || '').trim()\n i += 1\n continue\n }\n if (token.startsWith('--message=')) {\n args.message = token.slice('--message='.length).trim()\n continue\n }\n if (token === '--no-verify') {\n args.noVerify = true\n continue\n }\n if (token === '--human-override') {\n args.humanOverride = String(argv[i + 1] || '')\n .trim()\n .toLowerCase()\n i += 1\n continue\n }\n if (token.startsWith('--human-override=')) {\n args.humanOverride = token\n .slice('--human-override='.length)\n .trim()\n .toLowerCase()\n continue\n }\n if (token === '--override-note') {\n args.overrideNote = String(argv[i + 1] || '').trim()\n i += 1\n continue\n }\n if (token.startsWith('--override-note=')) {\n args.overrideNote = token.slice('--override-note='.length).trim()\n continue\n }\n if (token === '--trailer') {\n args.trailers.push(String(argv[i + 1] || '').trim())\n i += 1\n continue\n }\n if (token.startsWith('--trailer=')) {\n args.trailers.push(token.slice('--trailer='.length).trim())\n continue\n }\n if (token === '--help' || token === '-h') {\n args.help = true\n continue\n }\n\n throw new Error(`Unknown argument: ${token}`)\n }\n\n return args\n}\n\nfunction usage() {\n console.log(\n 'Usage: node tools/scripts/codex-commit.js --message [--no-verify --human-override true --override-note ] [--trailer ]'\n )\n}\n\nfunction validateArgs(args) {\n if (!args.message) {\n throw new Error('--message is required')\n }\n\n if (!args.noVerify) {\n return\n }\n\n if (args.humanOverride !== 'true') {\n throw new Error('--human-override true is required when using --no-verify')\n }\n\n if (!args.overrideNote) {\n throw new Error('--override-note is required when using --no-verify')\n }\n\n if (String(process.env.ALLOW_HUMAN_NO_VERIFY || '').trim() !== '1') {\n throw new Error(\n 'ALLOW_HUMAN_NO_VERIFY=1 is required for --no-verify override path'\n )\n }\n}\n\nfunction runCommit(args) {\n const gitArgs = ['commit', '-m', args.message]\n\n if (args.noVerify) {\n gitArgs.push('--no-verify')\n gitArgs.push('--trailer', 'human-override=no-verify')\n gitArgs.push('--trailer', `override-note=${args.overrideNote}`)\n }\n\n args.trailers.forEach((trailer) => {\n if (!trailer) return\n gitArgs.push('--trailer', trailer)\n })\n\n const result = spawnSync('git', gitArgs, { stdio: 'inherit' })\n return result.status === 0\n}\n\nfunction main() {\n const args = parseArgs(process.argv.slice(2))\n if (args.help) {\n usage()\n process.exit(0)\n }\n\n validateArgs(args)\n const ok = runCommit(args)\n if (!ok) {\n process.exit(1)\n }\n\n if (args.noVerify) {\n console.log('✅ Commit created with audited human no-verify override')\n } else {\n console.log('✅ Commit created')\n }\n}\n\nif (require.main === module) {\n try {\n main()\n } catch (error) {\n console.error(`❌ ${error.message}`)\n process.exit(1)\n }\n}\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/codex-safe-merge-with-stash.js",
+ "script": "codex-safe-merge-with-stash",
+ "category": "utility",
+ "purpose": "governance:agent-governance",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Codex merge utility — safely merges branches with stash handling to avoid codex conflicts",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/codex-safe-merge-with-stash.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script codex-safe-merge-with-stash\n * @category utility\n * @purpose governance:agent-governance\n * @scope tools/scripts\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Codex merge utility — safely merges branches with stash handling to avoid codex conflicts\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/codex-safe-merge-with-stash.js [flags]\n */\n\nconst { spawnSync } = require('child_process');\n\nfunction parseArgs(argv) {\n const args = {\n target: '',\n stashUntracked: true,\n dryRun: false,\n json: false\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--target') {\n args.target = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--target=')) {\n args.target = token.slice('--target='.length).trim();\n continue;\n }\n if (token === '--stash-untracked') {\n args.stashUntracked = true;\n continue;\n }\n if (token === '--no-stash-untracked') {\n args.stashUntracked = false;\n continue;\n }\n if (token === '--dry-run') {\n args.dryRun = true;\n continue;\n }\n if (token === '--json') {\n args.json = true;\n continue;\n }\n if (token === '--help' || token === '-h') {\n printUsage();\n process.exit(0);\n }\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (!args.target) {\n throw new Error('Missing required --target ');\n }\n\n return args;\n}\n\nfunction printUsage() {\n console.log('Usage: node tools/scripts/codex-safe-merge-with-stash.js --target [--dry-run] [--json]');\n}\n\nfunction runGit(args, options = {}) {\n const res = spawnSync('git', args, {\n encoding: 'utf8',\n ...options\n });\n return {\n status: res.status,\n stdout: String(res.stdout || ''),\n stderr: String(res.stderr || ''),\n ok: res.status === 0\n };\n}\n\nfunction runGitOrThrow(args, options = {}) {\n const res = runGit(args, options);\n if (!res.ok) {\n const detail = (res.stderr || res.stdout).trim() || `git ${args.join(' ')} failed`;\n throw new Error(detail);\n }\n return res.stdout.trim();\n}\n\nfunction hasOriginRemote() {\n const res = runGit(['remote', 'get-url', 'origin']);\n return res.ok;\n}\n\nfunction getStashRefByMessage(marker) {\n const list = runGitOrThrow(['stash', 'list', '--format=%gd|%s']);\n if (!list) return '';\n const line = list\n .split('\\n')\n .map((entry) => entry.trim())\n .find((entry) => entry.includes(marker));\n if (!line) return '';\n return line.split('|')[0].trim();\n}\n\nfunction getGitStatusPorcelain() {\n return runGitOrThrow(['status', '--porcelain']);\n}\n\nfunction printResult(result, jsonMode) {\n if (jsonMode) {\n process.stdout.write(`${JSON.stringify(result, null, 2)}\\n`);\n return;\n }\n\n const statusIcon = result.passed ? '✅' : '❌';\n console.log(`${statusIcon} ${result.message}`);\n if (result.actions && result.actions.length > 0) {\n result.actions.forEach((action) => console.log(` - ${action}`));\n }\n if (result.recovery && result.recovery.length > 0) {\n console.log('Recovery steps:');\n result.recovery.forEach((line) => console.log(` - ${line}`));\n }\n}\n\nfunction main() {\n const args = parseArgs(process.argv.slice(2));\n const actions = [];\n const recovery = [];\n\n try {\n const branch = runGitOrThrow(['rev-parse', '--abbrev-ref', 'HEAD']);\n const dirty = getGitStatusPorcelain().trim().length > 0;\n const marker = `codex-safe-merge:${new Date().toISOString()}:${args.target}`;\n let stashRef = '';\n\n if (args.dryRun) {\n actions.push(`would evaluate dirty tree on branch ${branch}`);\n if (dirty) {\n actions.push(\n `would run git stash push ${args.stashUntracked ? '-u ' : ''}-m \"${marker}\"`\n );\n }\n if (hasOriginRemote()) {\n actions.push(`would run git fetch origin ${args.target}`);\n }\n actions.push(`would run git merge --no-edit ${args.target}`);\n if (dirty) {\n actions.push('would run git stash pop after successful merge');\n }\n\n printResult(\n {\n passed: true,\n message: 'Dry-run completed.',\n branch,\n target: args.target,\n dirty,\n actions,\n recovery\n },\n args.json\n );\n process.exit(0);\n }\n\n if (dirty) {\n const stashArgs = ['stash', 'push'];\n if (args.stashUntracked) stashArgs.push('-u');\n stashArgs.push('-m', marker);\n runGitOrThrow(stashArgs);\n stashRef = getStashRefByMessage(marker);\n actions.push(`stashed local changes as ${stashRef || marker}`);\n }\n\n if (hasOriginRemote()) {\n const fetchRes = runGit(['fetch', 'origin', args.target]);\n if (!fetchRes.ok) {\n actions.push(`fetch skipped/failed for target ${args.target}; proceeding with local refs`);\n } else {\n actions.push(`fetched target from origin: ${args.target}`);\n }\n }\n\n const mergeRes = runGit(['merge', '--no-edit', args.target]);\n if (!mergeRes.ok) {\n actions.push(`merge failed for target ${args.target}`);\n recovery.push('Resolve or abort merge explicitly (for example: git merge --abort).');\n if (stashRef) {\n recovery.push(`Stashed changes are preserved in ${stashRef}. Apply later with: git stash pop ${stashRef}`);\n }\n\n printResult(\n {\n passed: false,\n message: `Merge failed for target ${args.target}.`,\n branch,\n target: args.target,\n dirty,\n stashRef,\n actions,\n recovery\n },\n args.json\n );\n process.exit(1);\n }\n\n actions.push(`merged target ${args.target} into ${branch}`);\n\n if (stashRef) {\n const popRes = runGit(['stash', 'pop', stashRef]);\n if (!popRes.ok) {\n actions.push(`stash pop failed for ${stashRef}`);\n recovery.push('Resolve stash-pop conflicts and stage resolved files manually.');\n recovery.push(`If needed, stash remains available as ${stashRef}.`);\n\n printResult(",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/codex/lock-release.js",
+ "script": "codex/lock-release",
+ "category": "utility",
+ "purpose": "governance:agent-governance",
+ "scope": "tools/scripts/codex, .codex/locks-local, .codex/task-contract.yaml",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Codex lock release utility — releases stale codex lock files",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/codex/lock-release.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script codex/lock-release\n * @category utility\n * @purpose governance:agent-governance\n * @scope tools/scripts/codex, .codex/locks-local, .codex/task-contract.yaml\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Codex lock release utility — releases stale codex lock files\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/codex/lock-release.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\nconst yaml = require('js-yaml');\n\nconst LOCK_DIR_REL = '.codex/locks-local';\nconst DEFAULT_CONTRACT = '.codex/task-contract.yaml';\n\nconst REPO_ROOT = getRepoRoot();\n\nfunction getRepoRoot() {\n const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n if (result.status === 0 && String(result.stdout || '').trim()) {\n return String(result.stdout || '').trim();\n }\n return process.cwd();\n}\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction runGit(args) {\n const result = spawnSync('git', args, { cwd: REPO_ROOT, encoding: 'utf8' });\n if (result.status !== 0) {\n const stderr = String(result.stderr || '').trim();\n const stdout = String(result.stdout || '').trim();\n throw new Error(stderr || stdout || `git ${args.join(' ')} failed`);\n }\n return String(result.stdout || '').trim();\n}\n\nfunction parseArgs(argv) {\n const args = {\n branch: '',\n lockId: '',\n contractPath: DEFAULT_CONTRACT\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--branch') {\n args.branch = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--branch=')) {\n args.branch = token.slice('--branch='.length).trim();\n continue;\n }\n if (token === '--lock-id') {\n args.lockId = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--lock-id=')) {\n args.lockId = token.slice('--lock-id='.length).trim();\n continue;\n }\n if (token === '--contract') {\n args.contractPath = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--contract=')) {\n args.contractPath = token.slice('--contract='.length).trim();\n continue;\n }\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n return args;\n}\n\nfunction usage() {\n console.log('Usage: node tools/scripts/codex/lock-release.js [--branch ] [--lock-id ] [--contract ]');\n}\n\nfunction detectBranch(args) {\n if (args.branch) return args.branch;\n\n const contractAbs = path.resolve(REPO_ROOT, args.contractPath);\n if (fs.existsSync(contractAbs)) {\n try {\n const parsed = yaml.load(fs.readFileSync(contractAbs, 'utf8'));\n const contractBranch = String(parsed && parsed.branch ? parsed.branch : '').trim();\n if (contractBranch) return contractBranch;\n } catch (_error) {\n // ignore and fallback to git branch detection\n }\n }\n\n return runGit(['rev-parse', '--abbrev-ref', 'HEAD']);\n}\n\nfunction main() {\n const args = parseArgs(process.argv.slice(2));\n if (args.help) {\n usage();\n process.exit(0);\n }\n\n const branch = detectBranch(args);\n const lockDirAbs = path.join(REPO_ROOT, LOCK_DIR_REL);\n if (!fs.existsSync(lockDirAbs)) {\n throw new Error(`Lock directory not found: ${LOCK_DIR_REL}`);\n }\n\n const entries = fs.readdirSync(lockDirAbs, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith('.json'));\n let releasedCount = 0;\n\n entries.forEach((entry) => {\n const abs = path.join(lockDirAbs, entry.name);\n let lock;\n try {\n lock = JSON.parse(fs.readFileSync(abs, 'utf8'));\n } catch (_error) {\n return;\n }\n\n if (args.lockId && String(lock.lock_id || '') !== args.lockId) {\n return;\n }\n\n if (!args.lockId && String(lock.branch || '') !== branch) {\n return;\n }\n\n if (String(lock.status || '') === 'released') {\n releasedCount += 1;\n return;\n }\n\n lock.status = 'released';\n lock.released_at = new Date().toISOString();\n fs.writeFileSync(abs, `${JSON.stringify(lock, null, 2)}\\n`, 'utf8');\n releasedCount += 1;\n });\n\n if (releasedCount === 0) {\n throw new Error(args.lockId ? `No lock found for lock_id=${args.lockId}` : `No active lock found for branch ${branch}`);\n }\n\n console.log(`✅ Released ${releasedCount} lock(s) for ${args.lockId ? `lock_id=${args.lockId}` : branch}`);\n}\n\nif (require.main === module) {\n try {\n main();\n } catch (error) {\n console.error(`❌ ${error.message}`);\n process.exit(1);\n }\n}\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/codex/.codex/locks-local",
+ "type": "directory",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/codex/.codex/locks-local",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/codex/task-finalize.js",
+ "script": "codex/task-finalize",
+ "category": "enforcer",
+ "purpose": "governance:agent-governance",
+ "scope": "tools/scripts/codex, tools/scripts/validate-codex-task-contract.js, tools/scripts/verify-pay-orc-gate-finalize.sh",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Codex task finaliser — enforces task completion requirements before closing",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/codex/task-finalize.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script codex/task-finalize\n * @category enforcer\n * @purpose governance:agent-governance\n * @scope tools/scripts/codex, tools/scripts/validate-codex-task-contract.js, tools/scripts/verify-pay-orc-gate-finalize.sh\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Codex task finaliser — enforces task completion requirements before closing\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/codex/task-finalize.js [flags]\n */\n\nconst { spawnSync } = require('child_process');\nconst path = require('path');\n\nconst REPO_ROOT = getRepoRoot();\n\nfunction getRepoRoot() {\n const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n if (result.status === 0 && String(result.stdout || '').trim()) {\n return String(result.stdout || '').trim();\n }\n return process.cwd();\n}\n\nfunction run(command, args) {\n const result = spawnSync(command, args, { cwd: REPO_ROOT, encoding: 'utf8' });\n if (result.stdout) process.stdout.write(result.stdout);\n if (result.stderr) process.stderr.write(result.stderr);\n return result.status === 0;\n}\n\nfunction parseArgs(argv) {\n const args = {\n branch: '',\n contractPath: '.codex/task-contract.yaml',\n baseRef: '',\n profile: ''\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--branch') {\n args.branch = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--branch=')) {\n args.branch = token.slice('--branch='.length).trim();\n continue;\n }\n if (token === '--contract') {\n args.contractPath = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--contract=')) {\n args.contractPath = token.slice('--contract='.length).trim();\n continue;\n }\n if (token === '--base-ref') {\n args.baseRef = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--base-ref=')) {\n args.baseRef = token.slice('--base-ref='.length).trim();\n continue;\n }\n if (token === '--profile') {\n args.profile = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--profile=')) {\n args.profile = token.slice('--profile='.length).trim();\n continue;\n }\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n return args;\n}\n\nfunction usage() {\n console.log('Usage: node tools/scripts/codex/task-finalize.js [--branch ] [--contract ] [--base-ref ] [--profile pay-orc-gate]');\n}\n\nfunction detectBranch(explicitBranch) {\n if (explicitBranch) return explicitBranch;\n const fromEnv = String(process.env.GITHUB_HEAD_REF || '').trim();\n if (fromEnv) return fromEnv;\n const result = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: REPO_ROOT, encoding: 'utf8' });\n return result.status === 0 ? String(result.stdout || '').trim() : '';\n}\n\nfunction main() {\n const args = parseArgs(process.argv.slice(2));\n if (args.help) {\n usage();\n process.exit(0);\n }\n\n const branch = detectBranch(args.branch);\n if (!branch) {\n throw new Error('Unable to detect branch; pass --branch explicitly');\n }\n\n const contractCheckArgs = ['tools/scripts/validate-codex-task-contract.js', '--branch', branch, '--contract', args.contractPath];\n if (args.baseRef) {\n contractCheckArgs.push('--base-ref', args.baseRef);\n }\n\n console.log('🔍 Running contract scope check...');\n if (!run('node', contractCheckArgs)) {\n throw new Error('Contract scope check failed');\n }\n\n console.log('🔍 Running local lock check...');\n if (!run('node', ['tools/scripts/codex/validate-locks.js', '--branch', branch])) {\n throw new Error('Local lock check failed');\n }\n\n if (args.profile) {\n if (args.profile === 'pay-orc-gate') {\n console.log('🔍 Running finalize profile: pay-orc-gate...');\n if (!run('bash', ['tools/scripts/verify-pay-orc-gate-finalize.sh'])) {\n throw new Error('Finalize profile pay-orc-gate failed');\n }\n } else {\n throw new Error(`Unknown finalize profile: ${args.profile}`);\n }\n }\n\n console.log('✅ Codex task finalize checks passed');\n console.log('ℹ️ Next step: node tools/scripts/codex/lock-release.js');\n}\n\nif (require.main === module) {\n try {\n main();\n } catch (error) {\n console.error(`❌ ${error.message}`);\n process.exit(1);\n }\n}\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/create-codex-pr.js",
+ "script": "create-codex-pr",
+ "category": "generator",
+ "purpose": "governance:agent-governance",
+ "scope": "tools/scripts, .codex/task-contract.yaml",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Codex PR creator — generates codex PR with correct branch naming, labels, and body template",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/create-codex-pr.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script create-codex-pr\n * @category generator\n * @purpose governance:agent-governance\n * @scope tools/scripts, .codex/task-contract.yaml\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Codex PR creator — generates codex PR with correct branch naming, labels, and body template\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/create-codex-pr.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\nconst yaml = require('js-yaml');\n\nconst DEFAULT_CONTRACT_PATH = '.codex/task-contract.yaml';\nconst DEFAULT_OUTPUT_PATH = '.codex/pr-body.generated.md';\nconst DEFAULT_BASE_BRANCH = 'docs-v2';\nconst CODEX_BRANCH_RE = /^codex\\/(\\d+)-([a-z0-9][a-z0-9-]*)$/;\nconst PR_GENERATOR_MARKER_PREFIX = 'codex-pr-body-generated';\n\nconst REPO_ROOT = getRepoRoot();\n\nfunction getRepoRoot() {\n const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n if (result.status === 0 && String(result.stdout || '').trim()) {\n return String(result.stdout || '').trim();\n }\n return process.cwd();\n}\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction parseCsv(raw) {\n if (!raw) return [];\n return [...new Set(String(raw).split(',').map((entry) => toPosix(entry.trim())).filter(Boolean))];\n}\n\nfunction usage() {\n const lines = [\n 'Usage: node tools/scripts/create-codex-pr.js [options]',\n '',\n 'Options:',\n ' --contract Task contract path (default: .codex/task-contract.yaml)',\n ' --output PR body output path (default: .codex/pr-body.generated.md)',\n ' --title Optional PR title override',\n ' --base Optional base branch override',\n ' --head Optional head branch override',\n ' --changed-files Optional explicit changed file list',\n ' --create Run gh pr create with generated body',\n ' --draft Create PR as draft (requires --create)',\n ' --dry-run Print gh command instead of executing',\n ' --json Emit JSON output',\n ' --help Show this help message'\n ];\n console.log(lines.join('\\n'));\n}\n\nfunction parseArgs(argv) {\n const args = {\n contractPath: DEFAULT_CONTRACT_PATH,\n outputPath: DEFAULT_OUTPUT_PATH,\n title: '',\n base: '',\n head: '',\n changedFiles: null,\n create: false,\n draft: false,\n dryRun: false,\n json: false\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n\n if (token === '--contract') {\n args.contractPath = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--contract=')) {\n args.contractPath = token.slice('--contract='.length).trim();\n continue;\n }\n\n if (token === '--output') {\n args.outputPath = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--output=')) {\n args.outputPath = token.slice('--output='.length).trim();\n continue;\n }\n\n if (token === '--title') {\n args.title = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--title=')) {\n args.title = token.slice('--title='.length).trim();\n continue;\n }\n\n if (token === '--base') {\n args.base = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--base=')) {\n args.base = token.slice('--base='.length).trim();\n continue;\n }\n\n if (token === '--head') {\n args.head = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--head=')) {\n args.head = token.slice('--head='.length).trim();\n continue;\n }\n\n if (token === '--changed-files') {\n args.changedFiles = parseCsv(argv[i + 1] || '');\n i += 1;\n continue;\n }\n if (token.startsWith('--changed-files=')) {\n args.changedFiles = parseCsv(token.slice('--changed-files='.length));\n continue;\n }\n\n if (token === '--create') {\n args.create = true;\n continue;\n }\n if (token === '--draft') {\n args.draft = true;\n continue;\n }\n if (token === '--dry-run') {\n args.dryRun = true;\n continue;\n }\n if (token === '--json') {\n args.json = true;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (args.draft && !args.create) {\n throw new Error('--draft requires --create');\n }\n\n return args;\n}\n\nfunction runGit(args) {\n const result = spawnSync('git', args, {\n cwd: REPO_ROOT,\n encoding: 'utf8'\n });\n if (result.status !== 0) {\n const stderr = String(result.stderr || '').trim();\n const stdout = String(result.stdout || '').trim();\n throw new Error(stderr || stdout || `git ${args.join(' ')} failed`);\n }\n return String(result.stdout || '').trim();\n}\n\nfunction tryRunGit(args) {\n try {\n return runGit(args);\n } catch (_error) {\n return '';\n }\n}\n\nfunction normalizeStringArray(value, fieldName, required = false) {\n if (value == null) {\n if (required) throw new Error(`\"${fieldName}\" is required and must be a non-empty array`);\n return [];\n }\n\n if (!Array.isArray(value)) {\n throw new Error(`\"${fieldName}\" must be an array`);\n }\n\n const normalized = value\n .map((entry) => String(entry == null ? '' : entry).trim())\n .filter(Boolean);\n\n if (required && normalized.length === 0) {\n throw new Error(`\"${fieldName}\" must include at least one entry`);\n }\n\n return normalized;\n}\n\nfunction normalizeIntegerArray(value, fieldName) {\n if (value == null) return [];\n if (!Array.isArray(value)) {\n throw new Error(`\"${fieldName}\" must be an array of integers`);\n }\n const normalized = [];\n value.forEach((entry) => {",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "codex:pr"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: codex:pr)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/cross-agent-packager.js",
+ "script": "cross-agent-packager",
+ "category": "generator",
+ "purpose": "governance:agent-governance",
+ "scope": "tools/scripts, ai-tools/ai-skills/catalog, ai-tools/agent-packs",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Cross-agent packager — bundles audit reports and repo state into agent-consumable packages",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/cross-agent-packager.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script cross-agent-packager\n * @category generator\n * @purpose governance:agent-governance\n * @scope tools/scripts, ai-tools/ai-skills/catalog, ai-tools/agent-packs\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Cross-agent packager — bundles audit reports and repo state into agent-consumable packages\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/cross-agent-packager.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst STAGE_ID = 'cross-agent-packager';\nconst REPO_ROOT = process.cwd();\nconst DEFAULT_OUTPUT_DIR = 'ai-tools/agent-packs';\nconst DEFAULT_CATALOG_PATH = 'ai-tools/ai-skills/catalog/skill-catalog.json';\nconst DEFAULT_MANIFEST_PATH = 'ai-tools/ai-skills/catalog/execution-manifest.json';\nconst VALID_PACKS = new Set(['codex', 'cursor', 'claude', 'windsurf', 'all']);\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction parseArgs(argv) {\n const out = {\n agentPack: 'all',\n outputDir: DEFAULT_OUTPUT_DIR,\n catalogPath: DEFAULT_CATALOG_PATH,\n manifestPath: DEFAULT_MANIFEST_PATH\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n\n if (token === '--agent-pack') {\n out.agentPack = String(argv[i + 1] || out.agentPack).trim() || out.agentPack;\n i += 1;\n continue;\n }\n if (token.startsWith('--agent-pack=')) {\n out.agentPack = token.slice('--agent-pack='.length).trim() || out.agentPack;\n continue;\n }\n if (token === '--output-dir') {\n out.outputDir = String(argv[i + 1] || out.outputDir).trim() || out.outputDir;\n i += 1;\n continue;\n }\n if (token.startsWith('--output-dir=')) {\n out.outputDir = token.slice('--output-dir='.length).trim() || out.outputDir;\n continue;\n }\n if (token === '--catalog') {\n out.catalogPath = String(argv[i + 1] || out.catalogPath).trim() || out.catalogPath;\n i += 1;\n continue;\n }\n if (token.startsWith('--catalog=')) {\n out.catalogPath = token.slice('--catalog='.length).trim() || out.catalogPath;\n continue;\n }\n if (token === '--manifest') {\n out.manifestPath = String(argv[i + 1] || out.manifestPath).trim() || out.manifestPath;\n i += 1;\n continue;\n }\n if (token.startsWith('--manifest=')) {\n out.manifestPath = token.slice('--manifest='.length).trim() || out.manifestPath;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (!VALID_PACKS.has(out.agentPack)) {\n throw new Error(`Invalid --agent-pack value: ${out.agentPack}`);\n }\n\n return out;\n}\n\nfunction readJson(repoPath) {\n return JSON.parse(fs.readFileSync(path.join(REPO_ROOT, repoPath), 'utf8'));\n}\n\nfunction mdEscape(value) {\n return String(value || '').replace(/\\|/g, '\\\\|').replace(/\\n/g, ' ');\n}\n\nfunction pickPipelineSkills(catalog, manifest) {\n const skillMap = new Map((catalog.skills || []).map((skill) => [skill.id, skill]));\n const pipeline = Array.isArray(manifest.pipeline) ? manifest.pipeline : [];\n\n return pipeline\n .slice()\n .sort((a, b) => Number(a.run_order || 0) - Number(b.run_order || 0))\n .map((entry) => ({\n ...entry,\n skill: skillMap.get(entry.id) || null\n }))\n .filter((entry) => entry.skill);\n}\n\nfunction buildTopReadme(pipelineSkills) {\n const lines = [];\n lines.push('# Agent Packs');\n lines.push('');\n lines.push('This folder contains generated packs for running the same docs audit pipeline across supported coding agents.');\n lines.push('');\n lines.push('## Pipeline Stages');\n lines.push('');\n lines.push('| Order | Skill ID | Goal |');\n lines.push('|---:|---|---|');\n\n pipelineSkills.forEach((entry) => {\n lines.push(`| ${entry.run_order} | \\`${mdEscape(entry.id)}\\` | ${mdEscape(entry.skill.goal)} |`);\n });\n\n lines.push('');\n lines.push('Generated by: `node tools/scripts/cross-agent-packager.js --agent-pack all`');\n lines.push('');\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction writeCodexPack(baseDirAbs, pipelineSkills, catalog, manifest) {\n const packDir = path.join(baseDirAbs, 'codex');\n ensureDir(packDir);\n\n const payload = {\n generated_at: new Date().toISOString(),\n source_catalog: DEFAULT_CATALOG_PATH,\n source_manifest: DEFAULT_MANIFEST_PATH,\n pipeline: pipelineSkills.map((entry) => ({\n run_order: entry.run_order,\n id: entry.id,\n goal: entry.skill.goal,\n commands: entry.skill.commands,\n outputs: entry.skill.outputs,\n severity_model: entry.skill.severity_model,\n autofix_mode: entry.skill.autofix_mode\n })),\n catalog_version: catalog.version,\n manifest_version: manifest.version\n };\n\n const manifestPath = path.join(packDir, 'skills-manifest.json');\n fs.writeFileSync(manifestPath, `${JSON.stringify(payload, null, 2)}\\n`);\n\n return [toPosix(path.relative(REPO_ROOT, manifestPath))];\n}\n\nfunction buildRulePackMarkdown(agentName, pipelineSkills) {\n const lines = [];\n lines.push(`# ${agentName} Repo Audit Pack`);\n lines.push('');\n lines.push('Use these stages in order for a static-first docs infrastructure audit pipeline.');\n lines.push('');\n\n pipelineSkills.forEach((entry) => {\n lines.push(`## ${entry.run_order}. ${entry.id}`);\n lines.push('');\n lines.push(`- Goal: ${entry.skill.goal}`);\n lines.push(`- Severity Model: ${entry.skill.severity_model}`);\n lines.push(`- Autofix Mode: ${entry.skill.autofix_mode}`);\n lines.push('- Commands:');\n (entry.skill.commands || []).forEach((command) => {\n lines.push(` - \\`${command.run}\\``);\n });\n lines.push('- Outputs:');\n (entry.skill.outputs || []).forEach((output) => {\n lines.push(` - \\`${output}\\``);\n });\n lines.push('');\n });\n\n lines.push('Run source: `node tools/scripts/cross-agent-packager.js --agent-pack all`');\n lines.push('');\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction writeCursorPack(baseDirAbs, pipelineSkills) {\n const packDir = path.join(baseDirAbs, 'cursor');\n ensureDir(packDir);\n\n const rulesPath = path.join(packDir, 'rules.md');\n fs.writeFileSync(rulesPath, buildRulePackMarkdown('Cursor', pipelineSkills));\n\n return [toPosix(path.relative(REPO_ROOT, rulesPath))];\n}\n\nfunction writeClaudePack(baseDirAbs, pipelineSkills) {\n const packDir = path.join(baseDirAbs, 'claude');\n ensureDir(packDir);\n\n const claudePath = path.join(packDir, 'CLAUDE.md');\n fs.writeFileSync(claudePath, buildRulePackMarkdown('Claude Code', pipelineSkills));\n\n return [toPosix(path.relative(REPO_ROOT, claudePath))];\n}\n\nfunction writeWindsurfPack(baseDirAbs, pipelineSkills) {\n const packDir = path.join(baseDirAbs, 'windsurf');\n ensureDir(packDir);\n\n const rulesPath = path.join(packDir, 'rules.md');\n fs.writeFileSync(rulesPath, buildRulePackMarkdown('Windsurf', pipelineSkills));\n\n return [toPosix(path.relative(REPO_ROOT, rulesPath))];\n}\n\nfunction main() {\n const args = parseArgs(process.argv.slice(2));",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "pack:agents"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/skills-manifest.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/scripts/rules.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/scripts/CLAUDE.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/scripts/README.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/skills-manifest.json, tools/scripts/rules.md, tools/scripts/CLAUDE.md, tools/scripts/README.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: pack:agents)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/sync-codex-skills.js",
+ "script": "sync-codex-skills",
+ "category": "automation",
+ "purpose": "governance:agent-governance",
+ "scope": "tools/scripts, ai-tools/ai-skills/templates, tests/unit/codex-skill-sync.test.js",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Codex skills sync — synchronises skill definition files between local and remote sources. Supports --check mode.",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/sync-codex-skills.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script sync-codex-skills\n * @category automation\n * @purpose governance:agent-governance\n * @scope tools/scripts, ai-tools/ai-skills/templates, tests/unit/codex-skill-sync.test.js\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Codex skills sync — synchronises skill definition files between local and remote sources. Supports --check mode.\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/sync-codex-skills.js [flags]\n */\n\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst yaml = require('js-yaml');\n\nconst REPO_ROOT = process.cwd();\nconst DEFAULT_SOURCE_DIR = 'ai-tools/ai-skills/templates';\nconst TEMPLATE_SUFFIX = '.template.md';\nconst TEMPLATE_FILE_RE = /^\\d{2}-[a-z0-9-]+\\.template\\.md$/;\nconst SKILL_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;\n\nconst REQUIRED_FRONTMATTER_KEYS = [\n 'name',\n 'description',\n 'tier',\n 'triggers',\n 'primary_paths',\n 'primary_commands'\n];\n\nconst REQUIRED_SECTIONS = [\n 'SKILL:',\n 'Goal',\n 'Constraints',\n 'Workflow',\n 'Deliverable Format',\n 'Failure Modes / Fallback',\n 'Validation Checklist'\n];\n\nconst ACRONYMS = new Set(['GH', 'MCP', 'API', 'CI', 'CLI', 'LLM', 'PDF', 'PR', 'UI', 'URL', 'SQL', 'SEO', 'MDX', 'WCAG', 'LPD', 'N8N']);\nconst BRANDS = new Map([\n ['openai', 'OpenAI'],\n ['openapi', 'OpenAPI'],\n ['github', 'GitHub'],\n ['mintlify', 'Mintlify'],\n ['codex', 'Codex'],\n ['cspell', 'cspell']\n]);\nconst SMALL_WORDS = new Set(['and', 'or', 'to', 'up', 'with', 'of', 'for']);\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction usage() {\n const msg = [\n 'Usage: node tools/scripts/sync-codex-skills.js [options]',\n '',\n 'Options:',\n ' --source-dir Canonical template directory',\n ' --dest Destination Codex skills directory',\n ' --check Validation-only mode (no writes; fails on drift)',\n ' --dry-run Print planned changes without writing',\n ' --skills Optional subset by template frontmatter name',\n ' --openai-yaml Generate/open update agents/openai.yaml (default)',\n ' --no-openai-yaml Skip agents/openai.yaml generation',\n ' --help Show this message'\n ];\n console.log(msg.join('\\n'));\n}\n\nfunction parseSkillsList(raw) {\n if (!raw) return null;\n const values = String(raw)\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean);\n if (values.length === 0) return null;\n return [...new Set(values)];\n}\n\nfunction resolveDefaultDest() {\n const codexHome = String(process.env.CODEX_HOME || '').trim();\n if (codexHome) return path.join(codexHome, 'skills');\n return path.join(os.homedir(), '.codex', 'skills');\n}\n\nfunction parseArgs(argv) {\n const out = {\n sourceDir: path.resolve(REPO_ROOT, DEFAULT_SOURCE_DIR),\n dest: path.resolve(resolveDefaultDest()),\n check: false,\n dryRun: false,\n skills: null,\n openaiYaml: true\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--help' || token === '-h') {\n out.help = true;\n continue;\n }\n\n if (token === '--source-dir') {\n out.sourceDir = path.resolve(REPO_ROOT, String(argv[i + 1] || '').trim());\n i += 1;\n continue;\n }\n if (token.startsWith('--source-dir=')) {\n out.sourceDir = path.resolve(REPO_ROOT, token.slice('--source-dir='.length).trim());\n continue;\n }\n\n if (token === '--dest') {\n out.dest = path.resolve(REPO_ROOT, String(argv[i + 1] || '').trim());\n i += 1;\n continue;\n }\n if (token.startsWith('--dest=')) {\n out.dest = path.resolve(REPO_ROOT, token.slice('--dest='.length).trim());\n continue;\n }\n\n if (token === '--skills') {\n out.skills = parseSkillsList(argv[i + 1] || '');\n i += 1;\n continue;\n }\n if (token.startsWith('--skills=')) {\n out.skills = parseSkillsList(token.slice('--skills='.length));\n continue;\n }\n\n if (token === '--check') {\n out.check = true;\n continue;\n }\n if (token === '--dry-run') {\n out.dryRun = true;\n continue;\n }\n if (token === '--openai-yaml') {\n out.openaiYaml = true;\n continue;\n }\n if (token === '--no-openai-yaml') {\n out.openaiYaml = false;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n return out;\n}\n\nfunction splitFrontmatter(content, filePath) {\n const match = content.match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n/);\n if (!match) {\n throw new Error(`${toPosix(filePath)}: missing or invalid YAML frontmatter`);\n }\n return {\n frontmatterRaw: match[1],\n body: content.slice(match[0].length)\n };\n}\n\nfunction escapeRegExp(value) {\n return String(value || '').replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction requireSection(body, sectionTitle, filePath) {\n const re = new RegExp(`^${escapeRegExp(sectionTitle)}(?:\\\\b|\\\\s|:)`, 'm');\n if (!re.test(body)) {\n throw new Error(`${toPosix(filePath)}: missing required section \"${sectionTitle}\"`);\n }\n}\n\nfunction assertArrayCount(value, minCount, field, filePath) {\n if (!Array.isArray(value)) {\n throw new Error(`${toPosix(filePath)}: frontmatter \"${field}\" must be an array`);\n }\n if (value.length < minCount) {\n throw new Error(`${toPosix(filePath)}: frontmatter \"${field}\" must have at least ${minCount} entries`);\n }\n}\n\nfunction parseTemplateFile(filePathAbs) {\n const content = fs.readFileSync(filePathAbs, 'utf8');\n const { frontmatterRaw, body } = splitFrontmatter(content, filePathAbs);\n let frontmatter;\n\n try {\n frontmatter = yaml.load(frontmatterRaw);\n } catch (error) {\n throw new Error(`${toPosix(filePathAbs)}: invalid frontmatter YAML (${error.message})`);\n }\n\n if (!frontmatter || typeof frontmatter !== 'object' || Array.isArray(frontmatter)) {\n throw new Error(`${toPosix(filePathAbs)}: frontmatter must be a YAML object`);\n }\n\n for (const key of REQUIRED_FRONTMATTER_KEYS) {\n if (!(key in frontmatter)) {\n throw new Error(`${toPosix(filePathAbs)}: missing required frontmatter key \"${key}\"`);\n }\n }\n\n const name = String(frontmatter.name || '').trim();\n if (!name) {\n throw new Error(`${toPosix(filePathAbs)}: frontmatter \"name\" must be non-empty`);\n }\n if (!SKILL_NAME_RE.test(name)) {\n throw new Error(`${toPosix(filePathAbs)}: frontmatter \"name\" must match ${SKILL_NAME_RE}`);\n }",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "skills:sync:codex"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "skills:sync:codex:check"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/SKILL.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/SKILL.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: skills:sync:codex); manual (npm script: skills:sync:codex:check)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/validators/governance/check-agent-docs-freshness.js",
+ "script": "check-agent-docs-freshness",
+ "category": "validator",
+ "purpose": "governance:agent-governance",
+ "scope": "tools/scripts/validators/governance, .github, ai-tools/ai-skills, contribute",
+ "owner": "docs",
+ "needs": "R-R14, R-R18",
+ "purpose_statement": "Validates that required agent governance docs exist and have been touched within a freshness threshold",
+ "pipeline_declared": "manual, ci",
+ "usage": "node tools/scripts/validators/governance/check-agent-docs-freshness.js [--threshold ] [--json]",
+ "header": "#!/usr/bin/env node\n/**\n * @script check-agent-docs-freshness\n * @category validator\n * @purpose governance:agent-governance\n * @scope tools/scripts/validators/governance, .github, ai-tools/ai-skills, contribute\n * @owner docs\n * @needs R-R14, R-R18\n * @purpose-statement Validates that required agent governance docs exist and have been touched within a freshness threshold\n * @pipeline manual, ci\n * @usage node tools/scripts/validators/governance/check-agent-docs-freshness.js [--threshold ] [--json]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst DEFAULT_THRESHOLD_DAYS = 90;\nconst REPO_ROOT = getRepoRoot();\n\nfunction getRepoRoot() {\n const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n if (result.status === 0 && String(result.stdout || '').trim()) {\n return String(result.stdout || '').trim();\n }\n return process.cwd();\n}\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction usage() {\n console.log(\n 'Usage: node tools/scripts/validators/governance/check-agent-docs-freshness.js [--threshold ] [--json]'\n );\n}\n\nfunction parseArgs(argv) {\n const args = {\n thresholdDays: DEFAULT_THRESHOLD_DAYS,\n json: false\n };\n\n for (let index = 0; index < argv.length; index += 1) {\n const token = argv[index];\n\n if (token === '--threshold') {\n args.thresholdDays = Number(argv[index + 1]);\n index += 1;\n continue;\n }\n\n if (token.startsWith('--threshold=')) {\n args.thresholdDays = Number(token.slice('--threshold='.length));\n continue;\n }\n\n if (token === '--json') {\n args.json = true;\n continue;\n }\n\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (!Number.isFinite(args.thresholdDays) || args.thresholdDays < 0) {\n throw new Error('Threshold must be a non-negative number of days');\n }\n\n return args;\n}\n\nfunction runGit(args) {\n const result = spawnSync('git', args, { cwd: REPO_ROOT, encoding: 'utf8' });\n if (result.status !== 0) {\n return '';\n }\n return String(result.stdout || '').trim();\n}\n\nfunction getGitTimestamp(repoPath) {\n const output = runGit(['log', '-1', '--format=%ct', '--', repoPath]);\n if (!output) return null;\n const timestamp = Number(output);\n return Number.isFinite(timestamp) ? timestamp : null;\n}\n\nfunction resolveFirstExisting(candidates) {\n for (const candidate of candidates) {\n if (fs.existsSync(path.join(REPO_ROOT, candidate))) {\n return candidate;\n }\n }\n return '';\n}\n\nfunction getSkillDocs() {\n const skillsDir = path.join(REPO_ROOT, 'ai-tools', 'ai-skills');\n if (!fs.existsSync(skillsDir)) return [];\n\n return fs\n .readdirSync(skillsDir, { withFileTypes: true })\n .filter((entry) => entry.isFile() && /\\.md$/i.test(entry.name))\n .map((entry) => toPosix(path.join('ai-tools/ai-skills', entry.name)))\n .sort((left, right) => left.localeCompare(right));\n}\n\nfunction buildRequiredEntries() {\n const required = [\n {\n label: 'AGENTS',\n candidates: ['AGENTS.md', 'ai-tools/ai-rules/AGENTS.md', '.github/AGENTS.md']\n },\n {\n label: 'ASSISTANT',\n candidates: ['ASSISTANT.md']\n },\n {\n label: 'AGENT-INSTRUCTIONS',\n candidates: ['AGENT-INSTRUCTIONS.md', 'contribute/CONTRIBUTING/AGENT-INSTRUCTIONS.md']\n }\n ];\n\n const records = required.map((entry) => ({\n label: entry.label,\n candidates: entry.candidates,\n resolvedPath: resolveFirstExisting(entry.candidates)\n }));\n\n const skillDocs = getSkillDocs();\n if (skillDocs.length === 0) {\n records.push({\n label: 'AI-SKILLS-TOPLEVEL',\n candidates: ['ai-tools/ai-skills/*.md'],\n resolvedPath: ''\n });\n } else {\n skillDocs.forEach((skillDoc) => {\n records.push({\n label: `AI-SKILL:${path.basename(skillDoc)}`,\n candidates: [skillDoc],\n resolvedPath: skillDoc\n });\n });\n }\n\n return records;\n}\n\nfunction buildRecord(entry, thresholdDays) {\n if (!entry.resolvedPath) {\n return {\n label: entry.label,\n candidates: entry.candidates,\n path: '',\n status: 'ERROR',\n ageDays: null,\n lastTouched: '',\n message: `Missing required file. Checked: ${entry.candidates.join(', ')}`\n };\n }\n\n const timestamp = getGitTimestamp(entry.resolvedPath);\n if (!timestamp) {\n return {\n label: entry.label,\n candidates: entry.candidates,\n path: entry.resolvedPath,\n status: 'ERROR',\n ageDays: null,\n lastTouched: '',\n message: 'File exists but has no git history'\n };\n }\n\n const ageDays = Math.floor((Date.now() - timestamp * 1000) / 86400000);\n const lastTouched = new Date(timestamp * 1000).toISOString().slice(0, 10);\n const status = ageDays > thresholdDays ? 'WARNING' : 'INFO';\n const message =\n status === 'WARNING'\n ? `Last updated ${ageDays} days ago`\n : `Touched within ${thresholdDays}-day threshold`;\n\n return {\n label: entry.label,\n candidates: entry.candidates,\n path: entry.resolvedPath,\n status,\n ageDays,\n lastTouched,\n message\n };\n}\n\nfunction summarize(records) {\n const summary = {\n info: 0,\n warnings: 0,\n errors: 0,\n total: records.length,\n thresholdDays: 0\n };\n\n records.forEach((record) => {\n if (record.status === 'INFO') summary.info += 1;\n if (record.status === 'WARNING') summary.warnings += 1;\n if (record.status === 'ERROR') summary.errors += 1;\n });\n\n return summary;\n}\n\nfunction writeHumanReport(records, summary) {\n records.forEach((record) => {",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/generate-content-gap-reconciliation.js",
+ "script": "generate-content-gap-reconciliation",
+ "category": "generator",
+ "purpose": "governance:index-management",
+ "scope": "tools/scripts, tools/config, tools/lib, v2, tasks/reports/content-gap",
+ "owner": "docs",
+ "needs": "R-R16, R-R17",
+ "purpose_statement": "Content-gap reconciliation generator — compares blueprint coverage against v2 MDX and writes reconciliation artefacts",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tools/scripts/generate-content-gap-reconciliation.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script generate-content-gap-reconciliation\n * @category generator\n * @purpose governance:index-management\n * @scope tools/scripts, tools/config, tools/lib, v2, tasks/reports/content-gap\n * @owner docs\n * @needs R-R16, R-R17\n * @purpose-statement Content-gap reconciliation generator — compares blueprint coverage against v2 MDX and writes reconciliation artefacts\n * @pipeline manual — not yet in pipeline\n * @usage node tools/scripts/generate-content-gap-reconciliation.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst {\n extractFrontmatter,\n stripForWordCount,\n countWords,\n normalizeRel\n} = require('../lib/docs-index-utils');\n\nconst REPO_ROOT = process.cwd();\n\nconst DEFAULTS = {\n blueprintPagesPath: 'tools/config/blueprint-pages.json',\n blueprintMappingPath: 'tools/config/blueprint-mapping.json',\n v2Root: 'v2',\n outDir: 'tasks/reports/content-gap'\n};\n\nconst CSV_COLUMNS = [\n 'source',\n 'tab',\n 'position',\n 'blueprint_name',\n 'blueprint_type',\n 'blueprint_phase',\n 'repo_file',\n 'status',\n 'word_count',\n 'has_title',\n 'has_description',\n 'brand_violations',\n 'decision',\n 'notes'\n];\n\nconst DECISION_OPTIONS_COMMENT = '# decision options: KEEP, CREATE, EXPAND, MERGE, RENAME, DELETE, ADD_TO_BLUEPRINT';\n\nconst EXCLUDED_REPO_ONLY_SEGMENTS = new Set([\n 'cn',\n 'es',\n 'fr',\n 'x-deprecated',\n 'x-experimental',\n 'x-notes',\n 'views',\n 'groups'\n]);\n\nconst TEMPLATE_EXPANSIONS = {\n '[Platform]': ['Studio', 'Daydream'],\n '[Provider]': ['Cloud SPE', 'Daydream', 'Studio']\n};\n\nconst REPO_TAB_MAP = {\n home: 'HOME',\n developers: 'DEVELOPERS',\n solutions: 'SOLUTIONS',\n gateways: 'GATEWAYS',\n orchestrators: 'GPU NODES',\n lpt: 'LP TOKEN',\n community: 'COMMUNITY',\n about: 'ABOUT',\n resources: 'RESOURCE HUB',\n internal: 'INTERNAL'\n};\n\nconst BANNED_WORD_PATTERNS = [\n { label: 'simply', regex: /\\bsimply\\b/i },\n { label: 'just', regex: /\\bjust\\b/i },\n { label: 'easily', regex: /\\beasily\\b/i },\n { label: 'utilize', regex: /\\butilize\\b/i },\n { label: 'leverage', regex: /\\bleverage\\b/i },\n { label: 'seamless', regex: /\\bseamless\\b/i },\n { label: 'best-in-class', regex: /\\bbest[\\s-]in[\\s-]class\\b/i }\n];\n\nconst DEPRECATED_WORD_PATTERNS = [\n { label: 'broadcaster->gateway', regex: /\\bbroadcasters?\\b/i },\n { label: 'miner->orchestrator', regex: /\\bminers?\\b/i }\n];\n\nconst PLACEHOLDER_PATTERN = /Coming Soon|\\bTODO\\b|\\bTBD\\b|PreviewCallout/i;\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction parseArgs(argv) {\n const out = { ...DEFAULTS };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--help' || token === '-h') {\n printUsage();\n process.exit(0);\n }\n\n if (token === '--blueprint-pages') {\n out.blueprintPagesPath = String(argv[i + 1] || out.blueprintPagesPath).trim() || out.blueprintPagesPath;\n i += 1;\n continue;\n }\n if (token.startsWith('--blueprint-pages=')) {\n out.blueprintPagesPath = token.slice('--blueprint-pages='.length).trim() || out.blueprintPagesPath;\n continue;\n }\n\n if (token === '--blueprint-mapping') {\n out.blueprintMappingPath = String(argv[i + 1] || out.blueprintMappingPath).trim() || out.blueprintMappingPath;\n i += 1;\n continue;\n }\n if (token.startsWith('--blueprint-mapping=')) {\n out.blueprintMappingPath = token.slice('--blueprint-mapping='.length).trim() || out.blueprintMappingPath;\n continue;\n }\n\n if (token === '--v2-root') {\n out.v2Root = String(argv[i + 1] || out.v2Root).trim() || out.v2Root;\n i += 1;\n continue;\n }\n if (token.startsWith('--v2-root=')) {\n out.v2Root = token.slice('--v2-root='.length).trim() || out.v2Root;\n continue;\n }\n\n if (token === '--out-dir') {\n out.outDir = String(argv[i + 1] || out.outDir).trim() || out.outDir;\n i += 1;\n continue;\n }\n if (token.startsWith('--out-dir=')) {\n out.outDir = token.slice('--out-dir='.length).trim() || out.outDir;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n return out;\n}\n\nfunction printUsage() {\n console.log('Usage: node tools/scripts/generate-content-gap-reconciliation.js [options]');\n console.log('');\n console.log('Options:');\n console.log(` --blueprint-pages Default: ${DEFAULTS.blueprintPagesPath}`);\n console.log(` --blueprint-mapping Default: ${DEFAULTS.blueprintMappingPath}`);\n console.log(` --v2-root Default: ${DEFAULTS.v2Root}`);\n console.log(` --out-dir Default: ${DEFAULTS.outDir}`);\n}\n\nfunction readJsonFile(repoPath) {\n const absPath = path.resolve(REPO_ROOT, repoPath);\n const raw = fs.readFileSync(absPath, 'utf8');\n return JSON.parse(raw);\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction expandBlueprintName(name) {\n let expanded = [String(name || '')];\n for (const [token, replacements] of Object.entries(TEMPLATE_EXPANSIONS)) {\n const next = [];\n for (const value of expanded) {\n if (!value.includes(token)) {\n next.push(value);\n continue;\n }\n replacements.forEach((replacement) => {\n next.push(value.replaceAll(token, replacement));\n });\n }\n expanded = next;\n }\n return expanded;\n}\n\nfunction buildMappedFileSet(blueprintMapping) {\n const mapped = new Set();\n Object.values(blueprintMapping).forEach((tabMap) => {\n if (!tabMap || typeof tabMap !== 'object') return;\n Object.values(tabMap).forEach((repoFile) => {\n if (!repoFile) return;\n mapped.add(normalizeRel(repoFile));\n });\n });\n return mapped;\n}\n\nfunction cleanTextForWordCount(body) {\n return String(stripForWordCount(body))\n .replace(/^export\\s.+$/gm, ' ')\n .replace(//g, ' ')\n .replace(/\\{[^{}]*\\}/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction findBrandViolations(rawContent) {\n const violations = [];\n for (const entry of BANNED_WORD_PATTERNS) {\n if (entry.regex.test(rawContent)) violations.push(`banned:${entry.label}`);\n }",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/reconciliation-summary.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/reconciliation-summary.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/generate-docs-guide-components-index.js",
+ "script": "generate-docs-guide-components-index",
+ "category": "generator",
+ "purpose": "governance:index-management",
+ "scope": "generated-output",
+ "owner": "docs",
+ "needs": "R-R10, R-R16, R-R17",
+ "purpose_statement": "Generates docs-guide and published component overview indexes from the governed component registry.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/scripts/generate-docs-guide-components-index.js [--write|--check]",
+ "header": "#!/usr/bin/env node\n/**\n * @script generate-docs-guide-components-index\n * @category generator\n * @purpose governance:index-management\n * @scope generated-output\n * @owner docs\n * @needs R-R10, R-R16, R-R17\n * @purpose-statement Generates docs-guide and published component overview indexes from the governed component registry.\n * @pipeline manual\n * @usage node tools/scripts/generate-docs-guide-components-index.js [--write|--check]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { VALID_CATEGORIES } = require('../lib/component-governance-utils');\nconst { buildRegistry } = require('./generate-component-registry');\nconst { renderOverviewPage } = require('./generate-component-docs');\nconst {\n buildGeneratedFrontmatterLines,\n buildGeneratedHiddenBannerLines,\n buildGeneratedNoteLines\n} = require('../lib/generated-file-banners');\n\nconst REPO_ROOT = path.resolve(__dirname, '..', '..');\nconst OUTPUT_PATHS = [\n 'docs-guide/indexes/components-index.mdx',\n 'v2/resources/documentation-guide/component-library/overview.mdx'\n];\n\nconst CATEGORY_LABELS = {\n primitives: 'Primitives',\n layout: 'Layout',\n content: 'Content',\n data: 'Data',\n 'page-structure': 'Page Structure'\n};\n\nconst CATEGORY_DESCRIPTIONS = {\n primitives: 'Standalone visual atoms reused across authored docs.',\n layout: 'Arrangement and grouping components that shape flow and spacing.',\n content: 'Reader-facing renderers for code, media, response fields, and structured content.',\n data: 'Components bound to feeds, release metadata, or external datasets.',\n 'page-structure': 'Portal and frame-mode scaffolding for whole sections or hero treatments.'\n};\n\nfunction usage() {\n console.log(\n [\n 'Usage: node tools/scripts/generate-docs-guide-components-index.js [options]',\n '',\n 'Options:',\n ' --write Write generated output files',\n ' --check Verify generated output files are current',\n ' --help, -h Show this help message'\n ].join('\\n')\n );\n}\n\nfunction parseArgs(argv) {\n const args = {\n write: false,\n check: false,\n help: false\n };\n\n argv.forEach((token) => {\n if (token === '--write') {\n args.write = true;\n return;\n }\n if (token === '--check') {\n args.check = true;\n return;\n }\n if (token === '--help' || token === '-h') {\n args.help = true;\n return;\n }\n throw new Error(`Unknown argument: ${token}`);\n });\n\n if (!args.write && !args.check) {\n args.write = true;\n }\n\n return args;\n}\n\nfunction normalizeFileContent(content) {\n return `${String(content || '').trim()}\\n`;\n}\n\nfunction renderComponentsIndex(registry) {\n const details = {\n script: 'tools/scripts/generate-docs-guide-components-index.js',\n purpose: 'Aggregate inventory of governed component exports from snippets/components for docs-guide maintenance.',\n runWhen: 'Governed component files, exported signatures, or registry metadata change.',\n runCommand: 'node tools/scripts/generate-docs-guide-components-index.js --write'\n };\n\n const summaryLines = [\n '| Category | Exports | Primary purpose |',\n '| --- | --- | --- |'\n ];\n\n VALID_CATEGORIES.forEach((category) => {\n summaryLines.push(\n `| [${CATEGORY_LABELS[category]}](#${category}) | ${registry.categories[category].count} | ${CATEGORY_DESCRIPTIONS[category]} |`\n );\n });\n\n const sections = VALID_CATEGORIES.map((category) => {\n const components = registry.components.filter((component) => component.category === category);\n const table = [\n '| Component | Status | Tier | File |',\n '| --- | --- | --- | --- |'\n ];\n\n components.forEach((component) => {\n table.push(\n `| ${component.name} | \\`${component.status}\\` | \\`${component.tier}\\` | \\`/${component.file}\\` |`\n );\n });\n\n return [\n `## ${category}`,\n '',\n CATEGORY_DESCRIPTIONS[category],\n '',\n table.join('\\n')\n ].join('\\n');\n });\n\n return normalizeFileContent(\n [\n ...buildGeneratedFrontmatterLines({\n title: 'Components Index',\n sidebarTitle: 'Components Index',\n description: 'Aggregate inventory of governed component exports from snippets/components.',\n pageType: 'overview',\n keywords: ['livepeer', 'components index', 'snippets', 'registry', 'inventory']\n }),\n '',\n ...buildGeneratedHiddenBannerLines(details),\n '',\n ...buildGeneratedNoteLines(details),\n '',\n `The governed component library currently exposes **${registry._meta.componentCount}** named export(s).`,\n '',\n '## Category Summary',\n '',\n summaryLines.join('\\n'),\n '',\n ...sections.flatMap((section, index) => (index === 0 ? [section] : ['', section]))\n ].join('\\n')\n );\n}\n\nfunction buildOutputs() {\n const { registry, issues } = buildRegistry();\n if (issues.length > 0) {\n throw new Error(issues.join('\\n'));\n }\n\n return new Map([\n ['docs-guide/indexes/components-index.mdx', renderComponentsIndex(registry)],\n ['v2/resources/documentation-guide/component-library/overview.mdx', normalizeFileContent(renderOverviewPage(registry))]\n ]);\n}\n\nfunction writeOutputs(outputs) {\n outputs.forEach((content, relativePath) => {\n const absolutePath = path.join(REPO_ROOT, relativePath);\n fs.mkdirSync(path.dirname(absolutePath), { recursive: true });\n fs.writeFileSync(absolutePath, content, 'utf8');\n });\n}\n\nfunction checkOutputs(outputs) {\n const stale = [];\n\n outputs.forEach((expected, relativePath) => {\n const absolutePath = path.join(REPO_ROOT, relativePath);\n const actual = fs.existsSync(absolutePath) ? fs.readFileSync(absolutePath, 'utf8') : '';\n if (normalizeFileContent(actual) !== expected) {\n stale.push(relativePath);\n }\n });\n\n return stale;\n}\n\nfunction run(argv = process.argv.slice(2)) {\n let args;\n try {\n args = parseArgs(argv);\n } catch (error) {\n console.error(`❌ ${error.message}`);\n usage();\n return 1;\n }\n\n if (args.help) {\n usage();\n return 0;\n }\n\n let outputs;\n try {\n outputs = buildOutputs();\n } catch (error) {\n console.error(`❌ ${error.message}`);\n return 1;\n }\n\n if (args.check) {\n const stale = checkOutputs(outputs);\n if (stale.length > 0) {\n stale.forEach((filePath) => console.error(`Components index is out of date: ${filePath}`));",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/generate-docs-guide-indexes.js",
+ "script": "generate-docs-guide-indexes",
+ "category": "generator",
+ "purpose": "governance:index-management",
+ "scope": "tools/scripts, docs-guide, .github/workflows, .github/ISSUE_TEMPLATE",
+ "owner": "docs",
+ "needs": "R-R16, R-R17",
+ "purpose_statement": "Generates docs-guide workflow/template indexes and optionally verifies freshness",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/generate-docs-guide-indexes.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script generate-docs-guide-indexes\n * @category generator\n * @purpose governance:index-management\n * @scope tools/scripts, docs-guide, .github/workflows, .github/ISSUE_TEMPLATE\n * @owner docs\n * @needs R-R16, R-R17\n * @purpose-statement Generates docs-guide workflow/template indexes and optionally verifies freshness\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @dualmode dual-mode (document flags)\n * @usage node tools/scripts/generate-docs-guide-indexes.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst {\n buildGeneratedFrontmatterLines,\n buildGeneratedHiddenBannerLines,\n buildGeneratedNoteLines\n} = require('../lib/generated-file-banners');\n\nlet yaml = null;\ntry {\n yaml = require('js-yaml');\n} catch (_err) {\n yaml = null;\n}\n\nconst REPO_ROOT = process.cwd();\nconst WORKFLOWS_DIR = '.github/workflows';\nconst ISSUE_TEMPLATE_DIR = '.github/ISSUE_TEMPLATE';\nconst PR_TEMPLATE_FILES = ['.github/pull-request-template-v2.md', '.github/pull_request_template.md'];\n\nconst OUTPUT_FILES = {\n workflows: 'docs-guide/indexes/workflows-index.mdx',\n templates: 'docs-guide/indexes/templates-index.mdx'\n};\n\nconst LEGACY_OUTPUT_FILES = {\n workflows: 'docs-guide/indexes/workflows-index.md',\n templates: 'docs-guide/indexes/templates-index.md'\n};\n\nconst WORKFLOWS_INDEX_FRONTMATTER_LINES = buildGeneratedFrontmatterLines({\n title: 'Workflows Index',\n sidebarTitle: 'Workflows Index',\n description: 'Aggregate inventory of repository GitHub workflows',\n keywords: ['livepeer', 'workflows index', 'aggregate inventory', 'repository', 'github', 'workflows'],\n keywordsStyle: 'multiline'\n});\n\nconst TEMPLATES_INDEX_FRONTMATTER_LINES = buildGeneratedFrontmatterLines({\n title: 'Templates Index',\n sidebarTitle: 'Templates Index',\n description: 'Aggregate inventory of repository templates',\n keywords: ['livepeer', 'templates index', 'aggregate inventory', 'repository', 'templates'],\n keywordsStyle: 'multiline'\n});\n\nfunction normalizeRepoPath(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction readFileSafe(repoPath) {\n try {\n return fs.readFileSync(path.join(REPO_ROOT, repoPath), 'utf8');\n } catch (_err) {\n return '';\n }\n}\n\nfunction fileExists(repoPath) {\n const full = path.join(REPO_ROOT, repoPath);\n return fs.existsSync(full) && fs.statSync(full).isFile();\n}\n\nfunction listFiles(dirRepoPath, predicate) {\n const full = path.join(REPO_ROOT, dirRepoPath);\n if (!fs.existsSync(full) || !fs.statSync(full).isDirectory()) return [];\n\n return fs\n .readdirSync(full)\n .filter((name) => (predicate ? predicate(name) : true))\n .map((name) => normalizeRepoPath(path.join(dirRepoPath, name)))\n .sort();\n}\n\nfunction parseYaml(content) {\n if (!yaml) return null;\n try {\n return yaml.load(content);\n } catch (_err) {\n return null;\n }\n}\n\nfunction mdEscape(value) {\n return String(value || '').replace(/\\|/g, '\\\\|').replace(/\\n/g, ' ');\n}\n\nfunction inferWorkflowTriggers(doc, content) {\n const tags = [];\n const triggerMap = {\n pull_request: 'pull_request',\n push: 'push',\n schedule: 'schedule',\n workflow_dispatch: 'workflow_dispatch',\n repository_dispatch: 'repository_dispatch'\n };\n\n const onValue = doc && Object.prototype.hasOwnProperty.call(doc, 'on') ? doc.on : null;\n\n if (typeof onValue === 'string' && triggerMap[onValue]) {\n tags.push(triggerMap[onValue]);\n } else if (Array.isArray(onValue)) {\n onValue.forEach((name) => {\n if (triggerMap[name]) tags.push(triggerMap[name]);\n });\n } else if (onValue && typeof onValue === 'object') {\n Object.keys(onValue).forEach((name) => {\n if (triggerMap[name]) tags.push(triggerMap[name]);\n });\n }\n\n if (tags.length === 0) {\n const raw = String(content || '');\n Object.keys(triggerMap).forEach((key) => {\n const re = new RegExp(`\\\\b${key}\\\\b`);\n if (re.test(raw)) tags.push(triggerMap[key]);\n });\n }\n\n return [...new Set(tags)].sort();\n}\n\nfunction inferWorkflowOutputs(content) {\n const outputs = [];\n const raw = String(content || '');\n\n if (/upload-artifact/i.test(raw)) outputs.push('artifact upload');\n if (/GITHUB_STEP_SUMMARY/.test(raw)) outputs.push('step summary');\n if (/gh\\s+pr\\s+comment|pull-requests:\\s*write/i.test(raw)) outputs.push('PR comments/metadata');\n if (/git\\s+push/i.test(raw)) outputs.push('repository commits');\n\n if (outputs.length === 0) outputs.push('workflow logs');\n return outputs.join(', ');\n}\n\nfunction inferWorkflowBlocking(content) {\n const raw = String(content || '');\n if (/continue-on-error:\\s*true/.test(raw)) {\n return 'advisory or partial-advisory';\n }\n return 'blocking by default (subject to branch protection)';\n}\n\nfunction firstSentence(value) {\n const text = String(value || '').replace(/\\s+/g, ' ').trim();\n if (!text) return '';\n const idx = text.indexOf('. ');\n if (idx > 0) return text.slice(0, idx + 1).trim();\n return text;\n}\n\nfunction inferWorkflowPurpose(doc, workflowPath) {\n const name = doc && doc.name ? String(doc.name).trim() : path.basename(workflowPath);\n return firstSentence(name) || `Workflow for ${workflowPath}.`;\n}\n\nfunction buildWorkflowsIndex() {\n const workflowFiles = listFiles(WORKFLOWS_DIR, (name) => /\\.(yml|yaml)$/i.test(name));\n\n const rows = workflowFiles.map((workflowPath) => {\n const content = readFileSafe(workflowPath);\n const doc = parseYaml(content) || {};\n const workflowName = String(doc.name || path.basename(workflowPath));\n const triggers = inferWorkflowTriggers(doc, content);\n const purpose = inferWorkflowPurpose(doc, workflowPath);\n const outputs = inferWorkflowOutputs(content);\n const blocking = inferWorkflowBlocking(content);\n\n return {\n workflowName,\n workflowPath,\n triggers: triggers.length ? triggers.join(', ') : 'unknown',\n purpose,\n blocking,\n outputs,\n owner: 'docs'\n };\n });\n\n const lines = [];\n const bannerDetails = {\n script: 'tools/scripts/generate-docs-guide-indexes.js',\n purpose: 'Workflow inventory for docs-guide maintenance.',\n runWhen: 'GitHub workflows are added, removed, or changed.',\n runCommand: 'node tools/scripts/generate-docs-guide-indexes.js --write'\n };\n WORKFLOWS_INDEX_FRONTMATTER_LINES.forEach((line) => lines.push(line));\n lines.push('');\n buildGeneratedHiddenBannerLines(bannerDetails).forEach((line) => lines.push(line));\n lines.push('');\n buildGeneratedNoteLines(bannerDetails).forEach((line) => lines.push(line));\n lines.push('');\n lines.push('| Workflow | File | Triggers | Purpose | Blocking Policy | Outputs | Owner |');\n lines.push('|---|---|---|---|---|---|---|');\n\n rows.forEach((row) => {\n lines.push(\n `| ${mdEscape(row.workflowName)} | \\`${mdEscape(row.workflowPath)}\\` | ${mdEscape(row.triggers)} | ${mdEscape(row.purpose)} | ${mdEscape(row.blocking)} | ${mdEscape(row.outputs)} | ${mdEscape(row.owner)} |`\n );\n });\n\n lines.push('');\n return `${lines.join('\\n')}`;\n}\n\nfunction inferTemplatePurposeFromMarkdown(content, fallback) {",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "docs-guide:indexes"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: docs-guide:indexes)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/generate-docs-guide-pages-index.js",
+ "script": "generate-docs-guide-pages-index",
+ "category": "generator",
+ "purpose": "governance:index-management",
+ "scope": "tools/scripts, docs-guide/indexes/pages-index.mdx, v2/index.mdx, docs.json",
+ "owner": "docs",
+ "needs": "R-R16, R-R17",
+ "purpose_statement": "Generates the docs-guide pages index",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/generate-docs-guide-pages-index.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script generate-docs-guide-pages-index\n * @category generator\n * @purpose governance:index-management\n * @scope tools/scripts, docs-guide/indexes/pages-index.mdx, v2/index.mdx, docs.json\n * @owner docs\n * @needs R-R16, R-R17\n * @purpose-statement Generates the docs-guide pages index\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/generate-docs-guide-pages-index.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst {\n buildGeneratedFrontmatterLines,\n buildGeneratedHiddenBannerLines,\n buildGeneratedNoteLines\n} = require('../lib/generated-file-banners');\n\nconst REPO_ROOT = process.cwd();\nconst SOURCE_INDEX_PATH = 'v2/index.mdx';\nconst DOCS_JSON_PATH = 'docs.json';\nconst OUTPUT_PATH = 'docs-guide/indexes/pages-index.mdx';\n\nconst FRONTMATTER_LINES = buildGeneratedFrontmatterLines({\n title: 'Pages Index',\n sidebarTitle: 'Pages Index',\n description: 'Tree inventory of docs pages included in docs.json navigation, generated from v2 index data.',\n keywords: ['livepeer', 'pages index', 'tree', 'docs.json', 'v2']\n});\n\nconst GENERATED_DETAILS = {\n script: 'tools/scripts/generate-docs-guide-pages-index.js',\n purpose: 'Tree inventory of docs pages included in docs.json navigation, generated from v2 index data.',\n runWhen: '`v2/index.mdx` links or docs.json navigation entries change.',\n runCommand: 'node tools/scripts/generate-docs-guide-pages-index.js --write'\n};\n\nfunction normalizeRepoPath(value) {\n return String(value || '').split(path.sep).join('/').replace(/^\\.?\\//, '');\n}\n\nfunction normalizeRouteKey(routePath) {\n let normalized = normalizeRepoPath(routePath).replace(/^\\/+/, '');\n normalized = normalized.replace(/\\.(md|mdx)$/i, '');\n normalized = normalized.replace(/\\/index$/i, '');\n normalized = normalized.replace(/\\/+$/, '');\n return normalized;\n}\n\nfunction sortAlpha(values) {\n return [...values].sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }));\n}\n\nfunction readFileSafe(repoPath) {\n try {\n return fs.readFileSync(path.join(REPO_ROOT, repoPath), 'utf8');\n } catch (_err) {\n return '';\n }\n}\n\nfunction fileExists(repoPath) {\n const fullPath = path.join(REPO_ROOT, repoPath);\n return fs.existsSync(fullPath) && fs.statSync(fullPath).isFile();\n}\n\nfunction collectDocsPageEntries(node, out = []) {\n if (typeof node === 'string') {\n const value = node.trim();\n if (value.startsWith('v1/') || value.startsWith('v2/')) {\n out.push(value);\n }\n return out;\n }\n\n if (Array.isArray(node)) {\n node.forEach((item) => collectDocsPageEntries(item, out));\n return out;\n }\n\n if (!node || typeof node !== 'object') return out;\n\n Object.values(node).forEach((value) => collectDocsPageEntries(value, out));\n return out;\n}\n\nfunction getDocsJsonRouteKeys() {\n const raw = readFileSafe(DOCS_JSON_PATH);\n if (!raw.trim()) {\n throw new Error(`Missing or empty ${DOCS_JSON_PATH}`);\n }\n\n const docsJson = JSON.parse(raw);\n const entries = collectDocsPageEntries(docsJson, []);\n const keys = new Set();\n entries.forEach((entry) => {\n const key = normalizeRouteKey(entry);\n if (key) keys.add(key);\n });\n return keys;\n}\n\nfunction isExternalHref(href) {\n return /^(https?:\\/\\/|mailto:|#|\\/)/i.test(String(href || '').trim());\n}\n\nfunction cleanHref(href) {\n return String(href || '').split('#')[0].split('?')[0].trim();\n}\n\nfunction parseV2IndexLinks() {\n const content = readFileSafe(SOURCE_INDEX_PATH);\n if (!content.trim()) {\n throw new Error(`Missing or empty ${SOURCE_INDEX_PATH}`);\n }\n\n const links = [];\n const lines = content.split('\\n');\n lines.forEach((rawLine) => {\n const line = rawLine.trim();\n const match = line.match(/^- \\[(.+?)\\]\\((.+?)\\)$/);\n if (!match) return;\n const title = String(match[1] || '').trim();\n const href = cleanHref(match[2]);\n if (!href) return;\n if (title.includes('⚠️')) return;\n if (isExternalHref(href)) return;\n links.push(href);\n });\n\n return [...new Set(links)];\n}\n\nfunction resolveRepoPathFromHref(href) {\n const sourceDir = path.posix.dirname(SOURCE_INDEX_PATH);\n const cleanedHref = cleanHref(href);\n if (!cleanedHref) return '';\n\n const joined = normalizeRepoPath(path.posix.normalize(path.posix.join(sourceDir, cleanedHref)));\n if (!joined || joined.startsWith('..') || joined.includes('/../')) return '';\n\n const hasExtension = /\\.(md|mdx)$/i.test(joined);\n const candidates = hasExtension\n ? [joined]\n : [`${joined}.mdx`, `${joined}.md`, path.posix.join(joined, 'index.mdx'), path.posix.join(joined, 'index.md')];\n\n for (const candidate of candidates) {\n const normalizedCandidate = normalizeRepoPath(candidate);\n if (!normalizedCandidate.startsWith('v2/')) continue;\n if (fileExists(normalizedCandidate)) return normalizedCandidate;\n }\n\n return '';\n}\n\nfunction buildTree(paths) {\n const root = { name: 'v2', folders: new Map(), files: new Set() };\n\n sortAlpha(paths).forEach((repoPath) => {\n const segments = normalizeRepoPath(repoPath).split('/').filter(Boolean);\n if (segments.length < 2 || segments[0] !== 'v2') return;\n\n let current = root;\n for (let i = 1; i < segments.length - 1; i += 1) {\n const folderName = segments[i];\n if (!current.folders.has(folderName)) {\n current.folders.set(folderName, { name: folderName, folders: new Map(), files: new Set() });\n }\n current = current.folders.get(folderName);\n }\n\n current.files.add(segments[segments.length - 1]);\n });\n\n return root;\n}\n\nfunction escapeJsxAttribute(value) {\n return String(value || '').replace(/&/g, '&').replace(/\"/g, '"');\n}\n\nfunction renderTreeFolder(node, indent, defaultOpen = false) {\n const attrs = [`name=\"${escapeJsxAttribute(node.name)}\"`];\n if (defaultOpen) attrs.push('defaultOpen');\n\n const lines = [`${indent}`];\n const folderNames = sortAlpha([...node.folders.keys()]);\n const fileNames = sortAlpha([...node.files]);\n\n folderNames.forEach((folderName) => {\n lines.push(...renderTreeFolder(node.folders.get(folderName), `${indent} `, false));\n });\n\n fileNames.forEach((fileName) => {\n lines.push(`${indent} `);\n });\n\n lines.push(`${indent} `);\n return lines;\n}\n\nfunction renderTree(paths) {\n const tree = buildTree(paths);\n const lines = [''];\n\n const hasChildren = tree.folders.size > 0 || tree.files.size > 0;\n if (!hasChildren) {\n lines.push(' ');\n } else {\n lines.push(...renderTreeFolder(tree, ' ', true));\n }\n\n lines.push(' ');\n return lines;\n}\n\nfunction buildContent() {",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/add-framework-headers.js",
+ "script": "add-framework-headers",
+ "category": "generator",
+ "purpose": "governance:repo-health",
+ "scope": "full-repo",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "Inserts or extends framework headers on all repo scripts from classification data.",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/add-framework-headers.js --data script-classifications.json --dry-run",
+ "header": "#!/usr/bin/env node\n/**\n * @script add-framework-headers\n * @summary Insert or verify governance framework metadata headers from classification JSON.\n * @category generator\n * @purpose governance:repo-health\n * @scope full-repo\n * @owner docs\n * @needs R-R14\n * @purpose-statement Inserts or extends framework headers on all repo scripts from classification data.\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/add-framework-headers.js --data script-classifications.json --dry-run\n *\n * @inputs\n * --data --dry-run|--write|--verify [--filter ] [--exclude-subdirs] [--force]\n *\n * @outputs\n * - Console summary with files processed, updated, skipped, and errors\n *\n * @exit-codes\n * 0 = success\n * 1 = argument, I/O, parse, or verification failure\n *\n * @examples\n * node tools/scripts/add-framework-headers.js --data script-classifications.json --dry-run\n * node tools/scripts/add-framework-headers.js --data script-classifications.json --filter tools/scripts --write\n *\n * @notes\n * Tool-only migration utility; it does not execute repo-wide header rewrites unless explicitly run.\n */\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst REPO_ROOT = process.cwd();\nconst JS_EXTENSIONS = new Set(['.js', '.cjs', '.mjs', '.ts', '.tsx']);\nconst SHELL_EXTENSIONS = new Set(['.sh', '.bash']);\nconst PYTHON_EXTENSIONS = new Set(['.py']);\n\nfunction printHelp() {\n console.log(\n [\n 'Usage:',\n ' node tools/scripts/add-framework-headers.js --data (--dry-run | --write | --verify) [options]',\n '',\n 'Required:',\n ' --data Path to classification JSON data file.',\n '',\n 'Modes (choose exactly one):',\n ' --dry-run Show planned changes without writing files.',\n ' --write Apply header updates to files.',\n ' --verify Read-only validation of headers against classification data.',\n '',\n 'Options:',\n ' --filter Process only files under this repo-relative prefix.',\n ' --exclude-subdirs With --filter, include only direct child files.',\n ' --force Overwrite rows that already contain @category or @purpose.',\n ' --help Show this help output.',\n '',\n 'Examples:',\n ' node tools/scripts/add-framework-headers.js --data script-classifications.json --dry-run',\n ' node tools/scripts/add-framework-headers.js --data /tmp/classifications.json --filter tools/scripts --write',\n ' node tools/scripts/add-framework-headers.js --data script-classifications.json --filter .githooks --exclude-subdirs --verify'\n ].join('\\n')\n );\n}\n\nfunction parseArgs(argv) {\n const args = {\n dataPath: '',\n filter: '',\n excludeSubdirs: false,\n dryRun: false,\n write: false,\n verify: false,\n force: false,\n help: false\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n\n if (token === '--help') {\n args.help = true;\n continue;\n }\n if (token === '--dry-run') {\n args.dryRun = true;\n continue;\n }\n if (token === '--write') {\n args.write = true;\n continue;\n }\n if (token === '--verify') {\n args.verify = true;\n continue;\n }\n if (token === '--exclude-subdirs') {\n args.excludeSubdirs = true;\n continue;\n }\n if (token === '--force') {\n args.force = true;\n continue;\n }\n\n if (token === '--data') {\n args.dataPath = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--data=')) {\n args.dataPath = token.slice('--data='.length).trim();\n continue;\n }\n\n if (token === '--filter') {\n args.filter = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--filter=')) {\n args.filter = token.slice('--filter='.length).trim();\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (args.help) return args;\n\n if (!args.dataPath) {\n throw new Error('Missing required --data argument.');\n }\n\n const modeCount = [args.dryRun, args.write, args.verify].filter(Boolean).length;\n if (modeCount !== 1) {\n throw new Error('Exactly one mode is required: --dry-run, --write, or --verify.');\n }\n\n if (args.excludeSubdirs && !args.filter) {\n throw new Error('--exclude-subdirs requires --filter.');\n }\n\n return args;\n}\n\nfunction normalizeRepoPath(value, label) {\n const raw = String(value || '').trim();\n if (!raw) {\n throw new Error(`Missing ${label || 'path'} value.`);\n }\n\n let candidate = raw;\n if (path.isAbsolute(raw)) {\n candidate = path.relative(REPO_ROOT, raw);\n }\n\n const posix = path.posix.normalize(String(candidate).replace(/\\\\/g, '/'));\n const withoutDotPrefix = posix.replace(/^\\.\\//, '');\n\n if (!withoutDotPrefix || withoutDotPrefix === '.') {\n throw new Error(`Invalid ${label || 'path'} value: \"${raw}\"`);\n }\n\n if (\n withoutDotPrefix === '..' ||\n withoutDotPrefix.startsWith('../') ||\n path.isAbsolute(withoutDotPrefix)\n ) {\n throw new Error(`Path escapes repository root: \"${raw}\"`);\n }\n\n return withoutDotPrefix;\n}\n\nfunction normalizePrefix(prefix) {\n const normalized = normalizeRepoPath(prefix, '--filter');\n return normalized.endsWith('/') ? normalized.slice(0, -1) : normalized;\n}\n\nfunction stringField(row, fieldName) {\n const value = row[fieldName];\n if (value === undefined || value === null) return '';\n return String(value).trim();\n}\n\nfunction loadClassificationData(dataPath) {\n const absPath = path.isAbsolute(dataPath) ? dataPath : path.join(REPO_ROOT, dataPath);\n\n let raw = '';\n try {\n raw = fs.readFileSync(absPath, 'utf8');\n } catch (error) {\n throw new Error(`Unable to read data file: ${absPath} (${error.message})`);\n }\n\n let parsed;\n try {\n parsed = JSON.parse(raw);\n } catch (error) {\n throw new Error(`Invalid JSON in ${absPath}: ${error.message}`);\n }\n\n if (!Array.isArray(parsed)) {\n throw new Error(`Classification data must be an array: ${absPath}`);\n }\n\n const rows = [];\n const seenPaths = new Set();\n\n parsed.forEach((row, index) => {\n if (!row || typeof row !== 'object' || Array.isArray(row)) {\n throw new Error(`data[${index}] must be an object.`);\n }\n\n const repoPath = normalizeRepoPath(row.path, `data[${index}].path`);\n if (seenPaths.has(repoPath)) {\n throw new Error(`Duplicate path in classification data: ${repoPath}`);",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/enforcers/pr/check-pr-template.js",
+ "script": "check-pr-template",
+ "category": "enforcer",
+ "purpose": "governance:repo-health",
+ "scope": "tools/scripts/enforcers/pr, .github/pull_request_template.md, .github/pull-request-template-v2.md",
+ "owner": "docs",
+ "needs": "R-R14, R-C6",
+ "purpose_statement": "Enforces that PR descriptions include required change and rationale sections before merge",
+ "pipeline_declared": "ci",
+ "usage": "PR_BODY=\"...\" node tools/scripts/enforcers/pr/check-pr-template.js",
+ "header": "#!/usr/bin/env node\n/**\n * @script check-pr-template\n * @category enforcer\n * @purpose governance:repo-health\n * @scope tools/scripts/enforcers/pr, .github/pull_request_template.md, .github/pull-request-template-v2.md\n * @owner docs\n * @needs R-R14, R-C6\n * @purpose-statement Enforces that PR descriptions include required change and rationale sections before merge\n * @pipeline ci\n * @usage PR_BODY=\"...\" node tools/scripts/enforcers/pr/check-pr-template.js\n */\n\nconst fs = require('fs');\n\nconst CHANGE_HEADINGS = new Set(['what changed', 'changes', 'changes made']);\nconst RATIONALE_HEADINGS = new Set(['why', 'motivation', 'reason', 'description']);\n\nfunction normalizeHeading(value) {\n return String(value || '')\n .toLowerCase()\n .replace(/[`*_~]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction readPrBody() {\n const fromEnv = String(process.env.PR_BODY || '');\n if (fromEnv.trim()) return fromEnv;\n if (!process.stdin.isTTY) {\n return fs.readFileSync(0, 'utf8');\n }\n return '';\n}\n\nfunction collectHeadings(body) {\n const headings = [];\n const re = /^\\s{0,3}#{2,6}\\s+(.+?)\\s*#*\\s*$/gm;\n let match = re.exec(body);\n\n while (match) {\n headings.push(normalizeHeading(match[1]));\n match = re.exec(body);\n }\n\n return headings;\n}\n\nfunction hasRequiredHeading(headings, allowed) {\n return headings.some((heading) => allowed.has(heading));\n}\n\nfunction main() {\n const prBody = readPrBody();\n const headings = collectHeadings(prBody);\n const hasChangeSection = hasRequiredHeading(headings, CHANGE_HEADINGS);\n const hasRationaleSection = hasRequiredHeading(headings, RATIONALE_HEADINGS);\n const missing = [];\n\n if (!hasChangeSection) {\n missing.push('change section (accepted: ## What changed, ## Changes, ## Changes Made)');\n }\n\n if (!hasRationaleSection) {\n missing.push('rationale section (accepted: ## Why, ## Motivation, ## Reason, ## Description)');\n }\n\n if (missing.length === 0) {\n console.log('✅ PR template sections present.');\n process.exit(0);\n }\n\n const branch = String(process.env.GITHUB_HEAD_REF || '').trim();\n const message = `Missing required PR sections: ${missing.join('; ')}`;\n\n if (branch.startsWith('codex/')) {\n console.warn(`⚠️ ${message}`);\n process.exit(0);\n }\n\n console.error(`❌ ${message}`);\n process.exit(1);\n}\n\nmain();\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/validators/governance/audit-script-inventory.js",
+ "script": "audit-script-inventory",
+ "category": "validator",
+ "purpose": "governance:repo-health",
+ "scope": "full-repo",
+ "owner": "docs",
+ "needs": "R-R14, R-R18, R-C6",
+ "purpose_statement": "Deep inventory audit of every script in the repo. Traces triggers, outputs, downstream chains, and governance compliance. Produces reports grouped by trigger category.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/scripts/validators/governance/audit-script-inventory.js [--json] [--md] [--output ] [--verbose]",
+ "header": "#!/usr/bin/env node\n/**\n * @script audit-script-inventory\n * @category validator\n * @purpose governance:repo-health\n * @scope full-repo\n * @owner docs\n * @needs R-R14, R-R18, R-C6\n * @purpose-statement Deep inventory audit of every script in the repo. Traces triggers, outputs, downstream chains, and governance compliance. Produces reports grouped by trigger category.\n * @pipeline manual\n * @usage node tools/scripts/validators/governance/audit-script-inventory.js [--json] [--md] [--output ] [--verbose]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execFileSync } = require('child_process');\nconst yaml = require('js-yaml');\nconst {\n CATEGORY_ENUM,\n CLASSIFICATION_DATA_PATH,\n DISCOVERY_ROOTS,\n FRAMEWORK_FIELDS,\n GROUP_LABELS,\n GROUP_ORDER,\n PIPELINE_ORDER,\n PURPOSE_ENUM,\n REQUIRED_FRAMEWORK_KEYS,\n SCOPE_ENUM,\n SCRIPT_EXTENSIONS,\n isDiscoveredScriptPath,\n normalizeRepoPath,\n parseDeclaredPipelines\n} = require('../../../lib/script-governance-config');\n\nconst REPO_ROOT = path.resolve(__dirname, '../../../..');\nconst DEFAULT_OUTPUT_DIR = 'tasks/reports/repo-ops';\nconst DEFAULT_MD_PATH = 'SCRIPT_INVENTORY_FULL.md';\nconst DEFAULT_JSON_PATH = 'SCRIPT_INVENTORY_FULL.json';\nconst REPO_ROOT_POSIX = normalizeRepoPath(REPO_ROOT);\nconst REPO_ROOT_LOOSE = REPO_ROOT_POSIX.replace(/^\\/+/, '');\nconst HEADER_SCAN_LINES = 220;\nconst SPECIAL_CALLERS = new Set(['.githooks/pre-commit', '.githooks/pre-push', 'tests/run-all.js', 'tests/run-pr-checks.js']);\nconst PACKAGE_JSON_PATHS = ['tools/package.json', 'tests/package.json'];\nconst WRITE_CALL_RE = /\\b(?:fs\\.)?(writeFileSync|writeFile|appendFileSync|appendFile|mkdirSync|mkdir|createWriteStream|writeJson)\\s*\\(([\\s\\S]{0,220}?)\\)/g;\nconst STRING_RE = /(['\"`])((?:\\\\.|(?!\\1).)*)\\1/g;\nconst CONST_RE = /^\\s*const\\s+([A-Za-z0-9_]+)\\s*=\\s*(.+?);\\s*$/gm;\nconst HEADER_TABLE_COLUMNS = [\n 'Path',\n 'Category',\n 'Purpose',\n 'Pipeline claimed',\n 'Pipeline actual',\n 'Pipeline verdict',\n 'Outputs',\n 'Triggers downstream?',\n 'Scope',\n 'Needs',\n 'Header completeness',\n 'Grade',\n 'Flags'\n];\nconst AUTOMATED_PIPELINES = new Set(['P1', 'P2', 'P3', 'P5', 'P6']);\n\nfunction usage() {\n console.log(\n 'Usage: node tools/scripts/validators/governance/audit-script-inventory.js [--json] [--md] [--output ] [--verbose]'\n );\n}\n\nfunction parseArgs(argv) {\n const args = {\n json: false,\n md: false,\n verbose: false,\n outputDir: DEFAULT_OUTPUT_DIR\n };\n\n for (let index = 0; index < argv.length; index += 1) {\n const token = argv[index];\n if (token === '--json') {\n args.json = true;\n continue;\n }\n if (token === '--md') {\n args.md = true;\n continue;\n }\n if (token === '--verbose') {\n args.verbose = true;\n continue;\n }\n if (token === '--output') {\n args.outputDir = String(argv[index + 1] || '').trim() || DEFAULT_OUTPUT_DIR;\n index += 1;\n continue;\n }\n if (token.startsWith('--output=')) {\n args.outputDir = token.slice('--output='.length).trim() || DEFAULT_OUTPUT_DIR;\n continue;\n }\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (!args.json && !args.md) {\n args.json = true;\n args.md = true;\n args.printSummary = true;\n }\n\n return args;\n}\n\nfunction ensureDirectory(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction readRepoFile(repoPath, warnings) {\n try {\n return fs.readFileSync(path.join(REPO_ROOT, repoPath), 'utf8');\n } catch (error) {\n warnings.push(`Could not read ${repoPath}: ${error.message}`);\n return '';\n }\n}\n\nfunction listTrackedFiles() {\n const output = execFileSync('git', ['ls-files'], {\n cwd: REPO_ROOT,\n encoding: 'utf8'\n });\n return output\n .split('\\n')\n .map((line) => normalizeRepoPath(line.trim()))\n .filter(Boolean);\n}\n\nfunction getWorkflowFiles(trackedFiles) {\n return trackedFiles.filter((repoPath) => repoPath.startsWith('.github/workflows/') && /\\.(ya?ml)$/i.test(repoPath));\n}\n\nfunction getDiscoveredScripts(trackedFiles) {\n return trackedFiles.filter(isDiscoveredScriptPath).sort();\n}\n\nfunction getHeaderChunk(content) {\n return String(content || '')\n .split('\\n')\n .slice(0, HEADER_SCAN_LINES)\n .join('\\n');\n}\n\nfunction getTagValue(header, tagName) {\n const pattern = new RegExp(`\\\\${tagName}\\\\s+(.+)`);\n const match = header.match(pattern);\n return match ? match[1].trim() : '';\n}\n\nfunction getSectionLines(header, tagName) {\n const lines = String(header || '').split('\\n');\n const tagToken = tagName.replace('@', '');\n const start = lines.findIndex((line) => line.includes(`@${tagToken}`));\n if (start === -1) return [];\n\n const values = [];\n for (let index = start + 1; index < lines.length; index += 1) {\n const line = lines[index].trim();\n if (!line) continue;\n const stripped = line.replace(/^\\*\\s?/, '').replace(/^#\\s?/, '').trim();\n if (!stripped) continue;\n if (stripped.startsWith('@')) break;\n if (stripped === '/**' || stripped === '*/') continue;\n values.push(stripped);\n }\n return values;\n}\n\nfunction readUsageValue(header) {\n const inline = getTagValue(header, '@usage');\n if (inline) return inline;\n return getSectionLines(header, '@usage').find(Boolean) || '';\n}\n\nfunction extractHeaderMetadata(scriptPath, content) {\n const header = getHeaderChunk(content);\n const values = {\n path: scriptPath,\n script: getTagValue(header, '@script'),\n category: getTagValue(header, '@category'),\n purpose: getTagValue(header, '@purpose'),\n scope: getTagValue(header, '@scope'),\n owner: getTagValue(header, '@owner'),\n needs: getTagValue(header, '@needs'),\n purpose_statement: getTagValue(header, '@purpose-statement'),\n pipeline_declared: getTagValue(header, '@pipeline'),\n usage: readUsageValue(header)\n };\n\n const headerFieldCount = FRAMEWORK_FIELDS.filter((field) => String(values[field.key] || '').trim()).length;\n const hasAnyHeader = header.includes('@script') || header.includes('@summary');\n const hasFrameworkHeader = header.includes('@category') || header.includes('@purpose') || header.includes('@purpose-statement');\n\n return {\n ...values,\n header,\n header_field_count: headerFieldCount,\n has_any_header: hasAnyHeader,\n has_framework_header: hasFrameworkHeader,\n category_valid: values.category ? CATEGORY_ENUM.includes(values.category) : false,\n purpose_valid: values.purpose ? PURPOSE_ENUM.includes(values.purpose) : false,\n scope_valid: values.scope ? SCOPE_ENUM.includes(values.scope) : false\n };\n}\n\nfunction loadClassificationRows(trackedFiles, warnings) {\n if (!trackedFiles.includes(CLASSIFICATION_DATA_PATH)) {\n warnings.push(`Missing classification data file: ${CLASSIFICATION_DATA_PATH}`);",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "audit:scripts"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "audit:scripts:verbose"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: audit:scripts); manual (npm script: audit:scripts:verbose)",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "snippets/automations/youtube/filterVideos.js",
+ "script": "filterVideos",
+ "category": "automation",
+ "purpose": "infrastructure:data-feeds",
+ "scope": "external",
+ "owner": "docs",
+ "needs": "F-R1",
+ "purpose_statement": "YouTube video filter — post-processes fetched YouTube data to filter/sort videos for display",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node snippets/automations/youtube/filterVideos.js [flags]",
+ "header": "/**\n * @script filterVideos\n * @category automation\n * @purpose infrastructure:data-feeds\n * @scope external\n * @owner docs\n * @needs F-R1\n * @purpose-statement YouTube video filter — post-processes fetched YouTube data to filter/sort videos for display\n * @pipeline manual — not yet in pipeline\n * @usage node snippets/automations/youtube/filterVideos.js [flags]\n */\nexport const filterVideos = (\n videos,\n excludeKeywords = [\"fireside\", \"watercooler\"]\n) => {\n return videos.filter((video) => {\n const title = video.title.toLowerCase();\n return !excludeKeywords.some((keyword) =>\n title.includes(keyword.toLowerCase())\n );\n });\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/convert-rss-to-mdx.js",
+ "script": "convert-rss-to-mdx",
+ "category": "automation",
+ "purpose": "infrastructure:data-feeds",
+ "scope": "tools/scripts, v2/internal/assets/transcripts",
+ "owner": "docs",
+ "needs": "F-R1",
+ "purpose_statement": "RSS-to-MDX converter — imports RSS feed items and converts to MDX page format",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/convert-rss-to-mdx.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script convert-rss-to-mdx\n * @category automation\n * @purpose infrastructure:data-feeds\n * @scope tools/scripts, v2/internal/assets/transcripts\n * @owner docs\n * @needs F-R1\n * @purpose-statement RSS-to-MDX converter — imports RSS feed items and converts to MDX page format\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/convert-rss-to-mdx.js [flags]\n */\n\nconst fs = require('fs')\nconst path = require('path')\n\nfunction parseArgs(argv) {\n const args = {}\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i]\n if (!token.startsWith('--')) continue\n const key = token.slice(2)\n const next = argv[i + 1]\n if (!next || next.startsWith('--')) {\n args[key] = true\n } else {\n args[key] = next\n i += 1\n }\n }\n return args\n}\n\nfunction toAbsolutePath(p) {\n if (!p) return ''\n return path.isAbsolute(p) ? p : path.join(process.cwd(), p)\n}\n\nfunction decodeEntities(input = '') {\n return input\n .replace(//g, '$1')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ')\n}\n\nfunction stripHtml(input = '') {\n return decodeEntities(input)\n .replace(/
/gi, '\\n')\n .replace(/<\\/p>/gi, '\\n')\n .replace(/]*>/gi, '')\n .replace(/<[^>]+>/g, '')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim()\n}\n\nfunction firstMatch(regex, text, fallback = '') {\n const match = text.match(regex)\n return match ? match[1].trim() : fallback\n}\n\nfunction escapeYamlSingleQuoted(value = '') {\n return String(value).replace(/'/g, \"''\")\n}\n\nfunction parseRss(xml) {\n const channelBlock = firstMatch(/([\\s\\S]*?)- /i, xml, xml)\n\n const title = decodeEntities(\n firstMatch(/
([\\s\\S]*?)<\\/title>/i, channelBlock, 'RSS Feed')\n )\n const description = stripHtml(\n firstMatch(/([\\s\\S]*?)<\\/description>/i, channelBlock, '')\n )\n const feedLink =\n firstMatch(/]*href=\"([^\"]+)\"/i, channelBlock, '') ||\n firstMatch(/([\\s\\S]*?)<\\/link>/i, channelBlock, '')\n const author = decodeEntities(\n firstMatch(/([\\s\\S]*?)<\\/author>/i, channelBlock, '') ||\n firstMatch(\n /([\\s\\S]*?)<\\/itunes:author>/i,\n channelBlock,\n ''\n )\n )\n const language = decodeEntities(\n firstMatch(/([\\s\\S]*?)<\\/language>/i, channelBlock, '')\n )\n const lastBuildDate = firstMatch(\n /([\\s\\S]*?)<\\/lastBuildDate>/i,\n channelBlock,\n ''\n )\n\n const itemBlocks = [...xml.matchAll(/- ([\\s\\S]*?)<\\/item>/gi)].map(\n (m) => m[1]\n )\n\n const episodes = itemBlocks.map((block, index) => {\n const episodeTitle = decodeEntities(\n firstMatch(/
([\\s\\S]*?)<\\/title>/i, block, `Episode ${index + 1}`)\n ).trim()\n\n return {\n title: episodeTitle,\n pubDate: firstMatch(/([\\s\\S]*?)<\\/pubDate>/i, block, ''),\n duration: firstMatch(\n /([\\s\\S]*?)<\\/itunes:duration>/i,\n block,\n ''\n ),\n explicit: firstMatch(\n /([\\s\\S]*?)<\\/itunes:explicit>/i,\n block,\n ''\n ),\n link: firstMatch(/([\\s\\S]*?)<\\/link>/i, block, ''),\n audioUrl: firstMatch(/]*url=\"([^\"]+)\"/i, block, ''),\n summary: stripHtml(\n firstMatch(/([\\s\\S]*?)<\\/description>/i, block, '') ||\n firstMatch(/([\\s\\S]*?)<\\/itunes:summary>/i, block, '')\n ),\n }\n })\n\n return {\n title,\n description,\n feedLink,\n author,\n language,\n lastBuildDate,\n episodes,\n }\n}\n\nfunction buildMdx(feed) {\n const lines = []\n\n lines.push('---')\n lines.push(`title: '${escapeYamlSingleQuoted(feed.title)}'`)\n lines.push(\"sidebarTitle: 'RSS Feed'\")\n lines.push(\n `description: '${escapeYamlSingleQuoted(feed.description || 'Converted from RSS to MDX')}'`\n )\n lines.push(\"keywords: ['rss', 'podcast', 'transcript', 'feed']\")\n lines.push('---')\n lines.push('')\n\n lines.push(`# ${feed.title}`)\n lines.push('')\n\n if (feed.description) {\n lines.push(feed.description)\n lines.push('')\n }\n\n if (feed.feedLink) lines.push(`- Feed URL: ${feed.feedLink}`)\n if (feed.author) lines.push(`- Author: ${feed.author}`)\n if (feed.language) lines.push(`- Language: ${feed.language}`)\n if (feed.lastBuildDate) lines.push(`- Last build date: ${feed.lastBuildDate}`)\n lines.push(`- Episode count: ${feed.episodes.length}`)\n lines.push('')\n lines.push('## Episodes')\n lines.push('')\n\n feed.episodes.forEach((episode) => {\n lines.push(`### ${episode.title}`)\n lines.push('')\n if (episode.pubDate) lines.push(`- Published: ${episode.pubDate}`)\n if (episode.duration) lines.push(`- Duration: ${episode.duration}`)\n if (episode.explicit) lines.push(`- Explicit: ${episode.explicit}`)\n if (episode.link) lines.push(`- Episode link: ${episode.link}`)\n if (episode.audioUrl) lines.push(`- Audio: ${episode.audioUrl}`)\n lines.push('')\n if (episode.summary) {\n lines.push(episode.summary)\n lines.push('')\n }\n })\n\n return `${lines.join('\\n')}\\n`\n}\n\nfunction main() {\n const args = parseArgs(process.argv.slice(2))\n\n if (args.help || !args.input || !args.output) {\n console.log(\n 'Usage: node tools/scripts/convert-rss-to-mdx.js --input --output '\n )\n process.exit(args.help ? 0 : 1)\n }\n\n const inputPath = toAbsolutePath(args.input)\n const outputPath = toAbsolutePath(args.output)\n\n if (!fs.existsSync(inputPath)) {\n throw new Error(`Input file not found: ${inputPath}`)\n }\n\n const xml = fs.readFileSync(inputPath, 'utf8')\n const feed = parseRss(xml)\n const mdx = buildMdx(feed)\n\n fs.mkdirSync(path.dirname(outputPath), { recursive: true })\n fs.writeFileSync(outputPath, mdx, 'utf8')\n\n console.log(`✓ Converted RSS to MDX`)\n console.log(` Input: ${inputPath}`)\n console.log(` Output: ${outputPath}`)\n console.log(` Episodes: ${feed.episodes.length}`)\n}\n\ntry {\n main()",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/snippets/fetch-lpt-exchanges.sh",
+ "script": "fetch-lpt-exchanges",
+ "category": "automation",
+ "purpose": "infrastructure:data-feeds",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R1",
+ "purpose_statement": "LPT exchange data fetcher — pulls exchange listing data for LPT token pages",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "bash tools/scripts/snippets/fetch-lpt-exchanges.sh [flags]",
+ "header": "#!/bin/bash\n# @script fetch-lpt-exchanges\n# @category automation\n# @purpose infrastructure:data-feeds\n# @scope tools/scripts\n# @owner docs\n# @needs F-R1\n# @purpose-statement LPT exchange data fetcher — pulls exchange listing data for LPT token pages\n# @pipeline manual — interactive developer tool, not suited for automated pipelines\n# @usage bash tools/scripts/snippets/fetch-lpt-exchanges.sh [flags]\n# Fetch LPT exchange listings from CoinGecko API and append to lpt-exchanges.mdx\n# Usage: ./tools/scripts/snippets/fetch-lpt-exchanges.sh\n\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nCONFIG_FILE=\"$SCRIPT_DIR/paths.config.json\"\n\n# Try to detect repo root via git, fallback to config file\nif git rev-parse --show-toplevel &>/dev/null; then\n REPO_ROOT=\"$(git rev-parse --show-toplevel)\"\nelif [ -f \"$CONFIG_FILE\" ]; then\n echo \"Warning: Not in a git repo, using paths.config.json\"\n REPO_ROOT=\"$(dirname \"$(dirname \"$SCRIPT_DIR\")\")\"\nelse\n echo \"Error: Cannot determine repo root. Run from git repo or ensure paths.config.json exists.\"\n exit 1\nfi\n\n# Read path from config or use default\nif [ -f \"$CONFIG_FILE\" ] && command -v node &>/dev/null; then\n OUTPUT_FILE=\"$REPO_ROOT/$(node -pe \"require('$CONFIG_FILE').paths.lptExchanges\")\"\nelse\n if [ -f \"$REPO_ROOT/v2/lpt/resources/exchanges.mdx\" ]; then\n OUTPUT_FILE=\"$REPO_ROOT/v2/lpt/resources/exchanges.mdx\"\n else\n OUTPUT_FILE=\"$REPO_ROOT/v2/lpt/resources/exchanges.mdx\"\n fi\nfi\n\n# Fetch data from CoinGecko\necho \"Fetching LPT exchange data from CoinGecko...\"\nAPI_RESPONSE=$(curl -s \"https://api.coingecko.com/api/v3/coins/livepeer\")\n\nif [ -z \"$API_RESPONSE\" ]; then\n echo \"Error: Failed to fetch data from CoinGecko API\"\n exit 1\nfi\n\n# Generate the exchange content using Node.js\nnode - \"$API_RESPONSE\" \"$OUTPUT_FILE\" << 'NODEJS_SCRIPT'\nconst fs = require('fs');\nconst path = require('path');\n\nconst apiResponse = process.argv[2];\nconst outputFile = process.argv[3];\n\nconst data = JSON.parse(apiResponse);\nconst tickers = data.tickers || [];\n\n// Group by exchange and deduplicate\nconst exchangeMap = new Map();\n\nfor (const ticker of tickers) {\n const exchangeName = ticker.market?.name;\n const exchangeId = ticker.market?.identifier;\n const tradeUrl = ticker.trade_url;\n const trustScore = ticker.trust_score;\n const isStale = ticker.is_stale;\n const target = ticker.target;\n const volume = ticker.converted_volume?.usd || 0;\n \n if (!exchangeName || isStale) continue;\n \n if (!exchangeMap.has(exchangeId)) {\n exchangeMap.set(exchangeId, {\n name: exchangeName,\n url: tradeUrl,\n trustScore: trustScore,\n pairs: [],\n totalVolume: 0\n });\n }\n \n const exchange = exchangeMap.get(exchangeId);\n if (!exchange.pairs.includes(target)) {\n exchange.pairs.push(target);\n }\n exchange.totalVolume += volume;\n \n // Keep the best trust score\n if (trustScore === 'green' || (trustScore === 'yellow' && exchange.trustScore !== 'green')) {\n exchange.trustScore = trustScore;\n exchange.url = tradeUrl;\n }\n}\n\n// Sort by volume (highest first)\nconst sortedExchanges = Array.from(exchangeMap.values())\n .filter(e => e.totalVolume > 0)\n .sort((a, b) => b.totalVolume - a.totalVolume);\n\n// Get trust score badge\nfunction getTrustBadge(score) {\n if (score === 'green') return '🟢';\n if (score === 'yellow') return '🟡';\n if (score === 'red') return '🔴';\n return '⚪';\n}\n\n// Generate MDX content\nconst lastUpdated = new Date().toISOString().split('T')[0];\n\nlet content = `\n\n---\n\n## Centralized Exchanges (CEX)\n\n\n **Last Updated:** ${lastUpdated} | Data sourced from [CoinGecko](https://www.coingecko.com/en/coins/livepeer)\n \n\n\n **Trust Score Legend:** 🟢 High | 🟡 Medium | 🔴 Low | ⚪ Unknown\n \n\n\n\n \n \n Exchange \n Trading Pairs \n 24h Volume (USD) \n Trust \n \n \n \n`;\n\nsortedExchanges.forEach((exchange, index) => {\n const bgColor = index % 2 === 0 ? '#1a1a1a' : 'transparent';\n const volumeFormatted = exchange.totalVolume >= 1000000 \n ? `$${(exchange.totalVolume / 1000000).toFixed(2)}M`\n : exchange.totalVolume >= 1000 \n ? `$${(exchange.totalVolume / 1000).toFixed(2)}K`\n : `$${exchange.totalVolume.toFixed(2)}`;\n \n const pairsDisplay = exchange.pairs.slice(0, 5).join(', ') + (exchange.pairs.length > 5 ? '...' : '');\n const trustBadge = getTrustBadge(exchange.trustScore);\n \n content += ` \n ${exchange.name} \n ${pairsDisplay} \n ${volumeFormatted} \n ${trustBadge} \n \n`;\n});\n\ncontent += ` \n
\n\n\n\n Trading volumes are approximate 24-hour figures. Always verify current data on the exchange.\n \n\n## Decentralized Exchanges (DEX)\n\n\n \n Trade LPT on Ethereum mainnet\n \n \n Legacy Uniswap pool on Ethereum\n \n \n\n## Contract Addresses\n\n\n\n \n \n Network \n Contract Address \n \n \n \n \n Ethereum \n 0x58b6a8a3302369daec383334672404ee733ab239 \n \n \n Arbitrum One \n 0x289ba1701c2f088cf0faf8b3705246331cb8a839 \n \n \n
\n\n`;\n\n// Read existing file and find where to append (after the Danger block)\nconst existingContent = fs.readFileSync(outputFile, 'utf8');\nconst dangerEndIndex = existingContent.indexOf('');\n\nif (dangerEndIndex === -1) {\n console.error('Could not find tag in the file');\n process.exit(1);\n}\n\n// Get content up to and including \nconst headerContent = existingContent.substring(0, dangerEndIndex + ''.length);\n\n// Write the new file\nfs.writeFileSync(outputFile, headerContent + content);\n\nconsole.log(`✅ Updated ${outputFile} with ${sortedExchanges.length} exchanges`);\nNODEJS_SCRIPT",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/snippets/update-component-library.sh",
+ "script": "update-component-library",
+ "category": "automation",
+ "purpose": "infrastructure:data-feeds",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R1",
+ "purpose_statement": "Component library updater — syncs component library documentation from source",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "bash tools/scripts/snippets/update-component-library.sh [flags]",
+ "header": "#!/bin/bash\n# @script update-component-library\n# @category automation\n# @purpose infrastructure:data-feeds\n# @scope tools/scripts\n# @owner docs\n# @needs F-R1\n# @purpose-statement Component library updater — syncs component library documentation from source\n# @pipeline manual — interactive developer tool, not suited for automated pipelines\n# @usage bash tools/scripts/snippets/update-component-library.sh [flags]\n# Auto-updates snippets/snippetsWiki/componentLibrary/index.mdx\n# Run this script after changes to snippets/components/\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nCONFIG_FILE=\"$SCRIPT_DIR/paths.config.json\"\n\n# Try to detect repo root via git, fallback to config file\nif git rev-parse --show-toplevel &>/dev/null; then\n REPO_ROOT=\"$(git rev-parse --show-toplevel)\"\nelif [ -f \"$CONFIG_FILE\" ]; then\n echo \"Warning: Not in a git repo, using paths.config.json\"\n REPO_ROOT=\"$(dirname \"$(dirname \"$SCRIPT_DIR\")\")\"\nelse\n echo \"Error: Cannot determine repo root. Run from git repo or ensure paths.config.json exists.\"\n exit 1\nfi\n\n# Read paths from config or use defaults\nif [ -f \"$CONFIG_FILE\" ] && command -v node &>/dev/null; then\n COMPONENTS_DIR=\"$REPO_ROOT/$(node -pe \"require('$CONFIG_FILE').paths.snippetsComponents\")\"\n OUTPUT_FILE=\"$REPO_ROOT/$(node -pe \"require('$CONFIG_FILE').paths.snippetsWikiComponentLibrary\")\"\nelse\n COMPONENTS_DIR=\"$REPO_ROOT/snippets/components\"\n OUTPUT_FILE=\"$REPO_ROOT/snippets/snippetsWiki/componentLibrary/index.mdx\"\nfi\n\n# Generate tree structure from the live component directory instead of hardcoded folders.\nget_top_level_dirs() {\n find \"$COMPONENTS_DIR\" -mindepth 1 -maxdepth 1 -type d \\\n ! -name \"_archive\" \\\n ! -name \"examples\" \\\n -print | sort\n}\n\nget_child_dirs() {\n local dir=\"$1\"\n find \"$dir\" -mindepth 1 -maxdepth 1 -type d \\\n ! -name \"_archive\" \\\n ! -name \"examples\" \\\n -print | sort\n}\n\nget_component_files() {\n local dir=\"$1\"\n find \"$dir\" -mindepth 1 -maxdepth 1 -type f \\( \\\n -name \"*.jsx\" -o -name \"*.tsx\" -o -name \"*.js\" \\\n \\) -print | sort\n}\n\nrender_folder() {\n local dir=\"$1\"\n local indent=\"$2\"\n local name\n name=\"$(basename \"$dir\")\"\n\n echo \"${indent}\"\n\n while IFS= read -r file; do\n [ -n \"$file\" ] || continue\n echo \"${indent} \"\n done < <(get_component_files \"$dir\")\n\n while IFS= read -r child_dir; do\n [ -n \"$child_dir\" ] || continue\n render_folder \"$child_dir\" \"${indent} \"\n done < <(get_child_dirs \"$dir\")\n\n echo \"${indent} \"\n}\n\n# Build the new content\nbuild_content() {\n cat << 'HEADER'\n---\ntitle: Component Library\ndescription: Library of custom components used in Livepeer documentation\nsidebarTitle: Component Library\n---\n\n{/* AUTO-GENERATED: Do not edit below this line. Run tools/scripts/snippets/update-component-library.sh to update. */}\n\n## Components Structure\n\nBelow is the current list of components found under `snippets/components/`.\n\n\n \nHEADER\n\n while IFS= read -r dir; do\n [ -n \"$dir\" ] || continue\n render_folder \"$dir\" \" \"\n done < <(get_top_level_dirs)\n\n cat << 'FOOTER'\n \n \n\n{/* END AUTO-GENERATED */}\n\n---\n\n## Component Examples\n\n{/* Add component examples below */}\n\nFOOTER\n}\n\n# Run and save\nbuild_content > \"$OUTPUT_FILE\"\necho \"Updated $OUTPUT_FILE\"\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": ".githooks/pre-commit-no-deletions",
+ "script": "pre-commit-no-deletions",
+ "category": "orchestrator",
+ "purpose": "infrastructure:pipeline-orchestration",
+ "scope": ".githooks",
+ "owner": "docs",
+ "needs": "R-R29",
+ "purpose_statement": "Variant pre-commit hook that blocks file deletions (safety net for content preservation)",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "bash .githooks/pre-commit-no-deletions [flags]",
+ "header": "#!/bin/bash\n# @script pre-commit-no-deletions\n# @category orchestrator\n# @purpose infrastructure:pipeline-orchestration\n# @scope .githooks\n# @owner docs\n# @needs R-R29\n# @purpose-statement Variant pre-commit hook that blocks file deletions (safety net for content preservation)\n# @pipeline manual — not yet in pipeline\n# @usage bash .githooks/pre-commit-no-deletions [flags]\n# Pre-commit hook: PREVENT DELETIONS\n# This hook blocks any file deletions to prevent accidental data loss\n\n# Get list of deleted files\nDELETED_FILES=$(git diff --cached --name-only --diff-filter=D)\nBLOCKED_DELETED_FILES=$(echo \"$DELETED_FILES\" | grep -Ev '^snippets/data/[^/]+/hrefs\\.jsx$' || true)\nALLOWED_GENERATED_DELETIONS=$(echo \"$DELETED_FILES\" | grep -E '^snippets/data/[^/]+/hrefs\\.jsx$' || true)\n\nif [ ! -z \"$ALLOWED_GENERATED_DELETIONS\" ]; then\n echo \"\"\n echo \"ℹ️ Allowing generated domain link map deletions:\"\n echo \"$ALLOWED_GENERATED_DELETIONS\" | sed 's/^/ ↺ /'\nfi\n\nif [ ! -z \"$BLOCKED_DELETED_FILES\" ]; then\n echo \"\"\n echo \"╔═══════════════════════════════════════════════════════════════╗\"\n echo \"║ ❌ FILE DELETIONS DETECTED - COMMIT BLOCKED ║\"\n echo \"╚═══════════════════════════════════════════════════════════════╝\"\n echo \"\"\n echo \"🚫 DELETIONS ARE NOT ALLOWED\"\n echo \"\"\n echo \"The following files are being deleted:\"\n echo \"$BLOCKED_DELETED_FILES\" | sed 's/^/ ❌ /'\n echo \"\"\n echo \"📋 INSTRUCTIONS:\"\n echo \" 1. If you need to move files, use 'git mv' instead of delete+add\"\n echo \" 2. If files should be removed, use 'git rm --cached' to unstage\"\n echo \" 3. If this is intentional, use: git commit --no-verify\"\n echo \" (But this should be VERY rare - deletions are dangerous!)\"\n echo \"\"\n echo \"⚠️ If you really need to delete files, you must:\"\n echo \" 1. Document WHY in the commit message\"\n echo \" 2. Verify files are backed up\"\n echo \" 3. Use: git commit --no-verify\"\n echo \"\"\n exit 1\nfi\n\nexit 0\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/repo-audit-orchestrator.js",
+ "script": "repo-audit-orchestrator",
+ "category": "orchestrator",
+ "purpose": "infrastructure:pipeline-orchestration",
+ "scope": "tools/scripts, ai-tools/ai-skills/catalog, tasks/reports/repo-ops",
+ "owner": "docs",
+ "needs": "R-R29",
+ "purpose_statement": "Repo audit orchestrator — dispatches all static analysis validators in sequence. Supports --mode (static/full), --scope (full/changed), --quarantine, --agent-pack.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/scripts/repo-audit-orchestrator.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script repo-audit-orchestrator\n * @category orchestrator\n * @purpose infrastructure:pipeline-orchestration\n * @scope tools/scripts, ai-tools/ai-skills/catalog, tasks/reports/repo-ops\n * @owner docs\n * @needs R-R29\n * @purpose-statement Repo audit orchestrator — dispatches all static analysis validators in sequence. Supports --mode (static/full), --scope (full/changed), --quarantine, --agent-pack.\n * @pipeline manual\n * @usage node tools/scripts/repo-audit-orchestrator.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst STAGE_ID = 'repo-audit-orchestrator';\nconst REPO_ROOT = process.cwd();\nconst DEFAULT_OUTPUT_DIR = 'tasks/reports/repo-ops';\nconst DEFAULT_CATALOG_PATH = 'ai-tools/ai-skills/catalog/skill-catalog.json';\nconst DEFAULT_MANIFEST_PATH = 'ai-tools/ai-skills/catalog/execution-manifest.json';\n\nconst VALID_MODES = new Set(['static', 'runtime', 'full']);\nconst VALID_SCOPES = new Set(['changed', 'full']);\nconst VALID_AGENT_PACKS = new Set(['codex', 'cursor', 'claude', 'windsurf', 'all', 'none']);\n\nconst SCORE_BUCKET_WEIGHTS = {\n critical: 12,\n high: 6,\n medium: 3,\n low: 1,\n info: 0\n};\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction parseArgs(argv) {\n const out = {\n mode: 'static',\n scope: 'full',\n outputDir: DEFAULT_OUTPUT_DIR,\n quarantine: false,\n agentPack: 'none',\n catalogPath: DEFAULT_CATALOG_PATH,\n manifestPath: DEFAULT_MANIFEST_PATH\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n\n if (token === '--mode') {\n out.mode = String(argv[i + 1] || out.mode).trim() || out.mode;\n i += 1;\n continue;\n }\n if (token.startsWith('--mode=')) {\n out.mode = token.slice('--mode='.length).trim() || out.mode;\n continue;\n }\n if (token === '--scope') {\n out.scope = String(argv[i + 1] || out.scope).trim() || out.scope;\n i += 1;\n continue;\n }\n if (token.startsWith('--scope=')) {\n out.scope = token.slice('--scope='.length).trim() || out.scope;\n continue;\n }\n if (token === '--output-dir') {\n out.outputDir = String(argv[i + 1] || out.outputDir).trim() || out.outputDir;\n i += 1;\n continue;\n }\n if (token.startsWith('--output-dir=')) {\n out.outputDir = token.slice('--output-dir='.length).trim() || out.outputDir;\n continue;\n }\n if (token === '--quarantine') {\n out.quarantine = true;\n continue;\n }\n if (token === '--agent-pack') {\n out.agentPack = String(argv[i + 1] || out.agentPack).trim() || out.agentPack;\n i += 1;\n continue;\n }\n if (token.startsWith('--agent-pack=')) {\n out.agentPack = token.slice('--agent-pack='.length).trim() || out.agentPack;\n continue;\n }\n if (token === '--catalog') {\n out.catalogPath = String(argv[i + 1] || out.catalogPath).trim() || out.catalogPath;\n i += 1;\n continue;\n }\n if (token.startsWith('--catalog=')) {\n out.catalogPath = token.slice('--catalog='.length).trim() || out.catalogPath;\n continue;\n }\n if (token === '--manifest') {\n out.manifestPath = String(argv[i + 1] || out.manifestPath).trim() || out.manifestPath;\n i += 1;\n continue;\n }\n if (token.startsWith('--manifest=')) {\n out.manifestPath = token.slice('--manifest='.length).trim() || out.manifestPath;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (!VALID_MODES.has(out.mode)) {\n throw new Error(`Invalid --mode: ${out.mode}`);\n }\n if (!VALID_SCOPES.has(out.scope)) {\n throw new Error(`Invalid --scope: ${out.scope}`);\n }\n if (!VALID_AGENT_PACKS.has(out.agentPack)) {\n throw new Error(`Invalid --agent-pack: ${out.agentPack}`);\n }\n\n return out;\n}\n\nfunction readJson(repoPath) {\n return JSON.parse(fs.readFileSync(path.join(REPO_ROOT, repoPath), 'utf8'));\n}\n\nfunction severitySortValue(severity) {\n switch (severity) {\n case 'critical':\n return 0;\n case 'high':\n return 1;\n case 'medium':\n return 2;\n case 'low':\n return 3;\n default:\n return 4;\n }\n}\n\nfunction computeSummary(issues) {\n const summary = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n total: issues.length\n };\n\n issues.forEach((issue) => {\n if (Object.prototype.hasOwnProperty.call(summary, issue.severity)) {\n summary[issue.severity] += 1;\n }\n });\n\n return summary;\n}\n\nfunction runCommand(label, command, args, cwd) {\n const result = spawnSync(command, args, {\n cwd,\n encoding: 'utf8'\n });\n\n return {\n label,\n command: [command, ...args].join(' '),\n status: Number(result.status || 0),\n stdout: result.stdout || '',\n stderr: result.stderr || '',\n ok: Number(result.status || 0) === 0\n };\n}\n\nfunction stageReportPath(stageId, outputDirAbs) {\n if (stageId === 'cleanup-quarantine-manager') {\n return path.join(outputDirAbs, 'cleanup-quarantine-manifest.json');\n }\n\n if (stageId === 'cross-agent-packager') {\n return '';\n }\n\n return path.join(outputDirAbs, `${stageId}.json`);\n}\n\nfunction buildStageCommands(stageId, options) {\n const outputDirRel = toPosix(path.relative(REPO_ROOT, options.outputDirAbs));\n\n if (stageId === 'cleanup-quarantine-manager') {\n const commands = [\n {\n label: `${stageId}:classify`,\n command: 'node',\n args: ['tools/scripts/cleanup-quarantine-manager.js', '--output-dir', outputDirRel]\n }\n ];\n\n if (options.quarantine) {\n commands.push({\n label: `${stageId}:apply`,\n command: 'node',\n args: ['tools/scripts/cleanup-quarantine-manager.js', '--output-dir', outputDirRel, '--apply']\n });\n }\n\n return commands;\n }",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "audit:repo"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "audit:repo:changed"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "audit:repo:full"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "audit:repo:quarantine"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "audit:repo:pack-all"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/repo-audit-summary.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/scripts/repo-audit-summary.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/repo-audit-summary.json, tools/scripts/repo-audit-summary.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: audit:repo); manual (npm script: audit:repo:changed); manual (npm script: audit:repo:full); manual (npm script: audit:repo:quarantine); manual (npm script: audit:repo:pack-all)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/integration/v2-wcag-audit.selftest.js",
+ "script": "v2-wcag-audit.selftest",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests/integration, v2, git index",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Self-test suite for v2-wcag-audit.js — validates WCAG audit logic against known fixtures",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/integration/v2-wcag-audit.selftest.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script v2-wcag-audit.selftest\n * @category validator\n * @purpose qa:content-quality\n * @scope tests/integration, v2, git index\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Self-test suite for v2-wcag-audit.js — validates WCAG audit logic against known fixtures\n * @pipeline manual — not yet in pipeline\n * @usage node tests/integration/v2-wcag-audit.selftest.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst http = require('http');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\nconst puppeteer = require('puppeteer');\nconst wcag = require('./v2-wcag-audit');\n\nfunction getRepoRoot() {\n return process.cwd();\n}\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nasync function withHttpFixture(html, fn) {\n const server = http.createServer((req, res) => {\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(html);\n });\n\n await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));\n const addr = server.address();\n const url = `http://127.0.0.1:${addr.port}`;\n\n try {\n return await fn(url);\n } finally {\n await new Promise((resolve) => server.close(resolve));\n }\n}\n\nfunction runGit(args) {\n const cmd = spawnSync('git', args, { cwd: getRepoRoot(), encoding: 'utf8' });\n return {\n status: cmd.status,\n stdout: (cmd.stdout || '').trim(),\n stderr: (cmd.stderr || '').trim()\n };\n}\n\nasync function testBrowserAxeFixture() {\n const html = `\n\n \n \n WCAG Fixture \n \n \n \n Fixture
\n
\n 🔗\n Focusable\n \n \n`;\n\n const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] });\n try {\n return await withHttpFixture(html, async (url) => {\n const result = await wcag.runAxeOnUrl(browser, url, {\n file: 'v2/internal/wcag-fixture.mdx',\n routeKey: 'v2/internal/wcag-fixture'\n });\n\n assert.strictEqual(result.ok, true);\n assert.ok(Array.isArray(result.wcagViolations));\n assert.ok(Array.isArray(result.bestPracticeViolations));\n assert.ok(result.wcagViolations.length >= 1, 'expected at least one WCAG violation');\n const ids = new Set(result.wcagViolations.map((v) => v.id));\n assert.ok(ids.has('image-alt') || ids.has('link-name'), 'expected image-alt or link-name violation');\n result.wcagViolations.forEach((v) => {\n assert.strictEqual(v.type, 'wcag');\n assert.ok(['critical', 'serious', 'moderate', 'minor', 'unknown'].includes(v.impact));\n });\n result.bestPracticeViolations.forEach((v) => {\n assert.strictEqual(v.type, 'best-practice');\n });\n return result;\n });\n } finally {\n await browser.close();\n }\n}\n\nasync function testRunAuditFixAndStage() {\n const root = getRepoRoot();\n const nonce = `${Date.now()}-${Math.floor(Math.random() * 100000)}`;\n const rel = `v2/internal/__wcag-selftest-${nonce}.mdx`;\n const abs = path.join(root, rel);\n const reportMd = `/tmp/v2-wcag-selftest-${nonce}.md`;\n const reportJson = `/tmp/v2-wcag-selftest-${nonce}.json`;\n\n fs.writeFileSync(abs, '# Temp WCAG Selftest\\n\\n
\\n', 'utf8');\n\n const testV2Pages = require('../../tools/scripts/test-v2-pages');\n const originalGetV2Pages = testV2Pages.getV2Pages;\n testV2Pages.getV2Pages = function patchedGetV2Pages() {\n const base = originalGetV2Pages.call(this);\n const route = rel.replace(/\\.mdx?$/i, '');\n return [...new Set([...base, route])];\n };\n\n try {\n const noFix = await wcag.runAudit({ argv: ['--files', rel, '--no-fix', '--max-pages', '0', '--report', reportMd, '--report-json', reportJson] });\n assert.ok(fs.readFileSync(abs, 'utf8').includes('
'));\n assert.strictEqual(noFix.results.length, 1);\n assert.strictEqual(noFix.results[0].kind, 'static-only');\n assert.strictEqual(noFix.results[0].autofixes.length, 0);\n assert.ok(noFix.results[0].staticFindings.some((f) => f.rule === 'raw-img-missing-alt'));\n\n const withFix = await wcag.runAudit({ argv: ['--files', rel, '--fix', '--stage', '--max-pages', '0', '--report', reportMd, '--report-json', reportJson] });\n const fixedContent = fs.readFileSync(abs, 'utf8');\n assert.ok(/
]*\\balt=/.test(fixedContent), 'expected img alt autofix');\n assert.ok(withFix.summary.totals.autofixes >= 1);\n\n const staged = runGit(['diff', '--cached', '--name-only', '--', rel]);\n assert.strictEqual(staged.status, 0);\n assert.ok(staged.stdout.split('\\n').map((s) => s.trim()).includes(rel));\n\n // Unstage only the temp file to avoid touching user changes.\n const unstage = runGit(['restore', '--staged', '--', rel]);\n if (unstage.status !== 0) {\n const fallback = runGit(['rm', '--cached', '-q', '--', rel]);\n assert.strictEqual(fallback.status, 0, fallback.stderr || fallback.stdout);\n }\n } finally {\n testV2Pages.getV2Pages = originalGetV2Pages;\n try {\n fs.unlinkSync(abs);\n } catch (_error) {\n // ignore\n }\n try {\n const cleanup = runGit(['restore', '--staged', '--', rel]);\n if (cleanup.status !== 0) runGit(['rm', '--cached', '-q', '--', rel]);\n } catch (_error) {\n // ignore\n }\n try {\n fs.unlinkSync(reportMd);\n } catch (_error) {\n // ignore\n }\n try {\n fs.unlinkSync(reportJson);\n } catch (_error) {\n // ignore\n }\n }\n}\n\nasync function main() {\n const failures = [];\n const run = async (name, fn) => {\n process.stdout.write(`🧪 ${name}... `);\n try {\n await fn();\n console.log('✅');\n } catch (error) {\n console.log('❌');\n failures.push({ name, error });\n }\n };\n\n await run('Browser axe fixture captures WCAG violations and separates best-practice results', testBrowserAxeFixture);\n await run('runAudit respects --no-fix and stages autofix changes with --stage', testRunAuditFixAndStage);\n\n if (failures.length > 0) {\n console.error(`\\n❌ ${failures.length} self-test(s) failed`);\n failures.forEach((f) => console.error(` - ${f.name}: ${f.error.message}`));\n process.exit(1);\n }\n\n console.log('\\n✅ v2 WCAG audit self-tests passed (2 cases)');\n}\n\nmain().catch((error) => {\n console.error(error);\n process.exit(1);\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:wcag:selftest"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tests/integration/v2/internal/__wcag-selftest-${nonce}.mdx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tests/integration/v2/internal/__wcag-selftest-${nonce}.mdx",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:wcag:selftest)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/mdx-component-runtime-smoke.test.js",
+ "script": "mdx-component-runtime-smoke.test",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests/unit, tests/integration",
+ "owner": "docs",
+ "needs": "E-R1, R-R29",
+ "purpose_statement": "Unit tests for the MDX runtime smoke helpers — covers arg parsing, sentinel route selection, trigger logic, and failure classification.",
+ "pipeline_declared": "manual — targeted smoke helper coverage",
+ "usage": "node tests/unit/mdx-component-runtime-smoke.test.js",
+ "header": "#!/usr/bin/env node\n/**\n * @script mdx-component-runtime-smoke.test\n * @category validator\n * @purpose qa:content-quality\n * @scope tests/unit, tests/integration\n * @owner docs\n * @needs E-R1, R-R29\n * @purpose-statement Unit tests for the MDX runtime smoke helpers — covers arg parsing, sentinel route selection, trigger logic, and failure classification.\n * @pipeline manual — targeted smoke helper coverage\n * @usage node tests/unit/mdx-component-runtime-smoke.test.js\n */\n\nconst assert = require('assert');\nconst smoke = require('../integration/mdx-component-runtime-smoke');\n\nlet errors = [];\nlet warnings = [];\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n rule: 'mdx-component-runtime-smoke unit',\n message: `${name}: ${error.message}`,\n line: 1,\n file: 'tests/unit/mdx-component-runtime-smoke.test.js'\n });\n }\n}\n\nasync function runTests() {\n errors = [];\n warnings = [];\n\n console.log('🧪 MDX Runtime Smoke Unit Tests');\n\n await runCase('Parses default args with sentinel routes', async () => {\n const parsed = smoke.parseArgs([]);\n assert.strictEqual(parsed.routes.length, 0);\n assert.ok(Array.isArray(smoke.getRoutes(parsed)));\n assert.strictEqual(smoke.getRoutes(parsed).length, smoke.SENTINEL_ROUTES.length);\n });\n\n await runCase('Parses explicit routes and base URL', async () => {\n const parsed = smoke.parseArgs([\n '--routes',\n '/v2/about/portal,/v2/developers/portal',\n '--base-url',\n 'http://localhost:3001'\n ]);\n assert.deepStrictEqual(smoke.getRoutes(parsed), ['/v2/about/portal', '/v2/developers/portal']);\n assert.strictEqual(parsed.baseUrl, 'http://localhost:3001');\n });\n\n await runCase('Trigger logic includes component, validator, smoke script, and sentinel page changes', async () => {\n assert.strictEqual(smoke.shouldRunForChangedFiles(['snippets/components/primitives/links.jsx']), true);\n assert.strictEqual(\n smoke.shouldRunForChangedFiles(['tools/scripts/validators/components/check-mdx-component-scope.js']),\n true\n );\n assert.strictEqual(smoke.shouldRunForChangedFiles(['tests/integration/mdx-component-runtime-smoke.js']), true);\n assert.strictEqual(smoke.shouldRunForChangedFiles(['v2/home/mission-control.mdx']), true);\n assert.strictEqual(smoke.shouldRunForChangedFiles(['docs-guide/governance-guides/component-governance.mdx']), false);\n });\n\n await runCase('Blocking console and page errors are classified correctly', async () => {\n assert.strictEqual(\n smoke.isBlockingConsoleMessage('error', 'ReferenceError: normalizeIconName is not defined'),\n true\n );\n assert.strictEqual(smoke.isBlockingConsoleMessage('warning', 'ReferenceError: test'), false);\n assert.strictEqual(smoke.isBlockingPageError(\"Cannot access 'BlinkingIcon' before initialization\"), true);\n assert.strictEqual(smoke.isBlockingPageError('ResizeObserver loop limit exceeded'), false);\n });\n\n await runCase('DOM failure classification catches 404, error boundary, empty, and runtime body markers', async () => {\n assert.strictEqual(\n smoke.classifyDomFailure({\n status: 404,\n bodyText: 'Not found',\n bodyLength: 700,\n h1Text: '404'\n }),\n 'Route returned HTTP 404'\n );\n\n assert.strictEqual(\n smoke.classifyDomFailure({\n status: 200,\n bodyText: 'content',\n bodyLength: 700,\n h1Text: 'Mission Control',\n hasErrorBoundary: true\n }),\n 'Page rendered an error boundary'\n );\n\n assert.strictEqual(\n smoke.classifyDomFailure({\n status: 200,\n bodyText: 'tiny',\n bodyLength: 4,\n h1Text: ''\n }),\n 'Page appears empty or failed to render (4 chars)'\n );\n\n assert.match(\n smoke.classifyDomFailure({\n status: 200,\n bodyText: 'ReferenceError: normalizeIconName is not defined',\n bodyLength: 900,\n h1Text: 'Mission Control'\n }),\n /blocking runtime error text/\n );\n });\n\n return {\n errors,\n warnings,\n passed: errors.length === 0,\n total: 5\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ MDX runtime smoke unit tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} MDX runtime smoke unit test failure(s)`);\n result.errors.forEach((err) => console.error(` - ${err.message}`));\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ MDX runtime smoke unit tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/repair-spelling.test.js",
+ "script": "repair-spelling.test",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests/unit, tools/scripts/remediators/content",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Unit tests for repair-spelling.js — validates deterministic spelling fixes and exclusion ranges",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/repair-spelling.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script repair-spelling.test\n * @category validator\n * @purpose qa:content-quality\n * @scope tests/unit, tools/scripts/remediators/content\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Unit tests for repair-spelling.js — validates deterministic spelling fixes and exclusion ranges\n * @pipeline manual — not yet in pipeline\n * @dualmode --dry-run (validator) | --write (remediator)\n * @usage node tests/unit/repair-spelling.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\n\nconst repairSpelling = require('../../tools/scripts/remediators/content/repair-spelling');\n\nlet errors = [];\nlet warnings = [];\n\nfunction rangeContains(ranges, start, end) {\n return ranges.some((range) => start >= range.start && end <= range.end);\n}\n\nfunction createFixture(content) {\n const dirPath = fs.mkdtempSync(path.join(os.tmpdir(), 'repair-spelling-test-'));\n const filePath = path.join(dirPath, 'fixture.mdx');\n fs.writeFileSync(filePath, content, 'utf8');\n return { dirPath, filePath };\n}\n\nfunction cleanupFixture(fixture) {\n if (!fixture) return;\n fs.rmSync(fixture.dirPath, { recursive: true, force: true });\n}\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n rule: 'repair-spelling unit',\n message: `${name}: ${error.message}`,\n line: 1,\n file: 'tests/unit/repair-spelling.test.js'\n });\n }\n}\n\nasync function runTests() {\n errors = [];\n warnings = [];\n\n console.log('🧪 Repair Spelling Unit Tests');\n\n await runCase('Parses default args as dry-run full-scope mode', async () => {\n const parsed = repairSpelling.parseArgs([]);\n assert.strictEqual(parsed.mode, 'dry-run');\n assert.strictEqual(parsed.stagedOnly, false);\n assert.deepStrictEqual(parsed.files, []);\n });\n\n await runCase('Collects eligible prose ranges while excluding frontmatter, code, and URLs', async () => {\n const content = [\n '---',\n 'title: Recieve',\n '---',\n '',\n 'This recieve sentence is prose.',\n '',\n '`recieve`',\n '',\n 'https://example.com/recieve',\n '',\n '```js',\n 'const recieve = true;',\n '```'\n ].join('\\n');\n\n const ranges = await repairSpelling.collectEligibleRanges(content);\n const proseStart = content.indexOf('recieve sentence');\n const inlineCodeStart = content.indexOf('`recieve`') + 1;\n const urlStart = content.indexOf('https://example.com/recieve');\n const frontmatterStart = content.indexOf('Recieve');\n const codeStart = content.indexOf('recieve = true');\n\n assert.strictEqual(rangeContains(ranges, proseStart, proseStart + 'recieve'.length), true);\n assert.strictEqual(rangeContains(ranges, inlineCodeStart, inlineCodeStart + 'recieve'.length), false);\n assert.strictEqual(rangeContains(ranges, urlStart, urlStart + 'https'.length), false);\n assert.strictEqual(rangeContains(ranges, frontmatterStart, frontmatterStart + 'Recieve'.length), false);\n assert.strictEqual(rangeContains(ranges, codeStart, codeStart + 'recieve'.length), false);\n });\n\n await runCase('Uses preferred issue suggestions for plain typos', async () => {\n const resolved = repairSpelling.resolveSafeReplacement({\n issue: {\n text: 'recieve',\n suggestionsEx: [\n { word: 'receive', isPreferred: true }\n ]\n }\n });\n\n assert.strictEqual(resolved.replacement, 'receive');\n assert.strictEqual(resolved.source, 'preferred-issue-suggestion');\n });\n\n await runCase('Prefers shared-dictionary candidate for flagged en-GB terms', async () => {\n const resolved = repairSpelling.resolveSafeReplacement({\n issue: {\n text: 'color',\n isFlagged: true\n },\n suggestions: {\n suggestions: [\n { word: 'colon', cost: 100, forbidden: false, noSuggest: false },\n { word: 'colour', cost: 100, forbidden: false, noSuggest: false },\n { word: 'conor', cost: 100, forbidden: false, noSuggest: false }\n ]\n },\n customWords: new Set(['colour'])\n });\n\n assert.strictEqual(resolved.replacement, 'colour');\n assert.strictEqual(resolved.source, 'custom-dictionary-suggestion');\n });\n\n await runCase('Leaves ambiguous suggestions unchanged', async () => {\n const resolved = repairSpelling.resolveSafeReplacement({\n issue: {\n text: 'thingg',\n isFlagged: false\n },\n suggestions: {\n suggestions: [\n { word: 'things', cost: 100, forbidden: false, noSuggest: false },\n { word: 'thins', cost: 100, forbidden: false, noSuggest: false }\n ]\n },\n customWords: new Set()\n });\n\n assert.strictEqual(resolved.replacement, null);\n assert.strictEqual(resolved.reason, 'ambiguous-suggestions');\n });\n\n await runCase('Skips replacements that drop possessive suffixes', async () => {\n const resolved = repairSpelling.resolveSafeReplacement({\n issue: {\n text: \"Arbitrium's\",\n isFlagged: true\n },\n suggestions: {\n suggestions: [\n { word: 'Arbitrum', cost: 100, forbidden: false, noSuggest: false }\n ]\n },\n customWords: new Set(['arbitrum'])\n });\n\n assert.strictEqual(resolved.replacement, null);\n assert.strictEqual(resolved.reason, 'inflected-form-mismatch');\n });\n\n await runCase('Does not use custom-dictionary fallback for unflagged words', async () => {\n const resolved = repairSpelling.resolveSafeReplacement({\n issue: {\n text: 'Bithumb',\n isFlagged: false\n },\n suggestions: {\n suggestions: [\n { word: 'Github', cost: 100, forbidden: false, noSuggest: false }\n ]\n },\n customWords: new Set(['github'])\n });\n\n assert.strictEqual(resolved.replacement, null);\n assert.strictEqual(resolved.reason, 'ambiguous-suggestions');\n });\n\n await runCase('Dry-run reports repairs without mutating the file', async () => {\n const fixture = createFixture(\n [\n '---',\n 'title: Recieve',\n '---',\n '',\n 'This recieve paragraph uses color.',\n '',\n '`recieve color`',\n '',\n 'https://example.com/recieve/color',\n '',\n '```js',\n \"const recieve = 'color';\",\n '```'\n ].join('\\n')\n );\n\n try {\n const before = fs.readFileSync(fixture.filePath, 'utf8');\n const summary = await repairSpelling.run({\n args: repairSpelling.parseArgs(['--dry-run']),\n files: [fixture.filePath],\n quiet: true\n });\n\n assert.strictEqual(summary.proposedRepairs, 2);\n assert.strictEqual(fs.readFileSync(fixture.filePath, 'utf8'), before);\n } finally {\n cleanupFixture(fixture);\n }\n });",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tests/unit/fixture.mdx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tests/unit/fixture.mdx",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/v2-wcag-audit.test.js",
+ "script": "v2-wcag-audit.test",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests/unit, tests/integration",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Unit tests for v2-wcag-audit.js — tests individual WCAG rules",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/v2-wcag-audit.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script v2-wcag-audit.test\n * @category validator\n * @purpose qa:content-quality\n * @scope tests/unit, tests/integration\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Unit tests for v2-wcag-audit.js — tests individual WCAG rules\n * @pipeline manual — not yet in pipeline\n * @usage node tests/unit/v2-wcag-audit.test.js [flags]\n */\n\nconst assert = require('assert');\nconst path = require('path');\nconst wcag = require('../integration/v2-wcag-audit');\nconst fileWalker = require('../utils/file-walker');\n\nlet errors = [];\nlet warnings = [];\n\nfunction runCase(name, fn) {\n try {\n fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n rule: 'v2-wcag-audit unit',\n message: `${name}: ${error.message}`,\n line: 1,\n file: 'tests/unit/v2-wcag-audit.test.js'\n });\n }\n}\n\nfunction runTests() {\n errors = [];\n warnings = [];\n\n console.log('🧪 v2 WCAG Audit Unit Tests');\n\n runCase('Excludes x-* paths anywhere under v2', () => {\n assert.strictEqual(fileWalker.isExcludedV2ExperimentalPath('v2/x-pages/a.mdx'), true);\n assert.strictEqual(fileWalker.isExcludedV2ExperimentalPath('v2/about/x-archive/a.mdx'), true);\n assert.strictEqual(fileWalker.isExcludedV2ExperimentalPath('v2/about/livepeer-overview.mdx'), false);\n });\n\n runCase('Parses WCAG args with defaults and flags', () => {\n const parsed = wcag.parseArgs(['--staged', '--no-fix', '--fail-impact', 'moderate']);\n assert.strictEqual(parsed.mode, 'staged');\n assert.strictEqual(parsed.fix, false);\n assert.strictEqual(parsed.failImpact, 'moderate');\n assert.strictEqual(parsed.respectMintIgnore, true);\n assert.strictEqual(parsed.maxPages, 10);\n });\n\n runCase('Parses --no-mintignore override', () => {\n const parsed = wcag.parseArgs(['--full', '--no-mintignore']);\n assert.strictEqual(parsed.mode, 'full');\n assert.strictEqual(parsed.respectMintIgnore, false);\n });\n\n runCase('Files mode does not default-cap browser pages', () => {\n const parsed = wcag.parseArgs(['--files', 'v2/about/livepeer-overview.mdx']);\n assert.strictEqual(parsed.mode, 'files');\n assert.strictEqual(parsed.maxPages, null);\n });\n\n runCase('Maps legacy and migrated v2 route keys to URL paths', () => {\n assert.strictEqual(wcag.routeKeyToUrlPath('v2/pages/04_gateways/index'), '/v2/04_gateways');\n assert.strictEqual(wcag.routeKeyToUrlPath('v2/about/livepeer-overview'), '/v2/about/livepeer-overview');\n });\n\n runCase('Supports v2-aware file route key mapping', () => {\n const root = process.cwd();\n const migrated = path.join(root, 'v2', 'about', 'livepeer-overview.mdx');\n const legacy = path.join(root, 'v2', 'x-pages', '00_home', 'home', 'primer.mdx');\n assert.strictEqual(wcag.fileToV2RouteKey(migrated), 'v2/about/livepeer-overview');\n assert.strictEqual(wcag.fileToV2RouteKey(legacy), '');\n });\n\n runCase('Severity threshold logic blocks expected impacts', () => {\n assert.strictEqual(wcag.severityMeetsThreshold('critical', 'serious'), true);\n assert.strictEqual(wcag.severityMeetsThreshold('serious', 'serious'), true);\n assert.strictEqual(wcag.severityMeetsThreshold('moderate', 'serious'), false);\n assert.strictEqual(wcag.severityMeetsThreshold('minor', 'none'), false);\n });\n\n runCase('Color contrast is normalized to advisory-only (non-blocking)', () => {\n const normalized = wcag.normalizeAxeViolation({\n id: 'color-contrast',\n impact: 'serious',\n tags: ['wcag2aa']\n });\n assert.strictEqual(normalized.impact, 'none');\n assert.strictEqual(normalized.advisory, true);\n assert.strictEqual(wcag.severityMeetsThreshold(normalized.impact, 'serious'), false);\n });\n\n runCase('Conservative autofix inserts iframe title, img alt, and icon-link aria-label with line numbers', () => {\n const content = [\n '# Demo',\n '',\n '
',\n ' '\n ].join('\\n');\n\n const out = wcag.applyConservativeAutofixes(content, 'v2/internal/test.mdx', { fix: true });\n assert.strictEqual(out.changed, true);\n assert.ok(out.content.includes('title=\"Embedded YouTube video\"'));\n assert.ok(out.content.includes('alt=\"Livepeer Stats\"') || out.content.includes('alt=\"Livepeer Stats.png\"') === false);\n assert.ok(out.content.includes('aria-label='));\n const rules = out.findings.map((f) => f.rule).sort();\n assert.deepStrictEqual(rules, [\n 'raw-anchor-missing-accessible-name',\n 'raw-iframe-missing-title',\n 'raw-img-missing-alt'\n ]);\n const iframeFinding = out.findings.find((f) => f.rule === 'raw-iframe-missing-title');\n const imgFinding = out.findings.find((f) => f.rule === 'raw-img-missing-alt');\n const anchorFinding = out.findings.find((f) => f.rule === 'raw-anchor-missing-accessible-name');\n assert.strictEqual(iframeFinding.line, 2);\n assert.strictEqual(imgFinding.line, 3);\n assert.strictEqual(anchorFinding.line, 4);\n assert.strictEqual(out.autofixes.length, 3);\n });\n\n runCase('Autofix is idempotent on second pass', () => {\n const content = '\\n';\n const first = wcag.applyConservativeAutofixes(content, 'v2/internal/one.mdx', { fix: true });\n const second = wcag.applyConservativeAutofixes(first.content, 'v2/internal/one.mdx', { fix: true });\n assert.strictEqual(first.changed, true);\n assert.strictEqual(second.changed, false);\n assert.strictEqual(second.autofixes.length, 0);\n assert.strictEqual(second.findings.length, 0);\n });\n\n runCase('No-fix mode reports findings without mutating content', () => {\n const content = '
';\n const out = wcag.applyConservativeAutofixes(content, 'v2/internal/two.mdx', { fix: false });\n assert.strictEqual(out.changed, false);\n assert.strictEqual(out.content, content);\n assert.strictEqual(out.findings.length, 1);\n assert.strictEqual(out.findings[0].fixed, false);\n });\n\n runCase('Summary/report helpers produce deterministic sorted rule keys and suggestions', () => {\n const results = [\n {\n file: 'v2/b.mdx',\n wcagViolations: [\n { id: 'color-contrast', impact: 'serious', suggestion: 'Fix contrast.' },\n { id: 'image-alt', impact: 'serious', suggestion: 'Add alt.' }\n ],\n bestPracticeViolations: [],\n incomplete: [],\n staticFindings: [],\n autofixes: []\n },\n {\n file: 'v2/a.mdx',\n wcagViolations: [{ id: 'color-contrast', impact: 'serious', suggestion: 'Fix contrast.' }],\n bestPracticeViolations: [],\n incomplete: [],\n staticFindings: [],\n autofixes: []\n }\n ];\n const staticFindings = [\n { rule: 'raw-img-missing-alt', impact: 'serious', fixed: false },\n { rule: 'raw-iframe-missing-title', impact: 'moderate', fixed: true }\n ];\n const summary = wcag.summarizeResults(results, staticFindings, 'serious');\n const keys = Object.keys(summary.byRule);\n assert.deepStrictEqual(keys, [...keys].sort());\n const suggestions = wcag.collectTopSuggestions(summary);\n assert.strictEqual(suggestions[0].rule, 'color-contrast');\n const report = wcag.buildJsonReport({\n args: {\n mode: 'full',\n failImpact: 'serious',\n fix: true,\n maxPages: null,\n report: wcag.DEFAULT_REPORT_MD,\n reportJson: wcag.DEFAULT_REPORT_JSON\n },\n files: [path.join(process.cwd(), 'v2', 'a.mdx')],\n excludedInputs: [],\n browserAuditedCount: 1,\n staticOnlyCount: 0,\n results,\n allStaticFindings: staticFindings,\n summary,\n suggestions,\n baseUrl: 'http://localhost:3000'\n });\n const markdown = wcag.buildMarkdownReport(report);\n assert.ok(markdown.includes('# V2 WCAG Accessibility Audit Report'));\n assert.ok(markdown.includes('color-contrast'));\n });\n\n return {\n errors,\n warnings,\n passed: errors.length === 0,\n total: 12\n };\n}\n\nif (require.main === module) {\n const result = runTests();\n if (result.passed) {\n console.log(`\\n✅ v2 WCAG audit unit tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} v2 WCAG audit unit test failure(s)`);\n result.errors.forEach((err) => console.error(` - ${err.message}`));\n process.exit(1);\n}\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:wcag:unit"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:wcag:unit)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/add-pagetype-mechanical.js",
+ "script": "add-pagetype-mechanical",
+ "category": "remediator",
+ "purpose": "qa:content-quality",
+ "scope": "tools/scripts, v2, tasks/reports",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Mechanically assigns pageType frontmatter to eligible v2 MDX pages.",
+ "pipeline_declared": "manual — deterministic metadata rollout utility for v2 docs",
+ "usage": "node tools/scripts/add-pagetype-mechanical.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script add-pagetype-mechanical\n * @category remediator\n * @purpose qa:content-quality\n * @scope tools/scripts, v2, tasks/reports\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Mechanically assigns pageType frontmatter to eligible v2 MDX pages.\n * @pipeline manual — deterministic metadata rollout utility for v2 docs\n * @usage node tools/scripts/add-pagetype-mechanical.js [flags]\n */\n\n'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\nconst matter = require('gray-matter');\n\nconst REPO_ROOT = path.resolve(__dirname, '..', '..');\nconst V2_ROOT = path.join(REPO_ROOT, 'v2');\nconst EXCLUDED_SEGMENTS = new Set(['cn', 'es', 'fr', 'views', 'groups']);\nconst SUMMARY_TYPES = ['reference', 'landing', 'quickstart', 'glossary', 'overview'];\n\nfunction toPosix(filePath) {\n return String(filePath || '').split(path.sep).join('/');\n}\n\nfunction detectNewline(content) {\n return String(content || '').includes('\\r\\n') ? '\\r\\n' : '\\n';\n}\n\nfunction printUsage() {\n console.log('Usage: node tools/scripts/add-pagetype-mechanical.js [--dry-run]');\n}\n\nfunction parseArgs(argv) {\n const args = {\n dryRun: false\n };\n\n for (const token of argv) {\n if (token === '--dry-run') {\n args.dryRun = true;\n continue;\n }\n if (token === '--help' || token === '-h') {\n printUsage();\n process.exit(0);\n }\n throw new Error(`Unknown argument: ${token}`);\n }\n\n return args;\n}\n\nfunction isExcluded(relPath) {\n const segments = toPosix(relPath).split('/');\n return segments.some((segment) => EXCLUDED_SEGMENTS.has(segment) || /^x-[^/]+$/i.test(segment));\n}\n\nfunction walkMdxFiles(dirPath, out = []) {\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = path.join(dirPath, entry.name);\n const relPath = toPosix(path.relative(REPO_ROOT, fullPath));\n\n if (isExcluded(relPath)) {\n continue;\n }\n\n if (entry.isDirectory()) {\n walkMdxFiles(fullPath, out);\n continue;\n }\n\n if (entry.isFile() && /\\.mdx$/i.test(entry.name)) {\n out.push(relPath);\n }\n }\n\n return out;\n}\n\nfunction extractFrontmatterBlock(content) {\n const match = String(content || '').match(/^---\\r?\\n([\\s\\S]*?)\\r?\\n---(\\r?\\n|$)/);\n if (!match) {\n return {\n exists: false,\n raw: '',\n full: '',\n body: String(content || '')\n };\n }\n\n return {\n exists: true,\n raw: match[1],\n full: match[0],\n body: String(content || '').slice(match[0].length)\n };\n}\n\nfunction hasField(frontmatterRaw, fieldName) {\n const pattern = new RegExp(`^${fieldName}\\\\s*:`, 'm');\n return pattern.test(String(frontmatterRaw || ''));\n}\n\nfunction hasModeFrame(frontmatterRaw) {\n return /^mode:\\s*['\"]?frame['\"]?\\s*$/m.test(String(frontmatterRaw || ''));\n}\n\nfunction classifyFile(relPath, frontmatterRaw) {\n const normalized = toPosix(relPath);\n const fileName = path.basename(normalized).toLowerCase();\n\n if (hasField(frontmatterRaw, 'openapi')) return { type: 'reference', rule: 1 };\n if (hasModeFrame(frontmatterRaw)) return { type: 'landing', rule: 2 };\n if (fileName.includes('portal') || /-hub\\.mdx$/i.test(fileName) || /-path\\.mdx$/i.test(fileName)) {\n return { type: 'landing', rule: 3 };\n }\n if (normalized.includes('/quickstart/') || fileName === 'quickstart.mdx') {\n return { type: 'quickstart', rule: 4 };\n }\n if (fileName.includes('glossary')) return { type: 'glossary', rule: 5 };\n if (fileName.includes('faq')) return { type: 'reference', rule: 6 };\n if (normalized.includes('/api-reference/') && fileName === 'overview.mdx') {\n return { type: 'overview', rule: 7 };\n }\n if (normalized.includes('/api-reference/') && !hasField(frontmatterRaw, 'openapi') && fileName !== 'overview.mdx') {\n return { type: 'reference', rule: 8 };\n }\n if (fileName === 'overview.mdx' || fileName === 'index.mdx') {\n return { type: 'overview', rule: 9 };\n }\n return { type: '', rule: 10 };\n}\n\nfunction insertPageType(frontmatterRaw, pageType, newline) {\n const lines = String(frontmatterRaw || '').split(/\\r?\\n/);\n if (lines.some((line) => /^pageType\\s*:/.test(line))) {\n return frontmatterRaw;\n }\n\n let insertAfterIndex = lines.findIndex((line) => /^description\\s*:/.test(line));\n if (insertAfterIndex === -1) {\n insertAfterIndex = lines.findIndex((line) => /^title\\s*:/.test(line));\n }\n if (insertAfterIndex === -1) {\n insertAfterIndex = lines.findIndex((line) => String(line || '').trim() !== '');\n }\n\n const insertAt = insertAfterIndex === -1 ? lines.length : getInsertionIndex(lines, insertAfterIndex);\n lines.splice(insertAt, 0, `pageType: ${pageType}`);\n return lines.join(newline);\n}\n\nfunction getInsertionIndex(lines, keyIndex) {\n const currentLine = String(lines[keyIndex] || '');\n const indentMatch = currentLine.match(/^(\\s*)/);\n const baseIndent = indentMatch ? indentMatch[1].length : 0;\n const valueMatch = currentLine.match(/^[^:]+:\\s*(.*)$/);\n const value = valueMatch ? valueMatch[1].trim() : '';\n const startsBlock = value === '' || value.startsWith('|') || value.startsWith('>');\n\n if (!startsBlock) {\n return keyIndex + 1;\n }\n\n let cursor = keyIndex;\n while (cursor + 1 < lines.length) {\n const nextLine = String(lines[cursor + 1] || '');\n const trimmed = nextLine.trim();\n if (!trimmed) {\n cursor += 1;\n continue;\n }\n\n const nextIndentMatch = nextLine.match(/^(\\s*)/);\n const nextIndent = nextIndentMatch ? nextIndentMatch[1].length : 0;\n if (nextIndent > baseIndent) {\n cursor += 1;\n continue;\n }\n\n break;\n }\n\n return cursor + 1;\n}\n\nfunction buildUpdatedContent(content, pageType) {\n const newline = detectNewline(content);\n const frontmatter = extractFrontmatterBlock(content);\n if (!frontmatter.exists) {\n return '';\n }\n\n const nextFrontmatter = insertPageType(frontmatter.raw, pageType, newline);\n return `---${newline}${nextFrontmatter}${newline}---${newline}${frontmatter.body}`;\n}\n\nfunction validateUpdatedFrontmatter(content, relPath) {\n try {\n matter(content);\n } catch (error) {\n throw new Error(`${relPath}: invalid frontmatter after pageType insertion: ${error.message}`);\n }\n}\n\nfunction writeOperations(operations) {\n operations.forEach((operation) => {\n fs.writeFileSync(operation.absPath, operation.content, 'utf8');\n });\n}\n\nfunction printSummary(summary) {\n console.log('Phase 1 classification complete:');\n console.log(` reference: ${summary.reference}`);\n console.log(` landing: ${summary.landing}`);",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": false,
+ "purpose_match": false,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope",
+ "header-json-category-mismatch",
+ "header-json-purpose-mismatch"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/assign-purpose-metadata.js",
+ "script": "assign-purpose-metadata",
+ "category": "generator",
+ "purpose": "qa:content-quality",
+ "scope": "tools/scripts, tools/lib/docs-usefulness, tools/config, v2",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Purpose metadata assigner — fills purpose and audience frontmatter for routable v2 pages",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/assign-purpose-metadata.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script assign-purpose-metadata\n * @category generator\n * @purpose qa:content-quality\n * @scope tools/scripts, tools/lib/docs-usefulness, tools/config, v2\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Purpose metadata assigner — fills purpose and audience frontmatter for routable v2 pages\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/assign-purpose-metadata.js [flags]\n */\n\n'use strict';\n\nconst fs = require('fs');\nconst path = require('path');\nconst matter = require('gray-matter');\nconst { execSync } = require('child_process');\nconst {\n getDocsJsonRouteKeys,\n toDocsRouteKeyFromFileV2Aware,\n getV2DocsFiles\n} = require('../../tests/utils/file-walker');\nconst {\n audienceTokensFromRaw,\n loadAudienceNormalization,\n loadLlmTiers,\n AUDIENCE_ENUM,\n PURPOSE_ENUM\n} = require('../lib/docs-usefulness/rubric-loader');\nconst { analyzeMdxPage } = require('../lib/docs-usefulness/scoring');\nconst prompts = require('../lib/docs-usefulness/prompts');\nconst { loadAndValidateUsefulnessConfig } = require('../lib/docs-usefulness/config-validator');\n\nconst OPENROUTER_URL = 'https://openrouter.ai/api/v1/chat/completions';\n\nfunction stripFrontmatter(content) {\n return String(content || '').replace(/^---\\s*\\n[\\s\\S]*?\\n---\\s*\\n?/, '');\n}\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nfunction toPosix(filePath) {\n return String(filePath || '').split(path.sep).join('/');\n}\n\nfunction parseArgs(argv) {\n const args = {\n dryRun: false,\n scope: 'pilot',\n files: [],\n classifyWithLlm: false,\n llmTier: 'free'\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--dry-run') {\n args.dryRun = true;\n continue;\n }\n if (token === '--scope') {\n args.scope = String(argv[i + 1] || args.scope).trim();\n i += 1;\n continue;\n }\n if (token === '--files' || token === '--file') {\n const raw = String(argv[i + 1] || '').trim();\n if (raw) {\n raw.split(',').map((part) => part.trim()).filter(Boolean).forEach((file) => args.files.push(file));\n }\n i += 1;\n continue;\n }\n if (token === '--classify-with-llm') {\n args.classifyWithLlm = true;\n continue;\n }\n if (token === '--llm-tier') {\n args.llmTier = String(argv[i + 1] || args.llmTier).trim();\n i += 1;\n continue;\n }\n }\n\n args.files = [...new Set(args.files)];\n return args;\n}\n\nfunction getAllRoutablePages(repoRoot) {\n const docsRoutes = getDocsJsonRouteKeys(repoRoot);\n const files = getV2DocsFiles({ rootDir: repoRoot, respectMintIgnore: true }).filter((file) => /\\.mdx$/i.test(file));\n return files\n .filter((abs) => {\n const key = toDocsRouteKeyFromFileV2Aware(abs, repoRoot);\n return key && docsRoutes.has(key);\n })\n .map((abs) => toPosix(path.relative(repoRoot, abs)))\n .sort((a, b) => a.localeCompare(b));\n}\n\nfunction isPilotFile(relPath) {\n const base = path.basename(relPath, '.mdx').toLowerCase();\n return (\n /portal/.test(base) ||\n /overview/.test(base) ||\n /get-started/.test(base) ||\n /^faq/.test(base) ||\n /troubleshoot/.test(base) ||\n /glossary/.test(base)\n );\n}\n\nconst PURPOSE_RULES = [\n { test: (p) => /portal/i.test(path.basename(p)), purpose: 'landing', source: 'filename' },\n { test: (p) => /mission-control/i.test(path.basename(p)), purpose: 'landing', source: 'filename' },\n { test: (p) => path.basename(p) === 'index.mdx', purpose: 'landing', source: 'filename' },\n { test: (p) => /quickstart/i.test(path.basename(p)), purpose: 'tutorial', source: 'filename' },\n { test: (p) => /get-started/i.test(path.basename(p)), purpose: 'tutorial', source: 'filename' },\n { test: (p) => /primer/i.test(path.basename(p)), purpose: 'tutorial', source: 'filename' },\n { test: (p) => /^first-/i.test(path.basename(p)), purpose: 'tutorial', source: 'filename' },\n { test: (p) => /^faq/i.test(path.basename(p)), purpose: 'faq', source: 'filename' },\n { test: (p) => /troubleshoot/i.test(path.basename(p)), purpose: 'troubleshooting', source: 'filename' },\n { test: (p) => /glossary/i.test(path.basename(p)), purpose: 'glossary', source: 'filename' },\n { test: (p) => /changelog|release-notes/i.test(path.basename(p)), purpose: 'changelog', source: 'filename' },\n { test: (p) => /api-reference|config-flags/i.test(path.basename(p)), purpose: 'reference', source: 'filename' },\n { test: (p) => /overview/i.test(path.basename(p)), purpose: 'overview', source: 'filename' },\n { test: (p) => /\\/references?\\//i.test(p), purpose: 'reference', source: 'directory' }\n];\n\nfunction inferPurposeByRules(relPath, content) {\n for (const rule of PURPOSE_RULES) {\n if (rule.test(relPath)) {\n return { purpose: rule.purpose, source: rule.source };\n }\n }\n\n const page = analyzeMdxPage({ content, filePath: relPath, routePath: `/${relPath.replace(/\\.mdx$/i, '').replace(/\\/index$/i, '')}` });\n const accordionCount = (String(page.content || '').match(/]/g) || []).length;\n\n if ((page.components || []).includes('Steps')) {\n return { purpose: 'how_to', source: 'content' };\n }\n if (accordionCount >= 5) {\n return { purpose: 'faq', source: 'content' };\n }\n if ((page.wordCount || 0) < 150 && (page.components || []).some((component) => ['Card', 'CardGroup', 'GotoCard', 'DisplayCard'].includes(component))) {\n return { purpose: 'landing', source: 'content' };\n }\n if ((page.wordCount || 0) > 300 && (page.headings || []).length >= 3 && !(page.components || []).includes('Steps')) {\n return { purpose: 'concept', source: 'content' };\n }\n\n return { purpose: 'unclassified', source: 'none' };\n}\n\nfunction sectionFromPath(relPath) {\n const parts = String(relPath || '').split('/');\n if (parts[0] === 'v2') return parts[1] || 'unknown';\n return parts[0] || 'unknown';\n}\n\nfunction inferAudience(frontmatterValue, section, normalization) {\n const candidates = audienceTokensFromRaw(frontmatterValue, normalization);\n if (candidates.length > 0) {\n if (candidates.length === 1) {\n return { audience: candidates[0], source: 'frontmatter', candidates };\n }\n const sectionDefault = normalization.section_defaults?.[section];\n if (sectionDefault && candidates.includes(sectionDefault)) {\n return { audience: sectionDefault, source: 'frontmatter', candidates };\n }\n const precedence = normalization.deterministic_precedence || AUDIENCE_ENUM;\n for (const audience of precedence) {\n if (candidates.includes(audience)) {\n return { audience, source: 'frontmatter', candidates };\n }\n }\n return { audience: candidates[0], source: 'frontmatter', candidates };\n }\n\n return {\n audience: normalization.section_defaults?.[section] || 'everyone',\n source: 'section',\n candidates\n };\n}\n\nasync function classifyPurposeWithLlm({ apiKey, tier, relPath, title, content }) {\n const llmConfig = loadLlmTiers();\n const tierConfig = llmConfig.tiers?.[tier] || llmConfig.tiers?.free;\n const models = tierConfig.models || [];\n if (!models.length) return null;\n\n const prompt = `Given this documentation page, what is its primary purpose?\\nRespond with exactly one of: landing, overview, concept, how_to, tutorial, reference, faq, glossary, changelog, troubleshooting\\n\\nDefinitions:\\n- landing: Routes users (portal, index, navigation pages)\\n- overview: Orients (\"what is this and why care\")\\n- concept: Explains one idea/mechanism in depth\\n- how_to: Task completion guide (assumes context)\\n- tutorial: Zero-to-result walkthrough (assumes nothing)\\n- reference: Lookup-oriented technical details\\n- faq: Common questions answered\\n- glossary: Term definitions\\n- changelog: Version history\\n- troubleshooting: Fix problems (symptom->cause->fix)\\n\\nPage title: ${title || ''}\\nPage path: ${relPath}\\nFirst 1500 words: ${String(content || '').split(/\\s+/).slice(0, 1500).join(' ')}\\n\\nRespond with ONLY the purpose type, nothing else.`;\n\n for (const model of models) {\n const response = await fetch(OPENROUTER_URL, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n 'HTTP-Referer': 'https://docs.livepeer.org',\n 'User-Agent': 'livepeer-docs-purpose-assigner/1.0'\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: 'system', content: 'Classify documentation page purpose with one enum token only.' },\n { role: 'user', content: prompt }\n ],\n max_tokens: 16,\n temperature: 0",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/dev/test-add-callouts.js",
+ "script": "test-add-callouts",
+ "category": "remediator",
+ "purpose": "qa:content-quality",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Test for add-callouts.js — validates callout insertion logic against fixtures",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/dev/test-add-callouts.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script test-add-callouts\n * @category remediator\n * @purpose qa:content-quality\n * @scope tools/scripts\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Test for add-callouts.js — validates callout insertion logic against fixtures\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/dev/test-add-callouts.js [flags]\n */\n\n/**\n * Test suite for add-callouts.js script\n * \n * Tests the logic for detecting content and adding appropriate callouts\n * \n * Usage: node test-add-callouts.js\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\n\n// Test cases\nconst testCases = [\n {\n name: 'Empty page with only metadata',\n content: `---\ntitle: 'Test Page'\n---\n`,\n expectedCallout: 'ComingSoonCallout',\n shouldHaveContent: false\n },\n {\n name: 'Page with only title heading',\n content: `---\ntitle: 'Test Page'\n---\n\n# Test Page\n`,\n expectedCallout: 'ComingSoonCallout',\n shouldHaveContent: false\n },\n {\n name: 'Page with content',\n content: `---\ntitle: 'Test Page'\n---\n\n# Test Page\n\nThis is some actual content that makes this a real page.\n`,\n expectedCallout: 'PreviewCallout',\n shouldHaveContent: true\n },\n {\n name: 'Page with imports and content',\n content: `---\ntitle: 'Test Page'\n---\n\nimport { SomeComponent } from '/snippets/components/test.jsx'\n\n# Test Page\n\nThis page has content and imports.\n`,\n expectedCallout: 'PreviewCallout',\n shouldHaveContent: true\n },\n {\n name: 'Page that already has ComingSoonCallout',\n content: `---\ntitle: 'Test Page'\n---\n\nimport {ComingSoonCallout} from '/snippets/components/primitives/previewCallouts.jsx'\n\n \n`,\n expectedCallout: null, // Should skip\n shouldHaveContent: false,\n hasExistingCallout: true\n },\n {\n name: 'Page that already has PreviewCallout',\n content: `---\ntitle: 'Test Page'\n---\n\nimport { PreviewCallout } from '/snippets/components/primitives/previewCallouts.jsx'\n\n \n\n# Test Page\n\nSome content here.\n`,\n expectedCallout: null, // Should skip\n shouldHaveContent: true,\n hasExistingCallout: true\n },\n {\n name: 'Remove top-level PreviewCallout with content',\n content: `---\ntitle: 'Test Page'\n---\n\nimport { PreviewCallout } from '/snippets/components/primitives/previewCallouts.jsx'\n\n \n\n# Test Page\n\nSome content here.\n`,\n removal: {\n shouldRemove: true,\n shouldHavePreviewImport: false,\n shouldHaveCallout: false\n }\n },\n {\n name: 'Remove top-level ComingSoonCallout with content',\n content: `---\ntitle: 'Test Page'\n---\n\nimport {ComingSoonCallout} from '/snippets/components/primitives/previewCallouts.jsx'\n\n \n\n# Test Page\n\nSome content here.\n`,\n removal: {\n shouldRemove: true,\n shouldHavePreviewImport: false,\n shouldHaveCallout: false\n }\n },\n {\n name: 'Do not remove callout used later in examples',\n content: `---\ntitle: 'Test Page'\n---\n\nimport { PreviewCallout, ReviewCallout } from '/snippets/components/primitives/previewCallouts.jsx'\n\n \n\n# Test Page\n\nSome content here.\n\n## Examples\n\n \n`,\n removal: {\n shouldRemove: true,\n shouldHavePreviewImport: true,\n shouldHaveCallout: true\n }\n },\n {\n name: 'Skip removal on page without content',\n content: `---\ntitle: 'Test Page'\n---\n\nimport { PreviewCallout } from '/snippets/components/primitives/previewCallouts.jsx'\n\n \n`,\n removal: {\n shouldRemove: false,\n shouldHavePreviewImport: true,\n shouldHaveCallout: true\n }\n }\n];\n\n// Import the functions we need to test (simplified versions for testing)\nfunction hasContent(content) {\n const parts = content.split('---');\n if (parts.length < 3) return false;\n \n const afterMetadata = parts.slice(2).join('---').trim();\n const withoutImports = afterMetadata.replace(/^import\\s+.*$/gm, '').trim();\n const withoutCallouts = withoutImports\n .replace(/ /g, '')\n .replace(/ /g, '')\n .trim();\n \n const lines = withoutCallouts.split('\\n').filter(line => line.trim().length > 0);\n \n if (lines.length === 0 || (lines.length === 1 && lines[0].trim().startsWith('#'))) {\n return false;\n }\n \n return true;\n}\n\nfunction hasCallout(content) {\n return content.includes(']",
+ "header": "#!/usr/bin/env node\n/**\n * @script check-anchor-usage\n * @category validator\n * @purpose qa:content-quality\n * @scope tools/scripts/validators/content, v2\n * @owner docs\n * @needs R-R14, R-C6\n * @purpose-statement Validates same-page anchor links in maintained v2 MDX files against heading IDs on the same page\n * @pipeline manual, ci\n * @usage node tools/scripts/validators/content/check-anchor-usage.js [--json] [--scope ]\n */\n// Baseline 2026-03-09: 100 errors, 8106 warnings - wired as advisory until debt cleared\n// Promote to blocking once error count reaches 0\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst REPO_ROOT = getRepoRoot();\nconst V2_ROOT = path.join(REPO_ROOT, 'v2');\n\nfunction getRepoRoot() {\n const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n if (result.status === 0 && String(result.stdout || '').trim()) {\n return String(result.stdout || '').trim();\n }\n return process.cwd();\n}\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction usage() {\n console.log(\n 'Usage: node tools/scripts/validators/content/check-anchor-usage.js [--json] [--scope ]'\n );\n}\n\nfunction parseArgs(argv) {\n const args = {\n json: false,\n scope: ''\n };\n\n for (let index = 0; index < argv.length; index += 1) {\n const token = argv[index];\n\n if (token === '--json') {\n args.json = true;\n continue;\n }\n\n if (token === '--scope') {\n args.scope = String(argv[index + 1] || '').trim();\n index += 1;\n continue;\n }\n\n if (token.startsWith('--scope=')) {\n args.scope = token.slice('--scope='.length).trim();\n continue;\n }\n\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (argv.includes('--scope') && !args.scope) {\n throw new Error('Missing value for --scope');\n }\n\n return args;\n}\n\nfunction shouldExclude(repoPath) {\n const relPath = toPosix(repoPath).replace(/^\\/+/, '');\n if (!relPath.startsWith('v2/')) return true;\n if (!/\\.mdx$/i.test(relPath)) return true;\n if (relPath.startsWith('v2/internal/')) return true;\n if (relPath.includes('/_contextData_/') || relPath.includes('/_context_data_/')) return true;\n if (relPath.includes('/_move_me/') || relPath.includes('/_tests-to-delete/')) return true;\n if (relPath.endsWith('/todo.mdx') || relPath.endsWith('/NOTES_V2.md') || relPath.endsWith('/todo.txt')) return true;\n return relPath.split('/').some((segment) => segment.toLowerCase().startsWith('x-'));\n}\n\nfunction globToRegExp(glob) {\n let pattern = String(glob || '').trim().replace(/^\\.?\\//, '');\n if (!pattern) return null;\n\n let out = '^';\n for (let index = 0; index < pattern.length; index += 1) {\n const char = pattern[index];\n const next = pattern[index + 1];\n\n if (char === '*') {\n if (next === '*') {\n out += '.*';\n index += 1;\n } else {\n out += '[^/]*';\n }\n continue;\n }\n\n if (char === '?') {\n out += '.';\n continue;\n }\n\n if ('\\\\.[]{}()+-^$|'.includes(char)) {\n out += `\\\\${char}`;\n continue;\n }\n\n out += char;\n }\n\n out += '$';\n return new RegExp(out);\n}\n\nfunction walkFiles(dirPath, out = []) {\n if (!fs.existsSync(dirPath)) return out;\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n\n entries.forEach((entry) => {\n if (entry.name === '.git' || entry.name === 'node_modules') {\n return;\n }\n\n const absPath = path.join(dirPath, entry.name);\n if (entry.isDirectory()) {\n walkFiles(absPath, out);\n return;\n }\n\n const relPath = toPosix(path.relative(REPO_ROOT, absPath));\n if (shouldExclude(relPath)) return;\n out.push({ absPath, relPath });\n });\n\n return out;\n}\n\nfunction loadTargetFiles(scopeGlob) {\n const matcher = scopeGlob ? globToRegExp(scopeGlob) : null;\n return walkFiles(V2_ROOT)\n .filter((entry) => (!matcher ? true : matcher.test(entry.relPath)))\n .sort((left, right) => left.relPath.localeCompare(right.relPath));\n}\n\nfunction maskComments(content) {\n return String(content || '')\n .replace(/\\{\\/\\*[\\s\\S]*?\\*\\/\\}/g, (match) => match.replace(/[^\\n]/g, ' '))\n .replace(//g, (match) => match.replace(/[^\\n]/g, ' '));\n}\n\nfunction decodeFragment(value) {\n try {\n return decodeURIComponent(value);\n } catch (_error) {\n return value;\n }\n}\n\nfunction normalizeAnchorId(value) {\n return decodeFragment(String(value || '').trim().replace(/^#/, ''))\n .toLowerCase()\n .replace(/\\s+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\nfunction slugify(value) {\n return String(value || '')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\nfunction stripHeadingMarkup(value) {\n return String(value || '')\n .replace(/`([^`]+)`/g, '$1')\n .replace(/!\\[([^\\]]*)\\]\\([^)]+\\)/g, '$1')\n .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1')\n .replace(/<[^>]+>/g, ' ')\n .replace(/[*_~]/g, ' ')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\nfunction collectHeadingIds(content) {\n const lines = maskComments(content).split('\\n');\n const headings = [];\n const slugCounts = new Map();\n\n let inFrontmatter = lines[0] && lines[0].trim() === '---';\n let inCodeFence = false;\n\n lines.forEach((line, index) => {\n const trimmed = String(line || '').trim();\n\n if (inFrontmatter) {\n if (index > 0 && trimmed === '---') {\n inFrontmatter = false;\n }\n return;\n }\n\n if (/^```/.test(trimmed)) {\n inCodeFence = !inCodeFence;\n return;\n }\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/validators/content/check-description-quality.js",
+ "script": "check-description-quality",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tools/scripts/validators/content, v2",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Validates English v2 frontmatter descriptions for SEO length, boilerplate openings, and duplicate reuse",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/validators/content/check-description-quality.js [--path ] [--strict]",
+ "header": "#!/usr/bin/env node\n/**\n * @script check-description-quality\n * @category validator\n * @purpose qa:content-quality\n * @scope tools/scripts/validators/content, v2\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Validates English v2 frontmatter descriptions for SEO length, boilerplate openings, and duplicate reuse\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/validators/content/check-description-quality.js [--path ] [--strict]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst yaml = require('js-yaml');\n\nconst REPO_ROOT = path.resolve(__dirname, '../../../../');\nconst DOCS_JSON_PATH = path.join(REPO_ROOT, 'docs.json');\nconst SUPPORTED_EXTENSIONS = new Set(['.mdx', '.md']);\nconst BOILERPLATE_OPENINGS = [\n 'this page',\n 'this document',\n 'this section',\n 'welcome to',\n 'a page about',\n 'an overview of'\n];\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction usage() {\n console.log(\n 'Usage: node tools/scripts/validators/content/check-description-quality.js [--path ] [--strict]'\n );\n}\n\nfunction parseArgs(argv) {\n const options = {\n targetPath: '',\n strict: false\n };\n\n for (let index = 0; index < argv.length; index += 1) {\n const token = argv[index];\n\n if (token === '--strict') {\n options.strict = true;\n continue;\n }\n\n if (token === '--help' || token === '-h') {\n usage();\n process.exit(0);\n }\n\n if (token === '--path') {\n options.targetPath = String(argv[index + 1] || '').trim();\n index += 1;\n continue;\n }\n\n if (token.startsWith('--path=')) {\n options.targetPath = token.slice('--path='.length).trim();\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (argv.includes('--path') && !options.targetPath) {\n throw new Error('Missing value for --path');\n }\n\n return options;\n}\n\nfunction normalizeRoutePath(routePath) {\n return toPosix(routePath)\n .trim()\n .replace(/^\\/+/, '')\n .replace(/\\.(md|mdx)$/i, '')\n .replace(/\\/index$/i, '')\n .replace(/\\/+$/, '');\n}\n\nfunction shouldExclude(repoPath) {\n const relPath = toPosix(repoPath).replace(/^\\/+/, '');\n if (!relPath.startsWith('v2/')) return true;\n if (relPath.startsWith('v2/es/') || relPath.startsWith('v2/fr/') || relPath.startsWith('v2/cn/')) return true;\n if (relPath.startsWith('v2/internal/')) return true;\n if (relPath.includes('/_contextData_/') || relPath.includes('/_context_data_/')) return true;\n if (relPath.includes('/_move_me/') || relPath.includes('/_tests-to-delete/')) return true;\n if (relPath.endsWith('todo.txt') || relPath.endsWith('todo.mdx') || relPath.endsWith('NOTES_V2.md')) return true;\n\n return relPath\n .split('/')\n .some((segment) => segment.toLowerCase().startsWith('x-'));\n}\n\nfunction isSupportedDocFile(repoPath) {\n return SUPPORTED_EXTENSIONS.has(path.extname(repoPath).toLowerCase());\n}\n\nfunction collectDocsPageEntries(node, out = []) {\n if (typeof node === 'string') {\n const value = node.trim().replace(/^\\/+/, '');\n if (value.startsWith('v2/') && !shouldExclude(value)) {\n out.push(value);\n }\n return out;\n }\n\n if (Array.isArray(node)) {\n node.forEach((item) => collectDocsPageEntries(item, out));\n return out;\n }\n\n if (!node || typeof node !== 'object') {\n return out;\n }\n\n Object.values(node).forEach((value) => collectDocsPageEntries(value, out));\n return out;\n}\n\nfunction fileEntryFromRepoPath(repoPath) {\n return {\n absPath: path.join(REPO_ROOT, repoPath),\n relPath: toPosix(repoPath)\n };\n}\n\nfunction loadDefaultFiles() {\n if (!fs.existsSync(DOCS_JSON_PATH)) {\n throw new Error('docs.json not found at repository root');\n }\n\n const docsJson = JSON.parse(fs.readFileSync(DOCS_JSON_PATH, 'utf8'));\n const versions = docsJson?.navigation?.versions || [];\n const routeEntries = [];\n\n versions.forEach((versionNode) => {\n const languages = versionNode?.languages;\n\n if (Array.isArray(languages)) {\n languages\n .filter((item) => item && item.language === 'en')\n .forEach((item) => collectDocsPageEntries(item, routeEntries));\n return;\n }\n\n if (languages && typeof languages === 'object' && languages.en) {\n collectDocsPageEntries(languages.en, routeEntries);\n return;\n }\n\n collectDocsPageEntries(versionNode, routeEntries);\n });\n\n const files = [];\n const seen = new Set();\n\n routeEntries.forEach((routePath) => {\n const routeKey = normalizeRoutePath(routePath);\n if (!routeKey) return;\n\n ['.mdx', '.md'].forEach((extension) => {\n const repoPath = `${routeKey}${extension}`;\n if (seen.has(repoPath) || shouldExclude(repoPath)) return;\n if (!fs.existsSync(path.join(REPO_ROOT, repoPath))) return;\n\n seen.add(repoPath);\n files.push(fileEntryFromRepoPath(repoPath));\n });\n });\n\n return files.sort((left, right) => left.relPath.localeCompare(right.relPath));\n}\n\nfunction resolvePathInput(targetPath) {\n const candidatePaths = [];\n const raw = path.isAbsolute(targetPath) ? targetPath : path.join(REPO_ROOT, targetPath);\n candidatePaths.push(raw);\n\n if (!path.extname(raw)) {\n candidatePaths.push(`${raw}.mdx`, `${raw}.md`);\n }\n\n const resolved = candidatePaths.find((candidate) => fs.existsSync(candidate));\n if (!resolved) {\n throw new Error(`Path not found: ${targetPath}`);\n }\n\n const repoRelative = toPosix(path.relative(REPO_ROOT, resolved));\n if (repoRelative.startsWith('..')) {\n throw new Error(`Path must be inside the repository: ${targetPath}`);\n }\n\n return resolved;\n}\n\nfunction walkDirectory(dirPath, out = []) {\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n\n entries.forEach((entry) => {\n if (entry.name === '.git' || entry.name === 'node_modules') {\n return;\n }\n\n const absPath = path.join(dirPath, entry.name);\n\n if (entry.isDirectory()) {\n walkDirectory(absPath, out);\n return;\n }\n\n const relPath = toPosix(path.relative(REPO_ROOT, absPath));",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/validators/content/check-double-headers.js",
+ "script": "check-double-headers",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tools/scripts/validators/content, v2, docs.json",
+ "owner": "docs",
+ "needs": "1.12, 1.13",
+ "purpose_statement": "Detects duplicate body H1 headings and opening paragraphs that repeat frontmatter title or description content.",
+ "pipeline_declared": "manual — validator, run on-demand only",
+ "usage": "node tools/scripts/validators/content/check-double-headers.js [--file ] [--files ] [--fix]",
+ "header": "#!/usr/bin/env node\n/**\n * @script check-double-headers\n * @category validator\n * @purpose qa:content-quality\n * @scope tools/scripts/validators/content, v2, docs.json\n * @owner docs\n * @needs 1.12, 1.13\n * @purpose-statement Detects duplicate body H1 headings and opening paragraphs that repeat frontmatter title or description content.\n * @pipeline manual — validator, run on-demand only\n * @usage node tools/scripts/validators/content/check-double-headers.js [--file ] [--files ] [--fix]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst matter = require('gray-matter');\nconst { getMdxFiles } = require('../../../../tests/utils/file-walker');\n\nconst RULE_DUPLICATE_TITLE = 'duplicate-title';\nconst RULE_DUPLICATE_DESCRIPTION = 'duplicate-description';\n\nlet parserPromise = null;\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nconst REPO_ROOT = getRepoRoot();\n\nasync function getParser() {\n if (!parserPromise) {\n parserPromise = (async () => {\n const [{ unified }, { default: remarkParse }, { default: remarkGfm }, { default: remarkMdx }] =\n await Promise.all([import('unified'), import('remark-parse'), import('remark-gfm'), import('remark-mdx')]);\n return unified().use(remarkParse).use(remarkGfm).use(remarkMdx);\n })();\n }\n\n return parserPromise;\n}\n\nfunction printHelp() {\n process.stdout.write(\n [\n 'Usage:',\n ' node tools/scripts/validators/content/check-double-headers.js [--file ] [--files ] [--fix]',\n '',\n 'Options:',\n ' --file Scan a single file (repeatable). Accepts absolute or repo-relative paths.',\n ' --files Scan a comma-separated list of files.',\n ' --fix Remove flagged duplicate H1 headings and exact duplicate opening paragraphs.',\n ' --help Show this help message.',\n '',\n 'Default behavior:',\n ' Scans routable v2 MDX pages from docs.json navigation.'\n ].join('\\n')\n );\n process.stdout.write('\\n');\n}\n\nfunction parseArgs(argv) {\n const args = {\n fix: false,\n help: false,\n files: []\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n if (token === '--fix') {\n args.fix = true;\n continue;\n }\n if (token === '--file') {\n const value = String(argv[i + 1] || '').trim();\n if (!value) throw new Error('Missing value for --file.');\n args.files.push(value);\n i += 1;\n continue;\n }\n if (token.startsWith('--file=')) {\n const value = token.slice('--file='.length).trim();\n if (!value) throw new Error('Missing value for --file.');\n args.files.push(value);\n continue;\n }\n if (token === '--files') {\n const value = String(argv[i + 1] || '').trim();\n if (!value) throw new Error('Missing value for --files.');\n parseCsvFiles(value).forEach((filePath) => args.files.push(filePath));\n i += 1;\n continue;\n }\n if (token.startsWith('--files=')) {\n const value = token.slice('--files='.length).trim();\n if (!value) throw new Error('Missing value for --files.');\n parseCsvFiles(value).forEach((filePath) => args.files.push(filePath));\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n args.files = dedupe(args.files.map(resolveInputPath));\n return args;\n}\n\nfunction parseCsvFiles(value) {\n return String(value || '')\n .split(',')\n .map((entry) => entry.trim())\n .filter(Boolean);\n}\n\nfunction resolveInputPath(filePath) {\n if (!filePath) return '';\n return path.isAbsolute(filePath) ? path.normalize(filePath) : path.resolve(REPO_ROOT, filePath);\n}\n\nfunction dedupe(values) {\n return [...new Set(values.filter(Boolean))];\n}\n\nfunction normalizeWhitespace(value) {\n return String(value || '').replace(/\\s+/g, ' ').trim();\n}\n\nfunction normalizeComparable(value) {\n return normalizeWhitespace(value).toLowerCase().replace(/[^a-z0-9]/g, '');\n}\n\nfunction levenshteinDistance(a, b) {\n const left = String(a || '');\n const right = String(b || '');\n\n if (!left) return right.length;\n if (!right) return left.length;\n\n const previous = new Array(right.length + 1);\n const current = new Array(right.length + 1);\n\n for (let j = 0; j <= right.length; j += 1) {\n previous[j] = j;\n }\n\n for (let i = 1; i <= left.length; i += 1) {\n current[0] = i;\n for (let j = 1; j <= right.length; j += 1) {\n const cost = left[i - 1] === right[j - 1] ? 0 : 1;\n current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + cost);\n }\n for (let j = 0; j <= right.length; j += 1) {\n previous[j] = current[j];\n }\n }\n\n return previous[right.length];\n}\n\nfunction isSimilar(a, b) {\n const na = normalizeComparable(a);\n const nb = normalizeComparable(b);\n\n if (!na || !nb) return false;\n if (na === nb) return true;\n if (na.includes(nb) || nb.includes(na)) return true;\n\n return levenshteinDistance(na, nb) < Math.min(na.length, nb.length) * 0.2;\n}\n\nfunction getBodyStartOffset(rawContent) {\n const raw = String(rawContent || '');\n const bomOffset = raw.startsWith('\\uFEFF') ? 1 : 0;\n const body = raw.slice(bomOffset);\n\n if (!body.startsWith('---')) {\n return bomOffset;\n }\n\n const match = body.match(/^---[ \\t]*\\r?\\n[\\s\\S]*?\\r?\\n---[ \\t]*(?:\\r?\\n|$)/);\n if (!match) {\n return bomOffset;\n }\n\n return bomOffset + match[0].length;\n}\n\nfunction getLineNumberAtOffset(rawContent, offset) {\n const raw = String(rawContent || '');\n const limited = raw.slice(0, Math.max(0, offset));\n let lines = 1;\n\n for (let i = 0; i < limited.length; i += 1) {\n if (limited[i] === '\\n') lines += 1;\n }\n\n return lines;\n}\n\nfunction formatDisplayPath(absPath) {\n const relative = path.relative(REPO_ROOT, absPath);\n if (!relative.startsWith('..') && !path.isAbsolute(relative)) {\n return relative.split(path.sep).join('/');\n }\n return absPath;\n}\n\nfunction getFirstTopLevelNode(tree, predicate) {\n const children = Array.isArray(tree?.children) ? tree.children : [];\n for (const child of children) {",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/validators/content/check-grammar-en-gb.js",
+ "script": "check-grammar-en-gb",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tools/scripts/validators/content, tools/script-index.md, tests/script-index.md, docs-guide/indexes/scripts-index.mdx, v2",
+ "owner": "docs",
+ "needs": "SE-1-11, S-1.15",
+ "purpose_statement": "Deterministic UK English grammar checker for prose content with optional conservative autofix for safe rules.",
+ "pipeline_declared": "manual/CI validator for English v2 docs and explicit content files",
+ "usage": "node tools/scripts/validators/content/check-grammar-en-gb.js [--scope full|changed] [--file ] [--fix] [--strict]",
+ "header": "#!/usr/bin/env node\n/**\n * @script check-grammar-en-gb\n * @category validator\n * @purpose qa:content-quality\n * @scope tools/scripts/validators/content, tools/script-index.md, tests/script-index.md, docs-guide/indexes/scripts-index.mdx, v2\n * @owner docs\n * @needs SE-1-11, S-1.15\n * @purpose-statement Deterministic UK English grammar checker for prose content with optional conservative autofix for safe rules.\n * @pipeline manual/CI validator for English v2 docs and explicit content files\n * @dualmode --check (default) | --fix (safe in-place rewrites)\n * @usage node tools/scripts/validators/content/check-grammar-en-gb.js [--scope full|changed] [--file ] [--fix] [--strict]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst DEFAULT_SCOPE = 'full';\nconst DEFAULT_REPORT_RULE_IDS = new Set([\n 'double-space',\n 'repeated-word',\n 'an-before-consonant',\n 'sentence-lowercase',\n 'its-vs-its'\n]);\nconst TERMINAL_PUNCTUATION_RE = /[.!?:;][)\"'\\]]*\\s*$/;\nconst URL_RE = /\\b(?:https?:\\/\\/|www\\.)[^\\s<>()]+/g;\nconst RULES = [\n {\n id: 'double-space',\n pattern: / +/g,\n message: 'Double space detected',\n fixable: true,\n fix: () => ' '\n },\n {\n id: 'repeated-word',\n pattern: /\\b(\\w+)\\s+\\1\\b/gi,\n message: 'Repeated word: \"$1 $1\"',\n fixable: true,\n fix: (_match, word) => word\n },\n {\n id: 'a-before-vowel',\n pattern: /\\ba\\s+(?=[aeiouAEIOU]\\w)/g,\n message: '\"a\" before vowel sound — should this be \"an\"?',\n fixable: false\n },\n {\n id: 'an-before-consonant',\n pattern: /\\ban\\s+(?=[^aeiouAEIOU\\s]\\w)/g,\n message: '\"an\" before consonant sound — should this be \"a\"?',\n fixable: false\n },\n {\n id: 'sentence-lowercase',\n pattern: /\\.\\s+[a-z]/g,\n message: 'Sentence starts with lowercase after full stop',\n fixable: true,\n fix: (match) => match.slice(0, -1) + match.slice(-1).toUpperCase()\n },\n {\n id: 'its-vs-its',\n pattern: /\\bit's\\s+(?:own|way|self|name|value|role)\\b/gi,\n message: 'Possible \"it\\'s\" vs \"its\" error — did you mean \"its\" (possessive)?',\n fixable: false\n },\n {\n id: 'missing-full-stop',\n pattern: null,\n message: 'Paragraph ends without punctuation',\n fixable: false\n }\n];\n\nconst REPO_ROOT = getRepoRoot();\nif (process.cwd() !== REPO_ROOT) {\n process.chdir(REPO_ROOT);\n}\n\nfunction getRepoRoot() {\n const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n if (result.status === 0) {\n const root = String(result.stdout || '').trim();\n if (root) return root;\n }\n return process.cwd();\n}\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction usage() {\n console.log(\n 'Usage: node tools/scripts/validators/content/check-grammar-en-gb.js [--scope full|changed] [--file ] [--fix] [--strict]'\n );\n}\n\nfunction parseArgs(argv) {\n const args = {\n scope: DEFAULT_SCOPE,\n files: [],\n fix: false,\n strict: false,\n help: false\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n\n if (token === '--scope') {\n args.scope = String(argv[i + 1] || '').trim() || DEFAULT_SCOPE;\n i += 1;\n continue;\n }\n\n if (token.startsWith('--scope=')) {\n args.scope = token.slice('--scope='.length).trim() || DEFAULT_SCOPE;\n continue;\n }\n\n if (token === '--file' || token === '--files') {\n const raw = String(argv[i + 1] || '').trim();\n if (raw) {\n raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((part) => args.files.push(part));\n }\n i += 1;\n continue;\n }\n\n if (token.startsWith('--file=')) {\n token\n .slice('--file='.length)\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((part) => args.files.push(part));\n continue;\n }\n\n if (token.startsWith('--files=')) {\n token\n .slice('--files='.length)\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((part) => args.files.push(part));\n continue;\n }\n\n if (token === '--fix') {\n args.fix = true;\n continue;\n }\n\n if (token === '--strict') {\n args.strict = true;\n continue;\n }\n\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (!['full', 'changed'].includes(args.scope)) {\n throw new Error(`Invalid --scope value: ${args.scope}`);\n }\n\n args.files = [...new Set(args.files.map(resolveInputPath))];\n\n return args;\n}\n\nfunction resolveInputPath(inputPath) {\n const raw = String(inputPath || '').trim();\n if (!raw) return '';\n return path.isAbsolute(raw) ? path.normalize(raw) : path.resolve(REPO_ROOT, raw);\n}\n\nfunction relFromRoot(absPath) {\n if (!absPath.startsWith(REPO_ROOT)) {\n return absPath;\n }\n return toPosix(path.relative(REPO_ROOT, absPath));\n}\n\nfunction isEnglishV2File(relPath) {\n const rel = toPosix(relPath);\n if (!rel.startsWith('v2/')) return false;\n if (!rel.endsWith('.mdx')) return false;\n if (rel.startsWith('v2/es/') || rel.startsWith('v2/fr/') || rel.startsWith('v2/cn/')) return false;\n if (rel.startsWith('v2/internal/')) return false;\n if (rel.includes('/x-')) return false;\n return true;\n}\n\nfunction walkEnglishV2Files(rootDir = path.join(REPO_ROOT, 'v2'), out = []) {\n if (!fs.existsSync(rootDir)) return out;\n\n const entries = fs.readdirSync(rootDir, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = path.join(rootDir, entry.name);\n const relPath = relFromRoot(fullPath);\n if (entry.isDirectory()) {\n if (entry.name === '.git' || entry.name === 'node_modules') continue;\n walkEnglishV2Files(fullPath, out);\n continue;\n }\n if (!isEnglishV2File(relPath)) continue;\n out.push(fullPath);",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/validators/content/check-page-endings.js",
+ "script": "check-page-endings",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tools/scripts/validators/content, v2",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "Validates that English v2 MDX pages end with an approved navigational or closing element",
+ "pipeline_declared": "manual, ci",
+ "usage": "node tools/scripts/validators/content/check-page-endings.js [--fix] [--json]",
+ "header": "#!/usr/bin/env node\n/**\n * @script check-page-endings\n * @category validator\n * @purpose qa:content-quality\n * @scope tools/scripts/validators/content, v2\n * @owner docs\n * @needs R-R14\n * @purpose-statement Validates that English v2 MDX pages end with an approved navigational or closing element\n * @pipeline manual, ci\n * @usage node tools/scripts/validators/content/check-page-endings.js [--fix] [--json]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst TODO_COMMENT = '';\nconst REPO_ROOT = getRepoRoot();\nconst V2_ROOT = path.join(REPO_ROOT, 'v2');\n\nfunction getRepoRoot() {\n const result = spawnSync('git', ['rev-parse', '--show-toplevel'], { encoding: 'utf8' });\n if (result.status === 0 && String(result.stdout || '').trim()) {\n return String(result.stdout || '').trim();\n }\n return process.cwd();\n}\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction usage() {\n console.log('Usage: node tools/scripts/validators/content/check-page-endings.js [--fix] [--json]');\n}\n\nfunction parseArgs(argv) {\n const args = {\n fix: false,\n json: false\n };\n\n for (let index = 0; index < argv.length; index += 1) {\n const token = argv[index];\n\n if (token === '--fix') {\n args.fix = true;\n continue;\n }\n\n if (token === '--json') {\n args.json = true;\n continue;\n }\n\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n return args;\n}\n\nfunction shouldExclude(repoPath) {\n const relPath = toPosix(repoPath).replace(/^\\/+/, '');\n if (!relPath.startsWith('v2/')) return true;\n if (!/\\.mdx$/i.test(relPath)) return true;\n if (relPath.startsWith('v2/es/') || relPath.startsWith('v2/fr/') || relPath.startsWith('v2/cn/')) return true;\n if (relPath.startsWith('v2/internal/')) return true;\n if (relPath.includes('/_contextData_/') || relPath.includes('/_context_data_/')) return true;\n if (relPath.includes('/_move_me/') || relPath.includes('/_tests-to-delete/')) return true;\n if (relPath.endsWith('/todo.mdx') || relPath.endsWith('/NOTES_V2.md') || relPath.endsWith('/todo.txt')) return true;\n return relPath.split('/').some((segment) => segment.toLowerCase().startsWith('x-'));\n}\n\nfunction walkFiles(dirPath, out = []) {\n if (!fs.existsSync(dirPath)) return out;\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n\n entries.forEach((entry) => {\n if (entry.name === '.git' || entry.name === 'node_modules') {\n return;\n }\n\n const absPath = path.join(dirPath, entry.name);\n if (entry.isDirectory()) {\n walkFiles(absPath, out);\n return;\n }\n\n const relPath = toPosix(path.relative(REPO_ROOT, absPath));\n if (shouldExclude(relPath)) return;\n out.push({ absPath, relPath });\n });\n\n return out;\n}\n\nfunction maskComments(content) {\n return String(content || '')\n .replace(/\\{\\/\\*[\\s\\S]*?\\*\\/\\}/g, (match) => match.replace(/[^\\n]/g, ' '))\n .replace(//g, (match) => match.replace(/[^\\n]/g, ' '));\n}\n\nfunction stripFrontmatter(content) {\n const raw = String(content || '');\n if (!raw.startsWith('---')) return raw;\n const match = raw.match(/^---[ \\t]*\\r?\\n[\\s\\S]*?\\r?\\n---[ \\t]*(?:\\r?\\n|$)/);\n return match ? raw.slice(match[0].length) : raw;\n}\n\nfunction getTrimmedBody(content) {\n return stripFrontmatter(maskComments(content)).replace(/\\s+$/, '');\n}\n\nfunction getLastMeaningfulRecord(body) {\n const lines = String(body || '').split('\\n');\n let inCodeFence = false;\n let lastRecord = null;\n\n lines.forEach((line, index) => {\n const trimmed = String(line || '').trim();\n if (!trimmed) return;\n if (/^(import|export)\\b/.test(trimmed)) return;\n\n if (/^```/.test(trimmed)) {\n lastRecord = {\n line: index + 1,\n type: 'code-fence',\n text: trimmed\n };\n inCodeFence = !inCodeFence;\n return;\n }\n\n lastRecord = {\n line: index + 1,\n type: inCodeFence ? 'code' : 'body',\n text: trimmed\n };\n });\n\n return lastRecord;\n}\n\nfunction endsWithApprovedComponent(body) {\n const trimmed = String(body || '').trim();\n if (!trimmed) return false;\n\n return (\n /\\s*$/i.test(trimmed) ||\n /\\s*$/i.test(trimmed) ||\n /\\s*$/i.test(trimmed) ||\n / \\s*$/i.test(trimmed)\n );\n}\n\nfunction getLastHeadingSection(body) {\n const lines = String(body || '').split('\\n');\n const headings = [];\n let inCodeFence = false;\n\n lines.forEach((line, index) => {\n const trimmed = String(line || '').trim();\n if (!trimmed) return;\n\n if (/^```/.test(trimmed)) {\n inCodeFence = !inCodeFence;\n return;\n }\n\n if (inCodeFence) return;\n\n const match = trimmed.match(/^(#{1,6})\\s+(.+?)\\s*#*\\s*$/);\n if (!match) return;\n\n headings.push({\n title: match[2].trim(),\n lineIndex: index\n });\n });\n\n if (headings.length === 0) return null;\n const heading = headings[headings.length - 1];\n return {\n title: heading.title,\n sectionBody: lines.slice(heading.lineIndex + 1).join('\\n')\n };\n}\n\nfunction hasApprovedNavigationalEnding(body) {\n const lastHeading = getLastHeadingSection(body);\n if (!lastHeading) return false;\n\n if (!/^(related|next\\s+steps|see\\s+also|resources)\\b/i.test(lastHeading.title)) {\n return false;\n }\n\n return /\\[[^\\]]+\\]\\([^)]+\\)|<(Card|CardGroup|AccordionGroup)\\b/i.test(lastHeading.sectionBody);\n}\n\nfunction analyzeFile(file, options) {\n const raw = fs.readFileSync(file.absPath, 'utf8');\n const body = getTrimmedBody(raw);\n const lastRecord = getLastMeaningfulRecord(body);\n\n let verdict = 'ok';\n let reason = 'approved-closing';\n\n if (!body.trim()) {\n verdict = 'warning';\n reason = 'no-meaningful-content';\n } else if (endsWithApprovedComponent(body)) {\n reason = 'approved-component-ending';\n } else if (hasApprovedNavigationalEnding(body)) {\n reason = 'approved-navigation-ending';",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/validators/content/check-proper-nouns.js",
+ "script": "check-proper-nouns",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "v2, tools/scripts/validators/content, tests/config/spell-dict.json",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Detects and fixes incorrect proper noun capitalisation in prose while skipping code, frontmatter, URLs, and path-like tokens.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/scripts/validators/content/check-proper-nouns.js [--file ] [--fix]",
+ "header": "#!/usr/bin/env node\n/**\n * @script check-proper-nouns\n * @category validator\n * @purpose qa:content-quality\n * @scope v2, tools/scripts/validators/content, tests/config/spell-dict.json\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Detects and fixes incorrect proper noun capitalisation in prose while skipping code, frontmatter, URLs, and path-like tokens.\n * @pipeline manual\n * @dualmode --check (default) | --fix\n * @usage node tools/scripts/validators/content/check-proper-nouns.js [--file ] [--fix]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst { unified } = require('unified');\nconst remarkParse = require('remark-parse').default;\nconst remarkMdx = require('remark-mdx').default;\nconst { getMdxFiles } = require('../../../../tests/utils/file-walker');\n\nconst PROPER_NOUNS = {\n livepeer: 'Livepeer',\n ethereum: 'Ethereum',\n arbitrum: 'Arbitrum',\n mintlify: 'Mintlify',\n infura: 'Infura',\n alchemy: 'Alchemy',\n metamask: 'MetaMask',\n openrouter: 'OpenRouter',\n github: 'GitHub',\n youtube: 'YouTube',\n deepwiki: 'DeepWiki',\n 'node.js': 'Node.js',\n 'livepeer studio': 'Livepeer Studio',\n 'livepeer network': 'Livepeer Network'\n};\n\nconst PROPER_NOUN_KEYS = Object.keys(PROPER_NOUNS).sort((a, b) => b.length - a.length);\nconst PROSE_BLOCK_TYPES = new Set(['paragraph', 'heading', 'tableCell']);\nconst SKIP_NODE_TYPES = new Set([\n 'code',\n 'inlineCode',\n 'yaml',\n 'html',\n 'mdxjsEsm',\n 'mdxTextExpression',\n 'mdxFlowExpression',\n 'mdxJsxTextElement',\n 'mdxJsxFlowElement'\n]);\nconst WORD_CHAR_RE = /[A-Za-z0-9_-]/;\nconst URL_RE = /\\bhttps?:\\/\\/[^\\s<>()]+/gi;\nconst WWW_RE = /\\bwww\\.[^\\s<>()]+/gi;\nconst DOMAIN_RE = /\\b(?:[a-z0-9-]+\\.)+[a-z]{2,}(?:\\/[^\\s<>()]*)?/gi;\nconst ROOT_PATH_RE = /(^|[\\s(])((?:\\.{1,2}\\/|\\/)[^\\s<>()]+)/g;\nconst SLASH_TOKEN_RE = /(^|[\\s(])(@?[A-Za-z0-9._-]+(?:\\/[A-Za-z0-9._-]+)+)/g;\n\nconst PARSER = unified().use(remarkParse).use(remarkMdx);\nconst REPO_ROOT = getRepoRoot();\n\nif (process.cwd() !== REPO_ROOT) {\n process.chdir(REPO_ROOT);\n}\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction toDisplayPath(filePath) {\n const relative = toPosix(path.relative(REPO_ROOT, filePath));\n if (!relative || relative.startsWith('..')) {\n return filePath;\n }\n return relative;\n}\n\nfunction escapeRegExp(value) {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction parseArgs(argv) {\n const args = {\n fix: false,\n files: []\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--fix') {\n args.fix = true;\n continue;\n }\n if (token === '--file' || token === '--files') {\n const raw = String(argv[i + 1] || '').trim();\n if (!raw) {\n throw new Error(`${token} requires a path value`);\n }\n raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((part) => args.files.push(part));\n i += 1;\n continue;\n }\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n throw new Error(`Unknown argument: ${token}`);\n }\n\n args.files = [...new Set(args.files.map(resolveInputPath))];\n return args;\n}\n\nfunction printHelp() {\n console.log('Usage: node tools/scripts/validators/content/check-proper-nouns.js [--file ] [--fix]');\n}\n\nfunction resolveInputPath(input) {\n const trimmed = String(input || '').trim();\n if (!trimmed) return '';\n if (path.isAbsolute(trimmed)) {\n return path.resolve(trimmed);\n }\n return path.resolve(REPO_ROOT, trimmed.replace(/^\\.\\//, ''));\n}\n\nfunction resolveTargetFiles(explicitFiles) {\n if (explicitFiles.length > 0) {\n explicitFiles.forEach((filePath) => {\n if (!fs.existsSync(filePath)) {\n throw new Error(`File not found: ${filePath}`);\n }\n });\n return explicitFiles;\n }\n\n return getMdxFiles(REPO_ROOT).map((filePath) => path.resolve(filePath));\n}\n\nfunction splitFrontmatter(raw) {\n const normalized = String(raw || '');\n if (!normalized.startsWith('---')) {\n return {\n prefix: '',\n body: normalized,\n bodyStartOffset: 0\n };\n }\n\n const match = normalized.match(/^---\\r?\\n[\\s\\S]*?\\r?\\n---(?:\\r?\\n)?/);\n if (!match) {\n return {\n prefix: '',\n body: normalized,\n bodyStartOffset: 0\n };\n }\n\n return {\n prefix: match[0],\n body: normalized.slice(match[0].length),\n bodyStartOffset: match[0].length\n };\n}\n\nfunction buildLineStarts(raw) {\n const starts = [0];\n for (let i = 0; i < raw.length; i += 1) {\n if (raw[i] === '\\n') {\n starts.push(i + 1);\n }\n }\n return starts;\n}\n\nfunction getLineNumber(lineStarts, offset) {\n let low = 0;\n let high = lineStarts.length - 1;\n\n while (low <= high) {\n const mid = Math.floor((low + high) / 2);\n if (lineStarts[mid] <= offset) {\n low = mid + 1;\n } else {\n high = mid - 1;\n }\n }\n\n return high + 1;\n}\n\nfunction collectProseBlocks(node, bodyStartOffset, out = []) {\n if (!node || typeof node !== 'object') return out;\n\n if (PROSE_BLOCK_TYPES.has(node.type)) {\n const block = buildVisibleBlock(node, bodyStartOffset);\n if (block && block.text.trim()) {\n out.push(block);\n }\n return out;\n }\n\n if (Array.isArray(node.children)) {\n node.children.forEach((child) => collectProseBlocks(child, bodyStartOffset, out));\n }\n\n return out;",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/wcag-repair-common.js",
+ "script": "wcag-repair-common",
+ "category": "remediator",
+ "purpose": "qa:content-quality",
+ "scope": "tools/scripts, tests/integration, tasks/reports, v2",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "WCAG repair shared logic — common repair functions used by WCAG audit fix mode",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/wcag-repair-common.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script wcag-repair-common\n * @category remediator\n * @purpose qa:content-quality\n * @scope tools/scripts, tests/integration, tasks/reports, v2\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement WCAG repair shared logic — common repair functions used by WCAG audit fix mode\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/wcag-repair-common.js [flags]\n */\n\nconst path = require('path');\n\nconst REPO_ROOT = path.resolve(__dirname, '..', '..');\nconst DEFAULT_REPORT_MD = path.join(REPO_ROOT, 'tasks', 'reports', 'quality-accessibility', 'v2-wcag-repair-common-report.md');\nconst DEFAULT_REPORT_JSON = path.join(REPO_ROOT, 'tasks', 'reports', 'quality-accessibility', 'v2-wcag-repair-common-report.json');\n\nfunction parseWrapperArgs(argv) {\n const passThrough = [];\n let reportProvided = false;\n let reportJsonProvided = false;\n let modeSet = false;\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--help' || token === '-h') {\n return { help: true, argv: [] };\n }\n if (token === '--full' || token === '--staged') {\n modeSet = true;\n passThrough.push(token);\n continue;\n }\n if (token === '--files' || token === '--file') {\n passThrough.push(token);\n if (i + 1 < argv.length) {\n passThrough.push(argv[i + 1]);\n i += 1;\n }\n continue;\n }\n if (token === '--report') {\n reportProvided = true;\n passThrough.push(token);\n if (i + 1 < argv.length) {\n passThrough.push(argv[i + 1]);\n i += 1;\n }\n continue;\n }\n if (token === '--report-json') {\n reportJsonProvided = true;\n passThrough.push(token);\n if (i + 1 < argv.length) {\n passThrough.push(argv[i + 1]);\n i += 1;\n }\n continue;\n }\n // Pass through supported/shared flags transparently.\n passThrough.push(token);\n }\n\n const out = [];\n if (!modeSet) out.push('--full');\n out.push(...passThrough);\n\n // Default to applying safe fixes unless the caller explicitly disabled them.\n if (!out.includes('--fix') && !out.includes('--no-fix')) {\n out.push('--fix');\n }\n\n // Disable browser auditing; this is the common-repair wrapper.\n if (!out.includes('--max-pages')) {\n out.push('--max-pages', '0');\n }\n\n if (!reportProvided) {\n out.push('--report', DEFAULT_REPORT_MD);\n }\n if (!reportJsonProvided) {\n out.push('--report-json', DEFAULT_REPORT_JSON);\n }\n\n return { help: false, argv: out };\n}\n\nfunction printHelp() {\n console.log(`Usage: node tools/scripts/wcag-repair-common.js [--full|--staged|--files ] [--fix|--no-fix] [--stage] [--fail-impact ] [--report ] [--report-json ]`);\n console.log('');\n console.log('Applies conservative WCAG-related source autofixes only (no browser audit) using the shared tests/integration WCAG engine.');\n console.log('Usable via lpd script runner: lpd tools wcag-repair-common -- --staged --stage');\n}\n\nasync function main() {\n const parsed = parseWrapperArgs(process.argv.slice(2));\n if (parsed.help) {\n printHelp();\n process.exit(0);\n }\n\n process.chdir(REPO_ROOT);\n const wcagAudit = require(path.join(REPO_ROOT, 'tests', 'integration', 'v2-wcag-audit.js'));\n const result = await wcagAudit.runAudit({ argv: parsed.argv });\n process.exit(result.exitCode || 0);\n}\n\nmain().catch((error) => {\n console.error(`wcag-repair-common failed: ${error.message}`);\n process.exit(1);\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/integration/v2-link-audit.selftest.js",
+ "script": "v2-link-audit.selftest",
+ "category": "validator",
+ "purpose": "qa:link-integrity",
+ "scope": "tests/integration",
+ "owner": "docs",
+ "needs": "E-R12, E-R14",
+ "purpose_statement": "Self-test suite for v2-link-audit.js — validates audit logic against known fixtures",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/integration/v2-link-audit.selftest.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script v2-link-audit.selftest\n * @category validator\n * @purpose qa:link-integrity\n * @scope tests/integration\n * @owner docs\n * @needs E-R12, E-R14\n * @purpose-statement Self-test suite for v2-link-audit.js — validates audit logic against known fixtures\n * @pipeline manual — not yet in pipeline\n * @dualmode --full (validator) | --write-links (remediator)\n * @usage node tests/integration/v2-link-audit.selftest.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst http = require('http');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst audit = require('./v2-link-audit');\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nasync function startServer() {\n const server = http.createServer((req, res) => {\n const url = req.url || '/';\n\n if (url.startsWith('/ok')) {\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('ok');\n return;\n }\n\n if (url.startsWith('/head-fallback')) {\n if (req.method === 'HEAD') {\n res.writeHead(405, { 'content-type': 'text/plain' });\n res.end();\n return;\n }\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('fallback-ok');\n return;\n }\n\n if (url.startsWith('/soft')) {\n res.writeHead(503, { 'content-type': 'text/plain' });\n res.end('soft-fail');\n return;\n }\n\n if (url.startsWith('/hard')) {\n res.writeHead(404, { 'content-type': 'text/plain' });\n res.end('hard-fail');\n return;\n }\n\n if (url.startsWith('/media.png')) {\n res.writeHead(200, { 'content-type': 'image/png' });\n res.end('PNG');\n return;\n }\n\n res.writeHead(500, { 'content-type': 'text/plain' });\n res.end('unexpected route');\n });\n\n await new Promise((resolve, reject) => {\n server.listen(0, '127.0.0.1', (error) => {\n if (error) reject(error);\n else resolve();\n });\n });\n\n const addr = server.address();\n const base = `http://127.0.0.1:${addr.port}`;\n return { server, base };\n}\n\nasync function closeServer(server) {\n if (!server) return;\n await new Promise((resolve) => server.close(() => resolve()));\n}\n\nasync function run() {\n let server = null;\n let fixtureFile = '';\n const reportMd = '/tmp/v2-link-audit-selftest.md';\n const reportJson = '/tmp/v2-link-audit-selftest.json';\n\n try {\n const started = await startServer();\n server = started.server;\n const base = started.base;\n\n const root = getRepoRoot();\n const fixtureDir = path.join(root, 'v2', 'internal', 'reports', 'navigation-links');\n fixtureFile = path.join(fixtureDir, `v2-link-audit-selftest-${Date.now()}.mdx`);\n const relFixture = path.relative(root, fixtureFile).split(path.sep).join('/');\n\n const content = [\n '# V2 Link Audit Selftest Fixture',\n '',\n `[ok](${base}/ok)`,\n `[head fallback](${base}/head-fallback#anchor)`,\n `[soft](${base}/soft)`,\n `[hard](${base}/hard)`,\n 'invalid',\n ``\n ].join('\\n');\n\n fs.writeFileSync(fixtureFile, `${content}\\n`, 'utf8');\n\n const out = await audit.runAudit({\n argv: [\n '--files', relFixture,\n '--external-policy', 'validate',\n '--external-link-types', 'navigational',\n '--external-timeout-ms', '3000',\n '--external-concurrency', '4',\n '--external-per-host-concurrency', '2',\n '--external-retries', '0',\n '--no-write-links',\n '--report', reportMd,\n '--report-json', reportJson\n ]\n });\n\n assert.strictEqual(out.exitCode, 0);\n assert.strictEqual(out.externalValidation.eligibleRefCount, 5);\n assert.strictEqual(out.externalValidation.urlClassCounts[audit.EXTERNAL_OK], 2);\n assert.strictEqual(out.externalValidation.urlClassCounts[audit.EXTERNAL_SOFT_FAIL], 1);\n assert.strictEqual(out.externalValidation.urlClassCounts[audit.EXTERNAL_HARD_FAIL], 2);\n\n const json = JSON.parse(fs.readFileSync(reportJson, 'utf8'));\n const classes = json.external.urlResults.reduce((acc, row) => {\n acc[row.class] = (acc[row.class] || 0) + 1;\n return acc;\n }, {});\n\n assert.strictEqual(classes[audit.EXTERNAL_OK], 2);\n assert.strictEqual(classes[audit.EXTERNAL_SOFT_FAIL], 1);\n assert.strictEqual(classes[audit.EXTERNAL_HARD_FAIL], 2);\n\n const fileEntry = json.files.find((item) => item.file === relFixture);\n assert.ok(fileEntry, 'Expected fixture file in JSON report');\n\n const mediaRef = fileEntry.refs.find((ref) => String(ref.rawPath).includes('/media.png'));\n assert.ok(mediaRef, 'Expected media ref in fixture file refs');\n assert.strictEqual(mediaRef.status, audit.EXTERNAL_UNTESTED);\n\n console.log('✅ v2-link-audit selftest passed');\n return 0;\n } catch (error) {\n console.error(`❌ v2-link-audit selftest failed: ${error.message}`);\n return 1;\n } finally {\n if (fixtureFile && fs.existsSync(fixtureFile)) {\n fs.unlinkSync(fixtureFile);\n }\n await closeServer(server);\n }\n}\n\nif (require.main === module) {\n run().then((code) => process.exit(code));\n}\n\nmodule.exports = { run };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:link-audit:selftest"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:link-audit:selftest)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/v2-link-audit.test.js",
+ "script": "v2-link-audit.test",
+ "category": "validator",
+ "purpose": "qa:link-integrity",
+ "scope": "tests/unit, tests/integration",
+ "owner": "docs",
+ "needs": "E-R12, E-R14",
+ "purpose_statement": "Unit tests for v2-link-audit.js — tests individual link checking rules",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/v2-link-audit.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script v2-link-audit.test\n * @category validator\n * @purpose qa:link-integrity\n * @scope tests/unit, tests/integration\n * @owner docs\n * @needs E-R12, E-R14\n * @purpose-statement Unit tests for v2-link-audit.js — tests individual link checking rules\n * @pipeline manual — not yet in pipeline\n * @dualmode --full (validator) | --write-links (remediator)\n * @usage node tests/unit/v2-link-audit.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst audit = require('../integration/v2-link-audit');\n\nlet errors = [];\nlet warnings = [];\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n rule: 'v2-link-audit unit',\n message: `${name}: ${error.message}`,\n line: 1,\n file: 'tests/unit/v2-link-audit.test.js'\n });\n }\n}\n\nasync function runTests() {\n errors = [];\n warnings = [];\n\n console.log('🧪 v2 Link Audit Unit Tests');\n\n await runCase('Parses default args with classify policy', async () => {\n const parsed = audit.parseArgs([]);\n assert.strictEqual(parsed.mode, 'full');\n assert.strictEqual(parsed.report, audit.DEFAULT_REPORT);\n assert.strictEqual(parsed.reportJson, audit.DEFAULT_REPORT_JSON);\n assert.strictEqual(parsed.respectMintIgnore, true);\n assert.strictEqual(parsed.externalPolicy, 'classify');\n assert.strictEqual(parsed.externalLinkTypes, 'navigational');\n assert.strictEqual(parsed.externalTimeoutMs, 10000);\n assert.strictEqual(parsed.externalConcurrency, 12);\n assert.strictEqual(parsed.externalPerHostConcurrency, 2);\n assert.strictEqual(parsed.externalRetries, 1);\n assert.strictEqual(parsed.writeLinks, true);\n });\n\n await runCase('Parses external validate overrides and no-write-links', async () => {\n const parsed = audit.parseArgs([\n '--staged',\n '--external-policy', 'validate',\n '--external-link-types', 'all',\n '--external-timeout-ms', '2000',\n '--external-concurrency', '7',\n '--external-per-host-concurrency', '3',\n '--external-retries', '4',\n '--no-mintignore',\n '--no-write-links',\n '--report-json', '/tmp/v2-link-audit-unit.json'\n ]);\n\n assert.strictEqual(parsed.mode, 'staged');\n assert.strictEqual(parsed.externalPolicy, 'validate');\n assert.strictEqual(parsed.externalLinkTypes, 'all');\n assert.strictEqual(parsed.externalTimeoutMs, 2000);\n assert.strictEqual(parsed.externalConcurrency, 7);\n assert.strictEqual(parsed.externalPerHostConcurrency, 3);\n assert.strictEqual(parsed.externalRetries, 4);\n assert.strictEqual(parsed.respectMintIgnore, false);\n assert.strictEqual(parsed.writeLinks, false);\n assert.ok(parsed.reportJson.endsWith('/tmp/v2-link-audit-unit.json'));\n });\n\n await runCase('Derives JSON report path from custom markdown report when report-json is omitted', async () => {\n const parsed = audit.parseArgs([\n '--report', '/tmp/v2-link-audit-unit-report.md',\n '--no-write-links'\n ]);\n\n assert.ok(parsed.report.endsWith('/tmp/v2-link-audit-unit-report.md'));\n assert.ok(parsed.reportJson.endsWith('/tmp/v2-link-audit-unit-report.json'));\n });\n\n await runCase('Normalizes external URLs by removing hash and preserving query', async () => {\n const out = audit.normalizeExternalUrl('https://example.com/docs?tab=one#section-2');\n assert.strictEqual(out, 'https://example.com/docs?tab=one');\n });\n\n await runCase('External eligibility filter handles navigational/media/all', async () => {\n const markdownLink = { sourceType: 'markdown-link' };\n const markdownImage = { sourceType: 'markdown-image' };\n const hrefAttr = { sourceType: 'jsx-attr', attr: 'href' };\n const srcAttr = { sourceType: 'jsx-attr', attr: 'src' };\n\n assert.strictEqual(audit.shouldValidateExternalRef(markdownLink, 'navigational'), true);\n assert.strictEqual(audit.shouldValidateExternalRef(hrefAttr, 'navigational'), true);\n assert.strictEqual(audit.shouldValidateExternalRef(markdownImage, 'navigational'), false);\n assert.strictEqual(audit.shouldValidateExternalRef(srcAttr, 'navigational'), false);\n\n assert.strictEqual(audit.shouldValidateExternalRef(markdownImage, 'media'), true);\n assert.strictEqual(audit.shouldValidateExternalRef(srcAttr, 'media'), true);\n assert.strictEqual(audit.shouldValidateExternalRef(markdownLink, 'media'), false);\n\n assert.strictEqual(audit.shouldValidateExternalRef(markdownLink, 'all'), true);\n assert.strictEqual(audit.shouldValidateExternalRef(markdownImage, 'all'), true);\n assert.strictEqual(audit.shouldValidateExternalRef(srcAttr, 'all'), true);\n });\n\n await runCase('HTTP status classification maps to expected external classes', async () => {\n assert.strictEqual(audit.classifyExternalStatus(200), audit.EXTERNAL_OK);\n assert.strictEqual(audit.classifyExternalStatus(301), audit.EXTERNAL_OK);\n assert.strictEqual(audit.classifyExternalStatus(404), audit.EXTERNAL_HARD_FAIL);\n assert.strictEqual(audit.classifyExternalStatus(401), audit.EXTERNAL_SOFT_FAIL);\n assert.strictEqual(audit.classifyExternalStatus(403), audit.EXTERNAL_SOFT_FAIL);\n assert.strictEqual(audit.classifyExternalStatus(429), audit.EXTERNAL_SOFT_FAIL);\n assert.strictEqual(audit.classifyExternalStatus(503), audit.EXTERNAL_SOFT_FAIL);\n });\n\n await runCase('Excludes x-* paths from explicit --files scope', async () => {\n const root = getRepoRoot();\n const tmpDir = path.join(root, 'v2', 'x-experimental', 'link-audit-unit-fixture');\n const tmpFile = path.join(tmpDir, `fixture-${Date.now()}.mdx`);\n\n fs.mkdirSync(tmpDir, { recursive: true });\n fs.writeFileSync(tmpFile, '# x path fixture\\n\\n[link](https://example.com)\\n', 'utf8');\n\n try {\n const rel = path.relative(root, tmpFile).split(path.sep).join('/');\n const result = await audit.runAudit({\n argv: [\n '--files', rel,\n '--no-write-links',\n '--report', '/tmp/v2-link-audit-unit-x.md',\n '--report-json', '/tmp/v2-link-audit-unit-x.json'\n ]\n });\n\n assert.strictEqual(result.fileCount, 0);\n assert.strictEqual(result.exitCode, 0);\n } finally {\n if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);\n try {\n fs.rmdirSync(tmpDir);\n } catch (_error) {\n // Ignore if directory is not empty.\n }\n }\n });\n\n return {\n errors,\n warnings,\n passed: errors.length === 0,\n total: 7\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ v2 link audit unit tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} v2 link audit unit test failure(s)`);\n result.errors.forEach((err) => console.error(` - ${err.message}`));\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ v2 link audit unit tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:link-audit:unit"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tests/unit/link-audit-unit-fixture",
+ "type": "directory",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": "tests/unit/fixture-${Date.now()}.mdx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tests/unit/link-audit-unit-fixture, tests/unit/fixture-${Date.now()}.mdx",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:link-audit:unit)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tasks/scripts/audit-python.py",
+ "script": "audit-python",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tasks/scripts",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Python audit utility — runs Python-based audit checks (alternative to Node auditors)",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "python3 tasks/scripts/audit-python.py [flags]",
+ "header": "#!/usr/bin/env python3\n# @script audit-python\n# @category validator\n# @purpose qa:repo-health\n# @scope tasks/scripts\n# @owner docs\n# @needs E-C1, R-R14\n# @purpose-statement Python audit utility — runs Python-based audit checks (alternative to Node auditors)\n# @pipeline manual — not yet in pipeline\n# @usage python3 tasks/scripts/audit-python.py [flags]\n\"\"\"\nComprehensive audit script for all v2 pages\nRuns file checks, MDX validation, and link checking\n\"\"\"\n\nimport json\nimport os\nfrom pathlib import Path\nfrom datetime import datetime\nimport re\n\n# Get absolute path to script, then go up 3 levels\nSCRIPT_DIR = Path(__file__).resolve().parent\nBASE_DIR = SCRIPT_DIR.parent.parent\nDOCS_JSON_PATH = BASE_DIR / 'docs.json'\nREPORT_DIR = BASE_DIR / 'tasks' / 'reports' / 'page-audits'\nV2_PAGES_DIR = BASE_DIR / 'v2' / 'pages'\nSNIPPETS_DIR = BASE_DIR / 'snippets'\n\n# Ensure report directory exists\nREPORT_DIR.mkdir(parents=True, exist_ok=True)\n\ndef extract_pages(nav, pages=None):\n \"\"\"Recursively extract all page paths from navigation structure\"\"\"\n if pages is None:\n pages = []\n \n if isinstance(nav, list):\n for item in nav:\n extract_pages(item, pages)\n elif isinstance(nav, dict):\n if 'pages' in nav:\n for page in nav['pages']:\n if isinstance(page, str) and page.strip() and page != ' ':\n pages.append(page)\n elif isinstance(page, dict) and 'pages' in page:\n extract_pages(page, pages)\n for value in nav.values():\n if isinstance(value, (dict, list)):\n extract_pages(value, pages)\n return pages\n\ndef get_v2_pages():\n \"\"\"Get all v2 pages from docs.json\"\"\"\n with open(DOCS_JSON_PATH, 'r') as f:\n docs = json.load(f)\n \n v2_version = next((v for v in docs['navigation']['versions'] if v['version'] == 'v2'), None)\n if not v2_version:\n raise ValueError('v2 version not found in docs.json')\n \n all_pages = extract_pages(v2_version)\n unique_pages = list(set([\n p.replace('.mdx', '').replace('.md', '') \n for p in all_pages \n if p and p.strip() and p != ' '\n ]))\n \n return unique_pages\n\ndef is_intentional_redirect(page_path):\n \"\"\"Check if a page path is an intentional redirect\"\"\"\n return '/redirect' in page_path or page_path.endswith('/redirect')\n\ndef check_file_exists(page_path):\n \"\"\"Check if file exists and return full path\"\"\"\n candidates = [\n BASE_DIR / f\"{page_path}.mdx\",\n BASE_DIR / f\"{page_path}.md\",\n BASE_DIR / page_path / 'index.mdx',\n BASE_DIR / page_path / 'index.md',\n BASE_DIR / page_path / 'README.mdx',\n BASE_DIR / page_path / 'README.md'\n ]\n for file_path in candidates:\n if file_path.exists():\n return {'exists': True, 'path': str(file_path)}\n \n return {'exists': False, 'path': None}\n\ndef check_mdx_errors(file_path):\n \"\"\"Check for MDX syntax errors\"\"\"\n errors = []\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n content = f.read()\n \n # Check for broken imports\n import_pattern = r\"import\\s+{([^}]+)}\\s+from\\s+['\\\"]([^'\\\"]+)['\\\"]\"\n for match in re.finditer(import_pattern, content):\n import_path = match.group(2)\n if import_path.startswith('/snippets/'):\n full_path = BASE_DIR / import_path.lstrip('/')\n if '/components/' in import_path:\n # Import path may already include .jsx extension\n component_file = full_path\n if not str(component_file).endswith('.jsx') and not str(component_file).endswith('.js'):\n component_file = Path(str(full_path) + '.jsx')\n if not component_file.exists():\n errors.append(f\"Missing import: {import_path}\")\n except Exception as e:\n errors.append(f\"File read error: {str(e)}\")\n \n return errors\n\ndef extract_links(file_path):\n \"\"\"Extract links from MDX file\"\"\"\n links = []\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n content = f.read()\n \n # Markdown links: [text](url)\n markdown_pattern = r'\\[([^\\]]+)\\]\\(([^)]+)\\)'\n for match in re.finditer(markdown_pattern, content):\n links.append({\n 'text': match.group(1),\n 'url': match.group(2),\n 'type': 'markdown'\n })\n \n # JSX links: \n jsx_pattern = r']+href=[\\'\"]([^\\'\"]+)[\\'\"]'\n for match in re.finditer(jsx_pattern, content):\n links.append({\n 'text': '',\n 'url': match.group(1),\n 'type': 'jsx'\n })\n \n # Anchor tags: \n anchor_pattern = r']+href=[\\'\"]([^\\'\"]+)[\\'\"]'\n for match in re.finditer(anchor_pattern, content):\n links.append({\n 'text': '',\n 'url': match.group(1),\n 'type': 'anchor'\n })\n except Exception:\n pass\n \n return links\n\ndef check_link(link, current_page_path):\n \"\"\"Check if link is broken\"\"\"\n url = link['url']\n \n # Skip external links\n if url.startswith(('http://', 'https://', 'mailto:', '#')):\n return {'broken': False, 'reason': 'external_or_anchor'}\n \n # Handle relative links\n if url.startswith('/'):\n # Absolute path from root\n target_path = url.lstrip('/').rstrip('/')\n file_check = check_file_exists(f\"v2/pages/{target_path}\")\n if not file_check['exists']:\n return {'broken': True, 'reason': 'file_not_found', 'expected': f\"v2/pages/{target_path}\"}\n else:\n # Relative path\n current_dir = Path(current_page_path).parent\n target_path = (current_dir / url).resolve()\n try:\n relative_path = target_path.relative_to(BASE_DIR)\n file_check = check_file_exists(str(relative_path).replace('.mdx', '').replace('.md', ''))\n if not file_check['exists']:\n return {'broken': True, 'reason': 'file_not_found', 'expected': str(relative_path)}\n except ValueError:\n return {'broken': True, 'reason': 'path_outside_repo', 'expected': str(target_path)}\n \n return {'broken': False, 'reason': 'valid'}\n\n# Main execution\n# Write progress immediately\nprogress_file = REPORT_DIR / 'audit-python-progress.log'\ntry:\n with open(progress_file, 'w') as f:\n f.write(f\"Audit started at: {datetime.now().isoformat()}\\n\")\nexcept Exception as e:\n print(f\"Warning: Could not write progress file: {e}\")\n\nprint('🔍 Extracting v2 pages from docs.json...')\ntry:\n with open(progress_file, 'a') as f:\n f.write('Extracting v2 pages from docs.json...\\n')\nexcept:\n pass\n\ntry:\n pages = get_v2_pages()\n try:\n with open(progress_file, 'a') as f:\n f.write(f'Found {len(pages)} pages to audit\\n')\n except:\n pass\n print(f'📄 Found {len(pages)} pages to audit\\n')\nexcept Exception as e:\n try:\n with open(progress_file, 'a') as f:\n f.write(f'ERROR: {str(e)}\\n')\n except:\n pass\n print(f'ERROR: {e}')\n raise\n\naudit_results = {\n 'timestamp': datetime.now().isoformat(),\n 'totalPages': len(pages),\n 'fileChecks': [],\n 'mdxErrors': [],",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/integration/domain-pages-audit.js",
+ "script": "domain-pages-audit",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests/integration, tests/reports, docs.livepeer.org",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Audits deployed docs page HTTP status codes (v1, v2, or both) and emits a stable JSON report",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/integration/domain-pages-audit.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script domain-pages-audit\n * @category validator\n * @purpose qa:repo-health\n * @scope tests/integration, tests/reports, docs.livepeer.org\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Audits deployed docs page HTTP status codes (v1, v2, or both) and emits a stable JSON report\n * @pipeline manual — not yet in pipeline\n * @usage node tests/integration/domain-pages-audit.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst puppeteer = require('puppeteer');\nconst { getStagedDocsPageFiles, toDocsRouteKeyFromFile } = require('../utils/file-walker');\n\nconst args = process.argv.slice(2);\nconst stagedOnly = args.includes('--staged');\nconst baseUrlArgIndex = args.indexOf('--base-url');\nconst versionArgIndex = args.indexOf('--version');\nconst baseUrl = baseUrlArgIndex >= 0 && args[baseUrlArgIndex + 1]\n ? args[baseUrlArgIndex + 1]\n : (process.env.MINT_BASE_URL || 'https://docs.livepeer.org');\nconst versionScopeRaw = versionArgIndex >= 0 && args[versionArgIndex + 1]\n ? args[versionArgIndex + 1]\n : (process.env.DOMAIN_AUDIT_VERSION || 'both');\nconst versionScope = String(versionScopeRaw).toLowerCase();\n\nconst ROOT = path.join(__dirname, '..', '..');\nconst DOCS_JSON_PATH = path.join(ROOT, 'docs.json');\nconst REPORT_PATH = path.join(ROOT, 'tests', 'reports', 'domain-page-load-report.json');\nconst REPORT_MD_PATH = path.join(ROOT, 'tests', 'reports', 'domain-page-load-report.md');\nconst TIMEOUT = 25000;\nconst CONCURRENCY = 8;\nconst ALLOWED_SCOPES = new Set(['v1', 'v2', 'both']);\n\nfunction isVersionSelected(pagePath) {\n if (versionScope === 'both') return true;\n if (versionScope === 'v1') return pagePath.startsWith('v1/');\n if (versionScope === 'v2') return pagePath.startsWith('v2/');\n return true;\n}\n\nfunction extractPagePathsFromObject(node, version, pages, seen) {\n if (typeof node === 'string') {\n const pagePath = node.replace(/\\.mdx?$/, '');\n if (pagePath.startsWith(`${version}/`) && !seen.has(pagePath)) {\n seen.add(pagePath);\n pages.push(pagePath);\n }\n return;\n }\n\n if (Array.isArray(node)) {\n node.forEach((item) => extractPagePathsFromObject(item, version, pages, seen));\n return;\n }\n\n if (!node || typeof node !== 'object') return;\n\n if (Array.isArray(node.pages)) {\n node.pages.forEach((item) => extractPagePathsFromObject(item, version, pages, seen));\n }\n\n Object.values(node).forEach((value) => {\n extractPagePathsFromObject(value, version, pages, seen);\n });\n}\n\nfunction getAllDocsPages() {\n const docsJson = JSON.parse(fs.readFileSync(DOCS_JSON_PATH, 'utf8'));\n const versions = docsJson?.navigation?.versions || [];\n const pages = [];\n const seen = new Set();\n\n versions.forEach((versionNode) => {\n const version = versionNode.version;\n if (!version || !versionNode.languages) return;\n extractPagePathsFromObject(versionNode.languages, version, pages, seen);\n });\n\n return pages;\n}\n\nfunction getStagedDocsPages() {\n return getStagedDocsPageFiles(ROOT)\n .map((filePath) => toDocsRouteKeyFromFile(filePath, ROOT))\n .filter(Boolean);\n}\n\nfunction is404Content(text, title) {\n const haystack = `${title || ''} ${text || ''}`.toLowerCase();\n return haystack.includes(\"ruh oh. this page doesn't exist\") ||\n haystack.includes('page not found') ||\n haystack.includes('404');\n}\n\nfunction pageToUrl(pagePath) {\n return `${baseUrl.replace(/\\/$/, '')}/${pagePath}`;\n}\n\nfunction toMarkdownReport(report) {\n const lines = [];\n lines.push('# Domain Page Load Report');\n lines.push('');\n lines.push(`- Timestamp: ${report.timestamp}`);\n lines.push(`- Completed: ${report.completedAt || report.timestamp}`);\n lines.push(`- Base URL: ${report.baseUrl}`);\n lines.push(`- Mode: ${report.mode}`);\n lines.push(`- Version Scope: ${report.versionScope}`);\n lines.push(`- Total: ${report.total}`);\n lines.push(`- Passed: ${report.passed}`);\n lines.push(`- Failed: ${report.failed}`);\n if (typeof report.durationSec === 'number') lines.push(`- Duration: ${report.durationSec}s`);\n lines.push('');\n\n const failed = (report.results || []).filter((r) => !r.passed);\n lines.push('## Failures');\n lines.push('');\n if (failed.length === 0) {\n lines.push('_No failures_');\n } else {\n failed.forEach((item) => {\n lines.push(`- \\`${item.pagePath}\\``);\n lines.push(` - URL: ${item.url}`);\n lines.push(` - Status: ${item.status === null ? '(none)' : item.status}`);\n lines.push(` - Title: ${item.title || '(none)'}`);\n lines.push(` - Content Length: ${item.contentLength}`);\n (item.errors || []).forEach((e) => lines.push(` - Error: ${e}`));\n });\n }\n lines.push('');\n\n return lines.join('\\n');\n}\n\nasync function testSinglePage(browser, pagePath) {\n const url = pageToUrl(pagePath);\n const page = await browser.newPage();\n\n try {\n const response = await page.goto(url, { waitUntil: 'networkidle2', timeout: TIMEOUT });\n const status = response ? response.status() : null;\n const data = await page.evaluate(() => ({\n title: document.title || '',\n text: document.body?.innerText || ''\n }));\n\n const warnings = [];\n const errors = [];\n\n if (status && status >= 400) {\n errors.push(`HTTP ${status}`);\n }\n if (is404Content(data.text, data.title)) {\n errors.push('404 content');\n }\n if ((data.text || '').trim().length < 100) {\n warnings.push(`low content length (${data.text.length})`);\n }\n\n return {\n pagePath,\n url,\n status,\n title: data.title,\n contentLength: data.text.length,\n passed: errors.length === 0,\n errors,\n warnings\n };\n } catch (error) {\n return {\n pagePath,\n url,\n status: null,\n title: '',\n contentLength: 0,\n passed: false,\n errors: [`navigation error: ${error.message}`],\n warnings: []\n };\n } finally {\n await page.close();\n }\n}\n\nasync function run() {\n if (!ALLOWED_SCOPES.has(versionScope)) {\n console.error(`❌ Invalid --version value: \"${versionScopeRaw}\". Use one of: v1, v2, both`);\n return 1;\n }\n\n const startedAt = new Date();\n const allPages = stagedOnly ? getStagedDocsPages() : getAllDocsPages();\n const shouldSkipInternal = baseUrl.includes('docs.livepeer.org');\n const pages = [...new Set(allPages)]\n .filter(isVersionSelected)\n .filter((pagePath) => !(shouldSkipInternal && pagePath.startsWith('v2/internal/')));\n\n if (pages.length === 0) {\n console.log('ℹ️ No matching docs pages to audit.');\n if (stagedOnly) {\n console.log('ℹ️ Staged mode with no matching pages; leaving existing reports unchanged.');\n return 0;\n }\n const emptyReport = {\n timestamp: startedAt.toISOString(),\n baseUrl,\n mode: stagedOnly ? 'staged' : 'all',\n versionScope,\n total: 0,\n passed: 0,\n failed: 0,\n results: []\n };\n fs.mkdirSync(path.dirname(REPORT_PATH), { recursive: true });\n fs.writeFileSync(REPORT_PATH, JSON.stringify(emptyReport, null, 2));",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:domain"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:domain:v1"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:domain:v2"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:domain:both"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tests/integration/domain-page-load-report.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tests/integration/domain-page-load-report.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tests/integration/domain-page-load-report.json, tests/integration/domain-page-load-report.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:domain); manual (npm script: test:domain:v1); manual (npm script: test:domain:v2); manual (npm script: test:domain:both)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/components/TEMPLATE.test.js",
+ "script": "component-template.test",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "R-R10",
+ "purpose_statement": "Template for category-scoped component unit tests.",
+ "pipeline_declared": "manual",
+ "usage": "node tests/unit/components/TEMPLATE.test.js",
+ "header": "#!/usr/bin/env node\n/**\n * @script component-template.test\n * @category validator\n * @purpose qa:repo-health\n * @scope tests\n * @owner docs\n * @needs R-R10\n * @purpose-statement Template for category-scoped component unit tests.\n * @pipeline manual\n * @usage node tests/unit/components/TEMPLATE.test.js\n */\n\nconst assert = require('assert');\n\nfunction runTests() {\n const errors = [];\n const warnings = [];\n\n try {\n assert.equal(true, true);\n } catch (error) {\n errors.push(error.message);\n }\n\n return { errors, warnings };\n}\n\nif (require.main === module) {\n const result = runTests();\n result.errors.forEach((error) => console.error(error));\n result.warnings.forEach((warning) => console.warn(warning));\n process.exit(result.errors.length > 0 ? 1 : 0);\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/docs-guide-sot.test.js",
+ "script": "docs-guide-sot.test",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests, docs-guide, README.md, tools/scripts/generate-docs-guide-indexes.js, tools/scripts/generate-docs-guide-pages-index.js, tools/scripts/generate-docs-guide-components-index.js",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Validates docs-guide source-of-truth coverage, README pointers, and generated index freshness",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/docs-guide-sot.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script docs-guide-sot.test\n * @category validator\n * @purpose qa:repo-health\n * @scope tests, docs-guide, README.md, tools/scripts/generate-docs-guide-indexes.js, tools/scripts/generate-docs-guide-pages-index.js, tools/scripts/generate-docs-guide-components-index.js\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Validates docs-guide source-of-truth coverage, README pointers, and generated index freshness\n * @pipeline manual — not yet in pipeline\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/docs-guide-sot.test.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst REPO_ROOT = process.cwd();\n\nconst REQUIRED_MANUAL_FILES = [\n 'docs-guide/README.mdx',\n 'docs-guide/source-of-truth-policy.mdx',\n 'docs-guide/feature-guides/feature-map.mdx',\n 'docs-guide/feature-guides/architecture-map.mdx',\n 'docs-guide/lpd.mdx',\n 'docs-guide/quality-testing/quality-gates.mdx',\n 'docs-guide/quality-testing/audit-system-overview.mdx',\n 'docs-guide/quality-testing/skill-pipeline-map.mdx',\n 'docs-guide/quality-testing/cleanup-quarantine-policy.mdx',\n 'docs-guide/quality-testing/component-layout-decision-matrix.mdx',\n 'docs-guide/feature-guides/automation-pipelines.mdx',\n 'docs-guide/feature-guides/content-system.mdx',\n 'docs-guide/feature-guides/data-integrations.mdx'\n];\n\nconst REQUIRED_GENERATED_FILES = [\n 'docs-guide/indexes/scripts-index.mdx',\n 'docs-guide/indexes/workflows-index.mdx',\n 'docs-guide/indexes/templates-index.mdx',\n 'docs-guide/indexes/pages-index.mdx',\n 'docs-guide/indexes/components-index.mdx'\n];\n\nconst REQUIRED_README_REFERENCES = [\n 'docs-guide/README.mdx',\n 'docs-guide/feature-guides/feature-map.mdx',\n 'docs-guide/source-of-truth-policy.mdx',\n 'docs-guide/lpd.mdx',\n 'docs-guide/quality-testing/quality-gates.mdx',\n 'docs-guide/quality-testing/audit-system-overview.mdx',\n 'docs-guide/quality-testing/skill-pipeline-map.mdx',\n 'docs-guide/quality-testing/cleanup-quarantine-policy.mdx',\n 'docs-guide/quality-testing/component-layout-decision-matrix.mdx',\n 'docs-guide/feature-guides/automation-pipelines.mdx',\n 'docs-guide/indexes/ai-tools.mdx',\n 'docs-guide/indexes/pages-index.mdx',\n 'docs-guide/indexes/components-index.mdx',\n 'docs-guide/indexes/scripts-index.mdx',\n 'docs-guide/indexes/workflows-index.mdx',\n 'docs-guide/indexes/templates-index.mdx'\n];\n\nfunction readFileSafe(repoPath) {\n try {\n return fs.readFileSync(path.join(REPO_ROOT, repoPath), 'utf8');\n } catch (_err) {\n return '';\n }\n}\n\nfunction isNonEmptyDoc(repoPath) {\n const content = readFileSafe(repoPath);\n return content.trim().length > 0;\n}\n\nfunction checkRequiredFiles(errors) {\n REQUIRED_MANUAL_FILES.forEach((repoPath) => {\n if (!isNonEmptyDoc(repoPath)) {\n errors.push({\n file: repoPath,\n rule: 'Required docs-guide manual file',\n message: 'Missing or empty canonical docs-guide file.',\n line: 1\n });\n }\n });\n\n REQUIRED_GENERATED_FILES.forEach((repoPath) => {\n if (!isNonEmptyDoc(repoPath)) {\n errors.push({\n file: repoPath,\n rule: 'Required docs-guide generated file',\n message: 'Missing or empty generated docs-guide index file.',\n line: 1\n });\n }\n });\n}\n\nfunction checkReadmeReferences(errors, warnings) {\n const readmePath = 'README.md';\n const content = readFileSafe(readmePath);\n if (!content.trim()) {\n errors.push({\n file: readmePath,\n rule: 'README required',\n message: 'README.md is missing or empty.',\n line: 1\n });\n return;\n }\n\n REQUIRED_README_REFERENCES.forEach((ref) => {\n if (!content.includes(ref)) {\n warnings.push({\n file: readmePath,\n rule: 'README docs-guide pointers',\n message: `README.md should reference ${ref}.`,\n line: 1\n });\n }\n });\n}\n\nfunction checkGeneratedIndexFreshness(errors) {\n const checks = [\n {\n args: ['tools/scripts/generate-docs-guide-indexes.js', '--check'],\n file: 'docs-guide/indexes/workflows-index.mdx',\n message: 'Generated docs-guide template/workflow indexes are out of date. Run generator script.'\n },\n {\n args: ['tools/scripts/generate-docs-guide-pages-index.js', '--check'],\n file: 'docs-guide/indexes/pages-index.mdx',\n message: 'Generated docs-guide pages index is out of date. Run pages index generator script.'\n },\n {\n args: ['tools/scripts/generate-docs-guide-components-index.js', '--check'],\n file: 'docs-guide/indexes/components-index.mdx',\n message: 'Generated docs-guide components index is out of date. Run components index generator script.'\n },\n {\n args: ['tests/unit/script-docs.test.js', '--check-indexes'],\n file: 'docs-guide/indexes/scripts-index.mdx',\n message: 'Generated docs-guide scripts index is out of date. Run script docs generator script.'\n },\n {\n args: ['tools/scripts/enforce-generated-file-banners.js', '--check'],\n file: 'tools/scripts/enforce-generated-file-banners.js',\n message: 'Generated banner enforcement failed. Run generated banner enforcer or relevant generators.'\n }\n ];\n\n checks.forEach((check) => {\n const cmd = spawnSync('node', check.args, { cwd: REPO_ROOT, encoding: 'utf8' });\n\n if (cmd.stdout) process.stdout.write(cmd.stdout);\n if (cmd.stderr) process.stderr.write(cmd.stderr);\n\n if (cmd.status !== 0) {\n errors.push({\n file: check.file,\n rule: 'Generated index freshness',\n message: check.message,\n line: 1\n });\n }\n });\n}\n\nfunction runTests(options = {}) {\n const errors = [];\n const warnings = [];\n\n checkRequiredFiles(errors);\n checkReadmeReferences(errors, warnings);\n checkGeneratedIndexFreshness(errors);\n\n const strict = Boolean(options.strict);\n const passed = strict ? errors.length === 0 && warnings.length === 0 : errors.length === 0;\n\n return {\n passed,\n errors,\n warnings,\n total: REQUIRED_MANUAL_FILES.length + REQUIRED_GENERATED_FILES.length\n };\n}\n\nfunction printResults(result, strict) {\n if (result.passed) {\n console.log(strict ? '✅ Docs-guide SoT checks passed in strict mode' : '✅ Docs-guide SoT checks passed');\n return;\n }\n\n if (result.errors.length > 0) {\n console.error(`❌ Docs-guide SoT errors: ${result.errors.length}`);\n result.errors.forEach((issue) => {\n console.error(` - [${issue.rule}] ${issue.file}: ${issue.message}`);\n });\n }\n\n if (result.warnings.length > 0) {\n const prefix = strict ? '❌' : '⚠️';\n const label = strict ? 'strict warnings' : 'advisory warnings';\n console.error(`${prefix} Docs-guide SoT ${label}: ${result.warnings.length}`);\n result.warnings.forEach((issue) => {\n console.error(` - [${issue.rule}] ${issue.file}: ${issue.message}`);\n });\n }\n}\n\nif (require.main === module) {\n const strict = process.argv.includes('--strict');\n const result = runTests({ strict });\n printResults(result, strict);\n process.exit(result.passed ? 0 : 1);\n}\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/mdx-component-scope.test.js",
+ "script": "mdx-component-scope.test",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests/unit, tools/scripts/validators/components",
+ "owner": "docs",
+ "needs": "R-R10, R-R29",
+ "purpose_statement": "Unit tests for the MDX-facing component scope validator — covers unsafe private helpers, safe inline logic, and imported helper patterns.",
+ "pipeline_declared": "manual — targeted validator unit coverage",
+ "usage": "node tests/unit/mdx-component-scope.test.js",
+ "header": "#!/usr/bin/env node\n/**\n * @script mdx-component-scope.test\n * @category validator\n * @purpose qa:repo-health\n * @scope tests/unit, tools/scripts/validators/components\n * @owner docs\n * @needs R-R10, R-R29\n * @purpose-statement Unit tests for the MDX-facing component scope validator — covers unsafe private helpers, safe inline logic, and imported helper patterns.\n * @pipeline manual — targeted validator unit coverage\n * @usage node tests/unit/mdx-component-scope.test.js\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst path = require('path');\nconst scopeValidator = require('../../tools/scripts/validators/components/check-mdx-component-scope');\n\nconst REPO_ROOT = path.resolve(__dirname, '../..');\nconst FIXTURE_ROOT = path.join(\n REPO_ROOT,\n 'snippets',\n 'components',\n '_codex-mdx-component-scope-fixtures'\n);\n\nlet errors = [];\nlet warnings = [];\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction writeFixtureFile(name, content) {\n fs.mkdirSync(FIXTURE_ROOT, { recursive: true });\n const fullPath = path.join(FIXTURE_ROOT, name);\n fs.writeFileSync(fullPath, `${content.trim()}\\n`, 'utf8');\n return toPosix(path.relative(REPO_ROOT, fullPath));\n}\n\nfunction removeFixtureRoot() {\n fs.rmSync(FIXTURE_ROOT, { recursive: true, force: true });\n}\n\nfunction buildIndex(relPath) {\n return new Map([\n [\n relPath,\n {\n componentPath: relPath,\n importers: new Set(['v2/home/mission-control.mdx'])\n }\n ]\n ]);\n}\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n rule: 'mdx-component-scope unit',\n message: `${name}: ${error.message}`,\n line: 1,\n file: 'tests/unit/mdx-component-scope.test.js'\n });\n }\n}\n\nasync function runTests() {\n errors = [];\n warnings = [];\n removeFixtureRoot();\n\n console.log('🧪 MDX Component Scope Unit Tests');\n\n await runCase('Unsafe private helper used by exported component fails', async () => {\n const relPath = writeFixtureFile(\n 'unsafe-helper.jsx',\n `\n const normalizeIconName = (value) => value || 'terminal';\n export const BlinkingIcon = ({ icon }) => {\n return {normalizeIconName(icon)};\n };\n `\n );\n const findings = scopeValidator.analyseComponentFile(relPath, buildIndex(relPath));\n assert.strictEqual(findings.length, 1);\n assert.strictEqual(findings[0].component, 'BlinkingIcon');\n assert.strictEqual(findings[0].helper, 'normalizeIconName');\n });\n\n await runCase('Inline guard logic passes', async () => {\n const relPath = writeFixtureFile(\n 'inline-safe.jsx',\n `\n export const BlinkingIcon = ({ icon }) => {\n const resolvedIcon = typeof icon === 'string' && icon.trim() ? icon : 'terminal';\n return {resolvedIcon};\n };\n `\n );\n const findings = scopeValidator.analyseComponentFile(relPath, buildIndex(relPath));\n assert.strictEqual(findings.length, 0);\n });\n\n await runCase('Imported .js helper passes', async () => {\n writeFixtureFile(\n 'scope-helper.js',\n `\n export const normalizeIconName = (value) => value || 'terminal';\n `\n );\n const relPath = writeFixtureFile(\n 'imported-helper.jsx',\n `\n import { normalizeIconName } from './scope-helper.js';\n export const BlinkingIcon = ({ icon }) => {\n return {normalizeIconName(icon)};\n };\n `\n );\n const findings = scopeValidator.analyseComponentFile(relPath, buildIndex(relPath));\n assert.strictEqual(findings.length, 0);\n });\n\n await runCase('Trailing export pattern fails correctly', async () => {\n const relPath = writeFixtureFile(\n 'trailing-export.jsx',\n `\n const normalizeIconName = (value) => value || 'terminal';\n const BlinkingIcon = ({ icon }) => {normalizeIconName(icon)};\n export { BlinkingIcon };\n `\n );\n const findings = scopeValidator.analyseComponentFile(relPath, buildIndex(relPath));\n assert.strictEqual(findings.length, 1);\n assert.strictEqual(findings[0].component, 'BlinkingIcon');\n assert.strictEqual(findings[0].helper, 'normalizeIconName');\n });\n\n await runCase('Non-MDX-facing component file is ignored', async () => {\n const relPath = writeFixtureFile(\n 'not-imported.jsx',\n `\n const normalizeIconName = (value) => value || 'terminal';\n export const BlinkingIcon = ({ icon }) => {normalizeIconName(icon)};\n `\n );\n const findings = scopeValidator.analyseComponentFile(relPath, new Map());\n assert.strictEqual(findings.length, 0);\n });\n\n await runCase('Explicit empty MDX scope does not expand to all routable pages', async () => {\n const importedFiles = scopeValidator.getComponentFilesImportedByMdxFiles([]);\n assert.deepStrictEqual(importedFiles, []);\n });\n\n await runCase('Component-local variables are not false-flagged', async () => {\n const relPath = writeFixtureFile(\n 'component-local.jsx',\n `\n export const BlinkingIcon = ({ icon }) => {\n const normalizeIconName = (value) => value || 'terminal';\n return {normalizeIconName(icon)};\n };\n `\n );\n const findings = scopeValidator.analyseComponentFile(relPath, buildIndex(relPath));\n assert.strictEqual(findings.length, 0);\n });\n\n removeFixtureRoot();\n\n return {\n errors,\n warnings,\n passed: errors.length === 0,\n total: 7\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ MDX component scope unit tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} MDX component scope unit test failure(s)`);\n result.errors.forEach((err) => console.error(` - ${err.message}`));\n process.exit(1);\n })\n .catch((error) => {\n removeFixtureRoot();\n console.error(`\\n❌ MDX component scope unit tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/migrate-assets-to-branch.test.js",
+ "script": "migrate-assets-to-branch.test",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests/unit, tools/scripts/remediators/assets, tools/scripts/audit-media-assets.js",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Unit tests for migrate-assets-to-branch.js — validates CLI defaults, ambiguous basename detection, deterministic rewrites, and end-to-end branch migration in a temp git repo",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/migrate-assets-to-branch.test.js",
+ "header": "#!/usr/bin/env node\n/**\n * @script migrate-assets-to-branch.test\n * @category validator\n * @purpose qa:repo-health\n * @scope tests/unit, tools/scripts/remediators/assets, tools/scripts/audit-media-assets.js\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Unit tests for migrate-assets-to-branch.js — validates CLI defaults, ambiguous basename detection, deterministic rewrites, and end-to-end branch migration in a temp git repo\n * @pipeline manual — not yet in pipeline\n * @dualmode --dry-run (validator) | --write (remediator)\n * @usage node tests/unit/migrate-assets-to-branch.test.js\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst migrateAssets = require('../../tools/scripts/remediators/assets/migrate-assets-to-branch');\n\nconst SCRIPT_PATH = path.join(\n __dirname,\n '..',\n '..',\n 'tools',\n 'scripts',\n 'remediators',\n 'assets',\n 'migrate-assets-to-branch.js'\n);\n\nlet errors = [];\n\nfunction runCommand(command, args, options = {}) {\n const env = { ...process.env };\n delete env.GIT_DIR;\n delete env.GIT_WORK_TREE;\n delete env.GIT_INDEX_FILE;\n delete env.GIT_OBJECT_DIRECTORY;\n delete env.GIT_ALTERNATE_OBJECT_DIRECTORIES;\n delete env.GIT_COMMON_DIR;\n delete env.GIT_PREFIX;\n\n const result = spawnSync(command, args, {\n cwd: options.cwd,\n encoding: 'utf8',\n env\n });\n\n if (result.status !== 0 && !options.allowFailure) {\n const detail = String(result.stderr || result.stdout || `exit ${result.status}`).trim();\n throw new Error(`${command} ${args.join(' ')} failed: ${detail}`);\n }\n\n return result;\n}\n\nfunction runCase(name, fn) {\n try {\n fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n rule: 'migrate-assets-to-branch unit',\n message: `${name}: ${error.message}`,\n line: 1,\n file: 'tests/unit/migrate-assets-to-branch.test.js'\n });\n }\n}\n\nfunction writeFile(repoRoot, repoPath, value) {\n const absPath = path.join(repoRoot, repoPath);\n fs.mkdirSync(path.dirname(absPath), { recursive: true });\n fs.writeFileSync(absPath, value);\n}\n\nfunction createFixtureRepo() {\n const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'migrate-assets-repo-'));\n const remoteDir = path.join(tempRoot, 'remote.git');\n const repoDir = path.join(tempRoot, 'repo');\n\n fs.mkdirSync(repoDir, { recursive: true });\n runCommand('git', ['init', '--bare', remoteDir]);\n runCommand('git', ['init', '-b', 'docs-v2'], { cwd: repoDir });\n runCommand('git', ['config', 'user.name', 'Codex Test'], { cwd: repoDir });\n runCommand('git', ['config', 'user.email', 'codex@example.com'], { cwd: repoDir });\n runCommand('git', ['remote', 'add', 'origin', remoteDir], { cwd: repoDir });\n\n writeFile(\n repoDir,\n 'snippets/assets/media/videos/HeroBackground.mp4',\n 'fake-video-binary'\n );\n writeFile(\n repoDir,\n 'v2/example.mdx',\n '\\n'\n );\n writeFile(\n repoDir,\n 'tasks/reports/media-audit/media-audit-manifest.json',\n `${JSON.stringify({\n assets: [\n {\n path: 'snippets/assets/media/videos/HeroBackground.mp4',\n migration_target: 'migrate_r2',\n mdx_references: ['v2/example.mdx']\n }\n ]\n }, null, 2)}\\n`\n );\n writeFile(\n repoDir,\n 'tests/package.json',\n `${JSON.stringify({\n name: 'fixture-tests',\n private: true,\n scripts: {\n test: 'node ok.js'\n }\n }, null, 2)}\\n`\n );\n writeFile(repoDir, 'tests/ok.js', \"console.log('ok');\\n\");\n\n runCommand('git', ['add', '.'], { cwd: repoDir });\n runCommand('git', ['commit', '-m', 'initial docs-v2 fixture'], { cwd: repoDir });\n runCommand('git', ['push', '-u', 'origin', 'docs-v2'], { cwd: repoDir });\n\n const existingAssetsBranch = runCommand(\n 'git',\n ['show-ref', '--verify', '--quiet', 'refs/heads/docs-v2-assets'],\n {\n cwd: repoDir,\n allowFailure: true\n }\n );\n if (existingAssetsBranch.status === 0) {\n runCommand('git', ['branch', '-D', 'docs-v2-assets'], { cwd: repoDir });\n }\n runCommand('git', ['checkout', '--orphan', 'docs-v2-assets'], { cwd: repoDir });\n runCommand('git', ['rm', '-rf', '.'], { cwd: repoDir });\n writeFile(repoDir, '.nojekyll', '');\n runCommand('git', ['add', '.nojekyll'], { cwd: repoDir });\n runCommand('git', ['commit', '-m', 'init assets branch'], { cwd: repoDir });\n runCommand('git', ['push', '-u', 'origin', 'docs-v2-assets'], { cwd: repoDir });\n runCommand('git', ['checkout', 'docs-v2'], { cwd: repoDir });\n runCommand('git', ['fetch', 'origin', 'docs-v2-assets'], { cwd: repoDir });\n\n return { tempRoot, remoteDir, repoDir };\n}\n\nfunction cleanupFixtureRepo(fixture) {\n if (!fixture) return;\n fs.rmSync(fixture.tempRoot, { recursive: true, force: true });\n}\n\nfunction runTests() {\n errors = [];\n\n console.log('🧪 Migrate Assets To Branch Unit Tests');\n\n runCase('Parses default args as dry-run with default targets', () => {\n const parsed = migrateAssets.parseArgs([]);\n assert.strictEqual(parsed.mode, 'dry-run');\n assert.deepStrictEqual(parsed.targets, ['migrate_r2', 'migrate_cloudinary']);\n assert.strictEqual(parsed.skipCopy, false);\n assert.strictEqual(parsed.skipRefs, false);\n });\n\n runCase('Flags basename-only references as ambiguous', () => {\n const analysis = migrateAssets.analyzeSourceReference(\n 'v2/example.mdx',\n 'snippets/assets/media/videos/HeroBackground.mp4',\n ''\n );\n\n assert.strictEqual(analysis.ambiguousBasenameOnly, true);\n assert.strictEqual(analysis.needsRewrite, false);\n });\n\n runCase('Rewrites rooted and relative references to canonical raw GitHub URLs', () => {\n const rooted = migrateAssets.rewriteSourceReference(\n 'v2/example.mdx',\n 'snippets/assets/media/videos/HeroBackground.mp4',\n ''\n );\n const relative = migrateAssets.rewriteSourceReference(\n 'v2/example.mdx',\n 'snippets/assets/media/videos/HeroBackground.mp4',\n ''\n );\n\n assert.ok(rooted.content.includes(migrateAssets.buildCanonicalAssetUrl('snippets/assets/media/videos/HeroBackground.mp4')));\n assert.ok(relative.content.includes(migrateAssets.buildCanonicalAssetUrl('snippets/assets/media/videos/HeroBackground.mp4')));\n });\n\n runCase('Write mode copies, rewrites, and deletes in a temp repo fixture', () => {\n const fixture = createFixtureRepo();\n\n try {\n const result = runCommand(\n 'node',\n [\n SCRIPT_PATH,\n '--manifest',\n 'tasks/reports/media-audit/media-audit-manifest.json',\n '--file',\n 'snippets/assets/media/videos/HeroBackground.mp4',\n '--write'\n ],\n {\n cwd: fixture.repoDir\n }\n );\n\n assert.strictEqual(result.status, 0);\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFile"
+ },
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tests/unit/repo",
+ "type": "directory",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": "tests/unit/repo",
+ "type": "directory",
+ "call": "writeFile"
+ }
+ ],
+ "outputs_display": ", , tests/unit/repo, tests/unit/repo",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/repo-audit-pipeline.test.js",
+ "script": "repo-audit-pipeline.test",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests/unit, tools/scripts, ai-tools/ai-skills/catalog, ai-tools/agent-packs",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Tests repo-audit-orchestrator.js pipeline — validates mode/scope combinations and report output",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/repo-audit-pipeline.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script repo-audit-pipeline.test\n * @category validator\n * @purpose qa:repo-health\n * @scope tests/unit, tools/scripts, ai-tools/ai-skills/catalog, ai-tools/agent-packs\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Tests repo-audit-orchestrator.js pipeline — validates mode/scope combinations and report output\n * @pipeline manual — not yet in pipeline\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/repo-audit-pipeline.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst REPO_ROOT = process.cwd();\n\nlet errors = [];\nlet warnings = [];\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction readJson(repoPath) {\n return JSON.parse(fs.readFileSync(path.join(REPO_ROOT, repoPath), 'utf8'));\n}\n\nfunction runNode(args) {\n return spawnSync('node', args, {\n cwd: REPO_ROOT,\n encoding: 'utf8'\n });\n}\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n rule: 'repo-audit-pipeline unit',\n message: `${name}: ${error.message}`,\n line: 1,\n file: 'tests/unit/repo-audit-pipeline.test.js'\n });\n }\n}\n\nasync function runTests() {\n errors = [];\n warnings = [];\n\n console.log('🧪 Repo Audit Pipeline Unit Tests');\n\n const tmpRoot = path.join(REPO_ROOT, 'tests/reports/repo-audit-pipeline-unit', String(Date.now()));\n const tmpAuditOutputRel = toPosix(path.relative(REPO_ROOT, path.join(tmpRoot, 'audit')));\n const tmpPacksOutputRel = toPosix(path.relative(REPO_ROOT, path.join(tmpRoot, 'packs')));\n\n fs.mkdirSync(path.join(tmpRoot, 'audit'), { recursive: true });\n fs.mkdirSync(path.join(tmpRoot, 'packs'), { recursive: true });\n\n await runCase('Skill catalogue includes required audit stages', async () => {\n const catalog = readJson('ai-tools/ai-skills/catalog/skill-catalog.json');\n const ids = new Set((catalog.skills || []).map((skill) => skill.id));\n\n [\n 'repo-audit-orchestrator',\n 'script-footprint-and-usage-audit',\n 'docs-quality-and-freshness-audit',\n 'style-and-language-homogenizer-en-gb',\n 'component-layout-governance',\n 'cleanup-quarantine-manager',\n 'cross-agent-packager'\n ].forEach((id) => assert.ok(ids.has(id), `Missing skill id: ${id}`));\n });\n\n await runCase('Execution manifest stage IDs map to skill catalogue IDs', async () => {\n const catalog = readJson('ai-tools/ai-skills/catalog/skill-catalog.json');\n const manifest = readJson('ai-tools/ai-skills/catalog/execution-manifest.json');\n const ids = new Set((catalog.skills || []).map((skill) => skill.id));\n\n (manifest.pipeline || []).forEach((entry) => {\n assert.ok(ids.has(entry.id), `Manifest stage missing from catalogue: ${entry.id}`);\n });\n });\n\n await runCase('Orchestrator static dry-run writes unified scorecard output', async () => {\n const result = runNode([\n 'tools/scripts/repo-audit-orchestrator.js',\n '--mode',\n 'static',\n '--scope',\n 'changed',\n '--output-dir',\n tmpAuditOutputRel\n ]);\n\n if (result.stdout) process.stdout.write(result.stdout);\n if (result.stderr) process.stderr.write(result.stderr);\n\n assert.strictEqual(result.status, 0, 'Orchestrator exited non-zero in static dry-run mode.');\n\n const summaryJson = path.join(tmpRoot, 'audit', 'repo-audit-summary.json');\n const summaryMd = path.join(tmpRoot, 'audit', 'repo-audit-summary.md');\n\n assert.ok(fs.existsSync(summaryJson), 'Missing repo-audit-summary.json output.');\n assert.ok(fs.existsSync(summaryMd), 'Missing repo-audit-summary.md output.');\n\n const payload = JSON.parse(fs.readFileSync(summaryJson, 'utf8'));\n assert.strictEqual(payload.mode, 'static');\n assert.strictEqual(payload.scope, 'changed');\n assert.ok(typeof payload.score?.score === 'number', 'Missing scorecard numeric score.');\n assert.ok(Array.isArray(payload.stages) && payload.stages.length >= 1, 'Missing stage results.');\n });\n\n await runCase('Cleanup manager classify mode writes non-mutating manifest', async () => {\n const result = runNode([\n 'tools/scripts/cleanup-quarantine-manager.js',\n '--output-dir',\n tmpAuditOutputRel\n ]);\n\n if (result.stdout) process.stdout.write(result.stdout);\n if (result.stderr) process.stderr.write(result.stderr);\n\n assert.strictEqual(result.status, 0, 'Cleanup classify exited non-zero.');\n\n const manifestJson = path.join(tmpRoot, 'audit', 'cleanup-quarantine-manifest.json');\n assert.ok(fs.existsSync(manifestJson), 'Missing cleanup quarantine manifest output.');\n\n const manifest = JSON.parse(fs.readFileSync(manifestJson, 'utf8'));\n assert.strictEqual(manifest.mode, 'classify');\n assert.ok(Array.isArray(manifest.entries), 'Manifest entries must be an array.');\n });\n\n await runCase('Cross-agent packager emits all pack targets from one catalogue', async () => {\n const result = runNode([\n 'tools/scripts/cross-agent-packager.js',\n '--agent-pack',\n 'all',\n '--output-dir',\n tmpPacksOutputRel\n ]);\n\n if (result.stdout) process.stdout.write(result.stdout);\n if (result.stderr) process.stderr.write(result.stderr);\n\n assert.strictEqual(result.status, 0, 'Cross-agent packager exited non-zero.');\n\n const expected = [\n path.join(tmpRoot, 'packs', 'codex', 'skills-manifest.json'),\n path.join(tmpRoot, 'packs', 'cursor', 'rules.md'),\n path.join(tmpRoot, 'packs', 'claude', 'CLAUDE.md'),\n path.join(tmpRoot, 'packs', 'windsurf', 'rules.md'),\n path.join(tmpRoot, 'packs', 'README.md')\n ];\n\n expected.forEach((outputPath) => {\n assert.ok(fs.existsSync(outputPath), `Missing output: ${toPosix(path.relative(REPO_ROOT, outputPath))}`);\n });\n });\n\n return {\n errors,\n warnings,\n passed: errors.length === 0,\n total: 5\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ Repo audit pipeline unit tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} repo audit pipeline unit test failure(s)`);\n result.errors.forEach((err) => console.error(` - ${err.message}`));\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ Repo audit pipeline unit tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:repo-audit"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:repo-audit)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/lib/docs-usefulness/prompts/index.js",
+ "script": "prompts/index",
+ "category": "utility",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "R-R14",
+ "purpose_statement": "LLM prompt template for index page-type usefulness evaluation.",
+ "pipeline_declared": "indirect -- library module",
+ "usage": "const { getPrompt } = require('../lib/docs-usefulness/prompts/index');",
+ "header": "'use strict';\n/**\n * @script prompts/index\n * @category utility\n * @purpose qa:repo-health\n * @scope single-domain\n * @owner docs\n * @needs R-R14\n * @purpose-statement LLM prompt template for index page-type usefulness evaluation.\n * @pipeline indirect -- library module\n * @usage const { getPrompt } = require('../lib/docs-usefulness/prompts/index');\n */\n\nconst landing = require('./landing');\nconst overview = require('./overview');\nconst concept = require('./concept');\nconst howTo = require('./how_to');\nconst tutorial = require('./tutorial');\nconst reference = require('./reference');\nconst faq = require('./faq');\nconst troubleshooting = require('./troubleshooting');\nconst glossary = require('./glossary');\nconst changelog = require('./changelog');\n\nmodule.exports = {\n ...landing,\n ...overview,\n ...concept,\n ...howTo,\n ...tutorial,\n ...reference,\n ...faq,\n ...troubleshooting,\n ...glossary,\n ...changelog\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/audit-tasks-folders.js",
+ "script": "audit-tasks-folders",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tools/scripts, tasks",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Tasks folder auditor — checks tasks/ structure, normalises report locations, applies recommendations with conflict-safe moves",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/audit-tasks-folders.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script audit-tasks-folders\n * @category validator\n * @purpose qa:repo-health\n * @scope tools/scripts, tasks\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Tasks folder auditor — checks tasks/ structure, normalises report locations, applies recommendations with conflict-safe moves\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/audit-tasks-folders.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\n\nconst REPO_ROOT = process.cwd();\nconst TASKS_ROOT = path.join(REPO_ROOT, 'tasks');\nconst TOOL_SCRIPTS_ROOT = path.join(REPO_ROOT, 'tools', 'scripts');\nconst AUDIT_FILE_SUFFIX = '_audit.md';\nconst DEFAULT_AUDIT_OUTPUT_DIR = 'tasks/reports/repo-ops';\nconst RECOMMENDATION_SUMMARY_PATH = 'tasks/reports/repo-ops/recommendation-apply-summary.json';\nconst RECOMMENDATION_CONFLICTS_PATH = 'tasks/reports/repo-ops/recommendation-conflicts.md';\n\nconst PATH_EXCLUSIONS = ['/tasks/context_data/', '/_contextData_/'];\nconst BINARY_EXTENSIONS = new Set([\n '.png',\n '.jpg',\n '.jpeg',\n '.gif',\n '.webp',\n '.svg',\n '.pdf',\n '.zip',\n '.gz',\n '.mp4',\n '.mov',\n '.avi',\n '.ico'\n]);\n\nconst AUTOGEN_FORCE_TRUE = new Set([\n 'tasks/reports/repo-ops/SCRIPT_AUDIT.json',\n 'tasks/reports/page-audits/browser-test-report.json',\n 'tasks/reports/repo-ops/component-usage-audit.json',\n 'tasks/reports/comprehensive-v2-pages-browser-audit.json',\n 'tasks/reports/page-audit-1771297377437.json'\n]);\n\nconst AUTOGEN_FORCE_FALSE = new Set([\n 'tasks/reports/readme-refactor-plan.md',\n 'tasks/reports/non-technical-contribution-proposal.md',\n 'tasks/reports/livepeer-docs-v2-retrospective-submission-draft.md',\n 'tasks/reports/pr-754-summary.md',\n 'tasks/reports/upstream-merge-plan.md',\n 'tasks/reports/repository-ruleset.md',\n 'tasks/reports/repository-structure-audit.md',\n 'tasks/reports/repository-audit-summary.md',\n 'tasks/reports/retrospective-claims-verification-2026-02-18.md'\n]);\n\nconst USEFUL_FORCE_FALSE = new Set([\n 'tasks/test-delete-me.md',\n 'tasks/plan/NEW-LIST.md',\n 'tasks/plan/create-github-issue-templates.md',\n 'tasks/reports/COMPONENT_USAGE_AUDIT_REPORT'\n]);\n\nconst USEFUL_FORCE_TRUE = new Set([\n 'tasks/errors/component-bugs.md',\n 'tasks/errors/component-verification-report.md',\n 'tasks/errors/component-recommendations.md',\n 'tasks/errors/testing-methodology.md'\n]);\n\nconst ROOT_DESTINATION_OVERRIDES = new Map([\n ['tasks/DRY-and-cleaner-recommendations.md', 'tasks/plan/rfp/DRY-and-cleaner-recommendations.md'],\n ['tasks/DRY-tasks-feasibility-report.md', 'tasks/plan/rfp/DRY-tasks-feasibility-report.md'],\n ['tasks/LIVEPEER-STUDIO-GAPS-AND-VERACITY.md', 'tasks/plan/rfp/LIVEPEER-STUDIO-GAPS-AND-VERACITY.md'],\n ['tasks/LIVEPEER-STUDIO-V1-INVENTORY-AND-IA.md', 'tasks/plan/rfp/LIVEPEER-STUDIO-V1-INVENTORY-AND-IA.md'],\n ['tasks/docs-v2-rfp-task-list-and-plan.md', 'tasks/plan/rfp/docs-v2-rfp-task-list-and-plan.md'],\n ['tasks/non-essential-tasks-audit-for-ai-and-community.md', 'tasks/plan/rfp/non-essential-tasks-audit-for-ai-and-community.md'],\n ['tasks/MDX-ERRORS-AND-FIXES-REPORT.md', 'tasks/errors/MDX-ERRORS-AND-FIXES-REPORT.md'],\n ['tasks/test-delete-me.md', 'tasks/plan/archived/test-delete-me.md']\n]);\n\nfunction usage() {\n console.log(\n [\n 'Usage:',\n ' node tools/scripts/audit-tasks-folders.js [--dry-run] [--apply] [--apply-recommendations] [--recommendation-scope full|targeted] [--conflict-policy pause] [--audit-output-dir ] [--folders ]',\n '',\n 'Examples:',\n ' node tools/scripts/audit-tasks-folders.js',\n ' node tools/scripts/audit-tasks-folders.js --dry-run',\n ' node tools/scripts/audit-tasks-folders.js --apply',\n ' node tools/scripts/audit-tasks-folders.js --apply-recommendations --recommendation-scope full',\n ' node tools/scripts/audit-tasks-folders.js --apply-recommendations --dry-run --recommendation-scope full',\n ' node tools/scripts/audit-tasks-folders.js --folders plan,reports,report'\n ].join('\\n')\n );\n}\n\nfunction parseArgs(argv) {\n const options = {\n apply: false,\n dryRun: false,\n applyRecommendations: false,\n recommendationScope: 'full',\n conflictPolicy: 'pause',\n foldersArg: '',\n auditOutputDir: DEFAULT_AUDIT_OUTPUT_DIR\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--apply') {\n options.apply = true;\n continue;\n }\n if (token === '--dry-run') {\n options.dryRun = true;\n continue;\n }\n if (token === '--apply-recommendations') {\n options.applyRecommendations = true;\n continue;\n }\n if (token === '--recommendation-scope') {\n options.recommendationScope = String(argv[i + 1] || '').trim() || options.recommendationScope;\n i += 1;\n continue;\n }\n if (token.startsWith('--recommendation-scope=')) {\n options.recommendationScope = token.slice('--recommendation-scope='.length).trim() || options.recommendationScope;\n continue;\n }\n if (token === '--conflict-policy') {\n options.conflictPolicy = String(argv[i + 1] || '').trim() || options.conflictPolicy;\n i += 1;\n continue;\n }\n if (token.startsWith('--conflict-policy=')) {\n options.conflictPolicy = token.slice('--conflict-policy='.length).trim() || options.conflictPolicy;\n continue;\n }\n if (token === '--audit-output-dir') {\n options.auditOutputDir = String(argv[i + 1] || '').trim() || options.auditOutputDir;\n i += 1;\n continue;\n }\n if (token.startsWith('--audit-output-dir=')) {\n options.auditOutputDir = token.slice('--audit-output-dir='.length).trim() || options.auditOutputDir;\n continue;\n }\n if (token === '--folders') {\n options.foldersArg = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--folders=')) {\n options.foldersArg = token.slice('--folders='.length).trim();\n continue;\n }\n\n console.error(`Unknown argument: ${token}`);\n usage();\n process.exit(1);\n }\n\n if (options.apply && options.dryRun && !options.applyRecommendations) {\n console.error('Use either --apply or --dry-run, not both.');\n process.exit(1);\n }\n\n if (!['full', 'targeted'].includes(options.recommendationScope)) {\n console.error(`Invalid --recommendation-scope: ${options.recommendationScope}`);\n usage();\n process.exit(1);\n }\n\n if (options.conflictPolicy !== 'pause') {\n console.error(`Invalid --conflict-policy: ${options.conflictPolicy}. Only \"pause\" is supported.`);\n usage();\n process.exit(1);\n }\n\n return options;\n}\n\nfunction normalizeRepoPath(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction trimLeadingDotSlash(value) {\n return String(value || '').replace(/^[.][/\\\\]+/, '');\n}\n\nfunction normalizeCliRepoPath(inputPath) {\n const raw = String(inputPath || '').trim();\n if (!raw) return '';\n\n let repoPath = raw;\n if (path.isAbsolute(raw)) {\n const rel = path.relative(REPO_ROOT, raw);\n if (rel.startsWith('..')) {\n throw new Error(`Path outside repository is not allowed: ${raw}`);\n }\n repoPath = rel;\n }\n\n return normalizeRepoPath(trimLeadingDotSlash(repoPath));\n}\n\nfunction normalizeAuditOutputDir(inputPath) {\n const repoPath = normalizeCliRepoPath(inputPath || DEFAULT_AUDIT_OUTPUT_DIR);\n if (!repoPath) return DEFAULT_AUDIT_OUTPUT_DIR;\n if (!repoPath.startsWith('tasks')) {\n throw new Error(`--audit-output-dir must resolve under tasks/: ${repoPath}`);",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/cleanup-quarantine-manager.js",
+ "script": "cleanup-quarantine-manager",
+ "category": "remediator",
+ "purpose": "qa:repo-health",
+ "scope": "tools/scripts, tasks/reports/repo-ops, tasks/quarantine",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Quarantine manager — classifies files for quarantine (default) or applies quarantine moves (--apply)",
+ "pipeline_declared": "manual",
+ "usage": "node tools/scripts/cleanup-quarantine-manager.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script cleanup-quarantine-manager\n * @category remediator\n * @purpose qa:repo-health\n * @scope tools/scripts, tasks/reports/repo-ops, tasks/quarantine\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Quarantine manager — classifies files for quarantine (default) or applies quarantine moves (--apply)\n * @pipeline manual\n * @usage node tools/scripts/cleanup-quarantine-manager.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst STAGE_ID = 'cleanup-quarantine-manager';\nconst REPO_ROOT = process.cwd();\nconst DEFAULT_OUTPUT_DIR = 'tasks/reports/repo-ops';\nconst DEFAULT_QUARANTINE_ROOT = 'tasks/quarantine/repo-audit';\n\nconst STAGE_REPORT_FILES = [\n 'script-footprint-and-usage-audit.json',\n 'docs-quality-and-freshness-audit.json',\n 'style-and-language-homogenizer-en-gb.json',\n 'component-layout-governance.json'\n];\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction parseArgs(argv) {\n const out = {\n apply: false,\n outputDir: DEFAULT_OUTPUT_DIR,\n manifestPath: '',\n quarantineRoot: DEFAULT_QUARANTINE_ROOT\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n\n if (token === '--apply') {\n out.apply = true;\n continue;\n }\n if (token === '--output-dir') {\n out.outputDir = String(argv[i + 1] || out.outputDir).trim() || out.outputDir;\n i += 1;\n continue;\n }\n if (token.startsWith('--output-dir=')) {\n out.outputDir = token.slice('--output-dir='.length).trim() || out.outputDir;\n continue;\n }\n if (token === '--manifest') {\n out.manifestPath = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--manifest=')) {\n out.manifestPath = token.slice('--manifest='.length).trim();\n continue;\n }\n if (token === '--quarantine-root') {\n out.quarantineRoot = String(argv[i + 1] || out.quarantineRoot).trim() || out.quarantineRoot;\n i += 1;\n continue;\n }\n if (token.startsWith('--quarantine-root=')) {\n out.quarantineRoot = token.slice('--quarantine-root='.length).trim() || out.quarantineRoot;\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n return out;\n}\n\nfunction readJsonFile(absPath, fallback = null) {\n try {\n return JSON.parse(fs.readFileSync(absPath, 'utf8'));\n } catch (_error) {\n return fallback;\n }\n}\n\nfunction walkFiles(dirPath, out = []) {\n if (!fs.existsSync(dirPath)) return out;\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = path.join(dirPath, entry.name);\n const relPath = toPosix(path.relative(REPO_ROOT, fullPath));\n\n if (entry.isDirectory()) {\n if (entry.name === '.git' || entry.name === 'node_modules') continue;\n walkFiles(fullPath, out);\n continue;\n }\n\n out.push({ absPath: fullPath, relPath });\n }\n\n return out;\n}\n\nfunction loadRetentionPolicy() {\n const policyPath = path.join(REPO_ROOT, 'tools/config/report-retention-policy.json');\n return readJsonFile(policyPath, {\n rules: [],\n default_action: 'keep_latest',\n size_thresholds: { warn_bytes: 524288, high_bytes: 2097152, critical_bytes: 10485760 }\n });\n}\n\nfunction makeEntry(pathValue, reason, confidence, action, sourceStage) {\n return {\n path: toPosix(pathValue),\n reason,\n confidence,\n action,\n restore_hint: '',\n review_owner: 'docs',\n source_stage: sourceStage\n };\n}\n\nfunction addEntry(entries, entry) {\n if (!entry.path || entry.path.includes('<->')) return;\n\n const key = `${entry.path}::${entry.action}`;\n const existing = entries._index.get(key);\n\n if (existing) {\n if (!existing.reason.includes(entry.reason)) {\n existing.reason = `${existing.reason} | ${entry.reason}`;\n }\n existing.confidence = Math.max(Number(existing.confidence || 0), Number(entry.confidence || 0));\n return;\n }\n\n entries._index.set(key, entry);\n entries.list.push(entry);\n}\n\nfunction classifyFromScriptIssues(issues, entryBag) {\n issues.forEach((issue) => {\n const issueId = String(issue.id || '');\n const issuePath = toPosix(issue.path || '');\n\n if (issueId === 'backup-artifact') {\n addEntry(entryBag, makeEntry(issuePath, 'Backup artifact should be quarantined from active tree.', 0.98, 'quarantine', STAGE_ID));\n return;\n }\n\n if (issueId === 'placeholder-script') {\n addEntry(entryBag, makeEntry(issuePath, 'Placeholder script is discoverable and should not stay in active script tree.', 0.96, 'quarantine', STAGE_ID));\n return;\n }\n\n if (issueId === 'duplicate-script-pair') {\n const parts = issuePath\n .split('<->')\n .map((part) => toPosix(part.trim()))\n .filter(Boolean);\n parts.forEach((candidatePath) => {\n addEntry(\n entryBag,\n makeEntry(candidatePath, 'Duplicate script pair candidate; keep one canonical source and quarantine/retire duplicate after validation.', 0.67, 'delete-later', STAGE_ID)\n );\n });\n return;\n }\n\n if (issueId === 'report-size-hotspot') {\n addEntry(\n entryBag,\n makeEntry(issuePath, 'Large report artifact should move to retention/archive policy path after summary extraction.', 0.63, 'delete-later', STAGE_ID)\n );\n }\n });\n}\n\nfunction classifyFromRetentionPolicy(entryBag, policy) {\n const rules = Array.isArray(policy.rules) ? policy.rules : [];\n const quarantineRule = rules.find((rule) =>\n String(rule.pattern || '').includes('tasks/quarantine/repo-audit')\n );\n if (!quarantineRule) return;\n\n const quarantineRoot = path.join(REPO_ROOT, 'tasks', 'quarantine', 'repo-audit');\n if (!fs.existsSync(quarantineRoot)) return;\n\n walkFiles(quarantineRoot).forEach((file) => {\n addEntry(\n entryBag,\n makeEntry(file.relPath, 'Matched report-retention policy quarantine pattern.', 0.95, 'quarantine', STAGE_ID)\n );\n });\n}\n\nfunction classifyBackupFiles(entryBag) {\n const roots = [\n path.join(REPO_ROOT, 'tools/scripts'),\n path.join(REPO_ROOT, 'tests'),\n path.join(REPO_ROOT, 'tasks')\n ];\n\n roots.forEach((root) => {\n walkFiles(root)\n .filter((file) => /\\.bak2?$/i.test(file.relPath))\n .forEach((file) => {\n addEntry(",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "cleanup:classify"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "cleanup:quarantine"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/cleanup-quarantine-manifest.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/cleanup-quarantine-manifest.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: cleanup:classify); manual (npm script: cleanup:quarantine)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/generate-component-governance-remediation-reports.js",
+ "script": "generate-component-governance-remediation-reports",
+ "category": "generator",
+ "purpose": "qa:repo-health",
+ "scope": "generated-output",
+ "owner": "docs",
+ "needs": "R-R10, R-R29",
+ "purpose_statement": "Generates component-governance remediation reports from the approved audit and live repo state, including defensive-rendering guidance for MDX-facing components.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/scripts/generate-component-governance-remediation-reports.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script generate-component-governance-remediation-reports\n * @category generator\n * @purpose qa:repo-health\n * @summary Generate Phase 2a remediation reports from the approved component-governance audit and live repo state.\n * @owner docs\n * @scope generated-output\n * @needs R-R10, R-R29\n * @purpose-statement Generates component-governance remediation reports from the approved audit and live repo state, including defensive-rendering guidance for MDX-facing components.\n * @pipeline manual\n * @usage node tools/scripts/generate-component-governance-remediation-reports.js [flags]\n *\n * @inputs\n * --audit-file (default: tasks/reports/component-governance-audit.md)\n * --output-dir (default: tasks/reports)\n * --reports (default: all)\n * --strict / --no-strict (default: strict)\n *\n * @outputs\n * - tasks/reports/migration-impact-report.md\n * - tasks/reports/colour-remediation-report.md\n * - tasks/reports/style-css-token-audit.md\n * - tasks/reports/defensive-rendering-remediation-report.md\n *\n * @exit-codes\n * 0 = reports generated successfully\n * 1 = invalid args, audit drift that blocks strict mode, or runtime failure\n *\n * @examples\n * node tools/scripts/generate-component-governance-remediation-reports.js\n * node tools/scripts/generate-component-governance-remediation-reports.js --reports migration,defensive\n * node tools/scripts/generate-component-governance-remediation-reports.js --no-strict\n *\n * @notes\n * Phase 2a is read-only for runtime docs/component source. This script only reads repo state and writes report artefacts.\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst {\n buildGeneratedMarkdownCommentLines,\n sanitizeMarkdownTableCellText\n} = require('../lib/mdx-safe-markdown');\nconst {\n buildMdxFacingComponentIndex\n} = require('./validators/components/check-mdx-component-scope');\n\nconst REPO_ROOT = path.resolve(__dirname, '..', '..');\nconst DEFAULT_AUDIT_FILE = 'tasks/reports/component-governance-audit.md';\nconst DEFAULT_OUTPUT_DIR = 'tasks/reports';\nconst REPORT_GENERATOR_SCRIPT = 'tools/scripts/generate-component-governance-remediation-reports.js';\nconst VALID_REPORTS = ['migration', 'colours', 'tokens', 'defensive'];\nconst REPORT_FILE_NAMES = {\n migration: 'migration-impact-report.md',\n colours: 'colour-remediation-report.md',\n tokens: 'style-css-token-audit.md',\n defensive: 'defensive-rendering-remediation-report.md'\n};\nconst IMPORTANT_RE = /!important\\b/;\nconst MDX_IMPORT_RE = /^\\s*import\\s+[\\s\\S]*?\\s+from\\s+['\"][^'\"]+['\"]\\s*;?/gm;\nconst ARRAY_METHODS = ['map', 'filter', 'slice', 'forEach', 'reduce', 'some', 'every', 'sort'];\nconst COLOR_DISTANCE_THRESHOLD = 34;\nconst LEGACY_TOKEN_EQUIVALENTS = {\n '--accent': '--lp-color-accent',\n '--accent-dark': '--lp-color-accent-dark',\n '--hero-text': '--lp-color-hero-text',\n '--text': '--lp-color-text',\n '--muted-text': '--lp-color-muted-text',\n '--background': '--lp-color-background',\n '--card-background': '--lp-color-card-background',\n '--border': '--lp-color-border',\n '--button-text': '--lp-color-button-text',\n '--background-highlight': '--lp-color-background-highlight'\n};\nconst HIGH_RISK_OVERRIDES = {\n BlinkingIcon: {\n issue:\n 'Props are passed straight through to `Icon` without runtime normalisation. Invalid `icon` or `size` values can surface as render failures in Mintlify.',\n snippetSearch: ' ',\n fix: `const resolvedIcon = typeof icon === \"string\" && icon.trim() ? icon : \"terminal\";\nconst resolvedSize = Number.isFinite(Number(size)) ? Number(size) : 16;\n\nreturn (\n \n \n \n);`,\n guards: [\n 'Normalise `icon` before rendering the nested `Icon`.',\n 'Coerce `size` to a finite number before passing it through.',\n 'Retain the existing default fallback values.'\n ]\n },\n BlogDataLayout: {\n issue: 'The component assumes `items` is always an array. Non-array inputs can break `slice()` and `map()` during render.',\n snippetSearch: 'const displayItems = limit ? items.slice(0, limit) : items;',\n fix: `const safeItems = Array.isArray(items) ? items : [];\nconst displayItems = limit ? safeItems.slice(0, limit) : safeItems;\n\nif (displayItems.length === 0) {\n return null;\n}\n\nreturn (\n \n {displayItems.map((props, idx) => (\n \n ))}\n \n);`,\n guards: [\n 'Normalise `items` to an array before any collection operations.',\n 'Use optional chaining for key selection on mapped items.',\n 'Return a stable empty-state fallback when no valid items remain.'\n ]\n },\n CardInCardLayout: {\n issue:\n 'The component ignores its `items` prop and renders `forumData`, which is not defined in the file. That is a hard runtime failure.',\n snippetSearch: ' ',\n fix: `const safeItems = Array.isArray(items) ? items : [];\n\nif (safeItems.length === 0) {\n console.warn(\"[CardInCardLayout] Missing or invalid items\");\n return null;\n}\n\nreturn (\n \n \n \n);`,\n guards: [\n 'Use the actual `items` prop instead of `forumData`.',\n 'Guard non-array `items` before delegating to `CardColumnsPostLayout`.',\n 'Warn and return `null` when no usable dataset is available.'\n ]\n },\n CoinGeckoExchanges: {\n issue:\n '`sortedExchanges.map()` and several fetch-derived values assume a healthy API response shape. Invalid or partial responses can still break rendering after the fetch resolves.',\n snippetSearch: '{sortedExchanges.map((exchange, index) => (',\n fix: `const safeExchanges = Array.isArray(sortedExchanges) ? sortedExchanges : [];\n\nif (safeExchanges.length === 0) {\n return No exchanges found for this coin.;\n}\n\nreturn (\n \n {safeExchanges.map((exchange, index) => (\n \n {/* existing row markup */}\n \n ))}\n \n);`,\n guards: [\n 'Normalise `sortedExchanges` before mapping.',\n 'Use stable optional chaining when reading `exchange` fields.',\n 'Keep the existing loading/error/empty branches intact.'\n ]\n },\n ColumnsBlogCardLayout: {\n issue: 'The component assumes `items` is always array-shaped before calling `slice()` and `map()`.',\n snippetSearch: 'const displayItems = limit ? items.slice(0, limit) : items;',\n fix: `const safeItems = Array.isArray(items) ? items : [];\nconst displayItems = limit ? safeItems.slice(0, limit) : safeItems;\n\nreturn (\n \n {displayItems.map((props, idx) => (\n \n ))}\n \n);`,\n guards: [\n 'Normalise `items` before collection work.',\n 'Use optional chaining when deriving row keys.',\n 'Preserve current layout when data is valid.'\n ]\n },\n LinkArrow: {\n issue:\n 'Required `href` and `label` props are rendered without any validation. Missing values can create broken anchors or crash downstream consumers.',\n snippetSearch: '',\n fix: `if (!href || !label) {\n console.warn(\"[LinkArrow] Missing required props: href and label\");\n return null;\n}\n\nreturn (\n <>\n {newline &&
}\n \n \n {label}\n \n \n \n {description && description}\n >\n);`,\n guards: [\n 'Return early when `href` or `label` is missing.',\n 'Add `rel` to the external link branch while touching the anchor.',\n 'Keep description rendering unchanged for valid inputs.'\n ]\n },\n PostCard: {\n issue:\n '`content` is treated as HTML and its length is read before any runtime type guard. Non-string content can break both the length check and `dangerouslySetInnerHTML`.',\n snippetSearch: 'dangerouslySetInnerHTML={{ __html: content }}',\n fix: `const contentHtml = typeof content === \"string\" ? content : \"\";\nconst showScrollHint = contentHtml.length > 500;",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "report:component-governance-remediation"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: report:component-governance-remediation)",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/new-script.js",
+ "script": "new-script",
+ "category": "generator",
+ "purpose": "qa:repo-health",
+ "scope": "tools/scripts, tests/unit/script-docs.test.js",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Script scaffolder — creates a new script file prefilled with the required docs header template",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/new-script.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script new-script\n * @category generator\n * @purpose qa:repo-health\n * @scope tools/scripts, tests/unit/script-docs.test.js\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Script scaffolder — creates a new script file prefilled with the required docs header template\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/new-script.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst args = process.argv.slice(2);\nconst ROOT = process.cwd();\n\nfunction argValue(name) {\n const idx = args.indexOf(name);\n if (idx === -1) return '';\n return args[idx + 1] || '';\n}\n\nfunction usage() {\n console.log('Usage: node tools/scripts/new-script.js --path [--owner ] [--summary ] [--scope ]');\n}\n\nfunction usageDefault(filePath) {\n if (filePath.endsWith('.sh') || filePath.endsWith('.bash')) return `bash ${filePath}`;\n if (filePath.endsWith('.py')) return `python3 ${filePath}`;\n return `node ${filePath}`;\n}\n\nfunction hashTemplate(params) {\n const cmd = usageDefault(params.filePath);\n const lines = [\n '# @script ' + params.scriptName,\n '# @summary ' + params.summary,\n '# @owner ' + params.owner,\n '# @scope ' + params.scope,\n '#',\n '# @usage',\n '# ' + cmd,\n '#',\n '# @inputs',\n '# TODO: --flag (default: ...)',\n '#',\n '# @outputs',\n '# - TODO: output file/path/side effect',\n '#',\n '# @exit-codes',\n '# 0 = success',\n '# 1 = failure',\n '#',\n '# @examples',\n '# ' + cmd,\n '#',\n '# @notes',\n '# TODO: caveats, constraints, safety notes',\n ''\n ];\n return lines.join('\\n');\n}\n\nfunction blockTemplate(params) {\n const cmd = usageDefault(params.filePath);\n const lines = [\n '/**',\n ` * @script ${params.scriptName}`,\n ` * @summary ${params.summary}`,\n ` * @owner ${params.owner}`,\n ` * @scope ${params.scope}`,\n ' *',\n ' * @usage',\n ` * ${cmd}`,\n ' *',\n ' * @inputs',\n ' * TODO: --flag (default: ...)',\n ' *',\n ' * @outputs',\n ' * - TODO: output file/path/side effect',\n ' *',\n ' * @exit-codes',\n ' * 0 = success',\n ' * 1 = failure',\n ' *',\n ' * @examples',\n ` * ${cmd}`,\n ' *',\n ' * @notes',\n ' * TODO: caveats, constraints, safety notes',\n ' */',\n ''\n ];\n return lines.join('\\n');\n}\n\nfunction createContent(filePath, owner, summary, scope) {\n const ext = path.extname(filePath).toLowerCase();\n const scriptName = path.basename(filePath, ext);\n const params = { filePath, owner, summary, scope, scriptName };\n const hashStyle = ext === '.sh' || ext === '.bash' || ext === '.py';\n\n let shebang = '';\n if (ext === '.sh' || ext === '.bash') shebang = '#!/usr/bin/env bash\\n';\n if (ext === '.py') shebang = '#!/usr/bin/env python3\\n';\n if (ext === '.js' || ext === '.mjs' || ext === '.cjs') shebang = '#!/usr/bin/env node\\n';\n\n const template = hashStyle ? hashTemplate(params) : blockTemplate(params);\n return `${shebang}${template}`;\n}\n\nfunction main() {\n const filePath = argValue('--path');\n if (!filePath) {\n usage();\n process.exit(1);\n }\n\n const normalized = filePath.split(path.sep).join('/');\n const fullPath = path.join(ROOT, normalized);\n if (fs.existsSync(fullPath)) {\n console.error(`❌ File already exists: ${normalized}`);\n process.exit(1);\n }\n\n const owner = argValue('--owner') || 'docs';\n const summary = argValue('--summary') || 'TODO: one-line purpose';\n const scope = argValue('--scope') || path.dirname(normalized);\n const content = createContent(normalized, owner, summary, scope);\n\n fs.mkdirSync(path.dirname(fullPath), { recursive: true });\n fs.writeFileSync(fullPath, content, 'utf8');\n\n console.log(`✅ Created ${normalized}`);\n console.log('Fill all TODO values before committing.');\n}\n\nmain();\n\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/publish-v2-internal-reports.js",
+ "script": "publish-v2-internal-reports",
+ "category": "automation",
+ "purpose": "qa:repo-health",
+ "scope": "tools/scripts, tools/config, v2/internal, docs.json, tasks/reports, tests/reports",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Report publisher — publishes v2 internal audit reports to configured output locations",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/publish-v2-internal-reports.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script publish-v2-internal-reports\n * @category automation\n * @purpose qa:repo-health\n * @scope tools/scripts, tools/config, v2/internal, docs.json, tasks/reports, tests/reports\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Report publisher — publishes v2 internal audit reports to configured output locations\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/publish-v2-internal-reports.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst manifest = require('../config/v2-internal-report-pages');\n\nconst REPO_ROOT = path.resolve(__dirname, '..', '..');\nconst DOCS_JSON_PATH = path.join(REPO_ROOT, 'docs.json');\nconst INTERNAL_REPORTS_ROOT = path.join(REPO_ROOT, 'v2', 'internal', 'reports');\nconst GENERATED_OG_IMAGE = '/snippets/assets/domain/SHARED/LivepeerDocsLogo.svg';\nconst UTC_MONTHS = [\n 'January',\n 'February',\n 'March',\n 'April',\n 'May',\n 'June',\n 'July',\n 'August',\n 'September',\n 'October',\n 'November',\n 'December',\n];\n\nfunction usage() {\n console.log(\n 'Usage: node tools/scripts/publish-v2-internal-reports.js [--check] [--strict] [--category ]'\n );\n}\n\nfunction parseArgs(argv) {\n const out = {\n check: false,\n strict: false,\n categories: null,\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--check') {\n out.check = true;\n continue;\n }\n if (token === '--strict') {\n out.strict = true;\n continue;\n }\n if (token === '--category') {\n const raw = String(argv[i + 1] || '').trim();\n if (!raw) {\n throw new Error('Missing --category value');\n }\n out.categories = new Set(\n raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n );\n i += 1;\n continue;\n }\n if (token.startsWith('--category=')) {\n const raw = token.slice('--category='.length).trim();\n out.categories = new Set(\n raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n );\n continue;\n }\n if (token === '--help' || token === '-h') {\n out.help = true;\n continue;\n }\n throw new Error(`Unknown arg: ${token}`);\n }\n\n return out;\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction toPosix(filePath) {\n return filePath.split(path.sep).join('/');\n}\n\nfunction toRepoPath(absPath) {\n return toPosix(path.relative(REPO_ROOT, absPath));\n}\n\nfunction stripExtension(repoPath) {\n return repoPath.replace(/\\.(md|mdx)$/i, '');\n}\n\nfunction escapeSingleQuotedYaml(value) {\n return String(value).replace(/'/g, \"''\");\n}\n\nfunction stripFrontmatter(content) {\n if (!content.startsWith('---\\n')) return content;\n const end = content.indexOf('\\n---\\n', 4);\n if (end === -1) return content;\n return content.slice(end + 5);\n}\n\nfunction slugify(value) {\n return String(value)\n .toLowerCase()\n .replace(/\\.(md|mdx)$/i, '')\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\nfunction titleFromBasename(baseName) {\n const withoutExt = baseName.replace(/\\.(md|mdx)$/i, '');\n return withoutExt\n .split(/[-_]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1))\n .join(' ');\n}\n\nfunction titleFromScope(scopeValue) {\n const rawScope = String(scopeValue || '').replace(/`/g, '').trim();\n const scopePath = rawScope.split('(')[0].trim();\n let pathSegments = scopePath.split('/').map((segment) => segment.trim()).filter(Boolean);\n if (pathSegments[0] === 'tasks') pathSegments = pathSegments.slice(1);\n if (pathSegments[0] === 'reports') pathSegments = pathSegments.slice(1);\n\n const source = pathSegments.length ? pathSegments.join('/') : scopePath;\n const words = source\n .split(/[^a-zA-Z0-9]+/)\n .filter(Boolean)\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1));\n if (!words.length) return '';\n const hasAuditWord = words.some((word) => /^audit(s)?$/i.test(word));\n if (!hasAuditWord) words.push('Audit');\n return words.join(' ');\n}\n\nfunction parseScopeFromReportBody(body) {\n const match = String(body || '').match(/^Scope:\\s*`?([^`\\n]+?)`?\\s*$/m);\n return match ? match[1].trim() : '';\n}\n\nfunction normalizeScriptHeaderLine(rawLine) {\n let line = String(rawLine || '').trim();\n if (!line) return '';\n line = line.replace(/^\\/\\*\\*?/, '').replace(/\\*\\/$/, '').trim();\n line = line.replace(/^\\*\\s?/, '').replace(/^#\\s?/, '').trim();\n return line;\n}\n\nfunction parseScriptHeaderMetadata(scriptRepoPath) {\n if (!scriptRepoPath) {\n return { summary: '', scope: '', outputs: [] };\n }\n const scriptAbsPath = path.join(REPO_ROOT, ...scriptRepoPath.split('/'));\n if (!fs.existsSync(scriptAbsPath)) {\n return { summary: '', scope: '', outputs: [] };\n }\n\n const lines = fs.readFileSync(scriptAbsPath, 'utf8').split('\\n');\n const metadata = { summary: '', scope: '', outputs: [] };\n let inOutputs = false;\n\n for (const rawLine of lines.slice(0, 280)) {\n const line = normalizeScriptHeaderLine(rawLine);\n if (!line) continue;\n\n if (line.startsWith('@summary')) {\n metadata.summary = line.replace(/^@summary\\s*:?\\s*/, '').trim();\n inOutputs = false;\n continue;\n }\n if (line.startsWith('@scope')) {\n metadata.scope = line.replace(/^@scope\\s*:?\\s*/, '').trim();\n inOutputs = false;\n continue;\n }\n if (line.startsWith('@outputs')) {\n inOutputs = true;\n continue;\n }\n if (line.startsWith('@')) {\n inOutputs = false;\n continue;\n }\n if (inOutputs && line.startsWith('- ')) {\n metadata.outputs.push(line.slice(2).trim());\n }\n }\n\n return metadata;\n}\n\nfunction isGenericSummary(summary) {\n return /^(utility script for\\b|general task script\\b)/i.test(String(summary || '').trim());\n}\n\nfunction buildScriptContextBlock(record, body, scriptMetadata) {\n const rawSummary = (scriptMetadata.summary || '').trim();\n const summary = rawSummary && !isGenericSummary(rawSummary) ? rawSummary : record.description;\n const reportScope = parseScopeFromReportBody(body);",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/reports",
+ "type": "directory",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/scripts/docs.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/reports, tools/scripts/docs.json",
+ "downstream_consumers": [
+ "tools/scripts/generate-component-governance-remediation-reports.js"
+ ],
+ "downstream_display": "Yes: tools/scripts/generate-component-governance-remediation-reports.js",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/script-footprint-and-usage-audit.js",
+ "script": "script-footprint-and-usage-audit",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tools/scripts, tests, tasks/reports, ai-tools/ai-skills",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Script footprint auditor — analyses script file sizes, dependencies, and usage patterns across the repo",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/script-footprint-and-usage-audit.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script script-footprint-and-usage-audit\n * @category validator\n * @purpose qa:repo-health\n * @scope tools/scripts, tests, tasks/reports, ai-tools/ai-skills\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Script footprint auditor — analyses script file sizes, dependencies, and usage patterns across the repo\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/script-footprint-and-usage-audit.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\n\nconst STAGE_ID = 'script-footprint-and-usage-audit';\nconst REPO_ROOT = process.cwd();\nconst DEFAULT_OUTPUT_DIR = 'tasks/reports/repo-ops';\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction parseArgs(argv) {\n const out = { scope: 'full', outputDir: DEFAULT_OUTPUT_DIR };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--scope') {\n out.scope = String(argv[i + 1] || out.scope).trim();\n i += 1;\n continue;\n }\n if (token.startsWith('--scope=')) {\n out.scope = token.slice('--scope='.length).trim() || out.scope;\n continue;\n }\n if (token === '--output-dir') {\n out.outputDir = String(argv[i + 1] || out.outputDir).trim() || out.outputDir;\n i += 1;\n continue;\n }\n if (token.startsWith('--output-dir=')) {\n out.outputDir = token.slice('--output-dir='.length).trim() || out.outputDir;\n continue;\n }\n throw new Error(`Unknown argument: ${token}`);\n }\n\n if (!['changed', 'full'].includes(out.scope)) {\n throw new Error(`Invalid --scope: ${out.scope}`);\n }\n\n return out;\n}\n\nfunction walkFiles(dirPath, out = []) {\n if (!fs.existsSync(dirPath)) return out;\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = path.join(dirPath, entry.name);\n const rel = toPosix(path.relative(REPO_ROOT, fullPath));\n\n if (entry.isDirectory()) {\n if (entry.name === '.git' || entry.name === 'node_modules') continue;\n walkFiles(fullPath, out);\n continue;\n }\n\n out.push({ absPath: fullPath, relPath: rel });\n }\n return out;\n}\n\nfunction readText(repoPath) {\n try {\n return fs.readFileSync(path.join(REPO_ROOT, repoPath), 'utf8');\n } catch (_error) {\n return '';\n }\n}\n\nfunction addIssue(issues, issue) {\n issues.push({\n id: issue.id,\n title: issue.title,\n severity: issue.severity,\n evidence: issue.evidence,\n recommendation: issue.recommendation,\n path: issue.path || '',\n line: Number.isFinite(Number(issue.line)) ? Number(issue.line) : 1\n });\n}\n\nfunction summarize(issues) {\n const summary = {\n critical: 0,\n high: 0,\n medium: 0,\n low: 0,\n info: 0,\n total: issues.length\n };\n\n for (const issue of issues) {\n if (Object.prototype.hasOwnProperty.call(summary, issue.severity)) {\n summary[issue.severity] += 1;\n }\n }\n\n return summary;\n}\n\nfunction markdownTableRows(issues, maxRows = 250) {\n return issues.slice(0, maxRows).map((issue) => {\n const safe = (value) => String(value || '').replace(/\\|/g, '\\\\|').replace(/\\n/g, ' ');\n return `| ${safe(issue.severity)} | ${safe(issue.title)} | ${safe(issue.path)} | ${safe(issue.evidence)} | ${safe(issue.recommendation)} |`;\n });\n}\n\nfunction buildMarkdown(report) {\n const lines = [];\n lines.push('# Script Footprint and Usage Audit');\n lines.push('');\n lines.push(`- Generated: ${report.generated_at}`);\n lines.push(`- Scope: ${report.scope}`);\n lines.push(`- Stage ID: ${report.stage_id}`);\n lines.push('');\n lines.push('## Severity Summary');\n lines.push('');\n lines.push(`- Critical: ${report.summary.critical}`);\n lines.push(`- High: ${report.summary.high}`);\n lines.push(`- Medium: ${report.summary.medium}`);\n lines.push(`- Low: ${report.summary.low}`);\n lines.push(`- Info: ${report.summary.info}`);\n lines.push(`- Total: ${report.summary.total}`);\n lines.push('');\n lines.push('## Issues');\n lines.push('');\n lines.push('| Severity | Title | Path | Evidence | Recommendation |');\n lines.push('|---|---|---|---|---|');\n lines.push(...markdownTableRows(report.issues));\n lines.push('');\n return `${lines.join('\\n')}\\n`;\n}\n\nfunction detectBackupArtifacts(issues, files) {\n files\n .filter((file) => /\\.bak2?$/.test(file.relPath))\n .forEach((file) => {\n const isScriptPath = file.relPath.startsWith('tools/scripts/') || file.relPath.startsWith('tests/');\n addIssue(issues, {\n id: 'backup-artifact',\n title: 'Backup artifact tracked in repo',\n severity: isScriptPath ? 'high' : 'medium',\n path: file.relPath,\n evidence: 'File name ends with .bak or .bak2',\n recommendation: 'Classify with cleanup-quarantine-manager and quarantine from active tree.'\n });\n });\n}\n\nfunction detectPlaceholderScripts(issues) {\n const targets = [\n 'tools/scripts/archive/fixtures/allowed.js',\n 'tools/scripts/archive/fixtures/allowed-script.js',\n 'tools/scripts/archive/fixtures/allowed-test.js'\n ];\n\n targets.forEach((target) => {\n if (!fs.existsSync(path.join(REPO_ROOT, target))) return;\n const text = readText(target);\n const stripped = text\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n .replace(/\\/\\/.*$/gm, '')\n .trim();\n\n if (/^test\\s*$/.test(stripped)) {\n addIssue(issues, {\n id: 'placeholder-script',\n title: 'Placeholder script is discoverable as runnable',\n severity: 'critical',\n path: target,\n evidence: 'Script body is only `test` and fails when executed.',\n recommendation: 'Quarantine or replace with explicit fixture guard that exits 0 with clear message.'\n });\n }\n });\n}\n\nfunction detectDuplicatePairs(issues, files) {\n const rootScripts = files\n .filter((file) => file.relPath.startsWith('tools/scripts/'))\n .filter((file) => /\\.js$/.test(file.relPath))\n .filter((file) => !file.relPath.startsWith('tools/scripts/test/'));\n\n for (const root of rootScripts) {\n const base = path.basename(root.relPath);\n const candidate = `tools/scripts/test/${base}`;\n const candidateAbs = path.join(REPO_ROOT, candidate);\n if (!fs.existsSync(candidateAbs)) continue;\n\n const rootContent = readText(root.relPath);\n const testContent = readText(candidate);\n\n const normalized = (value) =>\n String(value)\n .replace(/tools\\/scripts\\/test\\//g, 'tools/scripts/')\n .replace(/tools\\/scripts\\//g, 'tools/scripts/')\n .trim();\n\n if (normalized(rootContent) === normalized(testContent)) {\n addIssue(issues, {\n id: 'duplicate-script-pair',\n title: 'Duplicate root/test script pair detected',",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/${STAGE_ID}.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/scripts/${STAGE_ID}.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/${STAGE_ID}.json, tools/scripts/${STAGE_ID}.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/verify-all-pages.js",
+ "script": "verify-all-pages",
+ "category": "enforcer",
+ "purpose": "qa:repo-health",
+ "scope": "single-domain",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Loads component-library routes in a headless browser and fails on render, console, or 404 issues.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/scripts/verify-all-pages.js",
+ "header": "/**\n * @script verify-all-pages\n * @category enforcer\n * @purpose qa:repo-health\n * @summary Utility script for tools/scripts/verify-all-pages.js.\n * @owner docs\n * @scope single-domain\n * @needs E-C1, R-R14\n * @purpose-statement Loads component-library routes in a headless browser and fails on render, console, or 404 issues.\n * @pipeline manual\n * @usage node tools/scripts/verify-all-pages.js\n *\n * @inputs\n * No required CLI flags; optional flags are documented inline.\n *\n * @outputs\n * - Console output and/or file updates based on script purpose.\n *\n * @exit-codes\n * 0 = success\n * 1 = runtime or validation failure\n *\n * @examples\n * node tools/scripts/verify-all-pages.js\n *\n * @notes\n * Keep script behavior deterministic and update script indexes after changes.\n */\nconst puppeteer = require('puppeteer');\n\nconst BASE_URL = 'http://localhost:3333';\nconst PAGES = [\n {\n paths: [\n '/v2/resources/documentation-guide/component-library/component-library',\n '/v2/resources/documentation-guide/component-library/overview'\n ],\n name: 'Component Library'\n },\n {\n paths: [\n '/v2/resources/documentation-guide/component-library/primitives',\n '/v2/resources/documentation-guide/component-library/primitives'\n ],\n name: 'Primitives'\n },\n {\n paths: [\n '/v2/resources/documentation-guide/component-library/content',\n '/v2/resources/documentation-guide/component-library/content'\n ],\n name: 'Content'\n },\n {\n paths: [\n '/v2/resources/documentation-guide/component-library/layout',\n '/v2/resources/documentation-guide/component-library/layout'\n ],\n name: 'Layout'\n },\n {\n paths: [\n '/v2/resources/documentation-guide/component-library/data',\n '/v2/resources/documentation-guide/component-library/data'\n ],\n name: 'Data'\n },\n {\n paths: [\n '/v2/resources/documentation-guide/component-library/page-structure',\n '/v2/resources/documentation-guide/component-library/page-structure'\n ],\n name: 'Page Structure'\n },\n];\n\nasync function verifyPage(paths, name) {\n const browser = await puppeteer.launch({\n headless: true,\n args: ['--no-sandbox', '--disable-setuid-sandbox']\n });\n \n const page = await browser.newPage();\n const errors = [];\n const warnings = [];\n \n page.on('console', msg => {\n const text = msg.text();\n if (msg.type() === 'error') {\n errors.push(text);\n } else if (msg.type() === 'warning') {\n warnings.push(text);\n }\n });\n \n page.on('pageerror', error => {\n errors.push(error.toString());\n });\n \n try {\n console.log(`\\n🔍 Verifying: ${name}`);\n const pathCandidates = Array.isArray(paths) ? paths : [paths];\n let lastErrors = [];\n\n for (const pathCandidate of pathCandidates) {\n const url = `${BASE_URL}${pathCandidate}`;\n console.log(` URL: ${url}`);\n\n const response = await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });\n await new Promise(resolve => setTimeout(resolve, 5000));\n\n const title = await page.title().catch(() => '');\n const bodyText = await page.evaluate(() => {\n const body = document.body;\n if (!body) return '';\n return body.innerText || '';\n }).catch(() => '');\n\n const hasH1 = await page.$('h1') !== null;\n const hasContent = bodyText.length > 500;\n const is404 = title.includes('Page Not Found') || bodyText.includes('Page Not Found');\n const httpStatus = response.status();\n\n const realErrors = errors.filter(err => {\n const lower = err.toLowerCase();\n return !lower.includes('require is not defined') &&\n !lower.includes('puppeteer') &&\n !lower.includes('fs has already been declared') &&\n !lower.includes('unexpected token \\'export\\'') &&\n !lower.includes('identifier \\'') &&\n !lower.includes('appendchild') &&\n !lower.includes('failed to execute') &&\n !lower.includes('403') &&\n !lower.includes('500') &&\n !lower.includes('favicon');\n });\n\n console.log(` HTTP Status: ${httpStatus}`);\n console.log(` Title: ${title.substring(0, 60)}`);\n console.log(` Body Text Length: ${bodyText.length} chars`);\n console.log(` Has H1: ${hasH1}`);\n console.log(` Has Content: ${hasContent}`);\n console.log(` Is 404: ${is404}`);\n console.log(` Real Errors: ${realErrors.length}`);\n\n if (is404 || !hasContent || !hasH1 || httpStatus >= 400) {\n lastErrors = realErrors;\n continue;\n }\n\n if (realErrors.length > 0) {\n console.log(` ⚠️ PAGE RENDERS BUT HAS ERRORS`);\n return { success: false, errors: realErrors, path: pathCandidate };\n }\n\n console.log(` ✅ PAGE RENDERS CORRECTLY`);\n return { success: true, errors: [], path: pathCandidate };\n }\n\n console.log(` ❌ PAGE NOT RENDERING`);\n return { success: false, errors: lastErrors };\n \n } catch (error) {\n console.log(` ❌ ERROR: ${error.message}`);\n return { success: false, errors: [error.message] };\n } finally {\n await page.close();\n await browser.close();\n }\n}\n\nasync function main() {\n console.log('🔍 Verifying ALL component library pages in browser...\\n');\n \n const results = [];\n for (const pageSpec of PAGES) {\n const result = await verifyPage(pageSpec.paths, pageSpec.name);\n results.push({ ...pageSpec, ...result });\n }\n \n console.log('\\n📊 Summary:');\n const passed = results.filter(r => r.success).length;\n const failed = results.filter(r => !r.success).length;\n console.log(` ✅ Passed: ${passed}/${PAGES.length}`);\n console.log(` ❌ Failed: ${failed}/${PAGES.length}`);\n \n if (failed > 0) {\n console.log('\\n❌ Failed Pages:');\n results.filter(r => !r.success).forEach(r => {\n console.log(` - ${r.name}: ${r.path || r.paths.join(' | ')}`);\n if (r.errors.length > 0) {\n console.log(` Errors: ${r.errors.length}`);\n r.errors.slice(0, 2).forEach(err => {\n console.log(` - ${err.substring(0, 150)}`);\n });\n }\n });\n process.exit(1);\n } else {\n console.log('\\n✅ ALL PAGES RENDER CORRECTLY!');\n process.exit(0);\n }\n}\n\nmain().catch(err => {\n console.error('Fatal error:', err);\n process.exit(1);\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/verify-pay-orc-gate-finalize.sh",
+ "script": "verify-pay-orc-gate-finalize",
+ "category": "enforcer",
+ "purpose": "qa:repo-health",
+ "scope": "tools/scripts, .githooks/pre-commit, v2/gateways, v2/orchestrators",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Payment/orchestrator gate verifier — checks payment and orchestrator documentation gate conditions",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "bash tools/scripts/verify-pay-orc-gate-finalize.sh [flags]",
+ "header": "#!/usr/bin/env bash\n# @script verify-pay-orc-gate-finalize\n# @category enforcer\n# @purpose qa:repo-health\n# @scope tools/scripts, .githooks/pre-commit, v2/gateways, v2/orchestrators\n# @owner docs\n# @needs E-C1, R-R14\n# @purpose-statement Payment/orchestrator gate verifier — checks payment and orchestrator documentation gate conditions\n# @pipeline manual — diagnostic/investigation tool, run on-demand only\n# @usage bash tools/scripts/verify-pay-orc-gate-finalize.sh [flags]\nset -euo pipefail\n\nquiet=0\nprint_targets=0\n\nwhile [[ $# -gt 0 ]]; do\n case \"$1\" in\n --quiet)\n quiet=1\n shift\n ;;\n --print-targets)\n print_targets=1\n shift\n ;;\n *)\n echo \"Unknown argument: $1\" >&2\n exit 1\n ;;\n esac\ndone\n\nrepo_root=\"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\"\ncd \"$repo_root\"\n\n# Exact 12 deliverable targets from the insertion plan.\ntarget_files=(\n \"v2/gateways/payments/how-payments-work.mdx\"\n \"v2/gateways/payments/remote-signers.mdx\"\n \"v2/gateways/payments/payment-clearinghouse.mdx\"\n \"v2/gateways/payments/naap-platform.mdx\"\n \"v2/gateways/using-gateways/gateway-providers/cloud-spe-gateway.mdx\"\n \"v2/gateways/using-gateways/gateway-providers/daydream-gateway.mdx\"\n \"v2/gateways/using-gateways/gateway-providers/livepeer-studio-gateway.mdx\"\n \"v2/orchestrators/about-orchestrators/job-types.mdx\"\n \"v2/orchestrators/about-orchestrators/naap-platform.mdx\"\n \"v2/orchestrators/quickstart/realtime-ai.mdx\"\n \"v2/orchestrators/quickstart/batch-ai.mdx\"\n \"v2/orchestrators/quickstart/overview.mdx\"\n)\n\n# Required migration outcomes from implementation steps.\nmigration_required_files=(\n \"v2/orchestrators/quickstart/transcoding.mdx\"\n)\n\n# Legacy files that must not exist after migration.\nlegacy_blocked_files=(\n \"v2/gateways/run-a-gateway/payments/payment-clearinghouse.mdx\"\n \"v2/orchestrators/quickstart/orchestrator-setup.mdx\"\n \"v2/orchestrators/quickstart/realtime-ai-quickstart.mdx\"\n \"v2/orchestrators/quickstart/batch-ai-quickstart.mdx\"\n)\n\nif [[ \"$print_targets\" -eq 1 ]]; then\n echo \"Target deliverable files:\"\n printf ' %s\\n' \"${target_files[@]}\"\n echo \"Required migration files:\"\n printf ' %s\\n' \"${migration_required_files[@]}\"\n echo \"Legacy files that must be absent:\"\n printf ' %s\\n' \"${legacy_blocked_files[@]}\"\nfi\n\nmissing_targets=()\nmissing_migrations=()\nlegacy_present=()\n\nfor rel in \"${target_files[@]}\"; do\n [[ -f \"$rel\" ]] || missing_targets+=(\"$rel\")\ndone\n\nfor rel in \"${migration_required_files[@]}\"; do\n [[ -f \"$rel\" ]] || missing_migrations+=(\"$rel\")\ndone\n\nfor rel in \"${legacy_blocked_files[@]}\"; do\n [[ ! -e \"$rel\" ]] || legacy_present+=(\"$rel\")\ndone\n\nif [[ ${#missing_targets[@]} -eq 0 ]] \\\n && [[ ${#missing_migrations[@]} -eq 0 ]] \\\n && [[ ${#legacy_present[@]} -eq 0 ]]; then\n if [[ \"$quiet\" -eq 0 ]]; then\n echo \"Pay/Orc/Gate finalize check passed.\"\n echo \"Validated ${#target_files[@]} deliverable targets and migration guardrails.\"\n fi\n exit 0\nfi\n\nif [[ \"$quiet\" -eq 0 ]]; then\n echo \"Pay/Orc/Gate finalize check failed.\"\nfi\n\nfor rel in \"${missing_targets[@]}\"; do\n echo \"MISSING_TARGET $rel\"\ndone\nfor rel in \"${missing_migrations[@]}\"; do\n echo \"MISSING_MIGRATION $rel\"\ndone\nfor rel in \"${legacy_present[@]}\"; do\n echo \"LEGACY_STILL_PRESENT $rel\"\ndone\n\nexit 1\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/verify/.verify-large-change.sh",
+ "script": "verify-large-change",
+ "category": "enforcer",
+ "purpose": "qa:repo-health",
+ "scope": "tools/scripts/verify",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Large change verifier — blocks or warns when a commit touches an unusually large number of files",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "bash tools/scripts/verify/.verify-large-change.sh [flags]",
+ "header": "#!/usr/bin/env bash\n# @script verify-large-change\n# @category enforcer\n# @purpose qa:repo-health\n# @scope tools/scripts/verify\n# @owner docs\n# @needs E-C1, R-R14\n# @purpose-statement Large change verifier — blocks or warns when a commit touches an unusually large number of files\n# @pipeline manual — diagnostic/investigation tool, run on-demand only\n# @usage bash tools/scripts/verify/.verify-large-change.sh [flags]\necho \"verify-large-change placeholder: no-op\"\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/openapi-reference-audit.test.js",
+ "script": "openapi-reference-audit.test",
+ "category": "validator",
+ "purpose": "tooling:api-spec",
+ "scope": "tests/unit, tests/integration, v2, api",
+ "owner": "docs",
+ "needs": "F-R17",
+ "purpose_statement": "Unit tests for openapi-reference-audit.js — tests individual audit rules and fix logic",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/openapi-reference-audit.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script openapi-reference-audit.test\n * @category validator\n * @purpose tooling:api-spec\n * @scope tests/unit, tests/integration, v2, api\n * @owner docs\n * @needs F-R17\n * @purpose-statement Unit tests for openapi-reference-audit.js — tests individual audit rules and fix logic\n * @pipeline manual — not yet in pipeline\n * @dualmode --strict (enforcer) | --fix (remediator)\n * @usage node tests/unit/openapi-reference-audit.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst audit = require('../integration/openapi-reference-audit');\n\nlet errors = [];\nlet warnings = [];\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nconst REPO_ROOT = getRepoRoot();\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction mkFixture(relDir, filename, content) {\n const absDir = path.join(REPO_ROOT, relDir);\n fs.mkdirSync(absDir, { recursive: true });\n const absFile = path.join(absDir, filename);\n fs.writeFileSync(absFile, content, 'utf8');\n return {\n absFile,\n relFile: toPosix(path.relative(REPO_ROOT, absFile)),\n absDir\n };\n}\n\nfunction cleanupFixture(fixture) {\n if (!fixture) return;\n if (fs.existsSync(fixture.absFile)) {\n fs.unlinkSync(fixture.absFile);\n }\n try {\n fs.rmdirSync(fixture.absDir);\n } catch (_error) {\n // Ignore if directory has other files.\n }\n}\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` \u001b[32m✓\u001b[0m ${name}`);\n } catch (error) {\n errors.push({\n rule: 'openapi-reference-audit unit',\n message: `${name}: ${error.message}`,\n line: 1,\n file: 'tests/unit/openapi-reference-audit.test.js'\n });\n }\n}\n\nasync function runTests() {\n errors = [];\n warnings = [];\n\n console.log('🧪 OpenAPI Reference Audit Unit Tests');\n\n await runCase('Parses frontmatter METHOD /path references', async () => {\n const parsed = audit.parseMethodPathValue('post /stream');\n assert.ok(parsed);\n assert.strictEqual(parsed.key, 'POST /stream');\n });\n\n await runCase('Parses ', async () => {\n const parsed = audit.parseOpenApiTagReference(' ');\n assert.ok(parsed.valid);\n assert.strictEqual(parsed.endpoint.key, 'POST /task/{id}');\n });\n\n await runCase('Parses ', async () => {\n const parsed = audit.parseOpenApiTagReference(' ');\n assert.ok(parsed.valid);\n assert.strictEqual(parsed.endpoint.key, 'POST /task/{id}');\n });\n\n await runCase('Ignores non-endpoint frontmatter openapi values', async () => {\n assert.strictEqual(audit.isIgnoredFrontmatterOpenapiValue('3.0.3'), true);\n assert.strictEqual(audit.isIgnoredFrontmatterOpenapiValue('api/openapi.yaml'), true);\n assert.strictEqual(audit.isIgnoredFrontmatterOpenapiValue('GET /stream'), false);\n });\n\n await runCase('Resolves locale Studio API files to api/studio.yaml', async () => {\n const spec = audit.resolveSpecForFile('v2/es/solutions/livepeer-studio/api-reference/streams/create.mdx');\n assert.strictEqual(spec, 'api/studio.yaml');\n });\n\n await runCase('Flags unknown endpoint not found in resolved spec', async () => {\n const fixture = mkFixture(\n 'v2/es/solutions/livepeer-studio/api-reference/__openapi-audit-unit__',\n `unknown-${Date.now()}.mdx`,\n [\n '---',\n \"title: 'Unknown endpoint fixture'\",\n \"openapi: 'GET /definitely-not-real-endpoint'\",\n '---',\n '',\n '# Unknown',\n '',\n ' ',\n ''\n ].join('\\n')\n );\n\n try {\n const result = await audit.runAudit({\n argv: [\n '--files', fixture.relFile,\n '--strict',\n '--report', '/tmp/openapi-audit-unit-unknown.md',\n '--report-json', '/tmp/openapi-audit-unit-unknown.json'\n ]\n });\n\n assert.strictEqual(result.exitCode, 1);\n assert.ok(\n result.findings.some((finding) => finding.type === audit.FINDING_TYPES.ENDPOINT_NOT_FOUND)\n );\n } finally {\n cleanupFixture(fixture);\n }\n });\n\n await runCase('Flags frontmatter/component endpoint mismatch within same file', async () => {\n const fixture = mkFixture(\n 'v2/solutions/livepeer-studio/api-reference/__openapi-audit-unit__',\n `mismatch-${Date.now()}.mdx`,\n [\n '---',\n \"title: 'Mismatch fixture'\",\n \"openapi: 'GET /stream/{id}'\",\n '---',\n '',\n '# Mismatch',\n '',\n ' ',\n ''\n ].join('\\n')\n );\n\n try {\n const result = await audit.runAudit({\n argv: [\n '--files', fixture.relFile,\n '--strict',\n '--report', '/tmp/openapi-audit-unit-mismatch.md',\n '--report-json', '/tmp/openapi-audit-unit-mismatch.json'\n ]\n });\n\n assert.strictEqual(result.exitCode, 1);\n assert.ok(\n result.findings.some((finding) => finding.type === audit.FINDING_TYPES.INTRA_FILE_MISMATCH)\n );\n } finally {\n cleanupFixture(fixture);\n }\n });\n\n await runCase('Applies conservative autofix normalization with --fix --write', async () => {\n const fixture = mkFixture(\n 'v2/solutions/livepeer-studio/api-reference/__openapi-audit-unit__',\n `fix-${Date.now()}.mdx`,\n [\n '---',\n \"title: 'Fix fixture'\",\n \"openapi: 'post stream'\",\n '---',\n '',\n '# Fix',\n '',\n ' ',\n ' ',\n ''\n ].join('\\n')\n );\n\n try {\n const result = await audit.runAudit({\n argv: [\n '--files', fixture.relFile,\n '--fix',\n '--write',\n '--strict',\n '--report', '/tmp/openapi-audit-unit-fix.md',\n '--report-json', '/tmp/openapi-audit-unit-fix.json'\n ]\n });\n\n assert.strictEqual(result.exitCode, 0);\n const nextContent = fs.readFileSync(fixture.absFile, 'utf8');\n assert.ok(nextContent.includes(\"openapi: 'POST /stream'\"));\n assert.ok(nextContent.includes(' '));\n assert.ok(nextContent.includes(' '));\n } finally {\n cleanupFixture(fixture);\n }",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": ", ",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/openapi-rolling-issue.test.js",
+ "script": "openapi-rolling-issue.test",
+ "category": "validator",
+ "purpose": "tooling:api-spec",
+ "scope": "tests/unit, tests/utils, .github/workflows/openapi-reference-validation.yml",
+ "owner": "docs",
+ "needs": "F-R17",
+ "purpose_statement": "Tests OpenAPI rolling issue tracker — validates issue creation and dedup logic",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/openapi-rolling-issue.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script openapi-rolling-issue.test\n * @category validator\n * @purpose tooling:api-spec\n * @scope tests/unit, tests/utils, .github/workflows/openapi-reference-validation.yml\n * @owner docs\n * @needs F-R17\n * @purpose-statement Tests OpenAPI rolling issue tracker — validates issue creation and dedup logic\n * @pipeline manual — not yet in pipeline\n * @usage node tests/unit/openapi-rolling-issue.test.js [flags]\n */\n\nconst assert = require('assert');\nconst helpers = require('../utils/openapi-rolling-issue');\n\nlet errors = [];\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` \\u001b[32m✓\\u001b[0m ${name}`);\n } catch (error) {\n errors.push(`${name}: ${error.message}`);\n }\n}\n\nasync function runTests() {\n errors = [];\n\n console.log('🧪 OpenAPI Rolling Issue Unit Tests');\n\n await runCase('findMarkerIssue dedupes by marker and prefers open issue', async () => {\n const items = [\n { number: 101, state: 'closed', body: 'x x' },\n { number: 102, state: 'open', body: 'x x' },\n { number: 103, state: 'open', body: 'unrelated' }\n ];\n\n const result = helpers.findMarkerIssue(items);\n assert.ok(result);\n assert.strictEqual(result.number, 102);\n });\n\n await runCase('getIssueAction returns create vs update for non-zero failures', async () => {\n assert.strictEqual(\n helpers.getIssueAction({ existingIssue: null, totalFailures: 2 }),\n 'create'\n );\n\n assert.strictEqual(\n helpers.getIssueAction({ existingIssue: { number: 10, state: 'open' }, totalFailures: 2 }),\n 'update'\n );\n });\n\n await runCase('getIssueAction returns close when failures become zero and issue is open', async () => {\n assert.strictEqual(\n helpers.getIssueAction({ existingIssue: { number: 22, state: 'open' }, totalFailures: 0 }),\n 'close'\n );\n\n assert.strictEqual(\n helpers.getIssueAction({ existingIssue: null, totalFailures: 0 }),\n 'noop'\n );\n });\n\n await runCase('buildIssueBody includes required headings', async () => {\n const body = helpers.buildIssueBody({\n runUrl: 'https://example.test/run/1',\n topFindings: '- endpoint-not-found-in-spec: v2/foo.mdx:3 (GET /foo)',\n totalFailures: 1,\n totalFiles: 10,\n totalReferences: 20\n });\n\n const headings = [\n '### Area',\n '### Failing command or workflow',\n '### Script or workflow path',\n '### Full error output',\n '### Reproduction conditions',\n '### Expected behavior',\n '### Action requested from maintainers',\n '### Classification',\n '### Priority'\n ];\n\n headings.forEach((heading) => assert.ok(body.includes(heading), `missing ${heading}`));\n assert.ok(body.includes(helpers.ROLLING_ISSUE_MARKER));\n });\n\n await runCase('buildTopFindings enforces deterministic ordering and limit', async () => {\n const findings = [\n { type: 't', file: 'b.mdx', line: 9, reference: 'GET /b', resolvedSpec: 'api/studio.yaml' },\n { type: 't', file: 'a.mdx', line: 10, reference: 'GET /a/2', resolvedSpec: 'api/studio.yaml' },\n { type: 't', file: 'a.mdx', line: 2, reference: 'GET /a/1', resolvedSpec: 'api/studio.yaml' },\n { type: 't', file: 'c.mdx', line: 1, reference: 'GET /c', resolvedSpec: 'api/studio.yaml' }\n ];\n\n const text = helpers.buildTopFindings(findings, 3);\n const lines = text.split('\\n');\n assert.strictEqual(lines.length, 3);\n assert.ok(lines[0].includes('a.mdx:2'));\n assert.ok(lines[1].includes('a.mdx:10'));\n assert.ok(lines[2].includes('b.mdx:9'));\n });\n\n return {\n errors,\n passed: errors.length === 0,\n total: 5\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ OpenAPI rolling issue unit tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} OpenAPI rolling issue unit test failure(s)`);\n result.errors.forEach((msg) => console.error(` - ${msg}`));\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ OpenAPI rolling issue unit tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:openapi:issue"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:openapi:issue)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/snippets/fetch-openapi-specs.sh",
+ "script": "fetch-openapi-specs",
+ "category": "automation",
+ "purpose": "tooling:api-spec",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "F-R17",
+ "purpose_statement": "OpenAPI spec fetcher — pulls latest OpenAPI specs from Livepeer services for reference pages",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "bash tools/scripts/snippets/fetch-openapi-specs.sh [flags]",
+ "header": "#!/bin/bash\n# @script fetch-openapi-specs\n# @category automation\n# @purpose tooling:api-spec\n# @scope tools/scripts\n# @owner docs\n# @needs F-R17\n# @purpose-statement OpenAPI spec fetcher — pulls latest OpenAPI specs from Livepeer services for reference pages\n# @pipeline manual — interactive developer tool, not suited for automated pipelines\n# @usage bash tools/scripts/snippets/fetch-openapi-specs.sh [flags]\n# Pre-build script to fetch external OpenAPI specification files\n# Run this before building the docs to ensure API specs are up-to-date\n\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nCONFIG_FILE=\"$SCRIPT_DIR/paths.config.json\"\n\n# Try to detect repo root via git, fallback to config file\nif git rev-parse --show-toplevel &>/dev/null; then\n REPO_ROOT=\"$(git rev-parse --show-toplevel)\"\nelif [ -f \"$CONFIG_FILE\" ]; then\n echo \"Warning: Not in a git repo, using paths.config.json\"\n REPO_ROOT=\"$(dirname \"$(dirname \"$SCRIPT_DIR\")\")\"\nelse\n echo \"Error: Cannot determine repo root. Run from git repo or ensure paths.config.json exists.\"\n exit 1\nfi\n\n# Read path from config or use default\nif [ -f \"$CONFIG_FILE\" ] && command -v node &>/dev/null; then\n OPENAPI_DIR=\"$REPO_ROOT/$(node -pe \"require('$CONFIG_FILE').paths.aiWorkerApi\")\"\nelse\n OPENAPI_DIR=\"$REPO_ROOT/ai/worker/api\"\nfi\n\n# Create directory if it doesn't exist\nmkdir -p \"$OPENAPI_DIR\"\n\necho \"Fetching external OpenAPI specifications...\"\n\n# Fetch AI Runner OpenAPI spec (YAML) from livepeer/ai-runner\necho \" → Fetching livepeer/ai-runner openapi.yaml...\"\ncurl -sL \"https://raw.githubusercontent.com/livepeer/ai-runner/main/openapi.yaml\" \\\n -o \"$OPENAPI_DIR/openapi.yaml\"\n\n# Fetch AI Gateway OpenAPI spec (YAML) from livepeer/ai-runner\necho \" → Fetching livepeer/ai-runner gateway.openapi.yaml...\"\ncurl -sL \"https://raw.githubusercontent.com/livepeer/ai-runner/main/gateway.openapi.yaml\" \\\n -o \"$OPENAPI_DIR/gateway.openapi.yaml\"\n\n# Validate YAML files exist and have content\nfor file in \"$OPENAPI_DIR/openapi.yaml\" \"$OPENAPI_DIR/gateway.openapi.yaml\"; do\n if [ -s \"$file\" ]; then\n echo \" ✓ $(basename \"$file\") fetched ($(wc -c < \"$file\" | tr -d ' ') bytes)\"\n else\n echo \" ✗ Warning: $(basename \"$file\") is empty or missing\"\n fi\ndone\n\necho \"\"\necho \"✓ OpenAPI specs fetched successfully\"\necho \" Location: $OPENAPI_DIR\"\necho \"\"\necho \"Referenced specs:\"\nls -la \"$OPENAPI_DIR\"/*.yaml 2>/dev/null || echo \" No YAML files found\"\n\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": ".githooks/install.sh",
+ "script": "install",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": ".githooks",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Installs git hooks by setting core.hooksPath to .githooks/",
+ "pipeline_declared": "manual — developer tool",
+ "usage": "bash .githooks/install.sh [flags]",
+ "header": "#!/bin/bash\n# @script install\n# @category utility\n# @purpose tooling:dev-tools\n# @scope .githooks\n# @owner docs\n# @needs E-C6, F-C1\n# @purpose-statement Installs git hooks by setting core.hooksPath to .githooks/\n# @pipeline manual — developer tool\n# @usage bash .githooks/install.sh [flags]\n# Install git hooks\n\n# Support both regular repos and worktrees\nGIT_COMMON_DIR=$(git rev-parse --git-common-dir 2>/dev/null)\nif [ -z \"$GIT_COMMON_DIR\" ] || [ \"$GIT_COMMON_DIR\" = \"--git-common-dir\" ]; then\n GIT_COMMON_DIR=\".git\"\nfi\nHOOKS_DIR=\"$GIT_COMMON_DIR/hooks\"\nSOURCE_DIR=\".githooks\"\n\nif [ ! -d \"$HOOKS_DIR\" ]; then\n echo \"Error: hooks directory not found at $HOOKS_DIR\"\n exit 1\nfi\n\nif [ ! -d \"$SOURCE_DIR\" ]; then\n echo \"Error: .githooks directory not found. Are you in the repository root?\"\n exit 1\nfi\n\necho \"Installing git hooks...\"\n\n# Install pre-commit hook\nif [ -f \"$SOURCE_DIR/pre-commit\" ]; then\n cp \"$SOURCE_DIR/pre-commit\" \"$HOOKS_DIR/pre-commit\"\n chmod +x \"$HOOKS_DIR/pre-commit\"\n echo \"✓ Installed pre-commit hook\"\nelse\n echo \"✗ pre-commit hook not found in $SOURCE_DIR\"\nfi\n\n# Install pre-push hook\nif [ -f \"$SOURCE_DIR/pre-push\" ]; then\n cp \"$SOURCE_DIR/pre-push\" \"$HOOKS_DIR/pre-push\"\n chmod +x \"$HOOKS_DIR/pre-push\"\n echo \"✓ Installed pre-push hook\"\nelse\n echo \"✗ pre-push hook not found in $SOURCE_DIR\"\nfi\n\necho \"\"\necho \"Git hooks installed successfully!\"\necho \"\"\necho \"The pre-commit hook will now check for style guide violations.\"\necho \"The pre-push hook will enforce codex task contracts on codex/* branches.\"\necho \"See .githooks/README.md for details.\"\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": ".githooks/verify-browser.js",
+ "script": "verify-browser",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": ".githooks",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Verifies browser availability for Puppeteer-based integration tests",
+ "pipeline_declared": "manual — legacy browser validator invoked by .githooks/verify.sh when run directly",
+ "usage": "node .githooks/verify-browser.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script verify-browser\n * @category utility\n * @purpose tooling:dev-tools\n * @scope .githooks\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement Verifies browser availability for Puppeteer-based integration tests\n * @pipeline manual — legacy browser validator invoked by .githooks/verify.sh when run directly\n * @usage node .githooks/verify-browser.js [flags]\n */\n/**\n * Headless browser validation for staged MDX files\n * Tests that MDX files actually render in the browser without errors\n * \n * This script:\n * 1. Extracts staged MDX files\n * 2. Converts file paths to URLs\n * 3. Tests each page in headless browser\n * 4. Reports console errors, page errors, and render failures\n */\n\nconst { execSync } = require('child_process');\nconst path = require('path');\nconst fs = require('fs');\nconst { getStagedDocsPageFiles } = require('../tests/utils/file-walker');\n\n// Find puppeteer in tools/node_modules, tests/node_modules, or root node_modules\nlet puppeteer;\nconst possiblePaths = [\n path.join(__dirname, '..', 'tools', 'node_modules', 'puppeteer'),\n path.join(__dirname, '..', 'tests', 'node_modules', 'puppeteer'),\n path.join(__dirname, '..', 'node_modules', 'puppeteer')\n];\n\nfor (const puppeteerPath of possiblePaths) {\n if (fs.existsSync(puppeteerPath)) {\n puppeteer = require(puppeteerPath);\n break;\n }\n}\n\nif (!puppeteer) {\n console.error('❌ Puppeteer not found. Install dependencies: cd tools && npm install');\n process.exit(1);\n}\n\nconst { ensureServerRunning, stopServer, getServerUrl } = require('./server-manager');\n\n// Use server-manager's detected URL, or fall back to environment variable or default\n// getServerUrl() will return the actual port mint dev is using (may differ from 3145 if port is in use)\nconst BASE_URL = process.env.MINT_BASE_URL || 'http://localhost:3145';\nconst TIMEOUT = 15000; // 15 seconds per page (faster for pre-commit)\nconst MAX_PAGES = 10; // Limit to 10 pages for pre-commit speed\n\n/**\n * Get staged MDX files from git\n */\nfunction toPosix(filePath) {\n return String(filePath || '').split(path.sep).join('/');\n}\n\nfunction getStagedMdxFiles() {\n try {\n const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n const files = getStagedDocsPageFiles(repoRoot)\n .map((filePath) => toPosix(path.relative(repoRoot, filePath)))\n .filter((filePath) => filePath.endsWith('.mdx') && filePath.startsWith('v2/'))\n .slice(0, MAX_PAGES); // Limit for speed\n \n return files;\n } catch (error) {\n return [];\n }\n}\n\n/**\n * Build candidate URLs for a staged MDX file path.\n * Try legacy and current route patterns because local Mint dev routing\n * can differ from production path handling during migrations.\n */\nfunction filePathToUrls(filePath) {\n const withoutExt = filePath.replace(/\\.mdx$/, '');\n const routeWithoutPrefix = withoutExt\n .replace(/^v2\\/pages\\//, '')\n .replace(/^v2\\//, '');\n const normalizedRoute = routeWithoutPrefix.replace(/\\/index$/, '');\n\n const candidates = [\n `/${normalizedRoute}`,\n `/${withoutExt.replace(/^v2\\//, '').replace(/\\/index$/, '')}`,\n `/v2/${normalizedRoute}`,\n `/v2/pages/${normalizedRoute}`\n ];\n\n return [...new Set(candidates)];\n}\n\n/**\n * Test a single page in headless browser\n */\nasync function testPage(browser, filePath, baseUrl) {\n const candidateUrls = filePathToUrls(filePath);\n const page = await browser.newPage();\n \n const errors = [];\n const warnings = [];\n \n // Known false positives from test scripts and Mintlify build artifacts\n const isTestScriptArtifact = (message) => {\n const testArtifacts = [\n 'require is not defined',\n 'puppeteer',\n 'fs has already been declared',\n 'getMdxFiles',\n 'validateMdx',\n 'execSync',\n 'path',\n 'Unexpected token \\'export\\'',\n 'await is only valid',\n 'appendChild',\n 'Identifier \\'',\n 'has already been declared'\n ];\n return testArtifacts.some(artifact => message.toLowerCase().includes(artifact.toLowerCase()));\n };\n \n // Listen for console errors\n page.on('console', msg => {\n const type = msg.type();\n const text = msg.text();\n \n // Filter out common non-critical warnings\n const ignoredWarnings = [\n 'favicon',\n 'sourcemap',\n 'deprecated',\n 'experimental'\n ];\n \n if (type === 'error') {\n // Check if this is a known test script artifact\n if (isTestScriptArtifact(text)) {\n if (text.includes('require is not defined')) {\n warnings.push(`⚠️ ${text} (Likely cause: Mintlify build artifact - does not affect page functionality)`);\n } else {\n warnings.push(`⚠️ ${text} (Likely cause: Test script artifact - does not affect page functionality)`);\n }\n } else if (!text.includes('favicon') && !text.includes('sourcemap')) {\n errors.push(text);\n }\n } else if (type === 'warning' && !ignoredWarnings.some(ignored => text.toLowerCase().includes(ignored))) {\n warnings.push(text);\n }\n });\n \n // Listen for page errors\n page.on('pageerror', error => {\n const errorMessage = error.message;\n \n // Check if this is a known test script artifact\n if (isTestScriptArtifact(errorMessage)) {\n if (errorMessage.includes('require is not defined')) {\n warnings.push(`⚠️ Page Error: ${errorMessage} (Likely cause: Mintlify build artifact - does not affect page functionality)`);\n } else {\n warnings.push(`⚠️ Page Error: ${errorMessage} (Likely cause: Test script artifact - does not affect page functionality)`);\n }\n } else {\n errors.push(`Page Error: ${errorMessage}`);\n }\n });\n \n // Listen for request failures (but ignore some)\n page.on('requestfailed', request => {\n const failure = request.failure();\n const url = request.url();\n \n // Ignore favicon and other non-critical failures\n if (failure && !url.includes('favicon') && !url.includes('sourcemap')) {\n // Only report if it's a critical resource\n if (url.includes('/snippets/') || url.includes('/v2/')) {\n errors.push(`Request Failed: ${url} - ${failure.errorText}`);\n }\n }\n });\n \n const attempted = [];\n\n try {\n for (const candidateUrl of candidateUrls) {\n const fullUrl = `${baseUrl}${candidateUrl}`;\n attempted.push(fullUrl);\n errors.length = 0;\n warnings.length = 0;\n\n try {\n await page.goto(fullUrl, {\n waitUntil: 'networkidle2',\n timeout: TIMEOUT\n });\n\n await new Promise(resolve => setTimeout(resolve, 1000));\n\n const bodyText = await page.evaluate(() => document.body.innerText);\n if (!bodyText || bodyText.trim().length < 50) {\n errors.push('Page appears to be empty or failed to render');\n }\n\n const hasError = await page.evaluate(() => {\n return document.querySelector('[data-error-boundary]') !== null ||\n document.body.innerText.includes('Error:') ||\n document.body.innerText.includes('Failed to render');\n });\n\n if (hasError) {\n errors.push('Page contains render errors');\n }\n\n if (errors.length === 0) {",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": ".githooks/verify.sh",
+ "script": "verify",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": ".githooks",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Pre-commit sub-hook — verifies file-walker is available and runs structural checks on staged files",
+ "pipeline_declared": "manual — legacy pre-commit sub-hook retained for on-demand verification",
+ "usage": "bash .githooks/verify.sh [flags]",
+ "header": "#!/bin/bash\n# @script verify\n# @category utility\n# @purpose tooling:dev-tools\n# @scope .githooks\n# @owner docs\n# @needs E-C6, F-C1\n# @purpose-statement Pre-commit sub-hook — verifies file-walker is available and runs structural checks on staged files\n# @pipeline manual — legacy pre-commit sub-hook retained for on-demand verification\n# @usage bash .githooks/verify.sh [flags]\n# Verification script for pre-commit hook\n# Runs various validation checks on staged files\n\nset -e\n\nREPO_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\"\ncd \"$REPO_ROOT\"\n\nRED='\\033[0;31m'\nYELLOW='\\033[1;33m'\nGREEN='\\033[0;32m'\nNC='\\033[0m'\n\nVIOLATIONS=0\nWARNINGS=()\n\necho -e \"${YELLOW}🔍 Running verification checks...${NC}\"\n\n# Get staged files\nSTAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)\n\nget_staged_docs_pages() {\n if ! command -v node &>/dev/null || [ ! -f \"tests/utils/file-walker.js\" ]; then\n return 0\n fi\n\n node -e \"const path = require('path'); const { getStagedDocsPageFiles } = require('./tests/utils/file-walker'); const root = process.cwd(); const files = getStagedDocsPageFiles(root).map((filePath) => path.relative(root, filePath).split(path.sep).join('/')); process.stdout.write(files.join('\\n'));\"\n}\n\nSTAGED_DOCS_PAGES=$(get_staged_docs_pages)\n\nif [ -z \"$STAGED_FILES\" ]; then\n echo -e \"${GREEN}✓ No files staged${NC}\"\n exit 0\nfi\n\n# Check 1: MDX syntax validation (basic)\necho \"Checking MDX syntax...\"\nif [ -n \"$STAGED_DOCS_PAGES\" ]; then\n MDX_FILES=$(echo \"$STAGED_DOCS_PAGES\" | grep -E '\\.mdx$' || true)\nelse\n MDX_FILES=$(echo \"$STAGED_FILES\" | grep -E '\\.mdx$' || true)\nfi\nif [ -n \"$MDX_FILES\" ]; then\n for file in $MDX_FILES; do\n if [ -f \"$file\" ]; then\n # Basic check: ensure frontmatter is valid YAML\n if head -n 1 \"$file\" | grep -q \"^---[[:space:]]*$\"; then\n # Check if frontmatter closes properly\n FRONTMATTER_LINES=$(head -n 50 \"$file\" | grep -n \"^---[[:space:]]*$\" | head -2 | cut -d: -f1)\n if [ -z \"$FRONTMATTER_LINES\" ] || [ \"$(echo \"$FRONTMATTER_LINES\" | wc -l)\" -lt 2 ]; then\n WARNINGS+=(\"⚠️ $file: Frontmatter may be malformed\")\n VIOLATIONS=$((VIOLATIONS + 1))\n fi\n fi\n fi\n done\nfi\n\n# Check 2: JSON syntax validation\necho \"Checking JSON syntax...\"\nJSON_FILES=$(echo \"$STAGED_FILES\" | grep -E '\\.json$' || true)\nif [ -n \"$JSON_FILES\" ]; then\n for file in $JSON_FILES; do\n if [ -f \"$file\" ]; then\n if ! node -e \"JSON.parse(require('fs').readFileSync('$file'))\" 2>/dev/null; then\n WARNINGS+=(\"❌ $file: Invalid JSON syntax\")\n VIOLATIONS=$((VIOLATIONS + 1))\n fi\n fi\n done\nfi\n\n# Check 3: Shell script syntax\necho \"Checking shell script syntax...\"\nSH_FILES=$(echo \"$STAGED_FILES\" | grep -E '\\.sh$' || true)\nif [ -n \"$SH_FILES\" ]; then\n for file in $SH_FILES; do\n if [ -f \"$file\" ]; then\n if ! bash -n \"$file\" 2>/dev/null; then\n WARNINGS+=(\"❌ $file: Shell script syntax error\")\n VIOLATIONS=$((VIOLATIONS + 1))\n fi\n fi\n done\nfi\n\n# Check 4: JavaScript/JSX syntax (if node available)\nif command -v node &>/dev/null; then\n echo \"Checking JavaScript/JSX syntax...\"\n JS_FILES=$(echo \"$STAGED_FILES\" | grep -E '\\.(js|jsx)$' || true)\n if [ -n \"$JS_FILES\" ]; then\n for file in $JS_FILES; do\n if [ -f \"$file\" ]; then\n # Skip if it's a JSX file (node --check doesn't handle JSX well)\n if [[ \"$file\" == *.jsx ]]; then\n # Basic check: ensure file is readable\n if ! head -n 1 \"$file\" > /dev/null 2>&1; then\n WARNINGS+=(\"⚠️ $file: Cannot read file\")\n VIOLATIONS=$((VIOLATIONS + 1))\n fi\n else\n if ! node --check \"$file\" 2>/dev/null; then\n WARNINGS+=(\"❌ $file: JavaScript syntax error\")\n VIOLATIONS=$((VIOLATIONS + 1))\n fi\n fi\n fi\n done\n fi\nfi\n\n# Check 5: Mintlify config validation (if mintlify available)\nif command -v mintlify &>/dev/null; then\n echo \"Checking Mintlify configuration...\"\n if [ -f \"docs.json\" ] || [ -f \"mint.json\" ]; then\n CONFIG_FILE=\"docs.json\"\n [ -f \"mint.json\" ] && CONFIG_FILE=\"mint.json\"\n \n # Check if docs.json is valid JSON\n if ! node -e \"JSON.parse(require('fs').readFileSync('$CONFIG_FILE'))\" 2>/dev/null; then\n WARNINGS+=(\"❌ $CONFIG_FILE: Invalid JSON syntax\")\n VIOLATIONS=$((VIOLATIONS + 1))\n fi\n fi\nfi\n\n# Check 6: Import path validation (absolute paths for snippets)\necho \"Checking import paths...\"\nif [ -n \"$STAGED_DOCS_PAGES\" ]; then\n JSX_MDX_FILES=$(echo \"$STAGED_DOCS_PAGES\" | grep -E '\\.mdx$' | grep -v \"style-guide\" || true)\nelse\n JSX_MDX_FILES=$(echo \"$STAGED_FILES\" | grep -E '\\.(jsx|tsx|mdx)$' | grep -v \"style-guide\" || true)\nfi\nif [ -n \"$JSX_MDX_FILES\" ]; then\n for file in $JSX_MDX_FILES; do\n if [ -f \"$file\" ]; then\n # Skip style guide (it documents relative imports as examples of what NOT to do)\n if [[ \"$file\" == *\"style-guide\"* ]]; then\n continue\n fi\n # Check for snippets imports that aren't absolute\n if grep -E \"from ['\\\"].*snippets\" \"$file\" 2>/dev/null | grep -v \"from ['\\\"]/snippets\" > /dev/null; then\n WARNINGS+=(\"⚠️ $file: Snippets imports should be absolute (/snippets/...)\")\n VIOLATIONS=$((VIOLATIONS + 1))\n fi\n fi\n done\nfi\n\n# Check 7: Browser validation (if Node.js and Puppeteer available)\nif command -v node &>/dev/null; then\n # Check if puppeteer is available (tests/ first, then tools/, then legacy root node_modules)\n PUPPETEER_AVAILABLE=false\n if [ -f \"tests/node_modules/puppeteer/package.json\" ]; then\n PUPPETEER_AVAILABLE=true\n export NODE_PATH=\"$(pwd)/tests/node_modules:${NODE_PATH:-}\"\n elif [ -f \"tools/node_modules/puppeteer/package.json\" ]; then\n PUPPETEER_AVAILABLE=true\n export NODE_PATH=\"$(pwd)/tools/node_modules:${NODE_PATH:-}\"\n elif [ -f \"node_modules/puppeteer/package.json\" ]; then\n PUPPETEER_AVAILABLE=true\n elif [ -f \"tests/package.json\" ] && grep -q \"puppeteer\" tests/package.json; then\n PUPPETEER_AVAILABLE=true\n export NODE_PATH=\"$(pwd)/tests/node_modules:${NODE_PATH:-}\"\n elif [ -f \"tools/package.json\" ] && grep -q \"puppeteer\" tools/package.json; then\n PUPPETEER_AVAILABLE=true\n export NODE_PATH=\"$(pwd)/tools/node_modules:${NODE_PATH:-}\"\n elif [ -f \"package.json\" ] && grep -q \"puppeteer\" package.json; then\n PUPPETEER_AVAILABLE=true\n fi\n \n # If Puppeteer not available but package.json exists, try to install it\n if [ \"$PUPPETEER_AVAILABLE\" = false ]; then\n if [ -f \"tools/package.json\" ] && grep -q \"puppeteer\" tools/package.json; then\n echo -e \"${YELLOW}⚠️ Puppeteer not found, attempting to install dependencies...${NC}\"\n if cd tools && npm install --silent 2>&1; then\n cd \"$REPO_ROOT\"\n # Check again after install\n if [ -f \"tools/node_modules/puppeteer/package.json\" ]; then\n PUPPETEER_AVAILABLE=true\n export NODE_PATH=\"$(pwd)/tools/node_modules:${NODE_PATH:-}\"\n echo -e \"${GREEN}✓ Puppeteer installed successfully${NC}\"\n else\n echo -e \"${RED}❌ Puppeteer installation failed or incomplete${NC}\"\n fi\n else\n cd \"$REPO_ROOT\"\n echo -e \"${RED}❌ Failed to install dependencies${NC}\"\n fi\n elif [ -f \"tests/package.json\" ] && grep -q \"puppeteer\" tests/package.json; then\n echo -e \"${YELLOW}⚠️ Puppeteer not found, attempting to install dependencies...${NC}\"\n if cd tests && npm install --silent 2>&1; then\n cd \"$REPO_ROOT\"\n # Check again after install\n if [ -f \"tests/node_modules/puppeteer/package.json\" ]; then\n PUPPETEER_AVAILABLE=true\n export NODE_PATH=\"$(pwd)/tests/node_modules:${NODE_PATH:-}\"\n echo -e \"${GREEN}✓ Puppeteer installed successfully${NC}\"\n else\n echo -e \"${RED}❌ Puppeteer installation failed or incomplete${NC}\"\n fi\n else\n cd \"$REPO_ROOT\"\n echo -e \"${RED}❌ Failed to install dependencies${NC}\"\n fi\n fi\n fi\n \n if [ \"$PUPPETEER_AVAILABLE\" = true ] && [ -f \".githooks/verify-browser.js\" ]; then",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/lpd-scoped-mint-dev.test.js",
+ "script": "lpd-scoped-mint-dev.test",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": "tests/unit, lpd, tools/scripts/mint-dev.sh, tools/scripts/dev/generate-mint-dev-scope.js",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Tests lpd scoped mint-dev functionality — validates dev server scope filtering",
+ "pipeline_declared": "manual — developer tool",
+ "usage": "node tests/unit/lpd-scoped-mint-dev.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script lpd-scoped-mint-dev.test\n * @category utility\n * @purpose tooling:dev-tools\n * @scope tests/unit, lpd, tools/scripts/mint-dev.sh, tools/scripts/dev/generate-mint-dev-scope.js\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement Tests lpd scoped mint-dev functionality — validates dev server scope filtering\n * @pipeline manual — developer tool\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/lpd-scoped-mint-dev.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\nconst {\n collectRoutesFromNavigation,\n collectOptionsFromNavigation,\n buildScopedNavigation,\n buildScopedMetadata,\n buildScopedMintignore,\n createScopedProfile\n} = require('../../tools/scripts/dev/generate-mint-dev-scope');\n\nconst REPO_ROOT = process.cwd();\nconst LPD_PATH = path.join(REPO_ROOT, 'lpd');\nconst SCOPE_SCRIPT_PATH = path.join(REPO_ROOT, 'tools/scripts/dev/generate-mint-dev-scope.js');\n\nconst SAMPLE_NAVIGATION = {\n versions: [\n {\n version: 'v2',\n languages: [\n {\n language: 'en',\n tabs: [\n {\n tab: 'Developers',\n anchors: [\n {\n anchor: 'Build',\n groups: [\n {\n group: 'Dev',\n pages: ['v2/dev/get-started', 'v2/dev/api-reference/auth']\n }\n ]\n }\n ]\n },\n {\n tab: 'Gateways',\n anchors: [\n {\n anchor: 'Run',\n groups: [\n {\n group: 'Gateway',\n pages: ['v2/gateways/overview']\n }\n ]\n }\n ]\n }\n ]\n },\n {\n language: 'es',\n tabs: [\n {\n tab: 'Developers',\n anchors: [\n {\n anchor: 'Construir',\n groups: [\n {\n group: 'Dev',\n pages: ['v2/es/dev/get-started']\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n },\n {\n version: 'v1',\n languages: [\n {\n language: 'en',\n tabs: [\n {\n tab: 'Legacy',\n anchors: [\n {\n anchor: 'Legacy',\n groups: [\n {\n group: 'Legacy',\n pages: ['v1/legacy/overview']\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n};\n\nfunction runLpd(args) {\n return spawnSync('bash', [LPD_PATH, ...args], {\n cwd: REPO_ROOT,\n encoding: 'utf8'\n });\n}\n\nfunction runScopeScript(args, cwd) {\n return spawnSync('node', [SCOPE_SCRIPT_PATH, ...args], {\n cwd,\n encoding: 'utf8'\n });\n}\n\nfunction mkTmpDir(prefix) {\n return fs.mkdtempSync(path.join(os.tmpdir(), prefix));\n}\n\nfunction writeFile(absPath, content) {\n fs.mkdirSync(path.dirname(absPath), { recursive: true });\n fs.writeFileSync(absPath, content, 'utf8');\n}\n\nasync function runTests() {\n const failures = [];\n const cases = [];\n\n cases.push(async () => {\n const selection = {\n versions: ['v2'],\n languages: ['en'],\n tabs: ['Developers'],\n prefixes: ['v2/dev'],\n disableOpenapi: true\n };\n const scoped = buildScopedNavigation(SAMPLE_NAVIGATION, selection);\n const routes = collectRoutesFromNavigation(scoped);\n assert.deepStrictEqual(routes, ['v2/dev/get-started']);\n });\n\n cases.push(async () => {\n const selection = {\n versions: ['v2'],\n languages: ['en'],\n tabs: ['Developers'],\n prefixes: ['v2/dev'],\n disableOpenapi: true\n };\n const optionData = collectOptionsFromNavigation(SAMPLE_NAVIGATION);\n const allRoutes = collectRoutesFromNavigation(SAMPLE_NAVIGATION);\n const scopedRoutes = ['v2/dev/get-started'];\n const metadata = buildScopedMetadata(selection, optionData, allRoutes, scopedRoutes);\n const scopedMintignore = buildScopedMintignore('# base ignore\\n', metadata);\n assert.match(scopedMintignore, /\\/v1\\/\\*\\*/, 'should exclude unselected version');\n assert.match(scopedMintignore, /\\/v2\\/es\\/\\*\\*/, 'should exclude unselected language directory');\n assert.match(scopedMintignore, /api-reference/, 'should include openapi exclusion patterns');\n });\n\n cases.push(async () => {\n const run = runLpd([\n 'dev',\n '--dry-run',\n '--scoped',\n '--scope-version',\n 'v2',\n '--scope-language',\n 'en',\n '--scope-tab',\n 'Developers',\n '--scope-prefix',\n 'v2/dev',\n '--disable-openapi'\n ]);\n assert.strictEqual(run.status, 0, `lpd dry-run failed: ${run.stderr || run.stdout}`);\n assert.match(run.stdout, /Scoped mint profile: enabled/);\n assert.match(run.stdout, /scope_versions:\\s*v2/);\n assert.match(run.stdout, /scope_languages:\\s*en/);\n assert.match(run.stdout, /scope_tabs:\\s*Developers/);\n assert.match(run.stdout, /OpenAPI docs scope: disabled/);\n assert.match(run.stdout, /tools\\/scripts\\/mint-dev\\.sh/);\n });\n\n cases.push(async () => {\n const run = runLpd(['dev', '--dry-run', '--', '--port', '3333']);\n assert.strictEqual(run.status, 0, `lpd pass-through dry-run failed: ${run.stderr || run.stdout}`);\n assert.doesNotMatch(run.stdout, /Scoped mint profile: enabled/);\n assert.match(run.stdout, /--port/);\n assert.match(run.stdout, /3333/);\n });\n\n cases.push(async () => {\n const tmp = mkTmpDir('lpd-scope-test-');\n const docs = {\n $schema: 'https://mintlify.com/docs.json',\n theme: 'palm',\n name: 'Fixture Docs',\n navigation: SAMPLE_NAVIGATION\n };\n writeFile(path.join(tmp, 'docs.json'), `${JSON.stringify(docs, null, 2)}\\n`);\n writeFile(path.join(tmp, '.mintignore'), '# fixture\\n');\n\n const run = runScopeScript(",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/notion/5-export-to-notion.js",
+ "script": "5-export-to-notion",
+ "category": "automation",
+ "purpose": "tooling:dev-tools",
+ "scope": "external",
+ "owner": "docs",
+ "needs": "node, @notionhq/client, dotenv, NOTION_API_KEY, NOTION_DATABASE_ID, NOTION_WRITABLE_DATABASE_ID(optional)",
+ "purpose_statement": "Updates existing Notion page grouping fields from the exported docs navigation snapshot.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/notion/5-export-to-notion.js [flags]",
+ "header": "/**\n * @script 5-export-to-notion\n * @category automation\n * @purpose tooling:dev-tools\n * @scope external\n * @owner docs\n * @needs node, @notionhq/client, dotenv, NOTION_API_KEY, NOTION_DATABASE_ID, NOTION_WRITABLE_DATABASE_ID(optional)\n * @purpose-statement Updates existing Notion page grouping fields from the exported docs navigation snapshot.\n * @pipeline manual\n * @usage node tools/notion/5-export-to-notion.js [flags]\n */\n\nrequire(\"dotenv\").config();\nconst { Client } = require(\"@notionhq/client\");\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\nasync function exportToNotion() {\n console.log(\"Starting export to Notion...\\n\");\n\n // Check prerequisites\n const notionJsonPath = path.join(__dirname, \"data\", \"notion-read.json\");\n const docsJsonPath = path.join(__dirname, \"data\", \"docs-read.json\");\n const duplicatesReportPath = path.join(\n __dirname,\n \"reports\",\n \"duplicates-report.json\"\n );\n\n if (!fs.existsSync(notionJsonPath)) {\n console.error(\"ERROR: data/notion-read.json not found!\");\n console.error(\"Please run: node 1-read-notion-to-csv.js first\");\n process.exit(1);\n }\n\n if (!fs.existsSync(docsJsonPath)) {\n console.error(\"ERROR: data/docs-read.json not found!\");\n console.error(\"Please run: node 2-read-docs-to-csv.js first\");\n process.exit(1);\n }\n\n // Check for duplicates\n if (fs.existsSync(duplicatesReportPath)) {\n const duplicates = JSON.parse(\n fs.readFileSync(duplicatesReportPath, \"utf8\")\n );\n if (duplicates.length > 0) {\n console.error(\"ERROR: Duplicates still exist in Notion database!\");\n console.error(`Found ${duplicates.length} duplicate groups.`);\n console.error(\"Please run: node 4-remove-duplicates.js first\");\n process.exit(1);\n }\n }\n\n const notionPages = JSON.parse(fs.readFileSync(notionJsonPath, \"utf8\"));\n const docsData = JSON.parse(fs.readFileSync(docsJsonPath, \"utf8\"));\n const { pages, sectionGroupColors } = docsData;\n\n console.log(`Notion database has: ${notionPages.length} pages`);\n console.log(`Docs.json has: ${pages.length} pages\\n`);\n\n // Build map of existing pages - match by URL OR (Page Name + Tab Group) case-insensitive\n const existingByUrl = new Map();\n const existingByNameTab = new Map();\n notionPages.forEach((page) => {\n // Primary match: relativePath URL (case-insensitive)\n if (page.relativePath) {\n existingByUrl.set(page.relativePath.toLowerCase(), page.id);\n }\n // Secondary match: Page Name + Tab Group (case-insensitive)\n const nameTabKey = `${(page.pageName || \"\").toLowerCase()}|||${(\n page.tabGroup || \"\"\n ).toLowerCase()}`;\n existingByNameTab.set(nameTabKey, page.id);\n });\n\n const notion = new Client({ auth: process.env.NOTION_API_KEY });\n // Use the writable database for creating/updating pages\n const databaseId =\n process.env.NOTION_WRITABLE_DATABASE_ID || process.env.NOTION_DATABASE_ID;\n\n let createdCount = 0;\n let updatedCount = 0;\n let skippedCount = 0;\n let errorCount = 0;\n\n console.log(\"Syncing pages to Notion...\\n\");\n\n // Reverse pages array so Section Group options are created in correct order for Notion\n // (Notion shows select options in order they were created, we want reverse of docs.json order)\n const reversedPages = [...pages].reverse();\n\n for (const page of reversedPages) {\n try {\n // Match by URL first, then by Page Name + Tab Group (case-insensitive)\n let existingPageId = null;\n if (page.relativePath) {\n existingPageId = existingByUrl.get(page.relativePath.toLowerCase());\n }\n if (!existingPageId) {\n const nameTabKey = `${(page.pageName || \"\").toLowerCase()}|||${(\n page.tabGroup || \"\"\n ).toLowerCase()}`;\n existingPageId = existingByNameTab.get(nameTabKey);\n }\n\n if (existingPageId) {\n // ONLY update Section Group and Sub Section - nothing else\n const updateProperties = {\n \"Section Group\": page.sectionGroup\n ? {\n select: {\n name: page.sectionGroup,\n color: sectionGroupColors[page.sectionGroup],\n },\n }\n : { select: null },\n \"Sub Section\": page.subGroup\n ? { select: { name: page.subGroup } }\n : { select: null },\n };\n\n await notion.pages.update({\n page_id: existingPageId,\n properties: updateProperties,\n });\n updatedCount++;\n console.log(`↻ Updated: ${page.pageName}`);\n } else {\n // Page doesn't exist - skip (do NOT create new pages)\n skippedCount++;\n console.log(`⊘ Skipped (not in Notion): ${page.pageName}`);\n }\n } catch (error) {\n errorCount++;\n console.error(`✗ Failed to sync ${page.pageName}:`, error.message);\n }\n }\n\n console.log(`\\nSync complete:`);\n console.log(` Updated: ${updatedCount}`);\n console.log(` Skipped (not in Notion): ${skippedCount}`);\n console.log(` Errors: ${errorCount}`);\n\n // Write export report\n const reportsDir = path.join(__dirname, \"reports\");\n if (!fs.existsSync(reportsDir)) {\n fs.mkdirSync(reportsDir, { recursive: true });\n }\n\n const exportReport = {\n timestamp: new Date().toISOString(),\n totalDocsPages: pages.length,\n updated: updatedCount,\n skipped: skippedCount,\n errors: errorCount,\n };\n\n const reportPath = path.join(reportsDir, \"notion-export.json\");\n fs.writeFileSync(reportPath, JSON.stringify(exportReport, null, 2));\n console.log(`\\nExport report saved to: ${reportPath}`);\n}\n\nexportToNotion().catch(console.error);\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/notion/reports",
+ "type": "directory",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": "tools/notion/notion-export.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/notion/reports, tools/notion/notion-export.json",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/notion/backup-notion-table.js",
+ "script": "backup-notion-table",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": "external",
+ "owner": "docs",
+ "needs": "node, @notionhq/client, dotenv, NOTION_API_KEY, NOTION_DATABASE_ID or NOTION_WRITABLE_DATABASE_ID",
+ "purpose_statement": "Backs up the current Notion data source rows and metadata into timestamped JSON and CSV artifacts with a manifest.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/notion/backup-notion-table.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script backup-notion-table\n * @category utility\n * @purpose tooling:dev-tools\n * @scope external\n * @owner docs\n * @needs node, @notionhq/client, dotenv, NOTION_API_KEY, NOTION_DATABASE_ID or NOTION_WRITABLE_DATABASE_ID\n * @purpose-statement Backs up the current Notion data source rows and metadata into timestamped JSON and CSV artifacts with a manifest.\n * @pipeline manual\n * @usage node tools/notion/backup-notion-table.js [flags]\n */\n\nconst fs = require(\"fs\");\nconst path = require(\"path\");\nconst crypto = require(\"crypto\");\n\nrequire(\"dotenv\").config({ path: path.join(__dirname, \".env\") });\nconst { Client } = require(\"@notionhq/client\");\n\nfunction nowStamp() {\n const d = new Date();\n const yyyy = String(d.getUTCFullYear());\n const mm = String(d.getUTCMonth() + 1).padStart(2, \"0\");\n const dd = String(d.getUTCDate()).padStart(2, \"0\");\n const hh = String(d.getUTCHours()).padStart(2, \"0\");\n const mi = String(d.getUTCMinutes()).padStart(2, \"0\");\n const ss = String(d.getUTCSeconds()).padStart(2, \"0\");\n return `${yyyy}${mm}${dd}-${hh}${mi}${ss}Z`;\n}\n\nfunction sha256(value) {\n return crypto.createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nfunction csvEscape(value) {\n const text = value == null ? \"\" : String(value);\n if (/[\",\\n]/.test(text)) {\n return `\"${text.replace(/\"/g, '\"\"')}\"`;\n }\n return text;\n}\n\nfunction getTitle(property) {\n const title = property?.title;\n if (!Array.isArray(title) || title.length === 0) return \"\";\n return title.map((part) => part?.plain_text || \"\").join(\"\");\n}\n\nfunction getSelect(property) {\n return property?.select?.name || \"\";\n}\n\nfunction getMultiSelect(property) {\n const items = property?.multi_select;\n if (!Array.isArray(items) || items.length === 0) return \"\";\n return items.map((item) => item?.name || \"\").filter(Boolean).join(\"|\");\n}\n\nfunction getRichText(property) {\n const items = property?.rich_text;\n if (!Array.isArray(items) || items.length === 0) return \"\";\n return items.map((item) => item?.plain_text || \"\").join(\"\");\n}\n\nfunction getUrl(property) {\n return property?.url || \"\";\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nasync function resolveDataSourceId(notion) {\n const envDataSource = String(process.env.NOTION_DATABASE_ID || \"\").trim();\n const envDatabase = String(process.env.NOTION_WRITABLE_DATABASE_ID || \"\").trim();\n\n if (envDataSource) {\n try {\n await notion.dataSources.retrieve({ data_source_id: envDataSource });\n return {\n dataSourceId: envDataSource,\n databaseId: envDatabase || null,\n source: \"NOTION_DATABASE_ID\"\n };\n } catch (_error) {\n // Fall through to database-based resolution.\n }\n }\n\n if (envDatabase) {\n const db = await notion.databases.retrieve({ database_id: envDatabase });\n const firstDataSource = db?.data_sources?.[0]?.id || null;\n if (!firstDataSource) {\n throw new Error(\"Could not resolve data source ID from NOTION_WRITABLE_DATABASE_ID.\");\n }\n return {\n dataSourceId: firstDataSource,\n databaseId: envDatabase,\n source: \"NOTION_WRITABLE_DATABASE_ID\"\n };\n }\n\n throw new Error(\"Missing Notion IDs. Set NOTION_DATABASE_ID (data source) and/or NOTION_WRITABLE_DATABASE_ID.\");\n}\n\nasync function main() {\n if (!process.env.NOTION_API_KEY) {\n throw new Error(\"Missing NOTION_API_KEY in notion/.env\");\n }\n\n const notion = new Client({ auth: process.env.NOTION_API_KEY });\n const ids = await resolveDataSourceId(notion);\n\n const stamp = nowStamp();\n const backupsRoot = path.join(__dirname, \"backups\");\n const backupDir = path.join(backupsRoot, stamp);\n ensureDir(backupDir);\n\n const rows = [];\n let startCursor = undefined;\n let hasMore = true;\n\n while (hasMore) {\n const res = await notion.dataSources.query({\n data_source_id: ids.dataSourceId,\n start_cursor: startCursor,\n page_size: 100\n });\n rows.push(...res.results);\n hasMore = Boolean(res.has_more);\n startCursor = res.next_cursor || undefined;\n }\n\n const dataSourceMeta = await notion.dataSources.retrieve({ data_source_id: ids.dataSourceId });\n\n let databaseMeta = null;\n if (ids.databaseId) {\n try {\n databaseMeta = await notion.databases.retrieve({ database_id: ids.databaseId });\n } catch (_error) {\n databaseMeta = null;\n }\n }\n\n const normalizedRows = rows.map((row) => {\n const p = row.properties || {};\n return {\n id: row.id,\n inTrash: Boolean(row.in_trash),\n pageName: getTitle(p[\"Page Name\"]),\n tabGroup: getSelect(p[\"Tab Group\"]),\n sectionGroup: getSelect(p[\"Section Group\"]),\n subSection: getSelect(p[\"Sub Section\"]),\n pageStatus: getMultiSelect(p[\"Page Status\"]),\n notes: getRichText(p[\"Notes\"]),\n relativePathUrl: getUrl(p[\"Relative path URL\"]),\n url: getUrl(p[\"URL\"])\n };\n });\n\n const csvHeader = [\n \"ID\",\n \"In Trash\",\n \"Page Name\",\n \"Tab Group\",\n \"Section Group\",\n \"Sub Section\",\n \"Page Status\",\n \"Notes\",\n \"Relative path URL\",\n \"URL\"\n ].join(\",\");\n const csvBody = normalizedRows\n .map((r) =>\n [\n r.id,\n r.inTrash ? \"true\" : \"false\",\n r.pageName,\n r.tabGroup,\n r.sectionGroup,\n r.subSection,\n r.pageStatus,\n r.notes,\n r.relativePathUrl,\n r.url\n ]\n .map(csvEscape)\n .join(\",\")\n )\n .join(\"\\n\");\n const csvContent = `${csvHeader}\\n${csvBody}${csvBody ? \"\\n\" : \"\"}`;\n\n const rawPath = path.join(backupDir, \"rows-raw.json\");\n const normalizedJsonPath = path.join(backupDir, \"rows-normalized.json\");\n const csvPath = path.join(backupDir, \"rows-normalized.csv\");\n const dataSourceMetaPath = path.join(backupDir, \"data-source-metadata.json\");\n const dbMetaPath = path.join(backupDir, \"database-metadata.json\");\n const manifestPath = path.join(backupDir, \"backup-manifest.json\");\n\n fs.writeFileSync(rawPath, JSON.stringify(rows, null, 2));\n fs.writeFileSync(normalizedJsonPath, JSON.stringify(normalizedRows, null, 2));\n fs.writeFileSync(csvPath, csvContent);\n fs.writeFileSync(dataSourceMetaPath, JSON.stringify(dataSourceMeta, null, 2));\n if (databaseMeta) {\n fs.writeFileSync(dbMetaPath, JSON.stringify(databaseMeta, null, 2));\n }\n\n const manifest = {\n backupTimestampUtc: new Date().toISOString(),\n backupFolder: backupDir,\n idResolutionSource: ids.source,\n notionIds: {\n dataSourceId: ids.dataSourceId,\n databaseId: ids.databaseId\n },\n rowCounts: {\n totalRows: rows.length,\n normalizedRows: normalizedRows.length\n },",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/notion/rows-raw.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/notion/rows-normalized.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/notion/rows-normalized.csv",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/notion/data-source-metadata.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/notion/database-metadata.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tools/notion/backup-manifest.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/notion/rows-raw.json, tools/notion/rows-normalized.json, tools/notion/rows-normalized.csv, tools/notion/data-source-metadata.json, tools/notion/database-metadata.json, tools/notion/backup-manifest.json",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/notion/install-local-sync-hook.sh",
+ "script": "install-local-sync-hook",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": "single-file",
+ "owner": "docs",
+ "needs": "bash, git",
+ "purpose_statement": "Installs the managed local post-commit hook that invokes the Notion sync runner and preserves any prior hook as a backup.",
+ "pipeline_declared": "manual",
+ "usage": "bash tools/notion/install-local-sync-hook.sh [flags]",
+ "header": "#!/bin/bash\n# @script install-local-sync-hook\n# @category utility\n# @purpose tooling:dev-tools\n# @scope single-file\n# @owner docs\n# @needs bash, git\n# @purpose-statement Installs the managed local post-commit hook that invokes the Notion sync runner and preserves any prior hook as a backup.\n# @pipeline manual\n# @usage bash tools/notion/install-local-sync-hook.sh [flags]\n\nset -euo pipefail\n\nMARKER=\"LIVEPEER_NOTION_LOCAL_SYNC_HOOK\"\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"$SCRIPT_DIR/../..\" && pwd)\"\nHOOKS_DIR=\"$(git -C \"$REPO_ROOT\" rev-parse --git-path hooks)\"\nHOOK_PATH=\"$HOOKS_DIR/post-commit\"\nRUNNER=\"$SCRIPT_DIR/local-post-commit-sync.sh\"\n\nmkdir -p \"$HOOKS_DIR\"\n\nif [ ! -f \"$RUNNER\" ]; then\n echo \"Notion sync runner not found at $RUNNER\"\n exit 1\nfi\n\nchmod +x \"$RUNNER\"\n\nbackup_hook=\"\"\nif [ -f \"$HOOK_PATH\" ] && ! grep -q \"$MARKER\" \"$HOOK_PATH\"; then\n timestamp=\"$(date -u +%Y%m%d-%H%M%SZ)\"\n backup_hook=\"$HOOKS_DIR/post-commit.pre-notion-$timestamp\"\n cp \"$HOOK_PATH\" \"$backup_hook\"\n chmod +x \"$backup_hook\" || true\n echo \"Backed up existing post-commit hook to:\"\n echo \" $backup_hook\"\nfi\n\ncat >\"$HOOK_PATH\" </dev/null || pwd)\"\nRUNNER=\"\\$REPO_ROOT/tools/notion/local-post-commit-sync.sh\"\nPREVIOUS_HOOK=\"${backup_hook}\"\n\nif [ -n \"\\$PREVIOUS_HOOK\" ] && [ -x \"\\$PREVIOUS_HOOK\" ]; then\n \"\\$PREVIOUS_HOOK\" \"\\$@\" || true\nfi\n\nif [ -x \"\\$RUNNER\" ]; then\n \"\\$RUNNER\" \"\\$@\" || true\nelse\n bash \"\\$RUNNER\" \"\\$@\" || true\nfi\n\nexit 0\nEOF\n\nchmod +x \"$HOOK_PATH\"\n\necho \"Installed local post-commit Notion sync hook:\"\necho \" $HOOK_PATH\"\necho \"\"\necho \"Defaults:\"\necho \" NOTION_LOCAL_SYNC_MODE=write\"\necho \" NOTION_LOCAL_SYNC_STALE_TAB_NAME=Stale\"\necho \"\"\necho \"Optional env vars:\"\necho \" export NOTION_LOCAL_SYNC_MODE=dry-run\"\necho \" export NOTION_LOCAL_SYNC_STALE_TAB_NAME='Stale'\"\necho \" export NOTION_LOCAL_SYNC_DISABLE=1\"\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/notion/local-post-commit-sync.sh",
+ "script": "local-post-commit-sync",
+ "category": "orchestrator",
+ "purpose": "tooling:dev-tools",
+ "scope": "external",
+ "owner": "docs",
+ "needs": "bash, git, node, NOTION_LOCAL_SYNC_MODE(optional), NOTION_LOCAL_SYNC_STALE_TAB_NAME(optional), NOTION_LOCAL_SYNC_DISABLE(optional)",
+ "purpose_statement": "Detects docs.json or v2 content changes in the latest commit and runs the canonical Notion sync locally when enabled.",
+ "pipeline_declared": "manual",
+ "usage": "bash tools/notion/local-post-commit-sync.sh [flags]",
+ "header": "#!/bin/bash\n# @script local-post-commit-sync\n# @category orchestrator\n# @purpose tooling:dev-tools\n# @scope external\n# @owner docs\n# @needs bash, git, node, NOTION_LOCAL_SYNC_MODE(optional), NOTION_LOCAL_SYNC_STALE_TAB_NAME(optional), NOTION_LOCAL_SYNC_DISABLE(optional)\n# @purpose-statement Detects docs.json or v2 content changes in the latest commit and runs the canonical Notion sync locally when enabled.\n# @pipeline manual\n# @usage bash tools/notion/local-post-commit-sync.sh [flags]\n\nset -u\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"$SCRIPT_DIR/../..\" && pwd)\"\n\nif [ \"${NOTION_LOCAL_SYNC_DISABLE:-0}\" = \"1\" ]; then\n exit 0\nfi\n\nif ! command -v git >/dev/null 2>&1; then\n echo \"[notion-sync] git not found; skipping.\"\n exit 0\nfi\n\nif ! command -v node >/dev/null 2>&1; then\n echo \"[notion-sync] node not found; skipping.\"\n exit 0\nfi\n\nchanged_files=\"$(git -C \"$REPO_ROOT\" diff-tree --no-commit-id --name-only -r HEAD 2>/dev/null || true)\"\n\nif ! echo \"$changed_files\" | grep -Eq '^(docs\\.json|v2/.*\\.(md|mdx))$'; then\n exit 0\nfi\n\nif [ ! -f \"$SCRIPT_DIR/.env\" ]; then\n echo \"[notion-sync] tools/notion/.env not found; skipping.\"\n exit 0\nfi\n\nmode=\"${NOTION_LOCAL_SYNC_MODE:-write}\"\nstale_tab_name=\"${NOTION_LOCAL_SYNC_STALE_TAB_NAME:-Stale}\"\n\nif [ \"$mode\" != \"write\" ] && [ \"$mode\" != \"dry-run\" ]; then\n mode=\"write\"\nfi\n\ncmd=(node \"$SCRIPT_DIR/sync-v2-en-canonical.js\")\nif [ \"$mode\" = \"write\" ]; then\n cmd+=(--write)\nelse\n cmd+=(--dry-run)\nfi\ncmd+=(--stale-tab-name \"$stale_tab_name\")\n\necho \"[notion-sync] Detected docs.json/v2 content change in last commit. Running mode=$mode...\"\n\nif ! (\n cd \"$REPO_ROOT\" &&\n \"${cmd[@]}\"\n); then\n echo \"[notion-sync] Sync failed. Commit is kept; rerun manually when ready.\"\n exit 0\nfi\n\necho \"[notion-sync] Completed.\"\nexit 0\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/notion/remove-local-sync-hook.sh",
+ "script": "remove-local-sync-hook",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": "single-file",
+ "owner": "docs",
+ "needs": "bash, git",
+ "purpose_statement": "Removes the managed local Notion post-commit hook when it is present.",
+ "pipeline_declared": "manual",
+ "usage": "bash tools/notion/remove-local-sync-hook.sh [flags]",
+ "header": "#!/bin/bash\n# @script remove-local-sync-hook\n# @category utility\n# @purpose tooling:dev-tools\n# @scope single-file\n# @owner docs\n# @needs bash, git\n# @purpose-statement Removes the managed local Notion post-commit hook when it is present.\n# @pipeline manual\n# @usage bash tools/notion/remove-local-sync-hook.sh [flags]\n\nset -euo pipefail\n\nMARKER=\"LIVEPEER_NOTION_LOCAL_SYNC_HOOK\"\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nREPO_ROOT=\"$(cd \"$SCRIPT_DIR/../..\" && pwd)\"\nHOOKS_DIR=\"$(git -C \"$REPO_ROOT\" rev-parse --git-path hooks)\"\nHOOK_PATH=\"$HOOKS_DIR/post-commit\"\n\nif [ ! -f \"$HOOK_PATH\" ]; then\n echo \"No post-commit hook found at $HOOK_PATH\"\n exit 0\nfi\n\nif ! grep -q \"$MARKER\" \"$HOOK_PATH\"; then\n echo \"Post-commit hook exists but is not the Notion local hook; leaving unchanged.\"\n exit 0\nfi\n\nrm -f \"$HOOK_PATH\"\necho \"Removed local Notion post-commit hook:\"\necho \" $HOOK_PATH\"\necho \"\"\necho \"If you want to restore a previous hook, check for:\"\necho \" $HOOKS_DIR/post-commit.pre-notion-*\"\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/notion/sync-v2-en-canonical.js",
+ "script": "sync-v2-en-canonical",
+ "category": "orchestrator",
+ "purpose": "tooling:dev-tools",
+ "scope": "external",
+ "owner": "docs",
+ "needs": "node, @notionhq/client, dotenv, NOTION_API_KEY, NOTION_DATABASE_ID or NOTION_WRITABLE_DATABASE_ID",
+ "purpose_statement": "Builds canonical v2 English page metadata and syncs Notion schema, row metadata, and optional page-body blocks to match docs.",
+ "pipeline_declared": "manual",
+ "usage": "node tools/notion/sync-v2-en-canonical.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script sync-v2-en-canonical\n * @category orchestrator\n * @purpose tooling:dev-tools\n * @scope external\n * @owner docs\n * @needs node, @notionhq/client, dotenv, NOTION_API_KEY, NOTION_DATABASE_ID or NOTION_WRITABLE_DATABASE_ID\n * @purpose-statement Builds canonical v2 English page metadata and syncs Notion schema, row metadata, and optional page-body blocks to match docs.\n * @pipeline manual\n * @usage node tools/notion/sync-v2-en-canonical.js [flags]\n */\n\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\nrequire(\"dotenv\").config({ path: path.join(__dirname, \".env\") });\nconst { Client } = require(\"@notionhq/client\");\nconst {\n resolveDocPath,\n toPosix\n} = require(\"../lib/docs-index-utils\");\nconst {\n analyzeMdxPage,\n buildUsefulnessMatrixFields\n} = require(\"../lib/docs-usefulness/scoring\");\n\nconst MAPPING_REQUIRED_MARKER = \"[MAPPING_REQUIRED]\";\nconst PAGE_INFO_START_MARKER = \"AUTO-GENERATED PAGE INFO START\";\nconst PAGE_INFO_END_MARKER = \"AUTO-GENERATED PAGE INFO END\";\nconst REVIEW_START_MARKER = \"AUTO-GENERATED REVIEW SECTION START\";\nconst REVIEW_END_MARKER = \"AUTO-GENERATED REVIEW SECTION END\";\nconst LIVE_URL_PREFIX = \"https://docs.livepeer.org/\";\nconst SIDEBAR_TITLE_PROP_NAME = \"Sidebar Title\";\nconst NAV_ORDER_PROP_NAME = \"Navigation Order\";\nconst PAGE_STATUS_PROP_NAME = \"Page Status\";\nconst LEAVE_ME_PROP_NAME = \"Leave Me\";\nconst NOT_IN_NAV_STATUS = \"Not In Navigation\";\nconst STALE_NAV_ORDER_BASE = 1000000;\nconst SEVERITY_RANK = {\n critical: 4,\n serious: 3,\n moderate: 2,\n minor: 1,\n unknown: 0,\n none: -1\n};\n\nfunction nowStamp() {\n const d = new Date();\n const yyyy = String(d.getUTCFullYear());\n const mm = String(d.getUTCMonth() + 1).padStart(2, \"0\");\n const dd = String(d.getUTCDate()).padStart(2, \"0\");\n const hh = String(d.getUTCHours()).padStart(2, \"0\");\n const mi = String(d.getUTCMinutes()).padStart(2, \"0\");\n const ss = String(d.getUTCSeconds()).padStart(2, \"0\");\n return `${yyyy}${mm}${dd}-${hh}${mi}${ss}Z`;\n}\n\nfunction nowIso() {\n return new Date().toISOString();\n}\n\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction parseArgs(argv) {\n const args = {\n write: false,\n staleTabName: \"Stale\",\n outDir: path.join(__dirname, \"reports\"),\n logEvery: 25,\n contentConcurrency: 1,\n skipContentSync: false,\n rowSleepMs: 35,\n deleteSleepMs: 20,\n appendSleepMs: 20,\n skipUnchangedBody: true\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === \"--write\") {\n args.write = true;\n continue;\n }\n if (token === \"--dry-run\") {\n args.write = false;\n continue;\n }\n if (token === \"--stale-tab-name\") {\n args.staleTabName = String(argv[i + 1] || \"\").trim() || args.staleTabName;\n i += 1;\n continue;\n }\n if (token === \"--out-dir\") {\n args.outDir = path.resolve(__dirname, String(argv[i + 1] || \"\").trim() || \"reports\");\n i += 1;\n continue;\n }\n if (token === \"--log-every\") {\n const value = Number(argv[i + 1]);\n if (Number.isFinite(value) && value > 0) args.logEvery = Math.floor(value);\n i += 1;\n continue;\n }\n if (token === \"--row-sleep-ms\") {\n const value = Number(argv[i + 1]);\n if (Number.isFinite(value) && value >= 0) args.rowSleepMs = Math.floor(value);\n i += 1;\n continue;\n }\n if (token === \"--content-concurrency\") {\n const value = Number(argv[i + 1]);\n if (Number.isFinite(value) && value > 0) args.contentConcurrency = Math.floor(value);\n i += 1;\n continue;\n }\n if (token === \"--block-delete-sleep-ms\") {\n const value = Number(argv[i + 1]);\n if (Number.isFinite(value) && value >= 0) args.deleteSleepMs = Math.floor(value);\n i += 1;\n continue;\n }\n if (token === \"--append-sleep-ms\") {\n const value = Number(argv[i + 1]);\n if (Number.isFinite(value) && value >= 0) args.appendSleepMs = Math.floor(value);\n i += 1;\n continue;\n }\n if (token === \"--no-skip-unchanged-body\") {\n args.skipUnchangedBody = false;\n continue;\n }\n if (token === \"--skip-content-sync\") {\n args.skipContentSync = true;\n continue;\n }\n }\n\n return args;\n}\n\nfunction normalizeRoute(route) {\n let value = String(route || \"\").trim();\n value = value.replace(/^\\/+/, \"\");\n value = value.replace(/\\.(md|mdx)$/i, \"\");\n value = value.replace(/\\/index$/i, \"\");\n value = value.replace(/\\/+$/, \"\");\n return value;\n}\n\nfunction normalizeText(value) {\n return String(value || \"\").trim();\n}\n\nfunction normalizeKeyPart(value) {\n return normalizeText(value).toLowerCase();\n}\n\nfunction slugFromRoute(route) {\n const normalized = normalizeRoute(route);\n if (!normalized) return \"\";\n const parts = normalized.split(\"/\");\n return parts[parts.length - 1] || \"\";\n}\n\nfunction placementKey(route, tabGroup, sectionGroup, subSection) {\n return [\n normalizeKeyPart(normalizeRoute(route)),\n normalizeKeyPart(tabGroup),\n normalizeKeyPart(sectionGroup),\n normalizeKeyPart(subSection)\n ].join(\"|||\");\n}\n\nfunction pageNameTabKey(pageName, tabGroup) {\n return [normalizeKeyPart(pageName), normalizeKeyPart(tabGroup)].join(\"||\");\n}\n\nfunction csvEscape(value) {\n const text = value == null ? \"\" : String(value);\n if (/[\",\\n]/.test(text)) {\n return `\"${text.replace(/\"/g, '\"\"')}\"`;\n }\n return text;\n}\n\nfunction countBy(items, key) {\n return items.reduce((acc, item) => {\n const k = item[key] || \"\";\n acc[k] = (acc[k] || 0) + 1;\n return acc;\n }, {});\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction writeCsv(filePath, rows, headers) {\n const headerLine = headers.join(\",\");\n const body = rows\n .map((row) => headers.map((header) => csvEscape(row[header])).join(\",\"))\n .join(\"\\n\");\n const content = `${headerLine}\\n${body}${body ? \"\\n\" : \"\"}`;\n fs.writeFileSync(filePath, content);\n}\n\nfunction readJsonSafe(filePath, fallback = null) {\n try {\n return JSON.parse(fs.readFileSync(filePath, \"utf8\"));\n } catch (_error) {\n return fallback;\n }\n}\n\nfunction dedupeOrdered(values) {\n const seen = new Set();",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/notion/sync-v2-en-summary-${runStamp}.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/notion/sync-v2-en-summary-${runStamp}.json",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/debug-mint-dev.js",
+ "script": "debug-mint-dev",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Mintlify dev debugger — diagnostic tool for troubleshooting mint dev server issues",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "node tools/scripts/debug-mint-dev.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script debug-mint-dev\n * @category utility\n * @purpose tooling:dev-tools\n * @scope tools/scripts\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement Mintlify dev debugger — diagnostic tool for troubleshooting mint dev server issues\n * @pipeline manual — diagnostic/investigation tool, run on-demand only\n * @usage node tools/scripts/debug-mint-dev.js [flags]\n */\n/**\n * Debug script: gather evidence for why \"mint dev\" hangs at \"preparing local preview\".\n * Writes NDJSON to .cursor/debug.log. Run from repo root: node scripts/debug-mint-dev.js\n */\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync, spawn } = require('child_process');\nconst os = require('os');\n\nconst LOG_PATH = path.join(__dirname, '..', '.cursor', 'debug.log');\nconst TIMEOUT_MS = 25000;\n\nfunction log(obj) {\n const line = JSON.stringify({ ...obj, timestamp: Date.now() }) + '\\n';\n const dir = path.dirname(LOG_PATH);\n if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n fs.appendFileSync(LOG_PATH, line);\n}\n\nfunction safe(fn, fallback) {\n try { return fn(); } catch (e) { return fallback !== undefined ? fallback : (e.message || String(e)); }\n}\n\n// H1: what is \"mint\"?\nconst whichMint = safe(() => execSync('which mint', { encoding: 'utf8' }).trim());\nconst typeMint = safe(() => execSync('type mint 2>/dev/null || true', { encoding: 'utf8', shell: '/bin/bash' }).trim());\nlog({ hypothesisId: 'H1', message: 'which mint', data: { whichMint, typeMint } });\n\n// H2: cache dirs\nconst home = os.homedir();\nconst mintlifyDir = path.join(home, '.mintlify');\nconst mintlifyLast = path.join(home, '.mintlify-last');\nconst statMintlify = safe(() => fs.existsSync(mintlifyDir) ? (fs.statSync(mintlifyDir).isDirectory() ? 'dir' : 'other') : 'missing');\nconst statLast = safe(() => fs.existsSync(mintlifyLast) ? (fs.statSync(mintlifyLast).isDirectory() ? 'dir' : 'other') : 'missing');\nlet lastDirContents = 'N/A';\nif (fs.existsSync(mintlifyLast) && fs.statSync(mintlifyLast).isDirectory()) {\n lastDirContents = safe(() => fs.readdirSync(mintlifyLast).join(','), '');\n}\nlog({ hypothesisId: 'H2', message: 'cache dirs', data: { mintlifyDir: statMintlify, mintlifyLast: statLast, lastDirContents } });\n\n// H4: package.json scripts\nconst pkgPath = path.join(__dirname, '..', 'package.json');\nconst pkgScripts = safe(() => {\n const p = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));\n return p.scripts ? Object.keys(p.scripts) : [];\n}, []);\nlog({ hypothesisId: 'H4', message: 'package.json scripts', data: { scripts: pkgScripts } });\n\n// Run mint dev with timeout; capture first chunk of output (H3/H5: parse or network)\nlog({ hypothesisId: 'H3_H5', message: 'mint dev start' });\nconst cwd = path.join(__dirname, '..');\nconst child = spawn('mint', ['dev'], { cwd, stdio: ['ignore', 'pipe', 'pipe'], shell: false });\nlet out = '';\nlet err = '';\nchild.stdout.on('data', (chunk) => { out += chunk.toString(); });\nchild.stderr.on('data', (chunk) => { err += chunk.toString(); });\nconst timeout = setTimeout(() => {\n child.kill('SIGKILL');\n log({ hypothesisId: 'H3_H5', message: 'mint dev timeout', data: { stdout: out.slice(-2000), stderr: err.slice(-2000), killed: true } });\n setTimeout(() => process.exit(0), 500);\n}, TIMEOUT_MS);\nchild.on('close', (code, signal) => {\n clearTimeout(timeout);\n log({ hypothesisId: 'H3_H5', message: 'mint dev exit', data: { code, signal, stdoutTail: out.slice(-1500), stderrTail: err.slice(-1500) } });\n});\nchild.on('error', (e) => {\n clearTimeout(timeout);\n log({ hypothesisId: 'H1', message: 'mint dev spawn error', data: { error: e.message, code: e.code } });\n process.exit(1);\n});\nchild.on('exit', () => { process.exit(0); });\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/debug.log",
+ "type": "generated-output",
+ "call": "appendFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/debug.log",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/mint-dev.sh",
+ "script": "mint-dev",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Mintlify dev server launcher — starts mint dev with correct configuration",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "bash tools/scripts/mint-dev.sh [flags]",
+ "header": "#!/usr/bin/env bash\n# @script mint-dev\n# @category utility\n# @purpose tooling:dev-tools\n# @scope tools/scripts\n# @owner docs\n# @needs E-C6, F-C1\n# @purpose-statement Mintlify dev server launcher — starts mint dev with correct configuration\n# @pipeline manual — interactive developer tool, not suited for automated pipelines\n# @usage bash tools/scripts/mint-dev.sh [flags]\nset -euo pipefail\n\nREPO_ROOT=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")/../..\" && pwd)\"\nMINT_WORKDIR=\"$REPO_ROOT\"\nSCOPE_GENERATOR=\"$REPO_ROOT/tools/scripts/dev/generate-mint-dev-scope.js\"\nSCOPED_MODE=\"${LPD_SCOPED_MINT_DEV:-0}\"\nSCOPE_INTERACTIVE=\"${LPD_MINT_SCOPE_INTERACTIVE:-0}\"\nSCOPE_FILE=\"${LPD_MINT_SCOPE_FILE:-}\"\nSCOPE_VERSIONS=\"${LPD_MINT_SCOPE_VERSIONS:-}\"\nSCOPE_LANGUAGES=\"${LPD_MINT_SCOPE_LANGUAGES:-}\"\nSCOPE_TABS=\"${LPD_MINT_SCOPE_TABS:-}\"\nSCOPE_PREFIXES=\"${LPD_MINT_SCOPE_PREFIXES:-}\"\nDISABLE_OPENAPI=\"${LPD_MINT_DISABLE_OPENAPI:-0}\"\nMINT_LOCK_FILE=\"\"\n\n# chokidar treats glob metacharacters in watch paths as patterns. If the repo\n# path includes brackets (common in worktree names), change events can be lost.\npath_has_glob_meta() {\n local path=\"$1\"\n case \"$path\" in\n *'['*|*']'*|*'{'*|*'}'*|*'*'*|*'?'*|*'!'*)\n return 0\n ;;\n *)\n return 1\n ;;\n esac\n}\n\nif path_has_glob_meta \"$REPO_ROOT\"; then\n REPO_HASH=\"$(printf '%s' \"$REPO_ROOT\" | cksum | awk '{print $1}')\"\n WATCH_SAFE_LINK=\"/tmp/mint-dev-${REPO_HASH}\"\n ln -sfn \"$REPO_ROOT\" \"$WATCH_SAFE_LINK\"\n MINT_WORKDIR=\"$WATCH_SAFE_LINK\"\n echo \"Using watcher-safe path for mint dev: $MINT_WORKDIR\"\nfi\n\nmint_lock_file_path() {\n local repo_hash\n repo_hash=\"$(printf '%s' \"$REPO_ROOT\" | cksum | awk '{print $1}')\"\n echo \"/tmp/lpd-mint-dev-lock-${repo_hash}.pid\"\n}\n\nensure_no_active_mint_dev() {\n local lock_file=\"$1\"\n if [ ! -f \"$lock_file\" ]; then\n return 0\n fi\n\n local existing_pid existing_workdir\n existing_pid=\"$(sed -n '1p' \"$lock_file\" 2>/dev/null || true)\"\n existing_workdir=\"$(sed -n '2p' \"$lock_file\" 2>/dev/null || true)\"\n\n if [ -n \"$existing_pid\" ] && kill -0 \"$existing_pid\" 2>/dev/null; then\n echo \"Error: mint dev is already running for this repository.\" >&2\n echo \" pid: $existing_pid\" >&2\n if [ -n \"$existing_workdir\" ]; then\n echo \" workspace: $existing_workdir\" >&2\n fi\n echo \"Stop the existing process before launching another dev session.\" >&2\n exit 1\n fi\n\n rm -f \"$lock_file\"\n}\n\ncleanup_lock_file() {\n if [ -n \"$MINT_LOCK_FILE\" ]; then\n rm -f \"$MINT_LOCK_FILE\"\n fi\n}\n\nbuild_scoped_workspace() {\n if ! command -v node >/dev/null 2>&1; then\n echo \"Error: node is required for --scoped dev profile generation.\" >&2\n exit 1\n fi\n\n if [ ! -f \"$SCOPE_GENERATOR\" ]; then\n echo \"Error: scoped profile generator not found: $SCOPE_GENERATOR\" >&2\n exit 1\n fi\n\n local -a scope_cmd\n scope_cmd=(node \"$SCOPE_GENERATOR\" --repo-root \"$REPO_ROOT\")\n\n if [ \"$SCOPE_INTERACTIVE\" = \"1\" ]; then\n scope_cmd+=(--interactive)\n fi\n if [ -n \"$SCOPE_FILE\" ]; then\n scope_cmd+=(--scope-file \"$SCOPE_FILE\")\n fi\n if [ -n \"$SCOPE_VERSIONS\" ]; then\n scope_cmd+=(--versions \"$SCOPE_VERSIONS\")\n fi\n if [ -n \"$SCOPE_LANGUAGES\" ]; then\n scope_cmd+=(--languages \"$SCOPE_LANGUAGES\")\n fi\n if [ -n \"$SCOPE_TABS\" ]; then\n scope_cmd+=(--tabs \"$SCOPE_TABS\")\n fi\n if [ -n \"$SCOPE_PREFIXES\" ]; then\n scope_cmd+=(--prefixes \"$SCOPE_PREFIXES\")\n fi\n if [ \"$DISABLE_OPENAPI\" = \"1\" ]; then\n scope_cmd+=(--disable-openapi)\n fi\n\n local scope_json\n if ! scope_json=\"$(\"${scope_cmd[@]}\")\"; then\n echo \"Error: failed to generate scoped Mint workspace.\" >&2\n exit 1\n fi\n\n local parsed\n if ! parsed=\"$(printf '%s' \"$scope_json\" | node -e '\nlet input = \"\";\nprocess.stdin.on(\"data\", (chunk) => { input += chunk; });\nprocess.stdin.on(\"end\", () => {\n const payload = JSON.parse(input);\n const original = payload && payload.routeCounts ? Number(payload.routeCounts.original || 0) : 0;\n const scoped = payload && payload.routeCounts ? Number(payload.routeCounts.scoped || 0) : 0;\n const data = [payload.workspaceDir || \"\", payload.scopeHash || \"\", String(original), String(scoped)];\n process.stdout.write(data.join(\"|\"));\n});\n')\"; then\n echo \"Error: invalid scoped profile output from generator.\" >&2\n exit 1\n fi\n\n local scoped_workspace scope_hash original_routes scoped_routes\n IFS='|' read -r scoped_workspace scope_hash original_routes scoped_routes <<< \"$parsed\"\n\n if [ -z \"$scoped_workspace\" ] || [ ! -d \"$scoped_workspace\" ]; then\n echo \"Error: scoped workspace path is invalid: $scoped_workspace\" >&2\n exit 1\n fi\n\n MINT_WORKDIR=\"$scoped_workspace\"\n echo \"Using scoped Mint workspace: $MINT_WORKDIR (routes ${scoped_routes}/${original_routes}, hash ${scope_hash})\"\n}\n\ncd \"$REPO_ROOT\"\n\n# Support both regular repos and worktrees\nGIT_COMMON_DIR=$(git rev-parse --git-common-dir 2>/dev/null)\nif [ -z \"$GIT_COMMON_DIR\" ] || [ \"$GIT_COMMON_DIR\" = \"--git-common-dir\" ]; then\n GIT_COMMON_DIR=\".git\"\nfi\n\nHOOK_SOURCE=\".githooks/pre-commit\"\nHOOK_TARGET=\"$GIT_COMMON_DIR/hooks/pre-commit\"\n\nif [ -f \"$HOOK_SOURCE\" ]; then\n if [ ! -x \"$HOOK_TARGET\" ] || ! cmp -s \"$HOOK_SOURCE\" \"$HOOK_TARGET\"; then\n echo \"Installing git hooks...\"\n ./.githooks/install.sh\n echo \"\"\n fi\nelse\n echo \"Warning: $HOOK_SOURCE not found. Skipping hook installation.\"\nfi\n\nif ! command -v mint >/dev/null 2>&1; then\n echo \"Error: mint CLI not found.\"\n echo \"Install it with: npm i -g mintlify\"\n exit 1\nfi\n\necho \"Checking Mint watcher patch...\"\nif bash tools/scripts/dev/ensure-mint-watcher-patch.sh --apply; then\n echo \"Mint watcher patch preflight complete.\"\nelse\n echo \"Warning: Mint watcher patch preflight failed.\"\n echo \"Run manually: bash tools/scripts/dev/ensure-mint-watcher-patch.sh --apply\"\n echo \"Continuing with repo-local watcher-safe launcher path fallback.\"\nfi\n\necho \"Fetching external snippets...\"\nbash tools/scripts/snippets/fetch-external-docs.sh\n\nif [ \"$SCOPED_MODE\" = \"1\" ]; then\n build_scoped_workspace\nelif [ \"$DISABLE_OPENAPI\" = \"1\" ]; then\n echo \"Warning: --disable-openapi has no effect without --scoped.\"\nfi\n\nMINT_LOCK_FILE=\"$(mint_lock_file_path)\"\nensure_no_active_mint_dev \"$MINT_LOCK_FILE\"\n{\n echo \"$$\"\n echo \"$MINT_WORKDIR\"\n} > \"$MINT_LOCK_FILE\"\ntrap cleanup_lock_file EXIT INT TERM\n\ncd \"$MINT_WORKDIR\"\nmint dev \"$@\"\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/snippets/generate-api-docs.sh",
+ "script": "generate-api-docs",
+ "category": "generator",
+ "purpose": "tooling:dev-tools",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "API docs generator — generates API reference pages from OpenAPI specs",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "bash tools/scripts/snippets/generate-api-docs.sh [flags]",
+ "header": "#!/bin/bash\n# @script generate-api-docs\n# @category generator\n# @purpose tooling:dev-tools\n# @scope tools/scripts\n# @owner docs\n# @needs E-C6, F-C1\n# @purpose-statement API docs generator — generates API reference pages from OpenAPI specs\n# @pipeline manual — interactive developer tool, not suited for automated pipelines\n# @usage bash tools/scripts/snippets/generate-api-docs.sh [flags]\n# Generate API documentation from OpenAPI spec\n# Creates: landing page + individual endpoint pages + navigation JSON\n#\n# Usage: ./generate-api-docs.sh \n# Example: ./generate-api-docs.sh ai/worker/api/openapi.yaml v2/pages/04_gateways/guides-references/api-reference/AI-API \"AI API\"\n#\n\nset -e\n\nOPENAPI_SPEC=\"$1\"\nOUTPUT_DIR=\"$2\"\nAPI_NAME=\"$3\"\nGITHUB_REPO=\"$4\"\n\nif [ -z \"$OPENAPI_SPEC\" ] || [ -z \"$OUTPUT_DIR\" ] || [ -z \"$API_NAME\" ]; then\n echo \"Usage: $0 [github-repo-url]\"\n echo \"Example: $0 ai/worker/api/openapi.yaml v2/pages/04_gateways/api-reference/AI-API \\\"AI\\\" \\\"https://github.com/livepeer/ai-worker\\\"\"\n exit 1\nfi\n\n# Create output directory\nmkdir -p \"$OUTPUT_DIR\"\n\n# Convert YAML to JSON if needed, then generate pages\nif [[ \"$OPENAPI_SPEC\" == *.yaml ]] || [[ \"$OPENAPI_SPEC\" == *.yml ]]; then\n TEMP_JSON=$(mktemp)\n npx js-yaml \"$OPENAPI_SPEC\" > \"$TEMP_JSON\"\n SPEC_FILE=\"$TEMP_JSON\"\nelse\n SPEC_FILE=\"$OPENAPI_SPEC\"\nfi\n\n# Generate the endpoint pages and landing page using Node.js\nnode - \"$SPEC_FILE\" \"$OUTPUT_DIR\" \"$API_NAME\" \"$OPENAPI_SPEC\" \"$GITHUB_REPO\" << 'NODEJS_SCRIPT'\nconst fs = require('fs');\nconst path = require('path');\n\nconst [,, specPath, outputDir, apiName, originalSpecPath, githubRepo] = process.argv;\n\n// Read and parse OpenAPI spec (already converted to JSON)\nconst specContent = fs.readFileSync(specPath, 'utf8');\nconst spec = JSON.parse(specContent);\n\n// Icon mapping for endpoint types\nconst iconMap = {\n 'text-to-image': 'image',\n 'image-to-image': 'wand-magic-sparkles',\n 'image-to-video': 'video',\n 'video-to-video': 'film',\n 'live-video-to-video': 'film',\n 'upscale': 'up-right-and-down-left-from-center',\n 'audio-to-text': 'microphone',\n 'text-to-speech': 'volume-high',\n 'segment-anything': 'object-group',\n 'llm': 'brain',\n 'image-to-text': 'message-image',\n 'health': 'heart-pulse',\n 'hardware': 'microchip',\n 'info': 'circle-info',\n 'stats': 'chart-line',\n 'default': 'code'\n};\n\nfunction getIcon(endpointName) {\n for (const [key, icon] of Object.entries(iconMap)) {\n if (endpointName.toLowerCase().includes(key)) return icon;\n }\n return iconMap.default;\n}\n\nfunction slugify(str) {\n return str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');\n}\n\nconst endpoints = [];\nconst groups = {};\n\n// Process each path in the spec\nfor (const [pathUrl, methods] of Object.entries(spec.paths || {})) {\n for (const [method, details] of Object.entries(methods)) {\n if (['get', 'post', 'put', 'patch', 'delete'].includes(method)) {\n const tag = details.tags?.[0] || 'Other';\n const summary = details.summary || pathUrl;\n const description = details.description || '';\n const slug = slugify(pathUrl.replace(/\\//g, '-'));\n \n const endpoint = { pathUrl, method, summary, description, slug, tag };\n endpoints.push(endpoint);\n \n if (!groups[tag]) groups[tag] = [];\n groups[tag].push(endpoint);\n \n // Generate individual endpoint MDX file\n const mdxContent = `---\nopenapi: ${method} ${pathUrl}\n---\n`;\n fs.writeFileSync(path.join(outputDir, `${slug}.mdx`), mdxContent);\n }\n }\n}\n\n// Generate landing page\nlet landingContent = `---\ntitle: '${apiName} API Portal'\nsidebarTitle: '${apiName} API Portal'\ndescription: '${apiName} API Reference Portal - find all API endpoints and try them out here'\ntag: 'API Index'\n---\n\n`;\n\n// Add GitHub repo card if provided\nif (githubRepo) {\n landingContent += `\n Source code and OpenAPI specification\n \n\n`;\n}\n\n// Add base URLs if available (styled table)\nif (spec.servers && spec.servers.length > 0) {\n landingContent += `## Base URLs\n\n\n\n \n \n Environment \n URL \n \n \n \n`;\n spec.servers.forEach((server, index) => {\n const bgColor = index % 2 === 0 ? '#1a1a1a' : 'transparent';\n landingContent += ` \n ${server.description || 'Server'} \n ${server.url} \n \n`;\n });\n landingContent += ` \n
\n\n\n---\n\n`;\n}\n\n// Add endpoint cards grouped by tag\nfor (const [tag, tagEndpoints] of Object.entries(groups)) {\n // Capitalize tag name, skip \"generate\" tag name, add \"Endpoints\" suffix\n const tagLower = tag.toLowerCase();\n const tagTitle = tagLower === 'generate' ? 'Endpoints' : (tag.charAt(0).toUpperCase() + tag.slice(1) + ' Endpoints');\n\n landingContent += `## ${tagTitle}\n\n\n`;\n for (const ep of tagEndpoints) {\n const icon = getIcon(ep.slug);\n const cardDesc = ep.description.split('.')[0] || ep.summary;\n landingContent += ` \n ${cardDesc}\n \n`;\n }\n landingContent += ` \n\n`;\n}\n\nfs.writeFileSync(path.join(outputDir, `${slugify(apiName)}.mdx`), landingContent);\n\n// Generate navigation JSON snippet\nconst navPages = [`${outputDir}/${slugify(apiName)}`];\nfor (const ep of endpoints) {\n navPages.push(`${outputDir}/${ep.slug}`);\n}\n\nconsole.log('\\n✅ Generated files in:', outputDir);\nconsole.log('\\n📋 Add this to docs.json navigation:\\n');\nconsole.log(JSON.stringify({\n group: apiName,\n pages: navPages\n}, null, 2));\n\nNODEJS_SCRIPT\n\necho \"\"\necho \"Done! Check $OUTPUT_DIR for generated files.\"\n\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/snippets/generate-data/scripts/generate-glossary.js",
+ "script": "generate-glossary",
+ "category": "generator",
+ "purpose": "tooling:dev-tools",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Glossary generator — produces glossary data file from terminology sources",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/snippets/generate-data/scripts/generate-glossary.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script generate-glossary\n * @category generator\n * @purpose tooling:dev-tools\n * @scope tools/scripts\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement Glossary generator — produces glossary data file from terminology sources\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/snippets/generate-data/scripts/generate-glossary.js [flags]\n */\n\n/**\n * Glossary Generation Script\n * \n * Scans all MDX pages in v1 & v2 for terminology that:\n * - May not be commonly recognized by laypeople\n * - Is Livepeer-specific\n * - Is domain-specific (video, blockchain/crypto, AI)\n * - Is used frequently in the docs\n * \n * Outputs a JSON data file with:\n * - Term Name (full form with acronyms/abbreviations)\n * - Page(s) the term is used on\n * - Clear Definition\n * - Category (web3, video, AI, Livepeer protocol, technical, etc.)\n * - Tags (for searchability)\n * - External Verification (link to external source)\n * - Context/Usage Notes\n * \n * Usage: node generate-glossary.js [--dry-run]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\n\n// Configuration\nconst REPO_ROOT = getRepoRoot();\nconst V1_PAGES_DIR = path.join(REPO_ROOT, 'v1');\nconst V2_PAGES_DIRS = [\n 'v2/pages',\n 'v2/home',\n 'v2/solutions',\n 'v2/about',\n 'v2/community',\n 'v2/developers',\n 'v2/gateways',\n 'v2/orchestrators',\n 'v2/lpt',\n 'v2/resources',\n 'v2/internal',\n 'v2/deprecated',\n 'v2/experimental',\n 'v2/notes'\n]\n .map((dir) => path.join(REPO_ROOT, dir))\n .filter((dir) => fs.existsSync(dir));\nconst OUTPUT_DIR = path.join(__dirname, '..', 'data');\nconst OUTPUT_FILE = path.join(OUTPUT_DIR, 'glossary-terms.json');\n\nconst isDryRun = process.argv.includes('--dry-run');\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\n// Known terminology patterns and categories\nconst TERM_PATTERNS = {\n livepeer: {\n category: 'Livepeer Protocol',\n terms: [\n 'Livepeer', 'LPT', 'Livepeer Token', 'Livepeer Network', 'Livepeer Protocol',\n 'Livepeer Studio', 'Livepeer Foundation', 'Livepeer Inc', 'Livepeer DAO',\n 'Orchestrator', 'Delegator', 'Gateway', 'Broadcaster', 'Transcoder',\n 'go-livepeer', 'livepeer-ai', 'AI Worker', 'AI Gateway', 'AI Subnet',\n 'SPE', 'Special Purpose Entity', 'On-chain Treasury', 'Reward Cut', 'Fee Cut',\n 'Probabilistic Micropayments', 'Payment Ticket', 'Ticket', 'Rounds',\n 'Staking', 'Delegation', 'Slashing', 'Inflation', 'Reputation',\n 'Daydream', 'Streamplace'\n ]\n },\n web3: {\n category: 'Web3 / Blockchain',\n terms: [\n 'Ethereum', 'ETH', 'Arbitrum', 'ARB', 'Layer 2', 'L2', 'Mainnet', 'Testnet',\n 'Smart Contract', 'DAO', 'Decentralized Autonomous Organization',\n 'DePIN', 'Decentralized Physical Infrastructure Network',\n 'Wallet', 'Private Key', 'Public Key', 'Address', 'Transaction', 'Gas',\n 'Block', 'Blockchain', 'Consensus', 'Proof of Stake', 'PoS',\n 'Token', 'ERC-20', 'NFT', 'Bridge', 'Bridging', 'Rollup', 'Rollups',\n 'On-chain', 'Off-chain', 'Tokenomics', 'Game Theory', 'Governance'\n ]\n },\n video: {\n category: 'Video Engineering',\n terms: [\n 'Transcoding', 'Transcode', 'Ingest', 'Delivery', 'Streaming', 'Stream',\n 'RTMP', 'HLS', 'WebRTC', 'SRT', 'DASH', 'Codec', 'H.264', 'H.265', 'HEVC',\n 'VP9', 'AV1', 'Bitrate', 'Resolution', 'Frame Rate', 'FPS', 'Keyframe',\n 'Segment', 'Manifest', 'Playlist', 'ABR', 'Adaptive Bitrate',\n 'CDN', 'Content Delivery Network', 'Latency', 'Buffering',\n 'Rendition', 'Quality Ladder', 'Encoding', 'Decoding', 'Muxing',\n 'Live Stream', 'VOD', 'Video on Demand', 'Playback'\n ]\n },\n ai: {\n category: 'AI / Machine Learning',\n terms: [\n 'Inference', 'Model', 'Pipeline', 'GPU', 'CUDA', 'TensorRT',\n 'Diffusion', 'Stable Diffusion', 'ControlNet', 'LoRA', 'ComfyUI',\n 'LLM', 'Large Language Model', 'Transformer', 'Neural Network',\n 'Real-Time AI', 'World Model', 'Agent', 'Embedding', 'Vector',\n 'Image-to-Image', 'Text-to-Image', 'Image-to-Video', 'Text-to-Video',\n 'Upscaling', 'Super Resolution', 'Depth Estimation', 'Segmentation',\n 'Object Detection', 'Face Detection', 'Pose Estimation',\n 'Prompt', 'Negative Prompt', 'CFG Scale', 'Steps', 'Seed', 'Batch'\n ]\n },\n technical: {\n category: 'Technical',\n terms: [\n 'API', 'REST', 'GraphQL', 'WebSocket', 'HTTP', 'HTTPS', 'JSON', 'YAML',\n 'SDK', 'CLI', 'Docker', 'Container', 'Kubernetes', 'Node', 'Server',\n 'Endpoint', 'Request', 'Response', 'Authentication', 'Authorization',\n 'JWT', 'OAuth', 'Webhook', 'Callback', 'Polling', 'Rate Limit',\n 'Timeout', 'Retry', 'Cache', 'Load Balancer', 'Proxy', 'Reverse Proxy',\n 'SSL', 'TLS', 'Certificate', 'DNS', 'IP Address', 'Port', 'Protocol'\n ]\n }\n};\n\n/**\n * Find all MDX files in a directory recursively\n */\nfunction findMdxFiles(dir) {\n const files = [];\n if (!fs.existsSync(dir)) return files;\n \n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n files.push(...findMdxFiles(fullPath));\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n files.push(fullPath);\n }\n }\n return files;\n}\n\n/**\n * Extract text content from MDX file (strip JSX/imports)\n */\nfunction extractTextContent(content) {\n // Remove frontmatter\n content = content.replace(/^---[\\s\\S]*?---/, '');\n // Remove import statements\n content = content.replace(/^import\\s+.*$/gm, '');\n // Remove JSX tags but keep text content\n content = content.replace(/<[^>]+>/g, ' ');\n // Remove code blocks\n content = content.replace(/```[\\s\\S]*?```/g, '');\n content = content.replace(/`[^`]+`/g, '');\n // Remove markdown links but keep text\n content = content.replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1');\n // Remove markdown formatting\n content = content.replace(/[*_#]+/g, '');\n return content;\n}\n\n/**\n * Count term occurrences in content\n */\nfunction countTermOccurrences(content, term) {\n const regex = new RegExp(`\\\\b${term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}\\\\b`, 'gi');\n const matches = content.match(regex);\n return matches ? matches.length : 0;\n}\n\n/**\n * Scan files for terms and build term index\n */\nfunction scanFilesForTerms(files) {\n const termIndex = {};\n\n // Initialize term index with all known terms\n for (const [categoryKey, categoryData] of Object.entries(TERM_PATTERNS)) {\n for (const term of categoryData.terms) {\n const termKey = term.toLowerCase();\n if (!termIndex[termKey]) {\n termIndex[termKey] = {\n termName: term,\n category: categoryData.category,\n pages: [],\n totalOccurrences: 0,\n definition: '',\n tags: [categoryKey],\n externalVerification: '',\n contextNotes: ''\n };\n }\n }\n }\n\n // Scan each file\n for (const file of files) {\n const content = fs.readFileSync(file, 'utf-8');\n const textContent = extractTextContent(content);\n const relativePath = path.relative(REPO_ROOT, file);\n\n for (const [termKey, termData] of Object.entries(termIndex)) {\n const count = countTermOccurrences(textContent, termData.termName);\n if (count > 0) {\n termData.pages.push({",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/snippets/generate-data/scripts/data",
+ "type": "directory",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": "tools/scripts/snippets/generate-data/scripts/glossary-terms.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/snippets/generate-data/scripts/data, tools/scripts/snippets/generate-data/scripts/glossary-terms.json",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/snippets/generate-data/scripts/terminology-search.js",
+ "script": "terminology-search",
+ "category": "generator",
+ "purpose": "tooling:dev-tools",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Terminology search — searches glossary/terminology data for definitions",
+ "pipeline_declared": "manual — interactive developer tool, not suited for automated pipelines",
+ "usage": "node tools/scripts/snippets/generate-data/scripts/terminology-search.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script terminology-search\n * @category generator\n * @purpose tooling:dev-tools\n * @scope tools/scripts\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement Terminology search — searches glossary/terminology data for definitions\n * @pipeline manual — interactive developer tool, not suited for automated pipelines\n * @usage node tools/scripts/snippets/generate-data/scripts/terminology-search.js [flags]\n */\n\n/**\n * Terminology Search Script\n *\n * Scans all MDX pages to DISCOVER terminology that:\n * - May not be commonly recognized by laypeople\n * - Is Livepeer-specific\n * - Is domain-specific (video, blockchain/crypto, AI)\n * - Is used frequently in the docs\n *\n * Uses pattern matching to find CANDIDATE terms, then optionally\n * uses an LLM to evaluate and define them.\n *\n * Usage:\n * node terminology-search.js --dry-run # Find candidates only\n * node terminology-search.js --with-llm # Use LLM to evaluate/define\n * node terminology-search.js --min-occurrences=5 # Minimum occurrences threshold\n *\n * Environment:\n * Create a .env file in this directory with:\n * OPENROUTER_API_KEY=your-key-here (free, recommended)\n * or\n * OPENAI_API_KEY=your-key-here (paid)\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\n\n// Load .env file from the scripts directory (optional in local dry-run use)\ntry {\n require('dotenv').config({ path: path.join(__dirname, '.env') });\n} catch (_error) {\n // Optional dependency for local dry-run usage.\n}\n\n// Configuration\nconst REPO_ROOT = getRepoRoot();\nconst V1_PAGES_DIR = path.join(REPO_ROOT, 'v1');\nconst V2_PAGES_DIRS = [\n 'v2/pages',\n 'v2/home',\n 'v2/solutions',\n 'v2/about',\n 'v2/community',\n 'v2/developers',\n 'v2/gateways',\n 'v2/orchestrators',\n 'v2/lpt',\n 'v2/resources',\n 'v2/internal',\n 'v2/deprecated',\n 'v2/experimental',\n 'v2/notes'\n]\n .map((dir) => path.join(REPO_ROOT, dir))\n .filter((dir) => fs.existsSync(dir));\nconst OUTPUT_DIR = path.join(__dirname, '..', 'data');\nconst OUTPUT_FILE = path.join(OUTPUT_DIR, 'discovered-terms.json');\n\nconst args = process.argv.slice(2);\nconst isDryRun = args.includes('--dry-run');\nconst withLLM = args.includes('--with-llm');\nconst minOccurrencesArg = args.find(a => a.startsWith('--min-occurrences='));\nconst MIN_OCCURRENCES = minOccurrencesArg ? parseInt(minOccurrencesArg.split('=')[1]) : 3;\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\n// OpenRouter model selection - can be overridden with --model flag\nconst modelArg = args.find(a => a.startsWith('--model='));\nconst OPENROUTER_MODEL = modelArg\n ? modelArg.split('=')[1]\n : 'nousresearch/hermes-3-llama-3.1-405b:free';\n\n// Common words to exclude (not terminology)\nconst COMMON_WORDS = new Set([\n 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with',\n 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does',\n 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'can', 'this',\n 'that', 'these', 'those', 'it', 'its', 'they', 'them', 'their', 'we', 'us', 'our',\n 'you', 'your', 'he', 'she', 'him', 'her', 'his', 'i', 'me', 'my', 'if', 'then',\n 'else', 'when', 'where', 'why', 'how', 'what', 'which', 'who', 'whom', 'whose',\n 'all', 'each', 'every', 'both', 'few', 'more', 'most', 'other', 'some', 'such',\n 'no', 'nor', 'not', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 'just',\n 'also', 'now', 'here', 'there', 'about', 'after', 'before', 'between', 'into',\n 'through', 'during', 'above', 'below', 'from', 'up', 'down', 'out', 'off', 'over',\n 'under', 'again', 'further', 'once', 'any', 'as', 'by', 'because', 'while',\n // Common programming/docs words\n 'example', 'examples', 'note', 'notes', 'see', 'using', 'use', 'used', 'uses',\n 'create', 'created', 'creating', 'add', 'added', 'adding', 'set', 'setting',\n 'get', 'getting', 'run', 'running', 'start', 'started', 'starting', 'stop',\n 'file', 'files', 'folder', 'folders', 'directory', 'path', 'name', 'names',\n 'value', 'values', 'key', 'keys', 'type', 'types', 'data', 'string', 'number',\n 'true', 'false', 'null', 'undefined', 'return', 'returns', 'function', 'method',\n 'class', 'object', 'array', 'list', 'item', 'items', 'index', 'length', 'size',\n 'new', 'old', 'first', 'last', 'next', 'previous', 'current', 'default',\n 'error', 'errors', 'warning', 'warnings', 'success', 'failed', 'failure',\n 'click', 'button', 'link', 'page', 'pages', 'section', 'sections', 'step', 'steps',\n 'following', 'below', 'above', 'left', 'right', 'top', 'bottom', 'center',\n 'image', 'images', 'video', 'videos', 'text', 'content', 'title', 'description',\n 'import', 'export', 'from', 'const', 'let', 'var', 'async', 'await', 'try', 'catch',\n 'make', 'made', 'making', 'need', 'needs', 'needed', 'want', 'wants', 'wanted',\n 'like', 'look', 'looks', 'looking', 'find', 'found', 'finding', 'show', 'shows',\n 'shown', 'showing', 'work', 'works', 'working', 'call', 'calls', 'called', 'calling',\n 'read', 'reads', 'reading', 'write', 'writes', 'writing', 'send', 'sends', 'sending',\n 'receive', 'receives', 'receiving', 'check', 'checks', 'checking', 'test', 'tests',\n 'testing', 'build', 'builds', 'building', 'deploy', 'deploys', 'deploying',\n 'install', 'installs', 'installing', 'update', 'updates', 'updating', 'delete',\n 'deletes', 'deleting', 'remove', 'removes', 'removing', 'change', 'changes',\n 'changing', 'move', 'moves', 'moving', 'copy', 'copies', 'copying', 'paste',\n 'open', 'opens', 'opening', 'close', 'closes', 'closing', 'save', 'saves', 'saving',\n 'load', 'loads', 'loading', 'download', 'downloads', 'downloading', 'upload',\n 'uploads', 'uploading', 'enable', 'enables', 'enabling', 'disable', 'disables',\n 'disabling', 'allow', 'allows', 'allowing', 'require', 'requires', 'requiring',\n 'include', 'includes', 'including', 'exclude', 'excludes', 'excluding',\n 'provide', 'provides', 'providing', 'support', 'supports', 'supporting',\n 'available', 'required', 'optional', 'recommended', 'important', 'please',\n 'however', 'therefore', 'although', 'unless', 'whether', 'either', 'neither',\n 'within', 'without', 'against', 'along', 'among', 'around', 'behind', 'beside',\n 'besides', 'beyond', 'despite', 'except', 'inside', 'outside', 'since', 'toward',\n 'towards', 'upon', 'via', 'already', 'always', 'never', 'often', 'sometimes',\n 'usually', 'actually', 'basically', 'currently', 'especially', 'finally',\n 'generally', 'initially', 'mainly', 'mostly', 'normally', 'originally',\n 'particularly', 'previously', 'primarily', 'probably', 'recently', 'simply',\n 'specifically', 'typically', 'usually', 'directly', 'automatically', 'manually',\n 'different', 'similar', 'specific', 'general', 'common', 'unique', 'various',\n 'multiple', 'single', 'double', 'simple', 'complex', 'basic', 'advanced',\n 'main', 'primary', 'secondary', 'additional', 'extra', 'full', 'complete',\n 'partial', 'total', 'entire', 'whole', 'part', 'parts', 'piece', 'pieces',\n 'way', 'ways', 'time', 'times', 'day', 'days', 'week', 'weeks', 'month', 'months',\n 'year', 'years', 'second', 'seconds', 'minute', 'minutes', 'hour', 'hours',\n 'case', 'cases', 'issue', 'issues', 'problem', 'problems', 'solution', 'solutions',\n 'option', 'options', 'feature', 'features', 'version', 'versions', 'release',\n 'releases', 'guide', 'guides', 'tutorial', 'tutorials', 'documentation', 'docs',\n 'reference', 'references', 'resource', 'resources', 'tool', 'tools', 'service',\n 'services', 'system', 'systems', 'application', 'applications', 'app', 'apps',\n 'project', 'projects', 'code', 'codes', 'script', 'scripts', 'command', 'commands',\n 'parameter', 'parameters', 'argument', 'arguments', 'variable', 'variables',\n 'constant', 'constants', 'property', 'properties', 'attribute', 'attributes',\n 'element', 'elements', 'component', 'components', 'module', 'modules', 'package',\n 'packages', 'library', 'libraries', 'framework', 'frameworks', 'platform',\n 'platforms', 'environment', 'environments', 'configuration', 'configurations',\n 'config', 'configs', 'setup', 'setups', 'installation', 'installations',\n]);\n\n/**\n * Find all MDX files recursively\n */\nfunction findMdxFiles(dir) {\n const files = [];\n if (!fs.existsSync(dir)) return files;\n\n const entries = fs.readdirSync(dir, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n files.push(...findMdxFiles(fullPath));\n } else if (entry.name.endsWith('.mdx') || entry.name.endsWith('.md')) {\n files.push(fullPath);\n }\n }\n return files;\n}\n\n/**\n * Extract text content from MDX (strip JSX/imports/code)\n */\nfunction extractTextContent(content) {\n content = content.replace(/^---[\\s\\S]*?---/, '');\n content = content.replace(/^import\\s+.*$/gm, '');\n content = content.replace(/<[^>]+>/g, ' ');\n content = content.replace(/```[\\s\\S]*?```/g, '');\n content = content.replace(/`[^`]+`/g, '');\n content = content.replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, '$1');\n content = content.replace(/[*_#]+/g, ' ');\n content = content.replace(/https?:\\/\\/[^\\s]+/g, '');\n return content;\n}\n\n/**\n * Extract candidate terms from text using patterns\n */\nfunction extractCandidateTerms(text) {\n const candidates = new Map();\n\n // Pattern 1: Acronyms (2-6 uppercase letters)\n const acronymPattern = /\\b[A-Z]{2,6}\\b/g;\n let match;\n while ((match = acronymPattern.exec(text)) !== null) {\n const term = match[0];\n candidates.set(term, (candidates.get(term) || 0) + 1);\n }\n\n // Pattern 2: Capitalized words (potential proper nouns/terms)\n const capitalizedPattern = /\\b[A-Z][a-z]+(?:\\s+[A-Z][a-z]+)*\\b/g;\n while ((match = capitalizedPattern.exec(text)) !== null) {\n const term = match[0];\n if (term.length > 2) {\n candidates.set(term, (candidates.get(term) || 0) + 1);\n }\n }\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tools/scripts/snippets/generate-data/scripts/data",
+ "type": "directory",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": "tools/scripts/snippets/generate-data/scripts/discovered-terms.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tools/scripts/snippets/generate-data/scripts/data, tools/scripts/snippets/generate-data/scripts/discovered-terms.json",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tools/scripts/snippets/test-scripts.sh",
+ "script": "test-scripts",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": "tools/scripts",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Snippet test runner — runs basic validation on snippet scripts",
+ "pipeline_declared": "manual — diagnostic/investigation tool, run on-demand only",
+ "usage": "bash tools/scripts/snippets/test-scripts.sh [flags]",
+ "header": "#!/bin/bash\n# @script test-scripts\n# @category utility\n# @purpose tooling:dev-tools\n# @scope tools/scripts\n# @owner docs\n# @needs E-C6, F-C1\n# @purpose-statement Snippet test runner — runs basic validation on snippet scripts\n# @pipeline manual — diagnostic/investigation tool, run on-demand only\n# @usage bash tools/scripts/snippets/test-scripts.sh [flags]\n# Test suite for tools/scripts/snippets\n# Run this before using scripts to verify they work correctly\n#\n# Usage: ./tools/scripts/snippets/test-scripts.sh\n#\n# Exit codes:\n# 0 - All tests passed\n# 1 - One or more tests failed\n\nset -e\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\nCONFIG_FILE=\"$SCRIPT_DIR/paths.config.json\"\n\n# Colors for output\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m' # No Color\n\nPASSED=0\nFAILED=0\nSKIPPED=0\n\n# Test result tracking\npass() {\n echo -e \"${GREEN}✓ PASS${NC}: $1\"\n PASSED=$((PASSED + 1))\n}\n\nfail() {\n echo -e \"${RED}✗ FAIL${NC}: $1\"\n FAILED=$((FAILED + 1))\n}\n\nskip() {\n echo -e \"${YELLOW}○ SKIP${NC}: $1\"\n SKIPPED=$((SKIPPED + 1))\n}\n\necho \"========================================\"\necho \" Scripts Test Suite\"\necho \"========================================\"\necho \"\"\n\n# Test 1: Check paths.config.json exists and is valid JSON\necho \"--- Test: paths.config.json ---\"\nif [ -f \"$CONFIG_FILE\" ]; then\n if node -e \"JSON.parse(require('fs').readFileSync('$CONFIG_FILE'))\" 2>/dev/null; then\n pass \"paths.config.json exists and is valid JSON\"\n else\n fail \"paths.config.json is not valid JSON\"\n fi\nelse\n fail \"paths.config.json does not exist\"\nfi\n\n# Test 2: Check all shell scripts have valid syntax\necho \"\"\necho \"--- Test: Shell script syntax ---\"\nfor script in \"$SCRIPT_DIR\"/*.sh; do\n if [ -f \"$script\" ] && [ \"$(basename \"$script\")\" != \"test-scripts.sh\" ]; then\n script_name=$(basename \"$script\")\n if bash -n \"$script\" 2>/dev/null; then\n pass \"$script_name syntax valid\"\n else\n fail \"$script_name syntax error\"\n fi\n fi\ndone\n\n# Test 3: Check Node.js scripts have valid syntax\necho \"\"\necho \"--- Test: Node.js script syntax ---\"\nfor script in \"$SCRIPT_DIR\"/*.js; do\n if [ -f \"$script\" ]; then\n script_name=$(basename \"$script\")\n if node --check \"$script\" 2>/dev/null; then\n pass \"$script_name syntax valid\"\n else\n fail \"$script_name syntax error\"\n fi\n fi\ndone\n\n# Test 4: Check git repo root detection works\necho \"\"\necho \"--- Test: Git repo root detection ---\"\nif git rev-parse --show-toplevel &>/dev/null; then\n REPO_ROOT=\"$(git rev-parse --show-toplevel)\"\n pass \"Git repo root detected: $REPO_ROOT\"\nelse\n skip \"Not in a git repository - testing config fallback\"\nfi\n\n# Test 5: Check required paths from config exist\necho \"\"\necho \"--- Test: Required paths exist ---\"\nif [ -f \"$CONFIG_FILE\" ] && command -v node &>/dev/null; then\n REPO_ROOT=\"${REPO_ROOT:-$(dirname \"$(dirname \"$SCRIPT_DIR\")\")}\"\n \n # Check snippets folder\n SNIPPETS_PATH=\"$REPO_ROOT/$(node -pe \"require('$CONFIG_FILE').paths.snippets\")\"\n if [ -d \"$SNIPPETS_PATH\" ]; then\n pass \"snippets folder exists\"\n else\n fail \"snippets folder missing: $SNIPPETS_PATH\"\n fi\n \n # Check docs.json\n DOCS_JSON=\"$REPO_ROOT/$(node -pe \"require('$CONFIG_FILE').paths.docsJson\")\"\n if [ -f \"$DOCS_JSON\" ]; then\n pass \"docs.json exists\"\n else\n fail \"docs.json missing: $DOCS_JSON\"\n fi\n \n # Check components folder\n COMPONENTS=\"$REPO_ROOT/$(node -pe \"require('$CONFIG_FILE').paths.snippetsComponents\")\"\n if [ -d \"$COMPONENTS\" ]; then\n pass \"snippets/components folder exists\"\n else\n fail \"snippets/components folder missing\"\n fi\nfi\n\n# Test 6: Dry run update-component-library.sh (check it produces output)\necho \"\"\necho \"--- Test: update-component-library.sh dry run ---\"\nOUTPUT=$(bash \"$SCRIPT_DIR/update-component-library.sh\" 2>&1)\nif echo \"$OUTPUT\" | grep -q \"Updated\"; then\n pass \"update-component-library.sh runs successfully\"\nelse\n fail \"update-component-library.sh failed: $OUTPUT\"\nfi\n\n# Test 7: Deprecated generate-docs-status.js (moved out of snippets scope)\necho \"\"\necho \"--- Test: generate-docs-status.js dry run (deprecated) ---\"\nskip \"generate-docs-status.js deprecated and moved to tools/scripts/archive/deprecated/project-management-output-script.js\"\n\n\n# Summary\necho \"\"\necho \"========================================\"\necho \" Test Summary\"\necho \"========================================\"\necho -e \" ${GREEN}Passed${NC}: $PASSED\"\necho -e \" ${RED}Failed${NC}: $FAILED\"\necho -e \" ${YELLOW}Skipped${NC}: $SKIPPED\"\necho \"========================================\"\n\nif [ $FAILED -gt 0 ]; then\n exit 1\nelse\n exit 0\nfi\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ }
+ ],
+ "count": 89
+ }
+ },
+ "scripts": [
+ {
+ "path": ".githooks/install.sh",
+ "script": "install",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": ".githooks",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Installs git hooks by setting core.hooksPath to .githooks/",
+ "pipeline_declared": "manual — developer tool",
+ "usage": "bash .githooks/install.sh [flags]",
+ "header": "#!/bin/bash\n# @script install\n# @category utility\n# @purpose tooling:dev-tools\n# @scope .githooks\n# @owner docs\n# @needs E-C6, F-C1\n# @purpose-statement Installs git hooks by setting core.hooksPath to .githooks/\n# @pipeline manual — developer tool\n# @usage bash .githooks/install.sh [flags]\n# Install git hooks\n\n# Support both regular repos and worktrees\nGIT_COMMON_DIR=$(git rev-parse --git-common-dir 2>/dev/null)\nif [ -z \"$GIT_COMMON_DIR\" ] || [ \"$GIT_COMMON_DIR\" = \"--git-common-dir\" ]; then\n GIT_COMMON_DIR=\".git\"\nfi\nHOOKS_DIR=\"$GIT_COMMON_DIR/hooks\"\nSOURCE_DIR=\".githooks\"\n\nif [ ! -d \"$HOOKS_DIR\" ]; then\n echo \"Error: hooks directory not found at $HOOKS_DIR\"\n exit 1\nfi\n\nif [ ! -d \"$SOURCE_DIR\" ]; then\n echo \"Error: .githooks directory not found. Are you in the repository root?\"\n exit 1\nfi\n\necho \"Installing git hooks...\"\n\n# Install pre-commit hook\nif [ -f \"$SOURCE_DIR/pre-commit\" ]; then\n cp \"$SOURCE_DIR/pre-commit\" \"$HOOKS_DIR/pre-commit\"\n chmod +x \"$HOOKS_DIR/pre-commit\"\n echo \"✓ Installed pre-commit hook\"\nelse\n echo \"✗ pre-commit hook not found in $SOURCE_DIR\"\nfi\n\n# Install pre-push hook\nif [ -f \"$SOURCE_DIR/pre-push\" ]; then\n cp \"$SOURCE_DIR/pre-push\" \"$HOOKS_DIR/pre-push\"\n chmod +x \"$HOOKS_DIR/pre-push\"\n echo \"✓ Installed pre-push hook\"\nelse\n echo \"✗ pre-push hook not found in $SOURCE_DIR\"\nfi\n\necho \"\"\necho \"Git hooks installed successfully!\"\necho \"\"\necho \"The pre-commit hook will now check for style guide violations.\"\necho \"The pre-push hook will enforce codex task contracts on codex/* branches.\"\necho \"See .githooks/README.md for details.\"\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": ".githooks/pre-commit",
+ "script": "pre-commit",
+ "category": "orchestrator",
+ "purpose": "infrastructure:pipeline-orchestration",
+ "scope": ".githooks",
+ "owner": "docs",
+ "needs": "R-R29",
+ "purpose_statement": "Pre-commit hook orchestrator — runs structural checks, unit tests, codex validation, and docs-index freshness check before allowing commit",
+ "pipeline_declared": "P1 (commit, hook entry point)",
+ "usage": "bash .githooks/pre-commit [flags]",
+ "header": "#!/bin/bash\n# @script pre-commit\n# @category orchestrator\n# @purpose infrastructure:pipeline-orchestration\n# @scope .githooks\n# @owner docs\n# @needs R-R29\n# @purpose-statement Pre-commit hook orchestrator — runs structural checks, unit tests, codex validation, and docs-index freshness check before allowing commit\n# @pipeline P1 (commit, hook entry point)\n# @usage bash .githooks/pre-commit [flags]\n# Pre-commit hook to enforce style guide compliance\n# Checks for common violations before allowing commits\n#\n# To install this hook, run:\n# cp .githooks/pre-commit .git/hooks/pre-commit\n# chmod +x .git/hooks/pre-commit\n#\n# Bypass flags (use sparingly):\n# SKIP_STRUCTURE_CHECK=1 - Skip root directory and snippets structure checks\n# SKIP_STYLE_CHECK=1 - Skip style guide compliance checks\n# SKIP_VERIFICATION=1 - Skip verification scripts\n# SKIP_TESTS=1 - Skip test suite\n# SKIP_ALL=1 - Skip all checks (use with extreme caution)\n#\n# Human-only override flags:\n# --trailer \"allowlist-edit=true\" - Allow editing .allowlist file (HUMANS ONLY - AIs must never use this)\n# ALLOWLIST_EDIT=1 - Legacy fallback for automation contexts\n# --trailer \"allow-deletions=true\" - Allow file deletions (HUMANS ONLY - AIs must never use this)\n# ALLOW_DELETIONS=1 - Legacy fallback for automation contexts\n\nSTYLE_GUIDE_PATH=\"v2/pages/07_resources/documentation-guide/style-guide.mdx\"\nVIOLATIONS=0\nWARNINGS=()\n\n# Colors for output\nRED='\\033[0;31m'\nYELLOW='\\033[1;33m'\nGREEN='\\033[0;32m'\nNC='\\033[0m' # No Color\n\n# Check for bypass flags\nSKIP_STRUCTURE_CHECK=${SKIP_STRUCTURE_CHECK:-0}\nSKIP_STYLE_CHECK=${SKIP_STYLE_CHECK:-0}\nSKIP_VERIFICATION=${SKIP_VERIFICATION:-0}\nSKIP_TESTS=${SKIP_TESTS:-0}\nSKIP_ALL=${SKIP_ALL:-0}\nALLOW_DELETIONS=${ALLOW_DELETIONS:-0}\nCURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo \"\")\n\n# Freeze staged files snapshot to avoid hook-induced staging changes.\nSTAGED_FILES_SNAPSHOT=$(git diff --cached --name-only --diff-filter=ACMR 2>/dev/null || true)\nSTAGED_FILES_SNAPSHOT_ALL=$(git diff --cached --name-only --diff-filter=ACMRD 2>/dev/null || true)\nSTAGED_FILES_SNAPSHOT_ADDED=$(git diff --cached --name-only --diff-filter=A 2>/dev/null || true)\nSTAGED_FILES_SNAPSHOT_DELETED=$(git diff --cached --name-only --diff-filter=D 2>/dev/null || true)\nexport LPD_STAGED_FILES_SNAPSHOT=\"$STAGED_FILES_SNAPSHOT\"\n\nget_staged_docs_pages() {\n if ! command -v node &>/dev/null || [ ! -f \"tests/utils/file-walker.js\" ]; then\n return 0\n fi\n\n node -e \"const path = require('path'); const { getStagedDocsPageFiles } = require('./tests/utils/file-walker'); const root = process.cwd(); const files = getStagedDocsPageFiles(root).map((filePath) => path.relative(root, filePath).split(path.sep).join('/')); process.stdout.write(files.join('\\n'));\"\n}\n\nSTAGED_DOCS_PAGES=$(get_staged_docs_pages)\n# pre-commit hooks do not receive commit args directly. Detect trailer usage\n# from the parent git commit command.\nPARENT_GIT_CMD=$(ps -o command= -p \"$PPID\" 2>/dev/null || true)\n\nhas_human_override_trailer() {\n local trailer_key=\"$1\"\n case \"$PARENT_GIT_CMD\" in\n *\"--trailer ${trailer_key}\"*|*\"--trailer=${trailer_key}\"*|*\"--trailer \\\"${trailer_key}\"*|*\"--trailer '${trailer_key}\"*)\n return 0\n ;;\n esac\n return 1\n}\n\nis_codex_session() {\n if [[ \"${CODEX_SHELL:-0}\" == \"1\" ]]; then\n return 0\n fi\n\n if [[ -n \"${CODEX_THREAD_ID:-}\" ]]; then\n return 0\n fi\n\n if [[ \"${CODEX_CI:-0}\" == \"1\" ]]; then\n return 0\n fi\n\n if [[ -n \"${CODEX_INTERNAL_ORIGINATOR_OVERRIDE:-}\" ]]; then\n return 0\n fi\n\n return 1\n}\n\nallow_docs_v2_commit_override() {\n if [[ \"${ALLOW_MAIN_COMMIT:-0}\" == \"1\" ]]; then\n return 0\n fi\n\n if has_human_override_trailer \"allow-main-commit=true\"; then\n return 0\n fi\n\n return 1\n}\n\nenforce_docs_v2_codex_isolation() {\n if [[ \"$CURRENT_BRANCH\" != \"docs-v2\" ]]; then\n return 0\n fi\n\n if ! is_codex_session; then\n return 0\n fi\n\n if allow_docs_v2_commit_override; then\n echo -e \"${YELLOW}⚠️ docs-v2 commit override detected (human-authorized).${NC}\"\n return 0\n fi\n\n echo -e \"${RED}╔═══════════════════════════════════════════════════════════════╗${NC}\"\n echo -e \"${RED}║ ❌ COMMIT BLOCKED: CODEX SESSION ON docs-v2 ║${NC}\"\n echo -e \"${RED}╚═══════════════════════════════════════════════════════════════╝${NC}\"\n echo \"\"\n echo -e \"${YELLOW}Do not stash/reset. Preserve current staged+unstaged edits by branching in-place:${NC}\"\n echo \" git switch -c codex/-\"\n echo \" # if branch exists already: git switch codex/-\"\n echo \"\"\n echo -e \"${YELLOW}Then initialize contract + lock:${NC}\"\n echo \" node tools/scripts/codex/task-preflight.js --task --slug --scope \\\"\\\"\"\n echo \"\"\n echo -e \"${YELLOW}Human override (explicit chat instruction required):${NC}\"\n echo \" ALLOW_MAIN_COMMIT=1 git commit -m \\\"...\\\" --trailer \\\"allow-main-commit=true\\\"\"\n echo \"\"\n return 1\n}\n\nrun_codex_task_contract_check() {\n if [[ \"$CURRENT_BRANCH\" != codex/* ]]; then\n return 0\n fi\n\n echo -e \"${YELLOW}🔍 Validating codex task contract...${NC}\"\n if node tools/scripts/validate-codex-task-contract.js --branch \"$CURRENT_BRANCH\" --validate-contract-only --quiet; then\n echo -e \"${GREEN}✓ Codex task contract check passed${NC}\"\n return 0\n fi\n\n VIOLATIONS=$((VIOLATIONS + 1))\n WARNINGS+=(\"❌ Codex task contract validation failed\")\n echo -e \"${RED}❌ Codex task contract validation failed${NC}\"\n return 1\n}\n\nrun_codex_lock_check() {\n if [[ \"$CURRENT_BRANCH\" != codex/* ]]; then\n return 0\n fi\n\n local script_path=\"tools/scripts/codex/validate-locks.js\"\n if [ ! -f \"$script_path\" ]; then\n WARNINGS+=(\"❌ Missing codex lock validator: $script_path\")\n VIOLATIONS=$((VIOLATIONS + 1))\n echo -e \"${RED}❌ Missing codex lock validator: $script_path${NC}\"\n return 1\n fi\n\n echo -e \"${YELLOW}🔍 Validating codex local lock ownership...${NC}\"\n if node \"$script_path\" --branch \"$CURRENT_BRANCH\" --staged --quiet; then\n echo -e \"${GREEN}✓ Codex local lock check passed${NC}\"\n return 0\n fi\n\n WARNINGS+=(\"❌ Codex local lock validation failed\")\n VIOLATIONS=$((VIOLATIONS + 1))\n echo -e \"${RED}❌ Codex local lock validation failed${NC}\"\n return 1\n}\n\nrun_ai_stash_policy_check() {\n local script_path=\"tools/scripts/check-no-ai-stash.sh\"\n if [ ! -x \"$script_path\" ]; then\n WARNINGS+=(\"❌ Missing executable guard script: $script_path\")\n VIOLATIONS=$((VIOLATIONS + 1))\n echo -e \"${RED}❌ Missing executable guard script: $script_path${NC}\"\n return 1\n fi\n\n echo -e \"${YELLOW}🔍 Enforcing AI stash policy...${NC}\"\n if bash \"$script_path\" --quiet; then\n echo -e \"${GREEN}✓ AI stash policy check passed${NC}\"\n return 0\n fi\n\n VIOLATIONS=$((VIOLATIONS + 1))\n WARNINGS+=(\"❌ AI stash policy violation: stash-based isolation detected\")\n echo -e \"${RED}❌ AI stash policy violation: stash-based isolation detected${NC}\"\n echo -e \"${YELLOW} Required workflow: branch + WIP commit checkpoints (no git stash).${NC}\"\n return 1\n}\n\nshould_enforce_ai_stash_policy() {\n if [[ \"$CURRENT_BRANCH\" == codex/* ]]; then\n return 0\n fi\n\n return 1\n}\n\nget_git_blob_ref() {\n local object_ref=\"$1\"\n git rev-parse --verify \"$object_ref\" 2>/dev/null || true\n}\n\nget_archive_followup_target() {",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "pre-commit",
+ "caller": ".githooks/pre-commit",
+ "pipeline": "P1"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 (pre-commit)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": ".githooks/pre-commit-no-deletions",
+ "script": "pre-commit-no-deletions",
+ "category": "orchestrator",
+ "purpose": "infrastructure:pipeline-orchestration",
+ "scope": ".githooks",
+ "owner": "docs",
+ "needs": "R-R29",
+ "purpose_statement": "Variant pre-commit hook that blocks file deletions (safety net for content preservation)",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "bash .githooks/pre-commit-no-deletions [flags]",
+ "header": "#!/bin/bash\n# @script pre-commit-no-deletions\n# @category orchestrator\n# @purpose infrastructure:pipeline-orchestration\n# @scope .githooks\n# @owner docs\n# @needs R-R29\n# @purpose-statement Variant pre-commit hook that blocks file deletions (safety net for content preservation)\n# @pipeline manual — not yet in pipeline\n# @usage bash .githooks/pre-commit-no-deletions [flags]\n# Pre-commit hook: PREVENT DELETIONS\n# This hook blocks any file deletions to prevent accidental data loss\n\n# Get list of deleted files\nDELETED_FILES=$(git diff --cached --name-only --diff-filter=D)\nBLOCKED_DELETED_FILES=$(echo \"$DELETED_FILES\" | grep -Ev '^snippets/data/[^/]+/hrefs\\.jsx$' || true)\nALLOWED_GENERATED_DELETIONS=$(echo \"$DELETED_FILES\" | grep -E '^snippets/data/[^/]+/hrefs\\.jsx$' || true)\n\nif [ ! -z \"$ALLOWED_GENERATED_DELETIONS\" ]; then\n echo \"\"\n echo \"ℹ️ Allowing generated domain link map deletions:\"\n echo \"$ALLOWED_GENERATED_DELETIONS\" | sed 's/^/ ↺ /'\nfi\n\nif [ ! -z \"$BLOCKED_DELETED_FILES\" ]; then\n echo \"\"\n echo \"╔═══════════════════════════════════════════════════════════════╗\"\n echo \"║ ❌ FILE DELETIONS DETECTED - COMMIT BLOCKED ║\"\n echo \"╚═══════════════════════════════════════════════════════════════╝\"\n echo \"\"\n echo \"🚫 DELETIONS ARE NOT ALLOWED\"\n echo \"\"\n echo \"The following files are being deleted:\"\n echo \"$BLOCKED_DELETED_FILES\" | sed 's/^/ ❌ /'\n echo \"\"\n echo \"📋 INSTRUCTIONS:\"\n echo \" 1. If you need to move files, use 'git mv' instead of delete+add\"\n echo \" 2. If files should be removed, use 'git rm --cached' to unstage\"\n echo \" 3. If this is intentional, use: git commit --no-verify\"\n echo \" (But this should be VERY rare - deletions are dangerous!)\"\n echo \"\"\n echo \"⚠️ If you really need to delete files, you must:\"\n echo \" 1. Document WHY in the commit message\"\n echo \" 2. Verify files are backed up\"\n echo \" 3. Use: git commit --no-verify\"\n echo \"\"\n exit 1\nfi\n\nexit 0\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": ".githooks/pre-push",
+ "script": "pre-push",
+ "category": "orchestrator",
+ "purpose": "infrastructure:pipeline-orchestration",
+ "scope": ".githooks, tools/scripts/validate-codex-task-contract.js, .codex/task-contract.yaml",
+ "owner": "docs",
+ "needs": "R-R29",
+ "purpose_statement": "Pre-push hook — blocks push if AI stash files present, codex locks stale, or task contract invalid",
+ "pipeline_declared": "P2 (push, hook entry point)",
+ "usage": "bash .githooks/pre-push [flags]",
+ "header": "#!/bin/bash\n# @script pre-push\n# @category orchestrator\n# @purpose infrastructure:pipeline-orchestration\n# @scope .githooks, tools/scripts/validate-codex-task-contract.js, .codex/task-contract.yaml\n# @owner docs\n# @needs R-R29\n# @purpose-statement Pre-push hook — blocks push if AI stash files present, codex locks stale, or task contract invalid\n# @pipeline P2 (push, hook entry point)\n# @usage bash .githooks/pre-push [flags]\nset -u\n\nRED='\\033[0;31m'\nYELLOW='\\033[1;33m'\nGREEN='\\033[0;32m'\nNC='\\033[0m'\n\nCURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo \"\")\nALLOW_CODEX_FORCE_PUSH=\"${ALLOW_CODEX_FORCE_PUSH:-0}\"\nALLOW_MAIN_PUSH=\"${ALLOW_MAIN_PUSH:-0}\"\n\n# Read stdin first so we can inspect all pushed refs while still validating branch scope.\nPUSH_LINES=$(cat)\n\nis_codex_session() {\n if [[ \"${CODEX_SHELL:-0}\" == \"1\" ]]; then\n return 0\n fi\n\n if [[ -n \"${CODEX_THREAD_ID:-}\" ]]; then\n return 0\n fi\n\n if [[ \"${CODEX_CI:-0}\" == \"1\" ]]; then\n return 0\n fi\n\n if [[ -n \"${CODEX_INTERNAL_ORIGINATOR_OVERRIDE:-}\" ]]; then\n return 0\n fi\n\n return 1\n}\n\nTARGETS_DOCS_V2=0\nwhile IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do\n if [[ -z \"${local_ref:-}\" ]]; then\n continue\n fi\n\n if [[ \"${remote_ref:-}\" == \"refs/heads/docs-v2\" ]]; then\n TARGETS_DOCS_V2=1\n break\n fi\ndone <<< \"$PUSH_LINES\"\n\nif [[ \"$TARGETS_DOCS_V2\" -eq 1 ]] && is_codex_session; then\n if [[ \"$ALLOW_MAIN_PUSH\" != \"1\" ]]; then\n echo -e \"${RED}❌ Push blocked: Codex sessions must not push directly to docs-v2.${NC}\"\n echo -e \"${YELLOW}Use codex/- branches and open a PR.${NC}\"\n echo -e \"${YELLOW}Do not stash/reset to switch. Keep work in place with:${NC}\"\n echo \" git switch -c codex/-\"\n echo -e \"${YELLOW}Human override (explicit chat instruction required):${NC}\"\n echo \" ALLOW_MAIN_PUSH=1 git push origin :docs-v2\"\n exit 1\n fi\n\n echo -e \"${YELLOW}⚠️ ALLOW_MAIN_PUSH=1 set; overriding docs-v2 push block for this push.${NC}\"\nfi\n\nif [[ \"$CURRENT_BRANCH\" != codex/* ]]; then\n exit 0\nfi\n\necho -e \"${YELLOW}🔍 Running codex pre-push enforcement for ${CURRENT_BRANCH}${NC}\"\n\nif ! node tools/scripts/validate-codex-task-contract.js --branch \"$CURRENT_BRANCH\" --quiet; then\n echo -e \"${RED}❌ Codex task contract validation failed. Push blocked.${NC}\"\n exit 1\nfi\n\nif [ -f \"tools/scripts/codex/validate-locks.js\" ]; then\n if ! node tools/scripts/codex/validate-locks.js --branch \"$CURRENT_BRANCH\" --quiet; then\n echo -e \"${RED}❌ Codex local lock validation failed. Push blocked.${NC}\"\n exit 1\n fi\nelse\n echo -e \"${RED}❌ Missing codex lock validator: tools/scripts/codex/validate-locks.js${NC}\"\n exit 1\nfi\n\nif [ -x \"tools/scripts/check-no-ai-stash.sh\" ]; then\n if ! bash tools/scripts/check-no-ai-stash.sh --branch \"$CURRENT_BRANCH\" --quiet; then\n echo -e \"${RED}❌ AI stash policy violation detected. Push blocked.${NC}\"\n exit 1\n fi\nfi\n\nBLOCKED=0\n\nwhile IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do\n if [[ -z \"${local_ref:-}\" ]]; then\n continue\n fi\n\n if [[ \"${remote_ref:-}\" != \"refs/heads/${CURRENT_BRANCH}\" ]]; then\n continue\n fi\n\n if [[ \"${local_sha:-}\" =~ ^0+$ ]]; then\n echo -e \"${RED}❌ Deleting codex branches by push is blocked: ${remote_ref}${NC}\"\n BLOCKED=1\n continue\n fi\n\n # New remote branch creation is always a fast-forward push.\n if [[ \"${remote_sha:-}\" =~ ^0+$ ]]; then\n continue\n fi\n\n if ! git merge-base --is-ancestor \"$remote_sha\" \"$local_sha\" >/dev/null 2>&1; then\n echo -e \"${RED}❌ Non-fast-forward push detected for ${remote_ref}${NC}\"\n BLOCKED=1\n fi\ndone <<< \"$PUSH_LINES\"\n\nif [[ \"$BLOCKED\" -eq 1 ]]; then\n if [[ \"$ALLOW_CODEX_FORCE_PUSH\" == \"1\" ]]; then\n echo -e \"${YELLOW}⚠️ ALLOW_CODEX_FORCE_PUSH=1 set; overriding codex non-fast-forward block.${NC}\"\n exit 0\n fi\n echo -e \"${RED}Push blocked. Use a fast-forward push on codex/* branches.${NC}\"\n echo -e \"${YELLOW}If an override is explicitly approved, set ALLOW_CODEX_FORCE_PUSH=1 for that push.${NC}\"\n exit 1\nfi\n\necho -e \"${GREEN}✓ Codex pre-push checks passed${NC}\"\nexit 0\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "pre-push",
+ "caller": ".githooks/pre-push",
+ "pipeline": "P2"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P2 (pre-push)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P2"
+ },
+ {
+ "path": ".githooks/server-manager.js",
+ "script": "server-manager",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": ".githooks",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Manages Mintlify dev server lifecycle for browser tests (start/stop/health-check)",
+ "pipeline_declared": "indirect — legacy browser-validation module imported by .githooks/verify-browser.js",
+ "usage": "node .githooks/server-manager.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script server-manager\n * @category utility\n * @purpose tooling:dev-tools\n * @scope .githooks\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement Manages Mintlify dev server lifecycle for browser tests (start/stop/health-check)\n * @pipeline indirect — legacy browser-validation module imported by .githooks/verify-browser.js\n * @usage node .githooks/server-manager.js [flags]\n */\n/**\n * Server management utility for browser tests\n * Automatically starts mint dev if not running and manages the process lifecycle\n */\n\nconst { spawn, execSync } = require('child_process');\nconst http = require('http');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\n\n// Use a dedicated port for browser validation tests (unlikely to be in use)\nconst TEST_PORT = 3145;\nconst BASE_URL = process.env.MINT_BASE_URL || `http://localhost:${TEST_PORT}`;\nconst PORT = new URL(BASE_URL).port || TEST_PORT;\nconst PID_FILE = path.join(os.tmpdir(), 'mint-dev-test.pid');\nconst LOG_FILE = path.join(os.tmpdir(), 'mint-dev-test.log');\n\nlet serverProcess = null;\nlet serverStartedByUs = false;\nlet actualServerUrl = BASE_URL; // Will be updated if port is detected from log\nlet detectedServerPort = null; // Port where server was actually found\n\n/**\n * Check if server is already running (on expected port, detected port, or common ports)\n */\nasync function isServerRunning(options = {}) {\n const { probePath, allowCommonPorts = true } = options;\n // Check expected port first (3145)\n if (await isServerRunningOnPort(PORT, probePath)) {\n return true;\n }\n \n // Check common mint dev ports (3000, 3001, 3002, etc.)\n // Mint dev often uses these ports if 3000 is in use\n if (allowCommonPorts) {\n for (let commonPort = 3000; commonPort <= 3010; commonPort++) {\n if (await isServerRunningOnPort(commonPort, probePath)) {\n // Found server on common port - store it for getServerUrl()\n detectedServerPort = commonPort;\n console.log(` Found existing server on port ${commonPort}, using it`);\n return true;\n }\n }\n }\n \n // Check if log shows server on different port\n if (allowCommonPorts) {\n const detectedPort = detectPortFromLog();\n if (detectedPort && detectedPort !== PORT) {\n return await isServerRunningOnPort(detectedPort, probePath);\n }\n }\n \n return false;\n}\n\n/**\n * Parse log file to detect actual port mint dev is using\n * Looks for patterns like \"local → http://localhost:3001\" or \"port 3000 is already in use. trying 3001 instead\"\n */\nfunction detectPortFromLog() {\n if (!fs.existsSync(LOG_FILE)) {\n return null;\n }\n \n try {\n const logContent = fs.readFileSync(LOG_FILE, 'utf8');\n \n const lastMatch = (regex) => {\n const matches = [...logContent.matchAll(regex)];\n if (!matches.length) return null;\n const last = matches[matches.length - 1];\n return last && last[1] ? parseInt(last[1], 10) : null;\n };\n \n // Pattern 1: \"local → http://localhost:XXXX\"\n const localPort = lastMatch(/local\\s*→\\s*http:\\/\\/localhost:(\\d+)/gi);\n if (localPort) {\n return localPort;\n }\n \n // Pattern 2: \"port XXXX is already in use. trying YYYY instead\"\n const portPort = lastMatch(/port\\s+\\d+\\s+is\\s+already\\s+in\\s+use\\.\\s+trying\\s+(\\d+)\\s+instead/gi);\n if (portPort) {\n return portPort;\n }\n \n // Pattern 3: \"preview ready\" followed by port info\n const previewPort = lastMatch(/preview\\s+ready[^\\n]*localhost:(\\d+)/gi);\n if (previewPort) {\n return previewPort;\n }\n } catch (e) {\n // Ignore errors reading log\n }\n \n return null;\n}\n\n/**\n * Check if server is running on a specific port\n */\nfunction normalizeProbePath(probePath) {\n if (!probePath) {\n return '';\n }\n return probePath.startsWith('/') ? probePath : `/${probePath}`;\n}\n\nasync function isServerRunningOnPort(port, probePath) {\n const pathSuffix = normalizeProbePath(probePath);\n const url = `http://localhost:${port}${pathSuffix}`;\n return new Promise((resolve) => {\n const req = http.get(url, { timeout: 2000 }, (res) => {\n if (!Number.isInteger(res.statusCode)) {\n resolve(false);\n return;\n }\n if (pathSuffix) {\n // Treat 404 on probe paths as a mismatch (not the expected server).\n resolve(res.statusCode !== 404);\n return;\n }\n // Any HTTP response means the server is up (Mint may return redirects during startup).\n resolve(res.statusCode > 0);\n });\n \n req.on('error', () => resolve(false));\n req.on('timeout', () => {\n req.destroy();\n resolve(false);\n });\n });\n}\n\n/**\n * Wait for server to be ready, checking expected port, common ports, and detected port from log\n */\nasync function waitForServer(maxAttempts = 60, interval = 2000, options = {}) {\n const { probePath, allowCommonPorts = true } = options;\n for (let i = 0; i < maxAttempts; i++) {\n // First check expected port (3145)\n if (await isServerRunningOnPort(PORT, probePath)) {\n return true;\n }\n \n // Check common ports (3000-3010) - mint dev often uses these if 3145 is unavailable\n if (allowCommonPorts) {\n for (let commonPort = 3000; commonPort <= 3010; commonPort++) {\n if (await isServerRunningOnPort(commonPort, probePath)) {\n detectedServerPort = commonPort;\n console.log(` Server started on port ${commonPort} (expected ${PORT})`);\n return true;\n }\n }\n }\n \n // If not on expected or common ports, try to detect from log (after a few attempts to let log populate)\n if (allowCommonPorts && i >= 3) {\n const detectedPort = detectPortFromLog();\n if (detectedPort && detectedPort !== PORT) {\n // Check detected port\n if (await isServerRunningOnPort(detectedPort, probePath)) {\n detectedServerPort = detectedPort;\n console.log(` Server detected on port ${detectedPort} from log (expected ${PORT})`);\n return true;\n }\n }\n }\n \n if (i < maxAttempts - 1) {\n await new Promise(resolve => setTimeout(resolve, interval));\n }\n }\n return false;\n}\n\n/**\n * Start mint dev server\n */\nfunction startServer() {\n // Check if already running from a previous test\n if (fs.existsSync(PID_FILE)) {\n try {\n const existingPid = parseInt(fs.readFileSync(PID_FILE, 'utf8').trim());\n // Check if process is still running\n try {\n process.kill(existingPid, 0); // Signal 0 just checks if process exists\n console.log(`⚠️ Found existing mint dev process (PID: ${existingPid}), reusing...`);\n serverStartedByUs = false;\n return existingPid;\n } catch (e) {\n // Process doesn't exist, remove stale PID file\n fs.unlinkSync(PID_FILE);\n }\n } catch (e) {\n // Ignore errors reading PID file\n }\n }\n\n console.log(`🚀 Starting mint dev server on port ${PORT}...`);\n \n // Start mint dev in background with specific port via environment variable\n // Use 'pipe' instead of WriteStream directly to avoid stdio issues\n serverProcess = spawn('mint', ['dev', '--port', PORT.toString(), '--no-open'], {\n stdio: ['ignore', 'pipe', 'pipe'],\n detached: true,",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": ".githooks/verify-browser.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/integration/browser.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/integration/mdx-component-runtime-smoke.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/integration/v2-wcag-audit.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": ".githooks/mint-dev-test.log",
+ "type": "generated-output",
+ "call": "createWriteStream"
+ },
+ {
+ "output_path": ".githooks/mint-dev-test.pid",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": ".githooks/mint-dev-test.log, .githooks/mint-dev-test.pid",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via .githooks/verify-browser.js; indirect via tests/integration/browser.test.js; indirect via tests/integration/mdx-component-runtime-smoke.js; indirect via tests/integration/v2-wcag-audit.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": ".githooks/verify-browser.js",
+ "script": "verify-browser",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": ".githooks",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Verifies browser availability for Puppeteer-based integration tests",
+ "pipeline_declared": "manual — legacy browser validator invoked by .githooks/verify.sh when run directly",
+ "usage": "node .githooks/verify-browser.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script verify-browser\n * @category utility\n * @purpose tooling:dev-tools\n * @scope .githooks\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement Verifies browser availability for Puppeteer-based integration tests\n * @pipeline manual — legacy browser validator invoked by .githooks/verify.sh when run directly\n * @usage node .githooks/verify-browser.js [flags]\n */\n/**\n * Headless browser validation for staged MDX files\n * Tests that MDX files actually render in the browser without errors\n * \n * This script:\n * 1. Extracts staged MDX files\n * 2. Converts file paths to URLs\n * 3. Tests each page in headless browser\n * 4. Reports console errors, page errors, and render failures\n */\n\nconst { execSync } = require('child_process');\nconst path = require('path');\nconst fs = require('fs');\nconst { getStagedDocsPageFiles } = require('../tests/utils/file-walker');\n\n// Find puppeteer in tools/node_modules, tests/node_modules, or root node_modules\nlet puppeteer;\nconst possiblePaths = [\n path.join(__dirname, '..', 'tools', 'node_modules', 'puppeteer'),\n path.join(__dirname, '..', 'tests', 'node_modules', 'puppeteer'),\n path.join(__dirname, '..', 'node_modules', 'puppeteer')\n];\n\nfor (const puppeteerPath of possiblePaths) {\n if (fs.existsSync(puppeteerPath)) {\n puppeteer = require(puppeteerPath);\n break;\n }\n}\n\nif (!puppeteer) {\n console.error('❌ Puppeteer not found. Install dependencies: cd tools && npm install');\n process.exit(1);\n}\n\nconst { ensureServerRunning, stopServer, getServerUrl } = require('./server-manager');\n\n// Use server-manager's detected URL, or fall back to environment variable or default\n// getServerUrl() will return the actual port mint dev is using (may differ from 3145 if port is in use)\nconst BASE_URL = process.env.MINT_BASE_URL || 'http://localhost:3145';\nconst TIMEOUT = 15000; // 15 seconds per page (faster for pre-commit)\nconst MAX_PAGES = 10; // Limit to 10 pages for pre-commit speed\n\n/**\n * Get staged MDX files from git\n */\nfunction toPosix(filePath) {\n return String(filePath || '').split(path.sep).join('/');\n}\n\nfunction getStagedMdxFiles() {\n try {\n const repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n const files = getStagedDocsPageFiles(repoRoot)\n .map((filePath) => toPosix(path.relative(repoRoot, filePath)))\n .filter((filePath) => filePath.endsWith('.mdx') && filePath.startsWith('v2/'))\n .slice(0, MAX_PAGES); // Limit for speed\n \n return files;\n } catch (error) {\n return [];\n }\n}\n\n/**\n * Build candidate URLs for a staged MDX file path.\n * Try legacy and current route patterns because local Mint dev routing\n * can differ from production path handling during migrations.\n */\nfunction filePathToUrls(filePath) {\n const withoutExt = filePath.replace(/\\.mdx$/, '');\n const routeWithoutPrefix = withoutExt\n .replace(/^v2\\/pages\\//, '')\n .replace(/^v2\\//, '');\n const normalizedRoute = routeWithoutPrefix.replace(/\\/index$/, '');\n\n const candidates = [\n `/${normalizedRoute}`,\n `/${withoutExt.replace(/^v2\\//, '').replace(/\\/index$/, '')}`,\n `/v2/${normalizedRoute}`,\n `/v2/pages/${normalizedRoute}`\n ];\n\n return [...new Set(candidates)];\n}\n\n/**\n * Test a single page in headless browser\n */\nasync function testPage(browser, filePath, baseUrl) {\n const candidateUrls = filePathToUrls(filePath);\n const page = await browser.newPage();\n \n const errors = [];\n const warnings = [];\n \n // Known false positives from test scripts and Mintlify build artifacts\n const isTestScriptArtifact = (message) => {\n const testArtifacts = [\n 'require is not defined',\n 'puppeteer',\n 'fs has already been declared',\n 'getMdxFiles',\n 'validateMdx',\n 'execSync',\n 'path',\n 'Unexpected token \\'export\\'',\n 'await is only valid',\n 'appendChild',\n 'Identifier \\'',\n 'has already been declared'\n ];\n return testArtifacts.some(artifact => message.toLowerCase().includes(artifact.toLowerCase()));\n };\n \n // Listen for console errors\n page.on('console', msg => {\n const type = msg.type();\n const text = msg.text();\n \n // Filter out common non-critical warnings\n const ignoredWarnings = [\n 'favicon',\n 'sourcemap',\n 'deprecated',\n 'experimental'\n ];\n \n if (type === 'error') {\n // Check if this is a known test script artifact\n if (isTestScriptArtifact(text)) {\n if (text.includes('require is not defined')) {\n warnings.push(`⚠️ ${text} (Likely cause: Mintlify build artifact - does not affect page functionality)`);\n } else {\n warnings.push(`⚠️ ${text} (Likely cause: Test script artifact - does not affect page functionality)`);\n }\n } else if (!text.includes('favicon') && !text.includes('sourcemap')) {\n errors.push(text);\n }\n } else if (type === 'warning' && !ignoredWarnings.some(ignored => text.toLowerCase().includes(ignored))) {\n warnings.push(text);\n }\n });\n \n // Listen for page errors\n page.on('pageerror', error => {\n const errorMessage = error.message;\n \n // Check if this is a known test script artifact\n if (isTestScriptArtifact(errorMessage)) {\n if (errorMessage.includes('require is not defined')) {\n warnings.push(`⚠️ Page Error: ${errorMessage} (Likely cause: Mintlify build artifact - does not affect page functionality)`);\n } else {\n warnings.push(`⚠️ Page Error: ${errorMessage} (Likely cause: Test script artifact - does not affect page functionality)`);\n }\n } else {\n errors.push(`Page Error: ${errorMessage}`);\n }\n });\n \n // Listen for request failures (but ignore some)\n page.on('requestfailed', request => {\n const failure = request.failure();\n const url = request.url();\n \n // Ignore favicon and other non-critical failures\n if (failure && !url.includes('favicon') && !url.includes('sourcemap')) {\n // Only report if it's a critical resource\n if (url.includes('/snippets/') || url.includes('/v2/')) {\n errors.push(`Request Failed: ${url} - ${failure.errorText}`);\n }\n }\n });\n \n const attempted = [];\n\n try {\n for (const candidateUrl of candidateUrls) {\n const fullUrl = `${baseUrl}${candidateUrl}`;\n attempted.push(fullUrl);\n errors.length = 0;\n warnings.length = 0;\n\n try {\n await page.goto(fullUrl, {\n waitUntil: 'networkidle2',\n timeout: TIMEOUT\n });\n\n await new Promise(resolve => setTimeout(resolve, 1000));\n\n const bodyText = await page.evaluate(() => document.body.innerText);\n if (!bodyText || bodyText.trim().length < 50) {\n errors.push('Page appears to be empty or failed to render');\n }\n\n const hasError = await page.evaluate(() => {\n return document.querySelector('[data-error-boundary]') !== null ||\n document.body.innerText.includes('Error:') ||\n document.body.innerText.includes('Failed to render');\n });\n\n if (hasError) {\n errors.push('Page contains render errors');\n }\n\n if (errors.length === 0) {",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": ".githooks/verify.sh",
+ "script": "verify",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": ".githooks",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Pre-commit sub-hook — verifies file-walker is available and runs structural checks on staged files",
+ "pipeline_declared": "manual — legacy pre-commit sub-hook retained for on-demand verification",
+ "usage": "bash .githooks/verify.sh [flags]",
+ "header": "#!/bin/bash\n# @script verify\n# @category utility\n# @purpose tooling:dev-tools\n# @scope .githooks\n# @owner docs\n# @needs E-C6, F-C1\n# @purpose-statement Pre-commit sub-hook — verifies file-walker is available and runs structural checks on staged files\n# @pipeline manual — legacy pre-commit sub-hook retained for on-demand verification\n# @usage bash .githooks/verify.sh [flags]\n# Verification script for pre-commit hook\n# Runs various validation checks on staged files\n\nset -e\n\nREPO_ROOT=\"$(git rev-parse --show-toplevel 2>/dev/null || pwd)\"\ncd \"$REPO_ROOT\"\n\nRED='\\033[0;31m'\nYELLOW='\\033[1;33m'\nGREEN='\\033[0;32m'\nNC='\\033[0m'\n\nVIOLATIONS=0\nWARNINGS=()\n\necho -e \"${YELLOW}🔍 Running verification checks...${NC}\"\n\n# Get staged files\nSTAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)\n\nget_staged_docs_pages() {\n if ! command -v node &>/dev/null || [ ! -f \"tests/utils/file-walker.js\" ]; then\n return 0\n fi\n\n node -e \"const path = require('path'); const { getStagedDocsPageFiles } = require('./tests/utils/file-walker'); const root = process.cwd(); const files = getStagedDocsPageFiles(root).map((filePath) => path.relative(root, filePath).split(path.sep).join('/')); process.stdout.write(files.join('\\n'));\"\n}\n\nSTAGED_DOCS_PAGES=$(get_staged_docs_pages)\n\nif [ -z \"$STAGED_FILES\" ]; then\n echo -e \"${GREEN}✓ No files staged${NC}\"\n exit 0\nfi\n\n# Check 1: MDX syntax validation (basic)\necho \"Checking MDX syntax...\"\nif [ -n \"$STAGED_DOCS_PAGES\" ]; then\n MDX_FILES=$(echo \"$STAGED_DOCS_PAGES\" | grep -E '\\.mdx$' || true)\nelse\n MDX_FILES=$(echo \"$STAGED_FILES\" | grep -E '\\.mdx$' || true)\nfi\nif [ -n \"$MDX_FILES\" ]; then\n for file in $MDX_FILES; do\n if [ -f \"$file\" ]; then\n # Basic check: ensure frontmatter is valid YAML\n if head -n 1 \"$file\" | grep -q \"^---[[:space:]]*$\"; then\n # Check if frontmatter closes properly\n FRONTMATTER_LINES=$(head -n 50 \"$file\" | grep -n \"^---[[:space:]]*$\" | head -2 | cut -d: -f1)\n if [ -z \"$FRONTMATTER_LINES\" ] || [ \"$(echo \"$FRONTMATTER_LINES\" | wc -l)\" -lt 2 ]; then\n WARNINGS+=(\"⚠️ $file: Frontmatter may be malformed\")\n VIOLATIONS=$((VIOLATIONS + 1))\n fi\n fi\n fi\n done\nfi\n\n# Check 2: JSON syntax validation\necho \"Checking JSON syntax...\"\nJSON_FILES=$(echo \"$STAGED_FILES\" | grep -E '\\.json$' || true)\nif [ -n \"$JSON_FILES\" ]; then\n for file in $JSON_FILES; do\n if [ -f \"$file\" ]; then\n if ! node -e \"JSON.parse(require('fs').readFileSync('$file'))\" 2>/dev/null; then\n WARNINGS+=(\"❌ $file: Invalid JSON syntax\")\n VIOLATIONS=$((VIOLATIONS + 1))\n fi\n fi\n done\nfi\n\n# Check 3: Shell script syntax\necho \"Checking shell script syntax...\"\nSH_FILES=$(echo \"$STAGED_FILES\" | grep -E '\\.sh$' || true)\nif [ -n \"$SH_FILES\" ]; then\n for file in $SH_FILES; do\n if [ -f \"$file\" ]; then\n if ! bash -n \"$file\" 2>/dev/null; then\n WARNINGS+=(\"❌ $file: Shell script syntax error\")\n VIOLATIONS=$((VIOLATIONS + 1))\n fi\n fi\n done\nfi\n\n# Check 4: JavaScript/JSX syntax (if node available)\nif command -v node &>/dev/null; then\n echo \"Checking JavaScript/JSX syntax...\"\n JS_FILES=$(echo \"$STAGED_FILES\" | grep -E '\\.(js|jsx)$' || true)\n if [ -n \"$JS_FILES\" ]; then\n for file in $JS_FILES; do\n if [ -f \"$file\" ]; then\n # Skip if it's a JSX file (node --check doesn't handle JSX well)\n if [[ \"$file\" == *.jsx ]]; then\n # Basic check: ensure file is readable\n if ! head -n 1 \"$file\" > /dev/null 2>&1; then\n WARNINGS+=(\"⚠️ $file: Cannot read file\")\n VIOLATIONS=$((VIOLATIONS + 1))\n fi\n else\n if ! node --check \"$file\" 2>/dev/null; then\n WARNINGS+=(\"❌ $file: JavaScript syntax error\")\n VIOLATIONS=$((VIOLATIONS + 1))\n fi\n fi\n fi\n done\n fi\nfi\n\n# Check 5: Mintlify config validation (if mintlify available)\nif command -v mintlify &>/dev/null; then\n echo \"Checking Mintlify configuration...\"\n if [ -f \"docs.json\" ] || [ -f \"mint.json\" ]; then\n CONFIG_FILE=\"docs.json\"\n [ -f \"mint.json\" ] && CONFIG_FILE=\"mint.json\"\n \n # Check if docs.json is valid JSON\n if ! node -e \"JSON.parse(require('fs').readFileSync('$CONFIG_FILE'))\" 2>/dev/null; then\n WARNINGS+=(\"❌ $CONFIG_FILE: Invalid JSON syntax\")\n VIOLATIONS=$((VIOLATIONS + 1))\n fi\n fi\nfi\n\n# Check 6: Import path validation (absolute paths for snippets)\necho \"Checking import paths...\"\nif [ -n \"$STAGED_DOCS_PAGES\" ]; then\n JSX_MDX_FILES=$(echo \"$STAGED_DOCS_PAGES\" | grep -E '\\.mdx$' | grep -v \"style-guide\" || true)\nelse\n JSX_MDX_FILES=$(echo \"$STAGED_FILES\" | grep -E '\\.(jsx|tsx|mdx)$' | grep -v \"style-guide\" || true)\nfi\nif [ -n \"$JSX_MDX_FILES\" ]; then\n for file in $JSX_MDX_FILES; do\n if [ -f \"$file\" ]; then\n # Skip style guide (it documents relative imports as examples of what NOT to do)\n if [[ \"$file\" == *\"style-guide\"* ]]; then\n continue\n fi\n # Check for snippets imports that aren't absolute\n if grep -E \"from ['\\\"].*snippets\" \"$file\" 2>/dev/null | grep -v \"from ['\\\"]/snippets\" > /dev/null; then\n WARNINGS+=(\"⚠️ $file: Snippets imports should be absolute (/snippets/...)\")\n VIOLATIONS=$((VIOLATIONS + 1))\n fi\n fi\n done\nfi\n\n# Check 7: Browser validation (if Node.js and Puppeteer available)\nif command -v node &>/dev/null; then\n # Check if puppeteer is available (tests/ first, then tools/, then legacy root node_modules)\n PUPPETEER_AVAILABLE=false\n if [ -f \"tests/node_modules/puppeteer/package.json\" ]; then\n PUPPETEER_AVAILABLE=true\n export NODE_PATH=\"$(pwd)/tests/node_modules:${NODE_PATH:-}\"\n elif [ -f \"tools/node_modules/puppeteer/package.json\" ]; then\n PUPPETEER_AVAILABLE=true\n export NODE_PATH=\"$(pwd)/tools/node_modules:${NODE_PATH:-}\"\n elif [ -f \"node_modules/puppeteer/package.json\" ]; then\n PUPPETEER_AVAILABLE=true\n elif [ -f \"tests/package.json\" ] && grep -q \"puppeteer\" tests/package.json; then\n PUPPETEER_AVAILABLE=true\n export NODE_PATH=\"$(pwd)/tests/node_modules:${NODE_PATH:-}\"\n elif [ -f \"tools/package.json\" ] && grep -q \"puppeteer\" tools/package.json; then\n PUPPETEER_AVAILABLE=true\n export NODE_PATH=\"$(pwd)/tools/node_modules:${NODE_PATH:-}\"\n elif [ -f \"package.json\" ] && grep -q \"puppeteer\" package.json; then\n PUPPETEER_AVAILABLE=true\n fi\n \n # If Puppeteer not available but package.json exists, try to install it\n if [ \"$PUPPETEER_AVAILABLE\" = false ]; then\n if [ -f \"tools/package.json\" ] && grep -q \"puppeteer\" tools/package.json; then\n echo -e \"${YELLOW}⚠️ Puppeteer not found, attempting to install dependencies...${NC}\"\n if cd tools && npm install --silent 2>&1; then\n cd \"$REPO_ROOT\"\n # Check again after install\n if [ -f \"tools/node_modules/puppeteer/package.json\" ]; then\n PUPPETEER_AVAILABLE=true\n export NODE_PATH=\"$(pwd)/tools/node_modules:${NODE_PATH:-}\"\n echo -e \"${GREEN}✓ Puppeteer installed successfully${NC}\"\n else\n echo -e \"${RED}❌ Puppeteer installation failed or incomplete${NC}\"\n fi\n else\n cd \"$REPO_ROOT\"\n echo -e \"${RED}❌ Failed to install dependencies${NC}\"\n fi\n elif [ -f \"tests/package.json\" ] && grep -q \"puppeteer\" tests/package.json; then\n echo -e \"${YELLOW}⚠️ Puppeteer not found, attempting to install dependencies...${NC}\"\n if cd tests && npm install --silent 2>&1; then\n cd \"$REPO_ROOT\"\n # Check again after install\n if [ -f \"tests/node_modules/puppeteer/package.json\" ]; then\n PUPPETEER_AVAILABLE=true\n export NODE_PATH=\"$(pwd)/tests/node_modules:${NODE_PATH:-}\"\n echo -e \"${GREEN}✓ Puppeteer installed successfully${NC}\"\n else\n echo -e \"${RED}❌ Puppeteer installation failed or incomplete${NC}\"\n fi\n else\n cd \"$REPO_ROOT\"\n echo -e \"${RED}❌ Failed to install dependencies${NC}\"\n fi\n fi\n fi\n \n if [ \"$PUPPETEER_AVAILABLE\" = true ] && [ -f \".githooks/verify-browser.js\" ]; then",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": ".github/scripts/fetch-forum-data.js",
+ "script": "fetch-forum-data",
+ "category": "automation",
+ "purpose": "infrastructure:data-feeds",
+ "scope": ".github/scripts",
+ "owner": "docs",
+ "needs": "F-R1",
+ "purpose_statement": "Fetches latest topics and posts from Livepeer Forum API, writes to snippets/automations/forum/",
+ "pipeline_declared": "P5, P6",
+ "usage": "node .github/scripts/fetch-forum-data.js [flags]",
+ "header": "/**\n * @script fetch-forum-data\n * @category automation\n * @purpose infrastructure:data-feeds\n * @scope .github/scripts\n * @owner docs\n * @needs F-R1\n * @purpose-statement Fetches latest topics and posts from Livepeer Forum API, writes to snippets/automations/forum/\n * @pipeline P5, P6\n * @usage node .github/scripts/fetch-forum-data.js [flags]\n */\nconst https = require(\"https\");\nconst fs = require(\"fs\");\n\n// Fetch JSON from URL\nfunction fetchJSON(url) {\n return new Promise((resolve, reject) => {\n https\n .get(url, (res) => {\n let data = \"\";\n res.on(\"data\", (chunk) => {\n data += chunk;\n });\n res.on(\"end\", () => {\n try {\n resolve(JSON.parse(data));\n } catch (e) {\n reject(e);\n }\n });\n })\n .on(\"error\", reject);\n });\n}\n\n// Check if topic is old pinned\nfunction isOldPinned(topic) {\n const pinned = topic.pinned === true || topic.pinned_globally === true;\n if (!pinned) return false;\n const created = new Date(topic.created_at);\n const now = new Date();\n const ageDays = (now - created) / (1000 * 60 * 60 * 24);\n return ageDays > 30;\n}\n\n// Clean and format HTML\nfunction cleanAndFormatHTML(html) {\n let cleanHTML = html;\n\n // Remove anchor navigation links\n cleanHTML = cleanHTML.replace(\n /]*name=\"[^\"]*\"[^>]*class=\"anchor\"[^>]*>.*?<\\/a>/g,\n \"\"\n );\n\n // Clean up headings\n cleanHTML = cleanHTML.replace(/]*>(.*?)<\\/h1>/g, \"$1
\");\n cleanHTML = cleanHTML.replace(/]*>(.*?)<\\/h2>/g, \"$1
\");\n cleanHTML = cleanHTML.replace(/]*>(.*?)<\\/h3>/g, \"$1
\");\n cleanHTML = cleanHTML.replace(/]*>(.*?)<\\/h[4-6]>/g, \"$1
\");\n\n // Clean up images and their references\n cleanHTML = cleanHTML.replace(/]*class=\"lightbox\"[^>]*>.*?<\\/a>/g, \"\");\n cleanHTML = cleanHTML.replace(\n /]*class=\"lightbox-wrapper\"[^>]*>.*?<\\/div>/g,\n \"\"\n );\n cleanHTML = cleanHTML.replace(/
]*>/g, \"\");\n cleanHTML = cleanHTML.replace(/\\[!\\[.*?\\]\\(.*?\\)\\]\\(.*?\\)/g, \"\");\n cleanHTML = cleanHTML.replace(/image\\d+×\\d+\\s+[\\d.]+\\s*[KM]B/gi, \"\");\n\n // Keep paragraphs, lists, emphasis, code\n cleanHTML = cleanHTML.replace(//g, \"
\");\n cleanHTML = cleanHTML.replace(/<\\/p>/g, \"
\");\n cleanHTML = cleanHTML.replace(//g, \"\");\n cleanHTML = cleanHTML.replace(/<\\/ul>/g, \"
\");\n cleanHTML = cleanHTML.replace(//g, \"\");\n cleanHTML = cleanHTML.replace(/<\\/ol>/g, \"
\");\n cleanHTML = cleanHTML.replace(/- /g, \"
- \");\n cleanHTML = cleanHTML.replace(/<\\/li>/g, \"
\");\n cleanHTML = cleanHTML.replace(\n /(.*?)<\\/strong>/g,\n \"$1\"\n );\n cleanHTML = cleanHTML.replace(/(.*?)<\\/em>/g, \"$1\");\n cleanHTML = cleanHTML.replace(/(.*?)<\\/code>/g, \"$1\");\n\n // Simplify links\n cleanHTML = cleanHTML.replace(\n /]*href=\"([^\"]*)\"[^>]*>(.*?)<\\/a>/g,\n '$2'\n );\n\n // Decode HTML entities\n cleanHTML = cleanHTML.replace(/&/g, \"&\");\n cleanHTML = cleanHTML.replace(/</g, \"<\");\n cleanHTML = cleanHTML.replace(/>/g, \">\");\n cleanHTML = cleanHTML.replace(/"/g, '\"');\n cleanHTML = cleanHTML.replace(/'/g, \"'\");\n cleanHTML = cleanHTML.replace(/ /g, \" \");\n\n // Clean up whitespace\n cleanHTML = cleanHTML.replace(/\\s+/g, \" \");\n cleanHTML = cleanHTML.replace(/\\s*<\\/p>/g, \"\");\n\n return cleanHTML.trim();\n}\n\nasync function main() {\n console.log(\"Fetching latest topics...\");\n const latestData = await fetchJSON(\"https://forum.livepeer.org/latest.json\");\n\n const topics = latestData.topic_list?.topics || [];\n console.log(`Found ${topics.length} topics`);\n\n // Filter out old pinned topics\n const filteredTopics = topics.filter((t) => !isOldPinned(t));\n console.log(`After filtering: ${filteredTopics.length} topics`);\n\n // Get top 4\n const top4 = filteredTopics.slice(0, 4);\n console.log(`Processing top 4 topics...`);\n\n const processedTopics = [];\n\n for (const topic of top4) {\n console.log(`Processing topic ${topic.id}: ${topic.title}`);\n\n // Fetch full topic data\n const topicData = await fetchJSON(\n `https://forum.livepeer.org/t/${topic.id}.json`\n );\n\n // Extract first post\n const firstPost = topicData.post_stream?.posts?.find(\n (p) => p.post_number === 1\n );\n\n if (!firstPost) {\n console.log(` No first post found, skipping`);\n continue;\n }\n\n const htmlContent = cleanAndFormatHTML(firstPost.cooked || \"\");\n const datePosted = topic.created_at\n ? new Date(topic.created_at).toLocaleDateString(\"en-US\", {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n })\n : \"\";\n\n processedTopics.push({\n title: topic.title,\n href: `https://forum.livepeer.org/t/${topic.id}`,\n author: `By ${firstPost.name || firstPost.username || \"Unknown\"} (@${\n firstPost.username || \"unknown\"\n })`,\n content: htmlContent,\n replyCount: (topic.posts_count || 1) - 1,\n datePosted: datePosted,\n });\n }\n\n console.log(`Processed ${processedTopics.length} topics`);\n\n // Generate JavaScript export with exact formatting\n let jsExport = \"export const forumData = [\\n\";\n\n processedTopics.forEach((item, index) => {\n jsExport += \" {\\n\";\n jsExport += ` title: \"${item.title\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')}\",\\n`;\n jsExport += ` href: \"${item.href}\",\\n`;\n jsExport += ` author: \"${item.author\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')}\",\\n`;\n\n // Content with proper escaping and indentation\n const escapedContent = item.content\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \" \");\n\n jsExport += ` content:\\n \"${escapedContent}\",\\n`;\n jsExport += ` replyCount: ${item.replyCount},\\n`;\n jsExport += ` datePosted: \"${item.datePosted}\",\\n`;\n jsExport += \" }\";\n\n if (index < processedTopics.length - 1) {\n jsExport += \",\";\n }\n jsExport += \"\\n\";\n });\n\n jsExport += \"];\\n\";\n\n // Write to file\n const outputPath = \"snippets/automations/forum/forumData.jsx\";\n fs.mkdirSync(\"snippets/automations/forum\", { recursive: true });\n fs.writeFileSync(outputPath, jsExport);\n console.log(`Written to ${outputPath}`);\n}\n\nmain().catch((err) => {\n console.error(\"Error:\", err);\n process.exit(1);\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-schedule",
+ "caller": ".github/workflows/update-forum-data.yml",
+ "workflow": "Update Forum Data",
+ "pipeline": "P5",
+ "cron": "0 0 * * *"
+ },
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/update-forum-data.yml",
+ "workflow": "Update Forum Data",
+ "pipeline": "P6"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": ".github/scripts/snippets/automations/forum",
+ "type": "directory",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": ".github/scripts/snippets/automations/forum/forumData.jsx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": ".github/scripts/snippets/automations/forum, .github/scripts/snippets/automations/forum/forumData.jsx",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P5 (Update Forum Data cron 0 0 * * *); P6 (Update Forum Data)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P5"
+ },
+ {
+ "path": ".github/scripts/fetch-ghost-blog-data.js",
+ "script": "fetch-ghost-blog-data",
+ "category": "automation",
+ "purpose": "infrastructure:data-feeds",
+ "scope": ".github/scripts",
+ "owner": "docs",
+ "needs": "F-R1",
+ "purpose_statement": "Fetches blog posts from Ghost CMS API, writes to snippets/automations/blog/",
+ "pipeline_declared": "P5, P6",
+ "usage": "node .github/scripts/fetch-ghost-blog-data.js [flags]",
+ "header": "/**\n * @script fetch-ghost-blog-data\n * @category automation\n * @purpose infrastructure:data-feeds\n * @scope .github/scripts\n * @owner docs\n * @needs F-R1\n * @purpose-statement Fetches blog posts from Ghost CMS API, writes to snippets/automations/blog/\n * @pipeline P5, P6\n * @usage node .github/scripts/fetch-ghost-blog-data.js [flags]\n */\nconst https = require(\"https\");\nconst fs = require(\"fs\");\n\n// Fetch JSON from URL\nfunction fetchJSON(url) {\n return new Promise((resolve, reject) => {\n https\n .get(url, (res) => {\n let data = \"\";\n res.on(\"data\", (chunk) => {\n data += chunk;\n });\n res.on(\"end\", () => {\n try {\n resolve(JSON.parse(data));\n } catch (e) {\n reject(e);\n }\n });\n })\n .on(\"error\", reject);\n });\n}\n\n// Safe HTML escape - only escape backticks for template literals\nfunction safeHTML(html) {\n return (html || \"\").replace(/`/g, \"\\\\`\");\n}\n\n// Format date\nfunction formatDate(iso) {\n return new Date(iso).toLocaleDateString(\"en-US\", {\n month: \"short\",\n day: \"numeric\",\n year: \"numeric\",\n });\n}\n\nasync function main() {\n console.log(\"Fetching Ghost blog posts...\");\n\n const apiKey = process.env.GHOST_CONTENT_API_KEY;\n if (!apiKey) {\n throw new Error(\n \"GHOST_CONTENT_API_KEY environment variable is not set. Please configure your Ghost Content API key.\"\n );\n }\n\n const apiUrl =\n `https://livepeer-studio.ghost.io/ghost/api/content/posts/?key=${apiKey}&limit=4&include=tags,authors`;\n\n const response = await fetchJSON(apiUrl);\n\n if (!response.posts || response.posts.length === 0) {\n console.log(\"No posts found\");\n return;\n }\n\n console.log(`Found ${response.posts.length} posts`);\n\n // Process posts\n const posts = response.posts.map((p) => ({\n title: p.title,\n href: p.url,\n author: p.primary_author?.name\n ? `By ${p.primary_author.name}`\n : \"By Livepeer Team\",\n content: safeHTML(p.html),\n datePosted: formatDate(p.published_at),\n img: p.feature_image || \"\",\n excerpt: safeHTML(p.excerpt),\n readingTime: p.reading_time || 0,\n }));\n\n // Generate JavaScript export with template literals\n let jsExport = \"export const ghostData = [\\n\";\n\n posts.forEach((post, index) => {\n jsExport += \"{\\n\";\n jsExport += ` title: \\`${post.title}\\`,\\n`;\n jsExport += ` href: \\`${post.href}\\`,\\n`;\n jsExport += ` author: \\`${post.author}\\`,\\n`;\n jsExport += ` content: \\`${post.content}\\`,\\n`;\n jsExport += ` datePosted: \\`${post.datePosted}\\`,\\n`;\n jsExport += ` img: \\`${post.img}\\`,\\n`;\n jsExport += ` excerpt: \\`${post.excerpt}\\`,\\n`;\n jsExport += ` readingTime: ${post.readingTime}\\n`;\n jsExport += \"}\";\n\n if (index < posts.length - 1) {\n jsExport += \",\";\n }\n jsExport += \"\\n\";\n });\n\n jsExport += \"];\\n\";\n\n // Write to file\n const outputPath = \"snippets/automations/blog/ghostBlogData.jsx\";\n fs.mkdirSync(\"snippets/automations/blog\", { recursive: true });\n fs.writeFileSync(outputPath, jsExport);\n console.log(`Written to ${outputPath}`);\n}\n\nmain().catch((err) => {\n console.error(\"Error:\", err);\n process.exit(1);\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-schedule",
+ "caller": ".github/workflows/update-ghost-blog-data.yml",
+ "workflow": "Update Ghost Blog Data",
+ "pipeline": "P5",
+ "cron": "0 0 * * *"
+ },
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/update-ghost-blog-data.yml",
+ "workflow": "Update Ghost Blog Data",
+ "pipeline": "P6"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": ".github/scripts/snippets/automations/blog",
+ "type": "directory",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": ".github/scripts/snippets/automations/blog/ghostBlogData.jsx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": ".github/scripts/snippets/automations/blog, .github/scripts/snippets/automations/blog/ghostBlogData.jsx",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P5 (Update Ghost Blog Data cron 0 0 * * *); P6 (Update Ghost Blog Data)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P5"
+ },
+ {
+ "path": ".github/scripts/fetch-youtube-data.js",
+ "script": "fetch-youtube-data",
+ "category": "automation",
+ "purpose": "infrastructure:data-feeds",
+ "scope": ".github/scripts",
+ "owner": "docs",
+ "needs": "F-R1",
+ "purpose_statement": "Fetches video data from YouTube Data API, writes to snippets/automations/youtube/",
+ "pipeline_declared": "P5, P6",
+ "usage": "node .github/scripts/fetch-youtube-data.js [flags]",
+ "header": "/**\n * @script fetch-youtube-data\n * @category automation\n * @purpose infrastructure:data-feeds\n * @scope .github/scripts\n * @owner docs\n * @needs F-R1\n * @purpose-statement Fetches video data from YouTube Data API, writes to snippets/automations/youtube/\n * @pipeline P5, P6\n * @usage node .github/scripts/fetch-youtube-data.js [flags]\n */\nconst https = require(\"https\");\nconst fs = require(\"fs\");\n\nconst YOUTUBE_API_KEY = process.env.YOUTUBE_API_KEY;\nconst CHANNEL_ID = process.env.CHANNEL_ID || \"UCzfHtZnmUzMbJDxGCwIgY2g\";\n\nfunction httpsGet(url) {\n return new Promise((resolve, reject) => {\n https\n .get(url, (res) => {\n let data = \"\";\n res.on(\"data\", (chunk) => (data += chunk));\n res.on(\"end\", () => resolve(JSON.parse(data)));\n })\n .on(\"error\", reject);\n });\n}\n\nfunction parseDuration(duration) {\n const match = duration.match(/PT(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?/);\n if (!match) return 0;\n\n const hours = parseInt(match[1] || 0);\n const minutes = parseInt(match[2] || 0);\n const seconds = parseInt(match[3] || 0);\n\n return hours * 3600 + minutes * 60 + seconds;\n}\n\nfunction escapeForJSX(str) {\n return str\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/'/g, \"\\\\'\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \" \")\n .replace(/\\r/g, \"\")\n .replace(/\\t/g, \" \");\n}\n\nasync function main() {\n // Step 1: Get recent videos\n console.log(\"Fetching recent videos...\");\n const searchUrl = `https://www.googleapis.com/youtube/v3/search?part=snippet&channelId=${CHANNEL_ID}&maxResults=50&order=date&type=video&key=${YOUTUBE_API_KEY}`;\n const searchResults = await httpsGet(searchUrl);\n\n if (!searchResults.items || searchResults.items.length === 0) {\n console.log(\"No videos found\");\n return;\n }\n\n // Step 2: Get video details for each video\n console.log(\n `Found ${searchResults.items.length} videos, fetching details...`\n );\n const videoIds = searchResults.items.map((item) => item.id.videoId).join(\",\");\n const detailsUrl = `https://www.googleapis.com/youtube/v3/videos?part=contentDetails,snippet&id=${videoIds}&key=${YOUTUBE_API_KEY}`;\n const detailsResults = await httpsGet(detailsUrl);\n\n // Step 3: Process and filter videos\n const videos = [];\n for (const video of detailsResults.items) {\n const duration = video.contentDetails.duration;\n const durationSeconds = parseDuration(duration);\n const snippet = video.snippet;\n\n // Check if it's a livestream\n const isLivestream =\n snippet.liveBroadcastContent === \"live\" ||\n snippet.liveBroadcastContent === \"upcoming\" ||\n duration === \"PT0S\" ||\n snippet.title.toLowerCase().includes(\"watercooler\") ||\n snippet.title.toLowerCase().includes(\"fireside\");\n\n // Filter out Shorts (≤60 seconds and not livestreams)\n const isShort =\n durationSeconds <= 60 && durationSeconds > 0 && !isLivestream;\n\n if (!isShort) {\n videos.push({\n title: snippet.title,\n href: `https://www.youtube.com/watch?v=${video.id}`,\n author: `By ${snippet.channelTitle || \"Livepeer\"}`,\n content: (snippet.description || \"\").substring(0, 500),\n publishedDate: new Date(snippet.publishedAt).toLocaleDateString(\n \"en-US\",\n { month: \"short\", day: \"numeric\", year: \"numeric\" }\n ),\n duration: duration,\n thumbnailUrl: snippet.thumbnails.high.url,\n });\n }\n }\n\n console.log(`Filtered to ${videos.length} non-Short videos`);\n\n // Step 4: Generate JSX content\n const jsxContent = `export const youtubeData = [\n${videos\n .map(\n (v) => ` {\n title: '${escapeForJSX(v.title)}',\n href: '${v.href}',\n author: '${v.author}',\n content: '${escapeForJSX(v.content)}...',\n publishedDate: '${v.publishedDate}',\n duration: '${v.duration}',\n thumbnailUrl: '${v.thumbnailUrl}'\n }`\n )\n .join(\",\\n\")}\n];\n`;\n\n // Step 5: Write to file\n fs.writeFileSync(\"snippets/automations/youtube/youtubeData.jsx\", jsxContent);\n console.log(\"Successfully wrote youtubeData.jsx\");\n}\n\nmain().catch((err) => {\n console.error(\"Error:\", err);\n process.exit(1);\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-schedule",
+ "caller": ".github/workflows/update-youtube-data.yml",
+ "workflow": "Update YouTube Data",
+ "pipeline": "P5",
+ "cron": "0 0 * * 0"
+ },
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/update-youtube-data.yml",
+ "workflow": "Update YouTube Data",
+ "pipeline": "P6"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": ".github/scripts/snippets/automations/youtube/youtubeData.jsx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": ".github/scripts/snippets/automations/youtube/youtubeData.jsx",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P5 (Update YouTube Data cron 0 0 * * 0); P6 (Update YouTube Data)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P5"
+ },
+ {
+ "path": ".github/scripts/project-showcase-sync.js",
+ "script": "project-showcase-sync",
+ "category": "automation",
+ "purpose": "infrastructure:data-feeds",
+ "scope": ".github/scripts",
+ "owner": "docs",
+ "needs": "F-R1",
+ "purpose_statement": "Fetches project showcase data from external source, writes to snippets/automations/showcase/",
+ "pipeline_declared": "P5, P6",
+ "usage": "node .github/scripts/project-showcase-sync.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script project-showcase-sync\n * @category automation\n * @purpose infrastructure:data-feeds\n * @scope .github/scripts\n * @owner docs\n * @needs F-R1\n * @purpose-statement Fetches project showcase data from external source, writes to snippets/automations/showcase/\n * @pipeline P5, P6\n * @usage node .github/scripts/project-showcase-sync.js [flags]\n */\n/*\n * Project Showcase sync job for GitHub Actions.\n *\n * Modes:\n * - poll: process new submissions + pending review decisions from Sheets\n * - dispatch: process one decision from repository_dispatch payload\n */\n\nconst crypto = require('crypto');\nconst fs = require('fs');\n\nconst REQUIRED_ENVS = [\n 'GOOGLE_SERVICE_ACCOUNT_JSON',\n 'GOOGLE_SHEET_ID',\n 'DISCORD_BOT_TOKEN',\n 'DISCORD_REVIEWER_USER_ID'\n];\n\nconst cfg = {\n mode: process.env.SHOWCASE_SYNC_MODE || 'poll',\n googleSheetId: process.env.GOOGLE_SHEET_ID,\n submissionsSheetName: process.env.SUBMISSIONS_SHEET_NAME || 'Form Responses 1',\n submissionsSheetUrl: process.env.SUBMISSIONS_SHEET_URL || '',\n reviewSheetName: process.env.REVIEW_SHEET_NAME || 'Review Responses',\n transformedSheetName: process.env.TRANSFORMED_SHEET_NAME || 'Transformed Responses',\n reviewProcessedColumn: process.env.REVIEW_PROCESSED_COLUMN || 'Processed',\n approvalFormBaseUrl: process.env.APPROVAL_FORM_BASE_URL || '',\n githubOwner: process.env.GITHUB_OWNER || process.env.GITHUB_REPOSITORY?.split('/')[0],\n githubRepo: process.env.GITHUB_REPO || process.env.GITHUB_REPOSITORY?.split('/')[1],\n githubDataBranch: process.env.GITHUB_DATA_BRANCH || 'docs-v2-preview',\n githubAssetsBranch: process.env.GITHUB_ASSETS_BRANCH || 'docs-v2-assets',\n showcaseDataPath:\n process.env.SHOWCASE_DATA_FILE_PATH || 'snippets/automations/showcase/showcaseData.jsx',\n showcaseAssetsBasePath:\n process.env.SHOWCASE_ASSETS_BASE_PATH || 'snippets/assets/domain/00_HOME/showcase',\n discordApiBaseUrl: process.env.DISCORD_API_BASE_URL || 'https://discord.com/api/v10',\n discordReviewerUserId: process.env.DISCORD_REVIEWER_USER_ID,\n githubToken: process.env.GITHUB_TOKEN,\n maxRowsPerRun: Number(process.env.MAX_ROWS_PER_RUN || 10)\n};\n\nconst serviceAccount = JSON.parse(process.env.GOOGLE_SERVICE_ACCOUNT_JSON || '{}');\n\nfunction log(msg, obj) {\n if (obj !== undefined) {\n console.log(msg, JSON.stringify(obj));\n } else {\n console.log(msg);\n }\n}\n\nfunction assertEnv() {\n const missing = REQUIRED_ENVS.filter((key) => !process.env[key]);\n if (missing.length > 0) {\n throw new Error(`Missing required env vars: ${missing.join(', ')}`);\n }\n if (!cfg.githubOwner || !cfg.githubRepo || !cfg.githubToken) {\n throw new Error('Missing GitHub config: GITHUB_OWNER/GITHUB_REPO/GITHUB_TOKEN');\n }\n}\n\nfunction base64url(input) {\n return Buffer.from(input)\n .toString('base64')\n .replace(/=/g, '')\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_');\n}\n\nfunction sanitizePathSegment(input) {\n return String(input || '')\n .trim()\n .replace(/[^a-zA-Z0-9._-]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 120) || 'untitled-project';\n}\n\nfunction getDriveFileId(url) {\n if (!url) return null;\n const patterns = [\n /\\/file\\/d\\/([a-zA-Z0-9_-]+)/,\n /[?&]id=([a-zA-Z0-9_-]+)/,\n /\\/uc\\?export=download&id=([a-zA-Z0-9_-]+)/\n ];\n for (const p of patterns) {\n const m = String(url).match(p);\n if (m?.[1]) return m[1];\n }\n return null;\n}\n\nfunction isValidUrl(value) {\n try {\n new URL(String(value));\n return true;\n } catch {\n return false;\n }\n}\n\nfunction findHeader(headers, candidates) {\n const map = new Map(headers.map((h) => [String(h).toLowerCase().trim(), h]));\n for (const c of candidates) {\n const hit = map.get(String(c).toLowerCase().trim());\n if (hit) return hit;\n }\n return null;\n}\n\nfunction getCell(row, headers, candidates) {\n const header = findHeader(headers, candidates);\n if (!header) return '';\n return row[header] || '';\n}\n\nfunction toObjects(values) {\n if (!values.length) return { headers: [], rows: [] };\n const headers = values[0];\n const rows = values.slice(1).map((raw, i) => {\n const json = {};\n headers.forEach((h, idx) => {\n json[h] = raw[idx] || '';\n });\n json.__rowIndex = i + 2;\n return json;\n });\n return { headers, rows };\n}\n\nfunction colToA1(colIndexZeroBased) {\n let n = colIndexZeroBased + 1;\n let result = '';\n while (n > 0) {\n const rem = (n - 1) % 26;\n result = String.fromCharCode(65 + rem) + result;\n n = Math.floor((n - 1) / 26);\n }\n return result;\n}\n\nfunction parseDecision(raw) {\n const value = String(raw || '').trim().toLowerCase();\n if (['yes', 'approve', 'approved'].includes(value)) return 'yes';\n if (['no', 'deny', 'denied'].includes(value)) return 'no';\n return null;\n}\n\nasync function getGoogleAccessToken() {\n const now = Math.floor(Date.now() / 1000);\n const header = { alg: 'RS256', typ: 'JWT' };\n const payload = {\n iss: serviceAccount.client_email,\n scope: 'https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.readonly',\n aud: 'https://oauth2.googleapis.com/token',\n exp: now + 3600,\n iat: now\n };\n\n const unsignedToken = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(payload))}`;\n const signer = crypto.createSign('RSA-SHA256');\n signer.update(unsignedToken);\n signer.end();\n const signature = signer.sign(serviceAccount.private_key, 'base64url');\n const assertion = `${unsignedToken}.${signature}`;\n\n const body = new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion\n });\n\n const res = await fetch('https://oauth2.googleapis.com/token', {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body\n });\n\n if (!res.ok) {\n throw new Error(`Failed to get Google access token: ${res.status} ${await res.text()}`);\n }\n\n return (await res.json()).access_token;\n}\n\nasync function sheetsGetValues(accessToken, range) {\n const url = `https://sheets.googleapis.com/v4/spreadsheets/${cfg.googleSheetId}/values/${encodeURIComponent(\n range\n )}`;\n const res = await fetch(url, { headers: { Authorization: `Bearer ${accessToken}` } });\n if (!res.ok) {\n throw new Error(`Sheets read failed for ${range}: ${res.status} ${await res.text()}`);\n }\n return (await res.json()).values || [];\n}\n\nasync function sheetsUpdateValue(accessToken, range, value) {\n const url = `https://sheets.googleapis.com/v4/spreadsheets/${cfg.googleSheetId}/values/${encodeURIComponent(\n range\n )}?valueInputOption=RAW`;\n const res = await fetch(url, {\n method: 'PUT',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({ values: [[value]] })\n });\n if (!res.ok) {\n throw new Error(`Sheets update failed for ${range}: ${res.status} ${await res.text()}`);",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-schedule",
+ "caller": ".github/workflows/project-showcase-sync.yml",
+ "workflow": "Project Showcase Sync",
+ "pipeline": "P5",
+ "cron": "*/15 * * * *"
+ },
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/project-showcase-sync.yml",
+ "workflow": "Project Showcase Sync",
+ "pipeline": "P6"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P5 (Project Showcase Sync cron */15 * * * *); P6 (Project Showcase Sync)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P5"
+ },
+ {
+ "path": "snippets/automations/youtube/filterVideos.js",
+ "script": "filterVideos",
+ "category": "automation",
+ "purpose": "infrastructure:data-feeds",
+ "scope": "external",
+ "owner": "docs",
+ "needs": "F-R1",
+ "purpose_statement": "YouTube video filter — post-processes fetched YouTube data to filter/sort videos for display",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node snippets/automations/youtube/filterVideos.js [flags]",
+ "header": "/**\n * @script filterVideos\n * @category automation\n * @purpose infrastructure:data-feeds\n * @scope external\n * @owner docs\n * @needs F-R1\n * @purpose-statement YouTube video filter — post-processes fetched YouTube data to filter/sort videos for display\n * @pipeline manual — not yet in pipeline\n * @usage node snippets/automations/youtube/filterVideos.js [flags]\n */\nexport const filterVideos = (\n videos,\n excludeKeywords = [\"fireside\", \"watercooler\"]\n) => {\n return videos.filter((video) => {\n const title = video.title.toLowerCase();\n return !excludeKeywords.some((keyword) =>\n title.includes(keyword.toLowerCase())\n );\n });\n};\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "B",
+ "flags": [],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tasks/scripts/audit-python.py",
+ "script": "audit-python",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tasks/scripts",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Python audit utility — runs Python-based audit checks (alternative to Node auditors)",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "python3 tasks/scripts/audit-python.py [flags]",
+ "header": "#!/usr/bin/env python3\n# @script audit-python\n# @category validator\n# @purpose qa:repo-health\n# @scope tasks/scripts\n# @owner docs\n# @needs E-C1, R-R14\n# @purpose-statement Python audit utility — runs Python-based audit checks (alternative to Node auditors)\n# @pipeline manual — not yet in pipeline\n# @usage python3 tasks/scripts/audit-python.py [flags]\n\"\"\"\nComprehensive audit script for all v2 pages\nRuns file checks, MDX validation, and link checking\n\"\"\"\n\nimport json\nimport os\nfrom pathlib import Path\nfrom datetime import datetime\nimport re\n\n# Get absolute path to script, then go up 3 levels\nSCRIPT_DIR = Path(__file__).resolve().parent\nBASE_DIR = SCRIPT_DIR.parent.parent\nDOCS_JSON_PATH = BASE_DIR / 'docs.json'\nREPORT_DIR = BASE_DIR / 'tasks' / 'reports' / 'page-audits'\nV2_PAGES_DIR = BASE_DIR / 'v2' / 'pages'\nSNIPPETS_DIR = BASE_DIR / 'snippets'\n\n# Ensure report directory exists\nREPORT_DIR.mkdir(parents=True, exist_ok=True)\n\ndef extract_pages(nav, pages=None):\n \"\"\"Recursively extract all page paths from navigation structure\"\"\"\n if pages is None:\n pages = []\n \n if isinstance(nav, list):\n for item in nav:\n extract_pages(item, pages)\n elif isinstance(nav, dict):\n if 'pages' in nav:\n for page in nav['pages']:\n if isinstance(page, str) and page.strip() and page != ' ':\n pages.append(page)\n elif isinstance(page, dict) and 'pages' in page:\n extract_pages(page, pages)\n for value in nav.values():\n if isinstance(value, (dict, list)):\n extract_pages(value, pages)\n return pages\n\ndef get_v2_pages():\n \"\"\"Get all v2 pages from docs.json\"\"\"\n with open(DOCS_JSON_PATH, 'r') as f:\n docs = json.load(f)\n \n v2_version = next((v for v in docs['navigation']['versions'] if v['version'] == 'v2'), None)\n if not v2_version:\n raise ValueError('v2 version not found in docs.json')\n \n all_pages = extract_pages(v2_version)\n unique_pages = list(set([\n p.replace('.mdx', '').replace('.md', '') \n for p in all_pages \n if p and p.strip() and p != ' '\n ]))\n \n return unique_pages\n\ndef is_intentional_redirect(page_path):\n \"\"\"Check if a page path is an intentional redirect\"\"\"\n return '/redirect' in page_path or page_path.endswith('/redirect')\n\ndef check_file_exists(page_path):\n \"\"\"Check if file exists and return full path\"\"\"\n candidates = [\n BASE_DIR / f\"{page_path}.mdx\",\n BASE_DIR / f\"{page_path}.md\",\n BASE_DIR / page_path / 'index.mdx',\n BASE_DIR / page_path / 'index.md',\n BASE_DIR / page_path / 'README.mdx',\n BASE_DIR / page_path / 'README.md'\n ]\n for file_path in candidates:\n if file_path.exists():\n return {'exists': True, 'path': str(file_path)}\n \n return {'exists': False, 'path': None}\n\ndef check_mdx_errors(file_path):\n \"\"\"Check for MDX syntax errors\"\"\"\n errors = []\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n content = f.read()\n \n # Check for broken imports\n import_pattern = r\"import\\s+{([^}]+)}\\s+from\\s+['\\\"]([^'\\\"]+)['\\\"]\"\n for match in re.finditer(import_pattern, content):\n import_path = match.group(2)\n if import_path.startswith('/snippets/'):\n full_path = BASE_DIR / import_path.lstrip('/')\n if '/components/' in import_path:\n # Import path may already include .jsx extension\n component_file = full_path\n if not str(component_file).endswith('.jsx') and not str(component_file).endswith('.js'):\n component_file = Path(str(full_path) + '.jsx')\n if not component_file.exists():\n errors.append(f\"Missing import: {import_path}\")\n except Exception as e:\n errors.append(f\"File read error: {str(e)}\")\n \n return errors\n\ndef extract_links(file_path):\n \"\"\"Extract links from MDX file\"\"\"\n links = []\n try:\n with open(file_path, 'r', encoding='utf-8') as f:\n content = f.read()\n \n # Markdown links: [text](url)\n markdown_pattern = r'\\[([^\\]]+)\\]\\(([^)]+)\\)'\n for match in re.finditer(markdown_pattern, content):\n links.append({\n 'text': match.group(1),\n 'url': match.group(2),\n 'type': 'markdown'\n })\n \n # JSX links: \n jsx_pattern = r']+href=[\\'\"]([^\\'\"]+)[\\'\"]'\n for match in re.finditer(jsx_pattern, content):\n links.append({\n 'text': '',\n 'url': match.group(1),\n 'type': 'jsx'\n })\n \n # Anchor tags: \n anchor_pattern = r']+href=[\\'\"]([^\\'\"]+)[\\'\"]'\n for match in re.finditer(anchor_pattern, content):\n links.append({\n 'text': '',\n 'url': match.group(1),\n 'type': 'anchor'\n })\n except Exception:\n pass\n \n return links\n\ndef check_link(link, current_page_path):\n \"\"\"Check if link is broken\"\"\"\n url = link['url']\n \n # Skip external links\n if url.startswith(('http://', 'https://', 'mailto:', '#')):\n return {'broken': False, 'reason': 'external_or_anchor'}\n \n # Handle relative links\n if url.startswith('/'):\n # Absolute path from root\n target_path = url.lstrip('/').rstrip('/')\n file_check = check_file_exists(f\"v2/pages/{target_path}\")\n if not file_check['exists']:\n return {'broken': True, 'reason': 'file_not_found', 'expected': f\"v2/pages/{target_path}\"}\n else:\n # Relative path\n current_dir = Path(current_page_path).parent\n target_path = (current_dir / url).resolve()\n try:\n relative_path = target_path.relative_to(BASE_DIR)\n file_check = check_file_exists(str(relative_path).replace('.mdx', '').replace('.md', ''))\n if not file_check['exists']:\n return {'broken': True, 'reason': 'file_not_found', 'expected': str(relative_path)}\n except ValueError:\n return {'broken': True, 'reason': 'path_outside_repo', 'expected': str(target_path)}\n \n return {'broken': False, 'reason': 'valid'}\n\n# Main execution\n# Write progress immediately\nprogress_file = REPORT_DIR / 'audit-python-progress.log'\ntry:\n with open(progress_file, 'w') as f:\n f.write(f\"Audit started at: {datetime.now().isoformat()}\\n\")\nexcept Exception as e:\n print(f\"Warning: Could not write progress file: {e}\")\n\nprint('🔍 Extracting v2 pages from docs.json...')\ntry:\n with open(progress_file, 'a') as f:\n f.write('Extracting v2 pages from docs.json...\\n')\nexcept:\n pass\n\ntry:\n pages = get_v2_pages()\n try:\n with open(progress_file, 'a') as f:\n f.write(f'Found {len(pages)} pages to audit\\n')\n except:\n pass\n print(f'📄 Found {len(pages)} pages to audit\\n')\nexcept Exception as e:\n try:\n with open(progress_file, 'a') as f:\n f.write(f'ERROR: {str(e)}\\n')\n except:\n pass\n print(f'ERROR: {e}')\n raise\n\naudit_results = {\n 'timestamp': datetime.now().isoformat(),\n 'totalPages': len(pages),\n 'fileChecks': [],\n 'mdxErrors': [],",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [],
+ "outputs_display": "none detected",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/integration/browser.test.js",
+ "script": "browser.test",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Puppeteer browser integration test — renders pages from docs.json and checks for console errors, load failures, and visual regressions",
+ "pipeline_declared": "P1",
+ "usage": "node tests/integration/browser.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script browser.test\n * @category validator\n * @purpose qa:content-quality\n * @scope tests\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Puppeteer browser integration test — renders pages from docs.json and checks for console errors, load failures, and visual regressions\n * @pipeline P1\n * @dualmode dual-mode (document flags)\n * @usage node tests/integration/browser.test.js [flags]\n */\n/**\n * Browser rendering tests\n * Tests pages in headless browser with theme checks\n */\n\nconst fs = require('fs');\nconst path = require('path');\n\nlet puppeteer;\ntry {\n puppeteer = require('puppeteer');\n} catch (_error) {\n puppeteer = require(path.join(process.cwd(), 'tools', 'node_modules', 'puppeteer'));\n}\n\nconst { getMdxFiles, getStagedDocsPageFiles } = require('../utils/file-walker');\nconst { getV2Pages } = require('../../tools/scripts/test-v2-pages');\nconst { ensureServerRunning, stopServer, getServerUrl } = require('../../.githooks/server-manager');\n\nconst DEFAULT_BASE_URL = process.env.MINT_BASE_URL || 'http://localhost:3000';\nconst TIMEOUT = 30000;\n\n/**\n * Convert file path or page path to URL\n */\nfunction filePathToUrl(filePath) {\n // If it's already a page path from docs.json (starts with v2/pages)\n if (filePath.startsWith('v2/pages/')) {\n let url = filePath\n .replace(/^v2\\/pages\\//, '')\n .replace(/\\.mdx$/, '');\n \n if (url.endsWith('/index')) {\n url = url.replace(/\\/index$/, '');\n }\n \n return `/${url}`;\n }\n \n // Otherwise treat as file path\n let url = filePath\n .replace(/^.*v2\\/pages\\//, '')\n .replace(/\\.mdx$/, '');\n \n if (url.endsWith('/index')) {\n url = url.replace(/\\/index$/, '');\n }\n \n return `/${url}`;\n}\n\n/**\n * Test page in browser\n */\nasync function testPage(browser, filePath, options = {}) {\n const { theme = 'dark', baseUrl = DEFAULT_BASE_URL } = options;\n const url = filePathToUrl(filePath);\n const fullUrl = `${baseUrl}${url}`;\n const page = await browser.newPage();\n \n // Set desktop viewport (fixed size - documentation is not responsive)\n await page.setViewport({ width: 1920, height: 1080 });\n \n // Set theme if needed\n if (theme === 'light') {\n await page.evaluateOnNewDocument(() => {\n localStorage.setItem('theme', 'light');\n });\n }\n \n const errors = [];\n const warnings = [];\n \n // Listen for console errors\n page.on('console', msg => {\n const type = msg.type();\n const text = msg.text();\n \n // Filter out non-critical errors and test script artifacts\n const ignored = [\n 'favicon', \n 'sourcemap', \n 'deprecated', \n 'experimental',\n 'require is not defined', // Test scripts\n 'puppeteer', // Test script artifacts\n 'fs has already been declared', // Test script artifacts\n 'getMdxFiles', // Test script artifacts\n 'validateMdx', // Test script artifacts\n 'execSync', // Test script artifacts\n 'path', // Test script artifacts\n 'Unexpected token \\'export\\'', // Test script artifacts\n 'await is only valid in async functions' // Test script artifacts\n ];\n if (type === 'error' && !ignored.some(i => text.toLowerCase().includes(i.toLowerCase()))) {\n errors.push(`Console: ${text}`);\n }\n });\n \n page.on('pageerror', error => {\n const errorMsg = error.message;\n // Filter out test script errors and known false positives\n const ignored = [\n 'require is not defined',\n 'puppeteer',\n 'fs has already been declared',\n 'getMdxFiles',\n 'validateMdx',\n 'execSync',\n 'path',\n 'Unexpected token',\n 'await is only valid',\n 'appendChild', // Test script injection artifacts\n 'Identifier \\'fs\\'', // Test script artifacts\n 'Identifier \\'puppeteer\\'', // Test script artifacts\n 'Identifier \\'getMdxFiles\\'', // Test script artifacts\n 'Identifier \\'validateMdx\\'', // Test script artifacts\n 'Identifier \\'execSync\\'', // Test script artifacts\n 'Identifier \\'path\\'' // Test script artifacts\n ];\n if (!ignored.some(i => errorMsg.toLowerCase().includes(i.toLowerCase()))) {\n errors.push(`Page Error: ${errorMsg}`);\n }\n });\n \n try {\n await page.goto(fullUrl, { waitUntil: 'networkidle2', timeout: TIMEOUT });\n // Wait for content to render\n await new Promise(resolve => setTimeout(resolve, 2000));\n \n // Check content\n const bodyText = await page.evaluate(() => document.body.innerText);\n const h1Text = await page.evaluate(() => document.querySelector('h1')?.innerText || '');\n \n // Check for 404 pages\n const is404 = await page.evaluate(() => {\n const bodyText = document.body.innerText.toLowerCase();\n return bodyText.includes('doesn\\'t exist') ||\n bodyText.includes('page not found') ||\n bodyText.includes('404') ||\n bodyText.includes('ruh oh');\n });\n \n if (is404) {\n errors.push(`Page returns 404 - page doesn't exist`);\n }\n \n // Check minimum content length (real pages should have substantial content)\n if (!bodyText || bodyText.trim().length < 500) {\n errors.push(`Page appears empty or failed to render (only ${bodyText ? bodyText.length : 0} chars)`);\n }\n \n // Check for H1\n const hasH1 = await page.evaluate(() => {\n return document.querySelector('h1') !== null;\n });\n \n if (!hasH1) {\n errors.push('No H1 heading found');\n } else if (h1Text && (h1Text.includes('doesn\\'t exist') || h1Text.includes('404'))) {\n errors.push(`H1 indicates 404: \"${h1Text}\"`);\n }\n \n // Check for render errors\n const hasError = await page.evaluate(() => {\n return document.querySelector('[data-error-boundary]') !== null ||\n document.body.innerText.includes('Error:') ||\n document.body.innerText.includes('Failed to render');\n });\n \n if (hasError) {\n errors.push('Page contains render errors');\n }\n \n return {\n filePath,\n url: fullUrl,\n theme,\n success: errors.length === 0,\n errors,\n warnings,\n contentLength: bodyText ? bodyText.length : 0\n };\n } catch (error) {\n return {\n filePath,\n url: fullUrl,\n theme,\n success: false,\n errors: [`Navigation Error: ${error.message}`],\n warnings\n };\n } finally {\n await page.close();\n }\n}\n\n/**\n * Run browser tests\n */\nasync function runTests(options = {}) {\n const { files = null, stagedOnly = false, themes = ['dark'] } = options;\n \n let testFiles = files;\n if (!testFiles) {\n if (stagedOnly) {\n // Pre-commit: only test staged files (limited to 10 for speed)",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "runner",
+ "caller": "tests/run-all.js",
+ "pipeline": "P1"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-all.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:browser"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "test:browser"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "test:browser:all"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 via run-all; indirect via tests/run-all.js; manual (npm script: test:browser); manual (npm script: test:browser); manual (npm script: test:browser:all)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/integration/domain-pages-audit.js",
+ "script": "domain-pages-audit",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests/integration, tests/reports, docs.livepeer.org",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Audits deployed docs page HTTP status codes (v1, v2, or both) and emits a stable JSON report",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/integration/domain-pages-audit.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script domain-pages-audit\n * @category validator\n * @purpose qa:repo-health\n * @scope tests/integration, tests/reports, docs.livepeer.org\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Audits deployed docs page HTTP status codes (v1, v2, or both) and emits a stable JSON report\n * @pipeline manual — not yet in pipeline\n * @usage node tests/integration/domain-pages-audit.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst puppeteer = require('puppeteer');\nconst { getStagedDocsPageFiles, toDocsRouteKeyFromFile } = require('../utils/file-walker');\n\nconst args = process.argv.slice(2);\nconst stagedOnly = args.includes('--staged');\nconst baseUrlArgIndex = args.indexOf('--base-url');\nconst versionArgIndex = args.indexOf('--version');\nconst baseUrl = baseUrlArgIndex >= 0 && args[baseUrlArgIndex + 1]\n ? args[baseUrlArgIndex + 1]\n : (process.env.MINT_BASE_URL || 'https://docs.livepeer.org');\nconst versionScopeRaw = versionArgIndex >= 0 && args[versionArgIndex + 1]\n ? args[versionArgIndex + 1]\n : (process.env.DOMAIN_AUDIT_VERSION || 'both');\nconst versionScope = String(versionScopeRaw).toLowerCase();\n\nconst ROOT = path.join(__dirname, '..', '..');\nconst DOCS_JSON_PATH = path.join(ROOT, 'docs.json');\nconst REPORT_PATH = path.join(ROOT, 'tests', 'reports', 'domain-page-load-report.json');\nconst REPORT_MD_PATH = path.join(ROOT, 'tests', 'reports', 'domain-page-load-report.md');\nconst TIMEOUT = 25000;\nconst CONCURRENCY = 8;\nconst ALLOWED_SCOPES = new Set(['v1', 'v2', 'both']);\n\nfunction isVersionSelected(pagePath) {\n if (versionScope === 'both') return true;\n if (versionScope === 'v1') return pagePath.startsWith('v1/');\n if (versionScope === 'v2') return pagePath.startsWith('v2/');\n return true;\n}\n\nfunction extractPagePathsFromObject(node, version, pages, seen) {\n if (typeof node === 'string') {\n const pagePath = node.replace(/\\.mdx?$/, '');\n if (pagePath.startsWith(`${version}/`) && !seen.has(pagePath)) {\n seen.add(pagePath);\n pages.push(pagePath);\n }\n return;\n }\n\n if (Array.isArray(node)) {\n node.forEach((item) => extractPagePathsFromObject(item, version, pages, seen));\n return;\n }\n\n if (!node || typeof node !== 'object') return;\n\n if (Array.isArray(node.pages)) {\n node.pages.forEach((item) => extractPagePathsFromObject(item, version, pages, seen));\n }\n\n Object.values(node).forEach((value) => {\n extractPagePathsFromObject(value, version, pages, seen);\n });\n}\n\nfunction getAllDocsPages() {\n const docsJson = JSON.parse(fs.readFileSync(DOCS_JSON_PATH, 'utf8'));\n const versions = docsJson?.navigation?.versions || [];\n const pages = [];\n const seen = new Set();\n\n versions.forEach((versionNode) => {\n const version = versionNode.version;\n if (!version || !versionNode.languages) return;\n extractPagePathsFromObject(versionNode.languages, version, pages, seen);\n });\n\n return pages;\n}\n\nfunction getStagedDocsPages() {\n return getStagedDocsPageFiles(ROOT)\n .map((filePath) => toDocsRouteKeyFromFile(filePath, ROOT))\n .filter(Boolean);\n}\n\nfunction is404Content(text, title) {\n const haystack = `${title || ''} ${text || ''}`.toLowerCase();\n return haystack.includes(\"ruh oh. this page doesn't exist\") ||\n haystack.includes('page not found') ||\n haystack.includes('404');\n}\n\nfunction pageToUrl(pagePath) {\n return `${baseUrl.replace(/\\/$/, '')}/${pagePath}`;\n}\n\nfunction toMarkdownReport(report) {\n const lines = [];\n lines.push('# Domain Page Load Report');\n lines.push('');\n lines.push(`- Timestamp: ${report.timestamp}`);\n lines.push(`- Completed: ${report.completedAt || report.timestamp}`);\n lines.push(`- Base URL: ${report.baseUrl}`);\n lines.push(`- Mode: ${report.mode}`);\n lines.push(`- Version Scope: ${report.versionScope}`);\n lines.push(`- Total: ${report.total}`);\n lines.push(`- Passed: ${report.passed}`);\n lines.push(`- Failed: ${report.failed}`);\n if (typeof report.durationSec === 'number') lines.push(`- Duration: ${report.durationSec}s`);\n lines.push('');\n\n const failed = (report.results || []).filter((r) => !r.passed);\n lines.push('## Failures');\n lines.push('');\n if (failed.length === 0) {\n lines.push('_No failures_');\n } else {\n failed.forEach((item) => {\n lines.push(`- \\`${item.pagePath}\\``);\n lines.push(` - URL: ${item.url}`);\n lines.push(` - Status: ${item.status === null ? '(none)' : item.status}`);\n lines.push(` - Title: ${item.title || '(none)'}`);\n lines.push(` - Content Length: ${item.contentLength}`);\n (item.errors || []).forEach((e) => lines.push(` - Error: ${e}`));\n });\n }\n lines.push('');\n\n return lines.join('\\n');\n}\n\nasync function testSinglePage(browser, pagePath) {\n const url = pageToUrl(pagePath);\n const page = await browser.newPage();\n\n try {\n const response = await page.goto(url, { waitUntil: 'networkidle2', timeout: TIMEOUT });\n const status = response ? response.status() : null;\n const data = await page.evaluate(() => ({\n title: document.title || '',\n text: document.body?.innerText || ''\n }));\n\n const warnings = [];\n const errors = [];\n\n if (status && status >= 400) {\n errors.push(`HTTP ${status}`);\n }\n if (is404Content(data.text, data.title)) {\n errors.push('404 content');\n }\n if ((data.text || '').trim().length < 100) {\n warnings.push(`low content length (${data.text.length})`);\n }\n\n return {\n pagePath,\n url,\n status,\n title: data.title,\n contentLength: data.text.length,\n passed: errors.length === 0,\n errors,\n warnings\n };\n } catch (error) {\n return {\n pagePath,\n url,\n status: null,\n title: '',\n contentLength: 0,\n passed: false,\n errors: [`navigation error: ${error.message}`],\n warnings: []\n };\n } finally {\n await page.close();\n }\n}\n\nasync function run() {\n if (!ALLOWED_SCOPES.has(versionScope)) {\n console.error(`❌ Invalid --version value: \"${versionScopeRaw}\". Use one of: v1, v2, both`);\n return 1;\n }\n\n const startedAt = new Date();\n const allPages = stagedOnly ? getStagedDocsPages() : getAllDocsPages();\n const shouldSkipInternal = baseUrl.includes('docs.livepeer.org');\n const pages = [...new Set(allPages)]\n .filter(isVersionSelected)\n .filter((pagePath) => !(shouldSkipInternal && pagePath.startsWith('v2/internal/')));\n\n if (pages.length === 0) {\n console.log('ℹ️ No matching docs pages to audit.');\n if (stagedOnly) {\n console.log('ℹ️ Staged mode with no matching pages; leaving existing reports unchanged.');\n return 0;\n }\n const emptyReport = {\n timestamp: startedAt.toISOString(),\n baseUrl,\n mode: stagedOnly ? 'staged' : 'all',\n versionScope,\n total: 0,\n passed: 0,\n failed: 0,\n results: []\n };\n fs.mkdirSync(path.dirname(REPORT_PATH), { recursive: true });\n fs.writeFileSync(REPORT_PATH, JSON.stringify(emptyReport, null, 2));",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:domain"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:domain:v1"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:domain:v2"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:domain:both"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tests/integration/domain-page-load-report.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tests/integration/domain-page-load-report.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tests/integration/domain-page-load-report.json, tests/integration/domain-page-load-report.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:domain); manual (npm script: test:domain:v1); manual (npm script: test:domain:v2); manual (npm script: test:domain:both)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/integration/mdx-component-runtime-smoke.js",
+ "script": "mdx-component-runtime-smoke",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests/integration, .githooks/server-manager.js, tests/run-pr-checks.js",
+ "owner": "docs",
+ "needs": "E-R1, R-R29",
+ "purpose_statement": "Smoke-tests sentinel MDX routes for runtime component failures, focused on page-killing render errors from MDX-imported JSX modules.",
+ "pipeline_declared": "manual",
+ "usage": "node tests/integration/mdx-component-runtime-smoke.js [--routes route[,route...]] [--base-url http://localhost:3000]",
+ "header": "#!/usr/bin/env node\n/**\n * @script mdx-component-runtime-smoke\n * @category validator\n * @purpose qa:content-quality\n * @scope tests/integration, .githooks/server-manager.js, tests/run-pr-checks.js\n * @owner docs\n * @needs E-R1, R-R29\n * @purpose-statement Smoke-tests sentinel MDX routes for runtime component failures, focused on page-killing render errors from MDX-imported JSX modules.\n * @pipeline manual\n * @usage node tests/integration/mdx-component-runtime-smoke.js [--routes route[,route...]] [--base-url http://localhost:3000]\n */\n\nconst path = require('path');\n\nlet puppeteer;\ntry {\n puppeteer = require('puppeteer');\n} catch (_error) {\n puppeteer = require(path.join(process.cwd(), 'tools', 'node_modules', 'puppeteer'));\n}\nconst {\n ensureServerRunning,\n stopServer,\n getServerUrl\n} = require('../../.githooks/server-manager');\n\nconst DEFAULT_TIMEOUT_MS = 30000;\nconst SENTINEL_ROUTES = [\n '/v2/home/mission-control',\n '/v2/about/portal',\n '/v2/developers/portal',\n '/v2/gateways/gateways-portal',\n '/v2/resources/documentation-guide/component-library/primitives'\n];\nconst SENTINEL_ROUTE_FILES = new Set([\n 'v2/home/mission-control.mdx',\n 'v2/about/portal.mdx',\n 'v2/developers/portal.mdx',\n 'v2/gateways/gateways-portal.mdx',\n 'v2/resources/documentation-guide/component-library/primitives.mdx'\n]);\nconst BLOCKING_CONSOLE_PATTERNS = [\n /ReferenceError/i,\n /Cannot access/i,\n /is not defined/i,\n /BlinkingIcon/i,\n /normalizeIconName/i,\n /normalizeIconSize/i\n];\nconst BLOCKING_BODY_PATTERNS = [\n /Failed to render/i,\n /ReferenceError/i,\n /normalizeIconName/i,\n /normalizeIconSize/i\n];\nconst COMPONENT_CHANGE_RE = /^snippets\\/components\\/.+\\.jsx$/;\n\nfunction toPosix(value) {\n return String(value || '').replace(/\\\\/g, '/');\n}\n\nfunction usage() {\n console.log(\n [\n 'Usage: node tests/integration/mdx-component-runtime-smoke.js [options]',\n '',\n 'Options:',\n ` --routes Override sentinel routes (default: ${SENTINEL_ROUTES.join(', ')})`,\n ' --base-url Use an existing Mintlify base URL instead of server-manager discovery',\n ' --help, -h Show this message'\n ].join('\\n')\n );\n}\n\nfunction parseArgs(argv) {\n const args = {\n routes: [],\n baseUrl: String(process.env.MINT_BASE_URL || '').trim(),\n help: false\n };\n\n for (let index = 0; index < argv.length; index += 1) {\n const token = argv[index];\n\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n\n if (token === '--routes') {\n const raw = String(argv[index + 1] || '').trim();\n if (raw) {\n raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((route) => args.routes.push(route));\n }\n index += 1;\n continue;\n }\n\n if (token.startsWith('--routes=')) {\n token\n .slice('--routes='.length)\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((route) => args.routes.push(route));\n continue;\n }\n\n if (token === '--base-url') {\n args.baseUrl = String(argv[index + 1] || '').trim();\n index += 1;\n continue;\n }\n\n if (token.startsWith('--base-url=')) {\n args.baseUrl = token.slice('--base-url='.length).trim();\n continue;\n }\n\n throw new Error(`Unknown argument: ${token}`);\n }\n\n args.routes = [...new Set(args.routes)];\n return args;\n}\n\nfunction normalizeRoute(route) {\n const raw = String(route || '').trim();\n if (!raw) return '';\n return raw.startsWith('/') ? raw : `/${raw}`;\n}\n\nfunction getRoutes(args = {}) {\n const routes = Array.isArray(args.routes) && args.routes.length > 0 ? args.routes : SENTINEL_ROUTES;\n return [...new Set(routes.map(normalizeRoute).filter(Boolean))];\n}\n\nfunction isBlockingConsoleMessage(type, text) {\n if (String(type || '').toLowerCase() !== 'error') return false;\n return BLOCKING_CONSOLE_PATTERNS.some((pattern) => pattern.test(String(text || '')));\n}\n\nfunction isBlockingPageError(message) {\n return BLOCKING_CONSOLE_PATTERNS.some((pattern) => pattern.test(String(message || '')));\n}\n\nfunction classifyDomFailure(snapshot) {\n const status = Number(snapshot?.status || 0);\n const bodyText = String(snapshot?.bodyText || '');\n const bodyLength = Number(snapshot?.bodyLength || 0);\n\n if (status === 404) {\n return `Route returned HTTP 404`;\n }\n\n if (snapshot?.is404) {\n return 'Page rendered a 404 state';\n }\n\n if (snapshot?.hasErrorBoundary) {\n return 'Page rendered an error boundary';\n }\n\n if (bodyLength < 500) {\n return `Page appears empty or failed to render (${bodyLength} chars)`;\n }\n\n const blockingBodyPattern = BLOCKING_BODY_PATTERNS.find((pattern) => pattern.test(bodyText));\n if (blockingBodyPattern) {\n return `Page body includes blocking runtime error text (${blockingBodyPattern})`;\n }\n\n return '';\n}\n\nfunction shouldRunForChangedFiles(changedFiles) {\n return changedFiles.some((filePath) => {\n const relPath = toPosix(filePath);\n return (\n COMPONENT_CHANGE_RE.test(relPath) ||\n relPath === 'tools/scripts/validators/components/check-mdx-component-scope.js' ||\n relPath === 'tests/integration/mdx-component-runtime-smoke.js' ||\n SENTINEL_ROUTE_FILES.has(relPath)\n );\n });\n}\n\nasync function testRoute(browser, route, baseUrl) {\n const page = await browser.newPage();\n const consoleErrors = [];\n const pageErrors = [];\n\n await page.setViewport({ width: 1440, height: 900 });\n\n page.on('console', (msg) => {\n const type = msg.type();\n const text = msg.text();\n if (isBlockingConsoleMessage(type, text)) {\n consoleErrors.push(`Console ${type}: ${text}`);\n }\n });\n\n page.on('pageerror', (error) => {\n if (isBlockingPageError(error.message)) {\n pageErrors.push(`Page error: ${error.message}`);\n }\n });\n\n let response;\n try {\n response = await page.goto(`${baseUrl}${route}`, {\n waitUntil: 'networkidle2',\n timeout: DEFAULT_TIMEOUT_MS\n });\n await new Promise((resolve) => setTimeout(resolve, 1500));",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "script",
+ "caller": "tests/unit/mdx-component-runtime-smoke.test.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "indirect via tests/unit/mdx-component-runtime-smoke.test.js",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Indirect"
+ },
+ {
+ "path": "tests/integration/openapi-reference-audit.js",
+ "script": "openapi-reference-audit",
+ "category": "validator",
+ "purpose": "tooling:api-spec",
+ "scope": "tests/integration, v2, api, .github/workflows",
+ "owner": "docs",
+ "needs": "F-R17",
+ "purpose_statement": "Comprehensive OpenAPI spec validation — checks references, schemas, examples. Supports --strict (validate), --fix (repair), and report modes.",
+ "pipeline_declared": "P2, P3, P5, P6",
+ "usage": "node tests/integration/openapi-reference-audit.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script openapi-reference-audit\n * @category validator\n * @purpose tooling:api-spec\n * @scope tests/integration, v2, api, .github/workflows\n * @owner docs\n * @needs F-R17\n * @purpose-statement Comprehensive OpenAPI spec validation — checks references, schemas, examples. Supports --strict (validate), --fix (repair), and report modes.\n * @pipeline P2, P3, P5, P6\n * @dualmode --strict (enforcer) | --fix (remediator)\n * @usage node tests/integration/openapi-reference-audit.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst yaml = require('js-yaml');\nconst { execSync } = require('child_process');\n\nconst FINDING_TYPES = {\n INVALID_REFERENCE_FORMAT: 'invalid-reference-format',\n MISSING_SPEC_MAPPING: 'missing-spec-mapping',\n SPEC_LOAD_ERROR: 'spec-load-error',\n ENDPOINT_NOT_FOUND: 'endpoint-not-found-in-spec',\n INTRA_FILE_MISMATCH: 'intra-file-openapi-mismatch'\n};\n\nconst HTTP_METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS', 'TRACE']);\n\nconst DEFAULT_REPORT = 'tasks/reports/openapi-reference/openapi-reference-audit.md';\nconst DEFAULT_REPORT_JSON = 'tasks/reports/openapi-reference/openapi-reference-audit.json';\n\nconst SPEC_BY_KEY = {\n studio: 'api/studio.yaml',\n ai: 'api/openapi.yaml',\n cliHttp: 'api/cli-http.yaml'\n};\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nconst REPO_ROOT = getRepoRoot();\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction relFromRoot(absPath) {\n return toPosix(path.relative(REPO_ROOT, absPath));\n}\n\nfunction isExcludedV2ExperimentalPath(relPath) {\n const rel = toPosix(relPath).replace(/^\\/+/, '');\n if (!rel.startsWith('v2/')) return false;\n return rel.split('/').some((segment) => segment.toLowerCase().startsWith('x-'));\n}\n\nfunction walkFiles(dirPath, out = []) {\n if (!fs.existsSync(dirPath)) return out;\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n for (const entry of entries) {\n const abs = path.join(dirPath, entry.name);\n const rel = relFromRoot(abs);\n if (entry.isDirectory()) {\n if (entry.name === '.git' || entry.name === 'node_modules') continue;\n if (rel.startsWith('v2/') && isExcludedV2ExperimentalPath(rel)) continue;\n walkFiles(abs, out);\n } else if (/\\.(md|mdx)$/i.test(entry.name)) {\n if (rel.startsWith('v2/') && !isExcludedV2ExperimentalPath(rel)) {\n out.push(abs);\n }\n }\n }\n return out;\n}\n\nfunction normalizePath(rawPath) {\n const trimmed = String(rawPath || '').trim();\n if (!trimmed) return '';\n return `/${trimmed.replace(/^\\/+/, '')}`;\n}\n\nfunction normalizeMethod(rawMethod) {\n return String(rawMethod || '').trim().toUpperCase();\n}\n\nfunction normalizeEndpoint(method, endpointPath) {\n const normalizedMethod = normalizeMethod(method);\n const normalizedPath = normalizePath(endpointPath);\n if (!HTTP_METHODS.has(normalizedMethod) || !normalizedPath) {\n return null;\n }\n return {\n method: normalizedMethod,\n path: normalizedPath,\n key: `${normalizedMethod} ${normalizedPath}`\n };\n}\n\nfunction parseMethodPathValue(rawValue) {\n const text = String(rawValue || '').trim();\n const match = text.match(/^([A-Za-z]+)\\s+(.+)$/);\n if (!match) return null;\n return normalizeEndpoint(match[1], match[2]);\n}\n\nfunction isVersionLiteral(value) {\n return /^\\d+\\.\\d+(?:\\.\\d+)?$/.test(String(value || '').trim());\n}\n\nfunction isSpecPointer(value) {\n const text = String(value || '').trim().toLowerCase();\n return /\\.ya?ml$/.test(text) || /\\.json$/.test(text);\n}\n\nfunction isIgnoredFrontmatterOpenapiValue(value) {\n return isVersionLiteral(value) || isSpecPointer(value);\n}\n\nfunction parseArgs(argv) {\n const args = {\n mode: 'full',\n files: [],\n strict: false,\n fix: false,\n write: false,\n report: path.join(REPO_ROOT, DEFAULT_REPORT),\n reportJson: path.join(REPO_ROOT, DEFAULT_REPORT_JSON)\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--full') {\n args.mode = 'full';\n } else if (token === '--files' || token === '--file') {\n const raw = String(argv[i + 1] || '').trim();\n if (raw) {\n raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((part) => args.files.push(part));\n }\n i += 1;\n } else if (token === '--strict') {\n args.strict = true;\n } else if (token === '--fix') {\n args.fix = true;\n } else if (token === '--write') {\n args.write = true;\n } else if (token === '--report') {\n args.report = path.resolve(REPO_ROOT, String(argv[i + 1] || ''));\n i += 1;\n } else if (token === '--report-json') {\n args.reportJson = path.resolve(REPO_ROOT, String(argv[i + 1] || ''));\n i += 1;\n } else {\n throw new Error(`Unknown option: ${token}`);\n }\n }\n\n args.files = [...new Set(args.files)];\n if (args.files.length > 0) {\n args.mode = 'files';\n }\n\n if (args.write && !args.fix) {\n throw new Error('--write requires --fix');\n }\n\n return args;\n}\n\nfunction parseFrontmatter(content) {\n const match = content.match(/^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n?/);\n if (!match) {\n return {\n exists: false,\n raw: '',\n data: null,\n startIndex: -1,\n endIndex: -1,\n lineOffset: 0,\n parseError: null\n };\n }\n\n let data = null;\n let parseError = null;\n try {\n data = yaml.load(match[1]);\n } catch (error) {\n parseError = error.message;\n }\n\n const lineOffset = content.slice(0, match.index).split('\\n').length - 1;\n\n return {\n exists: true,\n raw: match[1],\n data,\n startIndex: match.index,\n endIndex: match.index + match[0].length,\n lineOffset,\n parseError\n };\n}\n\nfunction getFrontmatterOpenapiLine(frontmatter) {\n if (!frontmatter.exists) return 1;\n const lines = frontmatter.raw.split('\\n');\n const openapiIndex = lines.findIndex((line) => /^\\s*openapi\\s*:/.test(line));\n if (openapiIndex === -1) return frontmatter.lineOffset + 1;\n return frontmatter.lineOffset + openapiIndex + 2;\n}",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-push",
+ "caller": ".github/workflows/openapi-reference-validation.yml",
+ "workflow": "OpenAPI Reference Validation",
+ "pipeline": "P2"
+ },
+ {
+ "type": "workflow-pr",
+ "caller": ".github/workflows/openapi-reference-validation.yml",
+ "workflow": "OpenAPI Reference Validation",
+ "pipeline": "P3"
+ },
+ {
+ "type": "workflow-schedule",
+ "caller": ".github/workflows/openapi-reference-validation.yml",
+ "workflow": "OpenAPI Reference Validation",
+ "pipeline": "P5",
+ "cron": "35 4 * * *"
+ },
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/openapi-reference-validation.yml",
+ "workflow": "OpenAPI Reference Validation",
+ "pipeline": "P6"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/openapi-reference-audit.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:openapi:audit"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P2 (OpenAPI Reference Validation); P3 (OpenAPI Reference Validation); P5 (OpenAPI Reference Validation cron 35 4 * * *); P6 (OpenAPI Reference Validation); indirect via tests/unit/openapi-reference-audit.test.js; manual (npm script: test:openapi:audit)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P2"
+ },
+ {
+ "path": "tests/integration/v2-link-audit.js",
+ "script": "v2-link-audit",
+ "category": "validator",
+ "purpose": "qa:link-integrity",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "E-R12, E-R14",
+ "purpose_statement": "Comprehensive V2 MDX link audit — checks internal links, external links, anchor refs. Supports --staged, --full, --strict, --write-links modes.",
+ "pipeline_declared": "P1, P5, P6",
+ "usage": "node tests/integration/v2-link-audit.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script v2-link-audit\n * @category validator\n * @purpose qa:link-integrity\n * @scope tests\n * @owner docs\n * @needs E-R12, E-R14\n * @purpose-statement Comprehensive V2 MDX link audit — checks internal links, external links, anchor refs. Supports --staged, --full, --strict, --write-links modes.\n * @pipeline P1, P5, P6\n * @dualmode --full (validator) | --write-links (remediator)\n * @usage node tests/integration/v2-link-audit.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst { extractImports } = require('../utils/mdx-parser');\nconst {\n getStagedFiles,\n isExcludedV2ExperimentalPath,\n filterPathsByMintIgnore\n} = require('../utils/file-walker');\n\nconst REPO_ROOT = getRepoRoot();\nconst LEGACY_V2_PAGES_DIR = path.join(REPO_ROOT, 'v2', 'pages');\nconst MODERN_V2_PAGES_DIR = path.join(REPO_ROOT, 'v2');\nconst V2_PAGES_DIR = fs.existsSync(LEGACY_V2_PAGES_DIR) ? LEGACY_V2_PAGES_DIR : MODERN_V2_PAGES_DIR;\nconst INDEX_PATH = path.join(V2_PAGES_DIR, 'index.mdx');\nconst DOCS_CONFIG_PATH = path.join(REPO_ROOT, 'docs.json');\nconst DEFAULT_REPORT = path.join(REPO_ROOT, 'tasks', 'reports', 'navigation-links', 'LINK_TEST_REPORT.md');\nconst DEFAULT_REPORT_JSON = path.join(REPO_ROOT, 'tasks', 'reports', 'navigation-links', 'LINK_TEST_REPORT.json');\nconst LINKABLE_ATTRS = ['href', 'src', 'srcset', 'poster', 'action', 'data', 'to', 'image', 'url'];\nconst EXCLUDED_ATTRS = new Set(['icon']);\nconst FILE_EXT_CANDIDATES = ['.mdx', '.md', '.jsx', '.js', '.tsx', '.ts', '.json'];\nconst INDEX_CANDIDATES = ['index.mdx', 'index.md', 'README.mdx', 'README.md'];\nconst EXTERNAL_UNTESTED = '🟡 untested-external';\nconst EXTERNAL_PENDING = 'external-pending';\nconst EXTERNAL_OK = 'external-ok';\nconst EXTERNAL_SOFT_FAIL = 'external-soft-fail';\nconst EXTERNAL_HARD_FAIL = 'external-hard-fail';\nconst EXTERNAL_POLICY_VALUES = new Set(['classify', 'validate']);\nconst EXTERNAL_LINK_TYPES_VALUES = new Set(['navigational', 'media', 'all']);\nconst HEAD_FALLBACK_STATUSES = new Set([401, 403, 405, 501]);\nconst TRANSIENT_STATUSES = new Set([429, 500, 502, 503, 504]);\nconst MEDIA_ATTRS = new Set(['src', 'srcset', 'poster', 'image']);\nconst NAV_ATTRS = new Set(['href', 'to']);\nconst MIGRATED_V2_DOMAIN_DIRS = new Set([\n 'home',\n 'about',\n 'solutions',\n 'community',\n 'developers',\n 'gateways',\n 'orchestrators',\n 'lpt',\n 'resources',\n 'internal',\n 'deprecated',\n 'experimental',\n 'notes'\n]);\nconst MISSING_LINK_ALLOWLIST = new Set([\n '/gateways/run-a-gateway/test/test-gateway',\n './protocol-economics'\n]);\nconst EXTRA_V2_DIRS = (() => {\n if (!fs.existsSync(LEGACY_V2_PAGES_DIR)) return [];\n const dirs = [];\n for (const domain of MIGRATED_V2_DOMAIN_DIRS) {\n const candidate = path.join(MODERN_V2_PAGES_DIR, domain);\n if (fs.existsSync(candidate)) dirs.push(candidate);\n }\n return dirs;\n})();\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nfunction toPosix(p) {\n return String(p || '').split(path.sep).join('/');\n}\n\nfunction relFromRoot(absPath) {\n return toPosix(path.relative(REPO_ROOT, absPath));\n}\n\nfunction relNoExt(absPath) {\n const rel = relFromRoot(absPath);\n return toPosix(rel.replace(/\\.(mdx|md)$/i, ''));\n}\n\nfunction isExcludedV2AbsPath(absPath) {\n const rel = relFromRoot(absPath);\n return isExcludedV2ExperimentalPath(rel);\n}\n\nfunction isActiveV2DocRel(relPath) {\n const rel = toPosix(String(relPath || ''));\n if (rel.startsWith('v2/pages/')) return true;\n if (!rel.startsWith('v2/')) return false;\n if (isExcludedV2ExperimentalPath(rel)) return false;\n const first = rel.slice('v2/'.length).split('/')[0];\n return MIGRATED_V2_DOMAIN_DIRS.has(first);\n}\n\nfunction stripV2DocsRoot(relPath) {\n const rel = toPosix(String(relPath || ''));\n if (rel.startsWith('v2/pages/')) return rel.slice('v2/pages/'.length);\n if (rel.startsWith('v2/')) return rel.slice('v2/'.length);\n return rel;\n}\n\nfunction parseIntegerFlag(raw, fallback, { min = 0 } = {}) {\n const parsed = Number(raw);\n if (!Number.isFinite(parsed)) return fallback;\n const floored = Math.floor(parsed);\n if (floored < min) return fallback;\n return floored;\n}\n\nfunction deriveJsonReportPath(reportPath) {\n const parsed = path.parse(path.resolve(REPO_ROOT, reportPath || DEFAULT_REPORT));\n if (!parsed.ext) {\n return path.join(parsed.dir, `${parsed.base}.json`);\n }\n return path.join(parsed.dir, `${parsed.name}.json`);\n}\n\nfunction parseArgs(argv) {\n const args = {\n mode: 'full',\n report: DEFAULT_REPORT,\n reportJson: DEFAULT_REPORT_JSON,\n respectMintIgnore: true,\n strict: false,\n strictRootsOnly: false,\n writeLinks: undefined,\n externalPolicy: 'classify',\n externalLinkTypes: 'navigational',\n externalTimeoutMs: 10000,\n externalConcurrency: 12,\n externalPerHostConcurrency: 2,\n externalRetries: 1,\n files: []\n };\n let reportExplicit = false;\n let reportJsonExplicit = false;\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--full') args.mode = 'full';\n else if (token === '--staged') args.mode = 'staged';\n else if (token === '--files' || token === '--file') {\n const raw = String(argv[i + 1] || '').trim();\n if (raw) {\n raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((part) => args.files.push(part));\n }\n i += 1;\n }\n else if (token === '--strict') args.strict = true;\n else if (token === '--strict-roots-only') args.strictRootsOnly = true;\n else if (token === '--no-mintignore') args.respectMintIgnore = false;\n else if (token === '--write-links') args.writeLinks = true;\n else if (token === '--no-write-links') args.writeLinks = false;\n else if (token === '--report') {\n args.report = path.resolve(REPO_ROOT, argv[i + 1] || '');\n reportExplicit = true;\n i += 1;\n } else if (token === '--report-json') {\n args.reportJson = path.resolve(REPO_ROOT, argv[i + 1] || '');\n reportJsonExplicit = true;\n i += 1;\n } else if (token === '--external-policy') {\n args.externalPolicy = String(argv[i + 1] || '').trim().toLowerCase();\n i += 1;\n } else if (token === '--external-link-types') {\n args.externalLinkTypes = String(argv[i + 1] || '').trim().toLowerCase();\n i += 1;\n } else if (token === '--external-timeout-ms') {\n args.externalTimeoutMs = parseIntegerFlag(argv[i + 1], args.externalTimeoutMs, { min: 1 });\n i += 1;\n } else if (token === '--external-concurrency') {\n args.externalConcurrency = parseIntegerFlag(argv[i + 1], args.externalConcurrency, { min: 1 });\n i += 1;\n } else if (token === '--external-per-host-concurrency') {\n args.externalPerHostConcurrency = parseIntegerFlag(argv[i + 1], args.externalPerHostConcurrency, { min: 1 });\n i += 1;\n } else if (token === '--external-retries') {\n args.externalRetries = parseIntegerFlag(argv[i + 1], args.externalRetries, { min: 0 });\n i += 1;\n } else {\n throw new Error(`Unknown option: ${token}`);\n }\n }\n\n if (!EXTERNAL_POLICY_VALUES.has(args.externalPolicy)) {\n throw new Error('Invalid --external-policy value. Use classify|validate.');\n }\n\n if (!EXTERNAL_LINK_TYPES_VALUES.has(args.externalLinkTypes)) {\n throw new Error('Invalid --external-link-types value. Use navigational|media|all.');\n }\n\n args.files = [...new Set(args.files)];\n if (args.files.length > 0) {\n args.mode = 'files';\n }\n\n if (typeof args.writeLinks === 'undefined') {\n args.writeLinks = args.mode === 'full';",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "pre-commit",
+ "caller": ".githooks/pre-commit",
+ "pipeline": "P1"
+ },
+ {
+ "type": "workflow-schedule",
+ "caller": ".github/workflows/v2-external-link-audit.yml",
+ "workflow": "V2 External Link Audit (Advisory)",
+ "pipeline": "P5",
+ "cron": "0 4 * * *"
+ },
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/v2-external-link-audit.yml",
+ "workflow": "V2 External Link Audit (Advisory)",
+ "pipeline": "P6"
+ },
+ {
+ "type": "script",
+ "caller": "tests/integration/v2-link-audit.selftest.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/create-codex-pr.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/v2-link-audit.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:link-audit"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:link-audit:staged"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:link-audit:external"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tests/integration/hrefs.jsx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tests/integration/hrefs.jsx",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 (pre-commit); P5 (V2 External Link Audit (Advisory) cron 0 4 * * *); P6 (V2 External Link Audit (Advisory)); indirect via tests/integration/v2-link-audit.selftest.js; indirect via tests/unit/create-codex-pr.test.js; indirect via tests/unit/v2-link-audit.test.js; manual (npm script: test:link-audit); manual (npm script: test:link-audit:staged); manual (npm script: test:link-audit:external)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/integration/v2-link-audit.selftest.js",
+ "script": "v2-link-audit.selftest",
+ "category": "validator",
+ "purpose": "qa:link-integrity",
+ "scope": "tests/integration",
+ "owner": "docs",
+ "needs": "E-R12, E-R14",
+ "purpose_statement": "Self-test suite for v2-link-audit.js — validates audit logic against known fixtures",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/integration/v2-link-audit.selftest.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script v2-link-audit.selftest\n * @category validator\n * @purpose qa:link-integrity\n * @scope tests/integration\n * @owner docs\n * @needs E-R12, E-R14\n * @purpose-statement Self-test suite for v2-link-audit.js — validates audit logic against known fixtures\n * @pipeline manual — not yet in pipeline\n * @dualmode --full (validator) | --write-links (remediator)\n * @usage node tests/integration/v2-link-audit.selftest.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst http = require('http');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst audit = require('./v2-link-audit');\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nasync function startServer() {\n const server = http.createServer((req, res) => {\n const url = req.url || '/';\n\n if (url.startsWith('/ok')) {\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('ok');\n return;\n }\n\n if (url.startsWith('/head-fallback')) {\n if (req.method === 'HEAD') {\n res.writeHead(405, { 'content-type': 'text/plain' });\n res.end();\n return;\n }\n res.writeHead(200, { 'content-type': 'text/plain' });\n res.end('fallback-ok');\n return;\n }\n\n if (url.startsWith('/soft')) {\n res.writeHead(503, { 'content-type': 'text/plain' });\n res.end('soft-fail');\n return;\n }\n\n if (url.startsWith('/hard')) {\n res.writeHead(404, { 'content-type': 'text/plain' });\n res.end('hard-fail');\n return;\n }\n\n if (url.startsWith('/media.png')) {\n res.writeHead(200, { 'content-type': 'image/png' });\n res.end('PNG');\n return;\n }\n\n res.writeHead(500, { 'content-type': 'text/plain' });\n res.end('unexpected route');\n });\n\n await new Promise((resolve, reject) => {\n server.listen(0, '127.0.0.1', (error) => {\n if (error) reject(error);\n else resolve();\n });\n });\n\n const addr = server.address();\n const base = `http://127.0.0.1:${addr.port}`;\n return { server, base };\n}\n\nasync function closeServer(server) {\n if (!server) return;\n await new Promise((resolve) => server.close(() => resolve()));\n}\n\nasync function run() {\n let server = null;\n let fixtureFile = '';\n const reportMd = '/tmp/v2-link-audit-selftest.md';\n const reportJson = '/tmp/v2-link-audit-selftest.json';\n\n try {\n const started = await startServer();\n server = started.server;\n const base = started.base;\n\n const root = getRepoRoot();\n const fixtureDir = path.join(root, 'v2', 'internal', 'reports', 'navigation-links');\n fixtureFile = path.join(fixtureDir, `v2-link-audit-selftest-${Date.now()}.mdx`);\n const relFixture = path.relative(root, fixtureFile).split(path.sep).join('/');\n\n const content = [\n '# V2 Link Audit Selftest Fixture',\n '',\n `[ok](${base}/ok)`,\n `[head fallback](${base}/head-fallback#anchor)`,\n `[soft](${base}/soft)`,\n `[hard](${base}/hard)`,\n 'invalid',\n ``\n ].join('\\n');\n\n fs.writeFileSync(fixtureFile, `${content}\\n`, 'utf8');\n\n const out = await audit.runAudit({\n argv: [\n '--files', relFixture,\n '--external-policy', 'validate',\n '--external-link-types', 'navigational',\n '--external-timeout-ms', '3000',\n '--external-concurrency', '4',\n '--external-per-host-concurrency', '2',\n '--external-retries', '0',\n '--no-write-links',\n '--report', reportMd,\n '--report-json', reportJson\n ]\n });\n\n assert.strictEqual(out.exitCode, 0);\n assert.strictEqual(out.externalValidation.eligibleRefCount, 5);\n assert.strictEqual(out.externalValidation.urlClassCounts[audit.EXTERNAL_OK], 2);\n assert.strictEqual(out.externalValidation.urlClassCounts[audit.EXTERNAL_SOFT_FAIL], 1);\n assert.strictEqual(out.externalValidation.urlClassCounts[audit.EXTERNAL_HARD_FAIL], 2);\n\n const json = JSON.parse(fs.readFileSync(reportJson, 'utf8'));\n const classes = json.external.urlResults.reduce((acc, row) => {\n acc[row.class] = (acc[row.class] || 0) + 1;\n return acc;\n }, {});\n\n assert.strictEqual(classes[audit.EXTERNAL_OK], 2);\n assert.strictEqual(classes[audit.EXTERNAL_SOFT_FAIL], 1);\n assert.strictEqual(classes[audit.EXTERNAL_HARD_FAIL], 2);\n\n const fileEntry = json.files.find((item) => item.file === relFixture);\n assert.ok(fileEntry, 'Expected fixture file in JSON report');\n\n const mediaRef = fileEntry.refs.find((ref) => String(ref.rawPath).includes('/media.png'));\n assert.ok(mediaRef, 'Expected media ref in fixture file refs');\n assert.strictEqual(mediaRef.status, audit.EXTERNAL_UNTESTED);\n\n console.log('✅ v2-link-audit selftest passed');\n return 0;\n } catch (error) {\n console.error(`❌ v2-link-audit selftest failed: ${error.message}`);\n return 1;\n } finally {\n if (fixtureFile && fs.existsSync(fixtureFile)) {\n fs.unlinkSync(fixtureFile);\n }\n await closeServer(server);\n }\n}\n\nif (require.main === module) {\n run().then((code) => process.exit(code));\n}\n\nmodule.exports = { run };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:link-audit:selftest"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:link-audit:selftest)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/integration/v2-wcag-audit.js",
+ "script": "v2-wcag-audit",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests/integration, tests/utils, tasks/reports, v2",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "WCAG accessibility audit for v2 pages — checks heading hierarchy, alt text, ARIA. Supports --fix mode for auto-repair.",
+ "pipeline_declared": "P1",
+ "usage": "node tests/integration/v2-wcag-audit.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script v2-wcag-audit\n * @category validator\n * @purpose qa:content-quality\n * @scope tests/integration, tests/utils, tasks/reports, v2\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement WCAG accessibility audit for v2 pages — checks heading hierarchy, alt text, ARIA. Supports --fix mode for auto-repair.\n * @pipeline P1\n * @usage node tests/integration/v2-wcag-audit.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync, spawnSync } = require('child_process');\nconst { URL } = require('url');\nconst {\n getV2DocsFiles,\n toDocsRouteKeyFromFileV2Aware,\n isExcludedV2ExperimentalPath,\n filterPathsByMintIgnore\n} = require('../utils/file-walker');\n\nconst REPO_ROOT = getRepoRoot();\nif (process.cwd() !== REPO_ROOT) {\n process.chdir(REPO_ROOT);\n}\nconst DEFAULT_REPORT_MD = path.join(REPO_ROOT, 'tasks', 'reports', 'quality-accessibility', 'v2-wcag-audit-report.md');\nconst DEFAULT_REPORT_JSON = path.join(REPO_ROOT, 'tasks', 'reports', 'quality-accessibility', 'v2-wcag-audit-report.json');\nconst DEFAULT_TIMEOUT_MS = 30000;\nconst DEFAULT_WAIT_AFTER_NAV_MS = 2000;\nconst WCAG_PROFILE = 'WCAG 2.2 AA';\nconst WCAG_TAGS = ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa'];\nconst BEST_PRACTICE_TAGS = ['best-practice'];\nconst ADVISORY_ONLY_RULES = new Set(['color-contrast']);\nconst IMPACT_ORDER = ['minor', 'moderate', 'serious', 'critical'];\nconst IMPACT_RANK = {\n none: -1,\n minor: 0,\n moderate: 1,\n serious: 2,\n critical: 3,\n unknown: 0\n};\nconst VALID_FAIL_IMPACTS = new Set(['critical', 'serious', 'moderate', 'minor', 'none']);\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction relFromRoot(absPath) {\n return toPosix(path.relative(REPO_ROOT, absPath));\n}\n\nfunction parseArgs(argv) {\n const args = {\n mode: 'full',\n files: [],\n respectMintIgnore: true,\n fix: true,\n stage: false,\n maxPages: null,\n baseUrl: process.env.MINT_BASE_URL || '',\n failImpact: 'serious',\n report: DEFAULT_REPORT_MD,\n reportJson: DEFAULT_REPORT_JSON,\n timeoutMs: DEFAULT_TIMEOUT_MS\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--full') {\n args.mode = 'full';\n continue;\n }\n if (token === '--staged') {\n args.mode = 'staged';\n continue;\n }\n if (token === '--files' || token === '--file') {\n const raw = String(argv[i + 1] || '').trim();\n if (raw) {\n raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((part) => args.files.push(part));\n }\n i += 1;\n continue;\n }\n if (token === '--no-mintignore') {\n args.respectMintIgnore = false;\n continue;\n }\n if (token === '--fix') {\n args.fix = true;\n continue;\n }\n if (token === '--no-fix') {\n args.fix = false;\n continue;\n }\n if (token === '--stage') {\n args.stage = true;\n continue;\n }\n if (token === '--max-pages') {\n const parsed = Number(argv[i + 1]);\n if (Number.isFinite(parsed) && parsed >= 0) {\n args.maxPages = Math.floor(parsed);\n }\n i += 1;\n continue;\n }\n if (token === '--base-url') {\n args.baseUrl = String(argv[i + 1] || '').trim();\n i += 1;\n continue;\n }\n if (token === '--fail-impact') {\n const value = String(argv[i + 1] || '').trim().toLowerCase();\n if (!VALID_FAIL_IMPACTS.has(value)) {\n throw new Error(`Invalid --fail-impact value: ${value}. Use critical|serious|moderate|minor|none`);\n }\n args.failImpact = value;\n i += 1;\n continue;\n }\n if (token === '--report') {\n args.report = path.resolve(REPO_ROOT, argv[i + 1] || '');\n i += 1;\n continue;\n }\n if (token === '--report-json') {\n args.reportJson = path.resolve(REPO_ROOT, argv[i + 1] || '');\n i += 1;\n continue;\n }\n if (token === '--help' || token === '-h') {\n args.help = true;\n continue;\n }\n throw new Error(`Unknown option: ${token}`);\n }\n\n args.files = [...new Set(args.files)].map((p) => normalizeInputPath(p)).filter(Boolean);\n if (args.files.length > 0) {\n args.mode = 'files';\n }\n\n if (args.mode === 'staged' && args.maxPages === null) {\n // Keep staged/files runs bounded by default for local hook-style usage.\n args.maxPages = 10;\n }\n\n return args;\n}\n\nfunction normalizeInputPath(input) {\n return toPosix(String(input || '').trim().replace(/^\\.\\//, '').replace(/^\\//, ''));\n}\n\nfunction printHelp() {\n console.log(`Usage: node tests/integration/v2-wcag-audit.js [--full|--staged|--files ] [--no-mintignore] [--fix|--no-fix] [--stage] [--max-pages ] [--base-url ] [--fail-impact ] [--report ] [--report-json ]`);\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction severityMeetsThreshold(impact, threshold) {\n if (String(threshold) === 'none') return false;\n const impactKey = normalizeImpact(impact);\n return (IMPACT_RANK[impactKey] ?? IMPACT_RANK.unknown) >= (IMPACT_RANK[threshold] ?? IMPACT_RANK.serious);\n}\n\nfunction normalizeImpact(impact) {\n const value = String(impact || '').trim().toLowerCase();\n if (!value) return 'unknown';\n if (value === 'critical' || value === 'serious' || value === 'moderate' || value === 'minor') return value;\n if (value === 'none') return 'none';\n return 'unknown';\n}\n\nfunction impactCounts(items, field = 'impact') {\n const counts = { critical: 0, serious: 0, moderate: 0, minor: 0, unknown: 0 };\n (items || []).forEach((item) => {\n const key = normalizeImpact(item?.[field]);\n if (key === 'none') return;\n if (!Object.prototype.hasOwnProperty.call(counts, key)) counts.unknown += 1;\n else counts[key] += 1;\n });\n return counts;\n}\n\nfunction sortedObjectEntries(obj) {\n return Object.entries(obj || {}).sort((a, b) => a[0].localeCompare(b[0]));\n}\n\nfunction getLineNumberFromIndex(content, index) {\n if (!Number.isFinite(index) || index < 0) return 1;\n let line = 1;\n for (let i = 0; i < index && i < content.length; i += 1) {\n if (content[i] === '\\n') line += 1;\n }\n return line;\n}\n\nfunction buildSkipRanges(content) {\n const ranges = [];",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "pre-commit",
+ "caller": ".githooks/pre-commit",
+ "pipeline": "P1"
+ },
+ {
+ "type": "runner",
+ "caller": "tests/run-all.js",
+ "pipeline": "P1"
+ },
+ {
+ "type": "script",
+ "caller": "tests/integration/v2-wcag-audit.selftest.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-all.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/v2-wcag-audit.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:wcag"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:wcag:staged"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:wcag:nofix"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 (pre-commit); P1 via run-all; indirect via tests/integration/v2-wcag-audit.selftest.js; indirect via tests/run-all.js; indirect via tests/unit/v2-wcag-audit.test.js; manual (npm script: test:wcag); manual (npm script: test:wcag:staged); manual (npm script: test:wcag:nofix)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/integration/v2-wcag-audit.selftest.js",
+ "script": "v2-wcag-audit.selftest",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests/integration, v2, git index",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Self-test suite for v2-wcag-audit.js — validates WCAG audit logic against known fixtures",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/integration/v2-wcag-audit.selftest.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script v2-wcag-audit.selftest\n * @category validator\n * @purpose qa:content-quality\n * @scope tests/integration, v2, git index\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Self-test suite for v2-wcag-audit.js — validates WCAG audit logic against known fixtures\n * @pipeline manual — not yet in pipeline\n * @usage node tests/integration/v2-wcag-audit.selftest.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst http = require('http');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\nconst puppeteer = require('puppeteer');\nconst wcag = require('./v2-wcag-audit');\n\nfunction getRepoRoot() {\n return process.cwd();\n}\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nasync function withHttpFixture(html, fn) {\n const server = http.createServer((req, res) => {\n res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n res.end(html);\n });\n\n await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));\n const addr = server.address();\n const url = `http://127.0.0.1:${addr.port}`;\n\n try {\n return await fn(url);\n } finally {\n await new Promise((resolve) => server.close(resolve));\n }\n}\n\nfunction runGit(args) {\n const cmd = spawnSync('git', args, { cwd: getRepoRoot(), encoding: 'utf8' });\n return {\n status: cmd.status,\n stdout: (cmd.stdout || '').trim(),\n stderr: (cmd.stderr || '').trim()\n };\n}\n\nasync function testBrowserAxeFixture() {\n const html = `\n\n \n \n WCAG Fixture \n \n \n \n Fixture
\n
\n 🔗\n Focusable\n \n \n`;\n\n const browser = await puppeteer.launch({ headless: 'new', args: ['--no-sandbox', '--disable-setuid-sandbox'] });\n try {\n return await withHttpFixture(html, async (url) => {\n const result = await wcag.runAxeOnUrl(browser, url, {\n file: 'v2/internal/wcag-fixture.mdx',\n routeKey: 'v2/internal/wcag-fixture'\n });\n\n assert.strictEqual(result.ok, true);\n assert.ok(Array.isArray(result.wcagViolations));\n assert.ok(Array.isArray(result.bestPracticeViolations));\n assert.ok(result.wcagViolations.length >= 1, 'expected at least one WCAG violation');\n const ids = new Set(result.wcagViolations.map((v) => v.id));\n assert.ok(ids.has('image-alt') || ids.has('link-name'), 'expected image-alt or link-name violation');\n result.wcagViolations.forEach((v) => {\n assert.strictEqual(v.type, 'wcag');\n assert.ok(['critical', 'serious', 'moderate', 'minor', 'unknown'].includes(v.impact));\n });\n result.bestPracticeViolations.forEach((v) => {\n assert.strictEqual(v.type, 'best-practice');\n });\n return result;\n });\n } finally {\n await browser.close();\n }\n}\n\nasync function testRunAuditFixAndStage() {\n const root = getRepoRoot();\n const nonce = `${Date.now()}-${Math.floor(Math.random() * 100000)}`;\n const rel = `v2/internal/__wcag-selftest-${nonce}.mdx`;\n const abs = path.join(root, rel);\n const reportMd = `/tmp/v2-wcag-selftest-${nonce}.md`;\n const reportJson = `/tmp/v2-wcag-selftest-${nonce}.json`;\n\n fs.writeFileSync(abs, '# Temp WCAG Selftest\\n\\n
\\n', 'utf8');\n\n const testV2Pages = require('../../tools/scripts/test-v2-pages');\n const originalGetV2Pages = testV2Pages.getV2Pages;\n testV2Pages.getV2Pages = function patchedGetV2Pages() {\n const base = originalGetV2Pages.call(this);\n const route = rel.replace(/\\.mdx?$/i, '');\n return [...new Set([...base, route])];\n };\n\n try {\n const noFix = await wcag.runAudit({ argv: ['--files', rel, '--no-fix', '--max-pages', '0', '--report', reportMd, '--report-json', reportJson] });\n assert.ok(fs.readFileSync(abs, 'utf8').includes('
'));\n assert.strictEqual(noFix.results.length, 1);\n assert.strictEqual(noFix.results[0].kind, 'static-only');\n assert.strictEqual(noFix.results[0].autofixes.length, 0);\n assert.ok(noFix.results[0].staticFindings.some((f) => f.rule === 'raw-img-missing-alt'));\n\n const withFix = await wcag.runAudit({ argv: ['--files', rel, '--fix', '--stage', '--max-pages', '0', '--report', reportMd, '--report-json', reportJson] });\n const fixedContent = fs.readFileSync(abs, 'utf8');\n assert.ok(/
]*\\balt=/.test(fixedContent), 'expected img alt autofix');\n assert.ok(withFix.summary.totals.autofixes >= 1);\n\n const staged = runGit(['diff', '--cached', '--name-only', '--', rel]);\n assert.strictEqual(staged.status, 0);\n assert.ok(staged.stdout.split('\\n').map((s) => s.trim()).includes(rel));\n\n // Unstage only the temp file to avoid touching user changes.\n const unstage = runGit(['restore', '--staged', '--', rel]);\n if (unstage.status !== 0) {\n const fallback = runGit(['rm', '--cached', '-q', '--', rel]);\n assert.strictEqual(fallback.status, 0, fallback.stderr || fallback.stdout);\n }\n } finally {\n testV2Pages.getV2Pages = originalGetV2Pages;\n try {\n fs.unlinkSync(abs);\n } catch (_error) {\n // ignore\n }\n try {\n const cleanup = runGit(['restore', '--staged', '--', rel]);\n if (cleanup.status !== 0) runGit(['rm', '--cached', '-q', '--', rel]);\n } catch (_error) {\n // ignore\n }\n try {\n fs.unlinkSync(reportMd);\n } catch (_error) {\n // ignore\n }\n try {\n fs.unlinkSync(reportJson);\n } catch (_error) {\n // ignore\n }\n }\n}\n\nasync function main() {\n const failures = [];\n const run = async (name, fn) => {\n process.stdout.write(`🧪 ${name}... `);\n try {\n await fn();\n console.log('✅');\n } catch (error) {\n console.log('❌');\n failures.push({ name, error });\n }\n };\n\n await run('Browser axe fixture captures WCAG violations and separates best-practice results', testBrowserAxeFixture);\n await run('runAudit respects --no-fix and stages autofix changes with --stage', testRunAuditFixAndStage);\n\n if (failures.length > 0) {\n console.error(`\\n❌ ${failures.length} self-test(s) failed`);\n failures.forEach((f) => console.error(` - ${f.name}: ${f.error.message}`));\n process.exit(1);\n }\n\n console.log('\\n✅ v2 WCAG audit self-tests passed (2 cases)');\n}\n\nmain().catch((error) => {\n console.error(error);\n process.exit(1);\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:wcag:selftest"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tests/integration/v2/internal/__wcag-selftest-${nonce}.mdx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tests/integration/v2/internal/__wcag-selftest-${nonce}.mdx",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:wcag:selftest)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/run-all.js",
+ "script": "run-all",
+ "category": "orchestrator",
+ "purpose": "infrastructure:pipeline-orchestration",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "R-R29",
+ "purpose_statement": "Test orchestrator — dispatches all unit test suites. Called by pre-commit hook and npm test.",
+ "pipeline_declared": "P1, P2, P3",
+ "usage": "node tests/run-all.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script run-all\n * @category orchestrator\n * @purpose infrastructure:pipeline-orchestration\n * @scope tests\n * @owner docs\n * @needs R-R29\n * @purpose-statement Test orchestrator — dispatches all unit test suites. Called by pre-commit hook and npm test.\n * @pipeline P1, P2, P3\n * @usage node tests/run-all.js [flags]\n */\n/**\n * Main test runner - orchestrates all test suites\n */\nconst { spawnSync } = require('child_process');\nconst path = require('path');\n\nconst styleGuideTests = require('./unit/style-guide.test');\nconst mdxTests = require('./unit/mdx.test');\nconst mdxGuardsTests = require('./unit/mdx-guards.test');\nconst mdxSafeMarkdownUnitTests = require('./unit/mdx-safe-markdown.test');\nconst spellingTests = require('./unit/spelling.test');\nconst qualityTests = require('./unit/quality.test');\nconst linksImportsTests = require('./unit/links-imports.test');\nconst docsNavigationTests = require('./unit/docs-navigation.test');\nconst scriptDocsTests = require('./unit/script-docs.test');\nconst componentGovernanceUtilsTests = require('./unit/component-governance-utils.test');\nconst componentGovernanceGeneratorTests = require('./unit/component-governance-generators.test');\nconst componentNamingTests = require('../tools/scripts/validators/components/check-naming-conventions');\nconst mdxSafeMarkdownValidator = require('../tools/scripts/validators/content/check-mdx-safe-markdown');\nconst pagesIndexGenerator = require('../tools/scripts/generate-pages-index');\nconst browserTests = require('./integration/browser.test');\nconst REPO_ROOT = path.resolve(__dirname, '..');\n\nconst args = process.argv.slice(2);\nconst stagedOnly = args.includes('--staged');\nconst skipBrowser = args.includes('--skip-browser');\nconst watch = args.includes('--watch');\nconst runWcag = args.includes('--wcag');\nconst wcagNoFix = args.includes('--wcag-no-fix');\n\nlet totalErrors = 0;\nlet totalWarnings = 0;\n\n/**\n * Run all tests\n */\nasync function runAllTests() {\n console.log('🧪 Running Livepeer Documentation Test Suite\\n');\n console.log('='.repeat(60));\n \n // Style Guide Tests\n console.log('\\n📋 Running Style Guide Tests...');\n const styleResult = styleGuideTests.runTests({ stagedOnly });\n totalErrors += styleResult.errors.length;\n totalWarnings += styleResult.warnings.length;\n console.log(` ${styleResult.errors.length} errors, ${styleResult.warnings.length} warnings`);\n\n // Component Naming\n console.log('\\n🧩 Running Component Naming Checks...');\n const componentNamingResult = componentNamingTests.run();\n componentNamingResult.findings.forEach((finding) => {\n console.error(` ${componentNamingTests.formatFinding(finding)}`);\n });\n totalErrors += componentNamingResult.findings.length;\n console.log(` ${componentNamingResult.findings.length} errors, 0 warnings`);\n \n // MDX Tests\n console.log('\\n📄 Running MDX Validation Tests...');\n const mdxResult = mdxTests.runTests({ stagedOnly });\n totalErrors += mdxResult.errors.length;\n totalWarnings += mdxResult.warnings.length;\n console.log(` ${mdxResult.errors.length} errors, ${mdxResult.warnings.length} warnings`);\n\n // Repo-wide MDX-safe Markdown Validation\n console.log('\\n🧱 Running Repo-wide MDX-safe Markdown Validation...');\n const mdxSafeMarkdownResult = mdxSafeMarkdownValidator.run({\n args: {\n stagedOnly,\n files: [],\n json: false\n }\n });\n totalErrors += mdxSafeMarkdownResult.errors.length;\n totalWarnings += mdxSafeMarkdownResult.warnings.length;\n console.log(` ${mdxSafeMarkdownResult.errors.length} errors, ${mdxSafeMarkdownResult.warnings.length} warnings`);\n\n // MDX Guardrails\n console.log('\\n🛡️ Running MDX Guardrail Tests...');\n const mdxGuardsResult = mdxGuardsTests.runTests();\n totalErrors += mdxGuardsResult.errors.length;\n totalWarnings += mdxGuardsResult.warnings.length;\n console.log(` ${mdxGuardsResult.errors.length} errors, ${mdxGuardsResult.warnings.length} warnings`);\n\n // MDX-safe Markdown Unit Tests\n console.log('\\n🧪 Running MDX-safe Markdown Unit Tests...');\n const mdxSafeMarkdownUnitResult = mdxSafeMarkdownUnitTests.runTests();\n totalErrors += mdxSafeMarkdownUnitResult.errors.length;\n totalWarnings += mdxSafeMarkdownUnitResult.warnings.length;\n console.log(` ${mdxSafeMarkdownUnitResult.errors.length} errors, ${mdxSafeMarkdownUnitResult.warnings.length} warnings`);\n \n // Spelling Tests\n console.log('\\n🔤 Running Spelling Tests...');\n const spellResult = await spellingTests.runTests({ stagedOnly });\n totalErrors += spellResult.errors.length;\n totalWarnings += (spellResult.warnings || []).length;\n console.log(` ${spellResult.errors.length} errors`);\n \n // Quality Tests\n console.log('\\n✨ Running Quality Checks...');\n const qualityResult = qualityTests.runTests({ stagedOnly });\n totalErrors += qualityResult.errors.length;\n totalWarnings += qualityResult.warnings.length;\n console.log(` ${qualityResult.errors.length} errors, ${qualityResult.warnings.length} warnings`);\n \n // Links & Imports Tests\n console.log('\\n🔗 Running Links & Imports Validation...');\n const linksResult = linksImportsTests.runTests({ stagedOnly });\n totalErrors += linksResult.errors.length;\n totalWarnings += linksResult.warnings.length;\n console.log(` ${linksResult.errors.length} errors, ${linksResult.warnings.length} warnings`);\n\n // Docs Navigation Validation\n console.log('\\n🧭 Running Docs Navigation Validation...');\n const docsNavigationResult = docsNavigationTests.runTests({ writeReport: false });\n totalErrors += docsNavigationResult.errors.length;\n totalWarnings += docsNavigationResult.warnings.length;\n console.log(` ${docsNavigationResult.errors.length} errors, ${docsNavigationResult.warnings.length} warnings`);\n\n // Script Docs Enforcement\n console.log('\\n🧾 Running Script Documentation Enforcement...');\n const scriptDocsResult = scriptDocsTests.runTests({ stagedOnly });\n totalErrors += scriptDocsResult.errors.length;\n totalWarnings += scriptDocsResult.warnings.length;\n console.log(` ${scriptDocsResult.errors.length} errors, ${scriptDocsResult.warnings.length} warnings`);\n\n // Component Governance Utility Tests\n console.log('\\n🧩 Running Component Governance Utility Tests...');\n const componentGovernanceUtilsResult = componentGovernanceUtilsTests.runTests();\n totalErrors += componentGovernanceUtilsResult.errors.length;\n totalWarnings += componentGovernanceUtilsResult.warnings.length;\n console.log(\n ` ${componentGovernanceUtilsResult.errors.length} errors, ${componentGovernanceUtilsResult.warnings.length} warnings`\n );\n\n // Component Governance Generator Tests\n console.log('\\n🗂️ Running Component Governance Generator Tests...');\n const componentGovernanceGeneratorResult = componentGovernanceGeneratorTests.runTests();\n totalErrors += componentGovernanceGeneratorResult.errors.length;\n totalWarnings += componentGovernanceGeneratorResult.warnings.length;\n console.log(\n ` ${componentGovernanceGeneratorResult.errors.length} errors, ${componentGovernanceGeneratorResult.warnings.length} warnings`\n );\n\n // Usefulness Unit Tests\n console.log('\\n📈 Running Usefulness Unit Tests...');\n const usefulnessRubricCheck = spawnSync(\n 'node',\n ['tests/unit/usefulness-rubric.test.js'],\n { cwd: REPO_ROOT, encoding: 'utf8' }\n );\n if (usefulnessRubricCheck.stdout) process.stdout.write(usefulnessRubricCheck.stdout);\n if (usefulnessRubricCheck.stderr) process.stderr.write(usefulnessRubricCheck.stderr);\n if (usefulnessRubricCheck.status !== 0) totalErrors += 1;\n\n const usefulnessJourneyCheck = spawnSync(\n 'node',\n ['tests/unit/usefulness-journey.test.js'],\n { cwd: REPO_ROOT, encoding: 'utf8' }\n );\n if (usefulnessJourneyCheck.stdout) process.stdout.write(usefulnessJourneyCheck.stdout);\n if (usefulnessJourneyCheck.stderr) process.stderr.write(usefulnessJourneyCheck.stderr);\n if (usefulnessJourneyCheck.status !== 0) totalErrors += 1;\n const usefulnessFailures = (usefulnessRubricCheck.status === 0 ? 0 : 1) + (usefulnessJourneyCheck.status === 0 ? 0 : 1);\n console.log(` ${usefulnessFailures} errors, 0 warnings`);\n\n // Pages Index Sync Validation\n console.log('\\n🗂️ Running Pages Index Sync Validation...');\n const pagesIndexResult = pagesIndexGenerator.run({ stagedOnly });\n totalErrors += pagesIndexResult.errors.length;\n totalWarnings += pagesIndexResult.warnings.length;\n if (pagesIndexResult.skipped) {\n console.log(' skipped (no staged v2/pages changes)');\n } else {\n console.log(` ${pagesIndexResult.errors.length} errors, ${pagesIndexResult.warnings.length} warnings`);\n }\n\n // Generated Banner Enforcement\n console.log('\\n🏷️ Running Generated Banner Enforcement...');\n if (stagedOnly) {\n console.log(' skipped in --staged mode (covered by changed-file PR checks)');\n } else {\n const generatedBannerCheck = spawnSync(\n 'node',\n ['tools/scripts/enforce-generated-file-banners.js', '--check'],\n { cwd: REPO_ROOT, encoding: 'utf8' }\n );\n if (generatedBannerCheck.stdout) process.stdout.write(generatedBannerCheck.stdout);\n if (generatedBannerCheck.stderr) process.stderr.write(generatedBannerCheck.stderr);\n if (generatedBannerCheck.status !== 0) {\n totalErrors += 1;\n console.log(' 1 error, 0 warnings');\n } else {\n console.log(' 0 errors, 0 warnings');\n }\n }\n \n // Browser Tests (optional)\n if (!skipBrowser) {\n console.log('\\n🌐 Running Browser Tests...');\n try {\n const browserResult = await browserTests.runTests({ stagedOnly });\n totalErrors += browserResult.failed || 0;\n console.log(` ${browserResult.failed || 0} failed, ${browserResult.passed || 0} passed`);\n } catch (error) {\n console.warn(` ⚠️ Browser tests skipped: ${error.message}`);\n }\n }\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "pre-commit",
+ "caller": ".githooks/pre-commit",
+ "pipeline": "P1"
+ },
+ {
+ "type": "workflow-push",
+ "caller": ".github/workflows/test-suite.yml",
+ "workflow": "Docs CI - Content Quality Suite",
+ "pipeline": "P2",
+ "via_script": "test"
+ },
+ {
+ "type": "workflow-push",
+ "caller": ".github/workflows/test-v2-pages.yml",
+ "workflow": "Docs CI - V2 Browser Sweep",
+ "pipeline": "P2",
+ "via_script": "test"
+ },
+ {
+ "type": "workflow-pr",
+ "caller": ".github/workflows/test-suite.yml",
+ "workflow": "Docs CI - Content Quality Suite",
+ "pipeline": "P3",
+ "via_script": "test"
+ },
+ {
+ "type": "workflow-pr",
+ "caller": ".github/workflows/test-v2-pages.yml",
+ "workflow": "Docs CI - V2 Browser Sweep",
+ "pipeline": "P3",
+ "via_script": "test"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "test"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 (pre-commit); P2 (Docs CI - Content Quality Suite); P2 (Docs CI - V2 Browser Sweep); P3 (Docs CI - Content Quality Suite); P3 (Docs CI - V2 Browser Sweep); manual (npm script: test); manual (npm script: test)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/run-pr-checks.js",
+ "script": "run-pr-checks",
+ "category": "orchestrator",
+ "purpose": "infrastructure:pipeline-orchestration",
+ "scope": "tests, .github/workflows, tools/scripts",
+ "owner": "docs",
+ "needs": "R-R29",
+ "purpose_statement": "PR orchestrator — runs changed-file scoped validation checks for pull request CI. Dispatches per-file validators based on PR diff.",
+ "pipeline_declared": "P2, P3",
+ "usage": "node tests/run-pr-checks.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script run-pr-checks\n * @category orchestrator\n * @purpose infrastructure:pipeline-orchestration\n * @scope tests, .github/workflows, tools/scripts\n * @owner docs\n * @needs R-R29\n * @purpose-statement PR orchestrator — runs changed-file scoped validation checks for pull request CI. Dispatches per-file validators based on PR diff.\n * @pipeline P2, P3\n * @usage node tests/run-pr-checks.js [flags]\n */\n\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { execSync, spawnSync } = require('child_process');\nconst { getDocsJsonRouteKeys, toDocsRouteKeyFromFileV2Aware } = require('./utils/file-walker');\nconst { isEligibleRepoMarkdownPath } = require('../tools/lib/mdx-safe-markdown');\nconst {\n AGGREGATE_INDEX_PATH,\n GOVERNED_ROOTS,\n GROUP_INDEX_PATHS,\n SCRIPT_EXTENSIONS: GOVERNED_SCRIPT_EXTENSIONS,\n isWithinRoots\n} = require('../tools/lib/script-governance-config');\n\nconst styleGuideTests = require('./unit/style-guide.test');\nconst mdxTests = require('./unit/mdx.test');\nconst mdxGuardsTests = require('./unit/mdx-guards.test');\nconst spellingTests = require('./unit/spelling.test');\nconst qualityTests = require('./unit/quality.test');\nconst linksImportsTests = require('./unit/links-imports.test');\nconst docsNavigationTests = require('./unit/docs-navigation.test');\nconst scriptDocsTests = require('./unit/script-docs.test');\nconst componentNamingTests = require('../tools/scripts/validators/components/check-naming-conventions');\nconst mdxSafeMarkdownValidator = require('../tools/scripts/validators/content/check-mdx-safe-markdown');\n\nconst REPO_ROOT = getRepoRoot();\nconst SCRIPT_EXTENSIONS = new Set(GOVERNED_SCRIPT_EXTENSIONS);\nconst SCRIPT_SCOPES = GOVERNED_ROOTS;\nconst LINK_AUDIT_REPORT = '/tmp/livepeer-link-audit-pr.md';\nconst CODEX_BRANCH_RE = /^codex\\//;\nconst GENERATED_AFFECTING_PREFIXES = [\n 'docs-guide/indexes/',\n 'tools/scripts/generate-docs-guide-',\n 'tools/scripts/generate-pages-index.js',\n 'tools/scripts/enforce-generated-file-banners.js',\n 'tools/scripts/i18n/lib/',\n 'tools/lib/generated-file-banners.js'\n];\nconst GENERATED_AFFECTING_EXACT = new Set([\n 'tests/unit/script-docs.test.js',\n AGGREGATE_INDEX_PATH,\n ...GROUP_INDEX_PATHS,\n 'v2/index.mdx',\n 'v2/resources/documentation-guide/component-library/overview.mdx'\n]);\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nfunction toPosix(filePath) {\n return String(filePath || '').split(path.sep).join('/');\n}\n\nfunction parseArgs(argv) {\n const args = { baseRef: process.env.GITHUB_BASE_REF || '' };\n for (let i = 0; i < argv.length; i += 1) {\n if (argv[i] === '--base-ref') {\n args.baseRef = String(argv[i + 1] || '').trim();\n i += 1;\n }\n }\n return args;\n}\n\nfunction runGit(args) {\n return execSync(`git ${args}`, { cwd: REPO_ROOT, encoding: 'utf8' }).trim();\n}\n\nfunction ensureBaseRef(baseRef) {\n if (!baseRef) {\n throw new Error('Missing base ref. Provide --base-ref or set GITHUB_BASE_REF.');\n }\n\n try {\n runGit(`rev-parse --verify origin/${baseRef}`);\n } catch (_error) {\n throw new Error(\n `Could not resolve origin/${baseRef}. Ensure checkout uses fetch-depth: 0 and base ref is fetched.`\n );\n }\n}\n\nfunction detectCurrentBranch() {\n const headRef = String(process.env.GITHUB_HEAD_REF || '').trim();\n if (headRef) return headRef;\n\n try {\n return runGit('rev-parse --abbrev-ref HEAD');\n } catch (_error) {\n return '';\n }\n}\n\nfunction getChangedFiles(baseRef) {\n ensureBaseRef(baseRef);\n const mergeBase = runGit(`merge-base origin/${baseRef} HEAD`);\n if (!mergeBase) {\n throw new Error(`Failed to compute merge-base for origin/${baseRef} and HEAD.`);\n }\n\n const output = runGit(`diff --name-only --diff-filter=ACMR ${mergeBase}..HEAD`);\n if (!output) return [];\n return output\n .split('\\n')\n .map((line) => toPosix(line.trim()))\n .filter(Boolean);\n}\n\nfunction relToAbs(relPath) {\n return path.join(REPO_ROOT, relPath);\n}\n\nfunction dedupe(values) {\n return [...new Set(values)];\n}\n\nfunction partitionFiles(changedFiles) {\n const existingChangedFiles = changedFiles.filter((file) => fs.existsSync(relToAbs(file)));\n const docsRouteKeys = getDocsJsonRouteKeys(REPO_ROOT);\n const docsMdx = existingChangedFiles.filter((file) => {\n if (!file.endsWith('.mdx')) return false;\n const routeKey = toDocsRouteKeyFromFileV2Aware(file, REPO_ROOT);\n return Boolean(routeKey) && docsRouteKeys.has(routeKey);\n });\n const componentJsx = existingChangedFiles.filter(\n (file) => file.startsWith('snippets/components/') && file.endsWith('.jsx')\n );\n const repoMarkdownFiles = existingChangedFiles.filter((file) => isEligibleRepoMarkdownPath(file));\n\n const scriptFiles = existingChangedFiles.filter((file) => {\n const ext = path.extname(file).toLowerCase();\n return isWithinRoots(file, SCRIPT_SCOPES) && SCRIPT_EXTENSIONS.has(ext);\n });\n\n const usefulnessFiles = existingChangedFiles.filter((file) =>\n file === 'tools/scripts/audit-v2-usefulness.js' ||\n file === 'tools/scripts/assign-purpose-metadata.js' ||\n file === 'tools/scripts/docs-quality-and-freshness-audit.js' ||\n file === '.gitignore' ||\n file === 'tools/package.json' ||\n file === 'tools/package-lock.json' ||\n file.startsWith('tools/lib/docs-usefulness/') ||\n file.startsWith('tools/config/usefulness-') ||\n file.startsWith('tests/unit/usefulness-')\n );\n\n return {\n docsMdx,\n componentJsx,\n repoMarkdownFiles,\n styleFiles: dedupe([...docsMdx, ...componentJsx]).map(relToAbs),\n docsMdxAbs: docsMdx.map(relToAbs),\n repoMarkdownFilesAbs: dedupe(repoMarkdownFiles).map(relToAbs),\n scriptFiles: dedupe(scriptFiles),\n usefulnessFiles: dedupe(usefulnessFiles)\n };\n}\n\nfunction rowResult(status) {\n if (status === 'passed') return '✅ Pass';\n if (status === 'failed') return '❌ Fail';\n return '⏭️ Skipped';\n}\n\nfunction pushSummary(lines) {\n const summaryPath = process.env.GITHUB_STEP_SUMMARY;\n if (!summaryPath) return;\n fs.appendFileSync(summaryPath, `${lines.join('\\n')}\\n`, 'utf8');\n}\n\nasync function runUnitCheck(label, files, fn) {\n if (!files.length) {\n return { label, status: 'skipped', files: 0, errors: 0, warnings: 0 };\n }\n const result = await fn({ files });\n return {\n label,\n status: result.passed ? 'passed' : 'failed',\n files: files.length,\n errors: Array.isArray(result.errors) ? result.errors.length : 0,\n warnings: Array.isArray(result.warnings) ? result.warnings.length : 0\n };\n}\n\nfunction runScriptDocsCheck(files) {\n if (!files.length) {\n return { label: 'Script Docs', status: 'skipped', files: 0, errors: 0, warnings: 0 };\n }\n\n const result = scriptDocsTests.runTests({ files });\n return {\n label: 'Script Docs',\n status: result.passed ? 'passed' : 'failed',\n files: files.length,\n errors: Array.isArray(result.errors) ? result.errors.length : 0,\n warnings: Array.isArray(result.warnings) ? result.warnings.length : 0\n };\n}\n\nfunction runComponentNamingCheck(files) {\n if (!files.length) {\n return { label: 'Component Naming', status: 'skipped', files: 0, errors: 0, warnings: 0 };",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "workflow-push",
+ "caller": ".github/workflows/test-suite.yml",
+ "workflow": "Docs CI - Content Quality Suite",
+ "pipeline": "P2",
+ "via_script": "test:pr"
+ },
+ {
+ "type": "workflow-pr",
+ "caller": ".github/workflows/test-suite.yml",
+ "workflow": "Docs CI - Content Quality Suite",
+ "pipeline": "P3",
+ "via_script": "test:pr"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/create-codex-pr.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/usefulness-rubric.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/validate-codex-task-contract.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tools/scripts/codex/task-preflight.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:pr"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P2 (Docs CI - Content Quality Suite); P3 (Docs CI - Content Quality Suite); indirect via tests/unit/create-codex-pr.test.js; indirect via tests/unit/usefulness-rubric.test.js; indirect via tests/unit/validate-codex-task-contract.test.js; indirect via tools/scripts/codex/task-preflight.js; manual (npm script: test:pr)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P2"
+ },
+ {
+ "path": "tests/unit/codex-commit.test.js",
+ "script": "codex-commit.test",
+ "category": "validator",
+ "purpose": "governance:agent-governance",
+ "scope": "tests/unit, tools/scripts/codex-commit.js",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Tests codex-commit.js — validates commit message generation and contract compliance",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/codex-commit.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script codex-commit.test\n * @category validator\n * @purpose governance:agent-governance\n * @scope tests/unit, tools/scripts/codex-commit.js\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Tests codex-commit.js — validates commit message generation and contract compliance\n * @pipeline manual — not yet in pipeline\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/codex-commit.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst REPO_ROOT = process.cwd();\nconst SCRIPT_PATH = path.join(REPO_ROOT, 'tools/scripts/codex-commit.js');\n\nfunction run(cmd, args, cwd) {\n return spawnSync(cmd, args, { cwd, encoding: 'utf8' });\n}\n\nfunction runGit(args, cwd) {\n const out = run('git', args, cwd);\n if (out.status !== 0) {\n throw new Error(`git ${args.join(' ')} failed: ${out.stderr || out.stdout}`);\n }\n return (out.stdout || '').trim();\n}\n\nfunction writeFile(absPath, content) {\n fs.mkdirSync(path.dirname(absPath), { recursive: true });\n fs.writeFileSync(absPath, content, 'utf8');\n}\n\nfunction mkRepo(prefix) {\n const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));\n runGit(['init', '-b', 'main'], dir);\n runGit(['config', 'user.email', 'tests@example.com'], dir);\n runGit(['config', 'user.name', 'test-runner'], dir);\n writeFile(path.join(dir, 'file.txt'), 'init\\n');\n runGit(['add', 'file.txt'], dir);\n runGit(['commit', '-m', 'init'], dir);\n return dir;\n}\n\nfunction runScript(args, cwd) {\n return run('node', [SCRIPT_PATH, ...args], cwd);\n}\n\nasync function runTests() {\n const failures = [];\n const cases = [];\n\n cases.push(async () => {\n const repo = mkRepo('codex-commit-normal-');\n writeFile(path.join(repo, 'file.txt'), 'normal\\n');\n runGit(['add', 'file.txt'], repo);\n\n const exec = runScript(['--message', 'chore: normal commit'], repo);\n assert.strictEqual(exec.status, 0, exec.stderr || exec.stdout);\n const body = runGit(['log', '-1', '--pretty=%B'], repo);\n assert.match(body, /chore: normal commit/);\n });\n\n cases.push(async () => {\n const repo = mkRepo('codex-commit-reject-');\n writeFile(path.join(repo, 'file.txt'), 'reject\\n');\n runGit(['add', 'file.txt'], repo);\n\n const exec = runScript(['--message', 'chore: should fail', '--no-verify'], repo);\n assert.strictEqual(exec.status, 1, 'no-verify without override must fail');\n const output = `${exec.stdout}\\n${exec.stderr}`;\n assert.match(output, /requires --human-override true/i);\n });\n\n cases.push(async () => {\n const repo = mkRepo('codex-commit-override-');\n writeFile(path.join(repo, 'file.txt'), 'override\\n');\n runGit(['add', 'file.txt'], repo);\n\n const exec = runScript(\n [\n '--message',\n 'chore: override commit',\n '--no-verify',\n '--human-override',\n 'true',\n '--override-note',\n 'User said no-verify is allowed for this commit.'\n ],\n repo\n );\n assert.strictEqual(exec.status, 0, exec.stderr || exec.stdout);\n\n const body = runGit(['log', '-1', '--pretty=%B'], repo);\n assert.match(body, /\\[override-audit\\]/);\n assert.match(body, /override_type: no-verify/);\n assert.match(body, /requested_by: human/);\n assert.match(body, /User said no-verify is allowed/);\n });\n\n for (let i = 0; i < cases.length; i += 1) {\n const name = `case-${i + 1}`;\n try {\n // eslint-disable-next-line no-await-in-loop\n await cases[i]();\n console.log(` ✓ ${name}`);\n } catch (error) {\n failures.push(`${name}: ${error.message}`);\n }\n }\n\n return {\n passed: failures.length === 0,\n total: cases.length,\n errors: failures\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ codex-commit tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} codex-commit test failure(s)`);\n result.errors.forEach((entry) => console.error(` - ${entry}`));\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ codex-commit tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/codex-safe-merge-with-stash.test.js",
+ "script": "codex-safe-merge-with-stash.test",
+ "category": "utility",
+ "purpose": "governance:agent-governance",
+ "scope": "tests/unit, tools/scripts/codex-safe-merge-with-stash.js",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Tests codex-safe-merge-with-stash.js — validates safe merge logic with stash handling",
+ "pipeline_declared": "manual — developer tool",
+ "usage": "node tests/unit/codex-safe-merge-with-stash.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script codex-safe-merge-with-stash.test\n * @category utility\n * @purpose governance:agent-governance\n * @scope tests/unit, tools/scripts/codex-safe-merge-with-stash.js\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Tests codex-safe-merge-with-stash.js — validates safe merge logic with stash handling\n * @pipeline manual — developer tool\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/codex-safe-merge-with-stash.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst REPO_ROOT = process.cwd();\nconst SCRIPT_PATH = path.join(REPO_ROOT, 'tools/scripts/codex-safe-merge-with-stash.js');\n\nfunction run(cmd, args, cwd) {\n return spawnSync(cmd, args, { cwd, encoding: 'utf8' });\n}\n\nfunction runGit(args, cwd) {\n const out = run('git', args, cwd);\n if (out.status !== 0) {\n throw new Error(`git ${args.join(' ')} failed: ${out.stderr || out.stdout}`);\n }\n return (out.stdout || '').trim();\n}\n\nfunction writeFile(absPath, content) {\n fs.mkdirSync(path.dirname(absPath), { recursive: true });\n fs.writeFileSync(absPath, content, 'utf8');\n}\n\nfunction mkRepo(prefix) {\n const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));\n runGit(['init', '-b', 'main'], dir);\n runGit(['config', 'user.email', 'tests@example.com'], dir);\n runGit(['config', 'user.name', 'test-runner'], dir);\n writeFile(path.join(dir, 'app.txt'), 'base\\n');\n runGit(['add', 'app.txt'], dir);\n runGit(['commit', '-m', 'init'], dir);\n runGit(['checkout', '-b', 'feature'], dir);\n return dir;\n}\n\nfunction runScript(args, cwd) {\n return run('node', [SCRIPT_PATH, ...args], cwd);\n}\n\nasync function runTests() {\n const failures = [];\n const cases = [];\n\n cases.push(async () => {\n const repo = mkRepo('codex-safe-merge-clean-');\n runGit(['checkout', 'main'], repo);\n writeFile(path.join(repo, 'app.txt'), 'base\\nmain-change\\n');\n runGit(['add', 'app.txt'], repo);\n runGit(['commit', '-m', 'main change'], repo);\n runGit(['checkout', 'feature'], repo);\n\n const exec = runScript(['--target', 'main'], repo);\n assert.strictEqual(exec.status, 0, exec.stderr || exec.stdout);\n const merged = fs.readFileSync(path.join(repo, 'app.txt'), 'utf8');\n assert.match(merged, /main-change/, 'feature should include merged change');\n });\n\n cases.push(async () => {\n const repo = mkRepo('codex-safe-merge-dirty-');\n runGit(['checkout', 'main'], repo);\n writeFile(path.join(repo, 'app.txt'), 'base\\nmain-change\\n');\n runGit(['add', 'app.txt'], repo);\n runGit(['commit', '-m', 'main change'], repo);\n runGit(['checkout', 'feature'], repo);\n\n writeFile(path.join(repo, 'local.txt'), 'local-dirty\\n');\n writeFile(path.join(repo, 'temp-untracked.txt'), 'temp\\n');\n runGit(['add', 'local.txt'], repo);\n\n const exec = runScript(['--target', 'main'], repo);\n assert.strictEqual(exec.status, 0, exec.stderr || exec.stdout);\n\n const localTracked = fs.readFileSync(path.join(repo, 'local.txt'), 'utf8');\n const localUntracked = fs.readFileSync(path.join(repo, 'temp-untracked.txt'), 'utf8');\n assert.match(localTracked, /local-dirty/, 'tracked local file should be restored');\n assert.match(localUntracked, /temp/, 'untracked local file should be restored');\n });\n\n cases.push(async () => {\n const repo = mkRepo('codex-safe-merge-conflict-');\n runGit(['checkout', 'main'], repo);\n writeFile(path.join(repo, 'app.txt'), 'main-version\\n');\n runGit(['add', 'app.txt'], repo);\n runGit(['commit', '-m', 'main update'], repo);\n runGit(['checkout', 'feature'], repo);\n writeFile(path.join(repo, 'app.txt'), 'feature-version\\n');\n runGit(['add', 'app.txt'], repo);\n runGit(['commit', '-m', 'feature update'], repo);\n\n writeFile(path.join(repo, 'dirty.txt'), 'dirty\\n');\n runGit(['add', 'dirty.txt'], repo);\n\n const exec = runScript(['--target', 'main'], repo);\n assert.strictEqual(exec.status, 1, 'merge conflict should fail');\n\n const stashList = runGit(['stash', 'list'], repo);\n assert.match(stashList, /codex-safe-merge:/, 'stash should be preserved when merge fails');\n });\n\n for (let i = 0; i < cases.length; i += 1) {\n const name = `case-${i + 1}`;\n try {\n // eslint-disable-next-line no-await-in-loop\n await cases[i]();\n console.log(` ✓ ${name}`);\n } catch (error) {\n failures.push(`${name}: ${error.message}`);\n }\n }\n\n return {\n passed: failures.length === 0,\n total: cases.length,\n errors: failures\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ codex-safe-merge-with-stash tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} codex-safe-merge-with-stash test failure(s)`);\n result.errors.forEach((entry) => console.error(` - ${entry}`));\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ codex-safe-merge-with-stash tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/codex-skill-sync.test.js",
+ "script": "codex-skill-sync.test",
+ "category": "validator",
+ "purpose": "governance:agent-governance",
+ "scope": "tests/unit, tools/scripts/sync-codex-skills.js, ai-tools/ai-skills/templates",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Tests sync-codex-skills.js — validates skill file synchronisation between sources",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/codex-skill-sync.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script codex-skill-sync.test\n * @category validator\n * @purpose governance:agent-governance\n * @scope tests/unit, tools/scripts/sync-codex-skills.js, ai-tools/ai-skills/templates\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Tests sync-codex-skills.js — validates skill file synchronisation between sources\n * @pipeline manual — not yet in pipeline\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/codex-skill-sync.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst REPO_ROOT = process.cwd();\nconst SYNC_SCRIPT = path.join(REPO_ROOT, 'tools/scripts/sync-codex-skills.js');\n\nlet errors = [];\n\nfunction runNode(args, options = {}) {\n return spawnSync('node', [SYNC_SCRIPT, ...args], {\n cwd: REPO_ROOT,\n encoding: 'utf8',\n env: {\n ...process.env,\n ...(options.env || {})\n }\n });\n}\n\nfunction mkTmpDir(prefix) {\n return fs.mkdtempSync(path.join(os.tmpdir(), prefix));\n}\n\nfunction writeFile(absPath, content) {\n fs.mkdirSync(path.dirname(absPath), { recursive: true });\n fs.writeFileSync(absPath, content, 'utf8');\n}\n\nfunction createTemplate(sourceDir, number, name, description) {\n const fileName = `${String(number).padStart(2, '0')}-${name}.template.md`;\n const absPath = path.join(sourceDir, fileName);\n const content = [\n '---',\n `name: ${name}`,\n `description: ${description}`,\n 'tier: 1',\n 'triggers:',\n ' - \"trigger one\"',\n ' - \"trigger two\"',\n ' - \"trigger three\"',\n 'primary_paths:',\n ' - \"README.md\"',\n ' - \"tools/scripts\"',\n 'primary_commands:',\n ' - \"bash lpd setup --yes\"',\n ' - \"bash lpd doctor --strict\"',\n '---',\n '',\n `SKILL: ${name}`,\n '',\n 'Goal',\n 'Execute the workflow safely.',\n '',\n 'Constraints',\n '- Keep v1 immutable.',\n '',\n 'Workflow',\n '1. Run command one.',\n '2. Run command two.',\n '',\n 'Deliverable Format',\n '- Provide concise status and next actions.',\n '',\n 'Failure Modes / Fallback',\n '- If command fails, report exact remediation.',\n '',\n 'Validation Checklist',\n '- [ ] Required checks were run.',\n ''\n ].join('\\n');\n writeFile(absPath, content);\n return { absPath, content };\n}\n\nfunction readUtf8(absPath) {\n return fs.readFileSync(absPath, 'utf8');\n}\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n name,\n message: error.message\n });\n }\n}\n\nasync function runTests() {\n errors = [];\n console.log('🧪 Codex Skill Sync Unit Tests');\n\n await runCase('Sync installs templates and generates openai.yaml', async () => {\n const root = mkTmpDir('codex-skill-sync-install-');\n const sourceDir = path.join(root, 'source');\n const destDir = path.join(root, 'dest');\n\n const t1 = createTemplate(sourceDir, 1, 'alpha-skill', 'Alpha skill synchronization workflow.');\n createTemplate(sourceDir, 2, 'beta-skill', 'Beta skill synchronization workflow.');\n\n const result = runNode(['--source-dir', sourceDir, '--dest', destDir]);\n assert.strictEqual(result.status, 0, `sync exited non-zero: ${result.stderr || result.stdout}`);\n\n const skillPath = path.join(destDir, 'alpha-skill', 'SKILL.md');\n const openaiPath = path.join(destDir, 'alpha-skill', 'agents', 'openai.yaml');\n assert.ok(fs.existsSync(skillPath), 'alpha SKILL.md should exist');\n assert.ok(fs.existsSync(openaiPath), 'alpha openai.yaml should exist');\n assert.strictEqual(readUtf8(skillPath), t1.content, 'SKILL.md should match canonical template exactly');\n\n const openai = readUtf8(openaiPath);\n assert.ok(openai.includes('display_name: \"Alpha Skill\"'), 'openai.yaml should include deterministic display_name');\n assert.ok(openai.includes('short_description: \"'), 'openai.yaml should include short_description');\n assert.ok(openai.includes('$alpha-skill'), 'openai.yaml default_prompt should include explicit $skill reference');\n });\n\n await runCase('Check mode fails on drift', async () => {\n const root = mkTmpDir('codex-skill-sync-check-');\n const sourceDir = path.join(root, 'source');\n const destDir = path.join(root, 'dest');\n\n createTemplate(sourceDir, 1, 'drift-skill', 'Detect drift in check mode.');\n const syncResult = runNode(['--source-dir', sourceDir, '--dest', destDir]);\n assert.strictEqual(syncResult.status, 0, `initial sync failed: ${syncResult.stderr || syncResult.stdout}`);\n\n writeFile(path.join(destDir, 'drift-skill', 'SKILL.md'), 'tampered');\n const checkResult = runNode(['--source-dir', sourceDir, '--dest', destDir, '--check']);\n assert.strictEqual(checkResult.status, 1, 'check mode should fail on drift');\n assert.ok((checkResult.stdout + checkResult.stderr).includes('drift'), 'check output should include drift signal');\n });\n\n await runCase('Safe upsert preserves unmanaged skills', async () => {\n const root = mkTmpDir('codex-skill-sync-upsert-');\n const sourceDir = path.join(root, 'source');\n const destDir = path.join(root, 'dest');\n createTemplate(sourceDir, 1, 'managed-skill', 'Managed skill from templates.');\n\n const unmanagedPath = path.join(destDir, 'custom-local-skill', 'SKILL.md');\n writeFile(unmanagedPath, '---\\nname: custom-local-skill\\ndescription: local\\n---\\n\\nSKILL: Custom\\n');\n\n const result = runNode(['--source-dir', sourceDir, '--dest', destDir]);\n assert.strictEqual(result.status, 0, `sync failed: ${result.stderr || result.stdout}`);\n assert.ok(fs.existsSync(unmanagedPath), 'unmanaged skill should remain');\n assert.strictEqual(readUtf8(unmanagedPath).includes('custom-local-skill'), true, 'unmanaged content should remain intact');\n });\n\n await runCase('Subset install writes only selected skills', async () => {\n const root = mkTmpDir('codex-skill-sync-subset-');\n const sourceDir = path.join(root, 'source');\n const destDir = path.join(root, 'dest');\n createTemplate(sourceDir, 1, 'subset-one', 'Subset one.');\n createTemplate(sourceDir, 2, 'subset-two', 'Subset two.');\n\n const result = runNode(['--source-dir', sourceDir, '--dest', destDir, '--skills', 'subset-two']);\n assert.strictEqual(result.status, 0, `subset sync failed: ${result.stderr || result.stdout}`);\n assert.ok(!fs.existsSync(path.join(destDir, 'subset-one')), 'subset-one should not be installed');\n assert.ok(fs.existsSync(path.join(destDir, 'subset-two', 'SKILL.md')), 'subset-two should be installed');\n });\n\n await runCase('Dry-run does not mutate destination', async () => {\n const root = mkTmpDir('codex-skill-sync-dryrun-');\n const sourceDir = path.join(root, 'source');\n const destDir = path.join(root, 'dest');\n createTemplate(sourceDir, 1, 'dry-run-skill', 'Dry-run should not write files.');\n\n const result = runNode(['--source-dir', sourceDir, '--dest', destDir, '--dry-run']);\n assert.strictEqual(result.status, 0, `dry-run failed: ${result.stderr || result.stdout}`);\n assert.ok(!fs.existsSync(path.join(destDir, 'dry-run-skill')), 'dry-run should not create skill files');\n });\n\n return {\n passed: errors.length === 0,\n total: 5,\n errors\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ Codex skill sync unit tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} codex skill sync unit test failure(s)`);\n result.errors.forEach((entry) => {\n console.error(` - ${entry.name}: ${entry.message}`);\n });\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ Codex skill sync unit tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:codex-skills-sync"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tests/unit/${String(number).padStart(2, '0')}-${name}.template.md",
+ "type": "generated-output",
+ "call": "writeFile"
+ },
+ {
+ "output_path": "tests/unit/${String(number).padStart(2, '0')}-${name}.template.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tests/unit/SKILL.md",
+ "type": "generated-output",
+ "call": "writeFile"
+ }
+ ],
+ "outputs_display": "tests/unit/${String(number).padStart(2, '0')}-${name}.template.md, tests/unit/${String(number).padStart(2, '0')}-${name}.template.md, tests/unit/SKILL.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:codex-skills-sync)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/component-governance-generators.test.js",
+ "script": "component-governance-generators.test",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "R-R10",
+ "purpose_statement": "Verifies component governance generators produce coherent registry, usage-map, and docs outputs.",
+ "pipeline_declared": "P1 (commit, via run-all)",
+ "usage": "node tests/unit/component-governance-generators.test.js",
+ "header": "#!/usr/bin/env node\n/**\n * @script component-governance-generators.test\n * @category validator\n * @purpose qa:repo-health\n * @scope tests\n * @owner docs\n * @needs R-R10\n * @purpose-statement Verifies component governance generators produce coherent registry, usage-map, and docs outputs.\n * @pipeline P1 (commit, via run-all)\n * @usage node tests/unit/component-governance-generators.test.js\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst path = require('path');\nconst { buildRegistry } = require('../../tools/scripts/generate-component-registry');\nconst { buildUsageMap } = require('../../tools/scripts/scan-component-imports');\nconst { parseArgs: parseDocsArgs } = require('../../tools/scripts/generate-component-docs');\n\nconst REPO_ROOT = path.resolve(__dirname, '..', '..');\n\nfunction readFile(relativePath) {\n return fs.readFileSync(path.join(REPO_ROOT, relativePath), 'utf8');\n}\n\nfunction runTests() {\n const errors = [];\n const warnings = [];\n\n try {\n const { registry, issues } = buildRegistry();\n assert.equal(issues.length, 0);\n assert(registry._meta.componentCount > 0);\n assert.equal(registry.categories.data.count > 0, true);\n assert.equal(registry.categories['page-structure'].count > 0, true);\n } catch (error) {\n errors.push(`buildRegistry failed: ${error.message}`);\n }\n\n try {\n const { usageMap, drift } = buildUsageMap();\n assert.equal(drift.length, 0);\n assert(usageMap.components.length > 0);\n assert(Array.isArray(usageMap.orphaned));\n assert(Array.isArray(usageMap.mostImported));\n } catch (error) {\n errors.push(`buildUsageMap failed: ${error.message}`);\n }\n\n try {\n const args = parseDocsArgs(['--category', 'data']);\n assert.equal(args.category, 'data');\n assert.equal(args.templateOnly, true);\n } catch (error) {\n errors.push(`generate-component-docs argument parsing failed: ${error.message}`);\n }\n\n try {\n const englishFiles = [\n 'v2/resources/documentation-guide/component-library/component-library.mdx',\n 'v2/resources/documentation-guide/component-library/overview.mdx',\n 'v2/resources/documentation-guide/component-library/primitives.mdx',\n 'v2/resources/documentation-guide/component-library/layout.mdx',\n 'v2/resources/documentation-guide/component-library/content.mdx',\n 'v2/resources/documentation-guide/component-library/data.mdx',\n 'v2/resources/documentation-guide/component-library/page-structure.mdx'\n ];\n englishFiles.forEach((filePath) => {\n assert(fs.existsSync(path.join(REPO_ROOT, filePath)), `${filePath} should exist`);\n });\n assert(!fs.existsSync(path.join(REPO_ROOT, 'v2/resources/documentation-guide/component-library/display.mdx')));\n assert(!fs.existsSync(path.join(REPO_ROOT, 'v2/resources/documentation-guide/component-library/domain.mdx')));\n assert(!fs.existsSync(path.join(REPO_ROOT, 'v2/resources/documentation-guide/component-library/integrations.mdx')));\n\n const landingContent = readFile('v2/resources/documentation-guide/component-library/component-library.mdx');\n assert(landingContent.includes('./data'));\n assert(landingContent.includes('./page-structure'));\n } catch (error) {\n errors.push(`generated docs output validation failed: ${error.message}`);\n }\n\n return { errors, warnings };\n}\n\nif (require.main === module) {\n const result = runTests();\n result.errors.forEach((error) => console.error(error));\n result.warnings.forEach((warning) => console.warn(warning));\n process.exit(result.errors.length > 0 ? 1 : 0);\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "runner",
+ "caller": "tests/run-all.js",
+ "pipeline": "P1"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-all.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:components:governance"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 via run-all; indirect via tests/run-all.js; manual (npm script: test:components:governance)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/unit/component-governance-utils.test.js",
+ "script": "component-governance-utils.test",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "R-R10",
+ "purpose_statement": "Verifies shared component governance utility parsing, scanning, and archive exclusion behavior.",
+ "pipeline_declared": "P1 (commit, via run-all)",
+ "usage": "node tests/unit/component-governance-utils.test.js",
+ "header": "#!/usr/bin/env node\n/**\n * @script component-governance-utils.test\n * @category validator\n * @purpose qa:repo-health\n * @scope tests\n * @owner docs\n * @needs R-R10\n * @purpose-statement Verifies shared component governance utility parsing, scanning, and archive exclusion behavior.\n * @pipeline P1 (commit, via run-all)\n * @usage node tests/unit/component-governance-utils.test.js\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst {\n extractExports,\n getComponentFiles,\n parseJSDocBlock,\n scanMDXImports,\n scanStylingViolations\n} = require('../../tools/lib/component-governance-utils');\n\nfunction runTests() {\n const errors = [];\n const warnings = [];\n\n try {\n const parsed = parseJSDocBlock(`/**\n * Example summary.\n * @component SampleComponent\n * @category primitives\n * @tier primitive\n * @status stable\n * @description Sample description\n * @contentAffinity universal\n * @owner docs\n * @dependencies none\n * @usedIn none\n * @breakingChangeRisk low\n * @decision KEEP\n * @dataSource none\n * @duplicates none\n * @lastMeaningfulChange 2026-03-09\n * @param {string} [label=\"demo\"] - Label text.\n * @example\n * \n */`);\n\n assert.equal(parsed.component, 'SampleComponent');\n assert.equal(parsed.params.length, 1);\n assert.equal(parsed.params[0].name, 'label');\n assert.equal(parsed.examples[0], ' ');\n } catch (error) {\n errors.push(`parseJSDocBlock failed: ${error.message}`);\n }\n\n try {\n const exportsList = extractExports('snippets/components/content/response-field.jsx');\n const responseFieldGroup = exportsList.find((entry) => entry.name === 'ResponseFieldGroup');\n assert(responseFieldGroup, 'ResponseFieldGroup export should be discovered');\n assert(responseFieldGroup.jsDocBlock, 'ResponseFieldGroup should have an attached JSDoc block');\n assert(responseFieldGroup.props.some((prop) => prop.name === 'component'));\n } catch (error) {\n errors.push(`extractExports failed: ${error.message}`);\n }\n\n try {\n const importMap = scanMDXImports('v2/**/*.mdx');\n const codeComponent = importMap.get('CodeComponent');\n assert(codeComponent, 'CodeComponent should be present in the MDX usage map');\n assert(\n !codeComponent.pages.includes('v2/resources/documentation-guide/component-library/content.mdx'),\n 'fenced code samples must not be treated as live MDX imports'\n );\n } catch (error) {\n errors.push(`scanMDXImports failed: ${error.message}`);\n }\n\n try {\n const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'component-governance-utils-'));\n const tempFile = path.join(tempDir, 'styling-scan.jsx');\n fs.writeFileSync(\n tempFile,\n [\n '/**',\n ' * @example',\n ' * ',\n ' */',\n 'const Demo = () => ;',\n 'const ThemeSafe = () => {/* #123456 */};'\n ].join('\\n'),\n 'utf8'\n );\n\n const violations = scanStylingViolations(tempFile);\n assert.equal(violations.banned.length, 0);\n assert.equal(violations.advisory.length, 1);\n } catch (error) {\n errors.push(`scanStylingViolations failed: ${error.message}`);\n }\n\n try {\n const componentFiles = getComponentFiles();\n assert(componentFiles.length > 0, 'component file discovery should find governed files');\n assert(componentFiles.every((file) => !file.displayPath.includes('/_archive/')));\n } catch (error) {\n errors.push(`getComponentFiles failed: ${error.message}`);\n }\n\n return { errors, warnings };\n}\n\nif (require.main === module) {\n const result = runTests();\n result.errors.forEach((error) => console.error(error));\n result.warnings.forEach((warning) => console.warn(warning));\n process.exit(result.errors.length > 0 ? 1 : 0);\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "runner",
+ "caller": "tests/run-all.js",
+ "pipeline": "P1"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-all.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:components:governance"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tests/unit/styling-scan.jsx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tests/unit/styling-scan.jsx",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 via run-all; indirect via tests/run-all.js; manual (npm script: test:components:governance)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/unit/components/TEMPLATE.test.js",
+ "script": "component-template.test",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "R-R10",
+ "purpose_statement": "Template for category-scoped component unit tests.",
+ "pipeline_declared": "manual",
+ "usage": "node tests/unit/components/TEMPLATE.test.js",
+ "header": "#!/usr/bin/env node\n/**\n * @script component-template.test\n * @category validator\n * @purpose qa:repo-health\n * @scope tests\n * @owner docs\n * @needs R-R10\n * @purpose-statement Template for category-scoped component unit tests.\n * @pipeline manual\n * @usage node tests/unit/components/TEMPLATE.test.js\n */\n\nconst assert = require('assert');\n\nfunction runTests() {\n const errors = [];\n const warnings = [];\n\n try {\n assert.equal(true, true);\n } catch (error) {\n errors.push(error.message);\n }\n\n return { errors, warnings };\n}\n\nif (require.main === module) {\n const result = runTests();\n result.errors.forEach((error) => console.error(error));\n result.warnings.forEach((warning) => console.warn(warning));\n process.exit(result.errors.length > 0 ? 1 : 0);\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/create-codex-pr.test.js",
+ "script": "create-codex-pr.test",
+ "category": "generator",
+ "purpose": "governance:agent-governance",
+ "scope": "tests/unit, tools/scripts/create-codex-pr.js",
+ "owner": "docs",
+ "needs": "R-R27, R-R30",
+ "purpose_statement": "Tests create-codex-pr.js — validates PR creation logic and branch naming",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/create-codex-pr.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script create-codex-pr.test\n * @category generator\n * @purpose governance:agent-governance\n * @scope tests/unit, tools/scripts/create-codex-pr.js\n * @owner docs\n * @needs R-R27, R-R30\n * @purpose-statement Tests create-codex-pr.js — validates PR creation logic and branch naming\n * @pipeline manual — not yet in pipeline\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/create-codex-pr.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst REPO_ROOT = process.cwd();\nconst SCRIPT_PATH = path.join(REPO_ROOT, 'tools/scripts/create-codex-pr.js');\n\nfunction runScript(args) {\n return spawnSync('node', [SCRIPT_PATH, ...args], {\n cwd: REPO_ROOT,\n encoding: 'utf8'\n });\n}\n\nfunction mkTmpDir(prefix) {\n return fs.mkdtempSync(path.join(os.tmpdir(), prefix));\n}\n\nfunction writeFile(absPath, content) {\n fs.mkdirSync(path.dirname(absPath), { recursive: true });\n fs.writeFileSync(absPath, content, 'utf8');\n}\n\nfunction makeContract(absPath, taskId = 3456) {\n const content = [\n `task_id: ${taskId}`,\n 'base_branch: docs-v2',\n `branch: codex/${taskId}-auto-pr-body`,\n 'scope_in:',\n ' - v2/community/',\n ' - docs.json',\n 'scope_out:',\n ' - v1/',\n 'allowed_generated:',\n ' - docs-index.json',\n 'acceptance_checks:',\n ' - node tests/run-pr-checks.js --base-ref docs-v2',\n ' - node tests/integration/v2-link-audit.js --files v2/community/faq.mdx --strict',\n 'follow_up_issues:',\n ` - ${taskId + 1}`,\n ''\n ].join('\\n');\n writeFile(absPath, content);\n}\n\nasync function runTests() {\n const failures = [];\n const cases = [];\n\n cases.push(async () => {\n const tmp = mkTmpDir('codex-pr-body-');\n const contractPath = path.join(tmp, 'task-contract.yaml');\n const outputPath = path.join(tmp, 'pr-body.md');\n makeContract(contractPath);\n\n const run = runScript([\n '--contract',\n contractPath,\n '--output',\n outputPath,\n '--head',\n 'codex/3456-auto-pr-body',\n '--base',\n 'docs-v2',\n '--changed-files',\n 'v2/community/faq.mdx,docs.json'\n ]);\n\n assert.strictEqual(run.status, 0, `generator failed: ${run.stderr || run.stdout}`);\n const body = fs.readFileSync(outputPath, 'utf8');\n assert.match(body, /codex-pr-body-generated:/, 'body should include generated marker');\n assert.match(body, /^Fixes #3456$/m, 'body should include closing keyword for task issue');\n assert.match(body, /^## Scope/m, 'body should include Scope section');\n assert.match(body, /^## Validation/m, 'body should include Validation section');\n assert.match(body, /^## Follow-up Tasks/m, 'body should include Follow-up section');\n assert.match(body, /`v2\\/community\\/faq\\.mdx`/, 'body should include changed file list');\n assert.match(body, /#3457/, 'body should include follow-up issue id');\n });\n\n cases.push(async () => {\n const tmp = mkTmpDir('codex-pr-dry-run-');\n const contractPath = path.join(tmp, 'task-contract.yaml');\n const outputPath = path.join(tmp, 'pr-body.md');\n makeContract(contractPath, 4567);\n\n const run = runScript([\n '--contract',\n contractPath,\n '--output',\n outputPath,\n '--head',\n 'codex/4567-auto-pr-body',\n '--base',\n 'docs-v2',\n '--changed-files',\n 'docs.json',\n '--create',\n '--dry-run'\n ]);\n\n assert.strictEqual(run.status, 0, `dry-run create failed: ${run.stderr || run.stdout}`);\n const output = `${run.stdout}\\n${run.stderr}`;\n assert.match(output, /DRY RUN: gh pr create/, 'dry-run should print gh create command');\n });\n\n for (let index = 0; index < cases.length; index += 1) {\n const name = `case-${index + 1}`;\n try {\n // eslint-disable-next-line no-await-in-loop\n await cases[index]();\n console.log(` ✓ ${name}`);\n } catch (error) {\n failures.push(`${name}: ${error.message}`);\n }\n }\n\n return {\n passed: failures.length === 0,\n total: cases.length,\n errors: failures\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ create-codex-pr tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} create-codex-pr test failure(s)`);\n result.errors.forEach((entry) => console.error(` - ${entry}`));\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ create-codex-pr tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:codex-pr-create"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:codex-pr-create)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/docs-guide-sot.test.js",
+ "script": "docs-guide-sot.test",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests, docs-guide, README.md, tools/scripts/generate-docs-guide-indexes.js, tools/scripts/generate-docs-guide-pages-index.js, tools/scripts/generate-docs-guide-components-index.js",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Validates docs-guide source-of-truth coverage, README pointers, and generated index freshness",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/docs-guide-sot.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script docs-guide-sot.test\n * @category validator\n * @purpose qa:repo-health\n * @scope tests, docs-guide, README.md, tools/scripts/generate-docs-guide-indexes.js, tools/scripts/generate-docs-guide-pages-index.js, tools/scripts/generate-docs-guide-components-index.js\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Validates docs-guide source-of-truth coverage, README pointers, and generated index freshness\n * @pipeline manual — not yet in pipeline\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/docs-guide-sot.test.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst REPO_ROOT = process.cwd();\n\nconst REQUIRED_MANUAL_FILES = [\n 'docs-guide/README.mdx',\n 'docs-guide/source-of-truth-policy.mdx',\n 'docs-guide/feature-guides/feature-map.mdx',\n 'docs-guide/feature-guides/architecture-map.mdx',\n 'docs-guide/lpd.mdx',\n 'docs-guide/quality-testing/quality-gates.mdx',\n 'docs-guide/quality-testing/audit-system-overview.mdx',\n 'docs-guide/quality-testing/skill-pipeline-map.mdx',\n 'docs-guide/quality-testing/cleanup-quarantine-policy.mdx',\n 'docs-guide/quality-testing/component-layout-decision-matrix.mdx',\n 'docs-guide/feature-guides/automation-pipelines.mdx',\n 'docs-guide/feature-guides/content-system.mdx',\n 'docs-guide/feature-guides/data-integrations.mdx'\n];\n\nconst REQUIRED_GENERATED_FILES = [\n 'docs-guide/indexes/scripts-index.mdx',\n 'docs-guide/indexes/workflows-index.mdx',\n 'docs-guide/indexes/templates-index.mdx',\n 'docs-guide/indexes/pages-index.mdx',\n 'docs-guide/indexes/components-index.mdx'\n];\n\nconst REQUIRED_README_REFERENCES = [\n 'docs-guide/README.mdx',\n 'docs-guide/feature-guides/feature-map.mdx',\n 'docs-guide/source-of-truth-policy.mdx',\n 'docs-guide/lpd.mdx',\n 'docs-guide/quality-testing/quality-gates.mdx',\n 'docs-guide/quality-testing/audit-system-overview.mdx',\n 'docs-guide/quality-testing/skill-pipeline-map.mdx',\n 'docs-guide/quality-testing/cleanup-quarantine-policy.mdx',\n 'docs-guide/quality-testing/component-layout-decision-matrix.mdx',\n 'docs-guide/feature-guides/automation-pipelines.mdx',\n 'docs-guide/indexes/ai-tools.mdx',\n 'docs-guide/indexes/pages-index.mdx',\n 'docs-guide/indexes/components-index.mdx',\n 'docs-guide/indexes/scripts-index.mdx',\n 'docs-guide/indexes/workflows-index.mdx',\n 'docs-guide/indexes/templates-index.mdx'\n];\n\nfunction readFileSafe(repoPath) {\n try {\n return fs.readFileSync(path.join(REPO_ROOT, repoPath), 'utf8');\n } catch (_err) {\n return '';\n }\n}\n\nfunction isNonEmptyDoc(repoPath) {\n const content = readFileSafe(repoPath);\n return content.trim().length > 0;\n}\n\nfunction checkRequiredFiles(errors) {\n REQUIRED_MANUAL_FILES.forEach((repoPath) => {\n if (!isNonEmptyDoc(repoPath)) {\n errors.push({\n file: repoPath,\n rule: 'Required docs-guide manual file',\n message: 'Missing or empty canonical docs-guide file.',\n line: 1\n });\n }\n });\n\n REQUIRED_GENERATED_FILES.forEach((repoPath) => {\n if (!isNonEmptyDoc(repoPath)) {\n errors.push({\n file: repoPath,\n rule: 'Required docs-guide generated file',\n message: 'Missing or empty generated docs-guide index file.',\n line: 1\n });\n }\n });\n}\n\nfunction checkReadmeReferences(errors, warnings) {\n const readmePath = 'README.md';\n const content = readFileSafe(readmePath);\n if (!content.trim()) {\n errors.push({\n file: readmePath,\n rule: 'README required',\n message: 'README.md is missing or empty.',\n line: 1\n });\n return;\n }\n\n REQUIRED_README_REFERENCES.forEach((ref) => {\n if (!content.includes(ref)) {\n warnings.push({\n file: readmePath,\n rule: 'README docs-guide pointers',\n message: `README.md should reference ${ref}.`,\n line: 1\n });\n }\n });\n}\n\nfunction checkGeneratedIndexFreshness(errors) {\n const checks = [\n {\n args: ['tools/scripts/generate-docs-guide-indexes.js', '--check'],\n file: 'docs-guide/indexes/workflows-index.mdx',\n message: 'Generated docs-guide template/workflow indexes are out of date. Run generator script.'\n },\n {\n args: ['tools/scripts/generate-docs-guide-pages-index.js', '--check'],\n file: 'docs-guide/indexes/pages-index.mdx',\n message: 'Generated docs-guide pages index is out of date. Run pages index generator script.'\n },\n {\n args: ['tools/scripts/generate-docs-guide-components-index.js', '--check'],\n file: 'docs-guide/indexes/components-index.mdx',\n message: 'Generated docs-guide components index is out of date. Run components index generator script.'\n },\n {\n args: ['tests/unit/script-docs.test.js', '--check-indexes'],\n file: 'docs-guide/indexes/scripts-index.mdx',\n message: 'Generated docs-guide scripts index is out of date. Run script docs generator script.'\n },\n {\n args: ['tools/scripts/enforce-generated-file-banners.js', '--check'],\n file: 'tools/scripts/enforce-generated-file-banners.js',\n message: 'Generated banner enforcement failed. Run generated banner enforcer or relevant generators.'\n }\n ];\n\n checks.forEach((check) => {\n const cmd = spawnSync('node', check.args, { cwd: REPO_ROOT, encoding: 'utf8' });\n\n if (cmd.stdout) process.stdout.write(cmd.stdout);\n if (cmd.stderr) process.stderr.write(cmd.stderr);\n\n if (cmd.status !== 0) {\n errors.push({\n file: check.file,\n rule: 'Generated index freshness',\n message: check.message,\n line: 1\n });\n }\n });\n}\n\nfunction runTests(options = {}) {\n const errors = [];\n const warnings = [];\n\n checkRequiredFiles(errors);\n checkReadmeReferences(errors, warnings);\n checkGeneratedIndexFreshness(errors);\n\n const strict = Boolean(options.strict);\n const passed = strict ? errors.length === 0 && warnings.length === 0 : errors.length === 0;\n\n return {\n passed,\n errors,\n warnings,\n total: REQUIRED_MANUAL_FILES.length + REQUIRED_GENERATED_FILES.length\n };\n}\n\nfunction printResults(result, strict) {\n if (result.passed) {\n console.log(strict ? '✅ Docs-guide SoT checks passed in strict mode' : '✅ Docs-guide SoT checks passed');\n return;\n }\n\n if (result.errors.length > 0) {\n console.error(`❌ Docs-guide SoT errors: ${result.errors.length}`);\n result.errors.forEach((issue) => {\n console.error(` - [${issue.rule}] ${issue.file}: ${issue.message}`);\n });\n }\n\n if (result.warnings.length > 0) {\n const prefix = strict ? '❌' : '⚠️';\n const label = strict ? 'strict warnings' : 'advisory warnings';\n console.error(`${prefix} Docs-guide SoT ${label}: ${result.warnings.length}`);\n result.warnings.forEach((issue) => {\n console.error(` - [${issue.rule}] ${issue.file}: ${issue.message}`);\n });\n }\n}\n\nif (require.main === module) {\n const strict = process.argv.includes('--strict');\n const result = runTests({ strict });\n printResults(result, strict);\n process.exit(result.passed ? 0 : 1);\n}\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/docs-navigation.test.js",
+ "script": "docs-navigation.test",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests, docs.json",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Validates docs.json page-entry syntax, reports missing routes, warns on orphaned canonical v2 pages, suggests remaps, and optionally applies approved remaps",
+ "pipeline_declared": "P1, P3, P6",
+ "usage": "node tests/unit/docs-navigation.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script docs-navigation.test\n * @category validator\n * @purpose qa:repo-health\n * @scope tests, docs.json\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Validates docs.json page-entry syntax, reports missing routes, warns on orphaned canonical v2 pages, suggests remaps, and optionally applies approved remaps\n * @pipeline P1, P3, P6\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/docs-navigation.test.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst readline = require('readline');\nconst { execSync } = require('child_process');\nconst { listMintIgnoredRepoPaths } = require('../utils/mintignore');\n\nconst REPORT_MD_REL = 'tasks/reports/navigation-links/navigation-report.md';\nconst REPORT_JSON_REL = 'tasks/reports/navigation-links/navigation-report.json';\nconst I18N_CONFIG_REL = 'tools/i18n/config.json';\nconst DEFAULT_REMAP_THRESHOLD = 0.85;\nconst RESOURCE_HUB_REDIRECT_ROUTE = 'v2/resources/redirect';\nconst RESOURCE_HUB_PORTAL_ROUTE = 'v2/resources/resources-portal';\nconst LEGACY_RESOURCE_HUB_ROUTE = 'v2/pages/07_resources/redirect';\n\nlet errors = [];\nlet warnings = [];\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nfunction toPosix(filePath) {\n return String(filePath || '').split(path.sep).join('/');\n}\n\nfunction parseArgs(argv) {\n const args = {\n strictMissing: false,\n writeRemaps: false,\n writeReport: false,\n remapThreshold: DEFAULT_REMAP_THRESHOLD\n };\n\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (token === '--strict-missing') {\n args.strictMissing = true;\n continue;\n }\n if (token === '--write-remaps') {\n args.writeRemaps = true;\n continue;\n }\n if (token === '--write-report') {\n args.writeReport = true;\n continue;\n }\n if (token === '--no-write-report') {\n args.writeReport = false;\n continue;\n }\n if (token === '--remap-threshold') {\n const parsed = Number(argv[i + 1]);\n if (Number.isFinite(parsed) && parsed >= 0 && parsed <= 1) {\n args.remapThreshold = parsed;\n i += 1;\n }\n }\n }\n\n return args;\n}\n\nfunction ensureDir(dirPath) {\n fs.mkdirSync(dirPath, { recursive: true });\n}\n\nfunction walkDocsFiles(dirPath, out = []) {\n if (!fs.existsSync(dirPath)) return out;\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n for (const entry of entries) {\n const fullPath = path.join(dirPath, entry.name);\n if (entry.isDirectory()) {\n walkDocsFiles(fullPath, out);\n } else if (/\\.(md|mdx)$/i.test(entry.name)) {\n out.push(fullPath);\n }\n }\n return out;\n}\n\nfunction collectExistingRoutes(repoRoot) {\n const roots = [path.join(repoRoot, 'v1'), path.join(repoRoot, 'v2', 'pages'), path.join(repoRoot, 'v2')];\n const routeSet = new Set();\n\n roots.forEach((rootPath) => {\n const files = walkDocsFiles(rootPath);\n files.forEach((filePath) => {\n const relPath = toPosix(path.relative(repoRoot, filePath));\n const withoutExt = relPath.replace(/\\.(md|mdx)$/i, '');\n routeSet.add(withoutExt);\n\n if (withoutExt.endsWith('/index')) {\n routeSet.add(withoutExt.replace(/\\/index$/i, ''));\n }\n if (withoutExt.endsWith('/README')) {\n routeSet.add(withoutExt.replace(/\\/README$/i, ''));\n }\n });\n });\n\n return [...routeSet];\n}\n\nfunction collectPageEntries(node, pointer, out = []) {\n if (Array.isArray(node)) {\n node.forEach((item, index) => collectPageEntries(item, `${pointer}[${index}]`, out));\n return out;\n }\n\n if (!node || typeof node !== 'object') {\n return out;\n }\n\n if (Array.isArray(node.pages)) {\n node.pages.forEach((entry, index) => {\n const entryPointer = `${pointer}.pages[${index}]`;\n if (typeof entry === 'string') {\n out.push({ value: entry, pointer: entryPointer });\n return;\n }\n collectPageEntries(entry, entryPointer, out);\n });\n }\n\n Object.entries(node).forEach(([key, value]) => {\n if (key === 'pages') return;\n collectPageEntries(value, `${pointer}.${key}`, out);\n });\n\n return out;\n}\n\nfunction collectObjectNodes(node, pointer, out = []) {\n if (Array.isArray(node)) {\n node.forEach((item, index) => collectObjectNodes(item, `${pointer}[${index}]`, out));\n return out;\n }\n\n if (!node || typeof node !== 'object') return out;\n out.push({ node, pointer });\n\n Object.entries(node).forEach(([key, value]) => {\n collectObjectNodes(value, `${pointer}.${key}`, out);\n });\n\n return out;\n}\n\nfunction normalizeRoute(rawValue) {\n let value = String(rawValue || '').trim();\n value = value.replace(/^\\/+/, '');\n value = value.replace(/\\.(md|mdx)$/i, '');\n value = value.replace(/\\/+$/, '');\n return value;\n}\n\nfunction normalizeOrphanRouteKey(rawValue) {\n let value = toPosix(String(rawValue || '').trim());\n value = value.replace(/^\\/+/, '');\n value = value.replace(/\\.(md|mdx)$/i, '');\n value = value.replace(/\\/index$/i, '');\n value = value.replace(/\\/README$/i, '');\n value = value.replace(/\\/+$/, '');\n return value;\n}\n\nfunction loadI18nTargetLanguages(repoRoot) {\n const configPath = path.join(repoRoot, I18N_CONFIG_REL);\n if (!fs.existsSync(configPath)) return new Set();\n\n try {\n const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));\n const languages = Array.isArray(config?.targetLanguages) ? config.targetLanguages : [];\n return new Set(\n languages\n .map((language) => String(language || '').trim())\n .filter(Boolean)\n );\n } catch (_error) {\n return new Set();\n }\n}\n\nfunction getV2EnglishNavigationRouteKeys(docsJson) {\n const versions = Array.isArray(docsJson?.navigation?.versions) ? docsJson.navigation.versions : [];\n const v2VersionIndex = versions.findIndex((versionNode) => versionNode?.version === 'v2');\n if (v2VersionIndex === -1) return new Set();\n\n const languages = Array.isArray(versions[v2VersionIndex]?.languages) ? versions[v2VersionIndex].languages : [];\n const englishLanguageIndex = languages.findIndex((languageNode) => languageNode?.language === 'en');\n if (englishLanguageIndex === -1) return new Set();\n\n const pointer = `navigation.versions[${v2VersionIndex}].languages[${englishLanguageIndex}]`;\n const entries = collectPageEntries(languages[englishLanguageIndex], pointer);\n const routeKeys = new Set();\n\n entries.forEach(({ value }) => {\n const normalized = normalizeOrphanRouteKey(value);\n if (normalized.startsWith('v2/')) {\n routeKeys.add(normalized);\n }",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "runner",
+ "caller": "tests/run-all.js",
+ "pipeline": "P1"
+ },
+ {
+ "type": "runner",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "P3"
+ },
+ {
+ "type": "workflow-dispatch",
+ "caller": ".github/workflows/translate-docs.yml",
+ "workflow": "Docs Translation Pipeline",
+ "pipeline": "P6"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-all.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:docs-nav"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:docs-nav:write"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tests/unit/tasks/reports/navigation-links/navigation-report.json",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tests/unit/tasks/reports/navigation-links/navigation-report.md",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tests/unit/tasks/reports/navigation-links/navigation-report.json, tests/unit/tasks/reports/navigation-links/navigation-report.md",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 via run-all; P3 via run-pr-checks; P6 (Docs Translation Pipeline); indirect via tests/run-all.js; indirect via tests/run-pr-checks.js; manual (npm script: test:docs-nav); manual (npm script: test:docs-nav:write)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/unit/links-imports.test.js",
+ "script": "links-imports.test",
+ "category": "validator",
+ "purpose": "qa:link-integrity",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "E-R12, E-R14",
+ "purpose_statement": "Validates MDX internal links and snippet import paths are resolvable",
+ "pipeline_declared": "P1, P3",
+ "usage": "node tests/unit/links-imports.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script links-imports.test\n * @category validator\n * @purpose qa:link-integrity\n * @scope tests\n * @owner docs\n * @needs E-R12, E-R14\n * @purpose-statement Validates MDX internal links and snippet import paths are resolvable\n * @pipeline P1, P3\n * @usage node tests/unit/links-imports.test.js [flags]\n */\n/**\n * Broken links and imports validation\n * Checks that all internal links and imports resolve to existing files\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\nconst { getMdxFiles, getStagedDocsPageFiles, readFile } = require('../utils/file-walker');\nconst { extractImports } = require('../utils/mdx-parser');\n\nlet errors = [];\nlet warnings = [];\nconst GENERATED_EXTERNAL_DOCS = [\n 'awesome-livepeer-readme.mdx',\n 'box-additional-config.mdx',\n 'gwid-readme.mdx',\n 'whitepaper.mdx',\n 'wiki-readme.mdx'\n];\nconst V2_DOMAIN_DIRS = new Set([\n 'home',\n 'about',\n 'solutions',\n 'community',\n 'developers',\n 'gateways',\n 'orchestrators',\n 'lpt',\n 'resources',\n 'internal',\n 'deprecated',\n 'experimental',\n 'notes'\n]);\n\nfunction ensureExternalDocs() {\n const repoRoot = process.cwd();\n const externalDir = path.join(repoRoot, 'snippets', 'external');\n const missingFiles = GENERATED_EXTERNAL_DOCS.filter(\n (fileName) => !fs.existsSync(path.join(externalDir, fileName))\n );\n\n if (missingFiles.length === 0) {\n return;\n }\n\n const fetchScript = path.join(repoRoot, 'tools', 'scripts', 'snippets', 'fetch-external-docs.sh');\n if (!fs.existsSync(fetchScript)) {\n warnings.push({\n file: 'tools/scripts/snippets/fetch-external-docs.sh',\n message: `Missing external-doc bootstrap script; generated imports may fail: ${missingFiles.join(', ')}`\n });\n return;\n }\n\n const result = spawnSync('bash', [fetchScript], {\n cwd: repoRoot,\n encoding: 'utf8',\n env: {\n ...process.env,\n LC_ALL: process.env.LC_ALL || 'C',\n LANG: process.env.LANG || 'C'\n }\n });\n\n if (result.status !== 0) {\n const detail = String(result.stderr || result.stdout || '')\n .trim()\n .split('\\n')\n .slice(-3)\n .join(' ');\n errors.push({\n file: 'tools/scripts/snippets/fetch-external-docs.sh',\n rule: 'External docs fetch',\n message: `Failed to fetch generated external docs required for import validation.${detail ? ` ${detail}` : ''}`\n });\n }\n}\n\nfunction isStyleGuideExampleFile(file) {\n return file.includes('style-guide.mdx');\n}\n\n/**\n * Resolve a file path relative to the repository root\n */\nfunction resolveFilePath(filePath, rootDir = process.cwd()) {\n if (path.isAbsolute(filePath)) {\n return path.join(rootDir, filePath);\n }\n return path.resolve(rootDir, filePath);\n}\n\n/**\n * Check if a file exists (tries multiple extensions/variations)\n */\nfunction fileExists(filePath) {\n // Try exact path\n if (fs.existsSync(filePath)) {\n return { exists: true, path: filePath };\n }\n \n // Try with .mdx extension\n const withMdx = filePath.endsWith('.mdx') ? filePath : `${filePath}.mdx`;\n if (fs.existsSync(withMdx)) {\n return { exists: true, path: withMdx };\n }\n \n // Try as directory with index.mdx\n const dirIndex = path.join(filePath, 'index.mdx');\n if (fs.existsSync(dirIndex)) {\n return { exists: true, path: dirIndex };\n }\n \n // Try as directory with README.mdx\n const dirReadme = path.join(filePath, 'README.mdx');\n if (fs.existsSync(dirReadme)) {\n return { exists: true, path: dirReadme };\n }\n \n return { exists: false, path: null };\n}\n\n/**\n * Convert link path to file path\n */\nfunction linkToFilePath(linkPath, currentFile) {\n const rootDir = process.cwd();\n const normalizedLinkPath = linkPath\n .split('#')[0]\n .split('?')[0]\n .trim()\n .replace(/^['\"]+|['\"]+$/g, '');\n \n // Skip external links\n if (normalizedLinkPath.startsWith('http://') ||\n normalizedLinkPath.startsWith('https://') ||\n normalizedLinkPath.startsWith('mailto:') ||\n normalizedLinkPath.startsWith('#') ||\n normalizedLinkPath.length === 0) {\n return null;\n }\n \n // Get repo root directory (where .git is) - same fix as getStagedFiles\n const { execSync } = require('child_process');\n let repoRoot;\n try {\n repoRoot = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (e) {\n repoRoot = rootDir; // Fallback to rootDir if not in git repo\n }\n \n // Absolute path from root (starts with /)\n if (normalizedLinkPath.startsWith('/')) {\n // Remove leading slash and normalize trailing slash\n const repoRelativePath = normalizedLinkPath.replace(/^\\//, '').replace(/\\/$/, '');\n if (!repoRelativePath) {\n return null;\n }\n\n // Prefer repository-root absolute links when they already exist.\n const directRepoPath = path.join(repoRoot, repoRelativePath);\n if (fileExists(directRepoPath).exists) {\n return directRepoPath;\n }\n\n // If it already starts with v2/, treat it as a repo-relative docs path.\n if (repoRelativePath.startsWith('v2/')) {\n return path.join(repoRoot, repoRelativePath);\n }\n\n // Support migrated v2 domain folders, e.g. /home/... or /about/...\n const firstSegment = repoRelativePath.split('/')[0];\n if (V2_DOMAIN_DIRS.has(firstSegment)) {\n return path.join(repoRoot, 'v2', repoRelativePath);\n }\n\n // Fallback: treat bare absolute docs links as v2/pages-relative.\n return path.join(repoRoot, `v2/pages/${repoRelativePath}`);\n }\n \n // Relative path\n const currentDir = path.dirname(currentFile);\n const resolved = path.resolve(currentDir, normalizedLinkPath);\n\n // If this resolves into v2/pages//..., remap to v2//...\n // for migrated section folders.\n const relativePath = path.relative(rootDir, resolved);\n const normalizedRelative = relativePath.split(path.sep).join('/');\n const pagesPrefix = 'v2/pages/';\n if (normalizedRelative.startsWith(pagesPrefix)) {\n const parts = normalizedRelative.split('/');\n const maybeDomain = parts[2];\n if (V2_DOMAIN_DIRS.has(maybeDomain)) {\n const migratedPath = path.join(rootDir, 'v2', ...parts.slice(2));\n if (fileExists(migratedPath).exists) {\n return migratedPath;\n }\n }\n }\n\n return path.join(rootDir, relativePath);\n}\n\nfunction getIgnoredRanges(content) {\n const ignoredRanges = [];\n const ignoreRegexes = [",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "runner",
+ "caller": "tests/run-all.js",
+ "pipeline": "P1"
+ },
+ {
+ "type": "runner",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "P3"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-all.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:links"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "test:links"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 via run-all; P3 via run-pr-checks; indirect via tests/run-all.js; indirect via tests/run-pr-checks.js; manual (npm script: test:links); manual (npm script: test:links)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/unit/lpd-scoped-mint-dev.test.js",
+ "script": "lpd-scoped-mint-dev.test",
+ "category": "utility",
+ "purpose": "tooling:dev-tools",
+ "scope": "tests/unit, lpd, tools/scripts/mint-dev.sh, tools/scripts/dev/generate-mint-dev-scope.js",
+ "owner": "docs",
+ "needs": "E-C6, F-C1",
+ "purpose_statement": "Tests lpd scoped mint-dev functionality — validates dev server scope filtering",
+ "pipeline_declared": "manual — developer tool",
+ "usage": "node tests/unit/lpd-scoped-mint-dev.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script lpd-scoped-mint-dev.test\n * @category utility\n * @purpose tooling:dev-tools\n * @scope tests/unit, lpd, tools/scripts/mint-dev.sh, tools/scripts/dev/generate-mint-dev-scope.js\n * @owner docs\n * @needs E-C6, F-C1\n * @purpose-statement Tests lpd scoped mint-dev functionality — validates dev server scope filtering\n * @pipeline manual — developer tool\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/lpd-scoped-mint-dev.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\nconst {\n collectRoutesFromNavigation,\n collectOptionsFromNavigation,\n buildScopedNavigation,\n buildScopedMetadata,\n buildScopedMintignore,\n createScopedProfile\n} = require('../../tools/scripts/dev/generate-mint-dev-scope');\n\nconst REPO_ROOT = process.cwd();\nconst LPD_PATH = path.join(REPO_ROOT, 'lpd');\nconst SCOPE_SCRIPT_PATH = path.join(REPO_ROOT, 'tools/scripts/dev/generate-mint-dev-scope.js');\n\nconst SAMPLE_NAVIGATION = {\n versions: [\n {\n version: 'v2',\n languages: [\n {\n language: 'en',\n tabs: [\n {\n tab: 'Developers',\n anchors: [\n {\n anchor: 'Build',\n groups: [\n {\n group: 'Dev',\n pages: ['v2/dev/get-started', 'v2/dev/api-reference/auth']\n }\n ]\n }\n ]\n },\n {\n tab: 'Gateways',\n anchors: [\n {\n anchor: 'Run',\n groups: [\n {\n group: 'Gateway',\n pages: ['v2/gateways/overview']\n }\n ]\n }\n ]\n }\n ]\n },\n {\n language: 'es',\n tabs: [\n {\n tab: 'Developers',\n anchors: [\n {\n anchor: 'Construir',\n groups: [\n {\n group: 'Dev',\n pages: ['v2/es/dev/get-started']\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n },\n {\n version: 'v1',\n languages: [\n {\n language: 'en',\n tabs: [\n {\n tab: 'Legacy',\n anchors: [\n {\n anchor: 'Legacy',\n groups: [\n {\n group: 'Legacy',\n pages: ['v1/legacy/overview']\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n }\n ]\n};\n\nfunction runLpd(args) {\n return spawnSync('bash', [LPD_PATH, ...args], {\n cwd: REPO_ROOT,\n encoding: 'utf8'\n });\n}\n\nfunction runScopeScript(args, cwd) {\n return spawnSync('node', [SCOPE_SCRIPT_PATH, ...args], {\n cwd,\n encoding: 'utf8'\n });\n}\n\nfunction mkTmpDir(prefix) {\n return fs.mkdtempSync(path.join(os.tmpdir(), prefix));\n}\n\nfunction writeFile(absPath, content) {\n fs.mkdirSync(path.dirname(absPath), { recursive: true });\n fs.writeFileSync(absPath, content, 'utf8');\n}\n\nasync function runTests() {\n const failures = [];\n const cases = [];\n\n cases.push(async () => {\n const selection = {\n versions: ['v2'],\n languages: ['en'],\n tabs: ['Developers'],\n prefixes: ['v2/dev'],\n disableOpenapi: true\n };\n const scoped = buildScopedNavigation(SAMPLE_NAVIGATION, selection);\n const routes = collectRoutesFromNavigation(scoped);\n assert.deepStrictEqual(routes, ['v2/dev/get-started']);\n });\n\n cases.push(async () => {\n const selection = {\n versions: ['v2'],\n languages: ['en'],\n tabs: ['Developers'],\n prefixes: ['v2/dev'],\n disableOpenapi: true\n };\n const optionData = collectOptionsFromNavigation(SAMPLE_NAVIGATION);\n const allRoutes = collectRoutesFromNavigation(SAMPLE_NAVIGATION);\n const scopedRoutes = ['v2/dev/get-started'];\n const metadata = buildScopedMetadata(selection, optionData, allRoutes, scopedRoutes);\n const scopedMintignore = buildScopedMintignore('# base ignore\\n', metadata);\n assert.match(scopedMintignore, /\\/v1\\/\\*\\*/, 'should exclude unselected version');\n assert.match(scopedMintignore, /\\/v2\\/es\\/\\*\\*/, 'should exclude unselected language directory');\n assert.match(scopedMintignore, /api-reference/, 'should include openapi exclusion patterns');\n });\n\n cases.push(async () => {\n const run = runLpd([\n 'dev',\n '--dry-run',\n '--scoped',\n '--scope-version',\n 'v2',\n '--scope-language',\n 'en',\n '--scope-tab',\n 'Developers',\n '--scope-prefix',\n 'v2/dev',\n '--disable-openapi'\n ]);\n assert.strictEqual(run.status, 0, `lpd dry-run failed: ${run.stderr || run.stdout}`);\n assert.match(run.stdout, /Scoped mint profile: enabled/);\n assert.match(run.stdout, /scope_versions:\\s*v2/);\n assert.match(run.stdout, /scope_languages:\\s*en/);\n assert.match(run.stdout, /scope_tabs:\\s*Developers/);\n assert.match(run.stdout, /OpenAPI docs scope: disabled/);\n assert.match(run.stdout, /tools\\/scripts\\/mint-dev\\.sh/);\n });\n\n cases.push(async () => {\n const run = runLpd(['dev', '--dry-run', '--', '--port', '3333']);\n assert.strictEqual(run.status, 0, `lpd pass-through dry-run failed: ${run.stderr || run.stdout}`);\n assert.doesNotMatch(run.stdout, /Scoped mint profile: enabled/);\n assert.match(run.stdout, /--port/);\n assert.match(run.stdout, /3333/);\n });\n\n cases.push(async () => {\n const tmp = mkTmpDir('lpd-scope-test-');\n const docs = {\n $schema: 'https://mintlify.com/docs.json',\n theme: 'palm',\n name: 'Fixture Docs',\n navigation: SAMPLE_NAVIGATION\n };\n writeFile(path.join(tmp, 'docs.json'), `${JSON.stringify(docs, null, 2)}\\n`);\n writeFile(path.join(tmp, '.mintignore'), '# fixture\\n');\n\n const run = runScopeScript(",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/mdx-component-runtime-smoke.test.js",
+ "script": "mdx-component-runtime-smoke.test",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests/unit, tests/integration",
+ "owner": "docs",
+ "needs": "E-R1, R-R29",
+ "purpose_statement": "Unit tests for the MDX runtime smoke helpers — covers arg parsing, sentinel route selection, trigger logic, and failure classification.",
+ "pipeline_declared": "manual — targeted smoke helper coverage",
+ "usage": "node tests/unit/mdx-component-runtime-smoke.test.js",
+ "header": "#!/usr/bin/env node\n/**\n * @script mdx-component-runtime-smoke.test\n * @category validator\n * @purpose qa:content-quality\n * @scope tests/unit, tests/integration\n * @owner docs\n * @needs E-R1, R-R29\n * @purpose-statement Unit tests for the MDX runtime smoke helpers — covers arg parsing, sentinel route selection, trigger logic, and failure classification.\n * @pipeline manual — targeted smoke helper coverage\n * @usage node tests/unit/mdx-component-runtime-smoke.test.js\n */\n\nconst assert = require('assert');\nconst smoke = require('../integration/mdx-component-runtime-smoke');\n\nlet errors = [];\nlet warnings = [];\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n rule: 'mdx-component-runtime-smoke unit',\n message: `${name}: ${error.message}`,\n line: 1,\n file: 'tests/unit/mdx-component-runtime-smoke.test.js'\n });\n }\n}\n\nasync function runTests() {\n errors = [];\n warnings = [];\n\n console.log('🧪 MDX Runtime Smoke Unit Tests');\n\n await runCase('Parses default args with sentinel routes', async () => {\n const parsed = smoke.parseArgs([]);\n assert.strictEqual(parsed.routes.length, 0);\n assert.ok(Array.isArray(smoke.getRoutes(parsed)));\n assert.strictEqual(smoke.getRoutes(parsed).length, smoke.SENTINEL_ROUTES.length);\n });\n\n await runCase('Parses explicit routes and base URL', async () => {\n const parsed = smoke.parseArgs([\n '--routes',\n '/v2/about/portal,/v2/developers/portal',\n '--base-url',\n 'http://localhost:3001'\n ]);\n assert.deepStrictEqual(smoke.getRoutes(parsed), ['/v2/about/portal', '/v2/developers/portal']);\n assert.strictEqual(parsed.baseUrl, 'http://localhost:3001');\n });\n\n await runCase('Trigger logic includes component, validator, smoke script, and sentinel page changes', async () => {\n assert.strictEqual(smoke.shouldRunForChangedFiles(['snippets/components/primitives/links.jsx']), true);\n assert.strictEqual(\n smoke.shouldRunForChangedFiles(['tools/scripts/validators/components/check-mdx-component-scope.js']),\n true\n );\n assert.strictEqual(smoke.shouldRunForChangedFiles(['tests/integration/mdx-component-runtime-smoke.js']), true);\n assert.strictEqual(smoke.shouldRunForChangedFiles(['v2/home/mission-control.mdx']), true);\n assert.strictEqual(smoke.shouldRunForChangedFiles(['docs-guide/governance-guides/component-governance.mdx']), false);\n });\n\n await runCase('Blocking console and page errors are classified correctly', async () => {\n assert.strictEqual(\n smoke.isBlockingConsoleMessage('error', 'ReferenceError: normalizeIconName is not defined'),\n true\n );\n assert.strictEqual(smoke.isBlockingConsoleMessage('warning', 'ReferenceError: test'), false);\n assert.strictEqual(smoke.isBlockingPageError(\"Cannot access 'BlinkingIcon' before initialization\"), true);\n assert.strictEqual(smoke.isBlockingPageError('ResizeObserver loop limit exceeded'), false);\n });\n\n await runCase('DOM failure classification catches 404, error boundary, empty, and runtime body markers', async () => {\n assert.strictEqual(\n smoke.classifyDomFailure({\n status: 404,\n bodyText: 'Not found',\n bodyLength: 700,\n h1Text: '404'\n }),\n 'Route returned HTTP 404'\n );\n\n assert.strictEqual(\n smoke.classifyDomFailure({\n status: 200,\n bodyText: 'content',\n bodyLength: 700,\n h1Text: 'Mission Control',\n hasErrorBoundary: true\n }),\n 'Page rendered an error boundary'\n );\n\n assert.strictEqual(\n smoke.classifyDomFailure({\n status: 200,\n bodyText: 'tiny',\n bodyLength: 4,\n h1Text: ''\n }),\n 'Page appears empty or failed to render (4 chars)'\n );\n\n assert.match(\n smoke.classifyDomFailure({\n status: 200,\n bodyText: 'ReferenceError: normalizeIconName is not defined',\n bodyLength: 900,\n h1Text: 'Mission Control'\n }),\n /blocking runtime error text/\n );\n });\n\n return {\n errors,\n warnings,\n passed: errors.length === 0,\n total: 5\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ MDX runtime smoke unit tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} MDX runtime smoke unit test failure(s)`);\n result.errors.forEach((err) => console.error(` - ${err.message}`));\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ MDX runtime smoke unit tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/mdx-component-scope.test.js",
+ "script": "mdx-component-scope.test",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests/unit, tools/scripts/validators/components",
+ "owner": "docs",
+ "needs": "R-R10, R-R29",
+ "purpose_statement": "Unit tests for the MDX-facing component scope validator — covers unsafe private helpers, safe inline logic, and imported helper patterns.",
+ "pipeline_declared": "manual — targeted validator unit coverage",
+ "usage": "node tests/unit/mdx-component-scope.test.js",
+ "header": "#!/usr/bin/env node\n/**\n * @script mdx-component-scope.test\n * @category validator\n * @purpose qa:repo-health\n * @scope tests/unit, tools/scripts/validators/components\n * @owner docs\n * @needs R-R10, R-R29\n * @purpose-statement Unit tests for the MDX-facing component scope validator — covers unsafe private helpers, safe inline logic, and imported helper patterns.\n * @pipeline manual — targeted validator unit coverage\n * @usage node tests/unit/mdx-component-scope.test.js\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst path = require('path');\nconst scopeValidator = require('../../tools/scripts/validators/components/check-mdx-component-scope');\n\nconst REPO_ROOT = path.resolve(__dirname, '../..');\nconst FIXTURE_ROOT = path.join(\n REPO_ROOT,\n 'snippets',\n 'components',\n '_codex-mdx-component-scope-fixtures'\n);\n\nlet errors = [];\nlet warnings = [];\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction writeFixtureFile(name, content) {\n fs.mkdirSync(FIXTURE_ROOT, { recursive: true });\n const fullPath = path.join(FIXTURE_ROOT, name);\n fs.writeFileSync(fullPath, `${content.trim()}\\n`, 'utf8');\n return toPosix(path.relative(REPO_ROOT, fullPath));\n}\n\nfunction removeFixtureRoot() {\n fs.rmSync(FIXTURE_ROOT, { recursive: true, force: true });\n}\n\nfunction buildIndex(relPath) {\n return new Map([\n [\n relPath,\n {\n componentPath: relPath,\n importers: new Set(['v2/home/mission-control.mdx'])\n }\n ]\n ]);\n}\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n rule: 'mdx-component-scope unit',\n message: `${name}: ${error.message}`,\n line: 1,\n file: 'tests/unit/mdx-component-scope.test.js'\n });\n }\n}\n\nasync function runTests() {\n errors = [];\n warnings = [];\n removeFixtureRoot();\n\n console.log('🧪 MDX Component Scope Unit Tests');\n\n await runCase('Unsafe private helper used by exported component fails', async () => {\n const relPath = writeFixtureFile(\n 'unsafe-helper.jsx',\n `\n const normalizeIconName = (value) => value || 'terminal';\n export const BlinkingIcon = ({ icon }) => {\n return {normalizeIconName(icon)};\n };\n `\n );\n const findings = scopeValidator.analyseComponentFile(relPath, buildIndex(relPath));\n assert.strictEqual(findings.length, 1);\n assert.strictEqual(findings[0].component, 'BlinkingIcon');\n assert.strictEqual(findings[0].helper, 'normalizeIconName');\n });\n\n await runCase('Inline guard logic passes', async () => {\n const relPath = writeFixtureFile(\n 'inline-safe.jsx',\n `\n export const BlinkingIcon = ({ icon }) => {\n const resolvedIcon = typeof icon === 'string' && icon.trim() ? icon : 'terminal';\n return {resolvedIcon};\n };\n `\n );\n const findings = scopeValidator.analyseComponentFile(relPath, buildIndex(relPath));\n assert.strictEqual(findings.length, 0);\n });\n\n await runCase('Imported .js helper passes', async () => {\n writeFixtureFile(\n 'scope-helper.js',\n `\n export const normalizeIconName = (value) => value || 'terminal';\n `\n );\n const relPath = writeFixtureFile(\n 'imported-helper.jsx',\n `\n import { normalizeIconName } from './scope-helper.js';\n export const BlinkingIcon = ({ icon }) => {\n return {normalizeIconName(icon)};\n };\n `\n );\n const findings = scopeValidator.analyseComponentFile(relPath, buildIndex(relPath));\n assert.strictEqual(findings.length, 0);\n });\n\n await runCase('Trailing export pattern fails correctly', async () => {\n const relPath = writeFixtureFile(\n 'trailing-export.jsx',\n `\n const normalizeIconName = (value) => value || 'terminal';\n const BlinkingIcon = ({ icon }) => {normalizeIconName(icon)};\n export { BlinkingIcon };\n `\n );\n const findings = scopeValidator.analyseComponentFile(relPath, buildIndex(relPath));\n assert.strictEqual(findings.length, 1);\n assert.strictEqual(findings[0].component, 'BlinkingIcon');\n assert.strictEqual(findings[0].helper, 'normalizeIconName');\n });\n\n await runCase('Non-MDX-facing component file is ignored', async () => {\n const relPath = writeFixtureFile(\n 'not-imported.jsx',\n `\n const normalizeIconName = (value) => value || 'terminal';\n export const BlinkingIcon = ({ icon }) => {normalizeIconName(icon)};\n `\n );\n const findings = scopeValidator.analyseComponentFile(relPath, new Map());\n assert.strictEqual(findings.length, 0);\n });\n\n await runCase('Explicit empty MDX scope does not expand to all routable pages', async () => {\n const importedFiles = scopeValidator.getComponentFilesImportedByMdxFiles([]);\n assert.deepStrictEqual(importedFiles, []);\n });\n\n await runCase('Component-local variables are not false-flagged', async () => {\n const relPath = writeFixtureFile(\n 'component-local.jsx',\n `\n export const BlinkingIcon = ({ icon }) => {\n const normalizeIconName = (value) => value || 'terminal';\n return {normalizeIconName(icon)};\n };\n `\n );\n const findings = scopeValidator.analyseComponentFile(relPath, buildIndex(relPath));\n assert.strictEqual(findings.length, 0);\n });\n\n removeFixtureRoot();\n\n return {\n errors,\n warnings,\n passed: errors.length === 0,\n total: 7\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ MDX component scope unit tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} MDX component scope unit test failure(s)`);\n result.errors.forEach((err) => console.error(` - ${err.message}`));\n process.exit(1);\n })\n .catch((error) => {\n removeFixtureRoot();\n console.error(`\\n❌ MDX component scope unit tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/mdx-guards.test.js",
+ "script": "mdx-guards.test",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests, v2/pages, snippets/pages, snippets/snippetsWiki",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Enforces MDX guardrails — globals imports, math delimiters, markdown table line breaks",
+ "pipeline_declared": "P1, P3",
+ "usage": "node tests/unit/mdx-guards.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script mdx-guards.test\n * @category validator\n * @purpose qa:content-quality\n * @scope tests, v2/pages, snippets/pages, snippets/snippetsWiki\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Enforces MDX guardrails — globals imports, math delimiters, markdown table line breaks\n * @pipeline P1, P3\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/mdx-guards.test.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\n\nlet errors = [];\nlet warnings = [];\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nfunction toPosix(filePath) {\n return String(filePath || '').split(path.sep).join('/');\n}\n\nfunction toRelative(filePath, rootDir) {\n return toPosix(path.relative(rootDir, filePath));\n}\n\nfunction getLineFromIndex(content, index) {\n return content.slice(0, index).split('\\n').length;\n}\n\nfunction walkFiles(dirPath, predicate, out = []) {\n if (!fs.existsSync(dirPath)) return out;\n const entries = fs.readdirSync(dirPath, { withFileTypes: true });\n\n for (const entry of entries) {\n if (entry.name === '.git' || entry.name === 'node_modules') continue;\n const fullPath = path.join(dirPath, entry.name);\n if (entry.isDirectory()) {\n walkFiles(fullPath, predicate, out);\n continue;\n }\n if (predicate(fullPath)) out.push(fullPath);\n }\n\n return out;\n}\n\nfunction collectMdxFiles(rootDir, relDir) {\n return walkFiles(path.join(rootDir, relDir), (filePath) => filePath.endsWith('.mdx'));\n}\n\nfunction collectMarkdownFiles(rootDir, relDir) {\n return walkFiles(path.join(rootDir, relDir), (filePath) => /\\.(md|mdx)$/i.test(filePath));\n}\n\nfunction readFileSafe(filePath) {\n try {\n return fs.readFileSync(filePath, 'utf8');\n } catch (_error) {\n return '';\n }\n}\n\nfunction stripFencedCode(content) {\n return content.replace(/```[\\s\\S]*?```/g, '');\n}\n\nfunction checkLatestVersionAliasing(rootDir) {\n const files = [\n ...collectMdxFiles(rootDir, 'v2/pages'),\n ...collectMdxFiles(rootDir, 'snippets/pages')\n ];\n const aliasPattern = /import\\s*\\{[^}]*\\blatestVersion\\s+as\\s+[A-Za-z_$][\\w$]*[^}]*\\}\\s*from\\s*['\"][^'\"]+['\"]/g;\n\n files.forEach((filePath) => {\n const content = stripFencedCode(readFileSafe(filePath));\n let match;\n while ((match = aliasPattern.exec(content)) !== null) {\n errors.push({\n file: toRelative(filePath, rootDir),\n rule: 'latestVersion aliasing',\n line: getLineFromIndex(content, match.index),\n message: 'Do not alias latestVersion (use latestVersion directly).'\n });\n }\n });\n\n return files.length;\n}\n\nfunction checkGlobalsImportPath(rootDir) {\n const files = [\n ...collectMdxFiles(rootDir, 'v2/pages'),\n ...collectMdxFiles(rootDir, 'snippets/pages'),\n ...collectMdxFiles(rootDir, 'snippets/snippetsWiki')\n ];\n const globalsJsxPattern = /from\\s*['\"]\\/snippets\\/automations\\/globals\\/globals\\.jsx['\"]/g;\n\n files.forEach((filePath) => {\n const content = stripFencedCode(readFileSafe(filePath));\n let match;\n while ((match = globalsJsxPattern.exec(content)) !== null) {\n errors.push({\n file: toRelative(filePath, rootDir),\n rule: 'globals import path',\n line: getLineFromIndex(content, match.index),\n message: 'Use /snippets/automations/globals/globals.mdx (not globals.jsx).'\n });\n }\n });\n\n return files.length;\n}\n\nfunction checkLptMathDelimiters(rootDir) {\n const files = collectMdxFiles(rootDir, 'v2/pages/06_lptoken');\n const pattern = /\\\\\\(|\\\\\\[/g;\n\n files.forEach((filePath) => {\n const content = readFileSafe(filePath);\n let match;\n while ((match = pattern.exec(content)) !== null) {\n errors.push({\n file: toRelative(filePath, rootDir),\n rule: 'LPT math delimiters',\n line: getLineFromIndex(content, match.index),\n message: 'Use $...$ and $$...$$ delimiters; do not use \\\\( or \\\\[ in v2/pages/06_lptoken.'\n });\n }\n });\n\n return files.length;\n}\n\nfunction checkRawBrInMarkdownTables(rootDir) {\n const files = collectMarkdownFiles(rootDir, 'tests');\n const rawBrPattern = /
(?!\\s*\\/>)/;\n\n files.forEach((filePath) => {\n const content = readFileSafe(filePath);\n if (!content) return;\n const lines = content.split('\\n');\n\n lines.forEach((line, idx) => {\n if (!/^\\s*\\|/.test(line)) return;\n if (!rawBrPattern.test(line)) return;\n errors.push({\n file: toRelative(filePath, rootDir),\n rule: 'Raw
in markdown table',\n line: idx + 1,\n message: 'Use
in markdown table cells.'\n });\n });\n });\n\n return files.length;\n}\n\nfunction runTests() {\n errors = [];\n warnings = [];\n const rootDir = getRepoRoot();\n\n const scannedCounts = [];\n scannedCounts.push(checkLatestVersionAliasing(rootDir));\n scannedCounts.push(checkGlobalsImportPath(rootDir));\n scannedCounts.push(checkLptMathDelimiters(rootDir));\n scannedCounts.push(checkRawBrInMarkdownTables(rootDir));\n\n return {\n errors,\n warnings,\n passed: errors.length === 0,\n total: scannedCounts.reduce((acc, count) => acc + count, 0)\n };\n}\n\nif (require.main === module) {\n const result = runTests();\n\n if (result.errors.length > 0) {\n console.error('\\n❌ MDX Guardrail Errors:\\n');\n result.errors.forEach((err) => {\n console.error(` ${err.file}:${err.line} - ${err.rule}: ${err.message}`);\n });\n }\n\n if (result.warnings.length > 0) {\n console.warn('\\n⚠️ MDX Guardrail Warnings:\\n');\n result.warnings.forEach((warn) => {\n console.warn(` ${warn.file}:${warn.line || 1} - ${warn.message}`);\n });\n }\n\n if (result.passed) {\n console.log(`\\n✅ MDX guardrails passed (${result.total} file scans)`);\n process.exit(0);\n }\n\n console.error(`\\n❌ ${result.errors.length} guardrail error(s) found`);\n process.exit(1);\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "runner",
+ "caller": "tests/run-all.js",
+ "pipeline": "P1"
+ },
+ {
+ "type": "runner",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "P3"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-all.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:mdx:guards"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 via run-all; P3 via run-pr-checks; indirect via tests/run-all.js; indirect via tests/run-pr-checks.js; manual (npm script: test:mdx:guards)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/unit/mdx-safe-markdown.test.js",
+ "script": "mdx-safe-markdown.test",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests/unit, tests/fixtures/mdx-safe-markdown, tools/lib, tools/scripts/remediators/content, tools/scripts/validators/content",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Fixture-driven unit tests for repo-wide MDX-safe markdown repair and validation helpers.",
+ "pipeline_declared": "P1 (commit, via run-all)",
+ "usage": "node tests/unit/mdx-safe-markdown.test.js",
+ "header": "#!/usr/bin/env node\n/**\n * @script mdx-safe-markdown.test\n * @category validator\n * @purpose qa:content-quality\n * @scope tests/unit, tests/fixtures/mdx-safe-markdown, tools/lib, tools/scripts/remediators/content, tools/scripts/validators/content\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Fixture-driven unit tests for repo-wide MDX-safe markdown repair and validation helpers.\n * @pipeline P1 (commit, via run-all)\n * @usage node tests/unit/mdx-safe-markdown.test.js\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst path = require('path');\n\nconst { repairMarkdownContent, validateMarkdownContent } = require('../../tools/lib/mdx-safe-markdown');\n\nconst REPO_ROOT = path.resolve(__dirname, '../..');\nconst FIXTURE_DIR = path.join(REPO_ROOT, 'tests', 'fixtures', 'mdx-safe-markdown');\n\nlet errors = [];\nlet warnings = [];\n\nfunction loadJson(fileName) {\n return JSON.parse(fs.readFileSync(path.join(FIXTURE_DIR, fileName), 'utf8'));\n}\n\nfunction runCase(name, fn) {\n try {\n fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n file: 'tests/unit/mdx-safe-markdown.test.js',\n line: 1,\n rule: 'mdx-safe-markdown unit',\n message: `${name}: ${error.message}`\n });\n }\n}\n\nfunction runTests() {\n errors = [];\n warnings = [];\n\n console.log('🧪 MDX-safe Markdown Unit Tests');\n\n loadJson('repair-cases.json').forEach((testCase) => {\n runCase(testCase.name, () => {\n const input = testCase.inputLines.join('\\n');\n const expected = testCase.expectedLines.join('\\n');\n const result = repairMarkdownContent(input, testCase.filePath);\n\n assert.strictEqual(result.content, expected);\n assert.deepStrictEqual(\n result.changes.map((change) => change.rule),\n testCase.expectedRules\n );\n });\n });\n\n loadJson('validator-cases.json').forEach((testCase) => {\n runCase(testCase.name, () => {\n const input = testCase.contentLines.join('\\n');\n const result = validateMarkdownContent(input, testCase.filePath);\n\n assert.deepStrictEqual(\n result.findings.map((finding) => finding.rule),\n testCase.expectedRules\n );\n });\n });\n\n return {\n errors,\n warnings,\n passed: errors.length === 0,\n total: loadJson('repair-cases.json').length + loadJson('validator-cases.json').length\n };\n}\n\nif (require.main === module) {\n const result = runTests();\n\n if (result.errors.length > 0) {\n console.error('\\n❌ MDX-safe markdown unit test failures:\\n');\n result.errors.forEach((error) => {\n console.error(` ${error.file}:${error.line} - ${error.rule}: ${error.message}`);\n });\n } else {\n console.log(`\\n✅ MDX-safe markdown unit tests passed (${result.total} cases checked)`);\n }\n\n process.exit(result.passed ? 0 : 1);\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "runner",
+ "caller": "tests/run-all.js",
+ "pipeline": "P1"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-all.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:mdx:safe:unit"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 via run-all; indirect via tests/run-all.js; manual (npm script: test:mdx:safe:unit)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/unit/mdx.test.js",
+ "script": "mdx.test",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Validates MDX syntax and structure — checks for parse errors, invalid JSX, broken components",
+ "pipeline_declared": "P1, P3",
+ "usage": "node tests/unit/mdx.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script mdx.test\n * @category validator\n * @purpose qa:content-quality\n * @scope tests\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Validates MDX syntax and structure — checks for parse errors, invalid JSX, broken components\n * @pipeline P1, P3\n * @usage node tests/unit/mdx.test.js [flags]\n */\n/**\n * MDX validation tests\n */\n\nconst { getMdxFiles, getStagedDocsPageFiles, readFile } = require('../utils/file-walker');\nconst { validateMdx } = require('../utils/mdx-parser');\n\nlet errors = [];\nlet warnings = [];\n\n/**\n * Run MDX validation tests\n */\nfunction runTests(options = {}) {\n errors = [];\n warnings = [];\n \n const { files = null, stagedOnly = false } = options;\n \n let testFiles = files;\n if (!testFiles) {\n if (stagedOnly) {\n testFiles = getStagedDocsPageFiles().filter(f => f.endsWith('.mdx'));\n } else {\n testFiles = getMdxFiles();\n }\n }\n \n testFiles.forEach(file => {\n const content = readFile(file);\n if (!content) return;\n \n const result = validateMdx(content, file);\n errors.push(...result.errors.map(err => ({\n file,\n ...(typeof err === 'string' ? { message: err } : err)\n })));\n warnings.push(...result.warnings.map(warn => ({\n file,\n message: warn\n })));\n });\n \n return {\n errors,\n warnings,\n passed: errors.length === 0,\n total: testFiles.length\n };\n}\n\n// Run if called directly\nif (require.main === module) {\n const args = process.argv.slice(2);\n const stagedOnly = args.includes('--staged');\n \n const result = runTests({ stagedOnly });\n \n if (result.errors.length > 0) {\n console.error('\\n❌ MDX Validation Errors:\\n');\n result.errors.forEach(err => {\n const line = err.line ? `:${err.line}` : '';\n console.error(` ${err.file}${line} - ${err.message || err}`);\n });\n }\n \n if (result.warnings.length > 0) {\n console.warn('\\n⚠️ MDX Warnings:\\n');\n result.warnings.forEach(warn => {\n console.warn(` ${warn.file} - ${warn.message}`);\n });\n }\n \n if (result.passed) {\n console.log(`\\n✅ MDX validation passed (${result.total} files checked)`);\n process.exit(0);\n } else {\n console.error(`\\n❌ ${result.errors.length} error(s) found`);\n process.exit(1);\n }\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "runner",
+ "caller": "tests/run-all.js",
+ "pipeline": "P1"
+ },
+ {
+ "type": "runner",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "P3"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-all.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:mdx"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "test:mdx"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 via run-all; P3 via run-pr-checks; indirect via tests/run-all.js; indirect via tests/run-pr-checks.js; manual (npm script: test:mdx); manual (npm script: test:mdx)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/unit/migrate-assets-to-branch.test.js",
+ "script": "migrate-assets-to-branch.test",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests/unit, tools/scripts/remediators/assets, tools/scripts/audit-media-assets.js",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Unit tests for migrate-assets-to-branch.js — validates CLI defaults, ambiguous basename detection, deterministic rewrites, and end-to-end branch migration in a temp git repo",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/migrate-assets-to-branch.test.js",
+ "header": "#!/usr/bin/env node\n/**\n * @script migrate-assets-to-branch.test\n * @category validator\n * @purpose qa:repo-health\n * @scope tests/unit, tools/scripts/remediators/assets, tools/scripts/audit-media-assets.js\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Unit tests for migrate-assets-to-branch.js — validates CLI defaults, ambiguous basename detection, deterministic rewrites, and end-to-end branch migration in a temp git repo\n * @pipeline manual — not yet in pipeline\n * @dualmode --dry-run (validator) | --write (remediator)\n * @usage node tests/unit/migrate-assets-to-branch.test.js\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst migrateAssets = require('../../tools/scripts/remediators/assets/migrate-assets-to-branch');\n\nconst SCRIPT_PATH = path.join(\n __dirname,\n '..',\n '..',\n 'tools',\n 'scripts',\n 'remediators',\n 'assets',\n 'migrate-assets-to-branch.js'\n);\n\nlet errors = [];\n\nfunction runCommand(command, args, options = {}) {\n const env = { ...process.env };\n delete env.GIT_DIR;\n delete env.GIT_WORK_TREE;\n delete env.GIT_INDEX_FILE;\n delete env.GIT_OBJECT_DIRECTORY;\n delete env.GIT_ALTERNATE_OBJECT_DIRECTORIES;\n delete env.GIT_COMMON_DIR;\n delete env.GIT_PREFIX;\n\n const result = spawnSync(command, args, {\n cwd: options.cwd,\n encoding: 'utf8',\n env\n });\n\n if (result.status !== 0 && !options.allowFailure) {\n const detail = String(result.stderr || result.stdout || `exit ${result.status}`).trim();\n throw new Error(`${command} ${args.join(' ')} failed: ${detail}`);\n }\n\n return result;\n}\n\nfunction runCase(name, fn) {\n try {\n fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n rule: 'migrate-assets-to-branch unit',\n message: `${name}: ${error.message}`,\n line: 1,\n file: 'tests/unit/migrate-assets-to-branch.test.js'\n });\n }\n}\n\nfunction writeFile(repoRoot, repoPath, value) {\n const absPath = path.join(repoRoot, repoPath);\n fs.mkdirSync(path.dirname(absPath), { recursive: true });\n fs.writeFileSync(absPath, value);\n}\n\nfunction createFixtureRepo() {\n const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'migrate-assets-repo-'));\n const remoteDir = path.join(tempRoot, 'remote.git');\n const repoDir = path.join(tempRoot, 'repo');\n\n fs.mkdirSync(repoDir, { recursive: true });\n runCommand('git', ['init', '--bare', remoteDir]);\n runCommand('git', ['init', '-b', 'docs-v2'], { cwd: repoDir });\n runCommand('git', ['config', 'user.name', 'Codex Test'], { cwd: repoDir });\n runCommand('git', ['config', 'user.email', 'codex@example.com'], { cwd: repoDir });\n runCommand('git', ['remote', 'add', 'origin', remoteDir], { cwd: repoDir });\n\n writeFile(\n repoDir,\n 'snippets/assets/media/videos/HeroBackground.mp4',\n 'fake-video-binary'\n );\n writeFile(\n repoDir,\n 'v2/example.mdx',\n '\\n'\n );\n writeFile(\n repoDir,\n 'tasks/reports/media-audit/media-audit-manifest.json',\n `${JSON.stringify({\n assets: [\n {\n path: 'snippets/assets/media/videos/HeroBackground.mp4',\n migration_target: 'migrate_r2',\n mdx_references: ['v2/example.mdx']\n }\n ]\n }, null, 2)}\\n`\n );\n writeFile(\n repoDir,\n 'tests/package.json',\n `${JSON.stringify({\n name: 'fixture-tests',\n private: true,\n scripts: {\n test: 'node ok.js'\n }\n }, null, 2)}\\n`\n );\n writeFile(repoDir, 'tests/ok.js', \"console.log('ok');\\n\");\n\n runCommand('git', ['add', '.'], { cwd: repoDir });\n runCommand('git', ['commit', '-m', 'initial docs-v2 fixture'], { cwd: repoDir });\n runCommand('git', ['push', '-u', 'origin', 'docs-v2'], { cwd: repoDir });\n\n const existingAssetsBranch = runCommand(\n 'git',\n ['show-ref', '--verify', '--quiet', 'refs/heads/docs-v2-assets'],\n {\n cwd: repoDir,\n allowFailure: true\n }\n );\n if (existingAssetsBranch.status === 0) {\n runCommand('git', ['branch', '-D', 'docs-v2-assets'], { cwd: repoDir });\n }\n runCommand('git', ['checkout', '--orphan', 'docs-v2-assets'], { cwd: repoDir });\n runCommand('git', ['rm', '-rf', '.'], { cwd: repoDir });\n writeFile(repoDir, '.nojekyll', '');\n runCommand('git', ['add', '.nojekyll'], { cwd: repoDir });\n runCommand('git', ['commit', '-m', 'init assets branch'], { cwd: repoDir });\n runCommand('git', ['push', '-u', 'origin', 'docs-v2-assets'], { cwd: repoDir });\n runCommand('git', ['checkout', 'docs-v2'], { cwd: repoDir });\n runCommand('git', ['fetch', 'origin', 'docs-v2-assets'], { cwd: repoDir });\n\n return { tempRoot, remoteDir, repoDir };\n}\n\nfunction cleanupFixtureRepo(fixture) {\n if (!fixture) return;\n fs.rmSync(fixture.tempRoot, { recursive: true, force: true });\n}\n\nfunction runTests() {\n errors = [];\n\n console.log('🧪 Migrate Assets To Branch Unit Tests');\n\n runCase('Parses default args as dry-run with default targets', () => {\n const parsed = migrateAssets.parseArgs([]);\n assert.strictEqual(parsed.mode, 'dry-run');\n assert.deepStrictEqual(parsed.targets, ['migrate_r2', 'migrate_cloudinary']);\n assert.strictEqual(parsed.skipCopy, false);\n assert.strictEqual(parsed.skipRefs, false);\n });\n\n runCase('Flags basename-only references as ambiguous', () => {\n const analysis = migrateAssets.analyzeSourceReference(\n 'v2/example.mdx',\n 'snippets/assets/media/videos/HeroBackground.mp4',\n ''\n );\n\n assert.strictEqual(analysis.ambiguousBasenameOnly, true);\n assert.strictEqual(analysis.needsRewrite, false);\n });\n\n runCase('Rewrites rooted and relative references to canonical raw GitHub URLs', () => {\n const rooted = migrateAssets.rewriteSourceReference(\n 'v2/example.mdx',\n 'snippets/assets/media/videos/HeroBackground.mp4',\n ''\n );\n const relative = migrateAssets.rewriteSourceReference(\n 'v2/example.mdx',\n 'snippets/assets/media/videos/HeroBackground.mp4',\n ''\n );\n\n assert.ok(rooted.content.includes(migrateAssets.buildCanonicalAssetUrl('snippets/assets/media/videos/HeroBackground.mp4')));\n assert.ok(relative.content.includes(migrateAssets.buildCanonicalAssetUrl('snippets/assets/media/videos/HeroBackground.mp4')));\n });\n\n runCase('Write mode copies, rewrites, and deletes in a temp repo fixture', () => {\n const fixture = createFixtureRepo();\n\n try {\n const result = runCommand(\n 'node',\n [\n SCRIPT_PATH,\n '--manifest',\n 'tasks/reports/media-audit/media-audit-manifest.json',\n '--file',\n 'snippets/assets/media/videos/HeroBackground.mp4',\n '--write'\n ],\n {\n cwd: fixture.repoDir\n }\n );\n\n assert.strictEqual(result.status, 0);\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFile"
+ },
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFileSync"
+ },
+ {
+ "output_path": "tests/unit/repo",
+ "type": "directory",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": "tests/unit/repo",
+ "type": "directory",
+ "call": "writeFile"
+ }
+ ],
+ "outputs_display": ", , tests/unit/repo, tests/unit/repo",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/openapi-reference-audit.test.js",
+ "script": "openapi-reference-audit.test",
+ "category": "validator",
+ "purpose": "tooling:api-spec",
+ "scope": "tests/unit, tests/integration, v2, api",
+ "owner": "docs",
+ "needs": "F-R17",
+ "purpose_statement": "Unit tests for openapi-reference-audit.js — tests individual audit rules and fix logic",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/openapi-reference-audit.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script openapi-reference-audit.test\n * @category validator\n * @purpose tooling:api-spec\n * @scope tests/unit, tests/integration, v2, api\n * @owner docs\n * @needs F-R17\n * @purpose-statement Unit tests for openapi-reference-audit.js — tests individual audit rules and fix logic\n * @pipeline manual — not yet in pipeline\n * @dualmode --strict (enforcer) | --fix (remediator)\n * @usage node tests/unit/openapi-reference-audit.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst audit = require('../integration/openapi-reference-audit');\n\nlet errors = [];\nlet warnings = [];\n\nfunction getRepoRoot() {\n try {\n return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();\n } catch (_error) {\n return process.cwd();\n }\n}\n\nconst REPO_ROOT = getRepoRoot();\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction mkFixture(relDir, filename, content) {\n const absDir = path.join(REPO_ROOT, relDir);\n fs.mkdirSync(absDir, { recursive: true });\n const absFile = path.join(absDir, filename);\n fs.writeFileSync(absFile, content, 'utf8');\n return {\n absFile,\n relFile: toPosix(path.relative(REPO_ROOT, absFile)),\n absDir\n };\n}\n\nfunction cleanupFixture(fixture) {\n if (!fixture) return;\n if (fs.existsSync(fixture.absFile)) {\n fs.unlinkSync(fixture.absFile);\n }\n try {\n fs.rmdirSync(fixture.absDir);\n } catch (_error) {\n // Ignore if directory has other files.\n }\n}\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` \u001b[32m✓\u001b[0m ${name}`);\n } catch (error) {\n errors.push({\n rule: 'openapi-reference-audit unit',\n message: `${name}: ${error.message}`,\n line: 1,\n file: 'tests/unit/openapi-reference-audit.test.js'\n });\n }\n}\n\nasync function runTests() {\n errors = [];\n warnings = [];\n\n console.log('🧪 OpenAPI Reference Audit Unit Tests');\n\n await runCase('Parses frontmatter METHOD /path references', async () => {\n const parsed = audit.parseMethodPathValue('post /stream');\n assert.ok(parsed);\n assert.strictEqual(parsed.key, 'POST /stream');\n });\n\n await runCase('Parses ', async () => {\n const parsed = audit.parseOpenApiTagReference(' ');\n assert.ok(parsed.valid);\n assert.strictEqual(parsed.endpoint.key, 'POST /task/{id}');\n });\n\n await runCase('Parses ', async () => {\n const parsed = audit.parseOpenApiTagReference(' ');\n assert.ok(parsed.valid);\n assert.strictEqual(parsed.endpoint.key, 'POST /task/{id}');\n });\n\n await runCase('Ignores non-endpoint frontmatter openapi values', async () => {\n assert.strictEqual(audit.isIgnoredFrontmatterOpenapiValue('3.0.3'), true);\n assert.strictEqual(audit.isIgnoredFrontmatterOpenapiValue('api/openapi.yaml'), true);\n assert.strictEqual(audit.isIgnoredFrontmatterOpenapiValue('GET /stream'), false);\n });\n\n await runCase('Resolves locale Studio API files to api/studio.yaml', async () => {\n const spec = audit.resolveSpecForFile('v2/es/solutions/livepeer-studio/api-reference/streams/create.mdx');\n assert.strictEqual(spec, 'api/studio.yaml');\n });\n\n await runCase('Flags unknown endpoint not found in resolved spec', async () => {\n const fixture = mkFixture(\n 'v2/es/solutions/livepeer-studio/api-reference/__openapi-audit-unit__',\n `unknown-${Date.now()}.mdx`,\n [\n '---',\n \"title: 'Unknown endpoint fixture'\",\n \"openapi: 'GET /definitely-not-real-endpoint'\",\n '---',\n '',\n '# Unknown',\n '',\n ' ',\n ''\n ].join('\\n')\n );\n\n try {\n const result = await audit.runAudit({\n argv: [\n '--files', fixture.relFile,\n '--strict',\n '--report', '/tmp/openapi-audit-unit-unknown.md',\n '--report-json', '/tmp/openapi-audit-unit-unknown.json'\n ]\n });\n\n assert.strictEqual(result.exitCode, 1);\n assert.ok(\n result.findings.some((finding) => finding.type === audit.FINDING_TYPES.ENDPOINT_NOT_FOUND)\n );\n } finally {\n cleanupFixture(fixture);\n }\n });\n\n await runCase('Flags frontmatter/component endpoint mismatch within same file', async () => {\n const fixture = mkFixture(\n 'v2/solutions/livepeer-studio/api-reference/__openapi-audit-unit__',\n `mismatch-${Date.now()}.mdx`,\n [\n '---',\n \"title: 'Mismatch fixture'\",\n \"openapi: 'GET /stream/{id}'\",\n '---',\n '',\n '# Mismatch',\n '',\n ' ',\n ''\n ].join('\\n')\n );\n\n try {\n const result = await audit.runAudit({\n argv: [\n '--files', fixture.relFile,\n '--strict',\n '--report', '/tmp/openapi-audit-unit-mismatch.md',\n '--report-json', '/tmp/openapi-audit-unit-mismatch.json'\n ]\n });\n\n assert.strictEqual(result.exitCode, 1);\n assert.ok(\n result.findings.some((finding) => finding.type === audit.FINDING_TYPES.INTRA_FILE_MISMATCH)\n );\n } finally {\n cleanupFixture(fixture);\n }\n });\n\n await runCase('Applies conservative autofix normalization with --fix --write', async () => {\n const fixture = mkFixture(\n 'v2/solutions/livepeer-studio/api-reference/__openapi-audit-unit__',\n `fix-${Date.now()}.mdx`,\n [\n '---',\n \"title: 'Fix fixture'\",\n \"openapi: 'post stream'\",\n '---',\n '',\n '# Fix',\n '',\n ' ',\n ' ',\n ''\n ].join('\\n')\n );\n\n try {\n const result = await audit.runAudit({\n argv: [\n '--files', fixture.relFile,\n '--fix',\n '--write',\n '--strict',\n '--report', '/tmp/openapi-audit-unit-fix.md',\n '--report-json', '/tmp/openapi-audit-unit-fix.json'\n ]\n });\n\n assert.strictEqual(result.exitCode, 0);\n const nextContent = fs.readFileSync(fixture.absFile, 'utf8');\n assert.ok(nextContent.includes(\"openapi: 'POST /stream'\"));\n assert.ok(nextContent.includes(' '));\n assert.ok(nextContent.includes(' '));\n } finally {\n cleanupFixture(fixture);\n }",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "mkdirSync"
+ },
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": ", ",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/openapi-rolling-issue.test.js",
+ "script": "openapi-rolling-issue.test",
+ "category": "validator",
+ "purpose": "tooling:api-spec",
+ "scope": "tests/unit, tests/utils, .github/workflows/openapi-reference-validation.yml",
+ "owner": "docs",
+ "needs": "F-R17",
+ "purpose_statement": "Tests OpenAPI rolling issue tracker — validates issue creation and dedup logic",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/openapi-rolling-issue.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script openapi-rolling-issue.test\n * @category validator\n * @purpose tooling:api-spec\n * @scope tests/unit, tests/utils, .github/workflows/openapi-reference-validation.yml\n * @owner docs\n * @needs F-R17\n * @purpose-statement Tests OpenAPI rolling issue tracker — validates issue creation and dedup logic\n * @pipeline manual — not yet in pipeline\n * @usage node tests/unit/openapi-rolling-issue.test.js [flags]\n */\n\nconst assert = require('assert');\nconst helpers = require('../utils/openapi-rolling-issue');\n\nlet errors = [];\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` \\u001b[32m✓\\u001b[0m ${name}`);\n } catch (error) {\n errors.push(`${name}: ${error.message}`);\n }\n}\n\nasync function runTests() {\n errors = [];\n\n console.log('🧪 OpenAPI Rolling Issue Unit Tests');\n\n await runCase('findMarkerIssue dedupes by marker and prefers open issue', async () => {\n const items = [\n { number: 101, state: 'closed', body: 'x x' },\n { number: 102, state: 'open', body: 'x x' },\n { number: 103, state: 'open', body: 'unrelated' }\n ];\n\n const result = helpers.findMarkerIssue(items);\n assert.ok(result);\n assert.strictEqual(result.number, 102);\n });\n\n await runCase('getIssueAction returns create vs update for non-zero failures', async () => {\n assert.strictEqual(\n helpers.getIssueAction({ existingIssue: null, totalFailures: 2 }),\n 'create'\n );\n\n assert.strictEqual(\n helpers.getIssueAction({ existingIssue: { number: 10, state: 'open' }, totalFailures: 2 }),\n 'update'\n );\n });\n\n await runCase('getIssueAction returns close when failures become zero and issue is open', async () => {\n assert.strictEqual(\n helpers.getIssueAction({ existingIssue: { number: 22, state: 'open' }, totalFailures: 0 }),\n 'close'\n );\n\n assert.strictEqual(\n helpers.getIssueAction({ existingIssue: null, totalFailures: 0 }),\n 'noop'\n );\n });\n\n await runCase('buildIssueBody includes required headings', async () => {\n const body = helpers.buildIssueBody({\n runUrl: 'https://example.test/run/1',\n topFindings: '- endpoint-not-found-in-spec: v2/foo.mdx:3 (GET /foo)',\n totalFailures: 1,\n totalFiles: 10,\n totalReferences: 20\n });\n\n const headings = [\n '### Area',\n '### Failing command or workflow',\n '### Script or workflow path',\n '### Full error output',\n '### Reproduction conditions',\n '### Expected behavior',\n '### Action requested from maintainers',\n '### Classification',\n '### Priority'\n ];\n\n headings.forEach((heading) => assert.ok(body.includes(heading), `missing ${heading}`));\n assert.ok(body.includes(helpers.ROLLING_ISSUE_MARKER));\n });\n\n await runCase('buildTopFindings enforces deterministic ordering and limit', async () => {\n const findings = [\n { type: 't', file: 'b.mdx', line: 9, reference: 'GET /b', resolvedSpec: 'api/studio.yaml' },\n { type: 't', file: 'a.mdx', line: 10, reference: 'GET /a/2', resolvedSpec: 'api/studio.yaml' },\n { type: 't', file: 'a.mdx', line: 2, reference: 'GET /a/1', resolvedSpec: 'api/studio.yaml' },\n { type: 't', file: 'c.mdx', line: 1, reference: 'GET /c', resolvedSpec: 'api/studio.yaml' }\n ];\n\n const text = helpers.buildTopFindings(findings, 3);\n const lines = text.split('\\n');\n assert.strictEqual(lines.length, 3);\n assert.ok(lines[0].includes('a.mdx:2'));\n assert.ok(lines[1].includes('a.mdx:10'));\n assert.ok(lines[2].includes('b.mdx:9'));\n });\n\n return {\n errors,\n passed: errors.length === 0,\n total: 5\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ OpenAPI rolling issue unit tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} OpenAPI rolling issue unit test failure(s)`);\n result.errors.forEach((msg) => console.error(` - ${msg}`));\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ OpenAPI rolling issue unit tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:openapi:issue"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:openapi:issue)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/quality.test.js",
+ "script": "quality.test",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Content quality checks — validates frontmatter completeness, thin content detection, placeholder flagging",
+ "pipeline_declared": "P1, P3",
+ "usage": "node tests/unit/quality.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script quality.test\n * @category validator\n * @purpose qa:content-quality\n * @scope tests\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Content quality checks — validates frontmatter completeness, thin content detection, placeholder flagging\n * @pipeline P1, P3\n * @usage node tests/unit/quality.test.js [flags]\n */\n/**\n * Quality checks: alt text, links, frontmatter, SEO\n */\n\nconst path = require('path');\nconst { getMdxFiles, getStagedDocsPageFiles, readFile } = require('../utils/file-walker');\nconst { extractFrontmatter } = require('../utils/mdx-parser');\n\nconst ENFORCE_OG_IMAGE = process.env.ENFORCE_OG_IMAGE === '1';\nconst VALID_PAGE_TYPES = ['quickstart', 'tutorial', 'reference', 'conceptual', 'portal', 'api', 'guide', 'overview', 'index'];\nconst VALID_AUDIENCES = ['developer', 'orchestrator', 'gateway', 'delegator', 'community', 'all'];\nconst VALID_STATUSES = ['draft', 'published', 'review', 'deprecated'];\n\nlet errors = [];\nlet warnings = [];\n\nfunction report(severity, file, message, rule = 'Frontmatter') {\n const issue = { file, rule, message };\n if (severity === 'error') {\n errors.push(issue);\n return;\n }\n warnings.push(issue);\n}\n\nfunction collectFilesFromArgs(args) {\n const files = [];\n\n for (let i = 0; i < args.length; i += 1) {\n const token = args[i];\n if (token === '--files' || token === '--file') {\n const raw = String(args[i + 1] || '').trim();\n if (raw) {\n raw\n .split(',')\n .map((part) => part.trim())\n .filter(Boolean)\n .forEach((part) => {\n files.push(path.isAbsolute(part) ? part : path.resolve(part));\n });\n }\n i += 1;\n }\n }\n\n return [...new Set(files)];\n}\n\n/**\n * Check for image alt text\n */\nfunction checkImageAltText(files) {\n files.forEach(file => {\n const content = readFile(file);\n if (!content) return;\n \n // Check for img tags without alt\n const imgRegex = /
]*>/gi;\n const matches = content.match(imgRegex) || [];\n \n matches.forEach(match => {\n if (!match.includes('alt=') && !match.includes(\"alt='\") && !match.includes('alt=\"')) {\n errors.push({\n file,\n rule: 'Image alt text',\n message: 'Image tag missing alt attribute',\n tag: match\n });\n }\n });\n \n // Check for Image component usage (should have alt prop)\n const imageComponentRegex = /]*>/gi;\n const imageMatches = content.match(imageComponentRegex) || [];\n \n imageMatches.forEach(match => {\n if (!match.includes('alt=') && !match.includes(\"alt='\") && !match.includes('alt=\"')) {\n warnings.push({\n file,\n rule: 'Image component alt text',\n message: 'Image component should have alt prop',\n tag: match\n });\n }\n });\n });\n}\n\n/**\n * Check frontmatter completeness\n */\nfunction checkFrontmatter(files) {\n files.forEach(file => {\n const content = readFile(file);\n if (!content) return;\n \n const frontmatter = extractFrontmatter(content);\n \n if (!frontmatter.exists) {\n report('warning', file, 'Missing frontmatter (recommended: title, description)');\n return;\n }\n \n if (!frontmatter.data) {\n report('error', file, `Invalid frontmatter: ${frontmatter.error || 'parse error'}`);\n return;\n }\n\n const data = frontmatter.data;\n\n if (!data.title) {\n report('warning', file, 'Missing title in frontmatter');\n }\n\n if (!data.description) {\n report('warning', file, 'Missing description in frontmatter (important for SEO)');\n }\n\n if (ENFORCE_OG_IMAGE && !data['og:image'] && !data.ogImage) {\n report('error', file, 'Missing og:image in frontmatter');\n }\n\n if (!data.pageType) {\n report('advisory', file, 'Missing pageType field (recommended for audit framework)');\n } else if (!VALID_PAGE_TYPES.includes(data.pageType)) {\n report('advisory', file, `Invalid pageType: \"${data.pageType}\". Valid: ${VALID_PAGE_TYPES.join(', ')}`);\n }\n\n if (!data.audience) {\n report('advisory', file, 'Missing audience field (recommended for audit framework)');\n } else if (!VALID_AUDIENCES.includes(data.audience)) {\n report('advisory', file, `Invalid audience: \"${data.audience}\". Valid: ${VALID_AUDIENCES.join(', ')}`);\n }\n\n if (!data.status) {\n report('advisory', file, 'Missing status field (recommended for audit framework)');\n } else if (!VALID_STATUSES.includes(data.status)) {\n report('advisory', file, `Invalid status: \"${data.status}\". Valid: ${VALID_STATUSES.join(', ')}`);\n }\n\n if (!data.lastVerified) {\n report('advisory', file, 'Missing lastVerified field (recommended for audit framework)');\n } else if (Number.isNaN(Date.parse(data.lastVerified))) {\n report('advisory', file, `Invalid lastVerified date: \"${data.lastVerified}\"`);\n }\n\n if (data.title && String(data.title).length > 60) {\n report(\n 'warning',\n file,\n `Title may be truncated in search: ${String(data.title).length} chars (recommended max 60)`\n );\n }\n });\n}\n\n/**\n * Check for broken internal links (basic)\n */\nfunction checkInternalLinks(files) {\n files.forEach(file => {\n const content = readFile(file);\n if (!content) return;\n \n // Check for markdown links\n const linkRegex = /\\[([^\\]]+)\\]\\(([^)]+)\\)/g;\n let match;\n \n while ((match = linkRegex.exec(content)) !== null) {\n const linkPath = match[2];\n \n // Skip external links\n if (linkPath.startsWith('http') || linkPath.startsWith('mailto:')) {\n continue;\n }\n \n // Check for common issues\n if (linkPath.includes('..')) {\n warnings.push({\n file,\n rule: 'Link validation',\n message: `Relative link path: ${linkPath} (consider using absolute path)`,\n link: linkPath\n });\n }\n }\n });\n}\n\n/**\n * Run all quality tests\n */\nfunction runTests(options = {}) {\n errors = [];\n warnings = [];\n \n const { files = null, stagedOnly = false } = options;\n \n let testFiles = files;\n if (!testFiles) {\n if (stagedOnly) {\n testFiles = getStagedDocsPageFiles().filter(f => f.endsWith('.mdx'));\n } else {\n testFiles = getMdxFiles();\n }\n }\n \n checkImageAltText(testFiles);",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "runner",
+ "caller": "tests/run-all.js",
+ "pipeline": "P1"
+ },
+ {
+ "type": "runner",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "P3"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-all.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:quality"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "test:quality"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 via run-all; P3 via run-pr-checks; indirect via tests/run-all.js; indirect via tests/run-pr-checks.js; manual (npm script: test:quality); manual (npm script: test:quality)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/unit/repair-spelling.test.js",
+ "script": "repair-spelling.test",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests/unit, tools/scripts/remediators/content",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Unit tests for repair-spelling.js — validates deterministic spelling fixes and exclusion ranges",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/repair-spelling.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script repair-spelling.test\n * @category validator\n * @purpose qa:content-quality\n * @scope tests/unit, tools/scripts/remediators/content\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Unit tests for repair-spelling.js — validates deterministic spelling fixes and exclusion ranges\n * @pipeline manual — not yet in pipeline\n * @dualmode --dry-run (validator) | --write (remediator)\n * @usage node tests/unit/repair-spelling.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\n\nconst repairSpelling = require('../../tools/scripts/remediators/content/repair-spelling');\n\nlet errors = [];\nlet warnings = [];\n\nfunction rangeContains(ranges, start, end) {\n return ranges.some((range) => start >= range.start && end <= range.end);\n}\n\nfunction createFixture(content) {\n const dirPath = fs.mkdtempSync(path.join(os.tmpdir(), 'repair-spelling-test-'));\n const filePath = path.join(dirPath, 'fixture.mdx');\n fs.writeFileSync(filePath, content, 'utf8');\n return { dirPath, filePath };\n}\n\nfunction cleanupFixture(fixture) {\n if (!fixture) return;\n fs.rmSync(fixture.dirPath, { recursive: true, force: true });\n}\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n rule: 'repair-spelling unit',\n message: `${name}: ${error.message}`,\n line: 1,\n file: 'tests/unit/repair-spelling.test.js'\n });\n }\n}\n\nasync function runTests() {\n errors = [];\n warnings = [];\n\n console.log('🧪 Repair Spelling Unit Tests');\n\n await runCase('Parses default args as dry-run full-scope mode', async () => {\n const parsed = repairSpelling.parseArgs([]);\n assert.strictEqual(parsed.mode, 'dry-run');\n assert.strictEqual(parsed.stagedOnly, false);\n assert.deepStrictEqual(parsed.files, []);\n });\n\n await runCase('Collects eligible prose ranges while excluding frontmatter, code, and URLs', async () => {\n const content = [\n '---',\n 'title: Recieve',\n '---',\n '',\n 'This recieve sentence is prose.',\n '',\n '`recieve`',\n '',\n 'https://example.com/recieve',\n '',\n '```js',\n 'const recieve = true;',\n '```'\n ].join('\\n');\n\n const ranges = await repairSpelling.collectEligibleRanges(content);\n const proseStart = content.indexOf('recieve sentence');\n const inlineCodeStart = content.indexOf('`recieve`') + 1;\n const urlStart = content.indexOf('https://example.com/recieve');\n const frontmatterStart = content.indexOf('Recieve');\n const codeStart = content.indexOf('recieve = true');\n\n assert.strictEqual(rangeContains(ranges, proseStart, proseStart + 'recieve'.length), true);\n assert.strictEqual(rangeContains(ranges, inlineCodeStart, inlineCodeStart + 'recieve'.length), false);\n assert.strictEqual(rangeContains(ranges, urlStart, urlStart + 'https'.length), false);\n assert.strictEqual(rangeContains(ranges, frontmatterStart, frontmatterStart + 'Recieve'.length), false);\n assert.strictEqual(rangeContains(ranges, codeStart, codeStart + 'recieve'.length), false);\n });\n\n await runCase('Uses preferred issue suggestions for plain typos', async () => {\n const resolved = repairSpelling.resolveSafeReplacement({\n issue: {\n text: 'recieve',\n suggestionsEx: [\n { word: 'receive', isPreferred: true }\n ]\n }\n });\n\n assert.strictEqual(resolved.replacement, 'receive');\n assert.strictEqual(resolved.source, 'preferred-issue-suggestion');\n });\n\n await runCase('Prefers shared-dictionary candidate for flagged en-GB terms', async () => {\n const resolved = repairSpelling.resolveSafeReplacement({\n issue: {\n text: 'color',\n isFlagged: true\n },\n suggestions: {\n suggestions: [\n { word: 'colon', cost: 100, forbidden: false, noSuggest: false },\n { word: 'colour', cost: 100, forbidden: false, noSuggest: false },\n { word: 'conor', cost: 100, forbidden: false, noSuggest: false }\n ]\n },\n customWords: new Set(['colour'])\n });\n\n assert.strictEqual(resolved.replacement, 'colour');\n assert.strictEqual(resolved.source, 'custom-dictionary-suggestion');\n });\n\n await runCase('Leaves ambiguous suggestions unchanged', async () => {\n const resolved = repairSpelling.resolveSafeReplacement({\n issue: {\n text: 'thingg',\n isFlagged: false\n },\n suggestions: {\n suggestions: [\n { word: 'things', cost: 100, forbidden: false, noSuggest: false },\n { word: 'thins', cost: 100, forbidden: false, noSuggest: false }\n ]\n },\n customWords: new Set()\n });\n\n assert.strictEqual(resolved.replacement, null);\n assert.strictEqual(resolved.reason, 'ambiguous-suggestions');\n });\n\n await runCase('Skips replacements that drop possessive suffixes', async () => {\n const resolved = repairSpelling.resolveSafeReplacement({\n issue: {\n text: \"Arbitrium's\",\n isFlagged: true\n },\n suggestions: {\n suggestions: [\n { word: 'Arbitrum', cost: 100, forbidden: false, noSuggest: false }\n ]\n },\n customWords: new Set(['arbitrum'])\n });\n\n assert.strictEqual(resolved.replacement, null);\n assert.strictEqual(resolved.reason, 'inflected-form-mismatch');\n });\n\n await runCase('Does not use custom-dictionary fallback for unflagged words', async () => {\n const resolved = repairSpelling.resolveSafeReplacement({\n issue: {\n text: 'Bithumb',\n isFlagged: false\n },\n suggestions: {\n suggestions: [\n { word: 'Github', cost: 100, forbidden: false, noSuggest: false }\n ]\n },\n customWords: new Set(['github'])\n });\n\n assert.strictEqual(resolved.replacement, null);\n assert.strictEqual(resolved.reason, 'ambiguous-suggestions');\n });\n\n await runCase('Dry-run reports repairs without mutating the file', async () => {\n const fixture = createFixture(\n [\n '---',\n 'title: Recieve',\n '---',\n '',\n 'This recieve paragraph uses color.',\n '',\n '`recieve color`',\n '',\n 'https://example.com/recieve/color',\n '',\n '```js',\n \"const recieve = 'color';\",\n '```'\n ].join('\\n')\n );\n\n try {\n const before = fs.readFileSync(fixture.filePath, 'utf8');\n const summary = await repairSpelling.run({\n args: repairSpelling.parseArgs(['--dry-run']),\n files: [fixture.filePath],\n quiet: true\n });\n\n assert.strictEqual(summary.proposedRepairs, 2);\n assert.strictEqual(fs.readFileSync(fixture.filePath, 'utf8'), before);\n } finally {\n cleanupFixture(fixture);\n }\n });",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "manual",
+ "caller": "none",
+ "pipeline": "manual"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "tests/unit/fixture.mdx",
+ "type": "generated-output",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "tests/unit/fixture.mdx",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (none)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/repo-audit-pipeline.test.js",
+ "script": "repo-audit-pipeline.test",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": "tests/unit, tools/scripts, ai-tools/ai-skills/catalog, ai-tools/agent-packs",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Tests repo-audit-orchestrator.js pipeline — validates mode/scope combinations and report output",
+ "pipeline_declared": "manual — not yet in pipeline",
+ "usage": "node tests/unit/repo-audit-pipeline.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script repo-audit-pipeline.test\n * @category validator\n * @purpose qa:repo-health\n * @scope tests/unit, tools/scripts, ai-tools/ai-skills/catalog, ai-tools/agent-packs\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Tests repo-audit-orchestrator.js pipeline — validates mode/scope combinations and report output\n * @pipeline manual — not yet in pipeline\n * @dualmode dual-mode (document flags)\n * @usage node tests/unit/repo-audit-pipeline.test.js [flags]\n */\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst path = require('path');\nconst { spawnSync } = require('child_process');\n\nconst REPO_ROOT = process.cwd();\n\nlet errors = [];\nlet warnings = [];\n\nfunction toPosix(value) {\n return String(value || '').split(path.sep).join('/');\n}\n\nfunction readJson(repoPath) {\n return JSON.parse(fs.readFileSync(path.join(REPO_ROOT, repoPath), 'utf8'));\n}\n\nfunction runNode(args) {\n return spawnSync('node', args, {\n cwd: REPO_ROOT,\n encoding: 'utf8'\n });\n}\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(` ✓ ${name}`);\n } catch (error) {\n errors.push({\n rule: 'repo-audit-pipeline unit',\n message: `${name}: ${error.message}`,\n line: 1,\n file: 'tests/unit/repo-audit-pipeline.test.js'\n });\n }\n}\n\nasync function runTests() {\n errors = [];\n warnings = [];\n\n console.log('🧪 Repo Audit Pipeline Unit Tests');\n\n const tmpRoot = path.join(REPO_ROOT, 'tests/reports/repo-audit-pipeline-unit', String(Date.now()));\n const tmpAuditOutputRel = toPosix(path.relative(REPO_ROOT, path.join(tmpRoot, 'audit')));\n const tmpPacksOutputRel = toPosix(path.relative(REPO_ROOT, path.join(tmpRoot, 'packs')));\n\n fs.mkdirSync(path.join(tmpRoot, 'audit'), { recursive: true });\n fs.mkdirSync(path.join(tmpRoot, 'packs'), { recursive: true });\n\n await runCase('Skill catalogue includes required audit stages', async () => {\n const catalog = readJson('ai-tools/ai-skills/catalog/skill-catalog.json');\n const ids = new Set((catalog.skills || []).map((skill) => skill.id));\n\n [\n 'repo-audit-orchestrator',\n 'script-footprint-and-usage-audit',\n 'docs-quality-and-freshness-audit',\n 'style-and-language-homogenizer-en-gb',\n 'component-layout-governance',\n 'cleanup-quarantine-manager',\n 'cross-agent-packager'\n ].forEach((id) => assert.ok(ids.has(id), `Missing skill id: ${id}`));\n });\n\n await runCase('Execution manifest stage IDs map to skill catalogue IDs', async () => {\n const catalog = readJson('ai-tools/ai-skills/catalog/skill-catalog.json');\n const manifest = readJson('ai-tools/ai-skills/catalog/execution-manifest.json');\n const ids = new Set((catalog.skills || []).map((skill) => skill.id));\n\n (manifest.pipeline || []).forEach((entry) => {\n assert.ok(ids.has(entry.id), `Manifest stage missing from catalogue: ${entry.id}`);\n });\n });\n\n await runCase('Orchestrator static dry-run writes unified scorecard output', async () => {\n const result = runNode([\n 'tools/scripts/repo-audit-orchestrator.js',\n '--mode',\n 'static',\n '--scope',\n 'changed',\n '--output-dir',\n tmpAuditOutputRel\n ]);\n\n if (result.stdout) process.stdout.write(result.stdout);\n if (result.stderr) process.stderr.write(result.stderr);\n\n assert.strictEqual(result.status, 0, 'Orchestrator exited non-zero in static dry-run mode.');\n\n const summaryJson = path.join(tmpRoot, 'audit', 'repo-audit-summary.json');\n const summaryMd = path.join(tmpRoot, 'audit', 'repo-audit-summary.md');\n\n assert.ok(fs.existsSync(summaryJson), 'Missing repo-audit-summary.json output.');\n assert.ok(fs.existsSync(summaryMd), 'Missing repo-audit-summary.md output.');\n\n const payload = JSON.parse(fs.readFileSync(summaryJson, 'utf8'));\n assert.strictEqual(payload.mode, 'static');\n assert.strictEqual(payload.scope, 'changed');\n assert.ok(typeof payload.score?.score === 'number', 'Missing scorecard numeric score.');\n assert.ok(Array.isArray(payload.stages) && payload.stages.length >= 1, 'Missing stage results.');\n });\n\n await runCase('Cleanup manager classify mode writes non-mutating manifest', async () => {\n const result = runNode([\n 'tools/scripts/cleanup-quarantine-manager.js',\n '--output-dir',\n tmpAuditOutputRel\n ]);\n\n if (result.stdout) process.stdout.write(result.stdout);\n if (result.stderr) process.stderr.write(result.stderr);\n\n assert.strictEqual(result.status, 0, 'Cleanup classify exited non-zero.');\n\n const manifestJson = path.join(tmpRoot, 'audit', 'cleanup-quarantine-manifest.json');\n assert.ok(fs.existsSync(manifestJson), 'Missing cleanup quarantine manifest output.');\n\n const manifest = JSON.parse(fs.readFileSync(manifestJson, 'utf8'));\n assert.strictEqual(manifest.mode, 'classify');\n assert.ok(Array.isArray(manifest.entries), 'Manifest entries must be an array.');\n });\n\n await runCase('Cross-agent packager emits all pack targets from one catalogue', async () => {\n const result = runNode([\n 'tools/scripts/cross-agent-packager.js',\n '--agent-pack',\n 'all',\n '--output-dir',\n tmpPacksOutputRel\n ]);\n\n if (result.stdout) process.stdout.write(result.stdout);\n if (result.stderr) process.stderr.write(result.stderr);\n\n assert.strictEqual(result.status, 0, 'Cross-agent packager exited non-zero.');\n\n const expected = [\n path.join(tmpRoot, 'packs', 'codex', 'skills-manifest.json'),\n path.join(tmpRoot, 'packs', 'cursor', 'rules.md'),\n path.join(tmpRoot, 'packs', 'claude', 'CLAUDE.md'),\n path.join(tmpRoot, 'packs', 'windsurf', 'rules.md'),\n path.join(tmpRoot, 'packs', 'README.md')\n ];\n\n expected.forEach((outputPath) => {\n assert.ok(fs.existsSync(outputPath), `Missing output: ${toPosix(path.relative(REPO_ROOT, outputPath))}`);\n });\n });\n\n return {\n errors,\n warnings,\n passed: errors.length === 0,\n total: 5\n };\n}\n\nif (require.main === module) {\n runTests()\n .then((result) => {\n if (result.passed) {\n console.log(`\\n✅ Repo audit pipeline unit tests passed (${result.total} cases)`);\n process.exit(0);\n }\n console.error(`\\n❌ ${result.errors.length} repo audit pipeline unit test failure(s)`);\n result.errors.forEach((err) => console.error(` - ${err.message}`));\n process.exit(1);\n })\n .catch((error) => {\n console.error(`\\n❌ Repo audit pipeline unit tests crashed: ${error.message}`);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:repo-audit"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "manual (npm script: test:repo-audit)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "Manual"
+ },
+ {
+ "path": "tests/unit/script-docs.test.js",
+ "script": "script-docs-test",
+ "category": "validator",
+ "purpose": "qa:repo-health",
+ "scope": ".githooks, .github/scripts, tests, tools/scripts, tasks/scripts, docs-guide/indexes/scripts-index.mdx",
+ "owner": "docs",
+ "needs": "E-C1, R-R14",
+ "purpose_statement": "Enforces script header schema, keeps group script indexes in sync, and builds aggregate script index",
+ "pipeline_declared": "P1, P3",
+ "usage": "node tests/unit/script-docs.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script script-docs-test\n * @category validator\n * @purpose qa:repo-health\n * @scope .githooks, .github/scripts, tests, tools/scripts, tasks/scripts, docs-guide/indexes/scripts-index.mdx\n * @owner docs\n * @needs E-C1, R-R14\n * @purpose-statement Enforces script header schema, keeps group script indexes in sync, and builds aggregate script index\n * @pipeline P1, P3\n * @dualmode --check (validator) | --write --rebuild-indexes (generator)\n * @usage node tests/unit/script-docs.test.js [flags]\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst {\n buildGeneratedFrontmatterLines,\n buildGeneratedHiddenBannerLines,\n buildGeneratedNoteLines\n} = require('../../tools/lib/generated-file-banners');\nconst {\n AGGREGATE_INDEX_PATH,\n CLASSIFICATION_DATA_PATH,\n GOVERNED_ROOTS,\n GROUP_INDEX_MAP,\n INDEXED_ROOTS,\n LEGACY_AGGREGATE_INDEX_PATH,\n SCRIPT_EXTENSIONS: GOVERNED_SCRIPT_EXTENSIONS,\n isWithinRoots,\n normalizeRepoPath,\n shouldExcludeScriptPath\n} = require('../../tools/lib/script-governance-config');\n\nconst REPO_ROOT = path.resolve(__dirname, '../..');\nconst INDEX_START = '{/* SCRIPT-INDEX:START */}';\nconst INDEX_END = '{/* SCRIPT-INDEX:END */}';\n\nconst LEGACY_REQUIRED_TAGS = [\n '@script',\n '@summary',\n '@owner',\n '@scope',\n '@usage',\n '@inputs',\n '@outputs',\n '@exit-codes',\n '@examples',\n '@notes'\n];\n\nconst LEGACY_INLINE_REQUIRED_TAGS = ['@script', '@summary', '@owner', '@scope'];\nconst LEGACY_BLOCK_REQUIRED_TAGS = ['@usage', '@inputs', '@outputs', '@exit-codes', '@examples', '@notes'];\nconst FRAMEWORK_REQUIRED_TAGS = [\n '@script',\n '@category',\n '@purpose',\n '@scope',\n '@owner',\n '@needs',\n '@purpose-statement',\n '@pipeline',\n '@usage'\n];\nconst FRAMEWORK_INLINE_REQUIRED_TAGS = FRAMEWORK_REQUIRED_TAGS;\nconst PLACEHOLDER_PATTERNS = [\n /^<.*>$/,\n /^todo\\b/i,\n /^tbd\\b/i,\n /^fill\\b/i,\n /^replace$/i,\n /^replace me$/i,\n /^n\\/a$/i,\n /^none$/i,\n /^placeholder$/i\n];\n\nconst SCRIPT_EXTENSIONS = new Set(GOVERNED_SCRIPT_EXTENSIONS);\nconst VALIDATION_ROOTS = GOVERNED_ROOTS;\nconst CLASSIFICATION_ROOTS = GOVERNED_ROOTS;\nconst AGGREGATE_FRONTMATTER_LINES = buildGeneratedFrontmatterLines({\n title: 'Scripts Index',\n sidebarTitle: 'Scripts Index',\n description: 'This page provides an aggregate catalog inventory of repository scripts generated from group script indexes.',\n keywords: ['livepeer', 'scripts index', 'aggregate inventory', 'repository', 'scripts']\n});\nconst AGGREGATE_DETAILS = {\n script: 'tests/unit/script-docs.test.js',\n purpose: 'Enforce script header schema, keep group script indexes in sync, and build aggregate script index.',\n runWhen: 'Script metadata changes in validation roots or script changes in indexed roots.',\n runCommand: 'node tests/unit/script-docs.test.js --write --rebuild-indexes'\n};\n\nfunction readFileSafe(repoPath) {\n try {\n return fs.readFileSync(path.join(REPO_ROOT, repoPath), 'utf8');\n } catch (_err) {\n return '';\n }\n}\n\nfunction escapeRegExp(value) {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nfunction shouldExclude(repoPath) {\n const p = normalizeRepoPath(repoPath);\n return shouldExcludeScriptPath(p) || p.includes('/.venv/') || p.startsWith('.venv/') || p.includes('/tmp/') || p.startsWith('tmp/');\n}\n\nfunction isScriptFile(repoPath) {\n if (shouldExclude(repoPath)) return false;\n const ext = path.extname(repoPath).toLowerCase();\n if (SCRIPT_EXTENSIONS.has(ext)) return true;\n const content = readFileSafe(repoPath);\n return content.startsWith('#!/usr/bin/env') || content.startsWith('#!/bin/');\n}\n\nfunction walkFiles(dirPath, out = []) {\n const full = path.join(REPO_ROOT, dirPath);\n if (!fs.existsSync(full)) return out;\n\n const entries = fs.readdirSync(full, { withFileTypes: true });\n for (const entry of entries) {\n const rel = normalizeRepoPath(path.join(dirPath, entry.name));\n if (shouldExclude(rel)) continue;\n if (entry.isDirectory()) {\n walkFiles(rel, out);\n } else {\n out.push(rel);\n }\n }\n return out;\n}\n\nfunction getScriptsForRoots(roots) {\n const scripts = [];\n for (const root of roots) {\n walkFiles(root, scripts);\n }\n return [...new Set(scripts)].filter(isScriptFile).sort();\n}\n\nfunction getAllValidationScripts() {\n return getScriptsForRoots(VALIDATION_ROOTS);\n}\n\nfunction getAllIndexedScripts() {\n return getScriptsForRoots(INDEXED_ROOTS);\n}\n\nfunction getAllClassificationScripts() {\n return getScriptsForRoots(CLASSIFICATION_ROOTS);\n}\n\nfunction isManagedRootPath(repoPath) {\n return isWithinRoots(repoPath, CLASSIFICATION_ROOTS);\n}\n\nfunction fileExists(repoPath) {\n const fullPath = path.join(REPO_ROOT, repoPath);\n return fs.existsSync(fullPath) && fs.statSync(fullPath).isFile();\n}\n\nfunction loadClassificationData() {\n const fullPath = path.join(REPO_ROOT, CLASSIFICATION_DATA_PATH);\n const raw = fs.readFileSync(fullPath, 'utf8');\n const parsed = JSON.parse(raw);\n\n if (!Array.isArray(parsed)) {\n throw new Error('Classification data must be a JSON array.');\n }\n\n return parsed.map((row, index) => {\n if (!row || typeof row !== 'object' || Array.isArray(row)) {\n throw new Error(`classification[${index}] must be an object.`);\n }\n\n const repoPath = normalizeRepoPath(String(row.path || '').trim());\n if (!repoPath) {\n throw new Error(`classification[${index}].path is required.`);\n }\n\n return { ...row, path: repoPath };\n });\n}\n\nfunction validateClassificationCoverage(scopedScripts, classificationRows) {\n const errors = [];\n const liveScripts = [...new Set(scopedScripts.map(normalizeRepoPath))].sort();\n const managedRows = classificationRows\n .filter((row) => isManagedRootPath(row.path))\n .sort((a, b) => a.path.localeCompare(b.path));\n const classifiedPaths = new Set(managedRows.map((row) => row.path));\n\n for (const scriptPath of liveScripts) {\n if (!classifiedPaths.has(scriptPath)) {\n errors.push({\n file: CLASSIFICATION_DATA_PATH,\n rule: 'Script classification coverage',\n message: `Missing classification row for managed script: ${scriptPath}`,\n line: 1\n });\n }\n }\n\n for (const row of managedRows) {\n if (!fileExists(row.path)) {\n errors.push({\n file: CLASSIFICATION_DATA_PATH,\n rule: 'Script classification coverage',\n message: `Classification row points to a missing file: ${row.path}`,\n line: 1\n });\n }\n }\n\n for (const row of classificationRows) {\n if (row.path === 'tools/scripts/archive' || row.path.startsWith('tools/scripts/archive/')) {",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "pre-commit",
+ "caller": ".githooks/pre-commit",
+ "pipeline": "P1"
+ },
+ {
+ "type": "runner",
+ "caller": "tests/run-all.js",
+ "pipeline": "P1"
+ },
+ {
+ "type": "runner",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "P3"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-all.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:scripts"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "",
+ "type": "stdout",
+ "call": "writeFileSync"
+ }
+ ],
+ "outputs_display": "",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 (pre-commit); P1 via run-all; P3 via run-pr-checks; indirect via tests/run-all.js; indirect via tests/run-pr-checks.js; manual (npm script: test:scripts)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/unit/spelling.test.js",
+ "script": "spelling.test",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Spelling check — validates content against custom dictionary with en-GB rules",
+ "pipeline_declared": "P1, P3",
+ "usage": "node tests/unit/spelling.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script spelling.test\n * @category validator\n * @purpose qa:content-quality\n * @scope tests\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Spelling check — validates content against custom dictionary with en-GB rules\n * @pipeline P1, P3\n * @usage node tests/unit/spelling.test.js [flags]\n */\n/**\n * UK English spelling validation tests\n */\n\nconst { execSync } = require('child_process');\nconst { getMdxFiles, getStagedDocsPageFiles } = require('../utils/file-walker');\nconst { checkMultipleFiles, resolveCspellBinary, resolveCspellConfig } = require('../utils/spell-checker');\n\nlet errors = [];\n\n/**\n * Extract text content from MDX (excluding code blocks and frontmatter)\n */\nfunction extractTextContent(content) {\n // Remove frontmatter\n let text = content.replace(/^---\\s*\\n[\\s\\S]*?\\n---\\s*\\n/, '');\n \n // Remove code blocks\n text = text.replace(/```[\\s\\S]*?```/g, '');\n \n // Remove inline code\n text = text.replace(/`[^`]+`/g, '');\n \n // Remove JSX tags (keep text content)\n text = text.replace(/<[^>]+>/g, ' ');\n \n return text;\n}\n\n/**\n * Run spelling tests\n */\nasync function runTests(options = {}) {\n errors = [];\n \n const { files = null, stagedOnly = false } = options;\n \n let testFiles = files;\n if (!testFiles) {\n if (stagedOnly) {\n testFiles = getStagedDocsPageFiles().filter(f => f.endsWith('.mdx'));\n } else {\n testFiles = getMdxFiles();\n }\n }\n \n // Check if cspell is available\n const cspell = resolveCspellBinary();\n try {\n if (cspell.viaNpx) {\n execSync('npx cspell --version', { stdio: 'ignore' });\n } else {\n execSync(`\"${cspell.bin}\" --version`, { stdio: 'ignore' });\n }\n } catch (error) {\n console.warn('⚠️ cspell not available. Install with: npm install');\n return { errors: [], warnings: ['cspell not available'], passed: true, total: testFiles.length };\n }\n \n const cspellConfig = resolveCspellConfig();\n const results = checkMultipleFiles(testFiles, cspellConfig);\n \n results.forEach(result => {\n if (result.errors.length > 0) {\n errors.push({\n file: result.file,\n errors: result.errors\n });\n }\n });\n \n return {\n errors,\n passed: errors.length === 0,\n total: testFiles.length\n };\n}\n\n// Run if called directly\nif (require.main === module) {\n const args = process.argv.slice(2);\n const stagedOnly = args.includes('--staged');\n \n runTests({ stagedOnly }).then(result => {\n if (result.errors.length > 0) {\n console.error('\\n❌ Spelling Errors:\\n');\n result.errors.forEach(fileError => {\n console.error(` ${fileError.file}:`);\n fileError.errors.forEach(err => {\n console.error(` Line ${err.line}:${err.column} - Unknown word: \"${err.word}\"`);\n });\n });\n }\n \n if (result.passed) {\n console.log(`\\n✅ Spelling checks passed (${result.total} files checked)`);\n process.exit(0);\n } else {\n const totalErrors = result.errors.reduce((sum, e) => sum + e.errors.length, 0);\n console.error(`\\n❌ ${totalErrors} spelling error(s) found`);\n process.exit(1);\n }\n }).catch(error => {\n console.error('Spelling test error:', error);\n process.exit(1);\n });\n}\n\nmodule.exports = { runTests };\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "runner",
+ "caller": "tests/run-all.js",
+ "pipeline": "P1"
+ },
+ {
+ "type": "runner",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "P3"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-all.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/unit/repair-spelling.test.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:spell"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "test:spell"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 via run-all; P3 via run-pr-checks; indirect via tests/run-all.js; indirect via tests/run-pr-checks.js; indirect via tests/unit/repair-spelling.test.js; manual (npm script: test:spell); manual (npm script: test:spell)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/unit/style-guide.test.js",
+ "script": "style-guide.test",
+ "category": "validator",
+ "purpose": "qa:content-quality",
+ "scope": "tests",
+ "owner": "docs",
+ "needs": "E-R1, R-R11",
+ "purpose_statement": "Style guide compliance — checks en-GB conventions, heading case, formatting rules",
+ "pipeline_declared": "P1, P3",
+ "usage": "node tests/unit/style-guide.test.js [flags]",
+ "header": "#!/usr/bin/env node\n/**\n * @script style-guide.test\n * @category validator\n * @purpose qa:content-quality\n * @scope tests\n * @owner docs\n * @needs E-R1, R-R11\n * @purpose-statement Style guide compliance — checks en-GB conventions, heading case, formatting rules\n * @pipeline P1, P3\n * @usage node tests/unit/style-guide.test.js [flags]\n */\n/**\n * Style guide rule validation tests\n */\n\nconst fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\nconst { getMdxFiles, getJsxFiles, getStagedDocsPageFiles, readFile } = require('../utils/file-walker');\n\nconst REPO_ROOT = process.cwd();\nlet errors = [];\nlet warnings = [];\nlet changedLineMap = null;\n\nfunction toPosix(filePath) {\n return String(filePath || '').split(path.sep).join('/');\n}\n\nfunction getChangedLineSetFromDiff(filePath, diffArgs) {\n const relPath = toPosix(path.relative(REPO_ROOT, filePath));\n if (!relPath) {\n return null;\n }\n\n try {\n const diff = execSync(`git ${diffArgs.join(' ')} -- \"${relPath}\"`, {\n encoding: 'utf8',\n cwd: REPO_ROOT\n });\n const changedLines = new Set();\n const hunkRegex = /^@@ -\\d+(?:,\\d+)? \\+(\\d+)(?:,(\\d+))? @@/;\n\n diff.split('\\n').forEach((line) => {\n const match = line.match(hunkRegex);\n if (!match) return;\n\n const start = Number(match[1]);\n const count = match[2] ? Number(match[2]) : 1;\n if (!Number.isFinite(start) || !Number.isFinite(count) || count <= 0) return;\n\n for (let i = 0; i < count; i++) {\n changedLines.add(start + i);\n }\n });\n\n return changedLines;\n } catch (_error) {\n return null;\n }\n}\n\nfunction getStagedChangedLineSet(filePath) {\n return getChangedLineSetFromDiff(filePath, ['diff', '--cached', '--unified=0']);\n}\n\nfunction getBaseRefChangedLineSet(filePath, baseRef) {\n if (!baseRef) {\n return null;\n }\n\n return getChangedLineSetFromDiff(filePath, ['diff', '--unified=0', `origin/${baseRef}...HEAD`]);\n}\n\nfunction shouldCheckLine(file, line, changedOnly) {\n if (!changedOnly || !changedLineMap) {\n return true;\n }\n\n const changedLines = changedLineMap.get(file);\n if (!changedLines || changedLines.size === 0) {\n return false;\n }\n\n return changedLines.has(line);\n}\n\n/**\n * Check for ThemeData usage (deprecated)\n */\nfunction checkThemeData(files, stagedOnly = false) {\n files.forEach(file => {\n if (file.includes('style-guide.mdx')) return; // Skip style guide itself\n \n const content = readFile(file);\n if (!content) return;\n\n const lines = content.split('\\n');\n lines.forEach((line, index) => {\n if (!line.includes('ThemeData') && !line.includes('themeStyles.jsx')) {\n return;\n }\n const lineNumber = index + 1;\n if (!shouldCheckLine(file, lineNumber, stagedOnly)) {\n return;\n }\n errors.push({\n file,\n rule: 'ThemeData usage',\n message: 'Uses deprecated ThemeData - use CSS Custom Properties instead',\n line: lineNumber\n });\n });\n });\n}\n\n/**\n * Check for hardcoded colors\n */\nfunction checkHardcodedColors(files, stagedOnly = false) {\n const livepeerColors = ['#3CB540', '#2b9a66', '#18794E', '#181C18', '#E0E4E0', '#717571', '#A0A4A0'];\n \n files.forEach(file => {\n if (file.includes('style-guide.mdx')) return;\n \n const content = readFile(file);\n if (!content) return;\n \n // Skip code blocks and markdown tables\n const lines = content.split('\\n');\n let inCodeBlock = false;\n let inTable = false;\n \n lines.forEach((line, index) => {\n if (line.trim().startsWith('```')) inCodeBlock = !inCodeBlock;\n if (line.includes('|') && line.includes('---')) inTable = true;\n if (line.trim() === '') inTable = false;\n \n if (!inCodeBlock && !inTable) {\n const lineNumber = index + 1;\n if (!shouldCheckLine(file, lineNumber, stagedOnly)) {\n return;\n }\n livepeerColors.forEach(color => {\n if (line.includes(color) && !line.includes('var(--') && !line.includes('CSS Custom Properties')) {\n errors.push({\n file,\n rule: 'Hardcoded colors',\n message: `Contains hardcoded theme color ${color} - use CSS Custom Properties (var(--accent), etc.)`,\n line: lineNumber\n });\n }\n });\n }\n });\n });\n}\n\n/**\n * Check for inline styles in MDX\n */\nfunction checkInlineStylesInMdx(files, stagedOnly = false) {\n files.filter(f => f.endsWith('.mdx')).forEach(file => {\n if (file.includes('style-guide.mdx') || file.includes('component-library')) return;\n \n const content = readFile(file);\n if (!content) return;\n \n // Check for style={{}} in MDX (should use components instead)\n const styleRegex = /style\\s*=\\s*\\{\\{/g;\n const lines = content.split('\\n');\n \n lines.forEach((line, index) => {\n const lineNumber = index + 1;\n if (!shouldCheckLine(file, lineNumber, stagedOnly)) {\n return;\n }\n if (styleRegex.test(line) && !line.includes('//') && !line.includes('{/*')) {\n errors.push({\n file,\n rule: 'No inline styles in MDX',\n message: 'Inline styles in MDX files - use component primitives instead',\n line: lineNumber\n });\n }\n });\n });\n}\n\n/**\n * Check for Tailwind classes\n */\nfunction checkTailwindClasses(files, stagedOnly = false) {\n files.filter(f => f.endsWith('.mdx')).forEach(file => {\n const content = readFile(file);\n if (!content) return;\n \n // Common Tailwind patterns\n const tailwindPatterns = [\n /\\b(flex|grid|gap-\\d+|items-center|justify-center|p-\\d+|m-\\d+|w-\\d+|h-\\d+)\\b/\n ];\n \n const lines = content.split('\\n');\n lines.forEach((line, index) => {\n const lineNumber = index + 1;\n if (!shouldCheckLine(file, lineNumber, stagedOnly)) {\n return;\n }\n if (line.includes('className=')) {\n tailwindPatterns.forEach(pattern => {\n if (pattern.test(line)) {\n warnings.push({\n file,\n rule: 'No Tailwind classes',\n message: 'Tailwind classes detected - use component primitives instead',\n line: lineNumber\n });\n }\n });",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": false,
+ "triggers": [
+ {
+ "type": "runner",
+ "caller": "tests/run-all.js",
+ "pipeline": "P1"
+ },
+ {
+ "type": "runner",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "P3"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-all.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "indirect"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tests/package.json",
+ "pipeline": "manual",
+ "script_name": "test:style"
+ },
+ {
+ "type": "npm-script",
+ "caller": "tools/package.json",
+ "pipeline": "manual",
+ "script_name": "test:style"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": true,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P1 via run-all; P3 via run-pr-checks; indirect via tests/run-all.js; indirect via tests/run-pr-checks.js; manual (npm script: test:style); manual (npm script: test:style)",
+ "grade": "C",
+ "flags": [
+ "invalid-scope"
+ ],
+ "trigger_group": "P1"
+ },
+ {
+ "path": "tests/unit/usefulness-journey.test.js",
+ "script": "usefulness-journey.test",
+ "category": "utility",
+ "purpose": "qa:content-quality",
+ "scope": "full-repo",
+ "owner": "docs",
+ "needs": "R-R14, R-C6",
+ "purpose_statement": "Tests journey-check evaluation logic against fixture pages.",
+ "pipeline_declared": "P3",
+ "usage": "node tests/unit/usefulness-journey.test.js",
+ "header": "#!/usr/bin/env node\n/**\n * @script usefulness-journey.test\n * @category utility\n * @purpose qa:content-quality\n * @scope full-repo\n * @owner docs\n * @needs R-R14, R-C6\n * @purpose-statement Tests journey-check evaluation logic against fixture pages.\n * @pipeline P3\n * @usage node tests/unit/usefulness-journey.test.js\n */\n\n'use strict';\n\nconst assert = require('assert');\nconst { checkJourneys } = require('../../tools/lib/docs-usefulness/journey-check');\nconst { validateUsefulnessConfig } = require('../../tools/lib/docs-usefulness/config-validator');\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(`✓ ${name}`);\n return true;\n } catch (error) {\n console.error(`✗ ${name}`);\n console.error(` ${error.stack || error.message || error}`);\n return false;\n }\n}\n\nasync function main() {\n const results = [];\n\n results.push(await runCase('Journey checker reports complete/weak/missing states with blockers', async () => {\n const pages = [\n {\n path: 'v2/developers/portal.mdx',\n purpose: 'landing',\n combined_score: 78,\n internalLinks: ['/v2/developers/livepeer-overview']\n },\n {\n path: 'v2/developers/livepeer-overview.mdx',\n purpose: 'overview',\n combined_score: 52,\n internalLinks: ['/v2/developers/quickstart']\n },\n {\n path: 'v2/developers/quickstart.mdx',\n purpose: 'tutorial',\n combined_score: 45,\n internalLinks: []\n }\n ];\n\n const journeys = {\n 'developer-ai': {\n label: 'Developer (AI Inference)',\n maps_to: 'P1',\n success_criteria: 'Reach first AI job',\n priority: 1,\n steps: [\n { position: 1, purpose: 'landing', path_patterns: ['v2/developers/portal*'] },\n { position: 2, purpose: 'overview', path_patterns: ['v2/developers/*overview*'] },\n { position: 3, purpose: 'tutorial', path_patterns: ['v2/developers/*quickstart*'] },\n { position: 4, purpose: 'reference', path_patterns: ['v2/developers/*reference*'] }\n ]\n }\n };\n\n const reports = checkJourneys(pages, journeys);\n assert.strictEqual(reports.length, 1);\n assert.strictEqual(reports[0].steps_complete, 2);\n assert.strictEqual(reports[0].steps_weak, 1);\n assert.strictEqual(reports[0].steps_missing, 1);\n assert.strictEqual(reports[0].verdict, 'BLOCKED');\n assert.ok(reports[0].blockers[0].includes('Step 4'));\n }));\n\n results.push(await runCase('Config validation blocks deprecated v2/platforms journey patterns', async () => {\n const rubric = require('../../tools/config/usefulness-rubric.json');\n const audience = require('../../tools/config/usefulness-audience-normalization.json');\n const llm = require('../../tools/config/usefulness-llm-tiers.json');\n const prompts = require('../../tools/lib/docs-usefulness/prompts');\n\n const badJourneys = {\n 'platform-builder': {\n label: 'Platform Builder',\n maps_to: null,\n success_criteria: 'Validate',\n priority: 1,\n steps: [\n { position: 1, purpose: 'landing', path_patterns: ['v2/platforms/portal*'] }\n ]\n }\n };\n\n assert.throws(\n () => validateUsefulnessConfig({ rubric, journeys: badJourneys, audience, llmTiers: llm, prompts }),\n /forbidden pattern/\n );\n }));\n\n const passed = results.filter(Boolean).length;\n if (passed !== results.length) {\n process.exit(1);\n }\n console.log(`\\n${passed} test case(s) passed`);\n}\n\nmain().catch((error) => {\n console.error(error);\n process.exit(1);\n});\n",
+ "header_field_count": 8,
+ "has_any_header": true,
+ "has_framework_header": true,
+ "category_valid": true,
+ "purpose_valid": true,
+ "scope_valid": true,
+ "triggers": [
+ {
+ "type": "runner",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "P3"
+ },
+ {
+ "type": "script",
+ "caller": "tests/run-pr-checks.js",
+ "pipeline": "indirect"
+ }
+ ],
+ "outputs": [
+ {
+ "output_path": "stdout",
+ "type": "stdout",
+ "call": "console"
+ }
+ ],
+ "outputs_display": "stdout only",
+ "downstream_consumers": [],
+ "downstream_display": "No",
+ "in_json": true,
+ "category_match": false,
+ "purpose_match": true,
+ "pipeline_verified": "MATCH",
+ "declared_pipeline_set": {},
+ "actual_pipeline_set": {},
+ "pipeline_actual": "P3 via run-pr-checks; indirect via tests/run-pr-checks.js",
+ "grade": "B",
+ "flags": [
+ "header-json-category-mismatch"
+ ],
+ "trigger_group": "P3"
+ },
+ {
+ "path": "tests/unit/usefulness-rubric.test.js",
+ "script": "usefulness-rubric.test",
+ "category": "utility",
+ "purpose": "qa:content-quality",
+ "scope": "full-repo",
+ "owner": "docs",
+ "needs": "R-R14, R-C6",
+ "purpose_statement": "Tests rubric-based scoring logic against fixture pages.",
+ "pipeline_declared": "P3",
+ "usage": "node tests/unit/usefulness-rubric.test.js",
+ "header": "#!/usr/bin/env node\n/**\n * @script usefulness-rubric.test\n * @category utility\n * @purpose qa:content-quality\n * @scope full-repo\n * @owner docs\n * @needs R-R14, R-C6\n * @purpose-statement Tests rubric-based scoring logic against fixture pages.\n * @pipeline P3\n * @usage node tests/unit/usefulness-rubric.test.js\n */\n\n'use strict';\n\nconst assert = require('assert');\nconst fs = require('fs');\nconst os = require('os');\nconst path = require('path');\n\nconst prompts = require('../../tools/lib/docs-usefulness/prompts');\nconst { EVALUATORS } = require('../../tools/lib/docs-usefulness/rule-evaluators');\nconst { resolvePurpose, resolveAudience, loadRubric, loadAudienceNormalization } = require('../../tools/lib/docs-usefulness/rubric-loader');\nconst { validateUsefulnessConfig } = require('../../tools/lib/docs-usefulness/config-validator');\nconst { analyzeMdxPage, scorePage } = require('../../tools/lib/docs-usefulness/scoring');\nconst { parseArgs, resolveRouteToFile } = require('../../tools/scripts/audit-v2-usefulness');\nconst { LlmEvaluator } = require('../../tools/lib/docs-usefulness/llm-evaluator');\nconst { shouldRunGeneratedBannerCheck } = require('../run-pr-checks');\n\nasync function runCase(name, fn) {\n try {\n await fn();\n console.log(`✓ ${name}`);\n return true;\n } catch (error) {\n console.error(`✗ ${name}`);\n console.error(` ${error.stack || error.message || error}`);\n return false;\n }\n}\n\nasync function main() {\n const results = [];\n\n results.push(await runCase('Rubric config validates against prompts and journey/audience/llm schemas', async () => {\n const rubric = loadRubric();\n const journeys = require('../../tools/config/usefulness-journeys.json');\n const audience = loadAudienceNormalization();\n const llm = require('../../tools/config/usefulness-llm-tiers.json');\n validateUsefulnessConfig({ rubric, journeys, audience, llmTiers: llm, prompts });\n }));\n\n results.push(await runCase('All rubric rule types map to implemented evaluators', async () => {\n const rubric = loadRubric();\n const ruleTypes = new Set();\n Object.values(rubric).forEach((purposeConfig) => {\n Object.values(purposeConfig.tier1_rules || {}).forEach((rule) => ruleTypes.add(rule.type));\n });\n ruleTypes.forEach((type) => {\n assert.ok(typeof EVALUATORS[type] === 'function', `Missing evaluator for type: ${type}`);\n });\n }));\n\n results.push(await runCase('Purpose resolution prioritizes valid frontmatter and marks invalid enum values', async () => {\n const withFrontmatter = resolvePurpose({\n path: 'v2/gateways/example.mdx',\n frontmatter: { purpose: 'how_to' },\n components: [],\n headings: [],\n wordCount: 10\n });\n assert.strictEqual(withFrontmatter.purpose, 'how_to');\n assert.strictEqual(withFrontmatter.source, 'frontmatter');\n\n const invalid = resolvePurpose({\n path: 'v2/gateways/example.mdx',\n frontmatter: { purpose: 'not_real' },\n components: [],\n headings: [],\n wordCount: 10\n });\n assert.strictEqual(invalid.purpose, null);\n assert.strictEqual(invalid.invalid, true);\n }));\n\n results.push(await runCase('Audience normalization handles CSV and synonyms with deterministic precedence', async () => {\n const normalization = loadAudienceNormalization();\n const resolved = resolveAudience({\n section: 'about',\n frontmatter: { audience: 'developers, token-holder' }\n }, normalization);\n\n assert.strictEqual(resolved.audience, 'developer');\n assert.strictEqual(resolved.source, 'frontmatter');\n\n const fallback = resolveAudience({\n section: 'solutions',\n frontmatter: {}\n }, normalization);\n assert.strictEqual(fallback.audience, 'platform-builder');\n assert.strictEqual(fallback.source, 'inferred');\n }));\n\n results.push(await runCase('Audit argument parser hard-fails removed flags', async () => {\n assert.throws(() => parseArgs(['--accuracy-mode', 'tiered'], process.cwd()), /Deprecated flag/);\n assert.throws(() => parseArgs(['--scoring-engine', 'hybrid'], process.cwd()), /Deprecated flag/);\n }));\n\n results.push(await runCase('Route resolution precedence chooses route.mdx over route/index.mdx', async () => {\n const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'usefulness-route-'));\n try {\n fs.mkdirSync(path.join(tmp, 'v2', 'sample', 'demo'), { recursive: true });\n fs.writeFileSync(path.join(tmp, 'v2', 'sample', 'demo.mdx'), '# explicit');\n fs.writeFileSync(path.join(tmp, 'v2', 'sample', 'demo', 'index.mdx'), '# index');\n\n const resolved = resolveRouteToFile('v2/sample/demo', tmp);\n assert.strictEqual(resolved, 'v2/sample/demo.mdx');\n } finally {\n fs.rmSync(tmp, { recursive: true, force: true });\n }\n }));\n\n results.push(await runCase('LLM evaluator parses JSON payloads and reports exhaustion when free-tier capacity is zero', async () => {\n const evaluator = new LlmEvaluator('test-key', { tier: 'free' });\n const parsed = evaluator._extractJson('```json\\\\n{\\\"score\\\":72,\\\"pass\\\":true,\\\"reasoning\\\":\\\"clear\\\"}\\\\n```');\n assert.strictEqual(parsed.score, 72);\n assert.strictEqual(parsed.pass, true);\n\n (evaluator.tier.models || []).forEach((model) => {\n evaluator.modelDailyCounts[model] = evaluator._dailyLimit();\n });\n const selected = evaluator._selectModel();\n assert.strictEqual(selected, null);\n }));\n\n results.push(await runCase('Human/agent divergence is preserved across page types', async () => {\n const landingContent = `---\ntitle: Gateway Portal\ndescription: Choose your next route\npurpose: landing\naudience: gateway-operator\n---\n\n# Gateway Portal\n\nThis page helps operators choose where to go next.\n\n