<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: K Gann</title>
    <description>The latest articles on DEV Community by K Gann (@jubilee_happy_a567008f769).</description>
    <link>https://dev.to/jubilee_happy_a567008f769</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4020347%2F7a2e4d14-ff58-40f7-b71d-c0c65aee3826.png</url>
      <title>DEV Community: K Gann</title>
      <link>https://dev.to/jubilee_happy_a567008f769</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jubilee_happy_a567008f769"/>
    <language>en</language>
    <item>
      <title>Adding a Third Kind of Judge: What Cross-LLM Evaluation Changed About the Winner</title>
      <dc:creator>K Gann</dc:creator>
      <pubDate>Sat, 25 Jul 2026 01:49:49 +0000</pubDate>
      <link>https://dev.to/jubilee_happy_a567008f769/adding-a-third-kind-of-judge-what-cross-llm-evaluation-changed-about-the-winner-54i5</link>
      <guid>https://dev.to/jubilee_happy_a567008f769/adding-a-third-kind-of-judge-what-cross-llm-evaluation-changed-about-the-winner-54i5</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 3 of a series on auto-grooming Jira backlogs with ML and LLMs. &lt;a href="https://dev.to/jubilee_happy_a567008f769/how-i-auto-groomed-500-jira-tickets-with-ml-and-llm-1j7k"&gt;Read Part 1&lt;/a&gt; and &lt;a href="https://dev.to/jubilee_happy_a567008f769/judging-the-judges-what-happens-when-you-ask-an-llm-to-grade-two-other-llms-54oe"&gt;Part 2&lt;/a&gt; for the original pipeline and the first Gemini-vs-human evaluation.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking Up Where Part 2 Left Off
&lt;/h2&gt;

&lt;p&gt;Part 2 compared two clustering pipelines through two LLMs, judged by Gemini, then checked against my own review. The finding that stuck with me: Gemini kept flagging correct arithmetic as hallucination, specifically on outputs where a model had to compute something rather than just quote it back.&lt;/p&gt;

&lt;p&gt;That result raised an obvious follow-up. Was that a Gemini-specific quirk, or something LLM judges do generally? The only way to check is to add a different kind of judge — not another neutral third party, but the two LLMs actually under evaluation, each scoring the other's work. So I had Claude Sonnet 4.5 score all six of GPT-5.5's outputs, and had GPT-5.5 score all six of Claude's, using the same rubric Gemini used.&lt;/p&gt;

&lt;p&gt;That gives every output three independent scores instead of two: Gemini, me, and whichever model didn't produce it. Enough to actually answer two things I couldn't in Part 2 — a Kappa score I trust, and a real answer to "which one do I put in front of my manager."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Cross-Judging, Not Self-Judging
&lt;/h2&gt;

&lt;p&gt;To be clear about what this setup does and doesn't test: Claude never scores its own output, and neither does GPT-5.5. This isn't a check for a model favoring itself — I don't have that data. It's a check for whether a model is harsher or more lenient toward a &lt;em&gt;competitor's&lt;/em&gt; output than a neutral judge and a human are. Different question, still useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Weighting the Three Judges
&lt;/h2&gt;

&lt;p&gt;Averaging all three scores equally felt wrong the moment I thought about it. Gemini and the cross-judge are both LLMs, and Part 2 already showed they share a specific blind spot — treating unverified inference as hallucination — on the exact criteria that matter most (Data Synthesis, Age-Aware Reasoning). If I average three raters and two of them share a correlated bias, that bias doesn't cancel out. It gets a two-thirds vote.&lt;/p&gt;

&lt;p&gt;So the composite score here is weighted: &lt;strong&gt;50% human, 25% Gemini, 25% cross-judge.&lt;/strong&gt; Concretely, for every criterion on every output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;composite = 0.5 × Human_score + 0.25 × Gemini_score + 0.25 × Cross-judge_score
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, since not every criterion applies to every output type, I summed the composite scores across whichever criteria actually applied and divided by the applicable maximum:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;% score = sum(composite scores across applicable criteria) / (5 × count of applicable criteria)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the number in the winner table below — it answers one question: &lt;em&gt;how good is this output&lt;/em&gt;, once the human's judgment is weighted to matter most. And hallucination flags don't get silently averaged away — if I'd already reviewed a specific claim and judged it legitimate (like GPT-5.5's PM report in Part 2), that call stands. If a flag comes up on a claim I &lt;em&gt;hadn't&lt;/em&gt; specifically checked, it stays as an open caution rather than getting resolved by majority vote.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kappa, Now With Enough Data
&lt;/h2&gt;

