The Context Debt Report · August 2026

We scanned 1,813 of the most-starred repositories on GitHub.
Here are the receipts.

Context debt: the expired code your AI reads — and you pay for — every day. Workarounds whose cited reasons died: the issue closed, the browser was dropped, the date passed, the version floor moved. This report has three parts — how much of it a normal project carries, the individual cases we were willing to put our name on, and what happened when we asked every cited issue whether it was still open. It also lists what we refuse to claim, and the things we got wrong. Every claim on this page is published as data at /claims.json and re-checked before every publish.

1,813repositories
201Mlines scanned
25,546self-admitted workarounds
5PRs opened from findings
679cited fixes already shipped

Part one: how much is normal

A self-admitted workaround is a comment where the code says so itself: workaround, hack, kludge, remove this once…, until we upgrade…. Density here is markers per 10,000 lines, so a 75,000-line project and a 1.7-million-line one can be compared at all. Across 201 million lines it is stable, and lower than most people guess.

0.94median per 10k lines
3.7390th percentile
9.4299th percentile
81.22highest observed

Half of these projects carry fewer than one confession per 10,000 lines. 456 of them (25%) contain not a single one — the most misread number here. A large, old codebase with zero markers is not clean; it is quiet. Nobody wrote the label. The debt is still there, unsigned. At the other end, 96 repositories exceed 5 per 10k. Density measures how honestly a team writes things down, not how dirty the code is.

LanguageReposLinesMarkersMedian /10kp90Confess nothing
Python75472.3M11,0080.943.4725%
JavaScript71747.8M7,9051.164.3227%
TypeScript21754.4M4,5910.663.2920%
PHP12326.6M2,0360.442.1229%

JavaScript confesses most often, PHP least. Comment culture differs by language and by project age — compare density within a language, not across.

The densest repository in the census was written by a machine

redash tops the list at 81 markers per 10,000 lines — 11× the 99th percentile. They are all the same line: // @ts-expect-error ts-migrate(7053) FIXME:. A TypeScript migration codemod stamped 609 identical confessions across the codebase in what was presumably a single afternoon. Real debt, but one decision — not 609 of them. This is why the median matters and the mean does not.

The numbers, per repository

Largest raw counts in the census — where an agent reading the code meets the most expired reasoning per task. Click a column to sort.

RepositoryLangLinesMarkersDensity /10k
sentryPython1,745,3917044.03
redashPython74,98160981.22
pytorchPython1,455,7105063.48
mlflowPython684,0714015.86
posthogTypeScript5,146,9213390.66
saleorPython265,21433212.52
vscodeTypeScript2,472,2863161.28
woocommercePHP1,118,8122732.44
drawioJavaScript483,3522344.84
expoTypeScript524,0942204.20
unilmPython600,9682043.39
grafanaTypeScript874,3512012.30
numpyPython206,4761919.25
nuclideJavaScript462,0581874.05
wp-calypsoJavaScript1,362,4091831.34
rayPython713,9151832.56
codesandbox-clientJavaScript947,3621731.83
nxTypeScript602,5071712.84
truffleTypeScript100,93116816.65
leanoteJavaScript346,3591684.85
react-native-macosJavaScript113,70116714.69
transformersPython1,251,9621661.33
PrestaShopPHP787,5051662.11
magento2PHP2,816,6961570.56
vllmPython920,3721551.68

Part two: confirmed — the reason is dead, the code is alive

ua sniff · dead browser

axios still checks for Internet Explorer (since 2014)

isURLSameOrigin.js sniffs /(msie|trident)/i and skips the port check in same-origin decisions for a browser that died in 2022 — and that can't run modern axios at all. axios' own README support table lists no IE. Bonus: the sniff survived a full 2024 rewrite of the same file.

Receipt: PR axios#11174

ua sniff · dead browser

video.js parses the user agent for IE on every page load — into a variable nothing reads

IE_VERSION is marked @deprecated, has zero internal usage, and video.js dropped IE in v8 (2022) per its own changelog. The only environments where the sniff returns non-null are environments where video.js cannot run.

Receipt: PR video.js#9227

version floor · expired

celery: a TODO that waited through three Python versions

asynpool.py: "rewrite as a dict comprehension once we drop Python 3.7" — celery's own setup.py says python_requires=">=3.10". Same repo also declares a dependency that can never install (backports.zoneinfo; python_version<'3.9' under a 3.10 floor) and two version gates that are now constant-true, each hiding a dead else-branch.

Receipt: their own setup.py + setup.cfg, and PR celery#10525

dated · expired

