Architecture designer, author cluster names, FP filtering, new pages

- Add /architecture page: system-of-systems view with 8 layers, component
  cards, gap markers, source coverage chart, and clickable detail sidebar
- Give author clusters meaningful names from orgs + draft topic keywords
- Filter false positives (73 drafts, 54 ideas) from idea clusters,
  architecture, ideas listing, and search results
- Add NIST source fetcher with curated catalog of 11 AI publications
- New pages: trends, complexity, sources, false positives, idea analysis
- Clickable gap cards with full details (evidence, priority, nearby work)
- Component detail panel with linked drafts and top ideas

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 19:58:40 +01:00
parent a46a01bd8c
commit 8515e46d5d
19 changed files with 5672 additions and 202 deletions

View File

@@ -0,0 +1,53 @@
# Draft Complexity Matrix
*Generated 2026-03-08 18:05 UTC — 688 rated drafts (57.6% have page data)*
## Correlation Matrix
Pearson r between complexity metrics and rating dimensions.
| Metric | Novelty | Maturity | Overlap | Momentum | Relevance |
|--------|------: | ------: | ------: | ------: | ------: |
| Pages | +0.164 | +0.475 | -0.072 | +0.260 | +0.140 |
| Author Count | +0.151 | -0.152 | +0.011 | +0.016 | +0.169 |
| Citation Count | +0.254 | +0.033 | -0.058 | +0.069 | +0.212 |
| Idea Count | +0.187 | +0.019 | -0.033 | +0.137 | +0.127 |
| Category Count | +0.236 | -0.021 | +0.031 | +0.165 | +0.301 |
## Top 10 Most Complex Drafts
| # | Draft | Pages | Authors | Citations | Ideas | Score | Complexity |
|---|-------|------:|--------:|----------:|------:|------:|-----------:|
| 1 | draft-templin-intarea-aero2 | 113 | 1 | 83 | 1 | 3.40 | 0.572 |
| 2 | draft-templin-intarea-aero | 120 | 1 | 73 | 1 | 3.40 | 0.556 |
| 3 | draft-templin-6man-aero3 | 99 | 1 | 82 | 1 | 3.40 | 0.542 |
| 4 | draft-ietf-anima-constrained-voucher | 93 | 4 | 62 | 1 | 4.20 | 0.520 |
| 5 | draft-ietf-ace-edhoc-oscore-profile | 89 | 4 | 52 | 1 | 3.60 | 0.482 |
| 6 | draft-ietf-rats-corim | 127 | 5 | 0 | 1 | 3.40 | 0.417 |
| 7 | draft-barnes-hpke-hpke | 125 | 4 | 0 | 1 | 3.80 | 0.396 |
| 8 | draft-xu-rtgwg-fare-in-sun | 9 | 15 | 6 | 1 | 2.80 | 0.369 |
| 9 | draft-cui-ai-agent-discovery-invocation | 18 | 3 | 10 | 3 | 3.80 | 0.366 |
| 10 | draft-narajala-ans | 48 | 4 | 12 | 2 | 3.60 | 0.364 |
## Top 10 Most Efficient Drafts
*High ratings despite low structural complexity.*
| # | Draft | Pages | Authors | Score | Efficiency |
|---|-------|------:|--------:|------:|-----------:|
| 1 | w3c-UAAG20-Reference | - | 0 | 3.60 | 20.6 |
| 2 | iso-ts-23860-2022 | - | 0 | 3.40 | 19.4 |
| 3 | iso-ts-17575-3-2011-cor-1-2013 | - | 0 | 3.20 | 18.3 |
| 4 | iso-awi-tr-24492 | - | 0 | 3.20 | 18.3 |
| 5 | iso-iec-22989-2022-awi-amd-2 | - | 0 | 3.20 | 18.3 |
| 6 | iso-iec-23053-2022-awi-amd-2 | - | 0 | 3.20 | 18.3 |
| 7 | iso-iec-15938-18-2023 | - | 0 | 3.00 | 17.1 |
| 8 | iso-iec-tr-24030-2024 | - | 0 | 3.00 | 17.1 |
| 9 | iso-iec-cd-ts-22440-3 | - | 0 | 3.00 | 17.1 |
| 10 | iso-37181-2022 | - | 0 | 4.40 | 17.1 |
## Summary Statistics
- **Average pages**: 20.2 (57.6% coverage)
- **Average authors**: 1.7
- **Average citations**: 4.9
- **Total drafts analyzed**: 688

View File

@@ -0,0 +1,152 @@
# False Positive Profile Report
*Generated 2026-03-08 18:04 UTC*
## Overview
- **False positives**: 73 (9.6% of 761 total drafts)
- **% of rated**: 9.6% of 761 rated drafts
- These drafts matched AI/agent search keywords but were flagged as not genuinely about AI agent infrastructure.
## By Source
| Source | FP Count | % of FPs |
|--------|--------:|---------:|
| IETF | 73 | 100.0% |
## Rating Comparison: FP vs Non-FP
| Dimension | FP Avg | Non-FP Avg | Delta |
|-----------|------:|-----------:|------:|
| Novelty | 2.51 | 3.16 | -0.66 |
| Maturity | 3.26 | 3.13 | +0.13 |
| Overlap | 2.68 | 2.61 | +0.07 |
| Momentum | 2.68 | 3.07 | -0.39 |
| Relevance | 2.51 | 3.77 | -1.27 |
## Categories Assigned to False Positives
| Category | Count |
|----------|------:|
| Data formats/interop | 28 |
| Agent identity/auth | 25 |
| Other AI/agent | 16 |
| Policy/governance | 12 |
| Autonomous netops | 11 |
| A2A protocols | 9 |
| Agent discovery/reg | 7 |
| AI safety/alignment | 3 |
| ML traffic mgmt | 2 |
| Human-agent interaction | 1 |
## Top Terms in FP Abstracts
| Term | Occurrences |
|------|------------:|
| document | 79 |
| protocol | 60 |
| key | 60 |
| agent | 38 |
| network | 33 |
| data | 29 |
| defines | 27 |
| ipv | 27 |
| edhoc | 27 |
| information | 26 |
| rfc | 26 |
| diffie | 25 |
| hellman | 25 |
| list | 25 |
| security | 23 |
| user | 20 |
| authentication | 19 |
| public | 18 |
| control | 17 |
| configuration | 17 |
| mechanism | 17 |
| provides | 16 |
| header | 16 |
| specifies | 16 |
| ephemeral | 16 |
| secure | 15 |
| server | 15 |
| internet | 15 |
| certificate | 15 |
| multi | 15 |
## All False Positives
| Draft | Title | Source | Relevance | Categories |
|-------|-------|--------|----------:|------------|
| draft-ahc-green-smartpdu-yang | A YANG Model for SmartPDU Monitoring and Control | IETF | 3 | Data formats/interop, Policy/governance |
| draft-allman-tcpx2-hack | TCPx2: Don't Fence Me In | IETF | 3 | Other AI/agent |
| draft-amsuess-ace-brski-ace | Provisioning ACE credentials through BRSKI | IETF | 3 | Agent identity/auth, Autonomous netops |
| draft-bastian-jose-dvs | Public Key Derived HMAC for JOSE | IETF | 3 | Agent identity/auth, Data formats/interop |
| draft-bastian-jose-pkdh | Public Key Derived HMAC for JOSE | IETF | 3 | Data formats/interop |
| draft-condrey-rats-witnessd-enrollment | Trust Anchor Bootstrap Protocol for Proof of Proce | IETF | 3 | Agent identity/auth, Policy/governance |
| draft-contario-totp-secure-enrollment | TOTP Secure Enrollment | IETF | 3 | Agent identity/auth, AI safety/alignment |
| draft-daviel-html-geo-tag | Geographic registration of HTML documents | IETF | 2 | Data formats/interop, Agent discovery/reg |
| draft-doujiali-cloudnetwork-intelligentoperation | An requirement of Cloud Network Intelligent Operat | IETF | 2 | Autonomous netops, Policy/governance |
| draft-ecdh-psi | PSI based on ECDH | IETF | 3 | Data formats/interop |
| draft-eggert-mailmaint-uaautoconf | Automatic Configuration of Email, Calendar, and Co | IETF | 4 | Agent discovery/reg, Data formats/interop |
| draft-gont-dhcwg-dhcpv6-iids | A Method for Generating Semantically Opaque IPv6 I | IETF | 2 | Agent identity/auth, Data formats/interop |
| draft-hackett-ures | Unified Rendering of Email Standard (URES) | IETF | 2 | Data formats/interop, Other AI/agent |
| draft-he-yi-srv6ops-ipv6-enhancemnet-in-cloud-uc | Use Cases and Requirements for IPv6 enhancement te | IETF | 3 | Agent identity/auth, Autonomous netops |
| draft-housley-lamps-private-key-attest-attr | An Attribute for Statement of Possession of a Priv | IETF | 3 | Agent identity/auth, Data formats/interop |
| draft-hy-srv6ops-sfc-in-cloud-uc | Use Cases and Requirements for Service Function Ch | IETF | 3 | Autonomous netops, Data formats/interop |
| draft-ietf-anima-brski-prm | BRSKI with Pledge in Responder Mode (BRSKI-PRM) | IETF | 2 | Agent identity/auth, Autonomous netops |
| draft-ietf-bgp-idrp-usage | Application of the Border Gateway Protocol and IDR | IETF | 2 | A2A protocols, Policy/governance |
| draft-ietf-dnsop-ds-automation | Operational Recommendations for DS Automation | IETF | 1 | Other AI/agent |
| draft-ietf-dtn-bpv7-admin-iana | Bundle Protocol Version 7 Administrative Record Ty | IETF | 2 | Data formats/interop |
| draft-ietf-emu-eap-edhoc | Using the Extensible Authentication Protocol (EAP) | IETF | 3 | Agent identity/auth |
| draft-ietf-hpke-hpke | Hybrid Public Key Encryption | IETF | 5 | Agent identity/auth, Data formats/interop |
| draft-ietf-httpbis-layered-cookies | Cookies: HTTP State Management Mechanism | IETF | 2 | Other AI/agent |
| draft-ietf-httpbis-rfc6265bis | Cookies: HTTP State Management Mechanism | IETF | 2 | Other AI/agent |
| draft-ietf-idr-bgp-dpa | Destination Preference Attribute for BGP | IETF | 3 | A2A protocols |
| draft-ietf-lake-edhoc-impl-cons | Implementation Considerations for Ephemeral Diffie | IETF | 3 | Agent identity/auth |
| draft-ietf-lake-traces | Traces of EDHOC | IETF | 3 | Data formats/interop |
| draft-ietf-lamps-e2e-mail-guidance | Guidance on End-to-End E-mail Security | IETF | 2 | Data formats/interop, Policy/governance |
| draft-ietf-lamps-private-key-stmt-attr | An Attribute for Statement of Possession of a Priv | IETF | 3 | Data formats/interop |
| draft-ietf-lamps-rfc5274bis | Certificate Management Messages over CMS (CMC): Co | IETF | 3 | Data formats/interop |
| draft-ietf-mailmaint-pacc | Automatic Configuration of Email, Calendar, and Co | IETF | 2 | Data formats/interop |
| draft-ietf-pim-zeroconf-mcast-addr-alloc-ps | Zeroconf Multicast Address Allocation Problem Stat | IETF | 2 | Agent discovery/reg, Data formats/interop |
| draft-ietf-roll-enrollment-priority | Controlling Secure Network Enrollment in RPL netwo | IETF | 3 | Agent discovery/reg, Autonomous netops |
| draft-ietf-sip-location-conveyance | Location Conveyance for the Session Initiation Pro | IETF | 3 | A2A protocols, Agent discovery/reg |
| draft-ietf-sshm-ssh-agent | SSH Agent Protocol | IETF | 2 | Agent identity/auth |
| draft-ietf-suit-firmware-encryption | Encrypted Payloads in SUIT Manifests | IETF | 4 | Data formats/interop |
| draft-ingles-eap-edhoc | Using the Extensible Authentication Protocol with | IETF | 3 | Agent identity/auth, Data formats/interop |
| draft-jaju-httpbis-zstd-window-size | Window Sizing for Zstandard Content Encoding | IETF | 2 | Data formats/interop |
| draft-khatri-sipcore-call-transfer-fail-response | A SIP Response Code (497) for Call Transfer Failur | IETF | 3 | Other AI/agent |
| draft-kompella-lsr-mptecap | Multipath Traffic Engineering Capabilities | IETF | 2 | ML traffic mgmt |
| draft-lenders-core-dnr | Discovery of Network-designated OSCORE-based Resol | IETF | 3 | Agent discovery/reg, Data formats/interop |
| draft-leon-distributed-multi-signer | Distributed DNSSEC Multi-Signer Bootstrap | IETF | 2 | Autonomous netops, Data formats/interop |
| draft-liu-access-collaboration-agent | Ubiquitous Access Collaboration Requirements for A | IETF | 2 | A2A protocols, Other AI/agent |
| draft-lopez-lake-edhoc-psk | EDHOC PSK authentication | IETF | 3 | Agent identity/auth |
| draft-ma-v6ops-pe-ipv6only-reqs | Requirements for Provider Edge in IPv6-only Underl | IETF | 3 | Other AI/agent |
| draft-men-rtgwg-agent-networking-in-digibank | Agent Networking Scenarios of Digital Banking | IETF | 2 | A2A protocols, Agent discovery/reg |
| draft-meyerzuselha-oauth-web-message-response-mode | OAuth 2.0 Web Message Response Mode for Popup- and | IETF | 2 | Agent identity/auth, Human-agent interaction |
| draft-moonesamy-rfc2369bis | The Use of URIs as Meta-Syntax for Core Mail List | IETF | 2 | Other AI/agent |
| draft-moonesamy-rfc2919bis | List-Id: A Structured Field and Namespace for the | IETF | 2 | Other AI/agent |
| draft-moreno-lisp-uberlay | Uberlay Interconnection of Multiple LISP overlays | IETF | 3 | Data formats/interop, Autonomous netops |
| draft-mvieuille-kerpass-ephemsec | KerPass EPHEMSEC One-Time Password Algorithm | IETF | 3 | Agent identity/auth, AI safety/alignment |
| draft-mzhang-nfsv4-recursively-setting | Recursively Setting Attributes of Subdirectories a | IETF | 2 | Data formats/interop |
| draft-nsiangani-authenticatedsecuredlayer | ASL Authenticated Secure Layer Protocol | IETF | 1 | Other AI/agent |
| draft-ounsworth-lamps-cms-dhkem | Use of the DH-Based KEM (DHKEM) in the Cryptograph | IETF | 3 | Data formats/interop, A2A protocols |
| draft-pan-aqm-pie | PIE: A Lightweight Control Scheme To Address the B | IETF | 2 | Other AI/agent |
| draft-pan-tsvwg-pie | PIE: A Lightweight Control Scheme To Address the B | IETF | 3 | ML traffic mgmt |
| draft-ruas-cfrg-ecdp | ECDP: Elliptic Curve Data Protocol | IETF | 3 | A2A protocols, Agent identity/auth |
| draft-serafin-lake-ta-hint | Trust Anchor Hints in Ephemeral Diffie-Hellman Ove | IETF | 3 | Agent identity/auth |
| draft-sipos-dtn-bp-safe | Bundle Protocol (BP) Security Associations with Fe | IETF | 2 | Agent identity/auth, Autonomous netops |
| draft-steckbeck-ua-conn-sec | User Agent Connection Security | IETF | 2 | Policy/governance, Other AI/agent |
| draft-takagi-srta-trinity | SRTA and the Trinity Configuration: A Conceptual A | IETF | 2 | AI safety/alignment, Policy/governance |
| draft-templin-6man-mla | IPv6 Addresses for Ad Hoc Networks | IETF | 3 | A2A protocols |
| draft-templin-manet-inet | MANET Internetworking: Problem Statement and Gap A | IETF | 2 | Other AI/agent |
| draft-tiloca-lake-exporter-output-length | In-band Agreement of Output Lengths for the EDHOC_ | IETF | 2 | Agent identity/auth |
| draft-tjw-dbound2-problem-statement | Domain Boundaries 2.0 Problem Statement | IETF | 2 | Agent identity/auth, Policy/governance |
| draft-tojens-dhcp-option-concat-considerations | DHCP Option Concatenation Considerations | IETF | 3 | Data formats/interop |
| draft-tu-nmrg-blockchain-trusted-protocol | A Blockchain Trusted Protocol for Intelligent Comm | IETF | 2 | Policy/governance, Agent identity/auth |
| draft-vattaparambil-positioning-of-poa | Positioning of PoA | IETF | 2 | Agent identity/auth, Policy/governance |
| draft-wang-data-transmission-security-irii | Data Transmission Security of Identity Resolution | IETF | 2 | Agent identity/auth, Policy/governance |
| draft-wendt-stir-vesper | VESPER - Framework for VErifiable STI Personas | IETF | 2 | Agent identity/auth, Data formats/interop |
| draft-willman-rtgwg-conduit-tunnels | Underlay for IPsec Transport | IETF | 2 | Autonomous netops, Policy/governance |
| draft-zhul-dhc-bnc-up-specific-suboption | Broadband Network UP-Specific Information Suboptio | IETF | 2 | Other AI/agent |
| draft-zhul-intarea-bnc-up-specific-suboption | Broadband Network UP-Specific Information Suboptio | IETF | 2 | Other AI/agent |

44
data/reports/sources.md Normal file
View File

@@ -0,0 +1,44 @@
# Cross-Source Comparison Report
*Generated 2026-03-08 18:04 UTC — 761 drafts across 6 sources*
## Summary
| Source | Drafts | Rated | Authors | Ideas | Avg Score | Top Category |
|--------|-------:|------:|--------:|------:|----------:|--------------|
| ETSI | 12 | 12 | 0 | 13 | 3.16 | Policy/governance |
| IETF | 469 | 396 | 722 | 495 | 3.43 | Data formats/interop |
| ISO | 238 | 238 | 0 | 245 | 3.12 | Policy/governance |
| ITU | 20 | 20 | 0 | 20 | 3.52 | Policy/governance |
| NIST | 11 | 11 | 0 | 12 | 3.89 | AI safety/alignment |
| W3C | 11 | 11 | 0 | 11 | 2.61 | Human-agent interaction |
## Rating Dimensions by Source
| Source | Novelty | Maturity | Overlap | Momentum | Relevance |
|--------|--------:|---------:|--------:|---------:|----------:|
| ETSI | 2.58 | 3.08 | 2.75 | 3.08 | 3.92 |
| IETF | 3.41 | 2.98 | 2.55 | 3.09 | 4.03 |
| ISO | 2.84 | 3.26 | 2.65 | 2.96 | 3.35 |
| ITU | 2.95 | 3.85 | 2.95 | 3.80 | 3.95 |
| NIST | 3.36 | 4.00 | 2.64 | 4.18 | 4.45 |
| W3C | 2.09 | 3.55 | 3.64 | 2.36 | 2.73 |
## Category Distribution by Source
| Category | ETSI | IETF | ISO | ITU | NIST | W3C |
|----------|------:|------:|------:|------:|------:|------:|
| A2A protocols | 1 | 151 | 27 | 2 | 1 | 0 |
| AI safety/alignment | 7 | 46 | 90 | 8 | 9 | 0 |
| Agent discovery/reg | 1 | 86 | 6 | 4 | 0 | 1 |
| Agent identity/auth | 2 | 148 | 20 | 3 | 1 | 0 |
| Autonomous netops | 3 | 113 | 39 | 8 | 2 | 0 |
| Data formats/interop | 4 | 162 | 118 | 8 | 3 | 4 |
| Human-agent interaction | 0 | 32 | 39 | 3 | 2 | 9 |
| ML traffic mgmt | 2 | 79 | 10 | 3 | 0 | 0 |
| Model serving/inference | 0 | 44 | 41 | 5 | 6 | 0 |
| Other AI/agent | 7 | 22 | 69 | 5 | 3 | 1 |
| Policy/governance | 10 | 114 | 157 | 17 | 8 | 5 |
## Category Coverage Analysis
**Shared categories** (covered by 2+ bodies): 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

View File

