From 19767a30c6f6a147027b5e4c1a7ebe42a112d536 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:40:07 +0300 Subject: [PATCH 1/5] feat: add history change intelligence --- dashboard/lib/history-events.mjs | 47 ++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 dashboard/lib/history-events.mjs diff --git a/dashboard/lib/history-events.mjs b/dashboard/lib/history-events.mjs new file mode 100644 index 0000000..c366d6a --- /dev/null +++ b/dashboard/lib/history-events.mjs @@ -0,0 +1,47 @@ +const DIMENSION_LABELS = { + readme: 'README quality', + activity: 'activity', + freshness: 'freshness', + docs: 'documentation', + ci: 'CI/CD', + issues: 'issue maintenance', + community: 'community signal', + pr_velocity: 'PR maintenance', + security: 'security', +} + +export function buildHistoryEvents(previous, current) { + if (!previous) return ['Initial DevLens baseline recorded'] + const events = [] + const delta = current.score - previous.score + if (Math.abs(delta) >= 5) events.push(`Health ${delta > 0 ? 'improved' : 'declined'} by ${Math.abs(delta)} points`) + + for (const [key, label] of Object.entries(DIMENSION_LABELS)) { + const before = previous.scores?.[key] + const after = current.scores?.[key] + if (Number.isFinite(before) && Number.isFinite(after) && Math.abs(after - before) >= 10) { + events.push(`${label} ${after > before ? 'improved' : 'declined'} by ${Math.abs(after - before)} points`) + } + } + + const previousVulns = previous.advisory?.total ?? 0 + const currentVulns = current.advisory?.total ?? 0 + if (currentVulns !== previousVulns) { + events.push(`Known vulnerabilities ${currentVulns > previousVulns ? 'increased' : 'decreased'} from ${previousVulns} to ${currentVulns}`) + } + + const previousCritical = previous.advisory?.critical ?? 0 + const currentCritical = current.advisory?.critical ?? 0 + if (currentCritical !== previousCritical) { + events.push(`Critical vulnerabilities ${currentCritical > previousCritical ? 'increased' : 'decreased'} from ${previousCritical} to ${currentCritical}`) + } + + return events.slice(0, 6) +} + +export function shouldStartNewSnapshot(lastDate, now = Date.now()) { + if (!lastDate) return true + const timestamp = Date.parse(lastDate) + if (!Number.isFinite(timestamp)) return true + return now - timestamp >= 7 * 86400000 +} From dd6b18f1ba845002a73cfb861e82e45f93306509 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:40:09 +0300 Subject: [PATCH 2/5] test: cover history event detection --- dashboard/scripts/history-events.test.mjs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 dashboard/scripts/history-events.test.mjs diff --git a/dashboard/scripts/history-events.test.mjs b/dashboard/scripts/history-events.test.mjs new file mode 100644 index 0000000..e194ca1 --- /dev/null +++ b/dashboard/scripts/history-events.test.mjs @@ -0,0 +1,20 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { buildHistoryEvents, shouldStartNewSnapshot } from '../lib/history-events.mjs' + +test('history events identify meaningful health and security changes', () => { + const events = buildHistoryEvents( + { score: 70, scores: { security: 60, docs: 50 }, advisory: { total: 4, critical: 1 } }, + { score: 78, scores: { security: 75, docs: 40 }, advisory: { total: 2, critical: 0 } }, + ) + assert.ok(events.some(e => e.includes('Health improved'))) + assert.ok(events.some(e => e.includes('security improved'))) + assert.ok(events.some(e => e.includes('vulnerabilities decreased'))) +}) + +test('history snapshots remain weekly rather than per-request', () => { + const now = Date.parse('2026-09-26T00:00:00Z') + assert.equal(shouldStartNewSnapshot('2026-09-20T00:00:00Z', now), false) + assert.equal(shouldStartNewSnapshot('2026-09-18T00:00:00Z', now), true) + assert.equal(shouldStartNewSnapshot(null, now), true) +}) From 39ec0f708f7d4307fffa74e3dc39c021a9739de1 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:40:18 +0300 Subject: [PATCH 3/5] feat: persist weekly dimension history and events --- dashboard/lib/scorer.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/dashboard/lib/scorer.ts b/dashboard/lib/scorer.ts index 5d9b680..d914201 100644 --- a/dashboard/lib/scorer.ts +++ b/dashboard/lib/scorer.ts @@ -3,6 +3,7 @@ import { getRedis } from './redis' import { runAdvisoryCheck } from './advisory' import { sanitizeWeights } from './weights.mjs' import { scoreActivitySignals, scoreIssueMaintenance, scorePRMaintenance } from './scoring-metrics.mjs' +import { buildHistoryEvents, shouldStartNewSnapshot } from './history-events.mjs' export interface DimScores { readme: number @@ -349,12 +350,27 @@ export async function analyzeRepo( await redis.set(cacheKey, JSON.stringify(report), { ex: 900 }) const histKey = `history:${owner}:${name}` const existing = await redis.get(histKey) - const histArr: { week: string; score: number; date: string }[] = existing + const histArr: any[] = existing ? (typeof existing === 'string' ? JSON.parse(existing) : existing) : [] - histArr.push({ week: `W${new Date().toISOString().slice(5, 10)}`, score: health, date: new Date().toISOString() }) - await redis.set(histKey, JSON.stringify(histArr.slice(-12))) - } catch {} + const now = new Date() + const snapshot = { + week: `W${now.toISOString().slice(5, 10)}`, + score: health, + date: now.toISOString(), + scores, + advisory: secResult.advisory, + } + const last = histArr[histArr.length - 1] + if (shouldStartNewSnapshot(last?.date, now.getTime())) { + snapshot.events = buildHistoryEvents(last, snapshot) + histArr.push(snapshot) + } else { + snapshot.events = buildHistoryEvents(histArr.length > 1 ? histArr[histArr.length - 2] : undefined, snapshot) + if (histArr.length > 0) histArr[histArr.length - 1] = snapshot + else histArr.push(snapshot) + } + await redis.set(histKey, JSON.stringify(histArr.slice(-12))) } catch {} } return report From ff7bbb3239951c33dcfb40cd07d164d2b9835910 Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:40:24 +0300 Subject: [PATCH 4/5] ui: show historical change intelligence --- dashboard/app/report/[owner]/[repo]/page.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/dashboard/app/report/[owner]/[repo]/page.tsx b/dashboard/app/report/[owner]/[repo]/page.tsx index 87def99..246f1a5 100644 --- a/dashboard/app/report/[owner]/[repo]/page.tsx +++ b/dashboard/app/report/[owner]/[repo]/page.tsx @@ -16,7 +16,7 @@ export default function ReportPage() { const owner = params.owner as string const repo = params.repo as string const [report, setReport] = useState(null) - const [history, setHistory] = useState<{ week: string; score: number }[]>([]) + const [history, setHistory] = useState<{ week: string; score: number; events?: string[] }[]>([]) const [loading, setLoading] = useState(true) const [error, setError] = useState('') const [copied, setCopied] = useState(false) @@ -101,6 +101,19 @@ export default function ReportPage() { )} + {history.some(h => h.events?.length) && ( +
+

Change Intelligence

+
+ {history.slice().reverse().flatMap(h => (h.events ?? []).map(event => ( +
+ {h.week} + {event} +
+ )))} +
+
+ )} {report.suggestions.length > 0 && (

💡 How to improve this repo

From 82c6d86f97ce0ccce0b70037370f91ee43c4cdcf Mon Sep 17 00:00:00 2001 From: Ossama Hashim Date: Sat, 26 Sep 2026 20:41:30 +0300 Subject: [PATCH 5/5] fix: type weekly history events --- dashboard/lib/scorer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/lib/scorer.ts b/dashboard/lib/scorer.ts index d914201..d2ee5d3 100644 --- a/dashboard/lib/scorer.ts +++ b/dashboard/lib/scorer.ts @@ -354,7 +354,7 @@ export async function analyzeRepo( ? (typeof existing === 'string' ? JSON.parse(existing) : existing) : [] const now = new Date() - const snapshot = { + const snapshot: any = { week: `W${now.toISOString().slice(5, 10)}`, score: health, date: now.toISOString(),