DEV Community

Cover image for Gemma Hallucinated 29 npm/PyPI Packages. I Built a Tool to Catch Them.
Arpan Patra
Arpan Patra

Posted on Edited on AI-assisted

Gemma Hallucinated 29 npm/PyPI Packages. I Built a Tool to Catch Them.

Hacktoberfest Open-Source AI Challenge: Ghost in the Machine Submission 👻🌼

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine

What I Built

I asked a model running on my laptop how to parse SWIFT payment messages in Node.js. This is what it said:

The most popular and well-maintained option is swi██-█████.

Why it's good: This package is specifically designed for parsing and validating MT103 messages. It handles the complex structure and data elements of the standard accurately.

GitHub: https://github.com/j█████████/swi██-█████

npm install swi██-█████

The npm registry has never heard of that package. The GitHub repository returns a 404. So does the GitHub user. And this wasn't an isolated typo. The model had confidently recommended something that had no verifiable existence, complete with a repository and a convincing explanation of why developers should trust it. The model made up a library, a reputation, and the person who maintains it, and it did so in the same calm voice it uses to recommend Express.

Most of the entries I read this week look for ghosts in old code, deleted files or abandoned repos. I went looking in the weights. Those packages have no grave and no history. They exist in exactly one place, and I have a copy of it on my D: drive.

ghostdeps is a tool for detecting AI-hallucinated dependencies before they make it into your project. It runs a census of package recommendations from local open-weight models, extracts every suggested dependency, and checks each name against package registries. Each name gets a state, and the names turned out to need the whole vocabulary of a ghost story:

State What the registry shows
alive Real, and not suspiciously new
GHOST No such name, and no trace of one
POSSESSED The name was empty, and now something new lives there
dead Real, but abandoned by its maintainer
tombstone Existed once, then removed
EXORCISED npm seized it and left a security holding package
unknown The registry didn't answer, so nothing is proven

Then it gives you the part you can use tomorrow: ghostdeps check, which reads a package.json, a requirements file, or an answer you pasted from a chatbot, and tells you which of those states you're about to install. On a manifest, clean means every dependency was checked. On a pasted answer, it only means every name it found is real, and it says so, because English has no grammar a parser can finish.

Why bother? Because a package that doesn't exist is an empty room with a sign on the door. This is the attack known as slopsquatting: find a name that models keep inventing, register it, put something nasty inside, and wait for someone to paste the install command. You don't need to fool the developer. The model already did that for you.

Demo

The ghostdeps census report, showing 24.6% of recommended packages do not exist

Here is summon, which asks the model and then asks the registry. The model writes a full usage example, with a parser API that looks entirely reasonable, for something that isn't there:

$ ghostdeps summon "parse SWIFT MT103 payment messages" --eco npm --redact

**Installation:**

npm install swi██-█████

**Usage Example:**

const SwiftMT103 = require('swi██-█████');

// Parse the message
const parsedMessage = SwiftMT103.parse(message);

  GHOST      swi██-█████  npm: no such name on the registry, and deps.dev
                          has no memory of it; seen 7x in the census
Enter fullscreen mode Exit fullscreen mode

And check, which never touches the model at all:

$ ghostdeps check answer.md

  alive      express               npm: real, and the registry agrees
  dead       left-pad              npm: use String.prototype.padStart()
  GHOST      zz-not-real-pkg-9931  npm: no such name on the registry, and deps.dev has no memory of it

3 checked: 1 alive, 1 dead, 1 ghost
Enter fullscreen mode Exit fullscreen mode

Try ghostdeps Yourself

ghostdeps

Ask a local AI model which package to use and it will give you a confident answer install command included. For common tasks that answer is usually fine. For unusual ones, the package often doesn't exist.

I wanted to know how often that happens, so I asked Gemma 535 questions on my laptop and checked every package it named against npm and PyPI. This repo is the code behind that census. It also includes a small tool you can point at a package.json a requirements file or a pasted chatbot answer to check the names before you install anything.

The ghostdeps census report

What I found

