Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 1 addition & 11 deletions tools/catalog-build/build-stats.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { replaceClarityDays, unchangedStatsPayload } from './stats-state.js';
import { replaceClarityDays, slugFromClarityUrl, unchangedStatsPayload } from './stats-state.js';

const toolDirectory = dirname(fileURLToPath(import.meta.url));
const repositoryRoot = join(toolDirectory, '..', '..');
Expand Down Expand Up @@ -98,16 +98,6 @@ function collectFallbackViews(resourceFolders) {
return totals;
}

function slugFromClarityUrl(value, knownSlugs) {
if (typeof value !== 'string') return undefined;
try {
const slug = decodeURIComponent(new URL(value).hash.slice(1));
return knownSlugs.has(slug) ? slug : undefined;
} catch {
return undefined;
}
}

function dateFromClarityRow(row) {
const value = row.date ?? row.Date ?? row.day ?? row.Day;
if (typeof value !== 'string') return undefined;
Expand Down
19 changes: 19 additions & 0 deletions tools/catalog-build/stats-state.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
// Resolves a Clarity page URL to a catalog slug. The catalog's detail view uses
// `/#<slug>` and each shareable resource page lives at `/r/<slug>/`; both count as views.
export function slugFromClarityUrl(value, knownSlugs) {
if (typeof value !== 'string') return undefined;
try {
const url = new URL(value);
const fromHash = decodeURIComponent(url.hash.slice(1));
if (knownSlugs.has(fromHash)) return fromHash;
const fromPath = url.pathname.match(/\/r\/([^/]+)\/?$/);
if (fromPath) {
const slug = decodeURIComponent(fromPath[1]);
if (knownSlugs.has(slug)) return slug;
}
return undefined;
} catch {
return undefined;
}
}

export function replaceClarityDays(state, buckets, lastRun) {
for (const [date, metrics] of buckets) {
state.days[date] = Object.fromEntries(
Expand Down
14 changes: 14 additions & 0 deletions tools/catalog-build/test-stats-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@ import {
clarityStateContains,
reconcileClarityStates,
replaceClarityDays,
slugFromClarityUrl,
unchangedStatsPayload
} from './stats-state.js';

function assert(condition, message) {
if (!condition) throw new Error(message);
}

// Views on both the catalog detail route (#slug) and the shareable page (/r/slug/) count.
const known = new Set(['ai-council', 'agents-cost-calculator']);
assert(slugFromClarityUrl('https://microsoft.github.io/FastTrack/#ai-council', known) === 'ai-council',
'Hash routes must resolve to a slug.');
assert(slugFromClarityUrl('https://microsoft.github.io/FastTrack/r/agents-cost-calculator/', known) === 'agents-cost-calculator',
'Shareable resource pages must resolve to a slug.');
assert(slugFromClarityUrl('https://microsoft.github.io/FastTrack/r/agents-cost-calculator', known) === 'agents-cost-calculator',
'A missing trailing slash must still resolve.');
assert(slugFromClarityUrl('https://microsoft.github.io/FastTrack/r/unknown/', known) === undefined,
'Unknown slugs must not resolve.');
assert(slugFromClarityUrl('https://microsoft.github.io/FastTrack/', known) === undefined,
'The landing page is not a resource view.');

const state = {
lastRun: '2026-09-10',
days: {
Expand Down
Loading