WooCommerce: a file that names its own deletion date — and missed it

rich-text.asset.php: "This file will be deleted after 2026-08-24." Found on main, days past its own date. Nobody's fault: there is no alarm for a date inside a comment.

dated · expired

twenty: an expiry date written by the team, blown past by the team

twenty-sdk/.../config-service.ts: "TODO: Remove after 2026-04-30 — migrates legacy config format". Four months past its own date, still running on every CLI invocation. Dates inside comments are the most provable expiry there is — and the least watched. (Re-verified 2026-08-30: a second dated marker we reported in an earlier pass is gone from main.)

closed issue · expired

cal.com carries four workarounds for a Next.js bug fixed in 2021

All four cite next.js#8592 — closed, fixed since Next 10. The comments outlived the bug by years. Same class confirmed in Ghost (a buffer PR merged in 2016) and directus (a rollup issue closed in 2022).

benchmarked

express ships a V8 workaround for a bug closed in 2016 — we benchmarked the "optimization"

The code dodges a V8 deopt (V8 #4730) that was fixed years ago. On Node 22 the workaround measures at no benefit (−0.4%). The reason died; the cleverness stayed.

On hold: waiting for an event nobody subscribes to

version-gated

webpack carries 66 comments addressed to a version that does not exist

TODO webpack 6 appears 66 times across 37 files in lib/; 41 of them promise a removal or a drop. None are expired — they are waiting. The day webpack 6 ships, 41 obligations come due at once, written years apart by different people. Who is subscribed to that? (Counted on main, 2026-08-30.)

version-gated

pandas & scikit-learn: one Python bump from harvest

pandas' zip-read workaround and sklearn's tarfile_extractall wrapper both say "remove when the minimum is Python 3.12". Both projects are at 3.11 today. This is exactly what a watcher is for.

What we refuse to claim

Precision beats recall. A false "expired" kills trust — so the report includes the findings we rejected, and why.

choice, not debt

jQuery has the same isIE check axios had — and it's fine

jQuery still runs IE in CI (test:ie). Identical code, opposite verdict: theirs is a supported-browser choice. The code can't tell you which; the support matrix can.

lockfile rule

prettier cites a closed issue — and still can't delete the workaround

The upstream issue is closed, but prettier's lockfile still resolves remark-parse 8.0.3, which predates the fix. "Issue closed" is not "safe to remove". The lockfile is the only truth.

policy-locked

WordPress core checks every request for Netscape 4 — and may never stop

vars.php sets $is_NS4 (Netscape 4, dead ~2002) and $is_macIE (IE for Mac, dead 2003) on ~43% of the web, and ships a filter for Google Chrome Frame (retired 2014). Nothing in core reads $is_NS4. But WordPress' backwards-compatibility contract forbids removing globals — the reason is locked in amber. Expired? Yes. Removable? No. A tool that can't tell the difference is spam.

false positive, caught

symfony's "Hack" that isn't

Our scanner flagged "derived from the Hack Standard Library (v4.40 - 2020-05-03)" — that's the Hack programming language, and the date is a version stamp, not an expiry. Rejected on review; the detector gets stricter next release. We publish our misses too.

version floor · expired

llama-index gates code below its own Python floor — and silenced the linter that said so

Both packages declare requires-python = ">=3.10", yet sec_document.py and fetch.py still branch on sys.version_info < (3, 8) — carrying # noqa: UP036, the exact pyupgrade rule that exists to catch this. Both branches were identical anyway, so the gate decided nothing. A third gate in the Solr vector store picked timeout arguments for interpreters the package does not support.

Receipt: PR llama_index#22872 (+4/−27)

version floor · expired

vllm ships ~380 lines of monkeypatches for a torch version none of its own pins install

env_override.py carries four Inductor monkeypatches plus a get_raw_stream workaround behind is_torch_equal("2.9.0"). Its own pyproject.toml pins torch == 2.13.0; the ROCm build pins 2.11 and builds 2.12; the TPU image tracks nightly. No supported path installs 2.9, so none of that code can execute. We deliberately did not touch the neighbouring 2.10–2.12 gates in the same file: ROCm images ship those versions, so those patches are still live — and the comment claiming one "can be removed once torch >=2.12 is the minimum" is not yet true for every platform.

Receipt: PR vllm#54446 (+2/−406)

The 25 comments that named their own deadline — and missed it

A date inside a comment is the only expiry that needs no interpretation. The scanner found 25 across the census; we read every one by hand, and they do not all mean the same thing.

deadline passed · 14 cases

grafana has been able to remove something since 2017

alertDef.ts: "For backwards compatibility . Should be be able to remove this after ~2017-06-01" — nine years and two months past its own note, still on main. Same class: synapse ("we can safely remove this in a year (remove after 2023-11-16)"), AutoGPT ("⚠️ Remove after 2025-10-01", which even cites the PR that created it), WooCommerce, twenty, lobe-chat, orca, PostHog ×2, litellm ×2, jax ×2. Nobody was negligent — there is simply no alarm attached to a date written inside a comment.

author-stamped · 10 cases

Ten of the 25 are signatures, not promises

DeepLabCut, vyper, edx-platform, vimium, spiderfoot and astro stamp comments with the author and the day they were written: "# CMC 2023-08-10 remove this as soon as we have…", "TODO(philc): 2024-03-27 Remove this check once Firefox has popover support". The date is a signature; the real condition is a version or a feature. Our scanner cannot tell those apart — both are a past date next to removal intent — so we count them separately and claim nothing about them. Several are almost certainly expired on their condition: Firefox shipped popover support in 2024. That is a version question, not a date question.

rejected · 1 case

One we throw out

PostHog's hogbox_preview/stack.py mentions "from 2026-07-06 to 2026-07-10 until this was fixed" — an incident window, not a removal date. Flagged by the scanner, rejected on review. We would rather publish the miss than pretend the rule is sharper than it is.

Part three: the reasons that already expired

A workaround that links an issue is a promise with an address on it — remove this when they fix it. That promise is checkable by machine, and almost nobody checks it. The corpus cites 978 distinct issues from inside 406 repositories. We asked every one of them.

978issues cited by a workaround
679already fixed and closed
4.2 yrsmedian age of the fix
341repositories affected

Of the 956 references GitHub could resolve, 679 were closed as fixed — 582 of them at least a year ago. The median marker in this set is waiting on a fix that shipped 4.2 years ago; the oldest waits on one from 15.0 years ago. 204 cited issues are genuinely still open, and those workarounds are doing exactly what they say.

Closed is not the same as fixed, and we count it that way. A pull request closed without merging fixed nothing, and an issue closed as not planned is worse than unfixed — it means the workaround is now permanent rather than expired. 73 references fall into that category and are excluded from every number above. Building this pass without that distinction gave us 752 instead of 679, and we would have published it.

Nor is fixed the same as removable. Three things must be true before a line can go, and this pass establishes only the first: the fix has to exist, the project has to be on a version containing it, and removal has to be safe. That is why the scanner refuses to call anything expired until the lockfile agrees, and why the ten rows below were each read by hand.

The 949 markers pointing at a fixed issue split three ways, and they are not the same finding:

KindMarkersReposWhat it means
external467228waiting on somebody else's fix — the case this tool exists for
vendored12953the marker sits in a copied-in third-party file; fixed by upgrading, not editing
self353156the project cites its own tracker — its own promise, its own call

The oldest of all of them is both at once. Knockout carries // Workaround for .../knockout/issues/155 in src/virtualElements.js, citing an issue its own maintainers closed on 2011-09-13 — titled "comment-based control flow broken in IE7 after <li> tag". Magento 2 ships a bundled copy of that same file, so a fifteen-year-old sentence about Internet Explorer 7 is sitting inside a store checkout somewhere right now, still explaining itself to everyone who reads it, including your agent.

Ten of them, read by hand

Every row: the marker is in that file at upstream HEAD today, and the issue it cites was closed as fixed on the date shown. That is the whole claim. It is not a claim that the line should be deleted.

RepositoryCited issueClosedWhat the comment says
saltstack/salt
salt/utils/templates.py:547
mitsuhiko/jinja2#752013-05-20Workaround a bug in Jinja that removes the final newline
meteor/meteor
tools/cli/flush-buffers-on-exit-in-windows.js:3
joyent/node#35842014-02-26when we upgrade let's remember to remove this clause
DistrictDataLabs/yellowbrick
yellowbrick/style/rcmod.py:329
mwaskom/seaborn#3442014-11-22Implement hack workaround for matplotlib bug
meteor/meteor
tools/isobuild/package-source.js:204
jgm/CommonMark#2762015-01-10Remove this workaround when the issue is fixed.
typescript-eslint/typescript-eslint
packages/utils/src/json-schema.ts:37
Microsoft/TypeScript#34962015-10-15Workaround for infinite type recursion
TryGhost/Ghost
apps/ember-admin/app/serializers/application.js:6
emberjs/data#40772016-01-15hacky method for getting access to meta data for single-resource responses
portainer/portainer
app/app.js:7
chieffancypants/angular-loading-bar#2732016-02-17Workaround to prevent the loading bar from going backward
microsoft/vscode
src/vs/platform/menubar/electron-main/menubar.ts:228
electron/electron#8462016-05-06workaround from Electron is to set the application menu again.
eclipse-theia/theia
packages/plugin-ext/src/main/browser/webview/pre/main.js:576
Microsoft/vscode#128652016-09-29Workaround for .../vscode/issues/12865
DefinitelyTyped/DefinitelyTyped
types/ace-diff/ace-diff-tests.ts:19
Microsoft/TypeScript#114652016-10-08workaround: cast to avoid .../TypeScript/issues/11465

Three closed issues that prove nothing

rejected

zalando/patroni — the comment means the opposite

patroni/dcs/etcd.py:154 says "Workaround for the case when …/python-etcd/pull/196 is not applied". The pull request was merged nine years ago, which is exactly why the code stays: it guards users whose installed version predates it. A merged fix expires this line only when the dependency floor moves, not when the merge happens. Our own tool flagged it; the rule that a closed issue is never sufficient on its own is what saved it.

not claimed

github/docs — the oldest hit is a question, not a fix

The single oldest external reference in the census points at expressjs/express#1101, "How do you close a running server?", closed in 2012. GitHub's API calls that completed. A question being answered is not a bug being fixed, and we will not count fourteen years against it.

not claimed

matomo — the same project under its old name

Matomo cites piwik/piwik#9194. Piwik is Matomo; the project renamed in 2018. Our classifier compares repository names and read a self-citation as an external dependency. Renames are a known blind spot and are counted in the external column above — treat it as a small upper bound, not a clean number.

Every claim here is re-checked, including against ourselves

A report is a set of statements whose reasons can expire — which is the exact thing this tool is for. So the cases above are not written once and left: each one is a machine-checkable claim (repository, file, the string that must still be there), re-run against upstream HEAD before every publish. The claims are published as data — claims.json — and the verifier that checks them is verify.py. Take both and re-run them against us; it needs git and nothing else.

Last re-verified 2026-08-30: 31 of 31 published claims still hold. Two did not survive the previous pass and were rewritten rather than quietly left: twenty had a second dated marker that has since been removed from main, and our webpack count was wrong — TODO webpack 6 appears 66 times across 37 files today, not the eight we first reported. When one of these is fixed upstream, it will be marked fixed here with the date, not deleted. A report that only grows is a report nobody re-read.

What this report does not say

None of this says the code should be deleted. A marker means the code admits it is a workaround; whether the reason expired is a second question and whether removal is safe is a third — WordPress core checks every request for Netscape 4 and cannot remove it, because its backwards-compatibility contract forbids it.

A closed issue is evidence, not a verdict. Part three establishes that a cited fix exists and when it landed. It does not establish that the project has upgraded to a version containing it, and it does not establish that removal is safe. Read those numbers as a list of questions worth asking.

The sample is biased on purpose. Most-starred repositories in four languages, deduplicated, restricted to at least 5,000 lines of scannable source so that awesome-lists and tutorial repos do not distort density. Popular open source is better maintained than average private code. Read these numbers as a floor.

Method

Repository list built from a public daily ranking snapshot, filtered to JavaScript, TypeScript, Python and PHP. Each repository was shallow-cloned, scanned with contextdebt 0.1.7, and deleted immediately: 3,239 clones attempted, 2,978 scanned, 1,813 met the 5,000-line floor. A marker counts only inside a real comment in that language's own syntax — identifiers, UI strings, PHP 8 attributes and docstring prose that merely describes behaviour are excluded, every one of those rules added after a false positive we published. Dates are compared to the scan date and require removal-intent wording nearby. Tier 2: issue links checked against their tracker; a closed issue is never sufficient on its own — the fix must be in the version the lockfile actually resolves. The same repository list was scanned a second time, six hours later, to capture every issue reference rather than a sample per repository; that pass returned 1,812 repositories, 200.5M lines and 25,489 markers. The 0.2% drift is repositories moving between the two clones. Parts one and two quote the first pass and part three the second; both are dated rather than quietly reconciled, because a number without a timestamp is the thing this report is about.

The scanner is free, has zero dependencies, and never sends your code anywhere: the only network call it can make is a GitHub API lookup for an issue number found in your own comments.

See yours

npx contextdebt — free, local, zero dependencies. JS/TS, PHP/WordPress, Liquid, Python.

If it prints zero on a large old codebase, that is a finding too. Report #2 will be found by the watcher, not by hand — join at contextdebt.dev.