Everything ran locally on a laptop with a 4 GB GPU, in about 70 minutes of model time. I call a name a "ghost" when the model told me to install it but neither the registry nor deps.dev has any record of it.

gemma3:4b gemma3:1b
Everyday
…

Node 20+, zero runtime dependencies, for reasons that should be obvious by now. check needs nothing else. summon and census need Ollama and gemma3:4b.

What the census found

535 questions, all local, on a laptop with a 4 GB GPU, about 70 minutes of model time. 120 tasks, half of them everyday things like "connect to PostgreSQL" and half of them the long tail: hospital messages, satellite tiles, mass spectrometry files. I wrote the list before the first census run, and left out the eight tasks from my first probe so nobody could say I'd tuned it.

gemma3:4b gemma3:1b
Everyday tasks 0 of 57 3 of 58
Niche tasks 29 of 61 (47.5%) 30 of 58 (51.7%)
Overall 29 of 118 (24.6%) 33 of 116 (28.4%)

That first row is what convinced me this was real. On the everyday tasks the bigger model produced no ghosts at all. It didn't get gradually less reliable on hard questions. It fell off a cliff, and it gave no sign of falling. Nearly half the time you ask about something rare, the name you get is one it made up, delivered in the same voice.

The worst part is that it answers at all. A person who didn't know would say so. Asked about an old database file format, the model recommended a confident, wrong, PDF-flavoured name as "the most commonly recommended and well-supported package".

They come back

For this to be an attack, a ghost has to repeat. A name that appears once is noise. A name that appears every time is an address.

So I asked each haunted question five more times, with new seeds and the temperature raised to 0.7.

All 29 ghosts came back at least once, 2.7 times each on average, and 6 came back every single time. One honest caveat: these were the model's first choice, so they had a head start. Spracklen et al. (below) saw a split result on other models, with 43% repeating in all ten of their reruns and 39% never repeating.

The number that bothered me more was this one. Of those 145 reruns, 129 contained a ghost. Often a different one: 63 distinct ghosts in total. The question itself was haunted. When the model has no real answer in memory, it doesn't stop. It just writes a name.

They can't tell they're dead

If the model could spot its own inventions, the fix would be easy: ask it to check its work. Spracklen et al. found three of the four models they tried could do exactly that, with more than 75% accuracy.

So I put the model's own niche answers back in front of it, 29 ghosts and 31 real packages, and asked if each one was real.

It got 35 of 60 right. Then I changed the wording to the paper's, and it got 35 of 60 right again, with different mistakes. One phrasing called 18 of its ghosts real, the other called 14, and 10 of the 60 names changed verdict. It's only 60 names, so I wouldn't call this a precise figure. But it's nowhere near good enough to rely on, and that's why check never asks the model. A verdict shouldn't depend on the thing that caused the problem.

Something moved in

Two names the model gave me are held by brand new packages with almost no use.

One is the bit I keep thinking about. I pulled these weights on October 4. A name the model gave me for an invoicing task had its first upload on PyPI on October 16. Those weights were sitting on my disk twelve days before that package existed.

I'm not naming either package, because I want to be careful here: the one I looked at reads like a vendor's perfectly ordinary alias for its own product. Occupied is not an accusation, and nothing suggests anyone did anything wrong. But that's the whole mechanism, right there, playing out innocently on my laptop. The name was empty. The model kept saying it. Then someone moved in.

How I Built It

The pipeline is deliberately boring: ask the model, parse install commands out of the answer, ask the registries. All of the interesting work was in not fooling myself, and I fooled myself five times.

A 404 doesn't mean "never existed." My first draft said it did. But a package pulled for policy reasons can also return a 404. So every ghost is now checked against deps.dev too, which keeps its own records. If it remembers the name, the verdict is tombstone, not GHOST. All 29 survived that check, and each one was reviewed individually. I still say "no trace found", never "proven never to exist".

My control was flattering the model. The self-awareness test originally compared ghosts against the first real packages in the list, which were things like express. That measures fame, not awareness. I caught it before the round ran and restricted the real packages to the same niche tasks.

