#!/usr/bin/env node 'use strict'; /* * This audit deliberately uses only saved evidence. It is not a substitute for * a live deploy check and must never turn a legacy-only dashboard capture into * a green migration. */ const fs = require('fs'); const path = require('path'); const DEFAULT_LEGACY_PACKAGE = 'antigravity-awesome-skills'; const DEFAULT_CURRENT_PACKAGE = 'agentic-awesome-skills'; const DEFAULT_CURRENT_PAGES_URL = 'https://sickn33.github.io/agentic-awesome-skills/'; const DEFAULT_LEGACY_PAGES_URL = 'https://sickn33.github.io/antigravity-awesome-skills/'; const SNAPSHOT_DIRECTORY = /^\d{4}-\d{2}-\d{2}$/; const DEFAULT_MAX_EVIDENCE_AGE_DAYS = 7; const DASHBOARD_FILES = { gsc: { file: 'google-search-console.json', hosts: new Set(['search.google.com']), pathPrefix: '/search-console/', primaryParam: 'resource_id', }, bing: { file: 'bing-webmaster-search-performance.json', hosts: new Set(['bing.com', 'www.bing.com']), pathPrefix: '/webmasters/', primaryParam: 'siteUrl', }, }; function normaliseUrl(value) { if (typeof value !== 'string' || !value.trim()) return null; try { const url = new URL(value.trim()); if (url.protocol !== 'https:' && url.protocol !== 'http:') return null; url.hash = ''; url.search = ''; return url.toString(); } catch (_) { return null; } } function withTrailingSlash(value) { const url = normaliseUrl(value); return url && url.endsWith('/') ? url : url && `${url}/`; } function readJson(filePath, errors, label) { try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch (error) { errors.push(`${label}: ${error.message}`); return null; } } function parseSitemap(filePath, errors) { let source; try { source = fs.readFileSync(filePath, 'utf8'); } catch (error) { errors.push(`current sitemap: ${error.message}`); return []; } const locations = [...source.matchAll(/\s*([^<\s]+)\s*<\/loc>/gi)] .map((match) => withTrailingSlash(match[1])) .filter(Boolean); if (!locations.length) errors.push('current sitemap: contains no valid URLs'); return [...new Set(locations)].sort(); } function inferLegacyPagesUrl(currentPagesUrl) { if (!currentPagesUrl) return null; const inferred = withTrailingSlash(currentPagesUrl.replace('/agentic-awesome-skills/', '/antigravity-awesome-skills/')); return inferred && inferred !== currentPagesUrl ? inferred : null; } function isValidIsoDate(value) { if (typeof value !== 'string' || !SNAPSHOT_DIRECTORY.test(value)) return false; const [year, month, day] = value.split('-').map(Number); const parsed = new Date(Date.UTC(year, month - 1, day)); return parsed.getUTCFullYear() === year && parsed.getUTCMonth() === month - 1 && parsed.getUTCDate() === day; } function readSnapshots(snapshotRoot, filename, errors) { let entries; try { entries = fs.readdirSync(snapshotRoot, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && isValidIsoDate(entry.name)) .map((entry) => entry.name) .sort(); } catch (error) { errors.push(`snapshot root: ${error.message}`); return []; } return entries.flatMap((directory) => { const filePath = path.join(snapshotRoot, directory, filename); if (!fs.existsSync(filePath)) return []; const data = readJson(filePath, errors, `${directory}/${filename}`); return data && typeof data === 'object' ? [{ directory, filePath, data }] : []; }); } function propertyEvidence(data, dashboardConfig) { const signals = []; for (const [source, raw] of [ ['source_property', data.source_property], ['property_url', data.property_url], ['propertyUrl', data.propertyUrl], ['site_url', data.site_url], ['siteUrl', data.siteUrl], ]) { const value = withTrailingSlash(raw); if (value) signals.push({ source, value }); } let dashboardValid = false; let dashboardValidationReason = 'missing dashboard_url'; if (typeof data.dashboard_url === 'string') { try { const dashboard = new URL(data.dashboard_url); const hostValid = dashboard.protocol === 'https:' && dashboardConfig.hosts.has(dashboard.hostname.toLowerCase()); const pathValid = dashboard.pathname.startsWith(dashboardConfig.pathPrefix); const primary = withTrailingSlash(dashboard.searchParams.get(dashboardConfig.primaryParam)); dashboardValid = Boolean(hostValid && pathValid && primary); dashboardValidationReason = dashboardValid ? null : 'dashboard host, path, or primary property parameter is invalid'; if (dashboardValid) { for (const param of ['resource_id', 'siteUrl']) { const value = withTrailingSlash(dashboard.searchParams.get(param)); if (value) signals.push({ source: `dashboard_url:${param}`, value }); } } } catch (_) { dashboardValidationReason = 'dashboard_url is malformed'; } } const distinct = [...new Set(signals.map((signal) => signal.value))]; const intendedProperty = withTrailingSlash(data.intended_property); const property = distinct.length === 1 ? distinct[0] : null; return { property, signals, property_conflict: distinct.length > 1, intended_property: intendedProperty, intended_matches_observed: !intendedProperty || (property !== null && intendedProperty === property), dashboard_valid: dashboardValid, dashboard_validation_reason: dashboardValidationReason, }; } function classifyProperty(property, currentPagesUrl, legacyPagesUrl) { const value = withTrailingSlash(property); if (!value) return 'unknown'; if (currentPagesUrl && value === currentPagesUrl) return 'current'; if (legacyPagesUrl && value === legacyPagesUrl) return 'legacy'; return 'other'; } function validMetric(value) { return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0; } function captureFreshness(capturedAt, snapshotDate, asOfDate, maxAgeDays) { const captured = typeof capturedAt === 'string' ? new Date(capturedAt) : null; if (!captured || Number.isNaN(captured.getTime())) { return { valid: false, age_days: null, reason: 'missing or invalid captured_at_utc' }; } const capturedDate = captured.toISOString().slice(0, 10); const ageDays = (Date.parse(`${asOfDate}T00:00:00Z`) - Date.parse(`${capturedDate}T00:00:00Z`)) / 86400000; if (capturedDate !== snapshotDate) return { valid: false, age_days: ageDays, reason: 'capture date does not match snapshot directory' }; if (capturedDate > asOfDate) return { valid: false, age_days: ageDays, reason: 'capture is in the future' }; if (ageDays > maxAgeDays) return { valid: false, age_days: ageDays, reason: 'capture is stale' }; return { valid: true, age_days: ageDays, reason: null }; } function dashboardEvidence(snapshotRoot, dashboardConfig, currentPagesUrl, legacyPagesUrl, errors, asOfDate, maxAgeDays) { const evidence = readSnapshots(snapshotRoot, dashboardConfig.file, errors).map(({ directory, filePath, data }) => { const property = propertyEvidence(data, dashboardConfig); const signalPropertyClasses = [...new Set(property.signals.map((signal) => classifyProperty(signal.value, currentPagesUrl, legacyPagesUrl)))]; return { snapshot: directory, file: filePath, status: data.status || 'unknown', ...property, signal_property_classes: signalPropertyClasses, property_class: classifyProperty(property.property, currentPagesUrl, legacyPagesUrl), captured_at_utc: data.captured_at_utc || null, freshness: captureFreshness(data.captured_at_utc, directory, asOfDate, maxAgeDays), metrics_complete: validMetric(data?.totals?.clicks) && validMetric(data?.totals?.impressions), }; }); const current = evidence.filter((entry) => entry.status === 'success' && entry.property_class === 'current' && !entry.property_conflict && entry.intended_matches_observed && entry.dashboard_valid && entry.freshness.valid && entry.metrics_complete); const legacy = evidence.filter((entry) => entry.status === 'success' && entry.property_class === 'legacy'); return { status: current.length ? 'pass' : 'fail', current_evidence: current, rejected_current_evidence: evidence.filter((entry) => (entry.property_class === 'current' || entry.signal_property_classes.includes('current')) && !current.includes(entry)), legacy_evidence: legacy, other_evidence: evidence.filter((entry) => entry.property_class === 'other' || entry.property_class === 'unknown'), message: current.length ? 'Fresh, schema-complete saved current-property evidence is present.' : 'No fresh, schema-complete saved evidence for the exact current property; legacy-only or spoofed evidence is not a migration fix.', }; } function manifestPairs(payload) { if (Array.isArray(payload)) return payload; if (!payload || typeof payload !== 'object') return []; if (Array.isArray(payload.redirects)) return payload.redirects; if (Array.isArray(payload.routes)) return payload.routes; if (payload.routes && typeof payload.routes === 'object') { return Object.entries(payload.routes).map(([from, to]) => ({ from, to })); } if (payload.redirects && typeof payload.redirects === 'object') { return Object.entries(payload.redirects).map(([from, to]) => ({ from, to })); } return []; } function redirectCoverage(manifestPath, currentUrls, currentPagesUrl, legacyPagesUrl, errors) { if (!manifestPath) return { status: 'fail', expected: currentUrls.length, covered: 0, missing: currentUrls, message: 'No redirect manifest was supplied.' }; const payload = readJson(manifestPath, errors, 'redirect manifest'); if (!payload) return { status: 'fail', expected: currentUrls.length, covered: 0, missing: currentUrls, message: 'Redirect manifest is unreadable.' }; const redirects = new Map(); const duplicates = []; const invalid = []; for (const pair of manifestPairs(payload)) { const from = withTrailingSlash(pair && (pair.from || pair.source || pair.legacy)); const to = withTrailingSlash(pair && (pair.to || pair.destination || pair.current)); if (!from || !to) { invalid.push(pair); } else if (redirects.has(from)) { duplicates.push(from); } else { redirects.set(from, to); } } const expectedPairs = new Map(currentUrls.map((currentUrl) => [ withTrailingSlash(currentUrl.replace(currentPagesUrl, legacyPagesUrl)), currentUrl, ])); const missing = currentUrls.filter((currentUrl) => { const legacyUrl = withTrailingSlash(currentUrl.replace(currentPagesUrl, legacyPagesUrl)); return redirects.get(legacyUrl) !== currentUrl; }); const unexpected = [...redirects.entries()] .filter(([from, to]) => expectedPairs.get(from) !== to) .map(([from, to]) => ({ from, to })); const exact = currentUrls.length && !missing.length && !duplicates.length && !invalid.length && !unexpected.length && redirects.size === expectedPairs.size; return { status: exact ? 'pass' : 'fail', expected: currentUrls.length, covered: currentUrls.length - missing.length, missing, duplicates, invalid, unexpected, message: exact ? 'Redirect manifest maps every expected legacy route exactly once and contains no extras.' : 'Redirect manifest does not exactly cover the expected legacy route set once each.', }; } function packageIdentity(currentPackagePath, legacyPackagePath, currentPackageName, errors) { const current = readJson(currentPackagePath, errors, 'current package metadata'); const legacy = legacyPackagePath ? readJson(legacyPackagePath, errors, 'legacy package metadata') : null; const currentName = currentPackageName || DEFAULT_CURRENT_PACKAGE; const currentOk = Boolean(current && current.name === currentName && typeof current.version === 'string' && current.version); const explicitReplacement = legacy && (legacy.replacementPackage || legacy.replacement_package); const escapedName = currentName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const deprecatedMentionsExactPackage = typeof legacy?.deprecated === 'string' && new RegExp(`(^|[^a-z0-9_-])${escapedName}([^a-z0-9_-]|$)`, 'i').test(legacy.deprecated); const legacyOk = Boolean(legacy && legacy.name === DEFAULT_LEGACY_PACKAGE && typeof legacy.version === 'string' && (explicitReplacement === currentName || deprecatedMentionsExactPackage)); return { status: currentOk && legacyOk ? 'pass' : 'fail', current: current ? { name: current.name || null, version: current.version || null } : null, legacy: legacy ? { name: legacy.name || null, version: legacy.version || null, deprecated: legacy.deprecated || null } : null, message: currentOk && legacyOk ? 'Current package identity and explicit legacy migration path are present.' : 'Both a current package identity and an explicit legacy-to-current package migration path are required.', }; } function auditMigrationReadiness(options = {}) { const repoRoot = path.resolve(options.repoRoot || path.resolve(__dirname, '..', '..')); const errors = []; const sitemapPath = path.resolve(repoRoot, options.sitemapPath || 'apps/web-app/public/sitemap.xml'); const snapshotRoot = path.resolve(repoRoot, options.snapshotRoot || '.codex/traffic-snapshots'); const currentPackagePath = path.resolve(repoRoot, options.currentPackagePath || 'package.json'); const asOfDate = options.asOfDate || new Date().toISOString().slice(0, 10); const maxEvidenceAgeDays = Number(options.maxEvidenceAgeDays ?? DEFAULT_MAX_EVIDENCE_AGE_DAYS); if (!isValidIsoDate(asOfDate)) throw new Error(`invalid as-of date: ${asOfDate}`); if (!Number.isFinite(maxEvidenceAgeDays) || maxEvidenceAgeDays < 0) throw new Error('max evidence age must be a non-negative number'); const currentUrls = parseSitemap(sitemapPath, errors); const currentPagesUrl = withTrailingSlash(options.currentPagesUrl || DEFAULT_CURRENT_PAGES_URL); const legacyPagesUrl = withTrailingSlash(options.legacyPagesUrl || DEFAULT_LEGACY_PAGES_URL || inferLegacyPagesUrl(currentPagesUrl)); const identitiesDistinct = Boolean(currentPagesUrl && legacyPagesUrl && currentPagesUrl !== legacyPagesUrl); const currentSitemap = { status: identitiesDistinct && currentUrls.includes(currentPagesUrl) && currentUrls.every((url) => url.startsWith(currentPagesUrl)) ? 'pass' : 'fail', current_pages_url: currentPagesUrl, urls: currentUrls, message: identitiesDistinct && currentUrls.includes(currentPagesUrl) && currentUrls.every((url) => url.startsWith(currentPagesUrl)) ? 'Current sitemap includes its root and contains only the configured current Pages identity.' : 'Current sitemap is missing its root, malformed, uses identical legacy/current identities, or includes URLs outside the configured current Pages identity.', }; const checks = { identity_anchors: { status: identitiesDistinct ? 'pass' : 'fail', current_package_name: options.currentPackageName || DEFAULT_CURRENT_PACKAGE, current_pages_url: currentPagesUrl, legacy_pages_url: legacyPagesUrl, message: identitiesDistinct ? 'Configured current and legacy identities are present and distinct.' : 'Current and legacy Pages identities must be explicit and distinct.', }, current_sitemap_identity: currentSitemap, google_search_console: dashboardEvidence(snapshotRoot, DASHBOARD_FILES.gsc, currentPagesUrl, legacyPagesUrl, errors, asOfDate, maxEvidenceAgeDays), bing_webmaster: dashboardEvidence(snapshotRoot, DASHBOARD_FILES.bing, currentPagesUrl, legacyPagesUrl, errors, asOfDate, maxEvidenceAgeDays), redirect_manifest_coverage: redirectCoverage( options.redirectManifestPath && path.resolve(repoRoot, options.redirectManifestPath), currentUrls, currentPagesUrl, legacyPagesUrl, errors, ), npm_identities: packageIdentity( currentPackagePath, options.legacyPackagePath && path.resolve(repoRoot, options.legacyPackagePath), options.currentPackageName, errors, ), }; const failedChecks = Object.entries(checks).filter(([, check]) => check.status !== 'pass').map(([name]) => name); return { schema_version: 1, status: failedChecks.length || errors.length ? 'not_ready' : 'ready', evidence_policy: { as_of_date: asOfDate, max_age_days: maxEvidenceAgeDays }, identities: { current_pages_url: currentPagesUrl, legacy_pages_url: legacyPagesUrl }, checks, third_party_state: { status: 'not_assessed', message: 'Third-party directories are outside this local audit. A request or partial third-party update is not reported as fixed.', }, errors, failed_checks: failedChecks, }; } function writeJsonAtomically(filePath, value) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`; try { fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, 'utf8'); fs.renameSync(temporary, filePath); } finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); } } function parseArgs(argv) { const options = {}; const aliases = { '--repo-root': 'repoRoot', '--snapshot-root': 'snapshotRoot', '--sitemap': 'sitemapPath', '--redirect-manifest': 'redirectManifestPath', '--current-package': 'currentPackagePath', '--legacy-package': 'legacyPackagePath', '--current-pages-url': 'currentPagesUrl', '--legacy-pages-url': 'legacyPagesUrl', '--current-package-name': 'currentPackageName', '--output': 'outputPath', '--as-of': 'asOfDate', '--max-evidence-age-days': 'maxEvidenceAgeDays', }; for (let index = 0; index < argv.length; index += 1) { const key = aliases[argv[index]]; if (!key || !argv[index + 1] || argv[index + 1].startsWith('--')) throw new Error(`Unknown option or missing value: ${argv[index]}`); options[key] = argv[index + 1]; index += 1; } return options; } if (require.main === module) { try { const options = parseArgs(process.argv.slice(2)); const report = auditMigrationReadiness(options); const outputPath = path.resolve(options.repoRoot || path.resolve(__dirname, '..', '..'), options.outputPath || '.codex/traffic-snapshots/migration-readiness.json'); writeJsonAtomically(outputPath, report); process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); process.exitCode = report.status === 'ready' ? 0 : 1; } catch (error) { process.stderr.write(`migration readiness audit failed: ${error.message}\n`); process.exitCode = 2; } } module.exports = { auditMigrationReadiness, parseArgs, redirectCoverage, writeJsonAtomically };