Add auto-heal pipeline command and fix multi-source draft processing

- Add `ietf auto` command: fetches, analyzes, embeds, extracts ideas,
  and refreshes gaps across all sources with cost-based auto-approval
- Fix SourceDocument→Draft conversion in auto fetch step
- Fix gap_analysis method name in auto command
- Process all 270 unrated ETSI/ISO/ITU/NIST drafts (761 total, all rated)
- Update web UI templates and data layer for multi-source support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 18:41:42 +01:00
parent 1ec1f69bee
commit a46a01bd8c
15 changed files with 991 additions and 381 deletions

View File

@@ -116,34 +116,72 @@
<p class="text-xs text-slate-500 mb-4">Clusters are formed by connected-component analysis of the co-authorship graph: authors who share 2+ drafts are linked, and all authors reachable through such links form a cluster. This reveals research teams and institutional collaboration patterns — a cluster of 20 authors from 3 organizations means those groups actively co-author across org boundaries. Click a cluster to highlight it in the graph above.</p>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" id="clusterGrid">
{% for c in network.clusters[:12] %}
<div class="cluster-card bg-slate-800/50 rounded-lg border border-slate-700/50 p-4 cursor-pointer" data-cluster-id="{{ c.id }}" onclick="highlightCluster({{ c.id }})">
<div class="flex items-center justify-between mb-2">
<div class="cluster-card bg-slate-800/50 rounded-lg border border-slate-700/50 p-4 cursor-pointer" data-cluster-id="{{ c.id }}">
<!-- Header — click to highlight in graph -->
<div class="flex items-center justify-between mb-2" onclick="highlightCluster({{ c.id }})">
<span class="text-sm font-semibold text-white">Cluster #{{ c.id + 1 }}</span>
<div class="flex gap-1.5">
<div class="flex gap-1.5 items-center">
<span class="text-xs px-2 py-0.5 rounded-full bg-blue-500/20 text-blue-400">{{ c.size }} authors</span>
<span class="text-xs px-2 py-0.5 rounded-full bg-emerald-500/20 text-emerald-400">{{ c.draft_count }} drafts</span>
<svg class="w-4 h-4 text-slate-500 transition-transform cluster-chevron" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
</div>
</div>
<!-- Org mix -->
<div class="flex flex-wrap gap-1 mb-2">
{% for org, count in c.org_mix.items() %}
<span class="text-xs px-2 py-0.5 rounded-full bg-slate-700 text-slate-300">{{ org }} ({{ count }})</span>
{% endfor %}
</div>
<!-- Preview: first 3 members -->
<div class="text-xs text-slate-500 mb-2 truncate" title="{{ c.members | join(', ') }}">
{{ c.members[:5] | join(', ') }}{% if c.members | length > 5 %} +{{ c.members | length - 5 }} more{% endif %}
{{ c.members[:3] | join(', ') }}{% if c.members | length > 3 %} +{{ c.members | length - 3 }} more{% endif %}
</div>
<!-- Preview: first 3 drafts -->
{% if c.drafts %}
<div class="border-t border-slate-700/50 pt-2 mt-2">
{% for d in c.drafts[:5] %}
{% for d in c.drafts[:3] %}
<div class="text-xs truncate mb-0.5" title="{{ d.name }}: {{ d.title }}">
<a href="/drafts/{{ d.name }}" class="text-blue-400/70 hover:text-blue-300 transition" onclick="event.stopPropagation()">{{ d.title }}</a>
</div>
{% endfor %}
{% if c.draft_count > 5 %}
<div class="text-xs text-slate-600 mt-1">+{{ c.draft_count - 5 }} more drafts</div>
{% if c.draft_count > 3 %}
<div class="text-xs text-slate-600 mt-1">+{{ c.draft_count - 3 }} more drafts</div>
{% endif %}
</div>
{% endif %}
<!-- Expanded detail (hidden by default) -->
<div class="cluster-detail hidden mt-3 border-t border-slate-700/50 pt-3" id="authorCluster-{{ c.id }}">
<!-- All members with org -->
<h4 class="text-xs font-semibold text-slate-300 mb-2 uppercase tracking-wide">All {{ c.size }} Authors</h4>
<div class="max-h-48 overflow-y-auto mb-3 space-y-1">
{% for member in c.members %}
<div class="text-xs flex items-center justify-between gap-2">
<a href="/drafts?q={{ member | urlencode }}" class="text-slate-300 hover:text-blue-400 transition truncate" onclick="event.stopPropagation()">{{ member }}</a>
{% set member_org = c.member_orgs[member] if c.member_orgs is defined and member in c.member_orgs else '' %}
{% if member_org %}
<span class="text-slate-600 text-[10px] truncate max-w-[120px] flex-shrink-0">{{ member_org }}</span>
{% endif %}
</div>
{% endfor %}
</div>
<!-- All drafts -->
{% if c.drafts %}
<h4 class="text-xs font-semibold text-slate-300 mb-2 uppercase tracking-wide">All {{ c.draft_count }} Drafts</h4>
<div class="max-h-48 overflow-y-auto space-y-1.5">
{% for d in c.drafts %}
<div class="text-xs" title="{{ d.name }}">
<a href="/drafts/{{ d.name }}" class="text-blue-400/70 hover:text-blue-300 transition" onclick="event.stopPropagation()">{{ d.title }}</a>
<span class="text-slate-600 font-mono text-[10px] ml-1">{{ d.name | replace('draft-', '') | truncate(25) }}</span>
</div>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endfor %}
</div>
@@ -478,6 +516,20 @@ const network = {{ network | tojson }};
});
});
// Toggle expand/collapse on cluster card chevron click
document.querySelectorAll('.cluster-card').forEach(card => {
card.addEventListener('click', function(e) {
// Don't toggle if clicking a link or the highlight header
if (e.target.closest('a') || e.target.closest('[onclick*="highlightCluster"]')) return;
const detail = card.querySelector('.cluster-detail');
const chevron = card.querySelector('.cluster-chevron');
if (detail) {
detail.classList.toggle('hidden');
chevron.style.transform = detail.classList.contains('hidden') ? '' : 'rotate(180deg)';
}
});
});
// Expose cluster highlighting globally
window.highlightCluster = function(clusterId) {
const cluster = (network.clusters || []).find(c => c.id === clusterId);