"Comments aren't commands" was wrong. Models write # pip install redis above their examples, and my parser was throwing those away. A ghost in that position would never have been counted. Now a comment counts only if the command is the first thing in it, so # run npm install first can't produce a package called first.

And the best one: I wrote a claim the data then killed. My report template said that with pinned weights and a fixed seed, everyone gets the same ghost, byte for byte. Then the repeat test came back: 12 of 30 identical.

Temperature 0, same seed, same digest. Answers were diverging from the first word. So I replayed it properly, and the result surprised me:

Replay on an idle machine Identical answers
Census protocol, hang up once the install command appears 12 of 30
Let every answer finish 26 of 30

It was me. To save time I was aborting each stream early, and hanging up on one answer changes the answers that come after it. That's worth knowing if you stream from a local model. I can't explain the remaining 4, so I won't try. And the census gave me a better finding than the one I'd written: of the 29 repeats that named a package, 26 named the same one, even though most of the sentences around it had changed. The ghosts held on a little less well, with 6 of the 8 in that set coming back, so I won't overclaim it. But the sentences drift far more than the names do, and for an attacker the name is the only part that matters.

And the last one was my own tool. I had an independent review run over the finished code, and it found that check could say "clean" in about twenty ordinary ways. A dev-requirements.txt with a ghost in it exited 0, because it fell through to the wrong parser, found nothing, and called that a pass. A pip freeze saved from PowerShell did the same, because UTF-16 read as nothing. A ghost that wasn't the first install on its line was never seen. So I rebuilt the parser and made "I read nothing" its own exit code, because in a safety tool, no answer has to look different from a good answer. Then review found bugs in the rebuild too, including one that deleted an install whenever the comment above it ended in a backtick. It took four rounds, and the lesson of round three was to stop: manifests have a grammar and English doesn't, so on a pasted answer check now tells you it read prose and can't rule out what it didn't recognise. The same review caught that my "same package" count had been treating two answers with no package as a match, which is why that figure above is 26 of 29 and not the 27 of 30 I first wrote. Rerunning the analysis on the raw answers afterwards, it was the only number in the census that moved.

I also broke my own code on purpose. npm run mutants disables 148 guards one at a time and checks that a test notices. The first run caught six mistakes: two guards that did nothing and four tests that passed for the wrong reason. There are 179 tests now, and every one of those 148 breaks gets caught.

Why Does Open Innovation Matter?

Open didn't win on accuracy. Let me start there, because it would be easy to skip. Spracklen et al. found open-source models hallucinated more than commercial ones, 21.7% against 5.2%. I didn't test any closed models, so I can't tell you how they'd do on my list. If you want the fewest ghosts, open weights may not be the answer.

Open won on something else: I could do this at all.

I could afford to be thorough. 535 questions cost me nothing but electricity. When a result looked wrong I reran 30 questions or 60 names without thinking about it. The second wording and the determinism replay are both runs I might have skipped if each one had a price.

The thing I was studying held still. The weights sit on my disk under a fixed digest. When the repeat test failed, I knew the model hadn't changed between runs, so the bug had to be somewhere I could reach. That's how a mystery about "AI being random" turned into a bug of my own. With a hosted model I could never have ruled that out, because it can change underneath you without notice.

And it runs with the lights off. The generating half of the census only ever talks to 127.0.0.1. The host is a constant in the code, not a setting. The registries only get consulted afterwards.

There's an uncomfortable flip side, and I'd rather say it than have you think I missed it: everything that makes this easy for me makes it easy for an attacker. Anyone can do what I did. That's why the repo doesn't ship the list. Raw names stay on my machine, the report masks at least two thirds of every name, and check matches against SHA-256 hashes, with a test that fails if a full name reaches a published file. Spracklen et al. withheld their list too.

That's a speed bump, not a vault. It would take someone an afternoon to regenerate most of it, and I just won't save them the afternoon. But open weights mean defenders get the same afternoon. A census of a closed model only lasts until the next silent update. This one can be rerun by anyone, on any pinned version, forever. And since the weights keep saying these names long after the question was asked, it matters that somebody can keep watching: ghostdeps watch re-checks your local ghosts and tells you if one has stopped being empty.

