v0.3.0: Gap-to-Draft pipeline, Living Standards Observatory, blog series

Gap-to-Draft Pipeline (ietf pipeline):
- Context builder assembles ideas, RFC foundations, similar drafts, ecosystem vision
- Generator produces outlines + sections using rich context with Claude
- Quality gates: novelty (embedding similarity), references, format, self-rating
- Family coordinator generates 5-draft ecosystem (AEM/ATD/HITL/AEPB/APAE)
- I-D formatter with proper headers, references, 72-char wrapping

Living Standards Observatory (ietf observatory):
- Source abstraction with IETF + W3C fetchers
- 7-step update pipeline: snapshot, fetch, analyze, embed, ideas, gaps, record
- Static GitHub Pages dashboard (explorer, gap tracker, timeline)
- Weekly CI/CD automation via GitHub Actions

Also includes:
- 361 drafts (expanded from 260 with 6 new keywords), 403 authors, 1,262 ideas, 12 gaps
- Blog series (8 posts planned), reports, arXiv paper figures
- Agent team infrastructure (CLAUDE.md, scripts, dev journal)
- 5 new DB tables, schema migration, ~15 new query methods

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 00:48:57 +01:00
parent be9cf9c5d9
commit d6beb9c0a0
87 changed files with 24471 additions and 401 deletions

162
docs/assets/style.css Normal file
View File