@@ -1,72 +1,132 @@
# Category Trend Analysis # Temporal Evolution Report
*Generated 2026-03-03 19:59 UTC — 361 drafts, 19 months, 19 categories* *Generated 2026-03-08 18:05 UTC — 500 drafts, 87 months*
## Growth Summary ## Monthly Overview
| Month | Submissions | New Authors | Cum. Ideas | Avg Novelty | Avg Maturity | Avg Relevance | Safety Ratio |
|-------|------------:|------------:|-----------:|------------:|-------------:|--------------:|-------------:|
| 1995-12 | 1 | 0 | 0 | 0.0 | 0.0 | 0.0 | - |
| 1996112 | 1 → | 0 | 1 | 0.0 | 0.0 | 0.0 | - |
| 1997-12 | 1 → | 0 | 0 | 0.0 | 0.0 | 0.0 | - |
| 1997123 | 1 → | 0 | 2 | 0.0 | 0.0 | 0.0 | - |
| 1999-08 | 2 ↑ | 0 | 0 | 0.0 | 0.0 | 0.0 | - |
| 1999020 | 1 ↓ | 0 | 3 | 0.0 | 0.0 | 0.0 | - |
| 2002121 | 2 ↑ | 0 | 5 | 3.0 | 5.0 | 3.0 | - |
| 2003012 | 1 ↓ | 0 | 7 | 0.0 | 0.0 | 0.0 | - |
| 2004-01 | 1 → | 0 | 9 | 0.0 | 0.0 | 0.0 | - |
| 2007-02 | 1 → | 0 | 0 | 0.0 | 0.0 | 0.0 | - |
| 2007103 | 1 → | 0 | 10 | 0.0 | 0.0 | 0.0 | - |
| 2009-10 | 1 → | 0 | 11 | 0.0 | 0.0 | 0.0 | - |
| 2009-11 | 1 → | 0 | 12 | 0.0 | 0.0 | 0.0 | - |
| 2010-02 | 1 → | 0 | 13 | 0.0 | 0.0 | 0.0 | - |
| 2010-06 | 2 ↑ | 0 | 16 | 0.0 | 0.0 | 0.0 | - |
| 2011-04 | 2 → | 0 | 18 | 0.0 | 0.0 | 0.0 | - |
| 2012-02 | 1 ↓ | 0 | 19 | 2.0 | 5.0 | 2.0 | - |
| 2013-03 | 3 ↑ | 0 | 0 | 0.0 | 0.0 | 0.0 | - |
| 2014-03 | 1 ↓ | 0 | 20 | 0.0 | 0.0 | 0.0 | - |
| 2014-10 | 1 → | 0 | 21 | 0.0 | 0.0 | 0.0 | - |
| 2014032 | 1 → | 0 | 22 | 2.0 | 5.0 | 4.0 | - |
| 2015-11 | 2 ↑ | 0 | 25 | 0.0 | 0.0 | 0.0 | - |
| 2015/CD | 1 ↓ | 0 | 0 | 0.0 | 0.0 | 0.0 | - |
| 2015121 | 2 ↑ | 0 | 26 | 2.5 | 5.0 | 4.0 | - |
| 2016-01 | 3 ↑ | 0 | 30 | 0.0 | 0.0 | 0.0 | - |
| 2017-05 | 1 ↓ | 0 | 31 | 2.0 | 4.0 | 3.0 | - |
| 2017-06 | 3 ↑ | 0 | 32 | 0.0 | 0.0 | 0.0 | - |
| 2017-10 | 1 ↓ | 0 | 33 | 0.0 | 0.0 | 0.0 | - |
| 2018-01 | 1 → | 0 | 35 | 3.0 | 4.0 | 3.0 | - |
| 2018-02 | 1 → | 0 | 36 | 0.0 | 0.0 | 0.0 | - |
| 2019-02 | 1 → | 0 | 37 | 3.0 | 5.0 | 2.0 | - |
| 2019-07 | 1 → | 0 | 38 | 3.0 | 5.0 | 3.0 | - |
| 2019-11 | 1 → | 0 | 40 | 3.0 | 5.0 | 2.0 | - |
| 2020 | 1 → | 0 | 41 | 3.0 | 3.0 | 4.0 | - |
| 2020-03 | 1 → | 0 | 42 | 3.0 | 5.0 | 4.0 | - |
| 2020-08 | 1 → | 0 | 43 | 3.0 | 4.0 | 3.0 | - |
| 2020-09 | 1 → | 0 | 44 | 4.0 | 4.0 | 4.0 | - |
| 2021 | 2 ↑ | 0 | 45 | 0.0 | 0.0 | 0.0 | - |
| 2021-01 | 1 ↓ | 0 | 47 | 3.0 | 4.0 | 4.0 | - |
| 2021-03 | 1 → | 0 | 49 | 0.0 | 0.0 | 0.0 | - |
| 2021-07 | 1 → | 0 | 51 | 4.0 | 4.0 | 5.0 | - |
| 2021-08 | 1 → | 0 | 52 | 3.0 | 4.0 | 4.0 | - |
| 2021-11 | 1 → | 0 | 53 | 4.0 | 3.0 | 4.0 | - |
| 2021-12 | 1 → | 0 | 54 | 0.0 | 0.0 | 0.0 | - |
| 2022 | 3 ↑ | 0 | 57 | 3.0 | 4.3 | 4.3 | - |
| 2022-05 | 1 ↓ | 0 | 0 | 3.0 | 4.0 | 3.0 | - |
| 2022-06 | 2 ↑ | 0 | 59 | 4.0 | 5.0 | 4.5 | - |
| 2022-07 | 1 ↓ | 0 | 60 | 3.0 | 4.0 | 3.0 | - |
| 2022-08 | 3 ↑ | 0 | 64 | 2.5 | 4.5 | 2.5 | - |
| 2022-09 | 1 ↓ | 0 | 66 | 0.0 | 0.0 | 0.0 | - |
| 2022-10 | 2 ↑ | 0 | 68 | 3.0 | 4.0 | 4.0 | - |
| 2022-11 | 1 ↓ | 0 | 69 | 3.0 | 4.0 | 2.0 | - |
| 2022/AW | 2 ↑ | 0 | 0 | 0.0 | 0.0 | 0.0 | - |
| 2022/DA | 2 → | 0 | 71 | 2.0 | 4.0 | 3.5 | - |
| 2023 | 3 ↑ | 0 | 74 | 3.7 | 4.7 | 4.7 | - |
| 2023-01 | 2 ↓ | 0 | 76 | 3.0 | 4.5 | 5.0 | - |
| 2023-03 | 1 ↓ | 0 | 77 | 3.0 | 5.0 | 4.0 | - |
| 2023-05 | 1 → | 0 | 78 | 3.0 | 4.0 | 5.0 | - |
| 2023-06 | 1 → | 0 | 79 | 3.0 | 4.0 | 5.0 | - |
| 2023-07 | 3 ↑ | 0 | 81 | 3.5 | 4.0 | 4.0 | - |
| 2023-08 | 1 ↓ | 0 | 82 | 4.0 | 3.0 | 4.0 | - |
| 2023-11 | 1 → | 0 | 84 | 2.0 | 4.0 | 3.0 | - |
| 2024 | 3 ↑ | 0 | 87 | 3.7 | 3.7 | 5.0 | - |
| 2024-01 | 10 ↑ | 17 | 98 | 3.0 | 3.8 | 4.0 | - |
| 2024-02 | 5 ↓ | 5 | 103 | 3.0 | 3.5 | 4.0 | - |
| 2024-03 | 3 ↓ | 5 | 106 | 3.0 | 4.5 | 4.0 | - |
| 2024-04 | 9 ↑ | 9 | 114 | 3.5 | 3.2 | 4.0 | - |
| 2024-05 | 5 ↓ | 11 | 119 | 3.0 | 4.0 | 3.5 | - |
| 2024-06 | 3 ↓ | 8 | 121 | 3.0 | 5.0 | 2.0 | - |
| 2024-07 | 13 ↑ | 10 | 136 | 2.8 | 4.2 | 3.8 | - |
| 2024-08 | 4 ↓ | 5 | 140 | 3.5 | 4.0 | 4.0 | - |
| 2024-09 | 13 ↑ | 34 | 153 | 3.0 | 3.4 | 4.0 | - |
| 2024-10 | 3 ↓ | 9 | 157 | 3.0 | 4.0 | 4.0 | - |
| 2024-11 | 6 ↑ | 12 | 164 | 2.8 | 4.2 | 3.6 | - |
| 2024-12 | 10 ↑ | 14 | 174 | 3.6 | 4.0 | 4.4 | - |
| 2025 | 1 ↓ | 0 | 175 | 4.0 | 2.0 | 4.0 | - |
| 2025-01 | 8 ↑ | 18 | 182 | 3.7 | 3.0 | 4.3 | - |
| 2025-02 | 3 ↓ | 4 | 185 | 2.7 | 4.7 | 3.7 | - |
| 2025-03 | 6 ↑ | 4 | 192 | 3.0 | 4.3 | 3.2 | - |
| 2025-04 | 13 ↑ | 31 | 206 | 3.0 | 3.7 | 4.2 | - |
| 2025-05 | 8 ↓ | 13 | 214 | 3.3 | 3.4 | 4.3 | - |
| 2025-06 | 5 ↓ | 15 | 222 | 3.0 | 3.7 | 4.0 | - |
| 2025-07 | 9 ↑ | 4 | 231 | 3.8 | 3.1 | 4.1 | - |
| 2025-08 | 10 ↑ | 15 | 242 | 3.3 | 3.7 | 3.7 | - |
| 2025-09 | 19 ↑ | 38 | 263 | 3.5 | 3.2 | 3.8 | - |
| 2025-10 | 65 ↑ | 90 | 330 | 3.5 | 3.0 | 4.1 | - |
| 2025-11 | 27 ↓ | 65 | 390 | 3.6 | 3.1 | 4.3 | - |
## Category Growth Summary
| Category | Total | Last 3mo | Prev 3mo | Growth | | Category | Total | Last 3mo | Prev 3mo | Growth |
|----------|------:|---------:|---------:|-------:| |----------|------:|---------:|---------:|-------:|
| A2A protocols | 120 | 58 | 54 | +7% | | A2A protocols | 49 | 34 | 3 | +1033% |
| AI safety / guardrails / alignment | 1 | 1 | 0 | new | | AI safety/alignment | 53 | 13 | 5 | +160% |
| AI safety/alignment | 44 | 21 | 15 | +40% | | Agent discovery/reg | 33 | 20 | 3 | +567% |
| Agent discovery / registration | 14 | 9 | 5 | +80% | | Agent identity/auth | 58 | 27 | 7 | +286% |
| Agent discovery/reg | 65 | 28 | 32 | -12% | | Autonomous netops | 52 | 27 | 2 | +1250% |
| Agent identity/auth | 108 | 46 | 51 | -10% | | Data formats/interop | 105 | 44 | 6 | +633% |
| Agent-to-agent communication protocols | 16 | 11 | 5 | +120% | | Human-agent interaction | 33 | 11 | 3 | +267% |
| Autonomous netops | 93 | 38 | 40 | -5% | | ML traffic mgmt | 29 | 17 | 2 | +750% |
| Autonomous network operations | 5 | 4 | 1 | +300% | | Model serving/inference | 25 | 9 | 2 | +350% |
| Data formats / semantics for AI interop | 3 | 2 | 1 | +100% | | Other AI/agent | 33 | 3 | 0 | new |
| Data formats/interop | 145 | 52 | 66 | -21% | | Policy/governance | 101 | 19 | 5 | +280% |
| Human-agent interaction | 30 | 9 | 17 | -47% |
| Identity / authentication for AI agents | 13 | 9 | 4 | +125% |
| ML traffic mgmt | 73 | 33 | 25 | +32% |
| ML-based traffic management / optimization | 1 | 1 | 0 | new |
| Model serving/inference | 42 | 22 | 14 | +57% |
| Other AI/agent | 26 | 13 | 9 | +44% |
| Policy / governance / ethical frameworks | 2 | 2 | 0 | new |
| Policy/governance | 91 | 42 | 31 | +35% |
## Monthly Breakdown
| Month | A2A protocols | AI safety / gua | AI safety/align | Agent discovery | Agent discovery | Agent identity/ | Agent-to-agent | Autonomous neto | Autonomous netw | Data formats / | Data formats/in | Human-agent int | Identity / auth | ML traffic mgmt | ML-based traffi | Model serving/i | Other AI/agent | Policy / govern | Policy/governan | Total |
|-------|---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | -----:|
| 2024-01 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 5 |
| 2024-02 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 2 |
| 2024-04 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 3 |
| 2024-09 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 3 |
| 2024-10 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 2 |
| 2024-12 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
| 2025-01 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 2 | 0 | 0 | 4 | 0 | 2 | 0 | 0 | 0 | 8 |
| 2025-04 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 2 | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 3 | 10 |
| 2025-05 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 2 | 5 |
| 2025-06 | 1 | 0 | 0 | 0 | 0 | 2 | 0 | 3 | 0 | 0 | 1 | 0 | 0 | 3 | 0 | 1 | 0 | 0 | 1 | 12 |
| 2025-07 | 2 | 0 | 2 | 0 | 1 | 2 | 0 | 0 | 0 | 0 | 2 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 11 |
| 2025-08 | 0 | 0 | 0 | 0 | 0 | 2 | 0 | 2 | 0 | 0 | 6 | 1 | 0 | 0 | 0 | 0 | 2 | 0 | 3 | 16 |
| 2025-09 | 4 | 0 | 4 | 0 | 3 | 3 | 0 | 6 | 0 | 0 | 11 | 1 | 0 | 4 | 0 | 1 | 0 | 0 | 6 | 43 |
| 2025-10 | 26 | 0 | 5 | 2 | 13 | 23 | 2 | 19 | 0 | 1 | 35 | 7 | 1 | 13 | 0 | 9 | 5 | 0 | 10 | 171 |
| 2025-11 | 26 | 0 | 7 | 2 | 16 | 21 | 3 | 17 | 1 | 0 | 25 | 9 | 2 | 9 | 0 | 4 | 2 | 0 | 15 | 159 |
| 2025-12 | 2 | 0 | 3 | 1 | 3 | 7 | 0 | 4 | 0 | 0 | 6 | 1 | 1 | 3 | 0 | 1 | 2 | 0 | 6 | 40 |
| 2026-01 | 13 | 0 | 8 | 7 | 8 | 14 | 9 | 12 | 4 | 2 | 15 | 2 | 4 | 7 | 1 | 5 | 4 | 1 | 13 | 129 |
| 2026-02 | 36 | 1 | 12 | 2 | 17 | 29 | 2 | 18 | 0 | 0 | 32 | 6 | 5 | 15 | 0 | 8 | 8 | 1 | 23 | 215 |
| 2026-03 | 9 | 0 | 1 | 0 | 3 | 3 | 0 | 8 | 0 | 0 | 5 | 1 | 0 | 11 | 0 | 9 | 1 | 0 | 6 | 57 |
## Fastest Growing Categories (early vs late half) ## Fastest Growing Categories (early vs late half)
- **AI safety / guardrails / alignment**: new (0 -> 1 drafts) - **Model serving/inference**: new (0 → 25 drafts)
- **Agent discovery / registration**: new (0 -> 14 drafts) - **A2A protocols**: +4700% (1 → 48 drafts)
- **Agent-to-agent communication protocols**: new (0 -> 16 drafts) - **Agent discovery/reg**: +3100% (1 → 32 drafts)
- **Autonomous network operations**: new (0 -> 5 drafts) - **Agent identity/auth**: +2700% (2 → 56 drafts)
- **Data formats / semantics for AI interop**: new (0 -> 3 drafts) - **Other AI/agent**: +1450% (2 → 31 drafts)
- **Identity / authentication for AI agents**: new (0 -> 13 drafts) - **Data formats/interop**: +1112% (8 → 97 drafts)
- **ML-based traffic management / optimization**: new (0 -> 1 drafts) - **Autonomous netops**: +1100% (4 → 48 drafts)
- **Policy / governance / ethical frameworks**: new (0 -> 2 drafts) - **ML traffic mgmt**: +767% (3 → 26 drafts)
- **A2A protocols**: +11800% (1 -> 119 drafts) - **Policy/governance**: +718% (11 → 90 drafts)
- **Agent discovery/reg**: +6300% (1 -> 64 drafts) - **AI safety/alignment**: +683% (6 → 47 drafts)
- **Agent identity/auth**: +5200% (2 -> 106 drafts) - **Human-agent interaction**: +350% (6 → 27 drafts)
- **Human-agent interaction**: +2800% (1 -> 29 drafts)
- **Autonomous netops**: +2125% (4 -> 89 drafts) ## Rating Dimension Trends
- **AI safety/alignment**: +2000% (2 -> 42 drafts)
- **Data formats/interop**: +1871% (7 -> 138 drafts) - **Novelty**: 2.94 → 3.33 (+0.38) ↑
- **Other AI/agent**: +1100% (2 -> 24 drafts) - **Maturity**: 4.39 → 3.52 (-0.87) ↓
- **Policy/governance**: +1100% (7 -> 84 drafts) - **Overlap**: 2.72 → 2.54 (-0.18) ↓
- **Model serving/inference**: +850% (4 -> 38 drafts) - **Momentum**: 3.11 → 3.50 (+0.39) ↑
- **ML traffic mgmt**: +712% (8 -> 65 drafts) - **Relevance**: 3.44 → 4.00 (+0.56) ↑

View File

@@ -835,6 +835,51 @@ def wg_report(cfg, db):
console.print(f"Report saved: [bold]{path}[/]") console.print(f"Report saved: [bold]{path}[/]")
@report.command("sources")
@pass_cfg_db
def sources_report(cfg, db):
"""Cross-source comparison report — ratings and categories by standards body."""
from .reports import Reporter
path = Reporter(cfg, db).sources_report()
console.print(f"Report saved: [bold]{path}[/]")
@report.command("false-positives")
@pass_cfg_db
def false_positives_report(cfg, db):
"""False positive profiling report — what makes drafts look AI-related but not be."""
from .reports import Reporter
path = Reporter(cfg, db).false_positives_report()
console.print(f"Report saved: [bold]{path}[/]")
@report.command("citations")
@pass_cfg_db
def citations_report(cfg, db):
"""Citation influence and BCP dependency analysis."""
from .reports import Reporter
path = Reporter(cfg, db).citations_report()
console.print(f"Report saved: [bold]{path}[/]")
@report.command("complexity")
@pass_cfg_db
def complexity_report(cfg, db):
"""Draft complexity matrix: correlations between structural complexity and ratings."""
from .reports import Reporter
path = Reporter(cfg, db).complexity_report()
console.print(f"Report saved: [bold]{path}[/]")
@report.command("idea-analysis")
@pass_cfg_db
def idea_analysis_report(cfg, db):
"""Idea novelty deep dive — distribution, types, top ideas, cross-draft patterns."""
from .reports import Reporter
path = Reporter(cfg, db).idea_analysis()
console.print(f"Report saved: [bold]{path}[/]")
# ── wg (working group analysis) ───────────────────────────────────────── # ── wg (working group analysis) ─────────────────────────────────────────

View File

@@ -761,16 +761,30 @@ class Database:
).fetchall() ).fetchall()
return [r["name"] for r in rows] return [r["name"] for r in rows]
def all_ideas(self) -> list[dict]: def all_ideas(self, include_false_positives: bool = False) -> list[dict]:
if include_false_positives:
rows = self.conn.execute( rows = self.conn.execute(
"SELECT * FROM ideas ORDER BY draft_name" "SELECT * FROM ideas ORDER BY draft_name"
).fetchall() ).fetchall()
else:
rows = self.conn.execute(
"SELECT i.* FROM ideas i "
"WHERE i.draft_name NOT IN "
"(SELECT draft_name FROM ratings WHERE false_positive = 1) "
"ORDER BY i.draft_name"
).fetchall()
return [{"title": r["title"], "description": r["description"], return [{"title": r["title"], "description": r["description"],
"type": r["idea_type"], "draft_name": r["draft_name"], "type": r["idea_type"], "draft_name": r["draft_name"],
"novelty_score": r["novelty_score"]} for r in rows] "novelty_score": r["novelty_score"]} for r in rows]
def idea_count(self) -> int: def idea_count(self, include_false_positives: bool = False) -> int:
if include_false_positives:
return self.conn.execute("SELECT COUNT(*) FROM ideas").fetchone()[0] return self.conn.execute("SELECT COUNT(*) FROM ideas").fetchone()[0]
return self.conn.execute(
"SELECT COUNT(*) FROM ideas "
"WHERE draft_name NOT IN "
"(SELECT draft_name FROM ratings WHERE false_positive = 1)"
).fetchone()[0]
def ideas_with_drafts(self, unscored_only: bool = False, limit: int = 5000) -> list[dict]: def ideas_with_drafts(self, unscored_only: bool = False, limit: int = 5000) -> list[dict]:
"""Return ideas joined with draft title, optionally only unscored ones.""" """Return ideas joined with draft title, optionally only unscored ones."""

View File

