bradsansnow Claude Sonnet 4.6 commited on
Commit
5661ed6
Β·
1 Parent(s): 0bc5d1a

Add quarter label auto-update to leaderboard sync

Browse files

Sync 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
- if (!hasChanges(oldData, newData)) {
 
 
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
- const changeLines = diffBoards(oldData, newData);
133
- console.log('[sync] changes detected:\n' + changeLines.join('\n'));
134
-
135
- // 5. Sanity gates.
136
- const issues = validate(newData, oldData);
137
- if (issues.length) {
138
- console.error('[sync] sanity gates failed:\n - ' + issues.join('\n - '));
139
- await notifyFailure(
140
- [['Reason', 'sanity gates failed'], ...issues.map((s, i) => [`Issue ${i + 1}`, s])],
141
- 'warning',
142
- );
143
- process.exit(2);
 
 
 
 
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
- await mkdir(dirname(dataPath), { recursive: true });
166
- await writeFile(dataPath, JSON.stringify(newData, null, 2) + '\n');
167
- git(root, ['add', dataPath]);
 
 
 
 
 
 
 
 
168
  const today = new Date().toISOString().slice(0, 10);
169
- const msg = `Sync leaderboard: EOG + EVA ${today}\n\n${changeLines.join('\n')}`;
 
 
 
 
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) {