Skip to content

Commit 16fd423

Browse files
feat: schema contracts + dashboard freshness — v1.6.0
JSON Schema (draft-07) for all evidence outputs with zero-dep validator in CI. Dashboard now shows pipeline freshness banner, stale data warnings, per-repo status dots, and trend arrows from health-trends.json. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 94c751e commit 16fd423

File tree

12 files changed

+525
-27
lines changed

12 files changed

+525
-27
lines changed

.github/workflows/validate.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,18 @@ jobs:
6060
- name: Verify CLI runs
6161
working-directory: cli
6262
run: node bin/scan.js --version
63+
64+
validate-evidence:
65+
runs-on: ubuntu-latest
66+
67+
steps:
68+
- name: Checkout
69+
uses: actions/checkout@v6
70+
71+
- name: Setup Node.js
72+
uses: actions/setup-node@v4
73+
with:
74+
node-version: '20'
75+
76+
- name: Validate evidence schemas
77+
run: node scripts/validate-evidence.js

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22

33
All notable changes to this project will be documented here.
44

5+
## [1.6.0] — 2026-03-20
6+
7+
### Added
8+
- **JSON Schema contracts**: Draft-07 schemas for all evidence output files — `ecosystem-status`, `health-scores`, `manifest`, `action-queue`. Machine-readable structure definitions in `schemas/` directory.
9+
- **Evidence validator**: Zero-dep Node.js script (`scripts/validate-evidence.js`) validates evidence JSON against schemas in CI. Handles PowerShell BOM, validates types/required/enum/ranges, first 5 items per array.
10+
- **CI validation step**: New `validate-evidence` job in `validate.yml` catches schema violations before merge.
11+
- **Dashboard freshness banner**: Pipeline status badge (success/partial/failed), relative time since last refresh ("2h ago"), step counts ("8/8 steps"). Stale data warning (orange banner) when data >12h old with link to GitHub Actions.
12+
- **Per-repo freshness dots**: Green/yellow/red dots on health cards showing per-repository pipeline status from `manifest.json`.
13+
- **Trend arrows on health cards**: ↑ improving, → stable, ↓ declining with 7-day delta from `health-trends.json`.
14+
- Dashboard now fetches `manifest.json` + `health-trends.json` alongside existing data (5 parallel fetches).
15+
516
## [1.5.0] — 2026-03-20
617

718
### Added

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "oss-health-scan",
3-
"version": "1.5.0",
3+
"version": "1.6.0",
44
"description": "Scan npm dependencies for abandoned packages, outdated versions (libyear), and known CVEs (OSV.dev). Health scores 0-100, SARIF for GitHub Code Scanning, zero dependencies.",
55
"main": "./lib/api.js",
66
"exports": {

docs/ARCHITECTURE.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@ Package exports:
8080
- `critical-count` — number of packages below score 30
8181
- `avg-health` — average health score across tracked packages
8282

83+
## Schema Contracts
84+
85+
JSON Schema (draft-07) files in `schemas/` validate evidence output structure:
86+
87+
- `schemas/ecosystem-status.schema.json` — aggregated ecosystem snapshot
88+
- `schemas/health-scores.schema.json` — per-package health scoring with breakdown
89+
- `schemas/manifest.schema.json` — pipeline run health (steps, per-repo status)
90+
- `schemas/action-queue.schema.json` — prioritized SLA action items
91+
92+
Validated in CI by `scripts/validate-evidence.js` (zero dependencies, handles PowerShell BOM).
93+
8394
## Design Principles
8495

8596
- One source of truth for tracked repositories.

docs/ROADMAP.md

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,20 @@
2525
- ✅ CLI unit tests and smoke test in CI
2626
- ✅ Structural validation (config, markers, scripts, docs)
2727

28-
## Next High-Impact Engineering Moves
29-
30-
### 1. Schema Contracts
31-
32-
Current gap: outputs are validated structurally by scripts and markers, but not by formal JSON schemas.
33-
34-
Best next steps:
35-
36-
- add JSON Schema for each output family (ecosystem-status, health-scores, manifest)
37-
- validate generated evidence in CI before commit
38-
- keep backward compatibility explicit with schema versioning
28+
### Schema Contracts
29+
- ✅ JSON Schema (draft-07) for all evidence output families: ecosystem-status, health-scores, manifest, action-queue
30+
- ✅ Zero-dep Node.js validator (`scripts/validate-evidence.js`) — runs in CI, strips BOM, validates types/required/enum/ranges
31+
- ✅ CI step `validate-evidence` in validate.yml workflow
32+
- ✅ PowerShell single-item quirk documented in action-queue schema (object | array)
33+
34+
### Dashboard Freshness
35+
- ✅ Freshness banner: pipeline status badge (success/partial/failed), relative time, step counts
36+
- ✅ Stale data warning: orange banner when data >12h old with link to Actions
37+
- ✅ Per-repo freshness dots on health cards (green = fresh, yellow = partial, red = failed)
38+
- ✅ Trend arrows on health cards from health-trends.json (↑ improving, → stable, ↓ declining with 7d delta)
39+
- ✅ manifest.json + health-trends.json fetched alongside existing data
3940

40-
### 2. Dashboard Freshness
41-
42-
Current gap: dashboard consumes evidence JSON but doesn't surface staleness or partial-failure state.
43-
44-
Best next steps:
45-
46-
- consume manifest.json to show per-repo freshness status
47-
- add visual indicators for stale data (>12h since last refresh)
48-
- expose failing steps and link to manifest details
41+
## Next High-Impact Engineering Moves
4942

5043
## What Not To Prioritize
5144

index.html

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,60 @@
273273
font-size: 0.75rem;
274274
}
275275

