Skip to content

feat(fetch): report a dependency pinned to an exact version rather than leaving it unknown - #120

Open
justin13888 wants to merge 7 commits into
fix/96-unknown-graph-versionfrom
feat/107-exact-pin-version
Open

feat(fetch): report a dependency pinned to an exact version rather than leaving it unknown#120
justin13888 wants to merge 7 commits into
fix/96-unknown-graph-versionfrom
feat/107-exact-pin-version

Conversation

@justin13888

@justin13888 justin13888 commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Closes #107.

A manifest-only dependency graph reported every dependency's version as
unknown, including the ones the manifest had already resolved. serde = "=1.0.200"
admits exactly one release; so does <PackageReference Version="[1.2.3]" />, a
bare Gradle 4.12.0, and a PEP 440 ==2.28.1. dependable tree said unknown
for all of them.

What changed

dependable_core::exact_pin(constraint, ecosystem) -> Option<&str> (new,
crates/dependable-core/src/semver/pin.rs) — pure, IO-free. It decides by
translation, not by a table of spellings: the constraint goes through the same
to_semver_constraint every version check already uses, and it is a pin exactly
when that translation is one = comparator at full precision. No reading is
invented that an ecosystem's own translator does not already make, so tree and
check cannot disagree about the same line of the same file.