@@ -1813,3 +1813,845 @@ class Reporter:
path = self.output_dir / "wg-analysis.md" path = self.output_dir / "wg-analysis.md"
path.write_text(report) path.write_text(report)
return str(path) return str(path)
def idea_analysis(self) -> str:
"""Generate an idea novelty deep-dive report with distribution, types, and top ideas."""
from collections import Counter
from difflib import SequenceMatcher
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
all_ideas = self.db.all_ideas()
total = len(all_ideas)
# Rating lookup
pairs = self.db.drafts_with_ratings(limit=500)
rating_map: dict[str, Rating] = {}
draft_map: dict[str, Draft] = {}
for draft, rating in pairs:
rating_map[draft.name] = rating
draft_map[draft.name] = draft
scored = [i for i in all_ideas if i.get("novelty_score") is not None]
avg_novelty = sum(i["novelty_score"] for i in scored) / len(scored) if scored else 0
# Embedding coverage
embed_count = self.db.conn.execute("SELECT COUNT(*) FROM idea_embeddings").fetchone()[0]
embed_pct = round(embed_count / total * 100, 1) if total > 0 else 0
lines = [
"# Idea Novelty Deep Dive",
f"*Generated {now}{total} ideas, {len(scored)} scored, avg novelty {avg_novelty:.2f}*\n",
f"**Embedding coverage**: {embed_count}/{total} ({embed_pct}%)\n",
]
# Novelty score distribution
novelty_dist = Counter(i["novelty_score"] for i in scored)
lines.extend([
"## Novelty Score Distribution\n",
"| Score | Count | Bar |",
"|------:|------:|-----|",
])
for s in [1, 2, 3, 4, 5]:
count = novelty_dist.get(s, 0)
bar = _bar(s) * min(count, 40)
lines.append(f"| {s} | {count} | {bar} |")
# Ideas by type with avg novelty
type_data: dict[str, dict] = defaultdict(lambda: {"count": 0, "n_sum": 0, "n_count": 0})
for idea in all_ideas:
t = idea.get("type", "other") or "other"
type_data[t]["count"] += 1
if idea.get("novelty_score") is not None:
type_data[t]["n_sum"] += idea["novelty_score"]
type_data[t]["n_count"] += 1
lines.extend([
"\n## Ideas by Type\n",
"| Type | Count | Avg Novelty |",
"|------|------:|------------:|",
])
for t, d in sorted(type_data.items(), key=lambda x: x[1]["count"], reverse=True):
avg = d["n_sum"] / d["n_count"] if d["n_count"] > 0 else 0
lines.append(f"| {t} | {d['count']} | {avg:.2f} |")
# Top 20 most novel ideas
top_novel = sorted(
[i for i in all_ideas if i.get("novelty_score") and i["novelty_score"] >= 4],
key=lambda x: x["novelty_score"],
reverse=True,
)[:20]
if top_novel:
lines.extend([
"\n## Top 20 Most Novel Ideas\n",
"| # | Score | Idea | Type | Draft |",
"|--:|------:|------|------|-------|",
])
for idx, idea in enumerate(top_novel, 1):
title = idea["title"][:50]
draft_short = idea["draft_name"].replace("draft-", "")[:35]
lines.append(
f"| {idx} | {idea['novelty_score']} "
f"| {title} | {idea.get('type', 'other')} "
f"| [{draft_short}](https://datatracker.ietf.org/doc/{idea['draft_name']}/) |"
)
# Ideas per draft distribution
ideas_per_draft = Counter(i["draft_name"] for i in all_ideas)
ipd_dist = Counter(ideas_per_draft.values())
lines.extend([
"\n## Ideas per Draft\n",
"| Ideas/Draft | Drafts |",
"|------------:|-------:|",
])
for k in sorted(ipd_dist.keys()):
lines.append(f"| {k} | {ipd_dist[k]} |")
# Most prolific drafts
lines.extend([
"\n### Most Prolific Drafts\n",
"| Draft | Ideas | Score |",
"|-------|------:|------:|",
])
for name, count in ideas_per_draft.most_common(10):
r = rating_map.get(name)
score = f"{r.composite_score:.2f}" if r else "--"
short = name.replace("draft-", "")[:40]
lines.append(f"| {short} | {count} | {score} |")
# Shared ideas
idea_groups: list[dict] = []
for idea in all_ideas:
title_lower = idea["title"].lower().strip()
matched = False
for group in idea_groups:
ratio = SequenceMatcher(None, title_lower, group["canonical"]).ratio()
if ratio >= 0.75:
group["ideas"].append(idea)
group["drafts"].add(idea["draft_name"])
matched = True
break
if not matched:
idea_groups.append({
"canonical": title_lower,
"title": idea["title"],
"ideas": [idea],
"drafts": {idea["draft_name"]},
})
shared = [g for g in idea_groups if len(g["drafts"]) >= 2]
shared.sort(key=lambda g: len(g["drafts"]), reverse=True)
if shared:
lines.extend([
f"\n## Shared Ideas ({len(shared)} ideas in 2+ drafts)\n",
"| Idea | Appearances | Drafts |",
"|------|------------:|--------|",
])
for g in shared[:30]:
draft_list = ", ".join(sorted(g["drafts"])[:5])
if len(g["drafts"]) > 5:
draft_list += f" +{len(g['drafts']) - 5} more"
lines.append(f"| {g['title']} | {len(g['drafts'])} | {draft_list} |")
# Cross-tab: type x source
type_source: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
for idea in all_ideas:
t = idea.get("type", "other") or "other"
r = rating_map.get(idea["draft_name"])
source = "ietf" # default
if idea["draft_name"] in draft_map:
source = getattr(draft_map[idea["draft_name"]], "source", "ietf") or "ietf"
type_source[t][source] += 1
sources = sorted(set(s for td in type_source.values() for s in td.keys()))
if len(sources) > 1:
header = "| Type | " + " | ".join(sources) + " |"
sep = "|------|" + "|".join(["-----:" for _ in sources]) + "|"
lines.extend(["\n## Ideas by Type x Source\n", header, sep])
for t in sorted(type_source.keys(), key=lambda x: sum(type_source[x].values()), reverse=True):
row = f"| {t} | " + " | ".join(str(type_source[t].get(s, 0)) for s in sources) + " |"
lines.append(row)
# Correlation hint
draft_idea_novelty: dict[str, list[int]] = defaultdict(list)
for idea in scored:
draft_idea_novelty[idea["draft_name"]].append(idea["novelty_score"])
corr_pairs = []
for name, scores_list in draft_idea_novelty.items():
r = rating_map.get(name)
if r and r.relevance:
corr_pairs.append((sum(scores_list) / len(scores_list), r.relevance))
if len(corr_pairs) > 10:
x_vals = [p[0] for p in corr_pairs]
y_vals = [p[1] for p in corr_pairs]
n = len(corr_pairs)
mean_x = sum(x_vals) / n
mean_y = sum(y_vals) / n
cov = sum((x - mean_x) * (y - mean_y) for x, y in zip(x_vals, y_vals)) / n
std_x = (sum((x - mean_x) ** 2 for x in x_vals) / n) ** 0.5
std_y = (sum((y - mean_y) ** 2 for y in y_vals) / n) ** 0.5
corr = cov / (std_x * std_y) if std_x > 0 and std_y > 0 else 0
lines.extend([
f"\n## Correlation: Idea Novelty vs Draft Relevance\n",
f"Pearson r = **{corr:.3f}** (n={n} drafts with both scores)\n",
f"{'Positive' if corr > 0 else 'Negative'} correlation — "
f"{'drafts with more novel ideas tend to receive higher relevance ratings' if corr > 0.1 else 'weak or no linear relationship between idea novelty and draft relevance'}.",
])
# Embedding note
lines.extend([
f"\n## Embedding Status\n",
f"{embed_count} of {total} ideas ({embed_pct}%) have embeddings.",
f"To complete the remaining {total - embed_count} embeddings, run:\n",
"```",
"ietf embed-ideas",
"```\n",
"This requires Ollama running locally with the configured embedding model.",
])
report = "\n".join(lines)
path = self.output_dir / "idea-analysis.md"
path.write_text(report)
return str(path)
def sources_report(self) -> str:
"""Cross-source comparison report — rating dimensions and categories by standards body."""
from collections import Counter as _Counter
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
pairs = self.db.drafts_with_ratings(limit=2000)
all_drafts = self.db.list_drafts(limit=5000)
# Draft counts by source
source_draft_counts: dict[str, int] = defaultdict(int)
for d in all_drafts:
src = getattr(d, "source", "ietf") or "ietf"
source_draft_counts[src] += 1
# Rating stats by source
source_ratings: dict[str, dict[str, list]] = defaultdict(lambda: {
"novelty": [], "maturity": [], "overlap": [], "momentum": [],
"relevance": [], "scores": [],
})
source_categories: dict[str, _Counter] = defaultdict(_Counter)
for draft, rating in pairs:
src = getattr(draft, "source", "ietf") or "ietf"
source_ratings[src]["novelty"].append(rating.novelty)
source_ratings[src]["maturity"].append(rating.maturity)
source_ratings[src]["overlap"].append(rating.overlap)
source_ratings[src]["momentum"].append(rating.momentum)
source_ratings[src]["relevance"].append(rating.relevance)
source_ratings[src]["scores"].append(rating.composite_score)
for cat in rating.categories:
source_categories[src][cat] += 1
# Author counts by source
source_author_counts: dict[str, int] = {}
try:
rows = self.db.conn.execute(
"""SELECT d.source, COUNT(DISTINCT da.person_id) as cnt
FROM drafts d JOIN draft_authors da ON d.name = da.draft_name
GROUP BY d.source"""
).fetchall()
for r in rows:
source_author_counts[r["source"] or "ietf"] = r["cnt"]
except Exception:
pass
# Idea counts by source
source_idea_counts: dict[str, int] = {}
try:
rows = self.db.conn.execute(
"""SELECT d.source, COUNT(*) as cnt
FROM ideas i JOIN drafts d ON i.draft_name = d.name
GROUP BY d.source"""
).fetchall()
for r in rows:
source_idea_counts[r["source"] or "ietf"] = r["cnt"]
except Exception:
pass
all_sources = sorted(set(source_draft_counts.keys()) | set(source_ratings.keys()))
lines = [
"# Cross-Source Comparison Report",
f"*Generated {now}{len(all_drafts)} drafts across {len(all_sources)} sources*\n",
"## Summary\n",
"| Source | Drafts | Rated | Authors | Ideas | Avg Score | Top Category |",
"|--------|-------:|------:|--------:|------:|----------:|--------------|",
]
for src in all_sources:
rats = source_ratings.get(src, {"scores": []})
cats = source_categories.get(src, _Counter())
top_cat = cats.most_common(1)[0][0] if cats else "N/A"
avg = sum(rats["scores"]) / len(rats["scores"]) if rats["scores"] else 0.0
lines.append(
f"| {src.upper()} | {source_draft_counts.get(src, 0)} "
f"| {len(rats['scores'])} "
f"| {source_author_counts.get(src, 0)} "
f"| {source_idea_counts.get(src, 0)} "
f"| {avg:.2f} | {top_cat} |"
)
# Dimension averages per source
dims = ["novelty", "maturity", "overlap", "momentum", "relevance"]
lines.extend([
"\n## Rating Dimensions by Source\n",
"| Source | Novelty | Maturity | Overlap | Momentum | Relevance |",
"|--------|--------:|---------:|--------:|---------:|----------:|",
])
for src in all_sources:
rats = source_ratings.get(src, {d: [] for d in dims})
vals = []
for d in dims:
v = rats.get(d, [])
vals.append(f"{sum(v)/len(v):.2f}" if v else "-")
lines.append(f"| {src.upper()} | {' | '.join(vals)} |")
# Category distribution per source
all_cats = sorted({cat for cats in source_categories.values() for cat in cats})
if all_cats:
lines.extend([
"\n## Category Distribution by Source\n",
"| Category | " + " | ".join(s.upper() for s in all_sources) + " |",
"|----------|" + "|".join("------:" for _ in all_sources) + "|",
])
for cat in all_cats:
vals = [str(source_categories.get(src, {}).get(cat, 0)) for src in all_sources]
lines.append(f"| {cat} | {' | '.join(vals)} |")
# Unique vs shared categories
source_cat_sets = {src: set(cats.keys()) for src, cats in source_categories.items()}
shared = set()
for s1, c1 in source_cat_sets.items():
for s2, c2 in source_cat_sets.items():
if s1 != s2:
shared |= (c1 & c2)
lines.extend([
"\n## Category Coverage Analysis\n",
f"**Shared categories** (covered by 2+ bodies): {', '.join(sorted(shared)) or 'none'}\n",
])
for src, cats in source_cat_sets.items():
unique = cats - shared
if unique:
lines.append(f"**Unique to {src.upper()}**: {', '.join(sorted(unique))}")
report = "\n".join(lines)
path = self.output_dir / "sources.md"
path.write_text(report)
return str(path)
def false_positives_report(self) -> str:
"""False positive profiling report — what makes drafts look AI-related but not be."""
import json as _json
import re as _re
from collections import Counter as _Counter
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
# Get false positives
fp_rows = self.db.conn.execute(
"""SELECT d.*, r.novelty, r.maturity, r.overlap, r.momentum, r.relevance,
r.summary, r.categories as r_categories, r.false_positive
FROM drafts d
JOIN ratings r ON d.name = r.draft_name
WHERE r.false_positive = 1
ORDER BY d.name"""
).fetchall()
# Get non-FP rated drafts for comparison
nonfp_rows = self.db.conn.execute(
"""SELECT r.novelty, r.maturity, r.overlap, r.momentum, r.relevance
FROM ratings r WHERE COALESCE(r.false_positive, 0) = 0"""
).fetchall()
total_rated = self.db.conn.execute("SELECT COUNT(*) FROM ratings").fetchone()[0]
total_drafts = self.db.count_drafts(include_false_positives=True)
fp_count = len(fp_rows)
pct_total = round(100 * fp_count / total_drafts, 1) if total_drafts else 0
pct_rated = round(100 * fp_count / total_rated, 1) if total_rated else 0
lines = [
"# False Positive Profile Report",
f"*Generated {now}*\n",
"## Overview\n",
f"- **False positives**: {fp_count} ({pct_total}% of {total_drafts} total drafts)",
f"- **% of rated**: {pct_rated}% of {total_rated} rated drafts",
f"- These drafts matched AI/agent search keywords but were flagged as not genuinely about AI agent infrastructure.\n",
]
# Source distribution
fp_sources: _Counter = _Counter()
fp_categories: _Counter = _Counter()
fp_dims = {"novelty": [], "maturity": [], "overlap": [], "momentum": [], "relevance": []}
nonfp_dims = {"novelty": [], "maturity": [], "overlap": [], "momentum": [], "relevance": []}
for row in fp_rows:
src = row["source"] or "ietf"
fp_sources[src] += 1
cats = _json.loads(row["r_categories"]) if row["r_categories"] else []
for cat in cats:
fp_categories[cat] += 1
for d in ["novelty", "maturity", "overlap", "momentum", "relevance"]:
fp_dims[d].append(row[d])
for row in nonfp_rows:
for d in ["novelty", "maturity", "overlap", "momentum", "relevance"]:
nonfp_dims[d].append(row[d])
lines.extend([
"## By Source\n",
"| Source | FP Count | % of FPs |",
"|--------|--------:|---------:|",
])
for src, cnt in fp_sources.most_common():
pct = round(100 * cnt / fp_count, 1) if fp_count else 0
lines.append(f"| {src.upper()} | {cnt} | {pct}% |")
# Rating comparison
dims = ["novelty", "maturity", "overlap", "momentum", "relevance"]
lines.extend([
"\n## Rating Comparison: FP vs Non-FP\n",
"| Dimension | FP Avg | Non-FP Avg | Delta |",
"|-----------|------:|-----------:|------:|",
])
for d in dims:
fp_avg = sum(fp_dims[d]) / len(fp_dims[d]) if fp_dims[d] else 0
nfp_avg = sum(nonfp_dims[d]) / len(nonfp_dims[d]) if nonfp_dims[d] else 0
delta = fp_avg - nfp_avg
lines.append(f"| {d.capitalize()} | {fp_avg:.2f} | {nfp_avg:.2f} | {delta:+.2f} |")
# Categories
lines.extend([
"\n## Categories Assigned to False Positives\n",
"| Category | Count |",
"|----------|------:|",
])
for cat, cnt in fp_categories.most_common():
lines.append(f"| {cat} | {cnt} |")
# Top terms
stop_words = {
"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for",
"of", "with", "by", "from", "is", "it", "that", "this", "are", "was",
"be", "as", "can", "may", "will", "not", "has", "have", "been", "which",
"their", "its", "also", "such", "these", "would", "should", "could",
"more", "other", "than", "into", "about", "between", "over", "after",
"all", "one", "two", "new", "they", "we", "our", "each", "some", "any",
"there", "what", "when", "how", "where", "who", "does", "do", "did",
"no", "if", "so", "up", "out", "only", "used", "using", "use", "based",
"through", "both", "well", "within", "must", "while", "had", "were",
}
word_counter: _Counter = _Counter()
for row in fp_rows:
text = ((row["abstract"] or "") + " " + (row["title"] or "")).lower()
words = _re.findall(r'[a-z]{3,}', text)
for w in words:
if w not in stop_words:
word_counter[w] += 1
lines.extend([
"\n## Top Terms in FP Abstracts\n",
"| Term | Occurrences |",
"|------|------------:|",
])
for term, cnt in word_counter.most_common(30):
lines.append(f"| {term} | {cnt} |")
# Full list
lines.extend([
"\n## All False Positives\n",
"| Draft | Title | Source | Relevance | Categories |",
"|-------|-------|--------|----------:|------------|",
])
for row in fp_rows:
cats = _json.loads(row["r_categories"]) if row["r_categories"] else []
cat_str = ", ".join(cats[:2])
src = (row["source"] or "ietf").upper()
title = (row["title"] or "")[:50]
lines.append(f"| {row['name']} | {title} | {src} | {row['relevance']} | {cat_str} |")
report = "\n".join(lines)
path = self.output_dir / "false-positives.md"
path.write_text(report)
return str(path)
def citations_report(self) -> str:
"""Generate citation influence and BCP dependency analysis report."""
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
stats = self.db.ref_stats()
total_drafts = self.db.count_drafts()
# Build rating lookup for categories
pairs_data = self.db.drafts_with_ratings(limit=500)
rating_map: dict[str, Rating] = {}
draft_cats: dict[str, str] = {}
for draft, rating in pairs_data:
rating_map[draft.name] = rating
cats = rating.categories if rating.categories else []
draft_cats[draft.name] = cats[0] if cats else "Other"
# Well-known RFC names
rfc_names = {
"2119": "Key words (MUST/SHALL/MAY)", "8174": "Key words update",
"8259": "JSON", "7519": "JWT", "6749": "OAuth 2.0",
"7540": "HTTP/2", "9110": "HTTP Semantics", "7525": "TLS Recommendations",
"8446": "TLS 1.3", "3986": "URIs", "7230": "HTTP/1.1 Syntax",
"7231": "HTTP/1.1 Semantics", "8288": "Web Linking",
"7515": "JWS", "7516": "JWE", "7517": "JWK", "7518": "JWA",
"9449": "DPoP", "6750": "OAuth Bearer", "8725": "JWT Best Practices",
"9396": "Rich Authorization Requests", "9101": "JAR",
"8414": "OAuth Server Metadata", "7591": "Dynamic Client Registration",
"8705": "mTLS for OAuth", "9068": "JWT Access Tokens",
"6819": "OAuth Threat Model", "9200": "ACE-OAuth", "9052": "COSE",
"8392": "CWT", "7252": "CoAP",
}
lines = [
"# Citation Influence & BCP Dependency Analysis",
f"*Generated {now}{stats['drafts_with_refs']} of {total_drafts} drafts analyzed, "
f"{stats['total_refs']} total references "
f"({stats['rfc_refs']} RFC, {stats['draft_refs']} draft, {stats['bcp_refs']} BCP)*\n",
]
# ── Section 1: Top Cited RFCs ──
top_rfcs = self.db.top_referenced(ref_type="rfc", limit=20)
if top_rfcs:
lines.extend([
"## Top 20 Most-Cited RFCs\n",
"| # | RFC | Name | Cited By |",
"|--:|-----|------|--------:|",
])
for rank, (rid, cnt, drafts) in enumerate(top_rfcs, 1):
name = rfc_names.get(rid, "")
lines.append(f"| {rank} | RFC {rid} | {name} | {cnt} drafts |")
# ── Section 2: Top Citing Drafts ──
ref_counts = self.db.ref_counts_by_draft()
if ref_counts:
lines.extend([
"\n## Top 20 Most-Citing Drafts\n",
"Drafts with the highest outgoing reference count.\n",
"| # | Draft | Category | RFCs | Drafts | BCPs | Total |",
"|--:|-------|----------|-----:|-------:|-----:|------:|",
])
for rank, (name, rfcs, drafts, bcps) in enumerate(ref_counts[:20], 1):
cat = draft_cats.get(name, "Other")
total = rfcs + drafts + bcps
lines.append(f"| {rank} | {name} | {cat} | {rfcs} | {drafts} | {bcps} | {total} |")
# ── Section 3: PageRank-style influence ──
# Compute simple PageRank: sum of citation counts for each RFC cited
all_refs = self.db.conn.execute(
"SELECT draft_name, ref_type, ref_id FROM draft_refs"
).fetchall()
rfc_in_degree: dict[str, int] = defaultdict(int)
for r in all_refs:
if r["ref_type"] == "rfc":
rfc_in_degree[r["ref_id"]] += 1
draft_influence: dict[str, float] = defaultdict(float)
draft_out: dict[str, int] = defaultdict(int)
for r in all_refs:
draft_out[r["draft_name"]] += 1
if r["ref_type"] == "rfc" and r["ref_id"] in rfc_in_degree:
draft_influence[r["draft_name"]] += rfc_in_degree[r["ref_id"]]
influence_sorted = sorted(draft_influence.items(), key=lambda x: x[1], reverse=True)
if influence_sorted:
lines.extend([
"\n## Influence Score (PageRank-style)\n",
"Drafts ranked by weighted sum of how often their cited RFCs are themselves cited.\n",
"| # | Draft | Category | Out-Degree | Influence Score |",
"|--:|-------|----------|----------:|---------:|",
])
for rank, (name, score) in enumerate(influence_sorted[:20], 1):
cat = draft_cats.get(name, "Other")
out = draft_out.get(name, 0)
lines.append(f"| {rank} | {name} | {cat} | {out} | {score:.0f} |")
# ── Section 4: Citation density by category ──
cat_totals: dict[str, int] = defaultdict(int)
cat_counts: dict[str, int] = defaultdict(int)
for name, count in draft_out.items():
cat = draft_cats.get(name, "Other")
cat_totals[cat] += count
cat_counts[cat] += 1
if cat_totals:
lines.extend([
"\n## Citation Density by Category\n",
"| Category | Drafts | Total Refs | Avg Refs/Draft |",
"|:---------|-------:|-----------:|---------------:|",
])
cat_items = sorted(cat_totals.items(),
key=lambda x: x[1] / cat_counts[x[0]] if cat_counts[x[0]] else 0,
reverse=True)
for cat, total in cat_items:
cnt = cat_counts[cat]
avg = total / cnt if cnt > 0 else 0
lines.append(f"| {cat} | {cnt} | {total} | {avg:.1f} |")
# ── Section 5: Draft-to-Draft citations ──
top_draft_refs = self.db.top_referenced(ref_type="draft", limit=20)
if top_draft_refs:
lines.extend([
"\n## Most-Referenced Drafts (Draft-to-Draft)\n",
"| # | Draft | Cited By |",
"|--:|-------|--------:|",
])
for rank, (rid, cnt, _) in enumerate(top_draft_refs, 1):
lines.append(f"| {rank} | {rid} | {cnt} drafts |")
# ═══════════════════════════════════════════════════
# BCP DEPENDENCY ANALYSIS
# ═══════════════════════════════════════════════════
lines.extend([
"\n---\n",
"## BCP Dependency Analysis\n",
])
# BCP stats
bcp_refs = self.db.top_referenced(ref_type="bcp", limit=100)
total_bcp_citations = sum(cnt for _, cnt, _ in bcp_refs)
unique_bcps = len(bcp_refs)
# BCP coverage
drafts_with_bcp_rows = self.db.conn.execute(
"SELECT COUNT(DISTINCT draft_name) as cnt FROM draft_refs WHERE ref_type = 'bcp'"
).fetchone()
drafts_with_bcp = drafts_with_bcp_rows["cnt"]
coverage = (drafts_with_bcp / total_drafts * 100) if total_drafts > 0 else 0
lines.extend([
f"- **{unique_bcps}** unique BCPs cited across the corpus",
f"- **{total_bcp_citations}** total BCP citations",
f"- **{drafts_with_bcp}** of {total_drafts} drafts ({coverage:.1f}%) cite at least one BCP\n",
])
# All BCPs ranked
if bcp_refs:
lines.extend([
"### All BCPs by Citation Count\n",
"| # | BCP | Cited By | Example Drafts |",
"|--:|-----|--------:|:---------------|",
])
for rank, (bid, cnt, drafts) in enumerate(bcp_refs, 1):
examples = ", ".join(drafts[:3])
more = f" +{len(drafts) - 3} more" if len(drafts) > 3 else ""
lines.append(f"| {rank} | BCP {bid} | {cnt} | {examples}{more} |")
# BCP by category
bcp_all_rows = self.db.conn.execute(
"SELECT draft_name, ref_id FROM draft_refs WHERE ref_type = 'bcp'"
).fetchall()
cat_bcp: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
for r in bcp_all_rows:
cat = draft_cats.get(r["draft_name"], "Other")
cat_bcp[cat][r["ref_id"]] += 1
if cat_bcp:
lines.extend([
"\n### BCP Usage by Category\n",
"| Category | BCP Refs | Unique BCPs | Top BCPs |",
"|:---------|--------:|-----------:|:---------|",
])
for cat in sorted(cat_bcp.keys(),
key=lambda c: sum(cat_bcp[c].values()), reverse=True):
total = sum(cat_bcp[cat].values())
unique = len(cat_bcp[cat])
top3 = sorted(cat_bcp[cat].items(), key=lambda x: x[1], reverse=True)[:3]
top_str = ", ".join(f"BCP{bid}({c})" for bid, c in top3)
lines.append(f"| {cat} | {total} | {unique} | {top_str} |")
# BCP co-citation
draft_bcps: dict[str, list[str]] = defaultdict(list)
for r in bcp_all_rows:
draft_bcps[r["draft_name"]].append(r["ref_id"])
co_cite: dict[tuple[str, str], int] = defaultdict(int)
for _, bcps_list in draft_bcps.items():
bcps_sorted = sorted(set(bcps_list))
for i in range(len(bcps_sorted)):
for j in range(i + 1, len(bcps_sorted)):
co_cite[(bcps_sorted[i], bcps_sorted[j])] += 1
top_co = sorted(co_cite.items(), key=lambda x: x[1], reverse=True)[:15]
if top_co:
lines.extend([
"\n### Top BCP Co-Citations\n",
"BCP pairs most frequently cited together in the same draft.\n",
"| BCP A | BCP B | Co-cited in |",
"|:------|:------|----------:|",
])
for (a, b), cnt in top_co:
lines.append(f"| BCP {a} | BCP {b} | {cnt} drafts |")
report = "\n".join(lines)
path = self.output_dir / "citations.md"
path.write_text(report)
return str(path)
def complexity_report(self) -> str:
"""Generate draft complexity matrix report with correlations and outliers."""
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
conn = self.db.conn
# Gather per-draft complexity data
rows = conn.execute("""
SELECT d.name, d.title, d.pages, d.source,
r.novelty, r.maturity, r.overlap, r.momentum, r.relevance,
r.categories,
(r.novelty + r.maturity + r.overlap + r.momentum + r.relevance) / 5.0 AS score
FROM drafts d
JOIN ratings r ON d.name = r.draft_name
WHERE r.false_positive = 0
""").fetchall()
author_counts = dict(conn.execute(
"SELECT draft_name, COUNT(*) FROM draft_authors GROUP BY draft_name"
).fetchall())
citation_counts = dict(conn.execute(
"SELECT draft_name, COUNT(*) FROM draft_refs GROUP BY draft_name"
).fetchall())
idea_counts = dict(conn.execute(
"SELECT draft_name, COUNT(*) FROM ideas GROUP BY draft_name"
).fetchall())
drafts_data = []
for r in rows:
pages = r["pages"]
try:
cats = json.loads(r["categories"]) if r["categories"] else []
except (json.JSONDecodeError, TypeError):
cats = []
drafts_data.append({
"name": r["name"],
"title": r["title"],
"pages": pages,
"author_count": author_counts.get(r["name"], 0),
"citation_count": citation_counts.get(r["name"], 0),
"idea_count": idea_counts.get(r["name"], 0),
"category_count": len(cats),
"novelty": r["novelty"],
"maturity": r["maturity"],
"overlap": r["overlap"],
"momentum": r["momentum"],
"relevance": r["relevance"],
"score": r["score"],
})
total_with_pages = sum(1 for d in drafts_data if d["pages"] is not None)
pages_pct = round(total_with_pages / len(drafts_data) * 100, 1) if drafts_data else 0
# Composite complexity
max_pages = max((d["pages"] for d in drafts_data if d["pages"] is not None), default=1) or 1
max_auth = max((d["author_count"] for d in drafts_data), default=1) or 1
max_cite = max((d["citation_count"] for d in drafts_data), default=1) or 1
max_idea = max((d["idea_count"] for d in drafts_data), default=1) or 1
for d in drafts_data:
p = (d["pages"] / max_pages) if d["pages"] is not None else 0.3
d["complexity"] = round((p + d["author_count"] / max_auth +
d["citation_count"] / max_cite +
d["idea_count"] / max_idea) / 4, 3)
d["efficiency"] = round(d["score"] / (d["complexity"] + 0.1), 2)
# Pearson correlation
metrics = ["pages", "author_count", "citation_count", "idea_count", "category_count"]
dimensions = ["novelty", "maturity", "overlap", "momentum", "relevance"]
def _pearson(xs, ys):
n = len(xs)
if n < 3:
return 0.0
mx, my = sum(xs) / n, sum(ys) / n
cov = sum((x - mx) * (y - my) for x, y in zip(xs, ys))
sx = sum((x - mx) ** 2 for x in xs) ** 0.5
sy = sum((y - my) ** 2 for y in ys) ** 0.5
return round(cov / (sx * sy), 3) if sx and sy else 0.0
lines = [
"# Draft Complexity Matrix",
f"*Generated {now}{len(drafts_data)} rated drafts ({pages_pct}% have page data)*\n",
]
# Correlation matrix
lines.extend([
"## Correlation Matrix\n",
"Pearson r between complexity metrics and rating dimensions.\n",
"| Metric |" + " | ".join(f" {d.capitalize()}" for d in dimensions) + " |",
"|--------|" + " | ".join("------:" for _ in dimensions) + " |",
])
for metric in metrics:
vals = []
for dim in dimensions:
if metric == "pages":
pairs_list = [(d[metric], d[dim]) for d in drafts_data if d[metric] is not None]
else:
pairs_list = [(d[metric], d[dim]) for d in drafts_data]
if len(pairs_list) >= 3:
xs, ys = zip(*pairs_list)
r_val = _pearson(list(xs), list(ys))
else:
r_val = 0.0
vals.append(f"{r_val:+.3f}")
label = metric.replace("_", " ").title()
lines.append(f"| {label} | " + " | ".join(vals) + " |")
# Top 10 most complex
sorted_complex = sorted(drafts_data, key=lambda d: d["complexity"], reverse=True)
lines.extend([
"\n## Top 10 Most Complex Drafts\n",
"| # | Draft | Pages | Authors | Citations | Ideas | Score | Complexity |",
"|---|-------|------:|--------:|----------:|------:|------:|-----------:|",
])
for i, d in enumerate(sorted_complex[:10], 1):
pages_str = str(d["pages"]) if d["pages"] is not None else "-"
lines.append(
f"| {i} | {d['name']} | {pages_str} | {d['author_count']} | "
f"{d['citation_count']} | {d['idea_count']} | {d['score']:.2f} | {d['complexity']:.3f} |"
)
# Top 10 most efficient
sorted_efficient = sorted(drafts_data, key=lambda d: d["efficiency"], reverse=True)
lines.extend([
"\n## Top 10 Most Efficient Drafts\n",
"*High ratings despite low structural complexity.*\n",
"| # | Draft | Pages | Authors | Score | Efficiency |",
"|---|-------|------:|--------:|------:|-----------:|",
])
for i, d in enumerate(sorted_efficient[:10], 1):
pages_str = str(d["pages"]) if d["pages"] is not None else "-"
lines.append(
f"| {i} | {d['name']} | {pages_str} | {d['author_count']} | "
f"{d['score']:.2f} | {d['efficiency']:.1f} |"
)
# Stats summary
pages_vals = [d["pages"] for d in drafts_data if d["pages"] is not None]
avg_pages = sum(pages_vals) / len(pages_vals) if pages_vals else 0
avg_auth = sum(d["author_count"] for d in drafts_data) / len(drafts_data) if drafts_data else 0
avg_cite = sum(d["citation_count"] for d in drafts_data) / len(drafts_data) if drafts_data else 0
lines.extend([
"\n## Summary Statistics\n",
f"- **Average pages**: {avg_pages:.1f} ({pages_pct}% coverage)",
f"- **Average authors**: {avg_auth:.1f}",
f"- **Average citations**: {avg_cite:.1f}",
f"- **Total drafts analyzed**: {len(drafts_data)}",
])
report = "\n".join(lines)
path = self.output_dir / "complexity.md"
path.write_text(report)
return str(path)