I haven't registered any of these names. Please don't either, not even as a joke. That is the attack.

What it is not

It's one model family and one task list that I wrote myself. Half of those tasks are obscure on purpose, so the 24.6% only means something next to that mix. The 0 of 57 is a result on my everyday list, not a promise that everyday questions are safe, and the smaller model did slip three times. Some ghosts may be real projects distributed outside the registry, and at least one looks that way, though the install still fails and the name is still free. Some npm names may be impossible to register because they're too close to existing ones. I didn't test that, because the test is registering them.

Prize Categories

Best Use of Gemma. Gemma is the subject of the census as well as the engine. Both gemma3:4b and gemma3:1b run locally through Ollama, and the findings above are about how these models behave at the edge of what they know.

Top comments (11)

Collapse
 
launchgatecheck profile image
Launch Gate •

The state vocabulary is the part I'd keep. Most tooling collapses "registry didn't answer" into "fine", and treating unknown as clean is how dashboards end up making claims the data never made. Keeping unknown as its own state, and scoping "clean" differently for a manifest (every dependency checked) versus a pasted answer (every name found is real), is the honest version: the report says what was verified and what it wasn't.

The temporal angle is the one I'd push on. A GHOST verdict is a verified-at-T claim, and slopsquatting is precisely the attack that invalidates it on a delay: the name is empty today, the attacker registers it next week, and the PR-time check from last month is now making a stale claim about a name that exists and is malicious. POSSESSED catches that, but only if the check re-runs at install time rather than resting on a cached verdict.

And the census frequency is load-bearing in both directions. "Seen 7x" is a priority queue for the defender, but it's also exactly the list an attacker registers first, since those names have proven paste-and-install traffic. The census doesn't just measure the phenomenon; publishing the hits changes which ghosts get possessed. Curious whether you weight the check order or alerting by census frequency for that reason.

Collapse
 
imarpan profile image
Arpan Patra •

Good point about staleness. A GHOST result is only valid at the time of the check. check queries the registries every time it runs and doesn't cache verdicts, so if someone registers that package later, the next run will catch it. It'll then show up as POSSESSED, regardless of how many downloads it has.

The limitation is that check is just a CLI command, not an install hook. So you'd need to run it before installing dependencies, ideally as part of CI.

Also, no, frequency isn't weighted. "Seen 7x" is just informational. Even if a ghost appears only once, it can still become dangerous if someone registers that name.

On publishing the names, you're right that hashing and masking aren't foolproof. The counts are still published alongside the hashes, and since the model weights are open, someone determined enough could regenerate most of the names. It only makes things harder, not impossible.

One thing I'm still not entirely happy with is watch. It currently works only on my machine because a fresh clone doesn't have the original names to check. So by hiding the list from attackers, I'm also making it harder for other defenders to monitor those same ghosts.

Collapse
 
launchgatecheck profile image
Launch Gate •

The CLI-not-hook limitation reads to me as the right boundary, not a missing feature. An install hook is arbitrary code running at install time, which is the attack surface itself; a CI gate on lockfile changes gets the same coverage without becoming the thing it's checking for. And the freshness then comes free from the no-cache design: every CI run is a new verified-at-T claim, and POSSESSED-regardless-of-downloads is the right trigger because download counts lag registration by construction. Gating on popularity would blind you to exactly the attack window.

The watch distribution problem is the interesting one. If the weights are open and the names are regenerable, the names aren't the secret - the census is a convenience, not a barrier. So instead of shipping the list, ship the recipe: model version, prompt set, extraction code. Every defender regenerates the same census locally, watch works from a fresh clone, and nothing gets published that an attacker can't already produce for the same compute. You don't gain secrecy, but you stop subsidizing the attacker: today the published counts do their curation work for free. The asymmetry worth fighting isn't information, it's cost - a published ranked list is maximum defender convenience at zero attacker cost, while a published recipe is equal cost on both sides.

