TriageIQ
An ML issue-triage assistant for busy open-source maintainers, caught a target-leakage bug that had inflated its own error estimate 8×, then found two more leaks nobody was looking for.
Last updated 2026-10-13 · 5 min read
Verified source
Component classifier top-3 accuracy (vscode)
commit 4614dd1 · measured 2026-08-05
Component classifier top-3 accuracy (vscode), p50 4.9msVerified source
Component classifier top-3 accuracy (kubernetes)
commit 4614dd1 · measured 2026-08-05
Component classifier top-3 accuracy (kubernetes)Verified source
Resolution-time CQR interval coverage (kubernetes)
commit 4614dd1 · measured 2026-08-05
Resolution-time CQR interval coverage (kubernetes)
The problem
On a busy open-source repository, a maintainer has to work out, for every new issue that lands: which subsystem does this touch, has someone already reported this, how long will this realistically take to fix, and what should happen next. Doing that by hand doesn't scale past a small team.
TriageIQ automates the first pass. Give it an issue's title and body and it returns a structured triage decision, predicted component, similar past issues, an expected resolution-time range, a priority, and next steps, in under 4 seconds. It's trained on roughly 20,000 real issues from two very different repos, microsoft/vscode and kubernetes/kubernetes, so its numbers reflect what real, messy open-source data looks like rather than a clean lab set.
How it works
Rather than asking one big model to do everything, TriageIQ runs four independent ML and AI systems in sequence, each doing one job well: (1) a TF-IDF + logistic regression classifier picks the likely component in about 5ms, TF-IDF turns text into weighted word-frequency vectors, a cheap and fast representation; (2) BGE sentence embeddings plus a FAISS index retrieve similar past issues in about 27ms; (3) a LightGBM quantile-regression model estimates a resolution-time range with conformal calibration (a statistical technique that gives the range an honest, guaranteed coverage probability) in about 4ms; (4) a Groq-hosted LLM takes all of that structured output and writes it up as a readable plan, in about 3 seconds.
The LLM's job is deliberately narrow: it only writes narrative on top of facts the first three stages already computed, it's not asked to invent the component, the similar issues, or the time estimate itself. A grounding checker then verifies that the LLM's write-up didn't quietly claim something the earlier stages never actually produced.
Architecture
Four purpose-built models in a fixed pipeline, followed by a verifier that checks the one component (the LLM) capable of making things up.
New issue
POST /triage { repo, title, body }
Stage 1, Component classifier
TF-IDF (1–2gram) + per-repo logistic regression, 28–35 classes, ~5ms
Stage 2, Similar-issue retrieval
BGE-base-en-v1.5 embeddings + FAISS cosine search, top-5, ~27ms
Stage 3, Resolution-time estimate
LightGBM quantile regression (79 features) + Conformal Quantile Regression, ~4ms
Stage 4, LLM synthesis
Groq llama-3.1-8b-instant, 4-shot, JSON-schema retry + fallback, ~3s
Grounding verifier
checks every LLM claim against what stages 1–3 actually produced
TriagePlan
component + similar issues + resolution-time interval + priority + next steps
FastAPI on Cloud Run, Prometheus metrics, CI/CD via Workload Identity Federation, models stored in GCS.
Key decisions, and why
TF-IDF + logistic regression over a transformer, for the classifier
A three-way bake-off tested TF-IDF against DistilBERT and an LLM, before the later multi-label supervision fix (ADR-0036) that raised the shipped model's own headline accuracy. DistilBERT beat TF-IDF on vscode by only 1.2 percentage points of macro-F1, nowhere near the 11-point margin needed to justify 20x the latency, and actually lost on kubernetes by 5.1 points at 35x the latency. At the ~1,500–2,300 training examples per repo this system has, TF-IDF's simpler inductive bias (its built-in assumptions about how text maps to labels) generalizes better than a transformer does with that little data.
Conformal Quantile Regression, not the model's raw quantile outputs
LightGBM can output a Q10–Q90 prediction interval directly, but its raw coverage (how often the true answer actually fell inside the stated range) was unreliable, 74.4% on kubernetes and just 38.2% on vscode, against a nominal 80% target. Conformal Quantile Regression is a distribution-free, post-hoc calibration step that recalibrates those raw quantiles and comes with a guaranteed marginal coverage rate, which the raw model output simply didn't have.
Split the data by created_at, not closed_at
The original train/test split used each issue's closed_at date, which quietly guaranteed that every issue still open at the cutoff date landed in the test set, meaning test was structurally full of slow, unresolved issues while train was full of fast, already-closed ones. Re-splitting by created_at (when the issue was filed, not when it was closed) dropped mean absolute error from 693 days to 87 days, an 8x improvement that came entirely from fixing a methodology bug, not from improving the model.
Report top-3 accuracy as the headline classifier metric
The product surfaces three candidate components to the maintainer, not one, so top-1 accuracy alone understates how useful the classifier actually is in the workflow it's built for. Reporting top-3 instead of top-1 is a 13–27 percentage-point difference, using the metric that matches what the product actually shows is more honest than picking whichever number looks best.
Gate fabrication on Kubernetes, report it on vscode
Fabrication, a synthesized plan citing a component or similar issue the retriever and classifier never supplied, is a hard, blocking CI gate on the Kubernetes eval set: the fabrication rate may not exceed the committed baseline, currently 0 of 53 plans, and a separate grounding ratchet tolerates at most 1 of 53 ungrounded plans. The check replays a recorded cassette, so an unrelated change cannot trip it. The vscode set (11 issues) is deliberately report-only, because one plan moves its rate by about nine points, too noisy to block merges without false alarms. Gating only where the sample can support it keeps the gate trustworthy.
Results, the honest numbers
Verified source
Component classifier top-3 accuracy (vscode)
commit 4614dd1 · measured 2026-08-05
- Component classifier top-3 accuracy (vscode), p50 4.9ms · top-1 76.5%, multi-label supervision fix, ADR-0036, shipped model as of 2026-07-24
Verified source
Component classifier top-3 accuracy (kubernetes)
commit 4614dd1 · measured 2026-08-05
- Component classifier top-3 accuracy (kubernetes) · top-1 60.5%, same shipped model, the harder of the two eval corpora
Verified source
Resolution-time CQR interval coverage (kubernetes)
commit 4614dd1 · measured 2026-08-05
- Resolution-time CQR interval coverage (kubernetes) · vs. an 80% nominal target, post-calibration, and what's actually loaded in the live-serving artifact; see the CQR design decision below for how much worse the raw, uncalibrated model's coverage was before this
Verified source
Resolution-time CQR interval coverage (vscode)
commit 4614dd1 · measured 2026-08-05
- Resolution-time CQR interval coverage (vscode) · against the same 80% nominal target, same calibration path
Verified source
Similar-issue retrieval Recall@5, related task, clean eval subset (kubernetes)
commit 77bebcc · measured 2026-08-11
- Similar-issue retrieval, Recall@5 (kubernetes, related task, clean eval subset) · the ADR-0035 harness fix (title-only queries against a title+body production index) produced 18.0%, later superseded by ADR-0040's truncation/query-instruction fixes at 24.67% measured over the full 150-pair eval population, but a 2026-08-11 audit found ~56% of that population is structurally invalid as a content-similarity test (umbrella issues, causal-only references). 39.39% [27.3, 51.5] (n=66) is the hand-verified, outcome-blind clean subset and is now the project's own headline number for this metric. Three zero-training retrieval-improvement techniques (BM25 fusion, cross-encoder reranking, a stronger embedder) still failed to clear the bar earlier in this chain; reranking made it worse at 190–330x the latency
Verified source
Similar-issue retrieval Recall@5, duplicate task (vscode)
commit 77bebcc · measured 2026-08-11
- Similar-issue retrieval, Recall@5 (vscode, duplicate task) · vscode's original number was retired after ADR-0032 found the gold pairs were ~80% noise (dominated by a title_sim channel measured at only ~20% precision); ADR-0035's harness fix then corrected it to 50.5%, and ADR-0040's corpus-truncation fix (Lever 1, shipped for both repos) raised it again to 53.50% [46.5, 60.5]
Cited in provenance.md
`README.md:91-98`
verified against source 2026-10-10
- Resolution-time predictor MAE vs. naive baseline · vscode: 6.02d vs. 3.53d naive, 70.5% worse; served as-is with a transparency badge
Cited in provenance.md
`triage-iq/docs/architecture/adr/0018-gold-set-train-contamination.md` — root causes lines 22–55, blast radius lines 61–69, fix lines 98–108
verified against source 2026-10-10
- LLM fabrication rate (grounding-verified) · Committed baseline at triage-iq 877af11 (set 2026-10-08, cd2934f). k8s was 1.9% (1/53) on the 2026-09-23 recording; 9.1% vscode was an older audit.
Caught a target-leakage bug that inflated error 8×, then found two more leaks nobody was looking for
An early version of the resolution-time predictor reported a mean absolute error of roughly 682 days, flagged immediately as suspicious rather than accepted at face value. The investigation found the root cause was the train/test split itself: it divided issues by closed_at, the date an issue was closed. That guarantees, by construction, that any issue still open at the cutoff date lands in the test set and every already-resolved issue lands in train. The train set's median resolution time came out to 1.0 day; the test set's came out to 677 days. The model wasn't failing to learn, it had never seen anything like its test set during training.
Re-splitting by created_at (when an issue was filed, which doesn't leak information about how long it took to close) collapsed the MAE from 693 days down to 87 days. That 8x jump was entirely a bug fix, not a modeling improvement, and the team documented it as exactly that rather than presenting it as a win.
The same investigation surfaced a second, subtler leak: the model's top feature by importance, has_priority (correlation 0.595), turned out to be a label that gets applied during the triage process itself, meaning it wasn't actually available at the moment a real prediction would need to be made. Separately, a routine disjointness-guard build (checking that train and eval data never overlap) turned up a third finding: the judge-eval gold set had been silently contaminated with training data for about three months, affecting 54 of 60 evaluation cases. The team disclosed the contamination at the membership level honestly, rather than trying to compute and publish a "corrected" score for numbers they could no longer fully trust.
Try it
The same technique as stage 1, running live in your browser on a small sample of real GitHub issues:
The second question, in the space it is answered in
700 real kubernetes/kubernetes issues, embedded by the model this project actually ships and laid out in three dimensions. Pick one of the six and you see the five issues the retriever handed back for it, in its order, with the one the gold set calls related marked wherever it came in.
What the picture is, and what it is not. Every rank here was worked out in the model's own 768 dimensions before anything was drawn. What you see is a t-SNE layout, which is a flattening chosen to look tidy, so two dots sitting together are not necessarily each other's nearest neighbours and a highlighted issue can land anywhere on screen. That is why the highlight follows issue numbers rather than distance. The corpus is also a seed-fixed sample of 700 of the 6,612 issues in the gold set, so these ranks describe this sample, and the recall figures above are the ones measured over everything.
#12290 pkg/client separation from godep, or using godep save -r ./..., or ?
- 1#12272 Cannot get *client.Config to load properly - s/mergo.Merge/mergo.MergeWithOverwrite/ update needed0.764the one the gold set calls related
- 2#25823 release-1.2 build package dependence error0.753
- 3#18186 Move unversioned#testClient to another package0.748
- 4#25867 Refactor the deployment controller0.729
- 5#19672 godep restore broken by github.com/camlistore/camlistore/pkg/errorutil0.717
The issue the gold set marks as related, #12272, came back at rank 1.
#21633 https-nginx example can clarify secrets/cert generation step
- 1#21631 Assumptions in "Securing the Service" no longer hold0.856the one the gold set calls related
- 2#2363 make-cert.sh fails if /srv/kubernetes does not already exist0.754
- 3#20176 kubectl create secret tls0.750
- 4#8211 insecure-skip-tls-verify doesn't work for 'exec'0.699
- 5#16586 Can't mount secret volumes0.695
The issue the gold set marks as related, #21631, came back at rank 1.
#21799 Make ubernetes lite e2e tests run stable tests and in parallel.
- 1#21797 Make ubernetes lite e2e tests run in parallel0.777the one the gold set calls related
- 2#9493 e2e test for addon upgrade0.720
- 3#27577 Fix 7 broken example e2e tests0.711
- 4#27286 Make it blocks in NodeProblemDetector e2e test serial.0.703
- 5#29031 Make PD E2E Tests Wait for Detach to Prevent Kernel Errors0.701
The issue the gold set marks as related, #21797, came back at rank 1.
#27227 Counting pod volume towards PV limit even if PV/PVC is missing
- 1#27198 PVClaim deletion should be graceful wrt running pods0.706the one the gold set calls related
- 2#10661 When scheduling, treat zero limit-pods as having nonzero limit when calculating priorities0.704
- 3#3518 Disk space monitoring0.698
- 4#23122 Return more useful error information when a persistent volume fails to mount0.695
- 5#14537 PersistentVolume Provisioner Controller0.674
The issue the gold set marks as related, #27198, came back at rank 1.
#3201 Umbrella: Allow a 3rd party plugin to run on top of a Kubernetes cluster
- 1#2358 Exposing services outside the cluster (or in different parts of the cluster) to pods0.700
- 2#21648 Proposal: First-generation of `kubectl set *` commands0.696
- 3#14516 RFC: Inert deployments0.689
- 4#21950 docs: add an additional ABAC example with more context0.688
- 5#16177 HPA needs to handle discovery, controller credentials, privilege escalation protection0.688
Missed. The issue the gold set marks as related is #1702, configuration reconciliation (aka kubectl apply), and it came back at rank 6, outside the 5 shown.
#5830 Cap container hostname length to 63 chars.
- 1#5829 Failed to start container: unable to sethostname when calling0.620the one the gold set calls related
- 2#6024 Use the first token of hostname for generating static pod names0.595
- 3#6361 Include ContainerStatus in "describe pod".0.592
- 4#1149 kubecfg proxy "411 Length Required" error on POST/PUT0.589
- 5#19182 Set conntrack params in kube-proxy0.576
The issue the gold set marks as related, #5829, came back at rank 1.
What this means if you need something similar
If your model's metrics look implausibly bad, or implausibly good, this is the instinct that catches it before it ships: treat a suspicious number as a bug report on your own pipeline first, not a verdict on the model, and keep auditing once you find the first leak.
Building something like this? I'm looking for Lead or Senior Applied AI roles, and I take on select AI/ML consulting engagements.
Related projects
- Style Maitri
An AI shopping assistant for Indian weddings that searches eight store catalogues at once and will not invent a price or a size to make the sale.
- Expense Tracker
Type what you spent the way you would say it out loud. It files the expense, categorizes it, and predicts where your spending is heading.
- Warmer
A word game you play once a day, where your only clue is how close each guess comes in meaning to the secret word.