View File

@@ -0,0 +1,227 @@
"""Fetch AI-related publications from NIST CSRC.
NIST has no formal API but the publications search returns structured HTML.
We scrape the search results and supplement with a curated catalog of key
AI publications (AI RMF, AI 100-series, etc.).
"""
from __future__ import annotations
import re
import time as time_mod
import httpx
from rich.console import Console
from ..config import Config
from .base import SourceDocument
console = Console()
NIST_SEARCH_URL = "https://csrc.nist.gov/publications/search"
NIST_PUB_BASE = "https://csrc.nist.gov/publications/detail/"
# Curated catalog of key NIST AI publications
NIST_AI_CATALOG = [
# AI 100 series
("AI 100-1", "Artificial Intelligence Risk Management Framework (AI RMF 1.0)",
"The AI RMF provides a framework for managing risks associated with AI systems throughout their lifecycle, "
"addressing characteristics of trustworthy AI including validity, reliability, safety, security, resilience, "
"accountability, transparency, explainability, interpretability, privacy, and fairness.",
"https://csrc.nist.gov/pubs/ai/100/1/final", "2023-01-26", "Final"),
("AI 100-2 E2025", "Adversarial Machine Learning: A Taxonomy and Terminology of Attacks and Mitigations",
"Establishes a taxonomy of adversarial machine learning attacks and mitigations, covering evasion, poisoning, "
"and privacy attacks against predictive AI and generative AI systems including LLMs.",
"https://csrc.nist.gov/pubs/ai/100/2/e2025/final", "2025-03-24", "Final"),
("AI 100-4", "Reducing Risks Posed by Synthetic Content",
"Provides guidance on approaches to manage risks from synthetic content generated by AI, "
"including authentication, provenance tracking, and content labeling.",
"https://csrc.nist.gov/pubs/ai/100/4/final", "2024-07-26", "Final"),
("AI 600-1", "Artificial Intelligence Risk Management Framework: Generative AI Profile",
"Companion resource for the AI RMF focusing on risks unique to or exacerbated by generative AI, "
"covering hallucinations, data privacy, intellectual property, and CBRN information risks.",
"https://csrc.nist.gov/pubs/ai/600/1/final", "2024-07-26", "Final"),
# Special Publications
("SP 800-218A", "Secure Software Development Practices for Generative AI and Dual-Use Foundation Models",
"An SSDF community profile providing secure software development practices specific to generative AI "
"and dual-use foundation models, addressing data management, model training, and deployment security.",
"https://csrc.nist.gov/pubs/sp/800/218/a/final", "2024-07-26", "Final"),
("IR 8596", "Cybersecurity Framework Profile for Artificial Intelligence",
"A community profile mapping NIST Cybersecurity Framework functions to AI-specific risks and controls, "
"helping organizations manage cybersecurity risks in AI systems.",
"https://csrc.nist.gov/pubs/ir/8596/ipd", "2025-12-16", "Draft"),
("CSWP 31", "Proxy Validation and Verification for Critical AI Systems",
"Describes a proxy design process for validation and verification of critical AI systems, "
"addressing challenges of testing AI systems that are complex and opaque.",
"https://csrc.nist.gov/pubs/cswp/31/final", "2024-09-26", "Final"),
("IR 8579", "Developing the NCCoE Chatbot: Technical and Security Learnings",
"Documents technical and security lessons from implementing an LLM-based chatbot at the "
"National Cybersecurity Center of Excellence, including prompt injection mitigations.",
"https://csrc.nist.gov/pubs/ir/8579/ipd", "2025-07-31", "Draft"),
# Concept papers and other
("NIST-AI-Agent-IAM", "Accelerating the Adoption of Software and AI Agent Identity and Authorization",
"Concept paper on identity and authorization for AI agents, addressing how software agents "
"authenticate, authorize, and maintain accountability in automated systems.",
"https://csrc.nist.gov/publications/detail/other/2026/02/05/accelerating-the-adoption-of-software-and-artificial-intelligence-agent-identity-and-authorization/draft",
"2026-02-05", "Draft"),
# Trustworthy AI
("AI 100-6", "AI Measurement and Evaluation",
"Guidance on measurement and evaluation approaches for AI systems to assess trustworthiness "
"characteristics including accuracy, fairness, robustness, and security.",
"https://csrc.nist.gov/pubs/ai/100/6/final", "2024-04-29", "Final"),
# AI RMF playbook
("AI 100-1 Playbook", "AI RMF Playbook",
"Practical companion to the AI RMF providing suggested actions for achieving AI risk management outcomes, "
"organized by the framework's functions: Govern, Map, Measure, and Manage.",
"https://airc.nist.gov/AI_RMF_Playbook", "2023-01-26", "Final"),
]
def _nist_id_to_name(nist_id: str) -> str:
"""Convert NIST ID to slug. E.g. 'AI 100-1' -> 'nist-ai-100-1'."""
slug = nist_id.lower().replace(" ", "-").replace("/", "-").replace(".", "-")
return f"nist-{slug}"
class NISTFetcher:
"""Fetch AI-related publications from NIST CSRC.
Combines a curated catalog with search scraping for discovery.
"""
def __init__(self, config: Config | None = None):
self.config = config or Config.load()
self.client = httpx.Client(timeout=30, follow_redirects=True)
def search(
self, keywords: list[str], since: str | None = None
) -> list[SourceDocument]:
"""Return AI-relevant NIST publications."""
seen: dict[str, SourceDocument] = {}
# Strategy 1: Curated catalog
console.print(" Loading NIST AI publication catalog...")
for nist_id, title, abstract, url, date, status in NIST_AI_CATALOG:
if since and date and date < since:
continue
name = _nist_id_to_name(nist_id)
seen[name] = SourceDocument(
name=name,
title=f"NIST {nist_id}: {title}",
abstract=abstract,
source="nist",
source_id=nist_id,
source_url=url,
time=date,
doc_status=status.lower(),
)
# Strategy 2: Search CSRC for additional AI publications
console.print(" Searching NIST CSRC for AI publications...")
search_terms = ["artificial intelligence", "machine learning", "large language model"]
for term in search_terms:
new_docs = self._search_csrc(term, since)
for doc in new_docs:
if doc.name not in seen:
seen[doc.name] = doc
time_mod.sleep(0.5)
console.print(f" Found [bold green]{len(seen)}[/] NIST publications")
return list(seen.values())
def _search_csrc(self, keyword: str, since: str | None) -> list[SourceDocument]:
"""Search NIST CSRC publications page."""
docs = []
try:
resp = self.client.get(
NIST_SEARCH_URL,
params={
"keywords": keyword,
"status": "Final,Draft",
"sortBy": "relevance",
},
)
if resp.status_code != 200:
return docs
# Parse search results HTML
# Results are in structured divs with title, series, date, abstract
# Pattern: <a href="/publications/detail/...">TITLE</a>
entries = re.findall(
r'<a[^>]*href="(/publications/detail/[^"]+)"[^>]*>([^<]+)</a>',
resp.text,
)
for href, title in entries:
title = title.strip()
if not title or len(title) < 10:
continue
# Extract a usable ID from the URL path
parts = href.rstrip("/").split("/")
# e.g. /publications/detail/sp/800/218/a/final -> sp-800-218-a
slug_parts = [p for p in parts[3:] if p not in ("final", "draft", "ipd", "fpd")]
nist_id = "-".join(slug_parts).upper()
name = f"nist-{'-'.join(slug_parts).lower()}"
if not name or name == "nist-":
continue
docs.append(SourceDocument(
name=name,
title=title,
abstract=title, # Will be enriched later if needed
source="nist",
source_id=nist_id,
source_url=f"https://csrc.nist.gov{href}",
time="",
doc_status="published",
))
except httpx.HTTPError as e:
console.print(f"[yellow]NIST search error: {e}[/]")
return docs
def download_text(self, doc: SourceDocument) -> str | None:
"""NIST publications are free PDFs — try to fetch and extract."""
url = doc.source_url
if not url:
return None
try:
# First get the publication page to find the PDF link
resp = self.client.get(url)
if resp.status_code != 200:
return None
# Look for PDF download link
pdf_match = re.search(r'href="([^"]+\.pdf)"', resp.text)
if not pdf_match:
# Extract abstract from page instead
abstract_match = re.search(
r'(?:Abstract|Summary|Description)[:\s]*</[^>]+>\s*<[^>]+>(.+?)</(?:p|div)',
resp.text, re.DOTALL | re.IGNORECASE,
)
if abstract_match:
text = re.sub(r'<[^>]+>', '', abstract_match.group(1)).strip()
return text[:5000] if text else None
return None
pdf_url = pdf_match.group(1)
if not pdf_url.startswith("http"):
pdf_url = f"https://csrc.nist.gov{pdf_url}"
resp = self.client.get(pdf_url)
resp.raise_for_status()
try:
from io import BytesIO
from pdfminer.high_level import extract_text
text = extract_text(BytesIO(resp.content))
return text[:100000] if text else None
except ImportError:
return f"[PDF document: {doc.title}. Install pdfminer.six to extract text.]"
except httpx.HTTPError as e:
console.print(f"[dim]Could not download {doc.name}: {e}[/]")
return None
def close(self) -> None:
self.client.close()

View File

