yarn

yarn.lock, the v1 format.

$ ./target/release/stranger scan fixtures/yarn-l.yarn.lock

  yarn-l.yarn.lock         593 packages   (26 direct · 567 transitive)

  ⚠  TRIVIAL                23    (3.9% of third-party)

  ⚠  VERSION DRIFT          82    same package at 2+ versions in one tree

  ·  INSTALL SCRIPTS        — no signal in this format
  ·  UNPINNED               — no signal in this format

  risk 46/100    <ms>    third-party deps used to compute this: 0

The fixtures are three real lockfiles: npm packages ship their own inside their tarballs, so pdf-lib supplies a 4,408-line one and uri-js a 2,558-line one. Neither was written for this reader.

Not YAML, despite reading like it

yarn v1 wrote its own format: entries at column 0, two-space fields under each, and quoting that is optional except where a value would otherwise break the line. yaml.rs is next door and deliberately unused — a YAML parser hands back lodash@^4.17.20, lodash@~4.17.0 as one scalar key that then has to be taken apart anyway.

The edges are specifiers, not versions

This is the one thing that makes yarn different from every other format here, and getting it wrong produces a tree with no edges rather than an error.

A dependencies line names a range:

"@babel/code-frame@^7.0.0":
  version "7.0.0"
  dependencies:
    "@babel/highlight" "^7.0.0"

The entry "@babel/highlight" "^7.0.0" points at is the one whose key list contains the literal specifier @babel/highlight@^7.0.0. Only the key list can answer that: ^7.0.0 never appears in the target's version field, which says 7.0.0. A reader that matched on the resolved version would find nothing, parse every entry successfully, and report a package count with an empty edge set — which reads as "nothing depends on anything" and hands every package an in-degree of zero, the exact shape the co-occurrence rule fires on.

One entry answers to several keys:

"@babel/generator@^7.9.0", "@babel/generator@^7.9.5":
  version "7.9.5"

75 of the 593 entries in yarn-l are written that way, and the file depends on that one through both keys. A reader that took only the first would silently lose every edge arriving through the second.

What it refuses

$ mkdir -p /tmp/berry
$ printf '__metadata:\n  version: 8\n' > /tmp/berry/yarn.lock
$ ./target/release/stranger scan /tmp/berry/yarn.lock
stranger: /tmp/berry/yarn.lock: this is a Yarn Berry (v2+) lockfile; stranger reads the v1 format

Berry is a real YAML document keyed name@npm:range with its own __metadata version counter. Same filename, different format.

An empty file is the other one, and it used to be the worst answer this tool could give:

$ mkdir -p /tmp/empty-yarn
$ printf '' > /tmp/empty-yarn/yarn.lock
$ ./target/release/stranger scan /tmp/empty-yarn/yarn.lock
stranger: /tmp/empty-yarn/yarn.lock: no entries and no `# yarn lockfile v1` header; this is not the lockfile its name claims to be

Every other reader here refuses a file that is not the thing its name claims — Cargo.lock wants a [[package]], go.mod wants a module, poetry.lock wants a metadata.lock-version. This one had no such check, because the v1 header is a comment and a hand-edited or concatenated lockfile can be missing it while its entries are perfectly good. That reasoning holds for a file with entries and quietly fails for a file without any: a truncated or zero-byte yarn.lock read as a clean tree of nothing, risk 0/100, and a --fail-on gate went green over a lockfile nobody had read.

So the header is required only when there is no other evidence. A project with no dependencies really does produce a header and nothing else, and that is still zero packages rather than an error.

Nested blocks

An entry's fields are key value pairs, except for the ones that are a bare name: opening a block indented under it. Six of them turn up:

blockread as
dependencies, optionalDependencies, peerDependenciesgraph edges
engines, os, cpu, dependenciesMeta, peerDependenciesMetaconsumed and dropped

peerDependencies is an edge for the same reason it is one in npm: a peer dep is a real maintainer writing down a real name, which is the evidence the detection rule wants, and an in-edge can only make that rule quieter. Both readers produce Ecosystem::Npm and stranger diff will put one against the other, so a name with in-degree 1 under npm must not have in-degree 0 under yarn — otherwise migrating a project between the two reports findings the migration did not introduce.

engines is the counter-case and the reason the block matters rather than the line: node ">=6" is the same two-token shape as a dependency line and names no package at all.

The bare header itself is the trap. peerDependencies: has no value on its line, so a field scanner that splits every line into key value has nothing to split and reports a syntax error — against a file that has none, and against most of the real yarn v1 lockfiles in existence, since peer dependencies are ordinary.

What the format does not record

The root manifest. Direct dependencies live in package.json, which is not this file, so roots is derived the way poetry's is: an entry nothing else depends on can only have arrived through the root. The derivation is wrong in one direction — a direct dependency something else also needs has an in-edge and drops out of the count.

Install scripts, dev-ness, and workspace membership. None of the three is in a v1 lockfile, so install scripts reports no signal in this format rather than nothing found.

Integrity. Present on all 593 entries of yarn-l and on none of yarn-xs, whose yarn predates the field. Reported as present or absent, never as checked — std has no crypto.