@@ -0,0 +1,162 @@
:root {
--bg: #f5f7fa;
--card-bg: #ffffff;
--text: #1a1a2e;
--text-dim: #666;
--accent: #4a6cf7;
--accent-light: rgba(74,108,247,0.1);
--green: #10b981;
--orange: #f59e0b;
--red: #ef4444;
--border: #e5e7eb;
--shadow: 0 1px 4px rgba(0,0,0,0.08);
--radius: 10px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg); color: var(--text);
line-height: 1.5;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* Layout */
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.header {
background: var(--card-bg); border-bottom: 1px solid var(--border);
padding: 16px 0; margin-bottom: 24px;
}
.header .container { display: flex; align-items: center; justify-content: space-between; }
.header h1 { font-size: 1.3rem; }
.header nav { display: flex; gap: 20px; font-size: 0.9rem; }
.header nav a { color: var(--text-dim); font-weight: 500; }
.header nav a:hover, .header nav a.active { color: var(--accent); text-decoration: none; }
/* Cards */
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
.card {
background: var(--card-bg); border-radius: var(--radius);
padding: 20px; box-shadow: var(--shadow);
}
.card .label { font-size: 0.8rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.5px; }
.card .value { font-size: 2rem; font-weight: 700; margin-top: 4px; }
.card .sub { font-size: 0.8rem; color: var(--text-dim); margin-top: 4px; }
/* Tables */
.panel {
background: var(--card-bg); border-radius: var(--radius);
box-shadow: var(--shadow); overflow: hidden; margin-bottom: 24px;
}
.panel-header { padding: 16px 20px; border-bottom: 1px solid var(--border); font-weight: 600; }
table { width: 100%; border-collapse: collapse; }
th {
background: #f8f9fb; padding: 10px 12px; text-align: left;
font-size: 0.78rem; color: var(--text-dim); cursor: pointer; user-select: none;
white-space: nowrap; border-bottom: 2px solid var(--border);
}
th:hover { color: var(--accent); }
td { padding: 10px 12px; border-bottom: 1px solid #f0f0f0; font-size: 0.83rem; vertical-align: top; }
tr:hover { background: #fafbff; }
/* Controls */
.controls {
background: var(--card-bg); border-radius: var(--radius);
padding: 16px 20px; margin-bottom: 16px; box-shadow: var(--shadow);
}
.controls-row { display: flex; gap: 16px; align-items: center; flex-wrap: wrap; margin-bottom: 10px; }
.controls-row:last-child { margin-bottom: 0; }
.search-box {
flex: 1; min-width: 250px; padding: 8px 14px;
border: 1px solid var(--border); border-radius: 6px;
font-size: 0.9rem; outline: none;
}
.search-box:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-light); }
.slider-group { display: flex; align-items: center; gap: 6px; font-size: 0.8rem; color: var(--text-dim); }
.slider-group input[type=range] { width: 100px; cursor: pointer; }
.slider-val { font-weight: 600; min-width: 24px; text-align: center; }
/* Chips */
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
display: inline-block; padding: 3px 10px; border-radius: 12px;
font-size: 0.75rem; cursor: pointer; border: 1px solid var(--border);
background: var(--card-bg); transition: all 0.15s; user-select: none;
}
.chip.active { background: var(--accent); color: #fff; border-color: var(--accent); }
.chip:hover { border-color: var(--accent); }
/* Badges */
.score-badge {
display: inline-block; padding: 2px 8px; border-radius: 10px;
font-weight: 600; font-size: 0.8rem;
}
.score-high { background: #d4edda; color: #155724; }
.score-mid { background: #fff3cd; color: #856404; }
.score-low { background: #f8d7da; color: #721c24; }
.cat-badge {
display: inline-block; padding: 1px 7px; border-radius: 8px;
font-size: 0.68rem; margin: 1px 2px; background: #e8eaf6; color: #3949ab;
}
.source-badge {
display: inline-block; padding: 1px 7px; border-radius: 8px;
font-size: 0.68rem; margin: 1px 2px;
}
.source-ietf { background: #e3f2fd; color: #1565c0; }
.source-w3c { background: #fce4ec; color: #c62828; }
/* Severity */
.sev-critical { color: var(--red); font-weight: 600; }
.sev-high { color: var(--orange); font-weight: 600; }
.sev-medium { color: var(--text); }
.sev-low { color: var(--text-dim); }
/* Bar */
.bar { display: inline-block; height: 10px; border-radius: 3px; background: var(--accent); vertical-align: middle; }
/* Detail */
.detail-row td { padding: 12px 20px; background: #f8faff; }
.detail-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; max-width: 800px; }
.detail-item { font-size: 0.82rem; }
.detail-item strong { color: #333; }
.detail-item .note { color: var(--text-dim); font-size: 0.78rem; }
.summary-text { font-size: 0.82rem; color: #444; margin-top: 6px; line-height: 1.4; }
/* Chart container */
.chart-container {
background: var(--card-bg); border-radius: var(--radius);
box-shadow: var(--shadow); padding: 20px; margin-bottom: 24px;
}
/* Gap cards */
.gap-card {
background: var(--card-bg); border-radius: var(--radius);
border-left: 4px solid var(--accent); padding: 16px 20px;
box-shadow: var(--shadow); margin-bottom: 12px;
}
.gap-card h3 { font-size: 0.95rem; margin-bottom: 4px; }
.gap-card p { font-size: 0.83rem; color: var(--text-dim); margin-bottom: 4px; }
.gap-card .meta { font-size: 0.75rem; color: var(--text-dim); }
.gap-card.critical { border-left-color: var(--red); }
.gap-card.high { border-left-color: var(--orange); }
.dim { font-size: 0.75rem; color: var(--text-dim); }
.clickable { cursor: pointer; }
.reset-btn {
padding: 4px 12px; border: 1px solid var(--border); border-radius: 6px;
background: var(--card-bg); cursor: pointer; font-size: 0.78rem; color: var(--text-dim);
}
.reset-btn:hover { border-color: var(--accent); color: var(--accent); }
.result-count { font-size: 0.85rem; color: var(--text-dim); margin: 10px 0 8px; }
/* Timeline bars */
.tl-bar {
display: inline-block; height: 16px; border-radius: 3px;
vertical-align: middle; min-width: 2px;
}
@media (max-width: 768px) {
.cards { grid-template-columns: 1fr 1fr; }
.controls-row { flex-direction: column; align-items: stretch; }
.detail-grid { grid-template-columns: 1fr; }
}

9558
docs/data/drafts.json Normal file

File diff suppressed because it is too large Load Diff

101
docs/data/gaps.json Normal file
View File

@@ -0,0 +1,101 @@
{
"current": [
{
"id": 25,
"topic": "Agent Behavior Verification",
"description": "While many drafts address agent identity and authentication, few tackle how to verify that an agent is actually behaving according to its declared capabilities and policies. There's a critical gap in runtime behavioral attestation and compliance monitoring mechanisms.",
"category": "AI safety/alignment",
"evidence": "High overlap in identity/auth (108 drafts) but only 44 drafts on safety/alignment, with no specific focus on behavioral verification",
"severity": "critical"
},
{
"id": 26,
"topic": "Cross-Domain Agent Liability",
"description": "When autonomous agents operate across organizational boundaries and cause harm or make decisions with legal implications, there's no standardized framework for liability attribution. The policy/governance drafts don't address cross-jurisdictional legal accountability.",
"category": "Policy/governance",
"evidence": "91 policy/governance drafts but legal liability for cross-domain autonomous actions remains unaddressed",
"severity": "critical"
},
{
"id": 27,
"topic": "Agent Resource Exhaustion Protection",
"description": "Missing standardized mechanisms to prevent malicious or poorly designed agents from consuming excessive network, compute, or storage resources. Current drafts focus on traffic management but not on agent-specific resource quotas and enforcement.",
"category": "Autonomous netops",
"evidence": "93 autonomous netops drafts and 73 ML traffic management drafts lack agent-specific resource protection mechanisms",
"severity": "high"
},
{
"id": 28,
"topic": "Human Override Protocols",
"description": "Critical gap in standardized protocols for humans to safely interrupt, override, or take control of autonomous agents in emergency situations. Only 30 drafts address human-agent interaction, with no focus on emergency takeover procedures.",
"category": "Human-agent interaction",
"evidence": "Only 30 human-agent interaction drafts compared to 213+ autonomous operation drafts, with no emergency override standards",
"severity": "critical"
},
{
"id": 29,
"topic": "Agent-Generated Data Provenance",
"description": "While 145 drafts address data formats for AI interop, there's insufficient attention to tracking the provenance and lineage of data generated by agents. This creates trust and auditability issues in agent-to-agent data exchanges.",
"category": "Data formats/interop",
"evidence": "145 data format drafts with high overlap but no clear standards for agent-generated data provenance tracking",
"severity": "high"
},
{
"id": 30,
"topic": "Agent Capability Degradation Handling",
"description": "No standardized approaches for detecting and handling when an agent's capabilities degrade due to model drift, data corruption, or hardware issues. Systems need graceful degradation protocols rather than silent failures.",
"category": "AI safety/alignment",
"evidence": "Only 44 safety/alignment drafts don't address capability degradation, while 213+ drafts assume stable agent performance",
"severity": "high"
},
{
"id": 31,
"topic": "Multi-Agent Coordination Deadlocks",
"description": "With 120+ A2A protocol drafts, there's insufficient attention to preventing deadlock situations where multiple agents create circular dependencies or resource conflicts. Missing are standardized deadlock detection and resolution mechanisms.",
"category": "A2A protocols",
"evidence": "120 A2A protocol drafts with high internal overlap but no systematic deadlock prevention frameworks",
"severity": "high"
},
{
"id": 32,
"topic": "Agent Privacy Preservation",
"description": "Agents often process sensitive data but current drafts don't adequately address privacy-preserving computation, differential privacy, or secure multi-party computation for agent interactions. This is critical for deployment in regulated industries.",
"category": "Agent identity/auth",
"evidence": "108 identity/auth drafts focus on authentication but lack privacy preservation mechanisms for agent data processing",
"severity": "high"
},
{
"id": 33,
"topic": "Real-time Agent Debugging",
"description": "Missing standardized protocols for debugging autonomous agents in production environments. When agents make unexpected decisions, there are no standard interfaces for real-time introspection without disrupting operations.",
"category": "Other AI/agent",
"evidence": "26 other AI/agent drafts suggest various approaches but no standardized debugging protocols for production agents",
"severity": "medium"
},
{
"id": 34,
"topic": "Agent Firmware/Model Update Security",
"description": "While model serving is addressed in 42 drafts, there's insufficient focus on secure update mechanisms for agent models and firmware. Missing are standards for cryptographically verified, rollback-capable agent updates.",
"category": "Model serving/inference",
"evidence": "42 model serving drafts but no comprehensive security standards for agent software/model updates",
"severity": "high"
},
{
"id": 35,
"topic": "Cross-Protocol Agent Migration",
"description": "No standardized mechanisms for migrating agent state and context when moving between different A2A protocols or infrastructure providers. This creates vendor lock-in and limits agent mobility.",
"category": "A2A protocols",
"evidence": "120 A2A protocol drafts with high overlap suggest competing approaches but no migration standards between them",
"severity": "medium"
},
{
"id": 36,
"topic": "Agent Energy Consumption Optimization",
"description": "Missing standards for energy-aware agent deployment and operation. As AI workloads are energy-intensive, there's no framework for agents to optimize their energy consumption or for infrastructure to enforce energy budgets.",
"category": "ML traffic mgmt",
"evidence": "73 ML traffic management drafts focus on performance but lack energy consumption considerations for sustainable AI deployment",
"severity": "medium"
}
],
"history": []
}

5
docs/data/meta.json Normal file
View File

@@ -0,0 +1,5 @@
{
"generated_at": "2026-03-03T23:32:29.056739+00:00",
"version": "0.3.0",
"project": "IETF Living Standards Observatory"
}

View File

@@ -0,0 +1,9 @@
{
"total_docs": 361,
"sources": {},
"gaps_count": 12,
"unrated": 0,
"ideas": 1780,
"authors": 557,
"last_update": null
}

264
docs/data/timeline.json Normal file
View File

@@ -0,0 +1,264 @@
{
"months": [
"2024-01",
"2024-02",
"2024-04",
"2024-09",
"2024-10",
"2024-12",
"2025-01",
"2025-04",
"2025-05",
"2025-06",
"2025-07",
"2025-08",
"2025-09",
"2025-10",
"2025-11",
"2025-12",
"2026-01",
"2026-02",
"2026-03"
],
"by_category": {
"2024-01": {
"Agent identity/auth": 1,
"Policy/governance": 1,
"Other AI/agent": 1,
"Data formats/interop": 1,
"AI safety/alignment": 1
},
"2024-02": {
"Autonomous netops": 1,
"ML traffic mgmt": 1
},
"2024-04": {
"A2A protocols": 1,
"Autonomous netops": 1,
"Data formats/interop": 1
},
"2024-09": {
"Autonomous netops": 1,
"Policy/governance": 1,
"ML traffic mgmt": 1
},
"2024-10": {
"ML traffic mgmt": 1,
"Model serving/inference": 1
},
"2024-12": {
"Other AI/agent": 1
},
"2025-01": {
"Data formats/interop": 2,
"ML traffic mgmt": 4,
"Model serving/inference": 2
},
"2025-04": {
"Policy/governance": 3,
"Agent identity/auth": 1,
"Data formats/interop": 2,
"AI safety/alignment": 1,
"Autonomous netops": 1,
"Model serving/inference": 1,
"ML traffic mgmt": 1
},
"2025-05": {
"Policy/governance": 2,
"Agent discovery/reg": 1,
"Data formats/interop": 1,
"Human-agent interaction": 1
},
"2025-06": {
"Agent identity/auth": 2,
"A2A protocols": 1,
"Policy/governance": 1,
"Autonomous netops": 3,
"ML traffic mgmt": 3,
"Model serving/inference": 1,
"Data formats/interop": 1
},
"2025-07": {
"Agent identity/auth": 2,
"A2A protocols": 2,
"Agent discovery/reg": 1,
"Data formats/interop": 2,
"AI safety/alignment": 2,
"Human-agent interaction": 1,
"Policy/governance": 1
},
"2025-08": {
"Autonomous netops": 2,
"Data formats/interop": 6,
"Agent identity/auth": 2,
"Policy/governance": 3,
"Other AI/agent": 2,
"Human-agent interaction": 1
},
"2025-09": {
"Policy/governance": 6,
"Autonomous netops": 6,
"Data formats/interop": 11,
"ML traffic mgmt": 4,
"AI safety/alignment": 4,
"Agent discovery/reg": 3,
"A2A protocols": 4,
"Human-agent interaction": 1,
"Agent identity/auth": 3,
"Model serving/inference": 1
},
"2025-10": {
"Policy/governance": 10,
"Data formats/interop": 36,
"Model serving/inference": 9,
"ML traffic mgmt": 13,
"Autonomous netops": 19,
"Agent discovery/reg": 15,
"Agent identity/auth": 24,
"Human-agent interaction": 7,
"A2A protocols": 28,
"AI safety/alignment": 5,
"Other AI/agent": 5
},
"2025-11": {
"A2A protocols": 29,
"Autonomous netops": 18,
"Data formats/interop": 25,
"ML traffic mgmt": 9,
"Agent discovery/reg": 18,
"Agent identity/auth": 23,
"AI safety/alignment": 7,
"Policy/governance": 15,
"Human-agent interaction": 9,
"Model serving/inference": 4,
"Other AI/agent": 2
},
"2025-12": {
"Policy/governance": 6,
"Data formats/interop": 6,
"Human-agent interaction": 1,
"Agent identity/auth": 8,
"Autonomous netops": 4,
"Other AI/agent": 2,
"A2A protocols": 2,
"ML traffic mgmt": 3,
"AI safety/alignment": 3,
"Agent discovery/reg": 4,
"Model serving/inference": 1
},
"2026-01": {
"Agent identity/auth": 18,
"AI safety/alignment": 8,
"Policy/governance": 14,
"A2A protocols": 22,
"Agent discovery/reg": 15,
"ML traffic mgmt": 8,
"Data formats/interop": 17,
"Autonomous netops": 16,
"Model serving/inference": 5,
"Other AI/agent": 4,
"Human-agent interaction": 2
},
"2026-02": {
"A2A protocols": 38,
"Human-agent interaction": 6,
"Data formats/interop": 32,
"Agent identity/auth": 34,
"Policy/governance": 24,
"Autonomous netops": 18,
"ML traffic mgmt": 15,
"Agent discovery/reg": 19,
"Other AI/agent": 8,
"AI safety/alignment": 13,
"Model serving/inference": 8
},
"2026-03": {
"ML traffic mgmt": 11,
"A2A protocols": 9,
"Model serving/inference": 9,
"Agent identity/auth": 3,
"Data formats/interop": 5,
"Autonomous netops": 8,
"Policy/governance": 6,
"Agent discovery/reg": 3,
"Human-agent interaction": 1,
"Other AI/agent": 1,
"AI safety/alignment": 1
}
},
"by_source": {
"2024-01": {
"ietf": 3
},
"2024-02": {
"ietf": 1
},
"2024-04": {
"ietf": 1
},
"2024-09": {
"ietf": 2
},
"2024-10": {
"ietf": 1
},
"2024-12": {
"ietf": 1
},
"2025-01": {
"ietf": 4
},
"2025-04": {
"ietf": 5
},
"2025-05": {
"ietf": 2
},
"2025-06": {
"ietf": 5
},
"2025-07": {
"ietf": 5
},
"2025-08": {
"ietf": 8
},
"2025-09": {
"ietf": 17
},
"2025-10": {
"ietf": 67
},
"2025-11": {
"ietf": 61
},
"2025-12": {
"ietf": 16
},
"2026-01": {
"ietf": 54
},
"2026-02": {
"ietf": 86
},
"2026-03": {
"ietf": 22
}
},
"categories": [
"A2A protocols",
"AI safety/alignment",
"Agent discovery/reg",
"Agent identity/auth",
"Autonomous netops",
"Data formats/interop",
"Human-agent interaction",
"ML traffic mgmt",
"Model serving/inference",
"Other AI/agent",
"Policy/governance"
],
"sources": [
"ietf"
]
}

110
docs/index.html Normal file
View File

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Living Standards Observatory</title>
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<div class="header">
<div class="container">
<h1>Living Standards Observatory</h1>
<nav>
<a href="index.html" class="active">Dashboard</a>
<a href="observatory/explorer.html">Explorer</a>
<a href="observatory/gaps.html">Gaps</a>
<a href="observatory/timeline.html">Timeline</a>
</nav>
</div>
</div>
<div class="container">
<div class="cards" id="metricsCards">
<div class="card"><div class="label">Total Documents</div><div class="value" id="totalDocs">--</div><div class="sub" id="sourceSub"></div></div>
<div class="card"><div class="label">Standards Bodies</div><div class="value" id="sourceCount">--</div><div class="sub">Active sources</div></div>
<div class="card"><div class="label">Open Gaps</div><div class="value" id="gapCount">--</div><div class="sub">Identified coverage gaps</div></div>
<div class="card"><div class="label">Ideas Extracted</div><div class="value" id="ideaCount">--</div><div class="sub">Technical contributions</div></div>
<div class="card"><div class="label">Authors Tracked</div><div class="value" id="authorCount">--</div><div class="sub">Individual contributors</div></div>
<div class="card"><div class="label">Last Update</div><div class="value" id="lastUpdate" style="font-size:1rem">--</div><div class="sub" id="updateSub"></div></div>
</div>
<div class="panel">
<div class="panel-header">Top Rated Documents</div>
<table>
<thead>
<tr><th>Score</th><th>Document</th><th>Source</th><th>Date</th><th>Categories</th></tr>
</thead>
<tbody id="topDrafts"></tbody>
</table>
</div>
<div class="panel">
<div class="panel-header">Critical &amp; High Severity Gaps</div>
<div id="gapsList" style="padding: 16px;"></div>
</div>
</div>
<script>
function escHtml(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
function scoreBadge(s) {
const cls = s >= 4.0 ? 'score-high' : s >= 3.0 ? 'score-mid' : 'score-low';
return '<span class="score-badge ' + cls + '">' + s.toFixed(1) + '</span>';
}
async function init() {
const [obs, drafts, gaps] = await Promise.all([
fetch('data/observatory.json').then(r => r.json()),
fetch('data/drafts.json').then(r => r.json()),
fetch('data/gaps.json').then(r => r.json()),
]);
// Metrics
document.getElementById('totalDocs').textContent = obs.total_docs;
const srcNames = Object.keys(obs.sources || {});
document.getElementById('sourceCount').textContent = srcNames.length || 1;
document.getElementById('sourceSub').textContent = srcNames.map(s => s.toUpperCase() + ': ' + (obs.sources[s] || 0)).join(' | ') || '';
document.getElementById('gapCount').textContent = obs.gaps_count;
document.getElementById('ideaCount').textContent = obs.ideas;
document.getElementById('authorCount').textContent = obs.authors;
if (obs.last_update) {
document.getElementById('lastUpdate').textContent = obs.last_update.substring(0, 10);
}
// Top drafts
const top = drafts.sort((a, b) => b.score - a.score).slice(0, 15);
const tbody = document.getElementById('topDrafts');
top.forEach(d => {
const tr = document.createElement('tr');
const srcClass = 'source-' + (d.source || 'ietf');
tr.innerHTML =
'<td>' + scoreBadge(d.score) + '</td>' +
'<td><a href="' + escHtml(d.url) + '" target="_blank">' + escHtml(d.name) + '</a><br><span class="dim">' + escHtml(d.title.substring(0,80)) + '</span></td>' +
'<td><span class="source-badge ' + srcClass + '">' + (d.source || 'ietf').toUpperCase() + '</span></td>' +
'<td class="dim">' + d.date + '</td>' +
'<td>' + d.categories.map(c => '<span class="cat-badge">' + escHtml(c) + '</span>').join('') + '</td>';
tbody.appendChild(tr);
});
// Gaps
const gapsList = document.getElementById('gapsList');
const critical = (gaps.current || []).filter(g => g.severity === 'critical' || g.severity === 'high');
if (critical.length === 0) {
gapsList.innerHTML = '<p class="dim">No critical or high severity gaps found.</p>';
} else {
critical.forEach(g => {
const cls = g.severity === 'critical' ? 'critical' : 'high';
gapsList.innerHTML +=
'<div class="gap-card ' + cls + '">' +
'<h3>' + escHtml(g.topic) + '</h3>' +
'<p>' + escHtml(g.description) + '</p>' +
'<div class="meta"><span class="sev-' + g.severity + '">' + g.severity.toUpperCase() + '</span> &middot; ' + escHtml(g.category || '') + '</div>' +
'</div>';
});
}
}
init();
</script>
</body>
</html>

View File

@@ -0,0 +1,239 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Explorer - Living Standards Observatory</title>
<link rel="stylesheet" href="../assets/style.css">
</head>
<body>
<div class="header">
<div class="container">
<h1>Living Standards Observatory</h1>
<nav>
<a href="../index.html">Dashboard</a>
<a href="explorer.html" class="active">Explorer</a>
<a href="gaps.html">Gaps</a>
<a href="timeline.html">Timeline</a>
</nav>
</div>
</div>
<div class="container">
<div class="controls">
<div class="controls-row">
<input type="text" class="search-box" id="searchBox" placeholder="Search by name, title, summary, or keyword...">
<select id="sourceFilter" style="padding:8px;border:1px solid var(--border);border-radius:6px;font-size:0.85rem">
<option value="">All sources</option>
</select>
<div class="slider-group">Min score: <input type="range" id="minScore" min="1" max="5" step="0.1" value="1"><span class="slider-val" id="minScoreVal">1.0</span></div>
<div class="slider-group">Min novelty: <input type="range" id="minNovelty" min="1" max="5" step="1" value="1"><span class="slider-val" id="minNoveltyVal">1</span></div>
<div class="slider-group">Max overlap: <input type="range" id="maxOverlap" min="1" max="5" step="1" value="5"><span class="slider-val" id="maxOverlapVal">5</span></div>
<button class="reset-btn" onclick="resetFilters()">Reset</button>
</div>
<div class="controls-row">
<div class="chip-row" id="catChips"></div>
</div>
</div>
<div class="result-count" id="resultCount"></div>
<table>
<thead>
<tr>
<th onclick="sortBy('score')" width="60">Score <span class="sort-arrow" id="sort-score"></span></th>
<th onclick="sortBy('name')">Draft <span class="sort-arrow" id="sort-name"></span></th>
<th onclick="sortBy('source')" width="60">Src <span class="sort-arrow" id="sort-source"></span></th>
<th onclick="sortBy('date')" width="90">Date <span class="sort-arrow" id="sort-date"></span></th>
<th onclick="sortBy('novelty')" width="30">N</th>
<th onclick="sortBy('maturity')" width="30">M</th>
<th onclick="sortBy('overlap')" width="30">O</th>
<th onclick="sortBy('momentum')" width="30">Mom</th>
<th onclick="sortBy('relevance')" width="30">R</th>
<th>Categories</th>
</tr>
</thead>
<tbody id="tableBody"></tbody>
</table>
</div>
<script>
let DRAFTS = [];
let ALL_CATS = [];
let activeCats = new Set();
let sortField = 'score';
let sortAsc = false;
let expandedRow = null;
function escHtml(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
function scoreBadge(s) {
const cls = s >= 4.0 ? 'score-high' : s >= 3.0 ? 'score-mid' : 'score-low';
return '<span class="score-badge ' + cls + '">' + s.toFixed(1) + '</span>';
}
function dimBar(v) { return '<span class="bar" style="width:' + (v * 12) + 'px"></span> ' + v; }
const searchBox = document.getElementById('searchBox');
const sourceFilter = document.getElementById('sourceFilter');
const minScore = document.getElementById('minScore');
const minNovelty = document.getElementById('minNovelty');
const maxOverlap = document.getElementById('maxOverlap');
searchBox.oninput = render;
sourceFilter.onchange = render;
minScore.oninput = () => { document.getElementById('minScoreVal').textContent = parseFloat(minScore.value).toFixed(1); render(); };
minNovelty.oninput = () => { document.getElementById('minNoveltyVal').textContent = minNovelty.value; render(); };
maxOverlap.oninput = () => { document.getElementById('maxOverlapVal').textContent = maxOverlap.value; render(); };
function resetFilters() {
searchBox.value = '';
sourceFilter.value = '';
minScore.value = 1; document.getElementById('minScoreVal').textContent = '1.0';
minNovelty.value = 1; document.getElementById('minNoveltyVal').textContent = '1';
maxOverlap.value = 5; document.getElementById('maxOverlapVal').textContent = '5';
activeCats.clear();
document.querySelectorAll('.chip').forEach(c => c.classList.remove('active'));
sortField = 'score'; sortAsc = false;
render();
}
function sortBy(field) {
if (sortField === field) sortAsc = !sortAsc;
else { sortField = field; sortAsc = field === 'name' || field === 'date'; }
render();
}
function cmp(a, b) {
let va = a[sortField], vb = b[sortField];
if (typeof va === 'string') return sortAsc ? va.localeCompare(vb) : vb.localeCompare(va);
return sortAsc ? va - vb : vb - va;
}
function render() {
const q = searchBox.value.toLowerCase().trim();
const src = sourceFilter.value;
const ms = parseFloat(minScore.value);
const mn = parseInt(minNovelty.value);
const mo = parseInt(maxOverlap.value);
let filtered = DRAFTS.filter(d => {
if (d.score < ms) return false;
if (d.novelty < mn) return false;
if (d.overlap > mo) return false;
if (src && (d.source || 'ietf') !== src) return false;
if (activeCats.size > 0 && !d.categories.some(c => activeCats.has(c))) return false;
if (q) {
const hay = (d.name + ' ' + d.title + ' ' + d.summary + ' ' + d.categories.join(' ')).toLowerCase();
const words = q.split(/\s+/);
if (!words.every(w => hay.includes(w))) return false;
}
return true;
});
filtered.sort(cmp);
document.querySelectorAll('.sort-arrow').forEach(el => el.textContent = '');
const arrow = document.getElementById('sort-' + sortField);
if (arrow) arrow.textContent = sortAsc ? '\u25B2' : '\u25BC';
const tbody = document.getElementById('tableBody');
tbody.innerHTML = '';
expandedRow = null;
filtered.forEach(d => {
const tr = document.createElement('tr');
tr.className = 'clickable';
const srcClass = 'source-' + (d.source || 'ietf');
tr.innerHTML =
'<td>' + scoreBadge(d.score) + '</td>' +
'<td style="max-width:300px"><a href="' + escHtml(d.url) + '" target="_blank" onclick="event.stopPropagation()" style="color:var(--accent);font-weight:500">' + escHtml(d.name) + '</a>' +
'<br><span class="dim">' + escHtml(d.title.substring(0, 80)) + '</span></td>' +
'<td><span class="source-badge ' + srcClass + '">' + (d.source || 'ietf').toUpperCase() + '</span></td>' +
'<td class="dim">' + d.date + '</td>' +
'<td>' + dimBar(d.novelty) + '</td>' +
'<td>' + dimBar(d.maturity) + '</td>' +
'<td>' + dimBar(d.overlap) + '</td>' +
'<td>' + dimBar(d.momentum) + '</td>' +
'<td>' + dimBar(d.relevance) + '</td>' +
'<td>' + d.categories.map(c => '<span class="cat-badge">' + escHtml(c) + '</span>').join('') + '</td>';
tr.onclick = () => toggleDetail(tr, d);
tbody.appendChild(tr);
});
document.getElementById('resultCount').textContent =
'Showing ' + filtered.length + ' of ' + DRAFTS.length + ' drafts';
}
function toggleDetail(tr, d) {
if (expandedRow) {
expandedRow.previousElementSibling?.classList.remove('expanded');
expandedRow.remove();
if (expandedRow._draftName === d.name) { expandedRow = null; return; }
}
tr.classList.add('expanded');
const detail = document.createElement('tr');
detail.className = 'detail-row';
detail._draftName = d.name;
function detailItem(label, score, note) {
return '<div class="detail-item"><strong>' + label + ':</strong> ' + score + '/5 ' +
'<span class="bar" style="width:' + (score * 16) + 'px"></span>' +
(note ? '<div class="note">' + escHtml(note) + '</div>' : '') + '</div>';
}
detail.innerHTML = '<td colspan="10">' +
'<div class="summary-text"><strong>Summary:</strong> ' + escHtml(d.summary) + '</div>' +
'<div class="detail-grid" style="margin-top:10px">' +
detailItem('Novelty', d.novelty, d.novelty_note) +
detailItem('Maturity', d.maturity, d.maturity_note) +
detailItem('Overlap', d.overlap, d.overlap_note) +
detailItem('Momentum', d.momentum, d.momentum_note) +
detailItem('Relevance', d.relevance, d.relevance_note) +
'<div class="detail-item"><strong>Source:</strong> ' + (d.source || 'ietf').toUpperCase() + ' &middot; <strong>Pages:</strong> ' + d.pages + '</div>' +
'</div>' +
'<div style="margin-top:8px"><a href="' + escHtml(d.url) + '" target="_blank" style="color:var(--accent)">Open document \u2192</a></div>' +
'</td>';
tr.after(detail);
expandedRow = detail;
}
async function init() {
DRAFTS = await fetch('../data/drafts.json').then(r => r.json());
// Build categories
const catSet = new Set();
const sources = new Set();
DRAFTS.forEach(d => {
d.categories.forEach(c => catSet.add(c));
sources.add(d.source || 'ietf');
});
ALL_CATS = [...catSet].sort();
// Source filter options
sources.forEach(s => {
const opt = document.createElement('option');
opt.value = s;
opt.textContent = s.toUpperCase();
sourceFilter.appendChild(opt);
});
// Category chips
const chipBox = document.getElementById('catChips');
ALL_CATS.forEach(cat => {
const el = document.createElement('span');
el.className = 'chip';
const count = DRAFTS.filter(d => d.categories.includes(cat)).length;
el.innerHTML = escHtml(cat) + '<span style="font-size:0.65rem;opacity:0.7;margin-left:2px">(' + count + ')</span>';
el.onclick = () => {
if (activeCats.has(cat)) { activeCats.delete(cat); el.classList.remove('active'); }
else { activeCats.add(cat); el.classList.add('active'); }
render();
};
chipBox.appendChild(el);
});
render();
}
init();
</script>
</body>
</html>

118
docs/observatory/gaps.html Normal file
View File

@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gaps - Living Standards Observatory</title>
<link rel="stylesheet" href="../assets/style.css">
</head>
<body>
<div class="header">
<div class="container">
<h1>Living Standards Observatory</h1>
<nav>
<a href="../index.html">Dashboard</a>
<a href="explorer.html">Explorer</a>
<a href="gaps.html" class="active">Gaps</a>
<a href="timeline.html">Timeline</a>
</nav>
</div>
</div>
<div class="container">
<h2 style="margin-bottom:16px">Coverage Gaps</h2>
<p class="dim" style="margin-bottom:20px">Areas, problems, or technical challenges not adequately addressed by existing standards documents.</p>
<div class="controls">
<div class="controls-row">
<select id="sevFilter" style="padding:8px;border:1px solid var(--border);border-radius:6px;font-size:0.85rem">
<option value="">All severities</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
<input type="text" class="search-box" id="gapSearch" placeholder="Filter gaps..." style="max-width:400px">
</div>
</div>
<div id="gapsList"></div>
<h2 style="margin:32px 0 16px">Gap History</h2>
<p class="dim" style="margin-bottom:20px">How gaps have evolved across observatory snapshots.</p>
<div class="panel">
<table>
<thead>
<tr><th>Snapshot</th><th>Topic</th><th>Severity</th><th>Status</th></tr>
</thead>
<tbody id="historyBody"></tbody>
</table>
</div>
</div>
<script>
function escHtml(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
let GAPS_DATA = null;
function renderGaps() {
const sev = document.getElementById('sevFilter').value;
const q = document.getElementById('gapSearch').value.toLowerCase().trim();
const list = document.getElementById('gapsList');
list.innerHTML = '';
let current = GAPS_DATA.current || [];
if (sev) current = current.filter(g => g.severity === sev);
if (q) current = current.filter(g => (g.topic + ' ' + g.description + ' ' + (g.category || '')).toLowerCase().includes(q));
if (current.length === 0) {
list.innerHTML = '<p class="dim" style="padding:16px">No gaps match the current filters.</p>';
return;
}
const order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3};
current.sort((a, b) => (order[a.severity] || 2) - (order[b.severity] || 2));
current.forEach(g => {
const cls = (g.severity === 'critical' || g.severity === 'high') ? g.severity : '';
list.innerHTML +=
'<div class="gap-card ' + cls + '">' +
'<h3>' + escHtml(g.topic) + '</h3>' +
'<p>' + escHtml(g.description) + '</p>' +
'<div class="meta">' +
'<span class="sev-' + g.severity + '">' + (g.severity || 'medium').toUpperCase() + '</span>' +
(g.category ? ' &middot; ' + escHtml(g.category) : '') +
(g.evidence ? '<br><em>' + escHtml(g.evidence) + '</em>' : '') +
'</div></div>';
});
}
async function init() {
GAPS_DATA = await fetch('../data/gaps.json').then(r => r.json());
document.getElementById('sevFilter').onchange = renderGaps;
document.getElementById('gapSearch').oninput = renderGaps;
renderGaps();
// History table
const history = GAPS_DATA.history || [];
const tbody = document.getElementById('historyBody');
if (history.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" class="dim">No history recorded yet.</td></tr>';
} else {
history.slice(-50).reverse().forEach(h => {
const tr = document.createElement('tr');
tr.innerHTML =
'<td class="dim">' + (h.snapshot_at || h.recorded_at || '').substring(0, 10) + '</td>' +
'<td>' + escHtml(h.gap_topic) + '</td>' +
'<td><span class="sev-' + (h.severity || 'medium') + '">' + (h.severity || 'medium').toUpperCase() + '</span></td>' +
'<td>' + escHtml(h.status || 'open') + '</td>';
tbody.appendChild(tr);
});
}
}
init();
</script>
</body>
</html>

View File

@@ -0,0 +1,157 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Timeline - Living Standards Observatory</title>
<link rel="stylesheet" href="../assets/style.css">
<style>
.tl-row { display: flex; align-items: center; gap: 8px; padding: 6px 0; border-bottom: 1px solid #f0f0f0; }
.tl-month { min-width: 80px; font-size: 0.82rem; color: var(--text-dim); font-family: monospace; }
.tl-bars { flex: 1; display: flex; gap: 1px; align-items: center; }
.tl-count { min-width: 30px; text-align: right; font-size: 0.78rem; color: var(--text-dim); }
.legend { display: flex; gap: 16px; flex-wrap: wrap; margin-bottom: 16px; }
.legend-item { display: flex; align-items: center; gap: 4px; font-size: 0.8rem; }
.legend-swatch { width: 14px; height: 14px; border-radius: 3px; }
.view-toggle { display: flex; gap: 8px; margin-bottom: 16px; }
.view-btn { padding: 6px 16px; border: 1px solid var(--border); border-radius: 6px; background: var(--card-bg); cursor: pointer; font-size: 0.82rem; }
.view-btn.active { background: var(--accent); color: #fff; border-color: var(--accent); }
</style>
</head>
<body>
<div class="header">
<div class="container">
<h1>Living Standards Observatory</h1>
<nav>
<a href="../index.html">Dashboard</a>
<a href="explorer.html">Explorer</a>
<a href="gaps.html">Gaps</a>
<a href="timeline.html" class="active">Timeline</a>
</nav>
</div>
</div>
<div class="container">
<h2 style="margin-bottom:8px">Submission Timeline</h2>
<p class="dim" style="margin-bottom:20px">Monthly document submissions across standards bodies and categories.</p>
<div class="view-toggle">
<button class="view-btn active" id="btnSource" onclick="setView('source')">By Source</button>
<button class="view-btn" id="btnCategory" onclick="setView('category')">By Category</button>
</div>
<div class="legend" id="legend"></div>
<div class="chart-container" id="timeline"></div>
<div class="panel">
<div class="panel-header">Monthly Totals</div>
<table>
<thead><tr><th>Month</th><th>Total</th><th id="breakdownHeader">By Source</th></tr></thead>
<tbody id="monthTable"></tbody>
</table>
</div>
</div>
<script>
function escHtml(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
const COLORS_SOURCE = {'ietf': '#4a6cf7', 'w3c': '#ef4444', 'ieee': '#10b981', 'other': '#9ca3af'};
const COLORS_CAT = [
'#636EFA', '#EF553B', '#00CC96', '#AB63FA', '#FFA15A',
'#19D3F3', '#FF6692', '#B6E880', '#FF97FF', '#FECB52',
'#7C8CF5', '#FF8C69', '#66CDAA', '#BA55D3', '#FFD700',
];
let TL_DATA = null;
let currentView = 'source';
function setView(view) {
currentView = view;
document.getElementById('btnSource').className = 'view-btn' + (view === 'source' ? ' active' : '');
document.getElementById('btnCategory').className = 'view-btn' + (view === 'category' ? ' active' : '');
document.getElementById('breakdownHeader').textContent = view === 'source' ? 'By Source' : 'By Category';
renderTimeline();
}
function renderTimeline() {
if (!TL_DATA) return;
const months = TL_DATA.months;
const isSource = currentView === 'source';
const dataMap = isSource ? TL_DATA.by_source : TL_DATA.by_category;
const keys = isSource ? TL_DATA.sources : TL_DATA.categories;
// Assign colors
const colorMap = {};
if (isSource) {
keys.forEach(k => { colorMap[k] = COLORS_SOURCE[k] || '#9ca3af'; });
} else {
keys.forEach((k, i) => { colorMap[k] = COLORS_CAT[i % COLORS_CAT.length]; });
}
// Max for scaling
let maxTotal = 0;
months.forEach(m => {
const d = dataMap[m] || {};
let t = 0;
keys.forEach(k => { t += d[k] || 0; });
if (t > maxTotal) maxTotal = t;
});
const scale = maxTotal > 0 ? 500 / maxTotal : 1;
// Legend
const legendEl = document.getElementById('legend');
legendEl.innerHTML = '';
keys.forEach(k => {
legendEl.innerHTML += '<div class="legend-item"><div class="legend-swatch" style="background:' + colorMap[k] + '"></div>' + escHtml(k) + '</div>';
});
// Chart
const container = document.getElementById('timeline');
container.innerHTML = '';
months.forEach(m => {
const d = dataMap[m] || {};
let total = 0;
keys.forEach(k => { total += d[k] || 0; });
let barsHtml = '';
keys.forEach(k => {
const v = d[k] || 0;
if (v > 0) {
const w = Math.max(v * scale, 2);
barsHtml += '<div class="tl-bar" style="width:' + w + 'px;background:' + colorMap[k] + '" title="' + escHtml(k) + ': ' + v + '"></div>';
}
});
container.innerHTML += '<div class="tl-row"><span class="tl-month">' + m + '</span><div class="tl-bars">' + barsHtml + '</div><span class="tl-count">' + total + '</span></div>';
});
// Table
const tbody = document.getElementById('monthTable');
tbody.innerHTML = '';
[...months].reverse().forEach(m => {
const d = dataMap[m] || {};
let total = 0;
const parts = [];
keys.forEach(k => {
const v = d[k] || 0;
total += v;
if (v > 0) parts.push(k + ': ' + v);
});
if (total > 0) {
const tr = document.createElement('tr');
tr.innerHTML = '<td class="dim">' + m + '</td><td>' + total + '</td><td class="dim">' + parts.join(', ') + '</td>';
tbody.appendChild(tr);
}
});
}
async function init() {
TL_DATA = await fetch('../data/timeline.json').then(r => r.json());
renderTimeline();
}
init();
</script>
</body>
</html>