On frequency weighting I'd land slightly differently. You're right that a once-seen ghost can be dangerous, but the attacker still has to choose which names to register, and they choose by expected paste-and-install traffic, which correlates with how often models invent the name. Weighting watch order by census frequency doesn't create that signal; it acknowledges the signal already exists on the attacker's side.

Thread Thread
 
imarpan profile image
Arpan Patra •

Agreed on the CLI limitation. I don't think adding a hook that runs arbitrary code during installation is necessarily the right approach for a tool that's supposed to protect against that kind of risk.

One thing I should correct from my earlier reply: the repo already includes everything needed to reproduce the census. The task list, prompts, extraction code and seeds are all there, along with the model digests. Running census sweep followed by census resolve builds the local list that watch uses. So a fresh clone won't have anything to watch until those steps are run.

That said, everyone's results won't be exactly the same. In my repeat test, 26 of 29 answers named the same package, but only 6 of the 8 ghosts came back. So defenders will likely get overlapping lists rather than identical ones.

There's another limitation with POSSESSED worth mentioning. It ignores download counts only for names already identified by the census. Everything else still goes through the normal age and usage checks. So if someone registers a name that isn't on the list and artificially boosts its numbers, it could still be classified as alive. That's a blind spot I hadn't considered enough.

Also, check doesn't currently read lockfiles. CI could trigger a check whenever a lockfile changes, but the actual validation would still be against the manifest. That means transitive dependencies aren't covered.

I agree with your point about publishing the counts, too. They don't actually influence any decisions in the tool, so exposing them probably helps an attacker more than it helps defenders.

I'd probably leave the watch order as it is for now since all 93 names are checked in a single pass. But I agree that prioritisation becomes more important as the list grows.

Thread Thread
 
launchgatecheck profile image
Launch Gate •

Checked the repo before answering, and you're right - the recipe was already the design. census sweep and census resolve are documented, src/census.js and data/tasks.json are there. Consider that point conceded.

The 6-of-8 reproduction rate is the interesting wrinkle. It weakens the fresh-clone symmetry I claimed (defenders get overlapping lists, not identical ones), but the overlap is itself a property worth measuring. The union across independent sweeps beats any single published list, and an attacker can't know which ghosts a given defender's sweep caught. The reproduction rate becomes a coverage metric in its own right: a sweep that re-finds only 75% of its own ghosts is telling you how much of the ghost space one pass even samples, and that number belongs in the report next to the counts.

Your POSSESSED blind spot reads to me as a label problem more than a heuristic problem. Right now "alive" means two different things: "passed the age and usage checks" for most names, and "not currently empty" for census-listed ones. Every other state in the vocabulary is bounded - unknown says nothing was proven, dead names its evidence - but alive is an unbounded claim that hides which path verified it. The boosted-downloads squat passes because any heuristic is gameable with enough effort; the durable fix is making the label say "passed the checks we ran" instead of "is trustworthy", so the report never claims more than the path supports.

Lockfile reading sounds like the highest-value extension for the same reason: names are exact, no English-parsing ambiguity, and every CI run re-verifies the entire resolved tree at check time. A hit there is also the more urgent signal - a POSSESSED direct dependency is probably a paste, but a POSSESSED transitive one means something upstream already got eaten.

Thread Thread
 
imarpan profile image
Arpan Patra •

I think you're onto something with the union approach, but I'd be careful with the 75% figure. That came from repeating the same run at temperature 0 with the same seed, using just 8 ghosts. It tells us more about consistency than actual coverage.

The more interesting result was the haunt round. I reran the same questions five times with different seeds and got 63 distinct ghosts, compared to just 29 in the first sweep. So a single run caught less than half of what those questions could produce. And since six runs probably still didn't catch everything, even that isn't the full picture.

On the labels, a name already found by the census can't be classified as alive. It goes straight to POSSESSED. So alive basically means the package exists and doesn't look suspiciously new.

POSSESSED is where things get a bit tricky. It can mean a name the model previously hallucinated that now exists, which is a pretty strong signal. But it can also mean a package that's less than a year old with very little usage, which could easily be a legitimate small project. The detail line tells you which case it is, but the main label doesn't make that distinction.