276+
/* ── Freshness Banner ──────────────────────────────────── */
277+
.freshness-banner {
278+
background: var(--surface);
279+
border: 1px solid var(--border);
280+
border-radius: 12px;
281+
padding: 14px 20px;
282+
margin-bottom: 24px;
283+
display: flex;
284+
align-items: center;
285+
justify-content: space-between;
286+
flex-wrap: wrap;
287+
gap: 8px;
288+
font-size: 0.8rem;
289+
}
290+
.freshness-left { display: flex; align-items: center; gap: 10px; }
291+
.pipe-badge {
292+
padding: 3px 10px; border-radius: 20px;
293+
font-size: 0.68rem; font-weight: 600;
294+
text-transform: uppercase; letter-spacing: 0.04em;
295+
}
296+
.pipe-badge.success { background: var(--green-dim); color: var(--green); }
297+
.pipe-badge.partial { background: var(--yellow-dim); color: var(--yellow); }
298+
.pipe-badge.failed { background: var(--red-dim); color: var(--red); }
299+
.freshness-right { color: var(--muted); font-size: 0.72rem; }
300+
301+
.stale-banner {
302+
background: rgba(234,179,8,0.08);
303+
border: 1px solid rgba(234,179,8,0.25);
304+
border-radius: 12px;
305+
padding: 12px 20px;
306+
margin-bottom: 16px;
307+
color: var(--yellow);
308+
font-size: 0.8rem;
309+
font-weight: 500;
310+
}
311+
312+
/* ── Trend & Freshness on Cards ──────────────────────── */
313+
.trend-tag {
314+
font-size: 0.68rem; font-weight: 600;
315+
display: inline-flex; align-items: center; gap: 3px;
316+
padding: 2px 8px; border-radius: 10px;
317+
margin-top: 6px;
318+
}
319+
.trend-tag.up { background: var(--green-dim); color: var(--green); }
320+
.trend-tag.down { background: var(--red-dim); color: var(--red); }
321+
.trend-tag.stable { background: rgba(100,116,139,0.12); color: var(--muted); }
322+
.repo-status-dot {
323+
width: 7px; height: 7px; border-radius: 50%;
324+
display: inline-block; margin-right: 4px; flex-shrink: 0;
325+
}
326+
.repo-status-dot.fresh { background: var(--green); }
327+
.repo-status-dot.partial { background: var(--yellow); }
328+
.repo-status-dot.failed { background: var(--red); }
329+
276330
.loading { color: var(--muted); font-size: 0.85rem; padding: 24px; text-align: center; }
277331
.error { color: var(--red); font-size: 0.85rem; padding: 24px; }
278332

@@ -319,6 +373,9 @@ <h1>OSS Maintenance Log</h1>
319373
<div class="stat"><div class="label">Open PRs</div><div class="value purple" id="s-prs"></div></div>
320374
</div>
321375

376+
<!-- Freshness Banner -->
377+
<div id="freshness-wrap"></div>
378+
322379
<!-- Health Score Cards -->
323380
<section>
324381
<h2>Package Health Scores</h2>
@@ -367,6 +424,16 @@ <h2>Action Queue</h2>
367424
return String(n);
368425
}
369426