@@ -55,6 +55,14 @@ from webui.data import (
get_ask_synthesize, get_ask_synthesize,
get_category_summary, get_category_summary,
global_search, global_search,
get_architecture,
get_source_comparison,
get_false_positive_profile,
get_citation_influence,
get_bcp_analysis,
get_trends_data,
get_complexity_data,
get_idea_analysis,
) )
app = Flask( app = Flask(
@@ -306,6 +314,17 @@ def idea_clusters():
return render_template("idea_clusters.html", clusters=data) return render_template("idea_clusters.html", clusters=data)
@app.route("/architecture")
def architecture():
data = get_architecture(db())
return render_template("architecture.html", arch=data)
@app.route("/api/architecture")
def api_architecture():
return jsonify(get_architecture(db()))
@app.route("/similarity") @app.route("/similarity")
def similarity(): def similarity():
network = get_similarity_graph(db()) network = get_similarity_graph(db())
@@ -331,7 +350,9 @@ def authors():
@app.route("/citations") @app.route("/citations")
def citations(): def citations():
graph = get_citation_graph(db()) graph = get_citation_graph(db())
return render_template("citations.html", graph=graph) influence = get_citation_influence(db())
bcp = get_bcp_analysis(db())
return render_template("citations.html", graph=graph, influence=influence, bcp=bcp)
@app.route("/monitor") @app.route("/monitor")
@@ -674,6 +695,88 @@ def create_app(dev: bool = False) -> Flask:
return app return app
# ── Sources & False Positives ────────────────────────────────────────────
@app.route("/sources")
def sources_page():
data = get_source_comparison(db())
return render_template("sources.html", data=data)
@app.route("/false-positives")
def false_positives_page():
data = get_false_positive_profile(db())
return render_template("false_positives.html", data=data)
@app.route("/api/sources")
def api_sources():
data = get_source_comparison(db())
return jsonify(data)
@app.route("/api/false-positives")
def api_false_positives():
data = get_false_positive_profile(db())
return jsonify(data)
# ── Citation Influence & BCP ─────────────────────────────────────────────
@app.route("/api/citations/influence")
def api_citation_influence():
return jsonify(get_citation_influence(db()))
@app.route("/api/citations/bcp")
def api_bcp_analysis():
return jsonify(get_bcp_analysis(db()))
# ── Trends & Complexity ──────────────────────────────────────────────────
@app.route("/trends")
def trends():
data = get_trends_data(db())
return render_template("trends_analysis.html", data=data)
@app.route("/complexity")
def complexity():
data = get_complexity_data(db())
return render_template("complexity.html", data=data)
@app.route("/api/trends")
def api_trends():
data = get_trends_data(db())
return jsonify(data)
@app.route("/api/complexity")
def api_complexity():
data = get_complexity_data(db())
return jsonify(data)
# ── Idea Analysis ────────────────────────────────────────────────────────
@app.route("/idea-analysis")
def idea_analysis():
data = get_idea_analysis(db())
return render_template("idea_analysis.html", data=data)
@app.route("/api/idea-analysis")
def api_idea_analysis():
data = get_idea_analysis(db())
return jsonify(data)
if __name__ == "__main__": if __name__ == "__main__":
import argparse import argparse

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,465 @@
{% extends "base.html" %}
{% set active_page = "architecture" %}
{% block title %}Architecture — IETF Draft Analyzer{% endblock %}
{% block extra_head %}
<style>
.arch-layer {
border-left: 3px solid;
transition: all 0.2s;
}
.arch-layer:hover { background: rgba(255,255,255,0.02); }
.layer-transport { border-color: #6366f1; }
.layer-identity { border-color: #f59e0b; }
.layer-discovery { border-color: #10b981; }
.layer-communication { border-color: #3b82f6; }
.layer-coordination { border-color: #8b5cf6; }
.layer-intelligence { border-color: #ec4899; }
.layer-safety { border-color: #ef4444; }
.layer-application { border-color: #06b6d4; }
.comp-node {
background: rgba(30, 41, 59, 0.8);
backdrop-filter: blur(10px);
cursor: pointer;
transition: all 0.2s;
}
.comp-node:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.comp-node.selected {
box-shadow: 0 0 0 2px #3b82f6;
}
.gap-marker.selected {
box-shadow: 0 0 0 2px #ef4444;
}
.gap-marker {
background: rgba(239, 68, 68, 0.1);
border: 1px dashed rgba(239, 68, 68, 0.4);
animation: pulse-border 2s ease-in-out infinite;
}
@keyframes pulse-border {
0%, 100% { border-color: rgba(239, 68, 68, 0.4); }
50% { border-color: rgba(239, 68, 68, 0.8); }
}
.severity-critical { color: #ef4444; }
.severity-high { color: #f59e0b; }
.severity-medium { color: #3b82f6; }
.severity-low { color: #6b7280; }
.source-dot {
width: 8px; height: 8px; border-radius: 50%;
display: inline-block;
}
.source-ietf { background: #3b82f6; }
.source-w3c { background: #f59e0b; }
.source-etsi { background: #f97316; }
.source-itu { background: #ec4899; }
.source-iso { background: #a855f7; }
.source-nist { background: #06b6d4; }
.detail-panel {
background: linear-gradient(135deg, rgba(30, 41, 59, 0.95), rgba(15, 23, 42, 0.95));
backdrop-filter: blur(20px);
}
.maturity-bar {
height: 4px;
border-radius: 2px;
background: rgba(51, 65, 85, 0.5);
overflow: hidden;
}
.maturity-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s;
}
</style>
{% endblock %}
{% block content %}
<!-- Header -->
<div class="mb-6">
<h1 class="text-2xl font-bold text-white">System-of-Systems Architecture</h1>
<p class="text-slate-400 text-sm mt-1">
Holistic view of the AI agent standards landscape — {{ arch.stats.total_components }} components across
{{ arch.layers|length }} architectural layers, with {{ arch.stats.total_gaps }} identified gaps.
Built from {{ arch.stats.total_dependencies }} cross-component relationships.
</p>
</div>
<!-- Stats row -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
{% set severity_colors = {"critical": "red", "high": "amber", "medium": "blue", "low": "slate"} %}
<div class="bg-slate-800/50 rounded-lg border border-slate-700/50 p-4 text-center">
<div class="text-2xl font-bold text-white">{{ arch.stats.total_components }}</div>
<div class="text-xs text-slate-500">Components</div>
</div>
<div class="bg-slate-800/50 rounded-lg border border-slate-700/50 p-4 text-center">
<div class="text-2xl font-bold text-white">{{ arch.layers|length }}</div>
<div class="text-xs text-slate-500">Layers</div>
</div>
<div class="bg-slate-800/50 rounded-lg border border-slate-700/50 p-4 text-center">
<div class="text-2xl font-bold text-white">{{ arch.stats.total_dependencies }}</div>
<div class="text-xs text-slate-500">Dependencies</div>
</div>
<div class="bg-slate-800/50 rounded-lg border border-slate-700/50 p-4 text-center">
<div class="text-2xl font-bold text-red-400">{{ arch.stats.total_gaps }}</div>
<div class="text-xs text-slate-500">Gaps</div>
</div>
</div>
<!-- Layer Coverage Overview -->
<div class="bg-slate-800/30 rounded-xl border border-slate-700/50 p-4 mb-6">
<div class="flex items-center justify-between mb-3">
<h2 class="text-sm font-semibold text-slate-300">Coverage by Standards Body</h2>
<div class="flex items-center gap-3">
{% for src in ['ietf', 'w3c', 'etsi', 'itu', 'iso', 'nist'] %}
<div class="flex items-center gap-1">
<span class="source-dot source-{{ src }}"></span>
<span class="text-xs text-slate-500">{{ src|upper }}</span>
</div>
{% endfor %}
<div class="flex items-center gap-1 ml-2">
<svg class="w-3 h-3 text-red-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2L2 19h20L12 2z"/></svg>
<span class="text-xs text-slate-500">Gaps</span>
</div>
</div>
</div>
<div id="coverageChart"></div>
</div>
<!-- Layered Stack View + Detail Panel side-by-side -->
<div id="stackView" class="flex gap-4">
<div class="flex-1 min-w-0">
{% for layer in arch.layers | sort(attribute='order', reverse=True) %}
<div class="arch-layer layer-{{ layer.id }} rounded-r-lg mb-3 p-4 bg-slate-800/30">
<div class="flex items-center justify-between mb-3">
<div>
<h3 class="text-sm font-semibold text-white">{{ layer.label }}</h3>
<div class="flex items-center gap-3 mt-1">
<span class="text-xs text-slate-500">{{ layer.component_count }} components</span>
<span class="text-xs text-slate-500">{{ layer.idea_count }} ideas</span>
<span class="text-xs text-slate-500">{{ layer.total_drafts }} drafts</span>
{% if layer.gap_count > 0 %}
<span class="text-xs text-red-400">{{ layer.gap_count }} gap{{ 's' if layer.gap_count > 1 }}</span>
{% endif %}
</div>
</div>
<!-- Source coverage dots -->
<div class="flex items-center gap-1.5">
{% for src, count in layer.coverage.items() %}
<div class="flex items-center gap-1" title="{{ src|upper }}: {{ count }} drafts">
<span class="source-dot source-{{ src }}"></span>
<span class="text-xs text-slate-500">{{ count }}</span>
</div>
{% endfor %}
</div>
</div>
<!-- Components in this layer -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2">
{% for comp in arch.components if comp.layer == layer.id %}
<div class="comp-node rounded-lg border border-slate-700/50 p-3" data-comp-id="{{ comp.id }}" onclick="selectComponent({{ comp.id }})">
<div class="flex items-center justify-between mb-1.5">
<span class="text-xs font-semibold text-slate-200 truncate" title="{{ comp.name }}">{{ comp.name }}</span>
<span class="text-xs text-slate-500" title="{{ comp.size }} ideas, {{ comp.draft_count }} drafts">{{ comp.size }}i / {{ comp.draft_count }}d</span>
</div>
<!-- Maturity bar -->
<div class="maturity-bar mb-2">
<div class="maturity-fill {% if comp.maturity >= 4 %}bg-green-500{% elif comp.maturity >= 3 %}bg-yellow-500{% else %}bg-red-500{% endif %}"
style="width: {{ (comp.maturity / 5 * 100)|int }}%"></div>
</div>
<!-- Source dots -->
<div class="flex items-center gap-1">
{% for src, cnt in comp.sources.items() %}
<span class="source-dot source-{{ src }}" title="{{ src|upper }}: {{ cnt }}"></span>
{% endfor %}
{% if comp.type_breakdown %}
<span class="text-xs text-slate-600 ml-auto">
{{ comp.type_breakdown.keys() | list | first }}
</span>
{% endif %}
</div>
</div>
{% endfor %}
<!-- Gaps in this layer -->
{% for gap in arch.gaps if gap.layer == layer.id %}
<div class="gap-marker rounded-lg p-3 cursor-pointer" data-gap-id="{{ gap.id }}" onclick="selectGap({{ gap.id }})">
<div class="flex items-center gap-1.5 mb-1">
<svg class="w-3.5 h-3.5 severity-{{ gap.severity }}" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
<span class="text-xs font-semibold severity-{{ gap.severity }}">GAP</span>
<span class="text-xs text-slate-600">{{ gap.severity }}</span>
</div>
<div class="text-xs text-slate-300">{{ gap.topic }}</div>
<div class="text-xs text-slate-500 mt-1 line-clamp-2">{{ gap.description[:120] }}</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
<!-- Detail Panel (sticky sidebar, shown on component click) -->
<div id="detailPanel" class="detail-panel rounded-xl border border-slate-700/50 p-5 hidden w-[420px] shrink-0 sticky top-4 self-start max-h-[calc(100vh-2rem)] overflow-y-auto">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-white" id="detailTitle"></h3>
<button onclick="closeDetail()" class="text-slate-500 hover:text-slate-300 text-xs">Close</button>
</div>
<div id="detailContent"></div>
</div>
</div><!-- end stackView flex -->
{% endblock %}
{% block extra_scripts %}
<script src="/static/js/plotly.min.js"></script>
<script>
const COMPONENTS = {{ arch.components | tojson }};
const DEPENDENCIES = {{ arch.dependencies | tojson }};
const LAYERS = {{ arch.layers | tojson }};
const GAPS = {{ arch.gaps | tojson }};
const LAYER_COLORS = {
transport: '#6366f1', identity: '#f59e0b', discovery: '#10b981',
communication: '#3b82f6', coordination: '#8b5cf6', intelligence: '#ec4899',
safety: '#ef4444', application: '#06b6d4'
};
const SOURCE_COLORS = {
ietf: '#3b82f6', w3c: '#f59e0b', etsi: '#f97316',
itu: '#ec4899', iso: '#a855f7', nist: '#06b6d4'
};
function renderCoverageChart() {
const sources = ['ietf', 'w3c', 'etsi', 'itu', 'iso', 'nist'];
const sourceLabels = { ietf: 'IETF', w3c: 'W3C', etsi: 'ETSI', itu: 'ITU-T', iso: 'ISO/IEC', nist: 'NIST' };
// Sort layers top-down (highest order first)
const sortedLayers = [...LAYERS].sort((a, b) => b.order - a.order);
const labels = sortedLayers.map(l => l.label);
const traces = sources.map(src => ({
y: labels,
x: sortedLayers.map(l => (l.coverage[src] || 0)),
name: sourceLabels[src],
type: 'bar',
orientation: 'h',
marker: { color: SOURCE_COLORS[src], opacity: 0.85 },
hovertemplate: `%{y}<br>${sourceLabels[src]}: %{x} drafts<extra></extra>`,
}));
// Gap markers as annotations
const gapAnnotations = [];
sortedLayers.forEach(l => {
const layerGaps = GAPS.filter(g => g.layer === l.id);
if (layerGaps.length > 0) {
const total = sources.reduce((s, src) => s + (l.coverage[src] || 0), 0);
gapAnnotations.push({
x: total + 8,
y: l.label,
text: `${layerGaps.length} gap${layerGaps.length > 1 ? 's' : ''}`,
showarrow: false,
font: { size: 10, color: '#f87171' },
xanchor: 'left',
});
}
});
Plotly.newPlot('coverageChart', traces, {
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
barmode: 'stack',
margin: { l: 180, r: 60, t: 10, b: 30 },
xaxis: {
title: { text: 'Documents', font: { size: 11, color: '#64748b' } },
gridcolor: 'rgba(51,65,85,0.3)',
tickfont: { size: 10, color: '#94a3b8' },
},
yaxis: {
tickfont: { size: 11, color: '#cbd5e1' },
automargin: true,
},
annotations: gapAnnotations,
height: 320,
showlegend: false,
}, { responsive: true, displayModeBar: false });
}
function selectComponent(compId) {
const comp = COMPONENTS[compId];
if (!comp) return;
document.getElementById('detailPanel').classList.remove('hidden');
document.getElementById('detailTitle').textContent = comp.name;
// Find connected components
const connected = DEPENDENCIES.filter(d => d.source === compId || d.target === compId)
.map(d => {
const otherId = d.source === compId ? d.target : d.source;
return { ...COMPONENTS[otherId], sim: d.similarity, idea_a: d.idea_a, idea_b: d.idea_b };
})
.sort((a, b) => b.sim - a.sim);
// Find gaps in same layer
const layerGaps = GAPS.filter(g => g.layer === comp.layer);
let html = `
<div>
<div class="text-xs text-slate-500 mb-2">Layer: <span class="text-slate-300">${comp.layer}</span> &middot; Ideas: <span class="text-slate-300">${comp.size}</span> &middot; Maturity: <span class="text-slate-300">${comp.maturity}/5</span></div>
<div class="text-xs font-semibold text-slate-400 mb-1.5 mt-3">Source Coverage</div>
<div class="flex flex-wrap gap-2 mb-3">
${Object.entries(comp.sources).map(([s, n]) =>
`<span class="text-xs px-2 py-0.5 rounded-full" style="background:${SOURCE_COLORS[s]}22; color:${SOURCE_COLORS[s]}">${s.toUpperCase()}: ${n}</span>`
).join('')}
</div>
<div class="text-xs font-semibold text-slate-400 mb-1.5">Idea Types</div>
<div class="flex flex-wrap gap-1.5 mb-3">
${Object.entries(comp.type_breakdown).map(([t, n]) =>
`<span class="text-xs px-2 py-0.5 rounded-full bg-slate-700 text-slate-300">${t} (${n})</span>`
).join('')}
</div>
<div class="text-xs font-semibold text-slate-400 mb-1.5">Top Ideas</div>
<div class="space-y-1 mb-3">
${comp.top_ideas.map(i =>
`<div class="text-xs">&bull; ${i.draft_name ? `<a href="/drafts/${i.draft_name}" class="text-blue-400 hover:text-blue-300">${i.title}</a>` : `<span class="text-slate-300">${i.title}</span>`} <span class="text-slate-600">(${i.type})</span></div>`
).join('')}
</div>
<div class="text-xs font-semibold text-slate-400 mb-1.5">Drafts (${comp.draft_count})</div>
<div class="space-y-1 mb-3 max-h-36 overflow-y-auto">
${(comp.drafts || []).map(d =>
`<div class="text-xs"><a href="/drafts/${d.name}" class="text-blue-400 hover:text-blue-300">${d.title}</a> <span class="source-dot source-${d.source} ml-1" title="${d.source.toUpperCase()}"></span></div>`
).join('')}
${comp.draft_count > 20 ? `<div class="text-xs text-slate-600 italic">+ ${comp.draft_count - 20} more</div>` : ''}
</div>
<div class="text-xs font-semibold text-slate-400 mb-1.5">Connected Components (${connected.length})</div>
<div class="space-y-1.5 max-h-48 overflow-y-auto mb-3">
${connected.length > 0 ? connected.map(c =>
`<div class="text-xs p-2 rounded bg-slate-800/60 border border-slate-700/30">
<span class="text-slate-200">${c.name}</span>
<span class="text-slate-500 ml-1">(${c.layer})</span>
<span class="text-blue-400 ml-1 font-mono">${c.sim.toFixed(3)}</span>
<div class="text-slate-600 mt-0.5">${c.idea_a}${c.idea_b}</div>
</div>`
).join('') : '<div class="text-xs text-slate-600 italic">No connections</div>'}
</div>
${layerGaps.length > 0 ? `
<div class="text-xs font-semibold text-red-400 mb-1.5">Gaps in Layer (${layerGaps.length})</div>
<div class="space-y-1.5">
${layerGaps.map(g =>
`<div class="text-xs p-2 rounded gap-marker">
<span class="severity-${g.severity} font-semibold">${g.severity.toUpperCase()}</span>
<span class="text-slate-300 ml-1">${g.topic}</span>
</div>`
).join('')}
</div>` : ''}
</div>
`;
document.getElementById('detailContent').innerHTML = html;
// Highlight in stack view
document.querySelectorAll('.comp-node').forEach(n => n.classList.remove('selected'));
const node = document.querySelector(`[data-comp-id="${compId}"]`);
if (node) { node.classList.add('selected'); node.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); }
}
function selectGap(gapId) {
const gap = GAPS.find(g => g.id === gapId);
if (!gap) return;
document.getElementById('detailPanel').classList.remove('hidden');
document.getElementById('detailTitle').innerHTML =
`<span class="severity-${gap.severity}">GAP:</span> ${gap.topic}`;
// Components in same layer (what exists nearby)
const nearby = COMPONENTS.filter(c => c.layer === gap.layer);
// Components in adjacent layers
const layerOrder = {};
LAYERS.forEach(l => { layerOrder[l.id] = l.order; });
const gapOrder = layerOrder[gap.layer] || 0;
const adjacent = COMPONENTS.filter(c => {
const co = layerOrder[c.layer] || 0;
return Math.abs(co - gapOrder) === 1;
});
const sevLabel = { critical: 'Immediate standardization needed', high: 'Should be addressed soon',
medium: 'Worth exploring', low: 'Nice to have' };
let html = `
<div>
<div class="flex items-center gap-2 mb-3">
<span class="text-xs px-2 py-0.5 rounded-full severity-${gap.severity}"
style="background: ${gap.severity === 'critical' ? 'rgba(239,68,68,0.15)' : gap.severity === 'high' ? 'rgba(245,158,11,0.15)' : 'rgba(59,130,246,0.15)'}">
${gap.severity.toUpperCase()}
</span>
<span class="text-xs text-slate-500">${gap.category}</span>
<span class="text-xs text-slate-600">Layer: ${gap.layer}</span>
</div>
<div class="text-xs font-semibold text-slate-400 mb-1">What's Missing</div>
<div class="text-xs text-slate-300 leading-relaxed mb-3">${gap.description}</div>
<div class="text-xs font-semibold text-slate-400 mb-1">Evidence</div>
<div class="text-xs text-slate-400 leading-relaxed mb-3 p-2 rounded bg-slate-800/60 border border-slate-700/30">${gap.evidence || 'No evidence recorded'}</div>
<div class="text-xs font-semibold text-slate-400 mb-1">Priority</div>
<div class="text-xs text-slate-300 mb-3">${sevLabel[gap.severity] || gap.severity}</div>
${nearby.length > 0 ? `
<div class="text-xs font-semibold text-slate-400 mb-1">Existing Work in Same Layer (${nearby.length})</div>
<div class="space-y-1 mb-3 max-h-32 overflow-y-auto">
${nearby.map(c =>
`<div class="text-xs p-2 rounded bg-slate-800/60 border border-slate-700/30 cursor-pointer hover:border-slate-600" onclick="selectComponent(${c.id})">
<span class="text-slate-200">${c.name}</span>
<span class="text-slate-600 ml-1">${c.size} ideas</span>
</div>`
).join('')}
</div>` : `
<div class="text-xs font-semibold text-red-400 mb-1">No Existing Components in Layer</div>
<div class="text-xs text-slate-500 mb-3">This layer has no standardization components yet — a greenfield gap.</div>`}
${adjacent.length > 0 ? `
<div class="text-xs font-semibold text-slate-400 mb-1">Adjacent Layer Components (${adjacent.length})</div>
<div class="space-y-1 max-h-32 overflow-y-auto">
${adjacent.map(c =>
`<div class="text-xs p-2 rounded bg-slate-800/60 border border-slate-700/30 cursor-pointer hover:border-slate-600" onclick="selectComponent(${c.id})">
<span class="text-slate-200">${c.name}</span>
<span class="text-slate-500 ml-1">(${c.layer})</span>
<span class="text-slate-600 ml-1">${c.size} ideas</span>
</div>`
).join('')}
</div>` : ''}
</div>
`;
document.getElementById('detailContent').innerHTML = html;
// Highlight gap card
document.querySelectorAll('.comp-node').forEach(n => n.classList.remove('selected'));
document.querySelectorAll('.gap-marker').forEach(n => n.classList.remove('selected'));
const card = document.querySelector(`[data-gap-id="${gapId}"]`);
if (card) { card.classList.add('selected'); }
}
function closeDetail() {
document.getElementById('detailPanel').classList.add('hidden');
document.querySelectorAll('.comp-node').forEach(n => n.classList.remove('selected'));
document.querySelectorAll('.gap-marker').forEach(n => n.classList.remove('selected'));
}
document.addEventListener('DOMContentLoaded', () => {
renderCoverageChart();
});
</script>
{% endblock %}

View File

@@ -119,7 +119,7 @@
<div class="cluster-card bg-slate-800/50 rounded-lg border border-slate-700/50 p-4 cursor-pointer" data-cluster-id="{{ c.id }}"> <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 --> <!-- Header — click to highlight in graph -->
<div class="flex items-center justify-between mb-2" onclick="highlightCluster({{ c.id }})"> <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> <span class="text-sm font-semibold text-white">{{ c.name }}</span>
<div class="flex gap-1.5 items-center"> <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-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> <span class="text-xs px-2 py-0.5 rounded-full bg-emerald-500/20 text-emerald-400">{{ c.draft_count }} drafts</span>

View File

@@ -117,6 +117,14 @@
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2z"/><circle cx="19" cy="19" r="3" stroke="currentColor" stroke-width="2" fill="none"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2z"/><circle cx="19" cy="19" r="3" stroke="currentColor" stroke-width="2" fill="none"/></svg>
Idea Clusters Idea Clusters
</a> </a>
<a href="/idea-analysis" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'idea_analysis' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
Idea Analysis
</a>
<a href="/architecture" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'architecture' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
Architecture
</a>
{% if is_admin %} {% if is_admin %}
<a href="/gaps" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'gaps' }}"> <a href="/gaps" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'gaps' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
@@ -127,6 +135,14 @@
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
Timeline Timeline
</a> </a>
<a href="/trends" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'trends' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/></svg>
Trends
</a>
<a href="/complexity" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'complexity' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/></svg>
Complexity
</a>
<a href="/landscape" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'landscape' }}"> <a href="/landscape" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'landscape' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7"/></svg>
Landscape Landscape
@@ -139,6 +155,14 @@
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/></svg>
Citations Citations
</a> </a>
<a href="/sources" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'sources' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/></svg>
Sources
</a>
<a href="/false-positives" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'false_positives' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"/></svg>
False Positives
</a>
<a href="/authors" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'authors' }}"> <a href="/authors" class="sidebar-link flex items-center gap-3 px-5 py-2.5 text-sm text-slate-300 {{ 'active' if active_page == 'authors' }}">
<svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg> <svg class="w-4 h-4 opacity-60" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/></svg>
Authors Authors

View File

@@ -1,10 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% set active_page = "citations" %} {% set active_page = "citations" %}
{% block title %}Citation Graph — IETF Draft Analyzer{% endblock %} {% block title %}Citations & Influence — IETF Draft Analyzer{% endblock %}
{% block extra_head %} {% block extra_head %}
<script src="/static/js/d3.v7.min.js"></script> <script src="/static/js/d3.v7.min.js"></script>
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
<style> <style>
#citationSvg { #citationSvg {
width: 100%; width: 100%;
@@ -26,52 +27,75 @@
max-width: 320px; opacity: 0; transition: opacity 0.15s; max-width: 320px; opacity: 0; transition: opacity 0.15s;
} }
.tooltip-card.visible { opacity: 1; } .tooltip-card.visible { opacity: 1; }
.legend-swatch { width: 12px; height: 12px; border-radius: 3px; display: inline-block; } .tab-btn {
.filter-btn { transition: all 0.15s; } transition: all 0.15s;
.filter-btn:hover { background: rgba(59, 130, 246, 0.2); } border-bottom: 2px solid transparent;
.filter-btn.active { background: rgba(59, 130, 246, 0.3); border-color: #3b82f6; color: #60a5fa; } }
.tab-btn:hover { color: #e2e8f0; }
.tab-btn.active {
color: #60a5fa;
border-bottom-color: #3b82f6;
}
.tab-panel { display: none; }
.tab-panel.active { display: block; }
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="mb-6"> <div class="mb-6">
<h1 class="text-2xl font-bold text-white">Citation Graph</h1> <h1 class="text-2xl font-bold text-white">Citations & Influence</h1>
<p class="text-slate-400 text-sm mt-1">Cross-reference network: {{ graph.stats.draft_count }} drafts referencing {{ graph.stats.rfc_count }} RFCs. References are extracted from each draft's text (RFC mentions, draft citations, BCP references). Node size reflects influence — how many other documents cite it. Highly-cited RFCs represent foundational standards that AI/agent drafts build upon.</p> <p class="text-slate-400 text-sm mt-1">Cross-reference network, citation influence metrics, and BCP dependency analysis across {{ influence.stats.drafts_with_refs }} drafts and {{ influence.stats.total_citations }} total citations.</p>
</div> </div>
<!-- Summary stats --> <!-- Summary stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6"> <div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden"> <div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-blue-500 to-blue-400"></div> <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-xs text-slate-500 uppercase tracking-wider">Drafts</div> <div class="text-xs text-slate-500 uppercase tracking-wider">Total Citations</div>
<div class="text-2xl font-bold text-white mt-1">{{ graph.stats.draft_count }}</div> <div class="text-2xl font-bold text-white mt-1">{{ influence.stats.total_citations }}</div>
</div> </div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden"> <div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-orange-500 to-orange-400"></div> <div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-orange-500 to-orange-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Referenced RFCs</div> <div class="text-xs text-slate-500 uppercase tracking-wider">Unique RFCs Cited</div>
<div class="text-2xl font-bold text-white mt-1">{{ graph.stats.rfc_count }}</div> <div class="text-2xl font-bold text-white mt-1">{{ influence.stats.unique_rfcs }}</div>
</div> </div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden"> <div class="stat-card rounded-xl border border-slate-800 p-4 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> <div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-emerald-500 to-emerald-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Total Nodes</div> <div class="text-xs text-slate-500 uppercase tracking-wider">Drafts with Refs</div>
<div class="text-2xl font-bold text-white mt-1">{{ graph.stats.node_count }}</div> <div class="text-2xl font-bold text-white mt-1">{{ influence.stats.drafts_with_refs }}</div>
</div> </div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden"> <div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-purple-500 to-purple-400"></div> <div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-purple-500 to-purple-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Citation Links</div> <div class="text-xs text-slate-500 uppercase tracking-wider">Avg Refs/Draft</div>
<div class="text-2xl font-bold text-white mt-1">{{ graph.stats.edge_count }}</div> <div class="text-2xl font-bold text-white mt-1">{{ influence.stats.avg_refs_per_draft }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-amber-500 to-amber-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">BCP Coverage</div>
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.coverage_pct }}%</div>
</div> </div>
</div> </div>
<!-- D3 Force-directed Citation Graph --> <!-- Tabs -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6 relative"> <div class="border-b border-slate-800 mb-6">
<nav class="flex gap-6">
<button class="tab-btn active px-1 pb-3 text-sm font-medium text-slate-400" data-tab="graph">Citation Graph</button>
<button class="tab-btn px-1 pb-3 text-sm font-medium text-slate-400" data-tab="influence">Influence Analysis</button>
<button class="tab-btn px-1 pb-3 text-sm font-medium text-slate-400" data-tab="bcp">BCP Dependencies</button>
</nav>
</div>
<!-- ==================== TAB 1: Citation Graph ==================== -->
<div id="tab-graph" class="tab-panel active">
<!-- D3 Force-directed Citation Graph -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6 relative">
<div class="flex flex-wrap items-center justify-between gap-3 mb-3"> <div class="flex flex-wrap items-center justify-between gap-3 mb-3">
<div> <div>
<h2 class="text-sm font-semibold text-slate-300">Cross-Reference Network</h2> <h2 class="text-sm font-semibold text-slate-300">Cross-Reference Network</h2>
<p class="text-xs text-slate-500 mt-0.5"> <p class="text-xs text-slate-500 mt-0.5">
<span class="inline-block w-2.5 h-2.5 rounded-full bg-blue-500 align-middle mr-1"></span>Drafts <span class="inline-block w-2.5 h-2.5 rounded-full bg-blue-500 align-middle mr-1"></span>Drafts
<span class="inline-block w-2.5 h-2.5 rounded-full bg-orange-500 align-middle mr-1 ml-3"></span>RFCs <span class="inline-block w-2.5 h-2.5 rounded-full bg-orange-500 align-middle mr-1 ml-3"></span>RFCs
— Node size = influence (in-degree). Drag to rearrange. Scroll to zoom. — Node size = influence. Drag to rearrange. Scroll to zoom.
</p> </p>
</div> </div>
<div class="flex gap-2 items-center"> <div class="flex gap-2 items-center">
@@ -88,10 +112,10 @@
<svg id="citationSvg"></svg> <svg id="citationSvg"></svg>
<div id="tooltip" class="tooltip-card"></div> <div id="tooltip" class="tooltip-card"></div>
</div> </div>
</div> </div>
<!-- Top Referenced RFCs Table --> <!-- Top Referenced RFCs Table -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden"> <div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800"> <div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Most Referenced RFCs</h2> <h2 class="text-sm font-semibold text-slate-300">Most Referenced RFCs</h2>
<p class="text-xs text-slate-500 mt-0.5">RFCs cited by the most drafts in the corpus</p> <p class="text-xs text-slate-500 mt-0.5">RFCs cited by the most drafts in the corpus</p>
@@ -109,12 +133,262 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</div> </div>
<!-- ==================== TAB 2: Influence Analysis ==================== -->
<div id="tab-influence" class="tab-panel">
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Top Cited RFCs with details -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Top 20 Most-Cited RFCs</h2>
<p class="text-xs text-slate-500 mt-0.5">Foundational standards the AI/agent ecosystem builds upon</p>
</div>
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
<table class="w-full text-sm">
<thead class="sticky top-0 z-10">
<tr class="border-b border-slate-800 bg-slate-900">
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">RFC</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Name</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Cited By</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for rfc in influence.top_cited_rfcs %}
<tr class="hover:bg-slate-800/50 transition group">
<td class="px-4 py-2.5 text-slate-500 text-xs">{{ loop.index }}</td>
<td class="px-4 py-2.5">
<a href="https://www.rfc-editor.org/rfc/rfc{{ rfc.rfc_id }}" target="_blank" rel="noopener"
class="text-orange-400 hover:text-orange-300 transition font-medium text-sm">RFC {{ rfc.rfc_id }}</a>
</td>
<td class="px-4 py-2.5 text-slate-300 text-xs">{{ rfc.name }}</td>
<td class="px-4 py-2.5 text-right">
<span class="px-2 py-0.5 rounded-full text-xs font-medium
{% if rfc.count >= 20 %}bg-orange-500/20 text-orange-400
{% elif rfc.count >= 10 %}bg-blue-500/20 text-blue-400
{% else %}bg-slate-700/50 text-slate-400{% endif %}">
{{ rfc.count }} drafts
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Top Citing Drafts -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Top 20 Most-Citing Drafts</h2>
<p class="text-xs text-slate-500 mt-0.5">Drafts with the highest outgoing reference count</p>
</div>
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
<table class="w-full text-sm">
<thead class="sticky top-0 z-10">
<tr class="border-b border-slate-800 bg-slate-900">
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Draft</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Category</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Refs</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for d in influence.top_citing_drafts %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-2.5 text-slate-500 text-xs">{{ loop.index }}</td>
<td class="px-4 py-2.5">
<a href="/drafts/{{ d.name }}" class="text-blue-400 hover:text-blue-300 transition text-sm">
{{ d.title[:60] }}{% if d.title|length > 60 %}...{% endif %}
</a>
</td>
<td class="px-4 py-2.5">
<span class="px-2 py-0.5 rounded-full text-xs bg-slate-700/50 text-slate-300">{{ d.category }}</span>
</td>
<td class="px-4 py-2.5 text-right">
<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-blue-500/20 text-blue-400">{{ d.count }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- PageRank-style influence -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden mb-6">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Influence Score (PageRank-style)</h2>
<p class="text-xs text-slate-500 mt-0.5">Drafts ranked by weighted sum of how often their cited RFCs are themselves cited — higher score means citing more foundational standards</p>
</div>
<div class="overflow-x-auto max-h-[400px] overflow-y-auto">
<table class="w-full text-sm">
<thead class="sticky top-0 z-10">
<tr class="border-b border-slate-800 bg-slate-900">
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Draft</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Category</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Out-Degree</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Influence Score</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for d in influence.top_pagerank %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-2.5 text-slate-500 text-xs">{{ loop.index }}</td>
<td class="px-4 py-2.5">
<a href="/drafts/{{ d.name }}" class="text-blue-400 hover:text-blue-300 transition text-sm">
{{ d.title[:60] }}{% if d.title|length > 60 %}...{% endif %}
</a>
</td>
<td class="px-4 py-2.5">
<span class="px-2 py-0.5 rounded-full text-xs bg-slate-700/50 text-slate-300">{{ d.category }}</span>
</td>
<td class="px-4 py-2.5 text-right text-slate-400">{{ d.out_degree }}</td>
<td class="px-4 py-2.5 text-right">
<span class="px-2 py-0.5 rounded-full text-xs font-medium bg-purple-500/20 text-purple-400">{{ d.score }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Citation density by category chart -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Average Citations per Category</h2>
<p class="text-xs text-slate-500 mb-3">Which categories reference the most external standards</p>
<div id="categoryChart" style="height: 400px;"></div>
</div>
<!-- Draft-to-Draft network -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Draft-to-Draft Citation Network</h2>
<p class="text-xs text-slate-500 mb-3">{{ influence.draft_network|length }} cross-citations between drafts in the corpus</p>
<div id="draftNetworkChart" style="height: 500px;"></div>
</div>
</div>
<!-- ==================== TAB 3: BCP Dependencies ==================== -->
<div id="tab-bcp" class="tab-panel">
<!-- BCP Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-amber-500 to-amber-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Unique BCPs</div>
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.unique_bcps }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 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>
<div class="text-xs text-slate-500 uppercase tracking-wider">Total BCP Refs</div>
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.total_bcp_refs }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<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-xs text-slate-500 uppercase tracking-wider">Drafts with BCPs</div>
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.drafts_with_bcp }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-purple-500 to-purple-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">BCP Coverage</div>
<div class="text-2xl font-bold text-white mt-1">{{ bcp.coverage.coverage_pct }}%</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- BCP Citation Table -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">All BCPs by Citation Count</h2>
<p class="text-xs text-slate-500 mt-0.5">{{ bcp.coverage.unique_bcps }} unique BCPs cited across the corpus</p>
</div>
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
<table class="w-full text-sm">
<thead class="sticky top-0 z-10">
<tr class="border-b border-slate-800 bg-slate-900">
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">#</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">BCP</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Cited By</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Example Drafts</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for b in bcp.bcps %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-2.5 text-slate-500 text-xs">{{ loop.index }}</td>
<td class="px-4 py-2.5 font-medium text-amber-400">BCP {{ b.bcp_id }}</td>
<td class="px-4 py-2.5 text-right">
<span class="px-2 py-0.5 rounded-full text-xs font-medium
{% if b.count >= 50 %}bg-amber-500/20 text-amber-400
{% elif b.count >= 10 %}bg-blue-500/20 text-blue-400
{% else %}bg-slate-700/50 text-slate-400{% endif %}">
{{ b.count }}
</span>
</td>
<td class="px-4 py-2.5 text-xs text-slate-500 max-w-[200px] truncate">
{{ b.drafts[:3]|join(', ') }}{% if b.total_drafts > 3 %} +{{ b.total_drafts - 3 }} more{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- BCP by Category -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">BCP Usage by Category</h2>
<p class="text-xs text-slate-500 mt-0.5">Which categories rely most heavily on BCPs</p>
</div>
<div class="overflow-x-auto max-h-[500px] overflow-y-auto">
<table class="w-full text-sm">
<thead class="sticky top-0 z-10">
<tr class="border-b border-slate-800 bg-slate-900">
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Category</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">BCP Refs</th>
<th class="px-4 py-2.5 text-right text-xs font-medium text-slate-400">Unique BCPs</th>
<th class="px-4 py-2.5 text-left text-xs font-medium text-slate-400">Top BCPs</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for cat in bcp.by_category %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-2.5 text-slate-300 text-sm font-medium">{{ cat.category }}</td>
<td class="px-4 py-2.5 text-right text-slate-300">{{ cat.total_bcp_refs }}</td>
<td class="px-4 py-2.5 text-right text-slate-400">{{ cat.unique_bcps }}</td>
<td class="px-4 py-2.5 text-xs text-slate-500">
{% for tb in cat.top_bcps[:3] %}
<span class="inline-block px-1.5 py-0.5 rounded bg-slate-700/50 text-amber-400 mr-1 mb-0.5">BCP{{ tb.bcp_id }}({{ tb.count }})</span>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- BCP Co-citation Heatmap -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">BCP Co-Citation Heatmap</h2>
<p class="text-xs text-slate-500 mb-3">How often pairs of BCPs are cited together in the same draft. Darker = more co-citations.</p>
<div id="bcpHeatmap" style="height: 500px;"></div>
</div>
</div>
{% endblock %} {% endblock %}
{% block extra_scripts %} {% block extra_scripts %}
<script> <script>
const graph = {{ graph | tojson }}; const graph = {{ graph | tojson }};
const influence = {{ influence | tojson }};
const bcp = {{ bcp | tojson }};
const PALETTE = [ const PALETTE = [
'#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b', '#3b82f6', '#ef4444', '#22c55e', '#a855f7', '#f59e0b',
@@ -122,7 +396,22 @@ const PALETTE = [
]; ];
// =========================================================== // ===========================================================
// D3.js Force-Directed Citation Network // Tab switching
// ===========================================================
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
this.classList.add('active');
document.getElementById('tab-' + this.dataset.tab).classList.add('active');
// Trigger Plotly resize for charts that might not have rendered
window.dispatchEvent(new Event('resize'));
});
});
// ===========================================================
// D3.js Force-Directed Citation Network (Tab 1)
// =========================================================== // ===========================================================
(function() { (function() {
if (graph.nodes.length === 0) { if (graph.nodes.length === 0) {
@@ -184,27 +473,20 @@ const PALETTE = [
const maxInfluence = d3.max(nodes, n => n.influence) || 1; const maxInfluence = d3.max(nodes, n => n.influence) || 1;
const rScale = d3.scaleSqrt().domain([0, maxInfluence]).range([3, 24]); const rScale = d3.scaleSqrt().domain([0, maxInfluence]).range([3, 24]);
// Color: drafts = blue, rfcs = orange, others = amber
function nodeColor(n) { function nodeColor(n) {
if (n.type === 'rfc') return '#f59e0b'; if (n.type === 'rfc') return '#f59e0b';
if (n.type === 'bcp') return '#eab308'; if (n.type === 'bcp') return '#eab308';
return '#3b82f6'; return '#3b82f6';
} }
// Force simulation
const simulation = d3.forceSimulation(nodes) const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links) .force('link', d3.forceLink(links).id(d => d.id).distance(60).strength(0.15))
.id(d => d.id)
.distance(60)
.strength(0.15)
)
.force('charge', d3.forceManyBody().strength(-80).distanceMax(350)) .force('charge', d3.forceManyBody().strength(-80).distanceMax(350))
.force('center', d3.forceCenter(width / 2, height / 2)) .force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(d => rScale(d.influence) + 2)) .force('collision', d3.forceCollide().radius(d => rScale(d.influence) + 2))
.force('x', d3.forceX(width / 2).strength(0.04)) .force('x', d3.forceX(width / 2).strength(0.04))
.force('y', d3.forceY(height / 2).strength(0.04)); .force('y', d3.forceY(height / 2).strength(0.04));
// Zoom behavior
const g = svg.append('g'); const g = svg.append('g');
const zoom = d3.zoom() const zoom = d3.zoom()
.scaleExtent([0.15, 5]) .scaleExtent([0.15, 5])
@@ -215,33 +497,22 @@ const PALETTE = [
svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity); svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity);
}); });
// Draw edges
const linkGroup = g.append('g').attr('class', 'links'); const linkGroup = g.append('g').attr('class', 'links');
const link = linkGroup.selectAll('line') const link = linkGroup.selectAll('line')
.data(links) .data(links).join('line')
.join('line') .attr('class', 'link').attr('stroke', '#475569').attr('stroke-width', 0.8);
.attr('class', 'link')
.attr('stroke', '#475569')
.attr('stroke-width', 0.8);
// Draw nodes
const nodeGroup = g.append('g').attr('class', 'nodes'); const nodeGroup = g.append('g').attr('class', 'nodes');
const node = nodeGroup.selectAll('g') const node = nodeGroup.selectAll('g')
.data(nodes) .data(nodes).join('g').attr('class', 'node')
.join('g')
.attr('class', 'node')
.call(d3.drag() .call(d3.drag()
.on('start', dragStarted) .on('start', dragStarted).on('drag', dragged).on('end', dragEnded));
.on('drag', dragged)
.on('end', dragEnded)
);
node.append('circle') node.append('circle')
.attr('r', d => rScale(d.influence)) .attr('r', d => rScale(d.influence))
.attr('fill', d => nodeColor(d)) .attr('fill', d => nodeColor(d))
.attr('opacity', 0.85); .attr('opacity', 0.85);
// Labels for high-influence nodes
node.filter(d => d.influence >= 5) node.filter(d => d.influence >= 5)
.append('text') .append('text')
.text(d => { .text(d => {
@@ -251,11 +522,9 @@ const PALETTE = [
}) })
.attr('dy', d => -(rScale(d.influence) + 4)) .attr('dy', d => -(rScale(d.influence) + 4))
.attr('text-anchor', 'middle') .attr('text-anchor', 'middle')
.attr('fill', '#94a3b8') .attr('fill', '#94a3b8').attr('font-size', '8px')
.attr('font-size', '8px')
.attr('font-family', 'Inter, system-ui, sans-serif'); .attr('font-family', 'Inter, system-ui, sans-serif');
// Tooltip
const tooltip = document.getElementById('tooltip'); const tooltip = document.getElementById('tooltip');
node.on('mouseover', function(event, d) { node.on('mouseover', function(event, d) {
@@ -270,8 +539,6 @@ const PALETTE = [
</div> </div>
`; `;
tooltip.classList.add('visible'); tooltip.classList.add('visible');
// Highlight connected nodes
const connected = new Set(); const connected = new Set();
links.forEach(l => { links.forEach(l => {
const sid = typeof l.source === 'object' ? l.source.id : l.source; const sid = typeof l.source === 'object' ? l.source.id : l.source;
@@ -280,13 +547,9 @@ const PALETTE = [
if (tid === d.id) connected.add(sid); if (tid === d.id) connected.add(sid);
}); });
connected.add(d.id); connected.add(d.id);
node.select('circle').attr('opacity', n => connected.has(n.id) ? 1 : 0.1);
node.select('circle') node.selectAll('text').attr('opacity', n => connected.has(n.id) ? 1 : 0.1);
.attr('opacity', n => connected.has(n.id) ? 1 : 0.1); link.attr('stroke-opacity', l => {
node.selectAll('text')
.attr('opacity', n => connected.has(n.id) ? 1 : 0.1);
link
.attr('stroke-opacity', l => {
const sid = typeof l.source === 'object' ? l.source.id : l.source; const sid = typeof l.source === 'object' ? l.source.id : l.source;
const tid = typeof l.target === 'object' ? l.target.id : l.target; const tid = typeof l.target === 'object' ? l.target.id : l.target;
return (sid === d.id || tid === d.id) ? 0.6 : 0.02; return (sid === d.id || tid === d.id) ? 0.6 : 0.02;
@@ -311,30 +574,22 @@ const PALETTE = [
} }
}); });
// Tick handler
simulation.on('tick', () => { simulation.on('tick', () => {
link link.attr('x1', d => d.source.x).attr('y1', d => d.source.y)
.attr('x1', d => d.source.x) .attr('x2', d => d.target.x).attr('y2', d => d.target.y);
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);
node.attr('transform', d => `translate(${d.x},${d.y})`); node.attr('transform', d => `translate(${d.x},${d.y})`);
}); });
// Drag handlers
function dragStarted(event, d) { function dragStarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart(); if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x; d.fy = d.y; d.fx = d.x; d.fy = d.y;
} }
function dragged(event, d) { function dragged(event, d) { d.fx = event.x; d.fy = event.y; }
d.fx = event.x; d.fy = event.y;
}
function dragEnded(event, d) { function dragEnded(event, d) {
if (!event.active) simulation.alphaTarget(0); if (!event.active) simulation.alphaTarget(0);
d.fx = null; d.fy = null; d.fx = null; d.fy = null;
} }
// Category filter
catSelect.addEventListener('change', function() { catSelect.addEventListener('change', function() {
const cat = this.value; const cat = this.value;
if (!cat) { if (!cat) {
@@ -344,43 +599,33 @@ const PALETTE = [
return; return;
} }
const inCat = new Set(); const inCat = new Set();
nodes.forEach(n => { nodes.forEach(n => { if (n.type === 'draft' && n.category === cat) inCat.add(n.id); });
if (n.type === 'draft' && n.category === cat) inCat.add(n.id);
});
// Also include RFCs referenced by those drafts
links.forEach(l => { links.forEach(l => {
const sid = typeof l.source === 'object' ? l.source.id : l.source; const sid = typeof l.source === 'object' ? l.source.id : l.source;
const tid = typeof l.target === 'object' ? l.target.id : l.target; const tid = typeof l.target === 'object' ? l.target.id : l.target;
if (inCat.has(sid)) inCat.add(tid); if (inCat.has(sid)) inCat.add(tid);
}); });
node.select('circle') node.select('circle').attr('opacity', n => inCat.has(n.id) ? 1 : 0.05);
.attr('opacity', n => inCat.has(n.id) ? 1 : 0.05); node.selectAll('text').attr('opacity', n => inCat.has(n.id) ? 1 : 0.05);
node.selectAll('text')
.attr('opacity', n => inCat.has(n.id) ? 1 : 0.05);
link.attr('stroke-opacity', l => { link.attr('stroke-opacity', l => {
const sid = typeof l.source === 'object' ? l.source.id : l.source; const sid = typeof l.source === 'object' ? l.source.id : l.source;
return inCat.has(sid) ? 0.5 : 0.01; return inCat.has(sid) ? 0.5 : 0.01;
}); });
}); });
// Min refs slider (client-side filter)
const slider = document.getElementById('minRefsSlider'); const slider = document.getElementById('minRefsSlider');
const sliderVal = document.getElementById('minRefsVal'); const sliderVal = document.getElementById('minRefsVal');
slider.addEventListener('input', function() { slider.addEventListener('input', function() {
sliderVal.textContent = this.value; sliderVal.textContent = this.value;
const minR = parseInt(this.value); const minR = parseInt(this.value);
// Show/hide RFC nodes by influence node.select('circle').attr('opacity', n => {
node.select('circle')
.attr('opacity', n => {
if (n.type === 'draft') return 0.85; if (n.type === 'draft') return 0.85;
return n.influence >= minR ? 0.85 : 0.05; return n.influence >= minR ? 0.85 : 0.05;
}); });
node.selectAll('text') node.selectAll('text').attr('opacity', n => {
.attr('opacity', n => {
if (n.type === 'draft') return 1; if (n.type === 'draft') return 1;
return n.influence >= minR ? 1 : 0.05; return n.influence >= minR ? 1 : 0.05;
}); });
// Filter edges
const visibleRfcs = new Set(nodes.filter(n => n.type !== 'draft' && n.influence >= minR).map(n => n.id)); const visibleRfcs = new Set(nodes.filter(n => n.type !== 'draft' && n.influence >= minR).map(n => n.id));
link.attr('stroke-opacity', l => { link.attr('stroke-opacity', l => {
const tid = typeof l.target === 'object' ? l.target.id : l.target; const tid = typeof l.target === 'object' ? l.target.id : l.target;
@@ -388,5 +633,155 @@ const PALETTE = [
}); });
}); });
})(); })();
// ===========================================================
// Plotly: Citation density by category (Tab 2)
// ===========================================================
(function() {
const cats = influence.citations_by_category;
if (!cats || cats.length === 0) return;
Plotly.newPlot('categoryChart', [{
type: 'bar',
x: cats.map(c => c.category),
y: cats.map(c => c.avg_citations),
text: cats.map(c => `${c.avg_citations} avg (${c.total_citations} total, ${c.draft_count} drafts)`),
hovertemplate: '%{x}<br>Avg: %{y:.1f} refs/draft<br>%{text}<extra></extra>',
marker: {
color: cats.map((_, i) => PALETTE[i % PALETTE.length]),
opacity: 0.85,
},
}], {
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 11 },
margin: { t: 20, r: 20, b: 80, l: 50 },
xaxis: {
tickangle: -35,
gridcolor: 'rgba(71,85,105,0.2)',
},
yaxis: {
title: 'Avg Citations per Draft',
gridcolor: 'rgba(71,85,105,0.2)',
},
}, { responsive: true, displayModeBar: false });
})();
// ===========================================================
// Plotly: Draft-to-Draft Network (Tab 2)
// ===========================================================
(function() {
const edges = influence.draft_network;
if (!edges || edges.length === 0) {
document.getElementById('draftNetworkChart').innerHTML =
'<p class="text-slate-500 text-sm text-center py-20">No draft-to-draft citations found</p>';
return;
}
// Build a simple force-layout-like visualization using Plotly scatter
// We'll use a circular layout for nodes involved in draft-to-draft citations
const nodeSet = new Set();
edges.forEach(e => { nodeSet.add(e.source); nodeSet.add(e.target); });
const nodeList = [...nodeSet];
const nodeIdx = Object.fromEntries(nodeList.map((n, i) => [n, i]));
// Circular layout
const n = nodeList.length;
const cx = 0.5, cy = 0.5, radius = 0.4;
const nodeX = nodeList.map((_, i) => cx + radius * Math.cos(2 * Math.PI * i / n));
const nodeY = nodeList.map((_, i) => cy + radius * Math.sin(2 * Math.PI * i / n));
// Edge traces
const edgeX = [], edgeY = [];
edges.forEach(e => {
const si = nodeIdx[e.source], ti = nodeIdx[e.target];
if (si !== undefined && ti !== undefined) {
edgeX.push(nodeX[si], nodeX[ti], null);
edgeY.push(nodeY[si], nodeY[ti], null);
}
});
// Short names for display
const shortNames = nodeList.map(name => {
const s = name.replace(/^draft-/, '');
return s.length > 25 ? s.slice(0, 23) + '..' : s;
});
const data = [
{
type: 'scatter', mode: 'lines',
x: edgeX, y: edgeY,
line: { color: 'rgba(71,85,105,0.3)', width: 1 },
hoverinfo: 'none',
},
{
type: 'scatter', mode: 'markers+text',
x: nodeX, y: nodeY,
text: shortNames,
textposition: 'top center',
textfont: { size: 8, color: '#94a3b8' },
marker: { size: 8, color: '#3b82f6', opacity: 0.85 },
hovertext: nodeList.map(name => {
const outCount = edges.filter(e => e.source === name).length;
const inCount = edges.filter(e => e.target === name).length;
return `${name}\nOut: ${outCount} | In: ${inCount}`;
}),
hoverinfo: 'text',
},
];
Plotly.newPlot('draftNetworkChart', data, {
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif' },
margin: { t: 10, r: 10, b: 10, l: 10 },
xaxis: { visible: false },
yaxis: { visible: false },
showlegend: false,
}, { responsive: true, displayModeBar: false });
})();
// ===========================================================
// Plotly: BCP Co-citation Heatmap (Tab 3)
// ===========================================================
(function() {
const labels = bcp.heatmap_labels;
const matrix = bcp.heatmap_matrix;
if (!labels || labels.length === 0) {
document.getElementById('bcpHeatmap').innerHTML =
'<p class="text-slate-500 text-sm text-center py-20">No BCP co-citation data</p>';
return;
}
const displayLabels = labels.map(l => 'BCP ' + l);
Plotly.newPlot('bcpHeatmap', [{
type: 'heatmap',
x: displayLabels,
y: displayLabels,
z: matrix,
colorscale: [
[0, '#0f172a'],
[0.1, '#1e3a5f'],
[0.3, '#1d4ed8'],
[0.5, '#3b82f6'],
[0.7, '#60a5fa'],
[1, '#f59e0b'],
],
hovertemplate: '%{x} + %{y}<br>Co-cited in %{z} drafts<extra></extra>',
showscale: true,
colorbar: {
title: { text: 'Co-citations', font: { color: '#94a3b8', size: 10 } },
tickfont: { color: '#94a3b8', size: 9 },
},
}], {
paper_bgcolor: 'rgba(0,0,0,0)',
plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 10 },
margin: { t: 20, r: 60, b: 80, l: 80 },
xaxis: { tickangle: -45, side: 'bottom' },
yaxis: { autorange: 'reversed' },
}, { responsive: true, displayModeBar: false });
})();
</script> </script>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,332 @@
{% extends "base.html" %}
{% set active_page = "complexity" %}
{% block title %}Complexity — 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">Draft Complexity Matrix</h1>
<p class="text-slate-400 text-sm mt-1">Correlating structural complexity (pages, authors, citations, ideas) with quality ratings. Does more complexity mean better drafts?</p>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-4 mb-6">
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Avg Pages</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.stats.avg_pages }}</div>
<div class="text-xs text-slate-500 mt-1">{{ data.stats.pages_coverage_pct }}% have page data</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Avg Authors</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.stats.avg_authors }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Avg Citations</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.stats.avg_citations }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Drafts Analyzed</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.stats.total_drafts }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Metrics</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.metrics | length }} x {{ data.dimensions | length }}</div>
<div class="text-xs text-slate-500 mt-1">complexity x rating</div>
</div>
</div>
<!-- Scatter Plot Matrix -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Complexity vs Rating Scatter Plots</h2>
<p class="text-xs text-slate-500 mb-3">How structural complexity metrics relate to rating dimensions. Each dot is a rated draft. Click to navigate.</p>
<div id="scatterMatrix" style="height: 500px;"></div>
</div>
<!-- Correlation Table -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden mb-6">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Correlation Matrix</h2>
<p class="text-xs text-slate-500 mt-1">Pearson correlation between complexity metrics (rows) and rating dimensions (columns). Green = positive, red = negative. Values range from -1 to +1.</p>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-3 font-medium">Metric</th>
{% for dim in data.dimensions %}
<th class="px-4 py-3 font-medium text-center">{{ dim | capitalize }}</th>
{% endfor %}
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50" id="corrTable">
</tbody>
</table>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Top 10 Most Complex -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Top 10 Most Complex Drafts</h2>
<p class="text-xs text-slate-500 mt-1">Ranked by composite complexity (pages + authors + citations + ideas, normalized).</p>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-2 font-medium">#</th>
<th class="px-4 py-2 font-medium">Draft</th>
<th class="px-4 py-2 font-medium text-right">Pages</th>
<th class="px-4 py-2 font-medium text-right">Authors</th>
<th class="px-4 py-2 font-medium text-right">Cites</th>
<th class="px-4 py-2 font-medium text-right">Score</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50" id="complexTable">
</tbody>
</table>
</div>
</div>
<!-- Top 10 Most Efficient -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Top 10 Most Efficient Drafts</h2>
<p class="text-xs text-slate-500 mt-1">High ratings relative to low structural complexity. Efficiency = score / complexity.</p>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-2 font-medium">#</th>
<th class="px-4 py-2 font-medium">Draft</th>
<th class="px-4 py-2 font-medium text-right">Pages</th>
<th class="px-4 py-2 font-medium text-right">Authors</th>
<th class="px-4 py-2 font-medium text-right">Score</th>
<th class="px-4 py-2 font-medium text-right">Efficiency</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50" id="efficientTable">
</tbody>
</table>
</div>
</div>
</div>
<!-- Category Complexity -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Complexity by Category</h2>
<p class="text-xs text-slate-500 mb-3">Average complexity metrics per category. Wider bars = more complex category.</p>
<div id="catComplexity" style="height: 400px;"></div>
</div>
<!-- Source Complexity -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Complexity by Source</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-3 font-medium">Source</th>
<th class="px-4 py-3 font-medium text-right">Count</th>
<th class="px-4 py-3 font-medium text-right">Avg Pages</th>
<th class="px-4 py-3 font-medium text-right">Avg Authors</th>
<th class="px-4 py-3 font-medium text-right">Avg Citations</th>
<th class="px-4 py-3 font-medium text-right">Avg Score</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for s in data.source_complexity %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-2 text-slate-300 font-medium">{{ s.source | upper }}</td>
<td class="px-4 py-2 text-right text-slate-400">{{ s.count }}</td>
<td class="px-4 py-2 text-right text-slate-300">{{ s.avg_pages }}</td>
<td class="px-4 py-2 text-right text-slate-300">{{ s.avg_authors }}</td>
<td class="px-4 py-2 text-right text-slate-300">{{ s.avg_citations }}</td>
<td class="px-4 py-2 text-right">
{% if s.avg_score >= 3.0 %}
<span class="score-badge score-high">{{ s.avg_score }}</span>
{% elif s.avg_score >= 2.0 %}
<span class="score-badge score-mid">{{ s.avg_score }}</span>
{% else %}
<span class="score-badge score-low">{{ s.avg_score }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
const PLOTLY_LAYOUT = {
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 12 },
margin: { t: 30, r: 20, b: 40, l: 50 },
xaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
yaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
};
const CFG = { responsive: true, displayModeBar: false };
const cdata = {{ data | tojson }};
// Scatter plot matrix (2x3 subplots)
(function() {
const pairs = [
{ x: 'pages', y: 'novelty', xLabel: 'Pages', yLabel: 'Novelty' },
{ x: 'author_count', y: 'maturity', xLabel: 'Authors', yLabel: 'Maturity' },
{ x: 'citation_count', y: 'relevance', xLabel: 'Citations', yLabel: 'Relevance' },
{ x: 'idea_count', y: 'momentum', xLabel: 'Ideas', yLabel: 'Momentum' },
{ x: 'pages', y: 'maturity', xLabel: 'Pages', yLabel: 'Maturity' },
{ x: 'citation_count', y: 'novelty', xLabel: 'Citations', yLabel: 'Novelty' },
];
const colors = ['#3b82f6', '#22c55e', '#a855f7', '#f59e0b', '#06b6d4', '#ef4444'];
const traces = [];
const annotations = [];
pairs.forEach((p, i) => {
const row = i < 3 ? 1 : 2;
const col = (i % 3) + 1;
const xaxis = i === 0 ? 'x' : `x${i + 1}`;
const yaxis = i === 0 ? 'y' : `y${i + 1}`;
const filteredDrafts = p.x === 'pages'
? cdata.drafts.filter(d => d.pages !== null)
: cdata.drafts;
traces.push({
x: filteredDrafts.map(d => d[p.x]),
y: filteredDrafts.map(d => d[p.y]),
text: filteredDrafts.map(d => d.name),
type: 'scatter',
mode: 'markers',
marker: { color: colors[i], size: 5, opacity: 0.6 },
xaxis: xaxis,
yaxis: yaxis,
hovertemplate: `<b>%{text}</b><br>${p.xLabel}: %{x}<br>${p.yLabel}: %{y}<extra></extra>`,
showlegend: false,
});
});
const layout = {
...PLOTLY_LAYOUT,
grid: { rows: 2, columns: 3, pattern: 'independent' },
margin: { t: 30, r: 20, b: 40, l: 50 },
};
// Set axis labels
pairs.forEach((p, i) => {
const xKey = i === 0 ? 'xaxis' : `xaxis${i + 1}`;
const yKey = i === 0 ? 'yaxis' : `yaxis${i + 1}`;
layout[xKey] = { ...PLOTLY_LAYOUT.xaxis, title: { text: p.xLabel, font: { size: 10 } } };
layout[yKey] = { ...PLOTLY_LAYOUT.yaxis, title: { text: p.yLabel, font: { size: 10 } } };
});
Plotly.newPlot('scatterMatrix', traces, layout, CFG);
// Click to navigate
document.getElementById('scatterMatrix').on('plotly_click', function(ev) {
const pt = ev.points[0];
if (pt.text) window.location.href = '/drafts/' + pt.text;
});
})();
// Correlation table
(function() {
const tbody = document.getElementById('corrTable');
const metricLabels = { pages: 'Pages', author_count: 'Authors', citation_count: 'Citations', idea_count: 'Ideas', category_count: 'Categories' };
cdata.metrics.forEach(metric => {
const row = document.createElement('tr');
row.className = 'hover:bg-slate-800/50 transition';
let cells = `<td class="px-4 py-2 text-slate-300 font-medium">${metricLabels[metric] || metric}</td>`;
cdata.dimensions.forEach(dim => {
const val = cdata.correlations[metric][dim];
let bgClass, textColor;
const absVal = Math.abs(val);
if (val > 0.2) { bgClass = 'bg-green-900/30'; textColor = 'text-green-400'; }
else if (val < -0.2) { bgClass = 'bg-red-900/30'; textColor = 'text-red-400'; }
else if (absVal > 0.1) { bgClass = 'bg-yellow-900/20'; textColor = 'text-yellow-400'; }
else { bgClass = ''; textColor = 'text-slate-500'; }
cells += `<td class="px-4 py-2 text-center ${bgClass}"><span class="${textColor} font-mono text-xs">${val.toFixed(3)}</span></td>`;
});
row.innerHTML = cells;
tbody.appendChild(row);
});
})();
// Top 10 Complex table
(function() {
const tbody = document.getElementById('complexTable');
cdata.top_complex.forEach((d, i) => {
const row = document.createElement('tr');
row.className = 'hover:bg-slate-800/50 transition';
const shortName = d.name.replace('draft-', '').substring(0, 35);
const scoreClass = d.score >= 3.0 ? 'score-high' : d.score >= 2.0 ? 'score-mid' : 'score-low';
row.innerHTML = `
<td class="px-4 py-2 text-slate-500 font-mono text-xs">${i + 1}</td>
<td class="px-4 py-2"><a href="/drafts/${d.name}" class="text-blue-400 hover:text-blue-300 text-xs font-mono">${shortName}</a></td>
<td class="px-4 py-2 text-right text-slate-300">${d.pages !== null ? d.pages : '-'}</td>
<td class="px-4 py-2 text-right text-slate-300">${d.author_count}</td>
<td class="px-4 py-2 text-right text-slate-300">${d.citation_count}</td>
<td class="px-4 py-2 text-right"><span class="score-badge ${scoreClass}">${d.score.toFixed(2)}</span></td>
`;
tbody.appendChild(row);
});
})();
// Top 10 Efficient table
(function() {
const tbody = document.getElementById('efficientTable');
cdata.top_efficient.forEach((d, i) => {
const row = document.createElement('tr');
row.className = 'hover:bg-slate-800/50 transition';
const shortName = d.name.replace('draft-', '').substring(0, 35);
const scoreClass = d.score >= 3.0 ? 'score-high' : d.score >= 2.0 ? 'score-mid' : 'score-low';
row.innerHTML = `
<td class="px-4 py-2 text-slate-500 font-mono text-xs">${i + 1}</td>
<td class="px-4 py-2"><a href="/drafts/${d.name}" class="text-blue-400 hover:text-blue-300 text-xs font-mono">${shortName}</a></td>
<td class="px-4 py-2 text-right text-slate-300">${d.pages !== null ? d.pages : '-'}</td>
<td class="px-4 py-2 text-right text-slate-300">${d.author_count}</td>
<td class="px-4 py-2 text-right"><span class="score-badge ${scoreClass}">${d.score.toFixed(2)}</span></td>
<td class="px-4 py-2 text-right text-green-400 font-mono text-xs">${d.efficiency.toFixed(1)}</td>
`;
tbody.appendChild(row);
});
})();
// Category complexity bar chart
(function() {
const cats = cdata.category_complexity.slice(0, 12);
const catNames = cats.map(c => c.category);
Plotly.newPlot('catComplexity', [
{
y: catNames, x: cats.map(c => c.avg_pages),
name: 'Avg Pages', type: 'bar', orientation: 'h',
marker: { color: '#3b82f6', opacity: 0.8 },
},
{
y: catNames, x: cats.map(c => c.avg_citations),
name: 'Avg Citations', type: 'bar', orientation: 'h',
marker: { color: '#22c55e', opacity: 0.8 },
},
{
y: catNames, x: cats.map(c => c.avg_authors * 5),
name: 'Avg Authors (x5)', type: 'bar', orientation: 'h',
marker: { color: '#a855f7', opacity: 0.8 },
},
], {
...PLOTLY_LAYOUT,
barmode: 'group',
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Value' },
yaxis: { ...PLOTLY_LAYOUT.yaxis, autorange: 'reversed' },
legend: { font: { size: 10, color: '#94a3b8' }, orientation: 'h', y: -0.15 },
margin: { t: 10, r: 20, b: 60, l: 150 },
}, CFG);
})();
</script>
{% endblock %}