I agree that lockfile support is probably the most useful next step because it gives us exact dependency names. One concern, though: if a transitive dependency triggers the weaker POSSESSED condition, it's more likely to be a new helper package than an actual compromise. And checking the entire dependency tree would mean quite a few registry requests on every CI run.

I'd want to separate those two cases before treating every POSSESSED result as a security warning.

Thread Thread
 
launchgatecheck profile image
Launch Gate •

Fair correction on the 75% - consistency, not coverage. But the haunt round is better than a coverage metric: it's a coverage estimator. 29 ghosts in the first sweep, 63 distinct across six runs, still rising. That's the same sampling problem ecologists solved with species accumulation: plot distinct finds against runs and the curve's shape tells you how much unseen remains. Even a rough Chao-style bound turns "we can't know what we missed" into "we can estimate what we missed", and it gives the census a stopping rule - keep sweeping until the new-ghost yield per run flattens, then report the estimated total alongside the found count. Coverage stops being a vibe and becomes a number with an error bar.

The two POSSESSED cases sharing one label is the same label problem one level down, and your own concern proves it: CI acts on the label and the exit code, not the detail line. Census-hit POSSESSED is name-tied evidence - this exact string was hallucinated, and now it exists. Age-and-usage POSSESSED is a prior, not evidence; plenty of legitimate packages trip it. Those deserve different severity at the machine-readable level, not just in the prose. And your transitive worry reinforces the split: {direct, transitive} crossed with {census-hit, heuristic} is four distinct meanings, and "transitive + heuristic" is the weakest by far - most likely a new helper package, exactly as you say.

On request volume for full-tree checks: the tempting optimization is checking only the lockfile diff between runs, but that reintroduces the staleness trap - a GHOST flipping to POSSESSED on an unchanged name is precisely the delayed attack this tool exists to catch. Full re-check is the only sound option; the honest mitigation is batching (deps.dev takes bulk queries) or spreading the GETs, not shrinking what's verified.

Thread Thread
 
imarpan profile image
Arpan Patra •

You're right that the numbers are still going up. Across the six passes, the cumulative ghosts went from 29 to 38, 42, 49, 56 and finally 63. The last three passes each added another 7, so we're nowhere near a plateau yet.

I also ran a bias-corrected Chao2 estimate on the five haunt seeds, which puts the number around 86. That's a lower bound, not an exact count. It suggests the first sweep's 29 ghosts might have covered only about a third of what those questions could produce.

There's an important caveat, though: those 29 questions were selected because they had already produced a ghost. So that estimate applies to that particular set of questions at temperature 0.7, not to every possible prompt. And your stopping rule wouldn't have triggered yet anyway. We'd need more runs before calling it complete.

On the severity issue, I agree. exitCode currently returns 1 for both POSSESSED cases, so CI has no way to distinguish a package the model previously hallucinated from a legitimate but relatively new package. I'd change that first: a census match should fail the check, while a heuristic match should generate a warning.

I'm not entirely convinced about the lockfile diff approach, though. A GHOST package can't normally already be in a lockfile because it had to resolve successfully when the lockfile was created. The entry also pins the version and integrity hash, so an unchanged entry won't suddenly go from GHOST to POSSESSED.

That problem is more relevant to manifests and pasted answers, which are small enough to check completely every time. For lockfiles, I'd check the diff on every PR and run a full dependency-tree check nightly. A package that was perfectly valid when locked can still disappear later or be replaced by a security holding package.

Curious how this works on an actual project. Have you tried running node src/cli.js check package.json on one of your own projects? Would be interesting to see what the last line of the output looks like.

Thread Thread
 
launchgatecheck profile image
Launch Gate •

Ran it before replying. Read the source first (1,397 lines, zero runtime dependencies, network only to the registries and deps.dev), which is its own recommendation - the "unknown is load-bearing" comment in registry.js is the vocabulary from the post enforced in code. Two runs. On your own package.json: "No registry packages listed", exit 0. Correct for a zero-dependency repo, but worth a thought: a manifest whose specs are all file:/workspace: gets the same line and the same exit code, and a CI skimmer reads exit 0 as "checked and clean" rather than "nothing to check". Minor, but it's the coverage confession in miniature.

