Skip to content

Commit 7e100b6

Browse files
authored
Merge pull request #65 from letsgogeeky/feat/AIC-96
feat(cron): AIC-96 — transition events + richer snapshot data
2 parents 9a4687c + c0c1e3a commit 7e100b6

2 files changed

Lines changed: 84 additions & 7 deletions

File tree

app/api/cron/sync-health/route.ts

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export async function GET(request: Request) {
7070

7171
const { data: tools, error: toolsError } = await db
7272
.from("tools")
73-
.select("id, name, github_url")
73+
.select("id, name, github_url, health_score, is_stale")
7474
.not("github_url", "is", null)
7575
.or(`last_synced_at.is.null,last_synced_at.lt.${oneHourAgo}`);
7676

@@ -83,6 +83,7 @@ export async function GET(request: Request) {
8383
let processed = 0;
8484
let skipped = 0;
8585
let errors = 0;
86+
let eventsWritten = 0;
8687

8788
for (const tool of tools) {
8889
if (!tool.github_url) {
@@ -97,32 +98,94 @@ export async function GET(request: Request) {
9798
continue;
9899
}
99100

100-
// Find snapshot closest to 30 days ago for stars momentum
101+
// Find snapshot closest to 30 days ago for stars momentum + archived baseline
101102
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
102103
const { data: prevSnapshot } = await db
103104
.from("tool_snapshots")
104-
.select("stars")
105+
.select("stars, archived")
105106
.eq("tool_id", tool.id)
106107
.lte("recorded_at", thirtyDaysAgo)
107108
.order("recorded_at", { ascending: false })
108109
.limit(1)
109110
.single();
110111

111112
const prevStars = prevSnapshot?.stars ?? null;
113+
const starsDelta = prevStars !== null ? ghData.stars - prevStars : null;
112114
const healthScore = computeHealthScore(ghData, prevStars);
113115

114116
const daysSinceCommit =
115117
(Date.now() - new Date(ghData.last_commit_at).getTime()) / (1000 * 60 * 60 * 24);
116118
const isStale = ghData.archived || daysSinceCommit > 90;
117119
const now = new Date().toISOString();
118120

121+
// ── Transition events ──────────────────────────────────────────────────
122+
123+
// Health score change (≥10 point swing)
124+
const oldScore = tool.health_score ?? null;
125+
if (oldScore !== null && Math.abs(healthScore - oldScore) >= 10) {
126+
const { error: eventError } = await db.from("tool_events").insert({
127+
tool_id: tool.id,
128+
type: "health_score_change",
129+
metadata: { old_score: oldScore, new_score: healthScore, delta: healthScore - oldScore },
130+
});
131+
if (eventError) {
132+
console.error(
133+
`[sync-health] ✗ ${tool.name} — health_score_change event failed: ${eventError.message}`
134+
);
135+
} else {
136+
eventsWritten++;
137+
}
138+
}
139+
140+
// Stale transition (false → true)
141+
if (!tool.is_stale && isStale) {
142+
const { error: eventError } = await db.from("tool_events").insert({
143+
tool_id: tool.id,
144+
type: "stale_transition",
145+
metadata: {
146+
archived: ghData.archived,
147+
days_since_commit: Math.floor(daysSinceCommit),
148+
},
149+
});
150+
if (eventError) {
151+
console.error(
152+
`[sync-health] ✗ ${tool.name} — stale_transition event failed: ${eventError.message}`
153+
);
154+
} else {
155+
eventsWritten++;
156+
console.log(`[sync-health] ⚠ ${tool.name} — stale transition detected`);
157+
}
158+
}
159+
160+
// Archived transition (previously not archived → now archived)
161+
const wasArchived = prevSnapshot?.archived ?? false;
162+
if (!wasArchived && ghData.archived) {
163+
const { error: eventError } = await db.from("tool_events").insert({
164+
tool_id: tool.id,
165+
type: "archived_detected",
166+
metadata: {},
167+
});
168+
if (eventError) {
169+
console.error(
170+
`[sync-health] ✗ ${tool.name} — archived_detected event failed: ${eventError.message}`
171+
);
172+
} else {
173+
eventsWritten++;
174+
console.log(`[sync-health] 🗄 ${tool.name} — archived on GitHub`);
175+
}
176+
}
177+
178+
// ── Snapshot insert ────────────────────────────────────────────────────
179+
119180
const { error: snapshotError } = await db.from("tool_snapshots").insert({
120181
tool_id: tool.id,
121182
stars: ghData.stars,
122183
last_commit_at: ghData.last_commit_at,
123184
open_issues: ghData.open_issues,
124185
forks: ghData.forks,
125186
archived: ghData.archived,
187+
health_score: healthScore,
188+
stars_delta: starsDelta,
126189
});
127190

128191
if (snapshotError) {
@@ -146,17 +209,17 @@ export async function GET(request: Request) {
146209

147210
const starsDisplay =
148211
ghData.stars >= 1000 ? `${(ghData.stars / 1000).toFixed(0)}k` : String(ghData.stars);
212+
const deltaDisplay =
213+
starsDelta !== null ? ` (${starsDelta >= 0 ? "+" : ""}${starsDelta} vs 30d)` : "";
149214
const dayLabel =
150215
Math.floor(daysSinceCommit) === 1 ? "1d ago" : `${Math.floor(daysSinceCommit)}d ago`;
151216
console.log(
152-
`[sync-health] ✓ ${tool.name} (score: ${healthScore}, stars: ${starsDisplay}, last_commit: ${dayLabel})`
217+
`[sync-health] ✓ ${tool.name} (score: ${healthScore}, stars: ${starsDisplay}${deltaDisplay}, last_commit: ${dayLabel})`
153218
);
154219
processed++;
155220
}
156221

157222
// ── Pricing change detection ──────────────────────────────────────────────
158-
// Fetches ALL tools, hashes their pricing JSON, and records a tool_event
159-
// when the hash differs from what's stored. First run sets the baseline.
160223

161224
const { data: allTools, error: allToolsError } = await db
162225
.from("tools")
@@ -194,6 +257,7 @@ export async function GET(request: Request) {
194257
await db.from("tools").update({ pricing_hash: newHash }).eq("id", tool.id);
195258
console.log(`[sync-health] pricing change detected: ${tool.name}`);
196259
pricingChanged++;
260+
eventsWritten++;
197261
}
198262
}
199263

@@ -204,13 +268,14 @@ export async function GET(request: Request) {
204268

205269
const duration_ms = Date.now() - startTime;
206270
console.log(
207-
`[sync-health] Done — processed: ${processed}, skipped: ${skipped}, errors: ${errors}, duration: ${duration_ms}ms`
271+
`[sync-health] Done — processed: ${processed}, skipped: ${skipped}, errors: ${errors}, events: ${eventsWritten}, duration: ${duration_ms}ms`
208272
);
209273

210274
return Response.json({
211275
processed,
212276
skipped,
213277
errors,
278+
events_written: eventsWritten,
214279
pricing_checked: pricingChecked,
215280
pricing_changed: pricingChanged,
216281
duration_ms,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
-- ============================================================
2+
-- AIchitect — Enrich tool_snapshots
3+
-- Migration: 20260329000000_enrich_tool_snapshots
4+
--
5+
-- AIC-96: add health_score + stars_delta to tool_snapshots so
6+
-- the sync pipeline can persist historical score trends and
7+
-- actual star velocity instead of just direction.
8+
-- ============================================================
9+
10+
ALTER TABLE tool_snapshots
11+
ADD COLUMN health_score integer CHECK (health_score BETWEEN 0 AND 100),
12+
ADD COLUMN stars_delta integer; -- null on first snapshot for a tool; computed vs 30d-prior snapshot

0 commit comments

Comments
 (0)