&lt;p&gt;Part 2 skipped Cohen's Kappa — six outputs per model wasn't enough to trust a coefficient. With three judge pairs now, pooling both models gives 48 rated criterion-output pairs per comparison, which is enough to run a quadratic-weighted Kappa (appropriate here since these are ordinal 1–5 scores, not flat categories, so a 1-point miss shouldn't count the same as a 4-point miss) without over-reading it.&lt;/p&gt;

&lt;p&gt;It's worth being precise about what this number is actually answering, because it's a different question than the composite score above. The composite score asks &lt;em&gt;how good is this output&lt;/em&gt;. Kappa asks something narrower: &lt;em&gt;when two judges score the same thing, how often do they land in the same place, beyond what you'd expect from chance alone?&lt;/em&gt; A judge can agree with another judge constantly while both of them are still wrong — Kappa doesn't know or care which judge is closer to the truth. It only measures consistency.&lt;/p&gt;

&lt;p&gt;I ran &lt;code&gt;sklearn.metrics.cohen_kappa_score(weights='quadratic')&lt;/code&gt; on each of the three judge pairs, using the pooled 48-pair list each time, and labeled the results with the standard Landis &amp;amp; Koch bins (0.21–0.40 = "fair," 0.41–0.60 = "moderate") since that's the conventional way to read a Kappa value without just quoting a raw decimal:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Judge pair&lt;/th&gt;
&lt;th&gt;Weighted Kappa&lt;/th&gt;
&lt;th&gt;Agreement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemini vs. me&lt;/td&gt;
&lt;td&gt;0.365&lt;/td&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-LLM judge vs. me&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.494&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini vs. cross-LLM judge&lt;/td&gt;
&lt;td&gt;0.355&lt;/td&gt;
&lt;td&gt;Fair&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I expected the two LLM judges to agree with each other more than either agreed with me. That's not what happened. The cross-family judge — Claude scoring GPT's work, GPT scoring Claude's — landed closer to my own scoring than Gemini did. I don't have a confident explanation for why. Both judges ran on the exact same rubric and prompt, so it isn't a prompting difference — it may just be a trait of these particular models that shows up in how they weigh evidence, rather than anything specific to the cross-judging setup itself. I'd want to see this repeated on another dataset before reading much into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, Which One Wins?
&lt;/h2&gt;

&lt;p&gt;Combining all three judges with the weighting above, Claude Sonnet 4.5 comes out ahead on every artifact type, and within each model, the TF-IDF pipeline beat sentence embeddings every time:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Artifact&lt;/th&gt;
&lt;th&gt;Winning combination&lt;/th&gt;
&lt;th&gt;Composite score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate List&lt;/td&gt;
&lt;td&gt;Claude + TF-IDF&lt;/td&gt;
&lt;td&gt;55.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clusters&lt;/td&gt;
&lt;td&gt;Claude + TF-IDF&lt;/td&gt;
&lt;td&gt;77.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PM Insight Report&lt;/td&gt;
&lt;td&gt;Claude + TF-IDF&lt;/td&gt;
&lt;td&gt;70.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's a cleaner result than I expected — I'd assumed the pipelines would split by artifact type the way Part 1 hinted they might. Instead, TF-IDF's simplicity held up across the board, which says something about this being a keyword-dense backlog rather than a general verdict on embeddings versus TF-IDF.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnxw19khyklyj2fbjdy8n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnxw19khyklyj2fbjdy8n.png" alt="Cross-LLM judging" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before treating that as settled, though: the winning PM report is one of two Claude outputs GPT-5.5 flagged during cross-judging, citing specific figures — "8 confirmed duplicate pairs," "39 open TLS/SASL bugs," and "four of six clusters carry high alignment" — as not directly evidenced in the source text. Gemini didn't flag this one, and I hadn't ruled on it myself the way I did for GPT's Part 2 case.&lt;/p&gt;

&lt;p&gt;I went back and checked. "8 confirmed duplicate pairs" and "39 open TLS/SASL bugs" both hold up against the raw ticket data — legitimate, not invented. "Four of six clusters carry high alignment" I couldn't confirm either way with the same confidence. So the flag is partially resolved: two of the three claims were correct inference, not hallucination, and one stays an open question. Per the override rule above, only the resolved part gets cleared — the cluster-alignment claim still needs a closer look before this report goes anywhere near a manager.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Actually Do With This
&lt;/h2&gt;

&lt;p&gt;Present the Claude Sonnet 4.5 + TF-IDF versions of all three artifacts — duplicate list, clusters, PM report — as the primary recommendation. The duplicate-pair and bug-count figures in the PM report checked out against the raw data. The cluster-alignment claim didn't get the same confirmation, so I'd either soften that specific line before sharing the report or verify it against the cluster output directly first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Changes Going Forward
&lt;/h2&gt;

&lt;p&gt;The Kappa numbers reframe how I'll pick a judge next time. A neutral third-party judge sounds like the more rigorous choice on paper, but here it agreed with a human less than a judge with direct experience generating this exact kind of output did. If I'm running this again on a bigger backlog, I'd rather have Claude and GPT cross-check each other's work by default and treat a fully neutral judge as the tiebreaker, not the primary score.&lt;/p&gt;

&lt;p&gt;The other lesson: a hallucination flag that nobody has specifically resolved should never get averaged into a composite score as if it were just another data point. It's a to-do item, not a number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;I'd like to see whether the "cross-judge agrees with humans more" pattern holds on a second dataset before trusting it as a general rule rather than a one-off result. And the one remaining open item — the cluster-alignment claim in the winning PM report — is a small check, but worth closing out before this framework's first real recommendation goes to a manager.&lt;/p&gt;

&lt;p&gt;Full scoring tables for all three judges, both models, are on &lt;a href="https://github.com/wkgann/Jira-Backlog-Analyzer-part-2/tree/main/Jira%20Backlog%20Analyzer-part2/evaluations" rel="noopener noreferrer"&gt;Github&lt;/a&gt;.&lt;/p&gt;




</description>
      <category>ai</category>
      <category>python</category>
      <category>llm</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Judging the Judges: What Happens When You Ask an LLM to Grade Two Other LLMs</title>
      <dc:creator>K Gann</dc:creator>
      <pubDate>Sat, 18 Jul 2026 19:47:01 +0000</pubDate>
      <link>https://dev.to/jubilee_happy_a567008f769/judging-the-judges-what-happens-when-you-ask-an-llm-to-grade-two-other-llms-54oe</link>
      <guid>https://dev.to/jubilee_happy_a567008f769/judging-the-judges-what-happens-when-you-ask-an-llm-to-grade-two-other-llms-54oe</guid>
      <description>&lt;p&gt;&lt;em&gt;Part 2 of a series on auto-grooming Jira backlogs with ML and LLMs. Part 1 article: &lt;a href="https://dev.to/jubilee_happy_a567008f769/how-i-auto-groomed-500-jira-tickets-with-ml-and-llm-1j7k"&gt;How I Auto-Groomed 500 Jira Tickets with ML and LLM&lt;/a&gt; for the original TF-IDF + Gemini pipeline this builds on.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Question Part 1 Left Open
&lt;/h2&gt;

&lt;p&gt;Quick recap for anyone who missed Part 1: I pulled 500 real Apache ZooKeeper tickets from Jira's public API, cleaned the text, ran it through TF-IDF and K-Means to get five clusters, then flagged likely duplicates with cosine similarity. That's the part any ML pipeline can do. The interesting part was Phase II — feeding those raw clusters and duplicate pairs into Gemini 2.5 Flash, grounded with three selectively-weighted RAG files (project context, release notes, and a 2026 roadmap theme), so the output came back as named clusters with PM recommendations, age-aware duplicate calls, and an executive summary instead of a spreadsheet of keywords.&lt;/p&gt;

&lt;p&gt;But I built that pipeline with exactly one clustering method and one LLM, and never checked it against anything. If Gemini's cluster names sounded sharp, was that because Gemini did a good job, or because there was nothing else in the room to compare it to?&lt;/p&gt;

&lt;p&gt;That's the question this part tries to answer. Instead of one pipeline feeding one LLM, I ran two clustering pipelines through two LLMs, producing twelve outputs total, and had a third LLM score all of them against a rubric. Then I scored the same twelve outputs myself, by hand, and compared.&lt;/p&gt;

&lt;p&gt;I expected the comparison to tell me something about Claude versus GPT. It ended up telling me something more useful: about where an LLM judge can and can't be trusted.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Two Pipelines, Two LLMs
&lt;/h2&gt;

&lt;p&gt;One pipeline gives you no baseline. Adding a second clustering method and a second model lets me vary two things independently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Clustering approach&lt;/strong&gt; — sentence embeddings versus TF-IDF. Does semantic embedding actually help on a backlog this keyword-dense, or is TF-IDF's simplicity good enough?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The LLM itself&lt;/strong&gt; — Claude Sonnet 4.5 versus GPT-5.5, given identical inputs and identical RAG context.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More moving parts than Part 1, but it's the only way to make a claim like "Pipeline A clusters better" or "Claude reasons better about stale tickets" actually mean something, instead of being one anecdote dressed up as a finding.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It's Wired Together
&lt;/h2&gt;

&lt;p&gt;Five stages, and the diagram below the title maps them out, but here's the short version:&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6bf7lgvjrg3qvumj8zh8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6bf7lgvjrg3qvumj8zh8.png" alt="Jira Backlog Analyzer Architecture2-updated" width="799" height="447"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;1. Two pipelines, same input.&lt;/strong&gt; The same 500-ticket CSV runs through Pipeline A (sentence embeddings → K-Means → cosine similarity) and Pipeline B (TF-IDF → K-Means → cosine similarity), independently. Both pipelines picked K the same way — elbow method plus Silhouette Score, rather than reusing Part 1's fixed K=5 outright — so cluster count wasn't a hidden thumb on the scale for either method. Each pipeline spits out its own cluster set and duplicate list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Both LLMs see both pipelines.&lt;/strong&gt; This is the part that changes from Part 1. Claude Sonnet 4.5 enhances Pipeline A's output, then Pipeline B's. GPT-5.5 does the same — both pipelines, not just a matched one. Each of those four combinations produces the same three artifacts as Part 1: enhanced cluster names and descriptions, an enhanced duplicate list, and a PM Insight Report, grounded in the same three-tier RAG setup (project context, release notes, 2026 theme) and the same age categorization from Part 1.&lt;/p&gt;

&lt;p&gt;Two LLMs, two pipelines, three artifacts each — twelve outputs. Having each LLM touch both pipelines matters: it lets me separate "the LLM writes a better PM report" from "the embedding pipeline handed it better raw material," which a matched design (Claude only sees Pipeline A, GPT only sees Pipeline B) would have muddied.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A neutral judge scores all twelve.&lt;/strong&gt; Gemini 3.1 Pro — outside both the Claude and GPT families — scores every output against five criteria (scale 1-5, 5 the best and 1 the worst): &lt;em&gt;Strategic Alignment&lt;/em&gt;, &lt;em&gt;PM Recommendation Specificity&lt;/em&gt;, &lt;em&gt;Age-Aware Duplicate Reasoning&lt;/em&gt;, &lt;em&gt;Cluster Coherence&lt;/em&gt;, and &lt;em&gt;Data Synthesis &amp;amp; Quality&lt;/em&gt;. Not every criterion applies to every output type (a duplicate list doesn't get scored on cluster coherence, obviously), so each output is scored out of whatever denominator actually applies. The rubric also asks Gemini to flag suspected hallucinations directly, and to score only what's explicitly present rather than inferring intent — a rule that turns out to matter a lot, below.&lt;/p&gt;

&lt;p&gt;I'd considered having Claude and GPT also cross-judge each other's outputs, to control for a model favoring its own family. I skipped it here — with only twelve outputs, that's a different (and noisier) experiment than the one I actually wanted to run, which is simpler: does a judge that has no stake in either model agree with a human at all?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. I reviewed all twelve myself,&lt;/strong&gt; scoring against the same rubric, before comparing notes with the judge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. My review, not the judge's raw score, is what actually determined the final copy&lt;/strong&gt; for any downstream use of this pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Comparison Actually Showed
&lt;/h2&gt;

&lt;p&gt;The headline: I scored both models' work noticeably higher than Gemini did.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Gemini avg&lt;/th&gt;
&lt;th&gt;My avg&lt;/th&gt;
&lt;th&gt;Gap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4.5 outputs&lt;/td&gt;
&lt;td&gt;48.9%&lt;/td&gt;
&lt;td&gt;70.5%&lt;/td&gt;
&lt;td&gt;+21.6 pts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.5 outputs&lt;/td&gt;
&lt;td&gt;47.8%&lt;/td&gt;
&lt;td&gt;63.9%&lt;/td&gt;
&lt;td&gt;+16.1 pts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That gap wasn't spread evenly. Breaking it out by criterion:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Avg gap (mine − Gemini's)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Data Synthesis &amp;amp; Quality&lt;/td&gt;
&lt;td&gt;+1.17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Age-Aware Duplicate Reasoning&lt;/td&gt;
&lt;td&gt;+1.13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PM Rec. Specificity&lt;/td&gt;
&lt;td&gt;+0.88&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Strategic Alignment&lt;/td&gt;
&lt;td&gt;+0.58&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster Coherence&lt;/td&gt;
&lt;td&gt;+0.50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two biggest gaps — Data Synthesis and Age-Aware Reasoning — are also the two criteria that ask a model to &lt;em&gt;do&lt;/em&gt; something with the data rather than just cite it: run arithmetic across ticket counts, or reason about what a ticket's age actually implies. That's not a coincidence.&lt;/p&gt;

&lt;p&gt;The clearest example of this showed up in GPT-5.5's PM Insight Report from the embedding pipeline. Gemini flagged it as a hallucination, pointing to specific figures — a claim like "232 issues are Bugs and 147 are Improvements," a "63–121 issues per cluster" range — that it couldn't find written anywhere in the source documents. It scored Data Synthesis a 1.&lt;/p&gt;

&lt;p&gt;I gave that same output a 4 on Data Synthesis. The numbers weren't invented — GPT-5.5 had counted and aggregated them from the actual ticket data it was working with. It was correct analysis that happened to produce specific-sounding figures, and because those figures weren't quoted verbatim anywhere in the RAG files, the judge read "not explicitly sourced" as "fabricated."&lt;/p&gt;

&lt;p&gt;That's the finding I'd flag to anyone building something similar: an LLM judge, when it can't trace a number back to a quote, seems to default to calling it a hallucination — even when the number is a legitimate computation over data it actually had access to. From the judge's seat, a correct inference and a real hallucination can look identical unless it's willing to redo the math itself.&lt;/p&gt;

&lt;p&gt;Worth being honest about the exception, too: on GPT-5.5's TF-IDF PM Report, the pattern flips — Gemini scored Strategic Alignment and Data Synthesis &lt;em&gt;higher&lt;/em&gt; than I did. So this isn't "the judge is stingy" across the board. It's specifically the inference-versus-hallucination boundary where the disagreement lives, and it doesn't run one direction consistently enough to just apply a blanket correction factor.&lt;/p&gt;

&lt;p&gt;(Full per-criterion scores for both models are in the &lt;a href="https://github.com/wkgann/Jira-Backlog-Analyzer-part-2/tree/main/Jira%20Backlog%20Analyzer-part2/evaluations" rel="noopener noreferrer"&gt;Github evaluations folder&lt;/a&gt; if you want the raw numbers.)&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on the Judge Prompt
&lt;/h2&gt;

&lt;p&gt;I won't reproduce the full judge prompt here — it's a fairly long, rigid rubric document, and it's more useful sitting in the repo where you can actually copy and adapt it than pasted into an article. The short version: it told Gemini to act as an impartial judge, gave it explicit 5/3/1 scoring anchors for each of the five criteria, specified which criteria applied to which output type, and included one rule that turned out to matter more than I expected — &lt;em&gt;score only what's explicitly present, don't infer intent.&lt;/em&gt; That instruction is almost certainly why the judge treated GPT-5.5's derived figures as unsupported rather than derived: it was doing exactly what it was told, just a little too literally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delta, Not Kappa
&lt;/h2&gt;

&lt;p&gt;Cohen's Kappa is the standard tool for this kind of comparison, and I looked at it first. But Kappa needs enough trials to be stable, and with six outputs per model on a 1–5 scale, it would've produced a number I didn't trust. A simple delta — my score minus Gemini's, per criterion — was less elegant but told me exactly where the disagreement was concentrated, which is what actually mattered here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Human Still Earns Their Keep
&lt;/h2&gt;

&lt;p&gt;The GPT-5.5 hallucination flag above is the whole argument for keeping a person in the loop. It wasn't a vague "I felt more generous" adjustment — it was a specific, checkable disagreement about whether a number was invented or computed. A rubric and a capable judge get you most of the way there, but not all the way; they're not yet reliable at telling "unsupported" from "correct math the judge didn't bother to verify."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently Next Time
&lt;/h2&gt;

&lt;p&gt;A few honest takeaways, in no particular order:&lt;/p&gt;

&lt;p&gt;The judge is a triage tool, not a verdict. Gemini's scores tracked mine reasonably well most of the time — good enough to flag likely problems fast. Not good enough to be the last word, especially on hallucination calls.&lt;/p&gt;

&lt;p&gt;The disagreement wasn't random — it clustered on the two criteria that require inference rather than lookup. That's a fixable design problem: give the judge a way to re-derive a number before flagging it, rather than pattern-matching against the source text alone.&lt;/p&gt;

&lt;p&gt;Cross-pipeline coverage was worth the extra setup. Letting each LLM touch both pipelines is the only reason I can say anything about the LLM and the pipeline separately, instead of one tangled result.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I want to try giving the judge model an actual calculator or code tool, so it can check a number before calling it fabricated. I'd also like to run this on a bigger, messier backlog — enough outputs to make Kappa worth revisiting, and enough variety to stress-test the embedding pipeline somewhere TF-IDF might actually struggle. And a lighter-weight version of the human check seems doable: only spot-check the outputs where the judge's hallucination flag rests on a specific number, instead of re-scoring everything by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Part 1 showed the pipeline could work. Part 2 was about whether I could trust an LLM to tell me &lt;em&gt;how well&lt;/em&gt; it worked. Mostly, yes — Gemini's scores were directionally useful and cheap to generate at scale. But it consistently mistook correct inference for invention, which is exactly the kind of quiet, high-stakes mistake a human is there to catch. For anything a PM is actually going to read and act on, that check isn't optional.&lt;/p&gt;

&lt;p&gt;Code, RAG files, and full scoring tables for both models are on &lt;a href="https://github.com/wkgann/Jira-Backlog-Analyzer-part-2" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>datascience</category>
    </item>
    <item>
      <title>How I Auto-Groomed 500 Jira Tickets with ML and LLM</title>
      <dc:creator>K Gann</dc:creator>
      <pubDate>Wed, 08 Jul 2026 02:42:24 +0000</pubDate>
      <link>https://dev.to/jubilee_happy_a567008f769/how-i-auto-groomed-500-jira-tickets-with-ml-and-llm-1j7k</link>
      <guid>https://dev.to/jubilee_happy_a567008f769/how-i-auto-groomed-500-jira-tickets-with-ml-and-llm-1j7k</guid>
      <description>&lt;p&gt;&lt;em&gt;A practical, end-to-end walkthrough of applying NLP, machine learning, and Gemini to automatically cluster issues, surface duplicates, and generate an executive summary — no manual grooming required.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Doesn't Jira Just Do This Already?
&lt;/h2&gt;

&lt;p&gt;If you've inherited a Jira project with hundreds of open tickets, your first instinct is probably: can't the tool just handle this? It's a fair question. Atlassian has poured real investment into AI over the last couple of years, and what they've built — Atlassian Intelligence, and more recently the Rovo platform — genuinely holds up for a lot of tasks.&lt;/p&gt;

&lt;p&gt;Atlassian Intelligence can summarize a long comment thread in seconds, and its JQL assistance makes writing complex queries far less painful than it used to be. Rovo goes a step further, with task-specific agents built to automate things like triage and backlog grooming.&lt;/p&gt;

&lt;p&gt;What neither tool does, though, is cluster hundreds of tickets into thematic groups or systematically flag duplicates across an entire backlog. Rovo is built around individual ticket interactions: summarizing one issue, routing one request. There's no native way to run a similarity computation across 500 issues at once, or to partition a backlog into clusters a PM can actually act on.&lt;/p&gt;

&lt;p&gt;That's the gap this project sits in. What follows is a batch analytical pipeline, which is a different mode of operation entirely from anything Rovo or Atlassian Intelligence currently offers.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the Research Says
&lt;/h2&gt;

&lt;p&gt;The closest published work I found is a 2025 arXiv paper, &lt;em&gt;"GenAI-Enabled Backlog Grooming in Agile Software Projects,"&lt;/em&gt; which built a Jira plugin around vector embeddings, cosine similarity, and GPT-4o to propose merges and deletions. It reported 100% precision and a 45% reduction in time-to-completion. This pipeline follows a similar core approach, but diverges in two respects: it uses classical TF-IDF instead of embeddings (no GPU needed), and it adds selective RAG grounding with role-differentiated context files, which the academic study didn't touch.&lt;/p&gt;

&lt;p&gt;Earlier research has also shown that ML can predict issue link types across large Jira repositories reasonably well, with duplicate detection being one of the more studied categories. Separately, TF-IDF combined with K-Means has proven effective at filtering large text corpora into focal topics, a pattern that maps cleanly onto backlog clustering.&lt;/p&gt;

&lt;p&gt;Where this article tries to add something is the integration layer: stitching these techniques together with a selectively RAG-grounded LLM into one pipeline that ends in a PM-ready executive summary, rather than a set of disconnected outputs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dataset
&lt;/h2&gt;

&lt;p&gt;The sample dataset is 500 publicly available Apache ZooKeeper Jira issues. At a glance, the backlog skews toward core system work: stability, reliability, security, developer experience, and operational scalability all show up repeatedly, and those categories line up well with typical FY26-style strategic themes around hardening and reliability.&lt;/p&gt;

&lt;p&gt;The more interesting finding is what's &lt;em&gt;not&lt;/em&gt; healthy about the backlog. More than half the tickets are stale or very stale by the age categories defined later in this pipeline, and there's a heavy concentration of bug fixes and major-priority issues sitting in that aged pile. In other words, this isn't a tidy, well-groomed dataset picked to make the pipeline look good — it's a fairly realistic, somewhat neglected backlog, which is exactly the kind of mess this project is meant to help with.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Pipeline at a Glance
&lt;/h2&gt;

&lt;p&gt;The pipeline runs in two phases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phase I:&lt;/strong&gt; NLP preprocessing → TF-IDF vectorization → K-Means clustering → cosine similarity duplicate detection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase II:&lt;/strong&gt; Selective RAG grounding + Gemini 2.5 Flash → enriched cluster names, age-aware duplicate insights, and an executive summary&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvyd9zya52azxm2f5p040.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvyd9zya52azxm2f5p040.png" alt="Jira Backlog Analyzer workflow" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase I: Clustering and Duplicate Detection
&lt;/h2&gt;

&lt;p&gt;We pull 500 issues from Apache's public Jira REST API, including summary, description, status, priority, labels, and comments. One detail worth flagging early: we store &lt;code&gt;updated&lt;/code&gt; as a timezone-aware pandas Timestamp, since it feeds directly into the age categorization in Phase II.&lt;/p&gt;

&lt;p&gt;Text preprocessing combines each ticket's &lt;code&gt;summary&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; into a single &lt;code&gt;full_text&lt;/code&gt; field, then runs it through this cleanup pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;nltk.corpus&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;stopwords&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;nltk.tokenize&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;word_tokenize&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clean_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http\S+|www\S+|https\S+&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MULTILINE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[^a-zA-Z\s]&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;word_tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;stop_words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stopwords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;words&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;english&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stop_words&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;df_jira_issues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;processed_text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df_jira_issues&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;full_text&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clean_text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, standard TF-IDF vectorization produces a (500 × 6,842) matrix, and K-Means with &lt;code&gt;k-means++&lt;/code&gt; initialization sorts issues into 5 clusters. We then compute pairwise cosine similarity across all 500 issues and flag anything above a 0.8 threshold as a potential duplicate.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why TF-IDF instead of embeddings? ZooKeeper tickets tend to be keyword-dense: error names, component labels, CVE IDs. TF-IDF is fast, interpretable, and doesn't need a GPU. For a backlog with more conversational, prose-heavy tickets, &lt;code&gt;sentence-transformers&lt;/code&gt; would probably be the better call.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The cluster sizes came out lopsided: Cluster 3 alone held 286 issues, while Cluster 0 had just 42. That's already a signal on its own — client and server errors dominate this backlog. Cosine similarity turned up 12 potential duplicate pairs. That's where Phase I stops; the raw output here is useful to an engineer but not particularly readable to a PM. Phase II is where that gets fixed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Phase II: Selective RAG + Gemini
&lt;/h2&gt;

&lt;p&gt;The main design decision in this phase is that the RAG files aren't sent uniformly to every prompt — they're weighted by task. When all three context files carry equal weight, the LLM tends to hedge between historical patterns and forward-looking priorities, and the output gets mushy. Explicit weighting, using prompt instructions like &lt;code&gt;"PRIORITIZE for strategic alignment,"&lt;/code&gt; produces noticeably sharper results.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task&lt;/th&gt;
&lt;th&gt;Primary RAG&lt;/th&gt;
&lt;th&gt;Background&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cluster naming &amp;amp; PM recommendations&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2026_theme.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;project_context.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate insights&lt;/td&gt;
&lt;td&gt;&lt;code&gt;release_notes.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;project_context.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executive summary&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;2026_theme.md&lt;/code&gt; &amp;amp; &lt;code&gt;release_notes.md&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;project_context.md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each file plays a different role. &lt;code&gt;project_context.md&lt;/code&gt; is the strategic frame — project goals, workflows, what success looks like — and it shows up in every prompt as background context. &lt;code&gt;release_notes.md&lt;/code&gt; holds the historical record of bug fixes and shipped features, and it's the primary driver for duplicate detection and executive summary, since it can tell the LLM whether a reported bug was already resolved in a past release. &lt;code&gt;2026_theme.md&lt;/code&gt; carries the quarterly priorities (for example, FY26Q1: "Core System Hardening") and drives both cluster naming and the executive summary, so the output stays aligned with where the team is actually headed rather than where the backlog happens to be.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enriching Cluster Names
&lt;/h3&gt;

&lt;p&gt;The cluster prompt asks for a four-field JSON output — name, theme alignment, description, and PM recommendations — grounded primarily in the 2026 themes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_google_genai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ChatGoogleGenerativeAI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langchain_core.prompts&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PromptTemplate&lt;/span&gt;

&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ChatGoogleGenerativeAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models/gemini-2.5-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                              &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;cluster_enhancement_prompt_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PromptTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;input_variables&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;project_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;release_notes_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;theme_2026_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cluster_keywords&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cluster_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are an expert in Jira issue analysis and project management.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Project Context (use for overall understanding):&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;{project_context}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Release Notes (use for historical context):&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;{release_notes_context}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026 Themes (PRIORITIZE for theme alignment and recommendations):&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;{theme_2026_context}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Top keywords for Cluster {cluster_id}: {cluster_keywords}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Return JSON with keys: name, theme_alignment, description, pm_recommendations.&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Raw keywords → LLM-enhanced names:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cluster&lt;/th&gt;
&lt;th&gt;Raw Keywords&lt;/th&gt;
&lt;th&gt;LLM-Enhanced Name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;support, auth, apis, admin, prometheus&lt;/td&gt;
&lt;td&gt;Admin Server APIs, Authentication, and Prometheus Metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;cve, netty, jetty, vulnerability&lt;/td&gt;
&lt;td&gt;Security Vulnerability Fixes and Dependency Upgrades&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;client, session, connection, ssl&lt;/td&gt;
&lt;td&gt;ZooKeeper Client Connectivity, Sessions, and Security&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;error, exception, npe, zookeeper&lt;/td&gt;
&lt;td&gt;ZooKeeper Client/Server Errors and Stability Issues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;logging, dependency, core&lt;/td&gt;
&lt;td&gt;Core System Dependency and Logging Updates&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cluster 1's name lands on "Fixes and Dependency Upgrades" because &lt;code&gt;2026_theme.md&lt;/code&gt; explicitly calls out dependency remediation as a Core System Hardening priority — the LLM is reading the roadmap here, not just the keywords. Each cluster also comes back with PM-level output: how it connects to FY2026 themes, what the issues represent in practical engineering terms, and what the team should prioritize next quarter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Age-Aware Duplicate Detection
&lt;/h3&gt;

&lt;p&gt;Before sending duplicate pairs to the LLM, we compute a ticket age category from each issue's &lt;code&gt;updated&lt;/code&gt; timestamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;categorize_age&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_date&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isnull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;updated_date&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;age_days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;updated_date&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;age_days&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Active (&amp;lt; 30 days)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;age_days&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Recent (30-60 days)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;age_days&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Aging (&amp;gt; 60 days)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;age_days&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stale (&amp;gt; 180 days)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Very Stale (&amp;gt; 365 days)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;current_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timestamp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Age gets passed into the duplicate prompt alongside summaries, descriptions, and similarity scores, with &lt;code&gt;release_notes.md&lt;/code&gt; set as the primary context. Three real examples from the pipeline output show how much age category alone changes the recommendation, even when the similarity scores are close.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example 1 — True Duplicate, Stale: ZOOKEEPER-4948 &amp;amp; ZOOKEEPER-4947 (Similarity: 1.0000)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both tickets are Stale (&amp;gt; 180 days) and describe the exact same bug, almost word-for-word: ZooKeeper's &lt;code&gt;SendThread&lt;/code&gt; and &lt;code&gt;EventThread&lt;/code&gt; fail to terminate after &lt;code&gt;close()&lt;/code&gt; is called during a SASL &lt;code&gt;AuthFailed&lt;/code&gt; state. Because these are stale rather than freshly filed, the LLM adds a verification step before recommending any action:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Both issues are Stale (&amp;gt; 180 days) and relate to a SASL authentication bug introduced in Release 3.8.0. First verify whether a subsequent patch already resolved this. If fixed, close ZOOKEEPER-4947 as a duplicate and close ZOOKEEPER-4948 as Done, referencing the fix. If still open, merge into ZOOKEEPER-4948 and reprioritize as high-severity under FY26Q1 Core System Hardening."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An Active pair with the same similarity score would likely skip that verification caveat entirely. Age is what changes the action here, not the similarity score.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example 2 — Same Root Cause, Different Components: ZOOKEEPER-4665 &amp;amp; ZOOKEEPER-4664 (Similarity: 0.8624)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both are Very Stale (&amp;gt; 365 days) and report OWASP failures tied to vulnerable third-party dependencies, but in different components (&lt;code&gt;zooinspector&lt;/code&gt; vs. &lt;code&gt;zookeeper-contrib-rest&lt;/code&gt;) with entirely different CVE lists. The LLM correctly avoids treating these as exact duplicates:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"These are not identical duplicates but represent the same systemic security problem across two components. Both are Very Stale (&amp;gt; 365 days) — some specific CVEs may have been implicitly resolved through dependency updates in 3.6.3 or 3.8.1. Consolidate under a parent epic 'Address Third-Party Security Vulnerabilities' aligned with FY26Q1 Core System Hardening, and verify current CVE status before prioritizing each sub-task."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Very Stale category surfaces something a plain similarity check would miss entirely: the specific CVEs involved may not even exist anymore. So the recommendation is to verify first, not to close blindly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example 3 — Scope Diverged, Keep Both: ZOOKEEPER-4634 &amp;amp; ZOOKEEPER-4633 (Similarity: 0.9412)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both are Very Stale (&amp;gt; 365 days) and request authentication support for the same Admin Server APIs, referencing an identical design document. But they diverge on implementation — ZOOKEEPER-4634 targets x509 auth, ZOOKEEPER-4633 targets digest auth. The LLM picks up on the shared origin but treats them as having grown into separate workstreams:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"These are not duplicates — they are two implementation paths for a single feature. Both are Very Stale (&amp;gt; 365 days), and the Admin Server APIs they target were only introduced in Release 3.7.0 (February 2026), likely after these tickets were created. Verify whether either method has already been implemented. If not, consolidate under a parent epic 'Admin Server API Authentication Strategy' and determine whether both authentication types are still required or if the 3.8.0 SASL implementation has superseded them."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the case where age and release-note context together catch something neither could catch alone: the tickets were written before the feature they reference even existed, which makes their original requirements possibly obsolete.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;What age adds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ZOOKEEPER-4948/4947&lt;/td&gt;
&lt;td&gt;True duplicate&lt;/td&gt;
&lt;td&gt;Triggers "verify fix before closing" on stale tickets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZOOKEEPER-4665/4664&lt;/td&gt;
&lt;td&gt;Same root cause, different components&lt;/td&gt;
&lt;td&gt;Flags possible implicit resolution for very stale CVEs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ZOOKEEPER-4634/4633&lt;/td&gt;
&lt;td&gt;Scope diverged&lt;/td&gt;
&lt;td&gt;Exposes requirements written before the feature existed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  The Output: Executive Summary for PMs
&lt;/h2&gt;

&lt;p&gt;The executive summary prompt leverages RAG by incorporating &lt;code&gt;release_notes.md&lt;/code&gt;, &lt;code&gt;2026_theme.md&lt;/code&gt;, and &lt;code&gt;program_context.md&lt;/code&gt;. Rather than merely summarizing historical Jira activity, it combines release history, organizational priorities, program context, cluster theme alignments, and duplicate insights to generate a forward-looking 3–5 paragraph executive summary for project managers. The resulting report is saved as &lt;code&gt;executive_summary.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It covers a methodology overview, the 5 named clusters with FY2026 theme alignment and PM recommendations, all 12 duplicate pairs with age categories and specific actions, and a closing tied back to quarterly priorities. It's the one artifact meant to bridge engineering analysis and program management without anyone having to open a code cell.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Tradeoffs and Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Selective RAG weighting turned out to be the highest-leverage design choice by a wide margin. Moving from uniform context injection to task-specific prioritization made cluster names more strategically forward-looking and duplicate recommendations more historically grounded, without adding much complexity to the pipeline itself.&lt;/p&gt;

&lt;p&gt;Ticket age is a cheap addition that punches above its weight. A Very Stale duplicate pair can often be closed with confidence; an Active pair usually needs more digging first. One extra function call adds a dimension that meaningfully shifts the LLM's recommendations.&lt;/p&gt;

&lt;p&gt;Cluster imbalance is worth treating as a finding rather than a flaw. 286 issues in Cluster 3 against 42 in Cluster 0 tells you something real about where engineering effort is concentrated. For a production version of this pipeline, running silhouette scoring to find a better &lt;code&gt;k&lt;/code&gt; would be a reasonable next step — 5 may simply be too few clusters for a dataset like this one.&lt;/p&gt;

&lt;p&gt;The 60-second API delays add up more than expected. With 12 duplicate pairs and a rate-limit delay between calls, Phase II takes roughly 12 minutes end-to-end. Batching or async calls would speed this up considerably at scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-triage new tickets:&lt;/strong&gt; apply the trained K-Means model to incoming issues at creation time for automatic cluster routing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold tuning dashboard:&lt;/strong&gt; a Streamlit tool letting PMs adjust the 0.8 similarity threshold and preview duplicates in real time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic RAG updates:&lt;/strong&gt; automate refresh of &lt;code&gt;release_notes.md&lt;/code&gt; and &lt;code&gt;2026_theme.md&lt;/code&gt; from Confluence via API, so context stays current without manual upkeep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Richer embeddings:&lt;/strong&gt; replace TF-IDF with &lt;code&gt;sentence-transformers&lt;/code&gt; for semantically richer clustering on backlogs with more conversational language.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;What started as a 500-ticket mess turned into a structured, LLM-annotated backlog report a PM can actually act on, all from a single Jupyter notebook.&lt;/p&gt;

&lt;p&gt;The core idea underneath all of it: classical ML handles the heavy lifting (TF-IDF, K-Means, cosine similarity — fast, and no GPU needed), while a selectively RAG-grounded LLM handles the last mile, turning keyword lists into language that's strategically aligned, age-aware, and historically informed enough for both engineers and program managers to use.&lt;/p&gt;

&lt;p&gt;The selective RAG weighting is really the detail that makes the whole thing work. Routing the right knowledge source to the right task — release notes for historical duplicate reasoning, 2026 themes for forward-looking cluster strategy — is what makes the output feel genuinely actionable instead of generically descriptive.&lt;/p&gt;

&lt;p&gt;Full code is available on &lt;a href="https://github.com/wkgann/jira-backlog-analyzer" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;em&gt;A clustering approach for topic filtering within systematic literature reviews&lt;/em&gt; — NCBI, 2020. &lt;a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7078380/" rel="noopener noreferrer"&gt;https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7078380/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Apache ZooKeeper Public Jira Issue Tracker&lt;/em&gt; — Apache Software Foundation. &lt;a href="https://issues.apache.org/jira/projects/ZOOKEEPER/issues" rel="noopener noreferrer"&gt;https://issues.apache.org/jira/projects/ZOOKEEPER/issues&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Atlassian Rovo — Feature overview. &lt;a href="https://www.atlassian.com/software/rovo/features" rel="noopener noreferrer"&gt;https://www.atlassian.com/software/rovo/features&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Automated Detection of Typed Links in Issue Trackers&lt;/em&gt; — arXiv, 2022. &lt;a href="https://arxiv.org/pdf/2206.07182" rel="noopener noreferrer"&gt;https://arxiv.org/pdf/2206.07182&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;GenAI-Enabled Backlog Grooming in Agile Software Projects&lt;/em&gt; — arXiv, 2025. &lt;a href="https://arxiv.org/pdf/2507.10753" rel="noopener noreferrer"&gt;https://arxiv.org/pdf/2507.10753&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Rovo by Atlassian: What the AI can and can't do&lt;/em&gt; — eesel AI blog. &lt;a href="https://www.eesel.ai/blog/rovo" rel="noopener noreferrer"&gt;https://www.eesel.ai/blog/rovo&lt;/a&gt;
ineering effort is concentrated. For a production pipeline, run silhouette scoring to find a better &lt;code&gt;k&lt;/code&gt; — 5 may be too few for this dataset.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>python</category>
    </item>
  </channel>
</rss>
