Notable changes to the lowendinsight library and the hosted service in this
repository. The library is published to Hex; the service is deployed from the
same tree (ADR-003).
0.13.1 — 2026-10-03
Fixed
One contributor's name could lose a whole repository.
git log --authoris a regex, and a contributor name is data from the repository.apache/arrowhas a commit authored by[5~David Li— a real name, from a terminal control sequence that got into someone's git config — and the unterminated[is an invalid character class:fatal: header, '[5~David Li': Unmatched [, [^, [:, [., or [=git exits 128, the call raises, and the analysis of the entire repository is lost rather than one contributor's date.
pyarrowis in the top twenty PyPI packages and that is what happened to it.Fixed by passing
-F, so the name is matched literally — which is what the function means, and what theCo-Authored-Bysearch one function away already did.Not an injection.
Lei.Git.run/3usesSystem.cmd/3with an argument list, so there is no shell and the bytes reach git as data. The bug was data being interpreted as a pattern.It is also a correctness fix, not only a crash fix. A name containing
.is a valid regex where the dot matches any character, so the lookup could return a different person's commit:--author=A.C Dev -> 2026-01-01 (matched "ABC Dev") -F --author=A.C Dev -> 2020-01-01 (the actual contributor)Six years apart, and in the direction that makes a repository look more recently maintained than it is. Any contributor with an initial or a
Jr.in their name was exposed. The crash was the visible case; this one was silent.Only the unterminated
[raised — git uses basic regular expressions, soA[1] Dev,Foo (Bar,C++ Dev,Jo* SmithandAnn? Leeall exit 0 — which is why this went unnoticed until a name happened to crash.
0.13.0 — 2026-10-03
Fixed
numpy, pandas and scipy did not resolve. 10 of the 50 most-depended-upon PyPI packages failed, and the list was the core of scientific Python. PyPI does not normalise
project_urlskeys and the extractor matched"Source"exactly; numpy writessource, pandas writesrepository, pytest-cov writesSources. Keys are now matched case- and separator-insensitively, soSource Code,source-codeandsource_codeare one key. 80% → 96%.It failed as
:no_repository, which reads as "this package declares no repository" rather than "we did not look properly", so the gap looked like a property of PyPI rather than a bug in us.Still deliberately narrow: only source-like keys count. Accepting any repository-looking value would resolve a package to whatever GitHub project its documentation points at — a confident analysis of the wrong history.
A GitHub Pages homepage now maps to its repository. Pages serves
<owner>.github.io/<repo>/from that repository, so the first path segment is the repository name. Three of the 34 genuine top RubyGems —coveralls,vcr,guard— declare nosource_code_uriat all and only a Pages homepage. Applied to PyPI and Composer homepages too.A bare
<owner>.github.iois not mapped: that is a user or organisation page whose repository is the website, not the package's source. It is a valid repository shape, so it would pass every downstream check and resolve the package to its own marketing site.
0.12.0 — 2026-10-03
New
Go, Composer and RubyGems resolve.
Lei.PackageRepositoryunderstood npm, hex, pypi and cargo, which is why a customer scanning a Go service got nothing from us. Go needs no request at all — a module path is its location, andnormalize/1already trims a/v2major version, a/service/s3submodule path and anything on a host we cannot clone. Composer readsrepo.packagist.org's declaredsource.url; RubyGems readssource_code_uri, which commonly points at a tag (.../rails/tree/v8.1.4) and is trimmed to the repository.Two vanity prefixes are mapped, because measuring showed they are not a rounding error. Of the 100 most-depended-upon Go modules, 70 are already
github.com/..., 12 aregolang.org/x/...and 4 aregopkg.in/..., so the two documented conventions take Go coverage from 70% to 86%:golang.org/x/NAMEisgithub.com/golang/NAME, and gopkg.in's own scheme makespkg.vNintogithub.com/go-pkg/pkgwith the major version in the segment rather than a directory, soyaml.v2andyaml.v3are one repository. Every target was checked against the GitHub API.The reason is not the 16 points.
golang.org/x/*is the Go team's own foundational set, so excluding it is not random missingness — it removes the best-maintained corner of the ecosystem and biases any measurement of Go toward looking worse maintained than it is. A bias that flatters the hypothesis is the one to remove first.The remaining 14% —
google.golang.org,k8s.io,go.uber.org,cloud.google.com,sigs.k8s.io— stays refused. Each has a real GitHub home (google.golang.org/protobufisgithub.com/protocolbuffers/protobuf-go) but no rule derives it from the path; they are per-organisation facts, and a confident analysis of the wrong history is worse than a refusal.Lei.PackageRepository.ecosystems/0, so a caller can ask what resolves rather than keeping a list.LeiService.CacheBaselinekept its own copy of four, so adding a resolver here would otherwise leave every caller of that still refusing the ecosystem — two lists of one fact, and the copy decided what got measured.
0.11.0 — 2026-10-03
Sixteen commits since 0.10.0 were never released, and the gap was found by
depending on the published package from outside this tree: a survey built on
lowendinsight 0.10.0 could not see functional_commit_currency_weeks at all,
because the module that computes it is in this list.
New
Lei.CommitSubstance: commit currency measured from the last commit that meant something. Plain currency is reset by anything — a README typo, a licence year bump, a merged Dependabot PR — so a repository with no human involvement for a year could reportlowon automation alone, at any threshold. A commit is not substantive when its author classifies as a bot or every path it touches is documentation or repository metadata. AI co-authorship does not make a commit non-substantive: an agent-assisted commit still represents a person deciding the project needed changing, andagentic_classificationis the metric that speaks to that.functional_commit_currency_weeksandfunctional_commit_currency_riskare new result fields, with their own thresholds rather than sharing the plain ones.Lei.ReportFreshness: currency recomputed on the way out of the cache. Five of the six risk metrics are pure functions of a cloned history and stay correct while the repository does not move. Commit currency is derived fromDateTime.utc_now(), so a stored week count reported the risk a repository held when it was analysed — a project drifting into abandonment kept its old verdict, which made the cache a way of suppressing the one signal the analysis exists to raise. Recomputed from the storedlast_commit_date: no clone, no network.Lei.RiskProfile: the distribution a verdict was collapsed from.data.riskis the worst of a repository's metrics, which made "one functional contributor, active this month" and "one functional contributor, silent two years" read identically. The verdict is unchanged and the counts are reported beside it — no weights, no 0-100 score.- Manifest ranking, and directness rather than centrality. A report is rankable, the dependency edges already being received are kept rather than discarded, and one registry fetch answers both the repository and the dependency questions.
Fixed
- An npm
repositorypublished as a string raisedFunctionClauseErrorinside aTask, taking a whole batch job down rather than failing one package.@nodelib/fs.statpublishes that form and is a transitive dependency of most npm projects, so any batch analysis over a real npm manifest ended on it. The shorthand forms (github:o/r, a bareo/r,git@host:o/r) and URLs pointing into a monorepo directory now resolve too; each previously read as "this package has no repository". - Resolution asks for one version, not every version ever published. npm's
package document for
typescriptis 15.7 MB against 4.8 KB for/typescript/latest— the samerepositoryfield. The download was never the cost; decoding 15.7 MB to read one string was, at 19.8 s per package.describe/3anddependencies/3still take the full document, becausedist-tagsandversionsexist only there. - Analyses that determined nothing are no longer cached, and arguments are validated before work starts.
Changed
- git is run directly, dropping a dependency unmaintained since 2018.
- README and LICENCE ship in the package.
0.10.0 — 2026-09-16
Breaking, for library users
- Elixir 1.17 or newer is required. httpoison 3 requires it, and the toolchain moved to Elixir 1.20.4 / OTP 28.5.
- httpoison 3 / hackney 4. hackney 1.25 carried four advisories, the highest a SOCKS5 TLS upgrade that ignored the caller's timeout (GHSA-gp9c-pm5m-5cxr). The fix is hackney 4, which needs httpoison 3.
httpoison_retryis gone, replaced byLei.HTTP.Retry. The policy is the same — retry transport timeouts, closed connections,nxdomainand HTTP 500; 5 attempts 15s apart by default — and it also retries hackney 4's:connect_timeout. Callers passLei.HTTP.Retry.request(fn -> ... end)instead of piping throughautoretry/2.Lei.BatchAnalyzer.analyze/2takes a:schedulefunction. The library has no job queue; the caller supplies one, and it returns{:ok, job_id}or{:error, reason}per dependency. Without it a cache miss is reported"uncached", where it used to be reported"pending"with a generated id that named no work.cache_mode: "fresh"now bypasses the cache, for the results and forcache_split/2. It was accepted and ignored, so a caller asking for a fresh analysis was served the cached report.jasonis a declared dependency. The library called it in three modules without declaring it; inside this umbrella the service's copy hid that.
Fixed
- npm lookups use
registry.npmjs.org.replicate.npmjs.comanswers 404 for every package, so npm scans never found a repository and analysed the bare package name instead. The scan tests passed throughout: they count reports, and the fallback still produced one. - yarn.lock: scoped packages keep their names.
@babel/corewas split at its first@and came out as"". - yarn.lock: versions compare as versions.
Float.parsemade 1.10.0 lose to 1.9.0. - yarn 2+ (berry) lockfiles parse, via yarn_parser 0.4; workspace entries are not counted as dependencies.
- EEx comments use
<%!-- --%>, deprecated syntax removed for Elixir 1.20.
Added
Lei.PackageRepositoryresolves a package coordinate (npm, hex, pypi, cargo) to a repository URL, normalisinggit+ssh,git://and.gitforms, and refusing an ecosystem it does not know rather than guessing.
Security (hosted service)
From the 2026-09-14 review and its follow-ups:
- Analysis reports no longer publish the application environment; the canary scans every public body for secret-shaped content.
- Analyses are limited to public https URLs.
- Org key routes check the caller owns the org; sessions are bound to a live key; dashboard templates escape output.
- Admission checks and charges under a lock on the org, so simultaneous requests cannot all pass one org's allowance.
- Pro activation requires a Checkout Session confirmed with Stripe; agent checkout sells credits rather than a tier.
- Stripe webhooks are applied once, only when recent, and only when paid.
- A job's id is the credential for it, and polling cannot loop analysis.
- Only an operator can force a trending refresh.
- Operator tokens must carry
exp, in the future and no more than 24 hours out. Both paths verified the signature alone, so an expired token was accepted and one minted withoutexpnever expired. - Signup, login and recovery are rate limited per IP (5, 10 and 5 per hour). Recovery answers with a new admin API key, so unlimited guessing was an organisation takeover.
Background work (ADR-004, hosted service)
Deferred work ran four ways and only one survived a restart; 79 analyses sat
executing for days while every check stayed green.
- Oban 2.24 on schema 14, with Lifeline rescuing orphaned jobs and Pruner bounding the table.
- Queue health on
/readyzand/metrics; the monitor fails on stuck or backed-up work. - Every job bounded by a timeout; deploys drain for 60s instead of being killed after 5.
- Batch misses enqueue real jobs and return their ids.
- No billing write is fire-and-forget; a cached report's refresh is queued.
- Trending is one job per language on its own queue, and cache cleaning is a job; Quantum is removed.
Infrastructure
- ADR-003: the library is the analyzer only. The hosted service moved to
apps/lei_service, with the service's settings under:lei_service.scripts/library-isolation.shbuilds a project that depends only on the library and proves it compiles, boots with no configuration, analyses a repository, and declares no service dependency. - Dependency advisories are checked with
mix hex.auditand mix_audit againstscripts/acknowledged-advisories.txt; mix_audit 0.1 had reported nothing while the lock held HIGH advisories. - Toolchain pins (CI, every Dockerfile,
.tool-versions) must agree;scripts/check-toolchain.shfails when they do not. Production had been building on Elixir 1.15.7 with an end-of-life Alpine while CI tested 1.16.3.
0.9.2 — 2026-09-15
- Security release for the 0.9 line; see GHSA-mqqj-2vjh-xw24.
0.9.1 — 2026-03-07
- Maintenance and documentation cleanup.
- Standardized project references to GitHub.
0.9.0 — 2026-02-05
- SARIF output for the GitHub Security tab (
mix lei.sarif). - ZarfGate: quality gate for CI/CD pipelines with configurable thresholds.
- AI rules generation for Cursor and GitHub Copilot.
- Files analysis: binary file detection, README/LICENSE/CONTRIBUTING presence.
- SPDX parser: full SPDX SBOM parsing.