427+
function relTime(dateStr) {
428+
const ms = Date.now() - new Date(dateStr).getTime();
429+
const m = Math.floor(ms / 60000);
430+
if (m < 1) return 'just now';
431+
if (m < 60) return m + 'm ago';
432+
const h = Math.floor(m / 60);
433+
if (h < 24) return h + 'h ago';
434+
return Math.floor(h / 24) + 'd ago';
435+
}
436+
370437
function scoreColor(score) {
371438
if (score < 30) return COLORS.red;
372439
if (score < 60) return COLORS.yellow;
@@ -400,15 +467,30 @@ <h2>Action Queue</h2>
400467
</div>`;
401468
}
402469

403-
function healthCard(s, i) {
470+
function trendTag(trend) {
471+
if (!trend) return '';
472+
const t = trend.trend || 'stable';
473+
const d7 = trend.delta_7d;
474+
const arrow = t === 'improving' ? '↑' : t === 'declining' ? '↓' : '→';
475+
const cls = t === 'improving' ? 'up' : t === 'declining' ? 'down' : 'stable';
476+
const delta = d7 != null ? ` ${d7 > 0 ? '+' : ''}${d7.toFixed(1)}` : '';
477+
return `<span class="trend-tag ${cls}">${arrow}${delta}</span>`;
478+
}
479+
480+
function healthCard(s, i, trendMap, repoStatusMap) {
404481
const color = scoreColor(s.health_score);
405482
const b = s.breakdown;
483+
const key = s.owner + '/' + s.repo;
484+
const trend = trendMap && trendMap[key];
485+
const repoStatus = repoStatusMap && repoStatusMap[key];
486+
const dot = repoStatus ? `<span class="repo-status-dot ${repoStatus}" title="${repoStatus}"></span>` : '';
406487
return `<div class="health-card animate-in" style="animation-delay:${i * 0.08}s">
407488
<div class="card-header">
408489
<div>
409-
<div class="pkg-owner">${s.owner}</div>
490+
<div class="pkg-owner">${dot}${s.owner}</div>
410491
<div class="pkg-name"><a href="${s.repo_url}" target="_blank">${s.repo}</a></div>
411492
${riskBadge(s.risk_level)}
493+
${trendTag(trend)}
412494
</div>
413495
${scoreCircle(s.health_score, color)}
414496
</div>
@@ -428,10 +510,12 @@ <h2>Action Queue</h2>
428510

429511
async function load() {
430512
try {
431-
const [eco, health, queue] = await Promise.all([
513+
const [eco, health, queue, manifest, trends] = await Promise.all([
432514
fetch(RAW + 'ecosystem-status.json').then(r => r.json()),
433515
fetch(RAW + 'health-scores.json').then(r => r.json()).catch(() => null),
434-
fetch(RAW + 'action-queue.json').then(r => r.json()).catch(() => null)
516+
fetch(RAW + 'action-queue.json').then(r => r.json()).catch(() => null),
517+
fetch(RAW + 'manifest.json').then(r => r.json()).catch(() => null),
518+
fetch(RAW + 'health-trends.json').then(r => r.json()).catch(() => null)
435519
]);
436520

437521
const sm = eco.summary || {};
@@ -451,11 +535,46 @@ <h2>Action Queue</h2>
451535
'Last updated: ' + new Date(eco.generated_at_utc).toUTCString();
452536
}
453537

538+
// Freshness banner
539+
const fw = document.getElementById('freshness-wrap');
540+
if (manifest) {
541+
const ts = manifest.run_completed_at_utc || manifest.run_started_at_utc;
542+
const ms = manifest.summary || {};
543+
const status = manifest.run_status || 'unknown';
544+
const stepsInfo = ms.total_steps ? `${ms.successful_steps}/${ms.total_steps} steps` : '';
545+
const failInfo = ms.failed_steps > 0 ? `, ${ms.failed_steps} failed` : '';
546+
const ageMs = ts ? Date.now() - new Date(ts).getTime() : 0;
547+
const stale = ageMs > 12 * 3600 * 1000;
548+
549+
let html = '';
550+
if (stale) {
551+
html += `<div class="stale-banner">Data is ${relTime(ts)} old — pipeline may not be running. <a href="https://github.com/dusan-maintains/oss-maintenance-log/actions/workflows/evidence-daily.yml">Check Actions</a></div>`;
552+
}
553+
html += `<div class="freshness-banner">
554+
<div class="freshness-left">
555+
<span class="pipe-badge ${status}">${status}</span>
556+
<span style="color:var(--text)">Pipeline refreshed ${ts ? relTime(ts) : '—'}</span>
557+
</div>
558+
<div class="freshness-right">${stepsInfo}${failInfo}</div>
559+
</div>`;
560+
fw.innerHTML = html;
561+
}
562+
563+
// Build trend + repo-status maps
564+
const trendMap = {};
565+
if (trends && trends.trends) {
566+
for (const t of trends.trends) trendMap[t.repo] = t;
567+
}
568+
const repoStatusMap = {};
569+
if (manifest && manifest.repositories) {
570+
for (const r of manifest.repositories) repoStatusMap[r.repository] = r.status;
571+
}
572+
454573
// Health cards
455574
if (health && health.scores) {
456575
const sorted = [...health.scores].sort((a,b) => a.health_score - b.health_score);
457576
document.getElementById('health-cards').innerHTML =
458-
sorted.map((s, i) => healthCard(s, i)).join('');
577+
sorted.map((s, i) => healthCard(s, i, trendMap, repoStatusMap)).join('');
459578
} else {
460579
// Fallback: show ecosystem data without health scores
461580
document.getElementById('health-cards').innerHTML =

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "oss-health-scan",
3-
"version": "1.5.0",
3+
"version": "1.6.0",
44
"private": true,
55
"description": "Scan npm dependencies for abandoned/unhealthy packages. Get health scores (0-100).",
66
"bin": {

schemas/action-queue.schema.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "Action Queue",
4+
"description": "Cross-repo prioritized action list built from SLA outputs.",
5+
"type": "object",
6+
"required": ["generated_at_utc", "open_actions", "urgent_actions", "items"],
7+
"properties": {
8+
"generated_at_utc": { "type": "string" },
9+
"open_actions": { "type": "number", "minimum": 0 },
10+
"urgent_actions": { "type": "number", "minimum": 0 },
11+
"items": {
12+
"description": "Single object when 1 item (PowerShell quirk), array when multiple",
13+
"type": ["array", "object"],
14+
"items": {
15+
"type": "object",
16+
"required": ["repository", "pr_number"],
17+
"properties": {
18+
"repository": { "type": "string" },
19+
"pr_number": { "type": "number" },
20+
"pr_title": { "type": "string" },
21+
"pr_url": { "type": "string" },
22+
"hours_since_last_external": { "type": "number" },
23+
"overdue_sla": { "type": "boolean" },
24+
"priority": { "type": "string" }
25+
}
26+
}
27+
}
28+
}
29+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "Ecosystem Status",
4+
"description": "Aggregated snapshot across all tracked repositories. Generated by update-all-evidence.ps1.",
5+
"type": "object",
6+
"required": ["generated_at_utc", "summary", "projects"],
7+
"properties": {
8+
"generated_at_utc": { "type": "string" },
9+
"summary": {
10+
"type": "object",
11+
"required": ["tracked_projects", "tracked_prs_total", "tracked_prs_open", "total_stars", "total_forks", "total_npm_downloads_last_week"],
12+
"properties": {
13+
"tracked_projects": { "type": "number", "minimum": 0 },
14+
"tracked_prs_total": { "type": "number", "minimum": 0 },
15+
"tracked_prs_open": { "type": "number", "minimum": 0 },
16+
"total_stars": { "type": "number", "minimum": 0 },
17+
"total_forks": { "type": "number", "minimum": 0 },
18+
"total_npm_downloads_last_week": { "type": "number", "minimum": 0 }
19+
}
20+
},
21+
"projects": {
22+
"type": "array",
23+
"minItems": 1,
24+
"items": {
25+
"type": "object",
26+
"required": ["owner", "repo"],
27+
"properties": {
28+
"owner": { "type": "string" },
29+
"repo": { "type": "string" },
30+
"repo_url": { "type": "string" },
31+
"stars": { "type": "number" },
32+
"forks": { "type": "number" },
33+
"open_issues": { "type": "number" },
34+
"pushed_at": { "type": "string" },
35+
"package": { "type": ["string", "null"] },
36+
"npm_downloads_last_week": { "type": "number" },
37+
"status_label": { "type": "string" },
38+
"tracked_pr_numbers": { "type": "array", "items": { "type": "number" } },
39+
"tracked_prs_open": { "type": "number" },
40+
"tracked_pr_details": { "type": "array" }
41+
}
42+
}
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)