Two properties the tests pin down:

  • What comes back is a slice of the declared constraint, never the
    translation. to_semver_constraint pads and rewrites to produce a string the
    comparison engine accepts (1.01.0.0, 6.4.4.Final6.4.4, NuGet
    1.0.0.41.0.0) and none of those names a published artifact.
  • A pin whose spelling semver::Version cannot parse as written is not
    reported: 1.2.3.4, 6.4.4.Final, and equally a two-segment 4.12, a NuGet
    [1.0], a PEP 440 ==0.20. Each is exact beyond doubt, but every consumer
    compares with semver::Version on the declared string, and one that fails to
    parse there is treated as no version: dependable-tui's data.rs::lookup
    falls back to the newest compatible release and renders a green ok. Shipping a
    known false-green inside the branch whose purpose is removing false greens (fix(tui): a dependency whose version was never read renders as up to date #96)
    is the outcome that branch exists to prevent. The first push of this branch
    did exactly that
    — see "Repaired after review" below.

Wiring (crates/dependable-fetch/src/tree.rs, private) — both manifest-only
assemblers now carry the pin onto the node: direct_graph (the non-Cargo path)
and shallow_graph (Cargo with no Cargo.lock). Candidacy is gated on the
existing Item::is_checkable(), so git and path references, and an unresolved
Inherited entry, stay unknown without a second rule. Where two items share a
name and do not yield the same pin, the node reports None — ambiguity is not a
resolution, and first-wins would make the answer depend on source order.

Known limitation, stated plainly

This does not close the issue's own headline example. A *.csproj
<PackageReference Version="1.2.3" /> still reports unknown, because
nuget_constraint_to_semver reads a bare NuGet version as >=1.2.3 — an
inclusive minimum, not a pin (crates/dependable-core/src/semver/nuget.rs,
comment: "A bare version is an inclusive minimum in NuGet"). maven.rs makes
the opposite call for the identical shape ("A bare version is read as exact, as
Hex's is"
).

Filed separately as #113: that
asymmetry is a real defect — because >= makes latest_compatible equal
latest_available and there is no NuGet lockfile reader in the tree, check on a
bare-Version *.csproj reports UpToDate whatever NuGet publishes. Fixing it
changes check, list, fix, report, and SARIF output for every C# user and
would make --fix start offering rewrites it does not offer today, which is why
it was not folded into a PR about tree nodes.

Base

Based on fix/96-unknown-graph-version (PR #104), not master.

Decisions taken

1. Deliverable boundary — which manifest-only assemblers gain the pin

  • Taken: BOTH direct_graph (the non-Cargo path) and shallow_graph (the Cargo, no-lockfile path).
  • Rejected: direct_graph alone — run_tree calls build_workspace_graph and NEVER reaches direct_graph, so that option changes nothing a dependable tree user can observe; the whole delivery would be invisible from the CLI. The Cargo case is also the one needing no ecosystem judgement at all, since =1.2.3 translates to a single full-precision Op::Exact.
  • Rejected: the core helper alone, wiring deferred — it closes nothing a user can see and adds a public API with no in-tree caller.
  • Reverses: pass None for external packages in shallow_graph and delete its test case.

2. Which declared constraints count as a pin

  • Taken: translation-only. A pin is a constraint whose existing to_semver_constraint(constraint, ecosystem) output parses as a VersionReq with exactly ONE comparator, Op::Exact, with both minor and patch present. No new per-ecosystem judgement is invented.
  • Rejected: adding an explicit clause making a bare NuGet Version="1.2.3" count — the asymmetry was verified directly: nuget.rs maps a bare version to >={v} with the in-code comment "A bare version is an inclusive minimum in NuGet", while maven.rs maps a bare version to ={v} with the comment "A bare version is read as exact, as Hex's is." Admitting NuGet bare here would make the same string mean two different things in two modules, and would make tree say "update" for a package check reports UpToDate on the same file.
  • Rejected: a per-ManifestKind allowlist (csproj + Gradle only) — it would leave Python ==2.28.1 and Cargo =1.2.3, unambiguous pins by anyone's reading, still reporting unknown, and it makes the rule about FILES where the issue frames it about CONSTRAINTS.
  • Rejected: changing nuget_constraint_to_semver so a bare version becomes = — that is the most correct long-term fix and it repairs a real defect (issue fix(core): a bare NuGet Version reports UpToDate whatever the registry publishes #113), but it changes check, fix, report and SARIF output for every C# user inside a PR about tree nodes, and would make --fix start offering rewrites it does not offer today.
  • Reverses: add an Ecosystem::CSharp clause to the helper admitting a bare fully-specified literal.
  • COST, disclosed rather than hidden: this does NOT close the issue's own headline example. A *.csproj <PackageReference Version="1.2.3" /> still reports unknown, because NuGet's translation makes it >=1.2.3. See the section above and issue fix(core): a bare NuGet Version reports UpToDate whatever the registry publishes #113.

3. A pin whose spelling the comparison engine cannot parse

  • Taken: the helper reports a pin ONLY when its literal parses as a semver::Version exactly as the manifest wrote it, with no normalization. 1.2.3.4, 6.4.4.Final, 4.12, [1.0] and ==0.20 therefore stay unknown; 32.1.3-jre, 4.12.0, 13.0.1, =1.0.200 and ==2.28.1 are reportable.
  • Corrected in repair: this originally read "parses after normalize_version", and the code matched the words. That was wrong, and it is the HIGH finding this branch was repaired for. See "Repaired after review".
  • Rejected: reporting every admitted pin and filing the hazard — crates/dependable-tui/src/data.rs::lookup compares raw natives with check_version("*", &versions, Some(version)); when Version::parse fails, locked is None, current falls back to the newest compatible release, and the row renders a GREEN ok. Admitting Maven pins like 6.4.4.Final puts non-semver strings in front of that for the first time. Shipping a known false-green inside the branch whose whole purpose is removing false greens is the one outcome issue fix(tui): a dependency whose version was never read renders as up to date #96 exists to prevent.
  • Rejected: fixing data.rs::lookup to translate — the machinery it needs (to_semver_versions, in_native_versions, native_for) is private in dependable-fetch, so this means either duplicating a rule the repository keeps deliberately single, or a public-API decision larger than this issue. Filed as fix(tui): the package lookup compares untranslated versions on both sides, so a non-semver ecosystem is misread #148.
  • Why the guard belongs in exact_pin and not in data.rs::lookup: exact_pin is the component making the claim, so it is the component that must prove it. A Node::version is read by the CLI renderers, the JSON and DOT emitters, the OSV query, and the TUI alike; only the helper can guarantee the string is safe for all of them. Repairing one consumer leaves every other present and future consumer of an unparseable Node::version broken — which is exactly what fix(tui): the package lookup compares untranslated versions on both sides, so a non-semver ecosystem is misread #148 documents, and why it is filed rather than folded in here.
  • Rejected: a new public Checker evaluation method the TUI calls — adds public API to dependable-fetch for an issue about a graph node and drags a third crate into the change.
  • Reverses: drop the parse predicate from the helper and instead translate in data.rs::lookup.

4. Whether a declared pin is distinguishable from a lockfile resolution

  • Taken: no distinction. A version is a version; the difference is documented in the Node::version doc comment and README.
  • Rejected: deriving it in the renderer from GraphSource — that is per-GRAPH, not per-node, so it cannot express the mixed graph with_root already builds (a lockfile graph whose root node's version came from the manifest), and it reaches only the CLI, not the TUI the issue names.
  • Rejected: carrying a provenance field on Node via LockedPackage — it spans all four crates and either changes LockedPackage::new's arity, touching every lockfile parser, or adds a second constructor a future parser can pick wrongly.
  • Evidence for taking "no distinction": the graph ALREADY declines to distinguish for the root node and for workspace members, whose versions have been declared rather than resolved since PR fix(tui): report a dependency with no known version as unknown #104. A new distinction here would apply half a rule.
  • Reverses: add a VersionSource-style field to Node and LockedPackage, fed at each construction site.

5. (taken in-lane, then re-picked in repair) Witnesses for "report the declared spelling, never the translation"

  • Taken: keep the "report the native spelling" rule and its 32.1.3-jre row, and prove the rule with rows where the translation genuinely differs and the literal parses raw: a Maven release alias (1.0.0-RELEASE=1.0.0), a PEP 440 local segment (==1.2.3+local=1.2.3), and a PEP 440 pre-release respelling (==1.2.3-rc1=1.2.3-rc.1).
  • Corrected in repair: the first push proved the rule with 1.0 (Jvm), [1.0] (CSharp) and ==0.20 (Python) — all three padding cases, i.e. inputs the rule now rejects. A test proving a rule only on inputs the rule excludes proves nothing.
  • Also corrected: the comment claiming maven_to_semver("32.1.3-jre") == "32.1.3" is wrong in the other direction too. maven.rs keeps an unrecognized qualifier as a semver pre-release, so the translation is =32.1.3-jre. What makes the declared spelling the right answer for guava is that Maven Central publishes 32.1.3-jre and 32.1.3-android and no bare 32.1.3 — not that the translation truncates it. Verified by a failing assertion, not by reading.
  • Rejected: deleting the property test as unprovable — the hazard is real, it just has different witnesses.

6. (taken in-lane) Node::version's doc comment

  • Taken: restated the two sentences of Node::version's doc comment in crates/dependable-core/src/graph.rs that now say the opposite of what the code does. Documentation only — no field, no signature, no behaviour.
  • Note: the plan's MANIFEST both names graph.rs under "Do NOT touch" and, under "Signature changes", requires this exact restatement. The specific instruction was followed over the general one, on the reasoning that "Do NOT touch" is aimed at the structural change decision 4 rejected, and that leaving a public API's doc comment asserting the opposite of its behaviour is a worse outcome than a doc-only edit. Flagged here so a reviewer can reverse it in one hunk if the general rule was meant to win.

Repaired after review

Three findings from review of the first push. All three are fixed in this branch;
the commits are fix(core): prove a pin on the string its consumers actually parse and the two test: commits after it.

HIGH — the guard modelled a padded trip; its consumers make an unpadded one

exact_pin's last line was:

// And the literal has to survive the trip every consumer makes with it.
Version::parse(&normalize_version(literal)).ok()?;

normalize_version pads on dot count: 0 dots → {core}.0.0, 1 dot → {core}.0,
2+ unchanged. No consumer pads. git grep normalize_version over crates/*/src
returns no call site on the consumer path at all — declared_pin puts the literal
straight into a LockedPackage version, DependencyGraph::from_resolved clones
it, the TUI row clones it, App::selected_key reads it with
row.version.as_deref()?, model::key does a bare to_owned(), and
data.rs::lookup hands the raw string to check_version("*", &versions, Some(version)). The comment asserted the opposite of what the line did.

Reproduction. gradle/libs.versions.toml:

[versions]
junit = "4.12"
[libraries]
junit = { module = "junit:junit", version.ref = "junit" }

Maven Central publishes 4.13.2, 4.13.1, 4.13, 4.12, 4.11 for
junit:junit. exact_pin("4.12", Jvm) translated to =4.12.0 — one comparator,
Op::Exact, minor and patch present — and the guard padded 4.12 to 4.12.0,
which parses. So it returned Some("4.12"). Then in check_version("*", […], Some("4.12")): only 4.13.2 and 4.13.1 parse, latest_available = 4.13.2,
locked = None because Version::parse("4.12") fails, current falls back to
latest_compatible = 4.13.2, the arm Some(cur) if *cur >= latest_available is
taken, and ui/tree.rs renders a green ok for a dependency three releases
and nine years behind. Same walk for PEP 440 ==0.20 and NuGet [1.0].

This is verbatim the failure class of #96 — "a dependency whose version was
never read renders as up to date" — reintroduced inside the branch stacked on
#96's own fix.

The false green appears whenever the registry publishes at least one strict-semver
stable release. Two conditions suppress it, neither common: if no published
version parses as strict semver the status is Error and no badge renders; if the
newest parseable release is a pre-release the status is UpdateAvailable.

Why nothing caught it, which is the interesting part. The two consequences of
carrying an unparseable version suppress each other. ui/tree.rs renders VULN n
ahead of the status badge, so where OSV matches on the raw string the
vulnerability badge hides the false ok; where OSV does not match — the same
raw string missing an advisory, a silent false negative on vulnerabilities — there
is no VULN badge and the green ok is what shows. Each failure mode is only
visible in the state where the other is absent.

Fix. Parse the literal exactly as written, and correct the comment. Strictly a
narrowing, with no user-visible regression: every case it drops reported unknown
before this branch, and it is what #107's own criterion asks for — "the declared
string parses cleanly as an exact semver::Version".

What exact_pin returns, precisely:

Now Some(…) Now None (was Some before the repair)
Cargo =1.2.3, = 1.2.3, =1.2.3-alpha.1 Maven/Gradle 4.12, 1.0 — any two-segment bare version
npm =1.3.0 NuGet [1.0] — any single-version interval below three segments
PEP 440 ==2.28.1, ==1.2.3+local, ==1.2.3-rc1 PEP 440 ==0.20 — any two-segment ==
NuGet [1.2.3], [1.2.3-beta.1] (and, unchanged from before: 1.2.3.4, 6.4.4.Final, [1.2.3.4], every range, union, wildcard, dist-tag and MSBuild property)
Maven/Gradle 4.12.0, 1.9.24, 32.1.3-jre, 1.0.0-RELEASE
Hex 3.10.3, == 3.10.3

The rule in one line: the ecosystem's own translator must yield a single
full-precision Op::Exact comparator, and the declared literal must itself parse
as a semver::Version.
Neither condition implies the other.

MEDIUM — the partial-precision and NuGet-pin paths had no test above pin.rs

Every graph-level assertion used a three-segment version; the committed Gradle
catalog has no two-segment entry and the committed csproj has no [x.y.z]
interval, so the entire NuGet accept path was untested outside pin.rs. And this
PR's own claim that "the dependable-tui suite passing untouched is itself a
required outcome" was not supported: that suite's unknown tests build their
graphs by hand with DependencyGraph::from_resolved and never call
build_project_graph, so they are structurally incapable of reaching exact_pin.

Three tests close it:

  • a_two_segment_catalog_version_is_exact_and_still_resolves_nothing — a catalog
    holding 4.12 beside 4.12.0; the first resolves nothing, the second resolves.
  • a_nuget_single_version_interval_resolves_and_a_two_segment_one_does_not — a
    csproj holding [1.2.3], [1.0] and [1.0,2.0).
  • crates/dependable-tui/tests/pinned_lookup.rs (new file) — starts from a real
    Gradle catalog on disk, builds the project the way data::discover_projects
    does, walks the row through App::selected_key, and makes the same
    check_version call data::lookup makes with the key that comes out.
    a_pin_the_comparison_engine_cannot_read_is_never_looked_up is the test that
    would have caught the HIGH finding: it asserts junit:junit at 4.12 carries
    no version and spawns no lookup, and then runs the call the pipeline would
    have made, showing it answers UpToDate against a registry offering 4.13.2.

All three were confirmed to fail against the un-fixed guard before being kept.

LOW — the Maven union defence was untested as an expectation

[1.0],[2.0] was rejected only because pin_literal's character screen finds
,/[/]. maven::interval_range uses rfind and deliberately keeps the
last interval, so maven_constraint_to_semver("[1.0],[2.0]") is =2.0.0
one comparator, Op::Exact, full precision — which the comparator check would
pass. If pin_literal is ever relaxed, the helper would start reporting the last
interval of a union as a resolution with the suite still green. Union rows
asserting None are now in the table: [1.0],[2.0] for both Jvm and CSharp,
and (,1.0],[1.2,) for Jvm.

Issues filed out of this review

Validation

Every command prefixed env -u FORCE_COLOR -u COLORTERM, since this repository has
tests sensitive to an ambient FORCE_COLOR (#100).

Command Outcome
cargo test -p dependable-core semver::pin ok. 3 passed; 0 failed
cargo test -p dependable-core semver ok. 60 passed; 0 failed
cargo test -p dependable-fetch --test project_graph ok. 17 passed; 0 failed
cargo test -p dependable-fetch --test tree ok. 10 passed; 0 failed
cargo test -p dependable-tui ok. 71 + 32 + 2 + 44 passed; 0 failed
mise run test full workspace green; 0 failures in any binary
mise run fmt:check exit 0
mise run lint (clippy -D warnings) exit 0
convco check origin/fix/96-unknown-graph-version..HEAD no errors in 7 commits

Some pin.rs table rows changed verdict, which is the point of the repair rather
than a regression: ("==0.20", Python) went Some("0.20")None, and the
three witnesses of reports_the_declared_spelling_and_never_the_translation
("1.0" Jvm, "[1.0]" CSharp, "==0.20" Python) were replaced rather than
loosened. No test was weakened, skipped, or narrowed to make anything pass.

The earlier claim here — that "the dependable-tui suite passing untouched is
itself a required outcome" — was withdrawn: that suite could not reach this
change at all. It now has a test that can (tests/pinned_lookup.rs), which is
what actually protects decision 3.

A manifest usually declares a range, but some declarations name a single
release outright — Cargo's `=1.2.3`, PEP 440's `==2.28.1`, NuGet's
`[1.2.3]`, a bare Maven or Hex version. A caller holding only a manifest
had no way to tell those apart from a range, so it had to report every
dependency's version as unknown even where the manifest had already
answered.

`exact_pin` decides by translation rather than by a table of spellings:
the constraint goes through the same `to_semver_constraint` every version
check already uses, and it is a pin exactly when that translation is one
`=` comparator at full precision. So no reading is invented here that an
ecosystem's own translator does not already make, and a bare `1.2.3`
comes back as a pin for Maven and Hex and as `None` for Cargo, npm,
Python, and NuGet — which is what those translators already say.

Two properties the tests pin down:

- What comes back is a slice of the *declared* constraint, never the
  translation. `to_semver_constraint` pads and rewrites to make a string
  the comparison engine accepts (`1.0` → `1.0.0`, `6.4.4.Final` →
  `6.4.4`, `1.0.0.4` → `1.0.0`), and none of those names a published
  artifact. A version shown to a user has to be one the registry has.
- A pin whose spelling `semver::Version` cannot parse (`1.2.3.4`,
  `6.4.4.Final`) is not reported at all. It is exact beyond doubt, but
  every consumer compares with `semver::Version`, and a version that
  fails to parse there is treated as no version — which surfaces as a
  false "up to date" rather than as an honest unknown.
A manifest-only graph reported every dependency's version as unknown,
including the ones the manifest had already settled. `serde = "=1.0.200"`
admits exactly one release and nothing else; so does a NuGet `[1.2.3]`
and a bare Gradle `4.12.0`. Calling those unknown understated what had
been read, and did it in the same graph that already reports a workspace
member's declared version for exactly the same reason.

The constraint was being thrown away one line before the node was built:
`build_project_graph` mapped the parsed items to bare names, and
`shallow_graph` passed `None` for every external package. Both now carry
the pin `declared_pin` reads off the declaration.

Candidacy is `Item::is_checkable()`, the existing predicate for "there is
a version string here worth asking a registry about". That keeps a git or
path reference, and an `Inherited` entry no root has supplied a
constraint for, unknown — without a second rule that could drift from the
first. An inherited entry the root *did* supply is resolved before the
graph is assembled, so a centrally pinned crate reports the root's pin.

Where two declarations of one name do not agree on a pin, the node
reports nothing. They collapse into one node, so taking the first would
make the answer depend on the order the manifest lists them in, or the
order the directory walk found the members in. That is not a resolution.

`a_manifest_only_graph_leaves_every_dependency_version_unknown` asserted
the behaviour this changes; it is rewritten as a per-node expectation
naming the exact spelling of each version rather than deleted, because
the spelling is the part worth protecting: guava must report
`32.1.3-jre`, and Maven Central publishes no `32.1.3` at all.

What this does not do: a `*.csproj` `<PackageReference Version="1.2.3" />`
still reports unknown. NuGet's translator reads a bare version as an
inclusive minimum, not a pin, so admitting it here would make `tree`
claim a resolution for a line `check` reports as satisfied by every later
release. That reading is itself a defect and is filed as #113.
`Node::version` and the README's `tree` section both stated that a
dependency in a manifest-only graph is always `null`, "because a manifest
declares a constraint rather than a resolution". That is now the common
case rather than the rule: a constraint admitting exactly one release has
resolved it.

Both say so, and both say the two things a reader needs in order to
predict the output: the version reported is the one the manifest spells
rather than a normalized form of it, and whether a bare version is a pin
is the ecosystem's call rather than the string's shape — Cargo, npm and
Python read `1.2.3` as a range, NuGet as a lower bound, Maven and Hex as
exact.
`exact_pin`'s last guard checked the literal after `normalize_version`
padded it, and nothing downstream pads. `declared_pin` puts the literal
straight into a `Node::version`, and every reader of that field — the CLI
renderers, the JSON and DOT emitters, the OSV query, and the TUI's
lookup — parses it exactly as written. So the guard proved a claim about
a string no consumer ever constructs, and the comment above it said the
opposite of what the line did.

A Gradle catalog pinning `junit:junit` at `4.12` therefore reached
`check_version` as a raw `4.12`, which `Version::parse` rejects; that is
read as no current version at all, the comparison falls back to the
newest release, and the row rendered a green `ok` for a dependency three
releases and nine years behind. That is issue #96's failure verbatim,
reintroduced inside the branch stacked on its fix. Nothing caught it
because its two consequences suppress each other: the same raw string
also misses OSV, and `ui/tree.rs` draws the vulnerability badge ahead of
the status badge, so wherever OSV does match, the false `ok` is hidden.

Parse the literal as written instead. It is strictly a narrowing —
`4.12`, `1.0`, `[1.0]` and `==0.20` drop back to `unknown`, which is what
they reported before this branch — and it matches #107's own criterion,
that the declared string parse cleanly as an exact `semver::Version`.
Padding is not an alternative: `4.12.0` is a different artifact from
`4.12` and Maven Central publishes only one of them, so the declared
string is the only one that can be reported and therefore the only one
worth testing.

The three witnesses in the declared-spelling test were all padding cases,
so the test proving that rule proved it only on inputs the rule now
rejects. They are replaced with rewrites the guard still permits: a Maven
release alias, a PEP 440 local segment, and a PEP 440 pre-release
respelling. The usable-version invariant now parses the pin raw as well.
Every graph-level assertion about a manifest pin used a three-segment
version. The committed Gradle catalog contains no two-segment entry and
the committed csproj contains no `[x.y.z]` interval, so the whole NuGet
accept path had no test above `pin.rs` and the partial-precision reject
path had none either — the defect in the guard could not have surfaced
here.

Two cases close that. A catalog holding `4.12` beside `4.12.0` asserts
the first resolves nothing and the second resolves, in one file through
one code path. A csproj holding `[1.2.3]`, `[1.0]` and `[1.0,2.0)`
asserts NuGet's single-version interval is the one spelling in that
ecosystem which pins.

Also corrects the guava note: `maven_to_semver` keeps the `jre` token, so
the translation is not `32.1.3`. What makes the declared spelling the
right answer is that Maven Central publishes `32.1.3-jre` and
`32.1.3-android` and no bare `32.1.3`.
The crate's `unknown` tests build their graphs by hand with
`DependencyGraph::from_resolved`, so none of them calls
`build_project_graph` and none can reach `exact_pin` at all. "The
`dependable-tui` suite passing untouched" was therefore not evidence
about this change.

These start from a real Gradle catalog on disk, build the project the way
`data::discover_projects` does, walk the row through `App::selected_key`,
and then make the same `check_version` call `data::lookup` makes with the
key that comes out — the whole path a version travels from a manifest to
a badge.

The second test is the one that would have caught the guard defect: it
asserts `junit:junit` at `4.12` carries no version and spawns no lookup,
and then runs the call the pipeline *would* have made, showing it answers
`UpToDate` against a registry offering `4.13.2`. That false `ok` is why
the version must never reach there.

`tempfile` joins the crate as a dev-dependency because reaching
`build_project_graph` requires a manifest on disk.
`Node::version` and the README both said a manifest-only dependency
carries a version wherever its constraint named one release. That is half
the rule: the spelling also has to be one this crate can parse as
written, because a `Node::version` no consumer can compare with is read
downstream as no version at all. Name the forms that fall out — `4.12`,
`1.2.3.4`, `6.4.4.Final` — and why.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant