Files
playbook/antigravity-awesome-skills/tools/scripts/normalize_traffic_snapshots.js
T
2026-07-18 00:02:59 +00:00

735 lines
30 KiB
JavaScript

#!/usr/bin/env node
/**
* Normalize the read-only captures in .codex/traffic-snapshots.
*
* The source dashboards have different grains. GitHub supplies rolling daily
* rows, while Search Console can be a snapshot total and Bing can supply both
* totals and daily rows. This script deliberately keeps dashboard properties
* separate: a legacy Pages property and a current Pages property are not a
* time series that may safely be summed together.
*/
const fs = require("fs");
const path = require("path");
const DEFAULT_INPUT = path.resolve(__dirname, "..", "..", ".codex", "traffic-snapshots");
const DEFAULT_OUTPUT = path.join(DEFAULT_INPUT, "daily-normalized.json");
const SNAPSHOT_DIRECTORY = /^\d{4}-\d{2}-\d{2}$/;
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
const MONTH_NAMES = ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"];
const CURRENT_REPOSITORY = "sickn33/agentic-awesome-skills";
const REPOSITORY_ALIASES = new Map([
["sickn33/antigravity-awesome-skills", CURRENT_REPOSITORY],
[CURRENT_REPOSITORY, CURRENT_REPOSITORY],
]);
const CURRENT_PAGES_PROPERTY = "https://sickn33.github.io/agentic-awesome-skills/";
const LEGACY_PAGES_PROPERTY = "https://sickn33.github.io/antigravity-awesome-skills/";
const LEGACY_PACKAGE = "antigravity-awesome-skills";
const CURRENT_PACKAGE = "agentic-awesome-skills";
const NPM_PACKAGES = new Set([LEGACY_PACKAGE, CURRENT_PACKAGE]);
const DASHBOARDS = [
{
file: "google-search-console.json",
source: "google_search_console",
dashboardHosts: new Set(["search.google.com"]),
dashboardPathPrefix: "/search-console/",
primaryPropertyParam: "resource_id",
requiredDailyMetrics: ["clicks", "impressions"],
requiredTotalMetrics: ["clicks", "impressions"],
dailyMetrics: (value) => ({
clicks: nonNegativeIntegerOrNull(value.clicks),
impressions: nonNegativeIntegerOrNull(value.impressions),
}),
totals: (value) => ({
clicks: nonNegativeIntegerOrNull(value?.totals?.clicks ?? value.clicks),
impressions: nonNegativeIntegerOrNull(value?.totals?.impressions ?? value?.totals?.impressions_visible ?? value.impressions),
ctr: value?.totals?.ctr ?? value.ctr ?? null,
average_position: nonNegativeNumberOrNull(value?.totals?.average_position ?? value?.totals?.position ?? value.average_position),
}),
},
{
file: "bing-webmaster-search-performance.json",
source: "bing_search",
dashboardHosts: new Set(["bing.com", "www.bing.com"]),
dashboardPathPrefix: "/webmasters/",
primaryPropertyParam: "siteUrl",
requiredDailyMetrics: ["clicks", "impressions"],
requiredTotalMetrics: ["clicks", "impressions"],
dailyMetrics: (value) => ({
clicks: nonNegativeIntegerOrNull(value.clicks),
impressions: nonNegativeIntegerOrNull(value.impressions),
}),
totals: (value) => ({
clicks: nonNegativeIntegerOrNull(value?.totals?.clicks ?? value.clicks),
impressions: nonNegativeIntegerOrNull(value?.totals?.impressions ?? value?.totals?.impressions_label ?? value.impressions),
ctr: value?.totals?.ctr ?? value.ctr ?? null,
}),
},
{
file: "bing-webmaster-ai-performance.json",
source: "bing_ai",
dashboardHosts: new Set(["bing.com", "www.bing.com"]),
dashboardPathPrefix: "/webmasters/",
primaryPropertyParam: "siteUrl",
requiredDailyMetrics: ["citations"],
requiredTotalMetrics: ["citations"],
dailyMetrics: (value) => ({
citations: nonNegativeIntegerOrNull(value.citations ?? value.total_citations),
avg_cited_pages: nonNegativeNumberOrNull(value.avg_cited_pages),
}),
totals: (value) => ({
citations: nonNegativeIntegerOrNull(value.total_citations ?? value?.totals?.citations),
avg_cited_pages: nonNegativeNumberOrNull(value.avg_cited_pages ?? value?.totals?.avg_cited_pages),
}),
},
];
function numberOrNull(value) {
if (typeof value === "number" && Number.isFinite(value)) return value;
if (typeof value !== "string") return null;
const raw = value.trim().replace(/\s/g, "");
if (!raw) return null;
const hasK = /k$/i.test(raw);
let numeric = raw.replace(/k$/i, "");
// Dashboard captures use both 5.1K and Italian-style 39,4K labels.
if (hasK) {
numeric = numeric.replace(",", ".");
} else if (/^\d{1,3}(?:[.,]\d{3})+$/.test(numeric)) {
numeric = numeric.replace(/[.,]/g, "");
} else {
numeric = numeric.replace(",", ".");
}
const parsed = Number(numeric);
return Number.isFinite(parsed) ? (hasK ? parsed * 1000 : parsed) : null;
}
function nonNegativeNumberOrNull(value) {
const parsed = numberOrNull(value);
return parsed !== null && parsed >= 0 ? parsed : null;
}
function nonNegativeIntegerOrNull(value) {
const parsed = nonNegativeNumberOrNull(value);
return parsed !== null && Number.isInteger(parsed) ? parsed : null;
}
function isCompleteMetrics(metrics) {
return Object.values(metrics).some((value) => value !== null);
}
function normalizedDate(value) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
if (ISO_DATE.test(trimmed)) return isValidIsoDate(trimmed) ? trimmed : null;
if (/^\d{4}-\d{2}-\d{2}T/.test(trimmed)) {
const date = trimmed.slice(0, 10);
return isValidIsoDate(date) ? date : null;
}
const match = trimmed.match(
/^(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),?\s+(\d{4})$/i,
);
if (!match) return null;
const month = MONTH_NAMES.indexOf(match[1].toLowerCase()) + 1;
const date = `${match[3]}-${String(month).padStart(2, "0")}-${String(match[2]).padStart(2, "0")}`;
return isValidIsoDate(date) ? date : null;
}
function isValidIsoDate(value) {
if (!ISO_DATE.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 canonicalProperty(candidate) {
if (typeof candidate !== "string" || !candidate.trim()) return null;
try {
const url = new URL(candidate.trim());
if (url.protocol !== "https:" && url.protocol !== "http:") return null;
url.hash = "";
url.search = "";
const pathname = url.pathname.endsWith("/") ? url.pathname : `${url.pathname}/`;
return `${url.protocol}//${url.host}${pathname}`;
} catch {
return null;
}
}
function propertyFromDashboardUrl(dashboardUrl, config) {
if (typeof dashboardUrl !== "string") return { valid: false, signals: [], reason: "missing dashboard_url" };
try {
const url = new URL(dashboardUrl);
const hostValid = url.protocol === "https:" && config.dashboardHosts.has(url.hostname.toLowerCase());
const pathValid = url.pathname.startsWith(config.dashboardPathPrefix);
const primary = canonicalProperty(url.searchParams.get(config.primaryPropertyParam));
if (!hostValid || !pathValid || !primary) {
return { valid: false, signals: [], reason: "dashboard host, path, or primary property parameter is invalid" };
}
const signals = [];
for (const param of ["resource_id", "siteUrl"]) {
const value = canonicalProperty(url.searchParams.get(param));
if (value) signals.push({ source: `dashboard_url:${param}`, value });
}
return { valid: true, signals, reason: null };
} catch {
return { valid: false, signals: [], reason: "dashboard_url is malformed" };
}
}
function sourceProperty(snapshot, config) {
const observed = [];
for (const [source, raw] of [
["source_property", snapshot.source_property],
["property_url", snapshot.property_url],
["propertyUrl", snapshot.propertyUrl],
["site_url", snapshot.site_url],
["siteUrl", snapshot.siteUrl],
]) {
const value = canonicalProperty(raw);
if (value) observed.push({ source, value });
}
const dashboard = propertyFromDashboardUrl(snapshot.dashboard_url, config);
if (snapshot.dashboard_url && !dashboard.valid) {
return { property: null, provenance: "invalid", conflict: true, reason: dashboard.reason };
}
observed.push(...dashboard.signals);
const distinct = [...new Set(observed.map((signal) => signal.value))];
if (distinct.length > 1) {
return { property: null, provenance: "conflict", conflict: true, reason: "observed property signals conflict" };
}
const intended = canonicalProperty(snapshot.intended_property);
if (distinct.length === 1) {
return {
property: distinct[0],
provenance: "observed",
conflict: false,
reason: intended && intended !== distinct[0] ? "intended property disagrees with observed property" : null,
};
}
if (intended) {
return { property: intended, provenance: "intended_only", conflict: false, reason: "property identity comes from intended_property only" };
}
return { property: null, provenance: "unknown", conflict: false, reason: "no property identity signal" };
}
function propertyIdentity(property) {
const value = canonicalProperty(property);
if (value === LEGACY_PAGES_PROPERTY) return "legacy";
if (value === CURRENT_PAGES_PROPERTY) return "current";
return "unknown";
}
function coverageEnd(snapshot) {
const dailyValues = Array.isArray(snapshot.daily_values) ? snapshot.daily_values : [];
const dailyDates = dailyValues.map((row) => normalizedDate(row?.date)).filter(Boolean).sort();
if (dailyDates.length) return dailyDates.at(-1);
const text = [
snapshot.date_range_visible,
snapshot.date_range,
snapshot.chart_visible?.description,
].filter((value) => typeof value === "string").join(" ");
const isoDates = (text.match(/\d{4}-\d{2}-\d{2}/g) || []).map(normalizedDate).filter(Boolean);
if (isoDates?.length) return isoDates.sort().at(-1);
const monthDates = [...text.matchAll(
/\b(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d{1,2}),\s+(\d{4})\b/gi,
)].map((match) => {
const month = MONTH_NAMES.indexOf(match[1].toLowerCase()) + 1;
return normalizedDate(`${match[3]}-${String(month).padStart(2, "0")}-${String(match[2]).padStart(2, "0")}`);
}).filter(Boolean);
return monthDates.length ? monthDates.sort().at(-1) : null;
}
function readJson(filePath, warnings, label) {
try {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
} catch (error) {
warnings.push(`${label}: skipped malformed JSON (${error.message.split("\n")[0]})`);
return null;
}
}
function setLatest(map, key, observation) {
const previous = map.get(key);
if (!previous || observation.observed_from_snapshot > previous.observed_from_snapshot) {
map.set(key, observation);
}
}
function setBestDashboardEvidence(map, key, observation) {
const previous = map.get(key);
const strength = { observed: 2, intended_only: 1, unknown: 0 };
const candidateStrength = strength[observation.property_provenance] ?? 0;
const previousStrength = strength[previous?.property_provenance] ?? -1;
if (!previous
|| candidateStrength > previousStrength
|| (candidateStrength === previousStrength && observation.observed_from_snapshot > previous.observed_from_snapshot)) {
map.set(key, observation);
}
}
function snapshotDirectories(inputDirectory) {
if (!fs.existsSync(inputDirectory)) throw new Error(`input directory does not exist: ${inputDirectory}`);
if (!fs.statSync(inputDirectory).isDirectory()) throw new Error(`input path is not a directory: ${inputDirectory}`);
const snapshots = fs.readdirSync(inputDirectory, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && SNAPSHOT_DIRECTORY.test(entry.name) && isValidIsoDate(entry.name))
.map((entry) => entry.name)
.sort();
if (!snapshots.length) throw new Error(`input directory has no valid snapshot directories: ${inputDirectory}`);
return snapshots;
}
function canonicalRepository(value) {
if (typeof value !== "string" || !value.trim()) return null;
const normalized = value.trim().toLowerCase();
return REPOSITORY_ALIASES.get(normalized) || normalized;
}
function npmPackageEntries(payload) {
if (!payload || payload.status !== "success") return { ok: false, reason: "non-success capture" };
if (Array.isArray(payload.responses)) return { ok: true, entries: payload.responses };
if (Array.isArray(payload.packages)) return { ok: true, entries: payload.packages };
if (typeof payload.package === "string" && Array.isArray(payload.downloads)) return { ok: true, entries: [payload] };
if (!payload.packages || typeof payload.packages !== "object") {
return { ok: false, reason: "missing packages collection" };
}
return { ok: false, reason: "packages must contain official npm range responses" };
}
function nextIsoDate(date) {
const [year, month, day] = date.split("-").map(Number);
const next = new Date(Date.UTC(year, month - 1, day + 1));
return `${next.getUTCFullYear()}-${String(next.getUTCMonth() + 1).padStart(2, "0")}-${String(next.getUTCDate()).padStart(2, "0")}`;
}
function parseNpmDownloads(payload) {
const collection = npmPackageEntries(payload);
if (!collection.ok) return collection;
const packages = new Map();
const issues = [];
for (const entry of collection.entries) {
const name = typeof entry?.package === "string" ? entry.package.trim() : null;
if (!name || !NPM_PACKAGES.has(name)) {
issues.push("unknown package identity");
continue;
}
if (packages.has(name)) {
issues.push(`duplicate package identity: ${name}`);
continue;
}
if (!Array.isArray(entry.downloads)) {
issues.push(`missing npm range downloads for ${name}`);
continue;
}
const daily = new Map();
let valid = true;
for (const row of entry.downloads) {
const date = normalizedDate(row?.day);
const downloads = nonNegativeIntegerOrNull(row?.downloads);
if (!date || downloads === null) {
issues.push(`malformed daily downloads for ${name}`);
valid = false;
break;
}
if (daily.has(date)) {
issues.push(`duplicate daily downloads for ${name}/${date}`);
valid = false;
break;
}
daily.set(date, downloads);
}
const start = normalizedDate(entry.start);
const end = normalizedDate(entry.end);
const dates = [...daily.keys()].sort();
if (!start || !end || !dates.length || dates[0] !== start || dates.at(-1) !== end) {
issues.push(`npm range boundaries do not match daily downloads for ${name}`);
valid = false;
}
for (let index = 1; valid && index < dates.length; index += 1) {
if (nextIsoDate(dates[index - 1]) !== dates[index]) {
issues.push(`npm range daily downloads are incomplete for ${name}`);
valid = false;
}
}
if (!valid) continue;
packages.set(name, daily);
}
return { ok: true, packages, issues: [...new Set(issues)].sort() };
}
function setNpmObservation(observations, conflicts, packageName, date, observation) {
const key = `${packageName}\u0000${date}`;
if (conflicts.has(key)) return;
const previous = observations.get(key);
if (!previous) {
observations.set(key, observation);
return;
}
if (previous.downloads !== observation.downloads) {
observations.delete(key);
conflicts.set(key, `conflicting npm download observations for ${packageName}/${date}`);
return;
}
if (observation.observed_from_snapshot > previous.observed_from_snapshot) observations.set(key, observation);
}
function npmCombinedRecord(date, observations, conflicts, captureIssues) {
const legacyKey = `${LEGACY_PACKAGE}\u0000${date}`;
const currentKey = `${CURRENT_PACKAGE}\u0000${date}`;
const legacy = observations.get(legacyKey) || null;
const current = observations.get(currentKey) || null;
const conflictReasons = [conflicts.get(legacyKey), conflicts.get(currentKey)].filter(Boolean);
const issues = captureIssues.get(date) || [];
const packages = {
[LEGACY_PACKAGE]: legacy,
[CURRENT_PACKAGE]: current,
};
const base = {
label: "same-day old-plus-current npm downloads only",
packages,
total_downloads: null,
current_package_share: null,
};
if (issues.length) return { ...base, status: "invalid_capture", reason: issues.join("; ") };
if (conflictReasons.length) {
return { ...base, status: "conflicting_observations", reason: conflictReasons.join("; ") };
}
if (!legacy && !current) return { ...base, status: "not_observed", reason: "no accepted npm download observations for this date" };
if (!legacy) return { ...base, status: "missing_legacy", reason: `missing ${LEGACY_PACKAGE} observation for this date` };
if (!current) return { ...base, status: "missing_current", reason: `missing ${CURRENT_PACKAGE} observation for this date` };
const total = legacy.downloads + current.downloads;
if (total === 0) {
return {
...base,
status: "complete_zero_total",
reason: "both package observations exist but current-package share is undefined for a zero total",
total_downloads: 0,
};
}
return {
...base,
status: "complete",
reason: null,
total_downloads: total,
current_package_share: current.downloads / total,
};
}
function normalizeSnapshots(inputDirectory) {
const warnings = [];
const github = { views: new Map(), clones: new Map() };
const npmDownloads = new Map();
const npmConflicts = new Map();
const npmCaptureIssues = new Map();
const dashboardDaily = new Map();
const dashboardTotals = new Map();
const discoveredSnapshots = snapshotDirectories(inputDirectory);
const snapshots = [];
const sourceRepositories = new Set();
let repo = null;
let timezone = null;
for (const snapshotDate of discoveredSnapshots) {
const directory = path.join(inputDirectory, snapshotDate);
const manifestPath = path.join(directory, "manifest.json");
if (!fs.existsSync(manifestPath)) {
warnings.push(`${snapshotDate}/manifest.json: skipped snapshot without identity manifest`);
continue;
}
const manifest = readJson(manifestPath, warnings, `${snapshotDate}/manifest.json`);
const rawRepository = typeof manifest?.repo === "string" ? manifest.repo.trim() : null;
const snapshotRepository = canonicalRepository(rawRepository);
const snapshotTimezone = typeof manifest?.timezone === "string" ? manifest.timezone.trim() : null;
if (!snapshotRepository || !snapshotTimezone) {
warnings.push(`${snapshotDate}/manifest.json: skipped snapshot with missing repository or timezone identity`);
continue;
}
if ((repo && snapshotRepository !== repo) || (timezone && snapshotTimezone !== timezone)) {
warnings.push(`${snapshotDate}/manifest.json: skipped snapshot with repository or timezone mismatch`);
continue;
}
repo ||= snapshotRepository;
timezone ||= snapshotTimezone;
sourceRepositories.add(rawRepository);
snapshots.push(snapshotDate);
for (const [kind, file] of [["views", "views.json"], ["clones", "clones.json"]]) {
const filePath = path.join(directory, file);
if (!fs.existsSync(filePath)) continue;
const payload = readJson(filePath, warnings, `${snapshotDate}/${file}`);
const values = Array.isArray(payload?.[kind]) ? payload[kind] : null;
if (!values) {
warnings.push(`${snapshotDate}/${file}: skipped missing ${kind} array`);
continue;
}
for (const value of values) {
const date = normalizedDate(value?.timestamp);
const count = nonNegativeIntegerOrNull(value?.count);
const uniques = nonNegativeIntegerOrNull(value?.uniques);
if (!date || !ISO_DATE.test(date) || count === null || uniques === null) {
warnings.push(`${snapshotDate}/${file}: skipped malformed daily row`);
continue;
}
setLatest(github[kind], date, { count, uniques, observed_from_snapshot: snapshotDate });
}
}
const npmPath = path.join(directory, "npm-downloads.json");
if (fs.existsSync(npmPath)) {
const payload = readJson(npmPath, warnings, `${snapshotDate}/npm-downloads.json`);
if (payload) {
const parsed = parseNpmDownloads(payload);
if (!parsed.ok) {
warnings.push(`${snapshotDate}/npm-downloads.json: skipped ${parsed.reason}`);
} else {
for (const issue of parsed.issues) warnings.push(`${snapshotDate}/npm-downloads.json: ${issue}`);
for (const [packageName, daily] of parsed.packages) {
for (const [date, downloads] of daily) {
setNpmObservation(npmDownloads, npmConflicts, packageName, date, {
package_name: packageName,
downloads,
observed_from_snapshot: snapshotDate,
});
if (parsed.issues.length) {
const dateIssues = npmCaptureIssues.get(date) || [];
dateIssues.push(...parsed.issues.map((issue) => `${snapshotDate}: ${issue}`));
npmCaptureIssues.set(date, [...new Set(dateIssues)].sort());
}
}
}
}
}
}
for (const config of DASHBOARDS) {
const filePath = path.join(directory, config.file);
if (!fs.existsSync(filePath)) continue;
const payload = readJson(filePath, warnings, `${snapshotDate}/${config.file}`);
if (!payload) continue;
if (payload.status !== "success") {
warnings.push(`${snapshotDate}/${config.file}: skipped non-success capture`);
continue;
}
const propertyEvidence = sourceProperty(payload, config);
if (propertyEvidence.conflict) {
warnings.push(`${snapshotDate}/${config.file}: skipped capture because ${propertyEvidence.reason}`);
continue;
}
const property = propertyEvidence.property;
if (propertyEvidence.reason) warnings.push(`${snapshotDate}/${config.file}: ${propertyEvidence.reason}`);
const identity = propertyIdentity(property);
if (identity === "unknown") {
warnings.push(`${snapshotDate}/${config.file}: property identity is unknown and remains isolated`);
}
const end = coverageEnd(payload);
const base = {
source_property: property,
property_provenance: propertyEvidence.provenance,
property_identity: identity,
coverage_end: end,
observed_from_snapshot: snapshotDate,
};
const propertyKey = property || `unknown:${snapshotDate}`;
const dailyValues = Array.isArray(payload.daily_values) ? payload.daily_values : [];
for (const value of dailyValues) {
const date = normalizedDate(value?.date);
if (!date) {
warnings.push(`${snapshotDate}/${config.file}: skipped malformed dashboard daily row`);
continue;
}
const metrics = config.dailyMetrics(value);
if (!isCompleteMetrics(metrics)) {
warnings.push(`${snapshotDate}/${config.file}: skipped dashboard daily row without metrics`);
continue;
}
// Never stitch a partial payload together with an older snapshot. A
// dashboard row is useful only when its source-specific core metrics
// arrived in the same source observation.
if (config.requiredDailyMetrics.some((metric) => metrics[metric] === null)) {
warnings.push(`${snapshotDate}/${config.file}: skipped partial dashboard daily row`);
continue;
}
const key = `${config.source}\u0000${propertyKey}\u0000${date}`;
setBestDashboardEvidence(dashboardDaily, key, { source: config.source, date, ...base, ...metrics });
}
const totals = config.totals(payload);
if (config.requiredTotalMetrics.every((metric) => totals[metric] !== null)) {
const key = `${config.source}\u0000${propertyKey}\u0000${end || "unknown"}`;
setBestDashboardEvidence(dashboardTotals, key, { source: config.source, ...base, ...totals });
} else if (isCompleteMetrics(totals)) {
warnings.push(`${snapshotDate}/${config.file}: skipped partial dashboard totals`);
}
}
}
if (!snapshots.length) {
throw new Error(`no snapshots with a consistent repository and timezone identity were accepted from: ${inputDirectory}`);
}
const rowsByDate = new Map();
const ensureRow = (date) => {
if (!rowsByDate.has(date)) rowsByDate.set(date, { date });
return rowsByDate.get(date);
};
for (const [kind, values] of Object.entries(github)) {
for (const [date, observation] of values) {
const row = ensureRow(date);
row.github ||= {};
row.github[kind] = observation;
}
}
for (const observation of dashboardDaily.values()) {
const row = ensureRow(observation.date);
row[observation.source] ||= [];
const { source, date, ...record } = observation;
row[source].push(record);
}
const npmDates = new Set([
...[...npmDownloads.keys()].map((key) => key.split("\u0000")[1]),
...[...npmConflicts.keys()].map((key) => key.split("\u0000")[1]),
...npmCaptureIssues.keys(),
]);
for (const date of npmDates) ensureRow(date).npm_downloads = npmCombinedRecord(date, npmDownloads, npmConflicts, npmCaptureIssues);
const rows = [...rowsByDate.values()].sort((left, right) => left.date.localeCompare(right.date));
for (const row of rows) {
for (const source of DASHBOARDS.map((config) => config.source)) {
if (Array.isArray(row[source])) {
row[source].sort((left, right) =>
`${left.property_identity}\u0000${left.source_property || ""}\u0000${left.observed_from_snapshot}`.localeCompare(
`${right.property_identity}\u0000${right.source_property || ""}\u0000${right.observed_from_snapshot}`,
),
);
}
}
}
const snapshot_totals = {};
for (const config of DASHBOARDS) {
snapshot_totals[config.source] = [...dashboardTotals.values()]
.filter((record) => record.source === config.source)
.map(({ source, ...record }) => record)
.sort((left, right) =>
`${left.property_identity}\u0000${left.source_property || ""}\u0000${left.coverage_end || ""}\u0000${left.observed_from_snapshot}`.localeCompare(
`${right.property_identity}\u0000${right.source_property || ""}\u0000${right.coverage_end || ""}\u0000${right.observed_from_snapshot}`,
),
);
}
return {
schema_version: "3.0.0",
repo,
source_repositories: [...sourceRepositories].sort(),
timezone,
grain: "date",
caveats: [
"GitHub rolling-window rows are deduplicated by date and retain the latest snapshot observation.",
"Dashboard data is separated by source_property and property_identity; legacy and current properties are never summed or overwritten.",
"The combined view unifies GitHub repository rename aliases and combines npm downloads only when exact old/current package observations exist for the same date.",
"Search Console and Bing dashboard properties are explicitly not_combined, even when both legacy and current captures exist.",
"coverage_end describes the latest date visible in a dashboard capture, not a promise of complete data.",
"Malformed, partial, or non-success captures are skipped with warnings rather than merged into another observation.",
],
discovered_snapshots: discoveredSnapshots,
snapshots,
rows,
snapshot_totals,
combined_view: {
label: "rename-unified measurement view",
github_repository: {
status: repo === CURRENT_REPOSITORY ? "rename_unified" : "canonical_only",
canonical_repository: repo,
source_repositories: [...sourceRepositories].sort(),
},
npm_downloads: {
label: "same-day old-plus-current npm downloads only",
legacy_package: LEGACY_PACKAGE,
current_package: CURRENT_PACKAGE,
status: npmDates.size ? "per_date_status_in_rows" : "not_observed",
unavailable_behavior: "total_downloads and current_package_share are null unless both exact package observations are accepted for the same date",
},
dashboard_properties: {
status: "not_combined",
reason: "Google Search Console and Bing properties remain separate by source_property and property_identity.",
},
},
warnings: warnings.sort(),
};
}
function atomicWrite(outputPath, value) {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
const temporary = path.join(
path.dirname(outputPath),
`.${path.basename(outputPath)}.${process.pid}.${Date.now()}.tmp`,
);
try {
fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, "utf8");
fs.renameSync(temporary, outputPath);
} finally {
if (fs.existsSync(temporary)) fs.unlinkSync(temporary);
}
}
function parseArgs(args) {
const options = { input: DEFAULT_INPUT, output: DEFAULT_OUTPUT };
for (let index = 0; index < args.length; index += 1) {
const option = args[index];
if (option === "--input" || option === "--output") {
const value = args[index + 1];
if (!value || value.startsWith("--")) throw new Error(`${option} requires a value`);
options[option.slice(2)] = path.resolve(value);
index += 1;
} else if (option === "--help" || option === "-h") {
options.help = true;
} else {
throw new Error(`Unknown option: ${option}`);
}
}
return options;
}
function main() {
const options = parseArgs(process.argv.slice(2));
if (options.help) {
console.log("Usage: node tools/scripts/normalize_traffic_snapshots.js [--input DIR] [--output FILE]");
return;
}
const normalized = normalizeSnapshots(options.input);
atomicWrite(options.output, normalized);
console.log(`Normalized ${normalized.snapshots.length} snapshot directories to ${options.output}`);
}
if (require.main === module) {
try {
main();
} catch (error) {
console.error(`traffic normalization failed: ${error.message}`);
process.exitCode = 1;
}
}
module.exports = {
DEFAULT_INPUT,
DEFAULT_OUTPUT,
atomicWrite,
normalizeSnapshots,
parseArgs,
propertyIdentity,
sourceProperty,
};