View File

@@ -0,0 +1,215 @@
{% extends "base.html" %}
{% set active_page = "false_positives" %}
{% block title %}False Positive Profile — 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">False Positive Profile</h1>
<p class="text-slate-400 text-sm mt-1">Analysis of {{ data.count }} drafts flagged as false positives — documents that matched AI/agent search keywords but were determined not to be genuinely about AI agent infrastructure.</p>
</div>
<!-- Stats Panel -->
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-red-500 to-red-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">False Positives</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.count }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-yellow-500 to-yellow-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">% of All Drafts</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.pct_of_total }}%</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<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-xs text-slate-500 uppercase tracking-wider">% of Rated</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.pct_of_rated }}%</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-green-500 to-green-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Total Rated</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.total_rated }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-purple-500 to-purple-400"></div>
<div class="text-xs text-slate-500 uppercase tracking-wider">Total Drafts</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.total_drafts }}</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Box Plots: FP vs Non-FP -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Rating Distributions: FP vs Non-FP</h2>
<p class="text-xs text-slate-500 mb-3">Box plots comparing each rating dimension between false positives (red) and genuine AI/agent drafts (blue). Shows what rating patterns distinguish false positives.</p>
<div id="boxPlots" style="height: 400px;"></div>
</div>
<!-- Source distribution -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">False Positives by Source</h2>
<p class="text-xs text-slate-500 mb-3">Which standards bodies produce the most false positives in our search results.</p>
<div id="sourcePie" style="height: 400px;"></div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Category distribution -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Categories Assigned to False Positives</h2>
<p class="text-xs text-slate-500 mb-3">Categories that the classifier assigned to false positive drafts before they were flagged. Shows which categories are most prone to false matches.</p>
<div id="catBar" style="height: 400px;"></div>
</div>
<!-- Top Terms -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Top Terms in FP Abstracts</h2>
<p class="text-xs text-slate-500 mb-3">Most frequent words in false positive titles and abstracts (stop words excluded). These terms trigger AI/agent keyword matches but appear in unrelated contexts.</p>
<div id="termBar" style="height: 400px;"></div>
</div>
</div>
<!-- FP Table -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">All False Positives</h2>
<p class="text-xs text-slate-500 mt-1">Complete list of flagged drafts. Click a name to view details.</p>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-3 font-medium">#</th>
<th class="px-4 py-3 font-medium">Draft</th>
<th class="px-4 py-3 font-medium">Title</th>
<th class="px-4 py-3 font-medium text-center">Source</th>
<th class="px-4 py-3 font-medium text-center">Relevance</th>
<th class="px-4 py-3 font-medium">Categories</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for fp in data.fp_list %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-3 text-slate-500 font-mono text-xs">{{ loop.index }}</td>
<td class="px-4 py-3">
<a href="/drafts/{{ fp.name }}" class="text-blue-400 hover:text-blue-300 transition text-xs font-mono">{{ fp.name | replace('draft-', '') | truncate(40) }}</a>
</td>
<td class="px-4 py-3 text-slate-300 text-xs max-w-xs truncate">{{ fp.title }}</td>
<td class="px-4 py-3 text-center">
<span class="px-1.5 py-0.5 rounded text-[10px] font-semibold uppercase bg-slate-800 text-slate-400">{{ fp.source }}</span>
</td>
<td class="px-4 py-3 text-center">
<span class="{% if fp.relevance >= 3 %}text-yellow-400{% else %}text-slate-500{% endif %}">{{ fp.relevance }}</span>
</td>
<td class="px-4 py-3">
{% for cat in fp.categories[:2] %}
<span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 text-slate-400">{{ cat }}</span>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
const PLOTLY_LAYOUT = {
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 12 },
margin: { t: 20, r: 20, b: 40, l: 50 },
xaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
yaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
};
const CFG = { responsive: true, displayModeBar: false };
const fpData = {{ data | tojson }};
// Box plots: FP vs Non-FP
const dims = ['novelty', 'maturity', 'overlap', 'momentum', 'relevance'];
const dimLabels = ['Novelty', 'Maturity', 'Overlap', 'Momentum', 'Relevance'];
const boxTraces = [];
dims.forEach((d, i) => {
boxTraces.push({
y: fpData.nonfp_dims[d], name: dimLabels[i] + ' (non-FP)',
type: 'box', marker: { color: '#3b82f6' }, boxmean: true,
legendgroup: dimLabels[i], showlegend: i === 0,
});
boxTraces.push({
y: fpData.fp_dims[d], name: dimLabels[i] + ' (FP)',
type: 'box', marker: { color: '#ef4444' }, boxmean: true,
legendgroup: dimLabels[i], showlegend: i === 0,
});
});
Plotly.newPlot('boxPlots', boxTraces, {
...PLOTLY_LAYOUT,
boxmode: 'group',
showlegend: true,
legend: { font: { size: 10, color: '#94a3b8' }, orientation: 'h', y: 1.1 },
yaxis: { ...PLOTLY_LAYOUT.yaxis, range: [0.5, 5.5], dtick: 1, title: 'Rating' },
margin: { t: 40, r: 10, b: 30, l: 50 },
}, CFG);
// Source pie
const srcLabels = Object.keys(fpData.fp_sources);
const srcValues = Object.values(fpData.fp_sources);
const srcColors = {
'ietf': '#3b82f6', 'iso': '#22c55e', 'itu': '#eab308',
'w3c': '#a855f7', 'etsi': '#ef4444', 'nist': '#06b6d4',
};
Plotly.newPlot('sourcePie', [{
labels: srcLabels.map(s => s.toUpperCase()),
values: srcValues,
type: 'pie',
marker: { colors: srcLabels.map(s => srcColors[s] || '#64748b') },
textinfo: 'label+value',
textfont: { color: '#e2e8f0', size: 12 },
hovertemplate: '%{label}: %{value} drafts (%{percent})<extra></extra>',
hole: 0.4,
}], {
...PLOTLY_LAYOUT,
showlegend: false,
margin: { t: 10, r: 10, b: 10, l: 10 },
}, CFG);
// Category bar
const catLabels = Object.keys(fpData.fp_categories);
const catValues = Object.values(fpData.fp_categories);
Plotly.newPlot('catBar', [{
x: catValues,
y: catLabels,
type: 'bar',
orientation: 'h',
marker: { color: '#ef4444', opacity: 0.7 },
hovertemplate: '%{y}: %{x} drafts<extra></extra>',
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Draft Count' },
yaxis: { ...PLOTLY_LAYOUT.yaxis, autorange: 'reversed' },
margin: { t: 10, r: 10, b: 40, l: 180 },
}, CFG);
// Top terms bar
const terms = fpData.top_terms;
const termLabels = terms.map(t => t[0]);
const termValues = terms.map(t => t[1]);
Plotly.newPlot('termBar', [{
x: termValues,
y: termLabels,
type: 'bar',
orientation: 'h',
marker: { color: '#f59e0b', opacity: 0.7 },
hovertemplate: '%{y}: %{x} occurrences<extra></extra>',
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Occurrences' },
yaxis: { ...PLOTLY_LAYOUT.yaxis, autorange: 'reversed' },
margin: { t: 10, r: 10, b: 40, l: 120 },
}, CFG);
</script>
{% endblock %}

View File

@@ -0,0 +1,330 @@
{% extends "base.html" %}
{% set active_page = "idea_analysis" %}
{% block title %}Idea Novelty Analysis — 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">Idea Novelty Deep Dive</h1>
<p class="text-slate-400 text-sm mt-1">Comprehensive analysis of {{ data.total }} technical ideas extracted from IETF AI/agent drafts. Explores novelty distribution, type breakdowns, cross-draft patterns, and correlations with draft ratings.</p>
</div>
<!-- Stats panel -->
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-3xl font-bold text-blue-400">{{ data.total }}</div>
<div class="text-xs text-slate-400 mt-1">Total Ideas</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-3xl font-bold text-purple-400">{{ data.type_count }}</div>
<div class="text-xs text-slate-400 mt-1">Idea Types</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-3xl font-bold text-green-400">{{ data.avg_novelty }}</div>
<div class="text-xs text-slate-400 mt-1">Avg Novelty Score</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-3xl font-bold text-amber-400">{{ data.scored }}</div>
<div class="text-xs text-slate-400 mt-1">Scored Ideas</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-3xl font-bold text-cyan-400">{{ data.embed_pct }}%</div>
<div class="text-xs text-slate-400 mt-1">Embeddings ({{ data.embed_count }}/{{ data.total }})</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-3xl font-bold text-rose-400">{{ data.shared_ideas | length }}</div>
<div class="text-xs text-slate-400 mt-1">Shared Ideas (2+ drafts)</div>
</div>
</div>
<!-- Row 1: Novelty histogram + Type bar chart -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Novelty Score Distribution</h2>
<p class="text-xs text-slate-500 mb-3">How many ideas at each novelty level (1=incremental, 5=groundbreaking). {{ data.unscored }} ideas have no novelty score yet.</p>
<div id="noveltyHist" style="height: 300px;"></div>
</div>
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Ideas by Type (avg novelty color)</h2>
<p class="text-xs text-slate-500 mb-3">Count of ideas per type. Bar color intensity reflects average novelty score — brighter = more novel.</p>
<div id="typeChart" style="height: 300px;"></div>
</div>
</div>
<!-- Row 2: Scatter + Sunburst -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Draft Avg Idea Novelty vs Relevance</h2>
<p class="text-xs text-slate-500 mb-3">Each dot is a draft. X-axis = average novelty of its ideas, Y-axis = relevance score. Bubble size = number of ideas. Click to view draft.</p>
<div id="scatterChart" style="height: 380px;"></div>
</div>
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Idea Type Breakdown (Sunburst)</h2>
<p class="text-xs text-slate-500 mb-3">Hierarchical view: outer ring shows novelty bands (High/Medium/Low) within each type.</p>
<div id="sunburstChart" style="height: 380px;"></div>
</div>
</div>
<!-- Ideas per draft distribution -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Ideas per Draft Distribution</h2>
<p class="text-xs text-slate-500 mb-3">How many ideas does each draft contribute? Most drafts have 2-4 ideas; some prolific drafts generate 8+.</p>
<div id="ipdChart" style="height: 280px;"></div>
</div>
<!-- Top 20 most novel ideas -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden mb-6">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Top 20 Most Novel Ideas</h2>
<p class="text-xs text-slate-500 mt-1">Ideas with novelty score of 4 or 5, sorted by novelty then draft composite score.</p>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-3 font-medium">#</th>
<th class="px-4 py-3 font-medium">Idea</th>
<th class="px-4 py-3 font-medium text-center">Novelty</th>
<th class="px-4 py-3 font-medium">Type</th>
<th class="px-4 py-3 font-medium">Draft</th>
<th class="px-4 py-3 font-medium">Description</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for idea in data.top_novel %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-3 text-slate-500 font-mono text-xs">{{ loop.index }}</td>
<td class="px-4 py-3 text-slate-200 text-xs font-medium max-w-[200px] truncate" title="{{ idea.title }}">{{ idea.title }}</td>
<td class="px-4 py-3 text-center">
<span class="px-2 py-0.5 rounded text-xs font-mono {% if idea.novelty_score == 5 %}bg-green-500/20 text-green-400{% else %}bg-emerald-500/20 text-emerald-400{% endif %}">{{ idea.novelty_score }}</span>
</td>
<td class="px-4 py-3">
<span class="px-2 py-0.5 rounded text-[10px] bg-blue-500/20 text-blue-400">{{ idea.type }}</span>
</td>
<td class="px-4 py-3">
<a href="/drafts/{{ idea.draft_name }}" class="text-blue-400 hover:text-blue-300 transition text-xs font-mono">{{ idea.draft_name | replace('draft-', '') | truncate(35) }}</a>
</td>
<td class="px-4 py-3 text-xs text-slate-500 max-w-[300px] truncate" title="{{ idea.description }}">{{ idea.description | truncate(120) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Shared ideas across drafts -->
{% if data.shared_ideas %}
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden mb-6">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Ideas Shared Across Multiple Drafts</h2>
<p class="text-xs text-slate-500 mt-1">{{ data.shared_ideas | length }} ideas appear in 2 or more drafts, indicating convergent thinking or common building blocks.</p>
</div>
<div class="divide-y divide-slate-800/50 max-h-[500px] overflow-y-auto">
{% for idea in data.shared_ideas[:30] %}
<div class="px-4 py-3 hover:bg-slate-800/50 transition">
<div class="flex items-center gap-2 mb-1 flex-wrap">
<span class="text-sm font-medium text-slate-200">{{ idea.title }}</span>
<span class="px-2 py-0.5 rounded text-[10px] font-mono bg-amber-500/20 text-amber-400">{{ idea.appearances }}x</span>
{% for t in idea.types %}
<span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-700 text-slate-400">{{ t }}</span>
{% endfor %}
</div>
<div class="flex flex-wrap gap-1 mt-1">
{% for d in idea.drafts %}
<a href="/drafts/{{ d }}" class="text-[10px] text-slate-600 hover:text-blue-400 transition font-mono">{{ d | replace('draft-', '') | truncate(30) }}</a>
{% if not loop.last %}<span class="text-slate-700 text-[10px]">|</span>{% endif %}
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Top idea-producing drafts -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden mb-6">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Most Prolific Drafts (by idea count)</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-3 font-medium">#</th>
<th class="px-4 py-3 font-medium">Draft</th>
<th class="px-4 py-3 font-medium text-center">Ideas</th>
<th class="px-4 py-3 font-medium text-center">Score</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for d in data.top_idea_drafts %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-3 text-slate-500 font-mono text-xs">{{ loop.index }}</td>
<td class="px-4 py-3">
<a href="/drafts/{{ d.name }}" class="text-blue-400 hover:text-blue-300 transition text-xs font-mono">{{ d.name | replace('draft-', '') | truncate(45) }}</a>
{% if d.draft_title %}
<div class="text-[10px] text-slate-600 mt-0.5">{{ d.draft_title | truncate(60) }}</div>
{% endif %}
</td>
<td class="px-4 py-3 text-center text-slate-300 font-mono">{{ d.idea_count }}</td>
<td class="px-4 py-3 text-center">
{% if d.score %}
<span class="score-badge {% if d.score >= 3.5 %}score-high{% elif d.score >= 2.5 %}score-mid{% else %}score-low{% endif %}">{{ d.score | round(2) }}</span>
{% else %}
<span class="text-slate-600">--</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Embedding status note -->
<div class="bg-slate-900/50 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-2">Embedding Coverage</h2>
<div class="flex items-center gap-4">
<div class="flex-1">
<div class="w-full bg-slate-800 rounded-full h-3">
<div class="bg-blue-500 h-3 rounded-full transition-all" style="width: {{ data.embed_pct }}%"></div>
</div>
</div>
<span class="text-sm text-slate-400">{{ data.embed_count }} / {{ data.total }} ({{ data.embed_pct }}%)</span>
</div>
<p class="text-xs text-slate-500 mt-2">To complete missing embeddings, run: <code class="bg-slate-800 px-2 py-0.5 rounded text-slate-300">ietf embed-ideas</code>. This requires Ollama running locally. Embeddings enable idea similarity search and clustering.</p>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
const PLOTLY_LAYOUT = {
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 12 },
margin: { t: 20, r: 20, b: 40, l: 50 },
xaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
yaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
};
const CFG = { responsive: true, displayModeBar: false };
const data = {{ data | tojson }};
// --- Novelty Histogram ---
Plotly.newPlot('noveltyHist', [{
x: data.novelty_histogram.labels,
y: data.novelty_histogram.values,
type: 'bar',
marker: {
color: ['#ef4444', '#f97316', '#eab308', '#22c55e', '#10b981'],
line: { color: '#0f172a', width: 1 },
},
text: data.novelty_histogram.values,
textposition: 'outside',
textfont: { color: '#94a3b8', size: 12 },
hovertemplate: 'Novelty %{x}: %{y} ideas<extra></extra>',
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Novelty Score', dtick: 1 },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Count' },
margin: { t: 30, r: 20, b: 50, l: 60 },
}, CFG);
// --- Type Bar Chart (colored by avg novelty) ---
const types = data.by_type.map(t => t.type).reverse();
const typeCounts = data.by_type.map(t => t.count).reverse();
const typeAvgN = data.by_type.map(t => t.avg_novelty).reverse();
const typeColors = typeAvgN.map(n => {
// Map avg novelty (1-5) to color intensity: red -> yellow -> green
if (n >= 3.5) return '#22c55e';
if (n >= 3.0) return '#84cc16';
if (n >= 2.5) return '#eab308';
if (n >= 2.0) return '#f97316';
return '#ef4444';
});
Plotly.newPlot('typeChart', [{
y: types, x: typeCounts,
type: 'bar', orientation: 'h',
marker: { color: typeColors },
text: typeAvgN.map(n => `avg N: ${n.toFixed(1)}`),
textposition: 'auto',
textfont: { color: '#e2e8f0', size: 10 },
hovertemplate: '<b>%{y}</b><br>Count: %{x}<br>Avg Novelty: %{text}<extra></extra>',
}], {
...PLOTLY_LAYOUT,
margin: { t: 10, r: 20, b: 40, l: 120 },
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Count' },
}, CFG);
// --- Scatter: draft avg idea novelty vs relevance ---
const scatter = data.scatter_data;
const sourceGroups = {};
scatter.forEach(d => {
if (!sourceGroups[d.source]) sourceGroups[d.source] = { x: [], y: [], size: [], text: [] };
sourceGroups[d.source].x.push(d.avg_idea_novelty);
sourceGroups[d.source].y.push(d.relevance);
sourceGroups[d.source].size.push(Math.max(d.idea_count * 3, 6));
sourceGroups[d.source].text.push(d.name);
});
const scatterTraces = Object.entries(sourceGroups).map(([src, d]) => ({
x: d.x, y: d.y, text: d.text, name: src,
mode: 'markers', type: 'scatter',
marker: { size: d.size, opacity: 0.7 },
hovertemplate: '<b>%{text}</b><br>Avg Idea Novelty: %{x:.2f}<br>Relevance: %{y}<br>Ideas: %{marker.size:.0f}<extra>' + src + '</extra>',
}));
Plotly.newPlot('scatterChart', scatterTraces, {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Avg Idea Novelty', range: [0.5, 5.5] },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Draft Relevance Score', range: [0.5, 5.5], dtick: 1 },
legend: { font: { size: 10, color: '#94a3b8' } },
hovermode: 'closest',
margin: { t: 20, r: 20, b: 50, l: 60 },
}, CFG);
document.getElementById('scatterChart').on('plotly_click', function(ev) {
const pt = ev.points[0];
if (pt.text) window.location.href = '/drafts/' + pt.text;
});
// --- Sunburst ---
const sb = data.sunburst;
Plotly.newPlot('sunburstChart', [{
type: 'sunburst',
labels: sb.labels,
parents: sb.parents,
values: sb.values,
branchvalues: 'total',
textinfo: 'label+value',
textfont: { size: 10, color: '#e2e8f0' },
marker: {
line: { width: 1, color: '#0f172a' },
},
insidetextorientation: 'radial',
}], {
...PLOTLY_LAYOUT,
margin: { t: 10, r: 10, b: 10, l: 10 },
}, CFG);
// --- Ideas per Draft histogram ---
const ipd = data.ideas_per_draft_hist;
Plotly.newPlot('ipdChart', [{
x: ipd.labels,
y: ipd.values,
type: 'bar',
marker: { color: '#6366f1', line: { color: '#4338ca', width: 1 } },
text: ipd.values,
textposition: 'outside',
textfont: { color: '#94a3b8', size: 10 },
hovertemplate: '%{x} ideas/draft: %{y} drafts<extra></extra>',
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Ideas per Draft', dtick: 1 },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Number of Drafts' },
margin: { t: 30, r: 20, b: 50, l: 60 },
}, CFG);
</script>
{% endblock %}

View File

@@ -0,0 +1,198 @@
{% extends "base.html" %}
{% set active_page = "sources" %}
{% block title %}Cross-Source Comparison — 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">Cross-Source Comparison</h1>
<p class="text-slate-400 text-sm mt-1">Comparing drafts across {{ data.summary | length }} standards bodies on rating dimensions, category focus, and output volume.</p>
</div>
<!-- Summary Table -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden mb-6">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Standards Body Summary</h2>
<p class="text-xs text-slate-500 mt-1">Overview of each source's contribution to the AI/agent standards landscape.</p>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-3 font-medium">Source</th>
<th class="px-4 py-3 font-medium text-center">Drafts</th>
<th class="px-4 py-3 font-medium text-center">Rated</th>
<th class="px-4 py-3 font-medium text-center">Authors</th>
<th class="px-4 py-3 font-medium text-center">Ideas</th>
<th class="px-4 py-3 font-medium text-center">Avg Score</th>
<th class="px-4 py-3 font-medium">Top Category</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for row in data.summary | sort(attribute='drafts', reverse=True) %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-3">
<span class="inline-block px-2 py-0.5 rounded text-xs font-semibold uppercase
{% if row.source == 'ietf' %}bg-blue-500/20 text-blue-400
{% elif row.source == 'iso' %}bg-green-500/20 text-green-400
{% elif row.source == 'itu' %}bg-yellow-500/20 text-yellow-400
{% elif row.source == 'w3c' %}bg-purple-500/20 text-purple-400
{% elif row.source == 'etsi' %}bg-red-500/20 text-red-400
{% elif row.source == 'nist' %}bg-cyan-500/20 text-cyan-400
{% else %}bg-slate-500/20 text-slate-400{% endif %}">{{ row.source }}</span>
</td>
<td class="px-4 py-3 text-center text-white font-mono">{{ row.drafts }}</td>
<td class="px-4 py-3 text-center text-slate-400 font-mono">{{ row.rated }}</td>
<td class="px-4 py-3 text-center text-slate-400 font-mono">{{ row.authors }}</td>
<td class="px-4 py-3 text-center text-slate-400 font-mono">{{ row.ideas }}</td>
<td class="px-4 py-3 text-center">
{% if row.avg_score >= 3.5 %}
<span class="score-badge score-high">{{ row.avg_score }}</span>
{% elif row.avg_score >= 2.5 %}
<span class="score-badge score-mid">{{ row.avg_score }}</span>
{% else %}
<span class="score-badge score-low">{{ row.avg_score }}</span>
{% endif %}
</td>
<td class="px-4 py-3">
<span class="px-2 py-0.5 rounded text-[10px] bg-slate-800 text-slate-400">{{ row.top_category }}</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Radar Chart -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Rating Dimensions by Source</h2>
<p class="text-xs text-slate-500 mb-3">Average rating across five dimensions for each standards body. Larger shapes indicate higher average ratings.</p>
<div id="radarChart" style="height: 400px;"></div>
</div>
<!-- Stacked Bar Chart -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Category Distribution by Source</h2>
<p class="text-xs text-slate-500 mb-3">What topics each standards body focuses on. Stacked bars show the relative emphasis per source.</p>
<div id="stackedBar" style="height: 400px;"></div>
</div>
</div>
<!-- Heatmap -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Sources x Categories Heatmap</h2>
<p class="text-xs text-slate-500 mb-3">Draft counts per source-category pair. Darker cells = more drafts. Shows where each body concentrates its work.</p>
<div id="heatmap" style="height: 400px;"></div>
</div>
<!-- Unique/Shared Categories -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-3">Shared Categories</h2>
<p class="text-xs text-slate-500 mb-3">Categories covered by multiple standards bodies.</p>
<div class="flex flex-wrap gap-2">
{% for cat in data.shared_categories %}
<a href="/drafts?cat={{ cat }}" class="px-2 py-1 rounded text-xs bg-blue-500/15 text-blue-400 border border-blue-500/20 hover:bg-blue-500/25 transition">{{ cat }}</a>
{% endfor %}
</div>
</div>
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-3">Unique Categories by Source</h2>
<p class="text-xs text-slate-500 mb-3">Categories only covered by a single standards body.</p>
{% for src, cats in data.unique_categories.items() %}
{% if cats %}
<div class="mb-3">
<span class="text-xs font-semibold uppercase text-slate-500">{{ src }}</span>
<div class="flex flex-wrap gap-1 mt-1">
{% for cat in cats %}
<span class="px-2 py-0.5 rounded text-[10px] bg-slate-800 text-slate-400">{{ cat }}</span>
{% endfor %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
const PLOTLY_LAYOUT = {
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 12 },
margin: { t: 20, r: 20, b: 40, l: 50 },
xaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
yaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
};
const CFG = { responsive: true, displayModeBar: false };
const data = {{ data | tojson }};
// Source colors
const sourceColors = {
'ietf': '#3b82f6', 'iso': '#22c55e', 'itu': '#eab308',
'w3c': '#a855f7', 'etsi': '#ef4444', 'nist': '#06b6d4',
};
function srcColor(src) { return sourceColors[src] || '#64748b'; }
// Radar Chart
const radarDims = ['novelty', 'maturity', 'overlap', 'momentum', 'relevance'];
const radarLabels = ['Novelty', 'Maturity', 'Overlap', 'Momentum', 'Relevance'];
const radarTraces = Object.entries(data.radar).map(([src, vals]) => ({
type: 'scatterpolar',
r: radarDims.map(d => vals[d]).concat([vals[radarDims[0]]]),
theta: radarLabels.concat([radarLabels[0]]),
fill: 'toself',
name: `${src.toUpperCase()} (${vals.count})`,
opacity: 0.45,
line: { color: srcColor(src) },
fillcolor: srcColor(src) + '33',
}));
Plotly.newPlot('radarChart', radarTraces, {
...PLOTLY_LAYOUT,
polar: {
bgcolor: 'rgba(0,0,0,0)',
radialaxis: { visible: true, range: [0, 5], gridcolor: '#1e293b', color: '#64748b' },
angularaxis: { gridcolor: '#1e293b', color: '#94a3b8' },
},
legend: { font: { size: 10, color: '#94a3b8' } },
margin: { t: 30, r: 60, b: 30, l: 60 },
}, CFG);
// Stacked Bar Chart
const heatmap = data.heatmap;
const barTraces = heatmap.categories.map((cat, ci) => ({
name: cat,
type: 'bar',
x: heatmap.sources.map(s => s.toUpperCase()),
y: heatmap.values.map(row => row[ci]),
}));
Plotly.newPlot('stackedBar', barTraces, {
...PLOTLY_LAYOUT,
barmode: 'stack',
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: '' },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Draft Count' },
legend: { font: { size: 9, color: '#94a3b8' }, orientation: 'h', y: -0.25 },
margin: { t: 10, r: 10, b: 80, l: 50 },
}, CFG);
// Heatmap
Plotly.newPlot('heatmap', [{
z: heatmap.values,
x: heatmap.categories,
y: heatmap.sources.map(s => s.toUpperCase()),
type: 'heatmap',
colorscale: [[0, '#0f172a'], [0.5, '#1e40af'], [1, '#3b82f6']],
hovertemplate: '%{y} / %{x}<br>Drafts: %{z}<extra></extra>',
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, tickangle: -45 },
yaxis: { ...PLOTLY_LAYOUT.yaxis, autorange: 'reversed' },
margin: { t: 10, r: 10, b: 120, l: 60 },
}, CFG);
</script>
{% endblock %}

View File

@@ -0,0 +1,284 @@
{% extends "base.html" %}
{% set active_page = "trends" %}
{% block title %}Trends — 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">Temporal Evolution</h1>
<p class="text-slate-400 text-sm mt-1">How the AI standards landscape is changing over time. Submission volume, rating trends, category shifts, and the safety-vs-capability balance.</p>
</div>
<!-- Stats Cards -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Months Tracked</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.months | length }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Total Submissions</div>
<div class="text-2xl font-bold text-white mt-1">{{ data.monthly_table | sum(attribute='total') }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Fastest Growing</div>
<div class="text-lg font-bold text-green-400 mt-1">{{ data.stats.fastest_growing or 'N/A' }}</div>
</div>
<div class="stat-card rounded-xl border border-slate-800 p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">Newest Category</div>
<div class="text-lg font-bold text-blue-400 mt-1">{{ data.stats.newest_active or 'N/A' }}</div>
</div>
</div>
<!-- Monthly Submissions by Source -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Monthly Draft Submissions by Source</h2>
<p class="text-xs text-slate-500 mb-3">Stacked area chart showing submission volume over time, broken down by source (IETF, W3C, etc.).</p>
<div id="submissionChart" style="height: 350px;"></div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Monthly Average Ratings -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Monthly Average Ratings</h2>
<p class="text-xs text-slate-500 mb-3">Are drafts getting more mature? More novel? Track the five rating dimensions over time.</p>
<div id="ratingsChart" style="height: 350px;"></div>
</div>
<!-- Safety Ratio -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Safety vs Capability Ratio</h2>
<p class="text-xs text-slate-500 mb-3">Ratio of safety-related drafts (Security, Privacy, Trust, Safety, Governance) to capability drafts (Agents, Infrastructure, MCP, etc.). Higher = more safety focus.</p>
<div id="safetyChart" style="height: 350px;"></div>
</div>
</div>
<!-- Category Distribution Over Time -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5 mb-6">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Category Distribution Over Time</h2>
<p class="text-xs text-slate-500 mb-3">Stacked area showing which topics are growing or shrinking. Top 8 categories shown.</p>
<div id="categoryChart" style="height: 400px;"></div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<!-- Cumulative Ideas -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Cumulative Idea Count</h2>
<p class="text-xs text-slate-500 mb-3">Total number of unique technical ideas extracted from drafts over time.</p>
<div id="ideasChart" style="height: 300px;"></div>
</div>
<!-- New Authors -->
<div class="bg-slate-900 rounded-xl border border-slate-800 p-5">
<h2 class="text-sm font-semibold text-slate-300 mb-1">Monthly New Authors</h2>
<p class="text-xs text-slate-500 mb-3">First-time contributors entering the AI standards space each month.</p>
<div id="authorsChart" style="height: 300px;"></div>
</div>
</div>
<!-- Monthly Breakdown Table -->
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
<div class="p-4 border-b border-slate-800">
<h2 class="text-sm font-semibold text-slate-300">Monthly Breakdown</h2>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm">
<thead>
<tr class="border-b border-slate-800 text-left text-xs text-slate-500">
<th class="px-4 py-3 font-medium">Month</th>
<th class="px-4 py-3 font-medium text-right">Total</th>
<th class="px-4 py-3 font-medium text-right">Avg Score</th>
<th class="px-4 py-3 font-medium">Trend</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800/50">
{% for row in data.monthly_table %}
<tr class="hover:bg-slate-800/50 transition">
<td class="px-4 py-2 font-mono text-xs text-slate-300">{{ row.month }}</td>
<td class="px-4 py-2 text-right text-slate-300">{{ row.total }}</td>
<td class="px-4 py-2 text-right">
{% if row.avg_score >= 3.0 %}
<span class="text-green-400">{{ row.avg_score }}</span>
{% elif row.avg_score >= 2.0 %}
<span class="text-yellow-400">{{ row.avg_score }}</span>
{% else %}
<span class="text-slate-500">{{ row.avg_score }}</span>
{% endif %}
</td>
<td class="px-4 py-2">
{% if loop.index > 1 %}
{% set prev = data.monthly_table[loop.index0 - 1].total %}
{% if row.total > prev %}
<span class="text-green-400 text-xs font-mono">&#x2191; +{{ row.total - prev }}</span>
{% elif row.total < prev %}
<span class="text-red-400 text-xs font-mono">&#x2193; {{ row.total - prev }}</span>
{% else %}
<span class="text-slate-500 text-xs font-mono">&#x2192; 0</span>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
const PLOTLY_LAYOUT = {
paper_bgcolor: 'rgba(0,0,0,0)', plot_bgcolor: 'rgba(0,0,0,0)',
font: { color: '#94a3b8', family: 'Inter, system-ui, sans-serif', size: 12 },
margin: { t: 20, r: 20, b: 40, l: 50 },
xaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
yaxis: { gridcolor: '#1e293b', zerolinecolor: '#334155' },
};
const CFG = { responsive: true, displayModeBar: false };
const data = {{ data | tojson }};
// 1. Monthly submissions by source (stacked area)
(function() {
const bySource = {};
data.monthly_submissions.forEach(r => {
if (!bySource[r.source]) bySource[r.source] = {};
bySource[r.source][r.month] = r.count;
});
const months = data.months;
const colors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#a855f7', '#06b6d4'];
const traces = Object.entries(bySource).map(([src, monthly], i) => ({
x: months,
y: months.map(m => monthly[m] || 0),
name: src.toUpperCase(),
type: 'scatter',
mode: 'lines',
stackgroup: 'one',
line: { color: colors[i % colors.length], width: 0 },
fillcolor: colors[i % colors.length] + '80',
}));
Plotly.newPlot('submissionChart', traces, {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Month' },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Draft Count' },
legend: { font: { size: 10, color: '#94a3b8' } },
}, CFG);
})();
// 2. Monthly average ratings (5 lines)
(function() {
const dims = ['novelty', 'maturity', 'overlap', 'momentum', 'relevance'];
const labels = ['Novelty', 'Maturity', 'Overlap', 'Momentum', 'Relevance'];
const colors = ['#3b82f6', '#22c55e', '#ef4444', '#f59e0b', '#a855f7'];
const months = data.monthly_ratings.map(r => r.month);
const traces = dims.map((d, i) => ({
x: months,
y: data.monthly_ratings.map(r => r[d]),
name: labels[i],
type: 'scatter',
mode: 'lines+markers',
line: { color: colors[i], width: 2 },
marker: { size: 4 },
}));
Plotly.newPlot('ratingsChart', traces, {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Avg Rating (1-5)', range: [0.5, 5.5] },
legend: { font: { size: 10, color: '#94a3b8' } },
}, CFG);
})();
// 3. Safety ratio
(function() {
const months = data.safety_ratio.map(r => r.month);
Plotly.newPlot('safetyChart', [
{
x: months,
y: data.safety_ratio.map(r => r.ratio),
type: 'scatter',
mode: 'lines+markers',
line: { color: '#22c55e', width: 2 },
marker: { size: 5 },
name: 'Safety/Capability Ratio',
hovertemplate: '%{x}<br>Ratio: %{y:.2f}<br>Safety: %{customdata[0]}<br>Capability: %{customdata[1]}<extra></extra>',
customdata: data.safety_ratio.map(r => [r.safety, r.capability]),
},
{
x: months,
y: months.map(() => 1.0),
type: 'scatter',
mode: 'lines',
line: { color: '#64748b', width: 1, dash: 'dash' },
name: 'Parity line',
showlegend: false,
}
], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Ratio (safety / capability)' },
showlegend: false,
}, CFG);
})();
// 4. Category distribution (stacked area)
(function() {
const months = data.months;
const cats = data.top_categories;
const colors = ['#3b82f6', '#22c55e', '#f59e0b', '#ef4444', '#a855f7', '#06b6d4', '#f97316', '#ec4899'];
// Build per-cat monthly data
const catMonthly = {};
data.monthly_categories.forEach(r => {
if (!catMonthly[r.category]) catMonthly[r.category] = {};
catMonthly[r.category][r.month] = r.count;
});
const traces = cats.map((cat, i) => ({
x: months,
y: months.map(m => (catMonthly[cat] || {})[m] || 0),
name: cat,
type: 'scatter',
mode: 'lines',
stackgroup: 'one',
line: { color: colors[i % colors.length], width: 0 },
fillcolor: colors[i % colors.length] + '80',
}));
Plotly.newPlot('categoryChart', traces, {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis, title: 'Month' },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Draft Count' },
legend: { font: { size: 10, color: '#94a3b8' } },
}, CFG);
})();
// 5. Cumulative ideas
(function() {
Plotly.newPlot('ideasChart', [{
x: data.cumulative_ideas.map(r => r.month),
y: data.cumulative_ideas.map(r => r.total),
type: 'scatter',
mode: 'lines',
fill: 'tozeroy',
line: { color: '#3b82f6', width: 2 },
fillcolor: 'rgba(59,130,246,0.15)',
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'Total Ideas' },
showlegend: false,
}, CFG);
})();
// 6. New authors per month
(function() {
Plotly.newPlot('authorsChart', [{
x: data.monthly_new_authors.map(r => r.month),
y: data.monthly_new_authors.map(r => r.count),
type: 'bar',
marker: { color: '#a855f7', opacity: 0.8 },
}], {
...PLOTLY_LAYOUT,
xaxis: { ...PLOTLY_LAYOUT.xaxis },
yaxis: { ...PLOTLY_LAYOUT.yaxis, title: 'New Authors' },
showlegend: false,
}, CFG);
})();
</script>
{% endblock %}