Fix remaining critical, high, and medium issues from 4-perspective review

Critical fixes:
- Fix rating clamp range 1-10 → 1-5 (actual scale)
- Add `ietf ideas convergence` command (SequenceMatcher at 0.75 threshold)
- Fix "628 cross-org ideas" → 130 (verified from current DB) across 8 files

Security fixes:
- Sanitize FTS5 query input (strip special chars + boolean operators)
- Add rate limiting (10 req/min/IP) on Claude-calling endpoints
- Change <path:name> → <string:name> on draft routes

Codebase fixes:
- Add Database context manager (__enter__/__exit__)
- Wire false_positive filtering into queries (exclude by default in web UI)
- Fix Post 3 arithmetic ("~300" → "~409" distinct proposals)

Content & licensing:
- Add MIT LICENSE file
- Add IPR/FRAND notes (BCP 79, RFC 8179) to Posts 03 and 07
- Qualify "4:1 safety ratio" with monthly variation in 6 remaining files
- Add "Data as of March 2026" freeze-date headers to all 10 blog posts
- Hedge causal language in Post 04

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 12:47:47 +01:00
parent f1a0b0264c
commit e7527ad68e
40 changed files with 1005 additions and 169 deletions

View File

@@ -3,6 +3,8 @@
{% block title %}Timeline — IETF Draft Analyzer{% endblock %}
{% block extra_head %}<script src="/static/js/plotly.min.js"></script>{% endblock %}
{% block content %}
<div class="mb-6">
<h1 class="text-2xl font-bold text-white">Timeline Animation</h1>
@@ -53,6 +55,22 @@ const points = animData.points;
const months = animData.months;
const catMonthly = animData.category_monthly;
const MONTH_NAMES = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
function fmtMonth(ym) {
if (!ym) return ym;
let y, m;
if (ym.includes('-')) {
[y, m] = ym.split('-');
} else if (ym.length >= 6) {
y = ym.slice(0, 4);
m = ym.slice(4, 6);
} else {
return ym;
}
const mi = parseInt(m, 10) - 1;
return (MONTH_NAMES[mi] || m) + ' ' + y;
}
if (points.length > 0 && months.length > 0) {
// --- Stat cards ---
@@ -64,7 +82,7 @@ if (points.length > 0 && months.length > 0) {
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 to-blue-400"></div>
<div class="text-3xl font-bold text-blue-400">${months.length}</div>
<div class="text-xs text-slate-400 mt-1 uppercase tracking-wider">Months Span</div>
<div class="text-xs text-slate-500 mt-0.5">${firstMonth} to ${lastMonth}</div>
<div class="text-xs text-slate-500 mt-0.5">${fmtMonth(firstMonth)} ${fmtMonth(lastMonth)}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-5 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-emerald-400"></div>
@@ -130,7 +148,7 @@ if (points.length > 0 && months.length > 0) {
// Slider steps
const sliderSteps = months.map(month => ({
method: 'animate',
label: month,
label: fmtMonth(month),
args: [[month], { frame: { duration: 500, redraw: true }, transition: { duration: 300 }, mode: 'immediate' }],
}));
@@ -182,12 +200,12 @@ if (points.length > 0 && months.length > 0) {
// Update badge on animation frame
const badge = document.querySelector('#monthBadge span');
badge.textContent = `Month: ${months[0]} (${firstCount} drafts)`;
badge.textContent = `${fmtMonth(months[0])} ${firstCount} drafts`;
document.getElementById('tsneAnim').on('plotly_animatingframe', function(ev) {
const month = ev.name;
const cumCount = points.filter(p => p.month <= month).length;
badge.textContent = `Month: ${month} (${cumCount} drafts)`;
badge.textContent = `${fmtMonth(month)} ${cumCount} drafts`;
});
// Click to navigate
@@ -211,8 +229,9 @@ if (points.length > 0 && months.length > 0) {
return totalB - totalA;
});
const monthLabels = months.map(fmtMonth);
const areaTraces = areaCatList.map((cat, i) => ({
x: months,
x: monthLabels,
y: months.map(m => (catMonthly[m] || {})[cat] || 0),
name: cat,
type: 'scatter',