Spaces:
Running
Running
Commit Β·
5661ed6
1
Parent(s): 0bc5d1a
Add quarter label auto-update to leaderboard sync
Browse filesSync now detects when index.html header quarter (e.g. 2026Q2) is behind
the current calendar quarter and updates it as part of the same commit+push
flow. Quarter-only runs get a distinct Teams notification title and a note
that no ranking changes were involved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
.claude/skills/sync-nowai-leaderboard/sync.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
#!/usr/bin/env node
|
| 2 |
// Orchestrator for the leaderboard sync. Side effects only; pure logic in lib/transform.mjs.
|
| 3 |
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
| 4 |
-
import { existsSync } from 'node:fs';
|
| 5 |
import { createHash } from 'node:crypto';
|
| 6 |
import { execFileSync } from 'node:child_process';
|
| 7 |
import { join, dirname } from 'node:path';
|
|
@@ -58,6 +58,35 @@ function git(root, args) {
|
|
| 58 |
return execFileSync('git', args, { cwd: root, encoding: 'utf8' });
|
| 59 |
}
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
async function main() {
|
| 62 |
const args = new Set(process.argv.slice(2));
|
| 63 |
const mode = args.has('--push') ? 'push' : args.has('--apply') ? 'apply' : 'check';
|
|
@@ -105,8 +134,16 @@ async function main() {
|
|
| 105 |
}
|
| 106 |
}
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
// 4. Change detection.
|
| 109 |
-
|
|
|
|
|
|
|
| 110 |
console.log('[sync] no display changes. Done.');
|
| 111 |
// Friday heartbeat: post a card so the Teams channel confirms the job is alive
|
| 112 |
// even during quiet weeks with no ranking changes.
|
|
@@ -129,21 +166,28 @@ async function main() {
|
|
| 129 |
}
|
| 130 |
return;
|
| 131 |
}
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
'
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
}
|
| 145 |
|
| 146 |
if (mode === 'check') {
|
|
|
|
|
|
|
|
|
|
| 147 |
console.log('[sync] --check: no writes. Re-run with --apply (commit) or --push (commit + staging).');
|
| 148 |
return;
|
| 149 |
}
|
|
@@ -162,11 +206,23 @@ async function main() {
|
|
| 162 |
}
|
| 163 |
}
|
| 164 |
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
const today = new Date().toISOString().slice(0, 10);
|
| 169 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
git(root, ['commit', '-m', msg]);
|
| 171 |
console.log('[sync] committed.');
|
| 172 |
|
|
@@ -175,15 +231,23 @@ async function main() {
|
|
| 175 |
try {
|
| 176 |
git(root, ['push', 'origin', 'HEAD:main']);
|
| 177 |
console.log('[sync] pushed to origin (staging).');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
await postTeams({
|
| 179 |
kind: 'success',
|
| 180 |
-
title: 'β
Staging ready for review',
|
| 181 |
subtitle: subtitle(),
|
| 182 |
-
facts:
|
| 183 |
-
['Boards changed', String(changeLines.length)],
|
| 184 |
-
['Detail', changeLines.join(' Β· ')],
|
| 185 |
-
['Next', 'Review staging, then promote to prod'],
|
| 186 |
-
],
|
| 187 |
openUrl: STAGING_URL,
|
| 188 |
});
|
| 189 |
} catch (e) {
|
|
|
|
| 1 |
#!/usr/bin/env node
|
| 2 |
// Orchestrator for the leaderboard sync. Side effects only; pure logic in lib/transform.mjs.
|
| 3 |
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
| 4 |
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
| 5 |
import { createHash } from 'node:crypto';
|
| 6 |
import { execFileSync } from 'node:child_process';
|
| 7 |
import { join, dirname } from 'node:path';
|
|
|
|
| 58 |
return execFileSync('git', args, { cwd: root, encoding: 'utf8' });
|
| 59 |
}
|
| 60 |
|
| 61 |
+
// Quarter helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 62 |
+
|
| 63 |
+
function quarterForDate(d) {
|
| 64 |
+
return `${d.getUTCFullYear()}Q${Math.floor(d.getUTCMonth() / 3) + 1}`;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
// Read index.html, extract the year+quarter label, compare to today's quarter.
|
| 68 |
+
// Returns { needsUpdate, current, expected, html, htmlPath }.
|
| 69 |
+
function detectQuarterLabel(root) {
|
| 70 |
+
const htmlPath = join(root, 'index.html');
|
| 71 |
+
const html = readFileSync(htmlPath, 'utf8');
|
| 72 |
+
const RE = /(\d{4})<span class="text-on-surface-variant text-base font-medium ml-1">(Q[1-4])<\/span>/;
|
| 73 |
+
const m = html.match(RE);
|
| 74 |
+
if (!m) return { needsUpdate: false };
|
| 75 |
+
const current = `${m[1]}${m[2]}`;
|
| 76 |
+
const expected = quarterForDate(new Date());
|
| 77 |
+
return { needsUpdate: current !== expected, current, expected, html, htmlPath };
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
function applyQuarterLabel({ html, htmlPath, expected }) {
|
| 81 |
+
const year = expected.slice(0, 4);
|
| 82 |
+
const q = expected.slice(4);
|
| 83 |
+
const updated = html.replace(
|
| 84 |
+
/(\d{4})<span class="text-on-surface-variant text-base font-medium ml-1">(Q[1-4])<\/span>/,
|
| 85 |
+
`${year}<span class="text-on-surface-variant text-base font-medium ml-1">${q}</span>`,
|
| 86 |
+
);
|
| 87 |
+
writeFileSync(htmlPath, updated);
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
async function main() {
|
| 91 |
const args = new Set(process.argv.slice(2));
|
| 92 |
const mode = args.has('--push') ? 'push' : args.has('--apply') ? 'apply' : 'check';
|
|
|
|
| 134 |
}
|
| 135 |
}
|
| 136 |
|
| 137 |
+
// 3.5. Detect quarter label mismatch in index.html.
|
| 138 |
+
const quarterInfo = detectQuarterLabel(root);
|
| 139 |
+
if (quarterInfo.needsUpdate) {
|
| 140 |
+
console.log(`[sync] quarter label: ${quarterInfo.current} β ${quarterInfo.expected}`);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
// 4. Change detection.
|
| 144 |
+
const leaderboardChanged = hasChanges(oldData, newData);
|
| 145 |
+
|
| 146 |
+
if (!leaderboardChanged && !quarterInfo.needsUpdate) {
|
| 147 |
console.log('[sync] no display changes. Done.');
|
| 148 |
// Friday heartbeat: post a card so the Teams channel confirms the job is alive
|
| 149 |
// even during quiet weeks with no ranking changes.
|
|
|
|
| 166 |
}
|
| 167 |
return;
|
| 168 |
}
|
| 169 |
+
|
| 170 |
+
let changeLines = [];
|
| 171 |
+
if (leaderboardChanged) {
|
| 172 |
+
changeLines = diffBoards(oldData, newData);
|
| 173 |
+
console.log('[sync] changes detected:\n' + changeLines.join('\n'));
|
| 174 |
+
|
| 175 |
+
// 5. Sanity gates (only when leaderboard data changes).
|
| 176 |
+
const issues = validate(newData, oldData);
|
| 177 |
+
if (issues.length) {
|
| 178 |
+
console.error('[sync] sanity gates failed:\n - ' + issues.join('\n - '));
|
| 179 |
+
await notifyFailure(
|
| 180 |
+
[['Reason', 'sanity gates failed'], ...issues.map((s, i) => [`Issue ${i + 1}`, s])],
|
| 181 |
+
'warning',
|
| 182 |
+
);
|
| 183 |
+
process.exit(2);
|
| 184 |
+
}
|
| 185 |
}
|
| 186 |
|
| 187 |
if (mode === 'check') {
|
| 188 |
+
if (quarterInfo.needsUpdate) {
|
| 189 |
+
console.log(`[sync] header quarter: ${quarterInfo.current} β ${quarterInfo.expected}`);
|
| 190 |
+
}
|
| 191 |
console.log('[sync] --check: no writes. Re-run with --apply (commit) or --push (commit + staging).');
|
| 192 |
return;
|
| 193 |
}
|
|
|
|
| 206 |
}
|
| 207 |
}
|
| 208 |
|
| 209 |
+
if (leaderboardChanged) {
|
| 210 |
+
await mkdir(dirname(dataPath), { recursive: true });
|
| 211 |
+
await writeFile(dataPath, JSON.stringify(newData, null, 2) + '\n');
|
| 212 |
+
git(root, ['add', dataPath]);
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
if (quarterInfo.needsUpdate) {
|
| 216 |
+
applyQuarterLabel(quarterInfo);
|
| 217 |
+
git(root, ['add', join(root, 'index.html')]);
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
const today = new Date().toISOString().slice(0, 10);
|
| 221 |
+
const parts = [];
|
| 222 |
+
if (leaderboardChanged) parts.push('EOG + EVA');
|
| 223 |
+
if (quarterInfo.needsUpdate) parts.push(`header β ${quarterInfo.expected}`);
|
| 224 |
+
const commitDetail = leaderboardChanged ? '\n\n' + changeLines.join('\n') : '';
|
| 225 |
+
const msg = `Sync leaderboard: ${parts.join(', ')} ${today}${commitDetail}`;
|
| 226 |
git(root, ['commit', '-m', msg]);
|
| 227 |
console.log('[sync] committed.');
|
| 228 |
|
|
|
|
| 231 |
try {
|
| 232 |
git(root, ['push', 'origin', 'HEAD:main']);
|
| 233 |
console.log('[sync] pushed to origin (staging).');
|
| 234 |
+
const noticeFacts = [];
|
| 235 |
+
if (leaderboardChanged) {
|
| 236 |
+
noticeFacts.push(['Boards changed', String(changeLines.length)]);
|
| 237 |
+
noticeFacts.push(['Detail', changeLines.join(' Β· ')]);
|
| 238 |
+
}
|
| 239 |
+
if (quarterInfo.needsUpdate) {
|
| 240 |
+
noticeFacts.push(['Header updated', `${quarterInfo.current} β ${quarterInfo.expected}`]);
|
| 241 |
+
}
|
| 242 |
+
if (!leaderboardChanged) {
|
| 243 |
+
noticeFacts.push(['Note', 'Quarter label only β no ranking changes']);
|
| 244 |
+
}
|
| 245 |
+
noticeFacts.push(['Next', 'Review staging, then promote to prod']);
|
| 246 |
await postTeams({
|
| 247 |
kind: 'success',
|
| 248 |
+
title: leaderboardChanged ? 'β
Staging ready for review' : 'π
Header updated β staging ready',
|
| 249 |
subtitle: subtitle(),
|
| 250 |
+
facts: noticeFacts,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
openUrl: STAGING_URL,
|
| 252 |
});
|
| 253 |
} catch (e) {
|