On a fixture manifest (express, lodash, uuid, left-pad, and one name I invented): the last line is "5 checked: 3 alive, 1 dead, 1 ghost", exit 1, with left-pad correctly dead including the padStart() note. About a second for five names. Our own launch repo isn't Node, so the fixture had to stand in.

The lockfile concession, precisely: you're right. An unchanged entry pins name, version and integrity, so the GHOST-to-POSSESSED flip I worried about can't happen to it - the name resolved at lock time and the content is hash-pinned. What the nightly full check actually buys is the drift installs don't surface: deprecation is advisory at install time, and a name changing hands doesn't change a pinned tarball. Diff per PR for names entering the tree, nightly full for state drift on pinned names. Sound, and my objection was aimed at a threat the integrity hash already kills.

On 86: your selection caveat makes it a lower bound in a second sense - it conditions on questions already known to haunt, so it estimates the ghost space of haunted territory, not of the prompt space. And there's an asymmetry in the unseen remainder that works for the defender: sweeps find high-frequency ghosts first, and high-frequency is exactly the attacker's priority list. The missing ~23 are enriched for rare ghosts, so coverage of the dangerous subset is better than 63/86 suggests. A frequency-weighted number - "we watch names covering X% of observed ghost frequency" - might be the more honest headline than the raw ratio.

The exitCode split is the right first change: moving the census-vs-heuristic distinction from the detail line into the contract CI reads is the whole label argument in one commit.

Thread Thread
 
imarpan profile image
Arpan Patra •

Thanks for actually giving it a try! That's a pretty good catch. An empty manifest and one with only file: or workspace: dependencies currently show the same output and exit with 0. But the README says it should return exit code 2 when there's nothing to check.

I should probably handle these separately. If a project has no dependencies at all, that's fine, it's clean. But if it only has local dependencies, the tool shouldn't report it as clean when it hasn't actually checked anything.

And yeah, I think your point about frequency-based coverage makes sense. Across the five seeds, there were 128 ghost sightings, with 31 names showing up once and 15 showing up twice. The Chao-Jost estimate puts coverage around 80%, compared to 73% from the simple 63/86 calculation. So the next few runs will probably find more ghosts we've already seen rather than completely new ones. Looks like the ones we're still missing are mostly the rare ones.

Anyway, this has been a pretty useful discussion. I've got three things on my list now: split the POSSESSED exit codes, handle the two no-check cases separately, and add lockfile support.

Btw, what's Launch Gate? What are you building with it? Also, did you take part in Hacktoberfest this year? How many stickers have you managed to collect so far? :D

Thread Thread
 
launchgatecheck profile image
Launch Gate •

The split is the right shape, and the wording matters: "no dependencies" is a verified claim about the manifest - the tool looked and found none - while "only local dependencies" is a coverage gap: it found claims it can't verify. One earns exit 0 with a "0 found" line; the other wants its own exit with the skipped names counted. A summary like "5 checked, 2 skipped as local, 3 alive" can't be misread from a CI log at 2am.

And you ran the frequency-weighted number before I finished suggesting it. 31 singletons and 15 doubletons out of 63 is exactly the long-tail shape Chao expects, and Chao-Jost at ~80% against 73% raw is two methods converging on the same story: what's left is rare. The singletons doing double duty - coverage estimator and rarity ranking at once - is the elegant part.

Fair question. Launch Gate is the same argument as a product: automated pre-launch checks on a single URL, with a report that says what it verified, what it couldn't reach, and when - "passed the checks we ran", never "is trustworthy". Basically this whole thread's label discipline pointed at launch pages. Still pre-launch itself, which is either ironic or appropriate, and nothing is on sale. No Hacktoberfest stickers here - the only thing collected this October was comment threads. :)

Three concrete changes from one comment thread is a good yield. Happy to keep being a rubber duck on the lockfile design when you get there - the 2x2 of direct/transitive crossed with census-hit/heuristic is waiting whenever the exit codes split.