Skip to content

fix(tui): report a dependency with no known version as unknown - #104

Open
justin13888 wants to merge 7 commits into
masterfrom
fix/96-unknown-graph-version
Open

fix(tui): report a dependency with no known version as unknown#104
justin13888 wants to merge 7 commits into
masterfrom
fix/96-unknown-graph-version

Conversation

@justin13888

@justin13888 justin13888 commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Closes #96

Phase 1 verdict: the symptom is not currently reachable. The fix is prophylactic.

I tried to reproduce the reported green ok before changing anything, by driving
App over a graph shaped exactly as direct_graph/shallow_graph synthesize one
(a NodeKind::Registry node with String::new() as its version). Two routes:

  1. Natural selection. Select the version-less serde row. App::selected_key
    returns None, so spawn_lookup is never called and nothing is ever stored in
    app.packages under an empty-version key. The row rendered with a blank
    VERSION, blank AGE, and a blank STATUS; the detail pane said "version not
    resolved".
  2. Forced facts. Inject PackageData::Ready with status: UpToDate and
    latest: 2.0.0 directly under the (Rust, "serde", "") key, bypassing
    selected_key entirely. status_badge's own row.version.is_empty() check
    still blanked the STATUS cell. No ok, no latest version.

age() in ui/tree.rs — the one route without the check — is also inert, because
it only reads app.packages, and no code path ever populates that map under an
empty-version key. So three independent guards, none of them load-bearing on its
own, are what currently hold. A user cannot see the reported green ok on
master today.

What is visible on master is the same dishonesty one layer over, in a public
schema. dependable tree --format json on a Rust project with no Cargo.lock
emitted "version": "" for every node — including the root, whose version the
manifest plainly declares.

The issue's Direction paragraph is the substance either way: the distinction
between "no known version" and "the empty version" was held by three copies of a
convention, and deleting any one of them silently restores the bug. That is what
this changes.

The change

Node::version and LockedPackage::version in dependable-core become
Option<String>. The distinction is now the type, not a sentinel string plus a
convention that every consumer has to remember.

None is produced only where nothing read a version: a shallow Cargo workspace
(no Cargo.lock), a manifest-only project graph (Gradle, pom.xml, *.csproj,
pubspec.yaml), an npm/Bun workspace link, and a project whose manifest declares
no version of its own. Every lockfile parser still yields Some.

Row::version in the TUI follows, so selected_key, status_badge, age, and
the detail pane get the check from the type. age never had it. The detail
pane's resolved version is now threaded into facts_lines/metadata_lines
rather than read back off the row, so the code that formats a version cannot be
reached without one.

The TUI's STATUS column now reports unknown for such a row instead of
leaving the cell blank. A blank cell reads as "nothing to report about this
package"; the truth is that the tool never learned what version is declared.

Breaking changes

Library (dependable-core, re-exported from dependable_fetch::core):

  • Node::version is Option<String> (was String).
  • LockedPackage::version is Option<String> (was String), and
    LockedPackage::new takes Option<String>.

Both types are #[non_exhaustive], so the field change is the breaking part.
The only external consumer named in the repo is an IDE via dependable-fetch.

JSON output schema (dependable tree --format json):

  • A node whose version was never read now serialises as "version": null, where
    it previously serialised as "version": "". Every node of a shallow tree is in
    that state.
  • Judgement call: null, not an omitted key. Every node keeps the same shape,
    so a consumer never has to distinguish an absent key from an absent version.
    Reverses by adding #[serde(skip_serializing_if = "Option::is_none")] to
    NodeDto::version.

The ASCII and DOT renderers are byte-for-byte unchanged — both already printed the
bare name for a node with no version, so output/tree.rs's existing assertions
stand untouched. No test was deleted or weakened.

What now falsifies the bug

Three new tests in crates/dependable-tui/tests/render.rs, driving the real
renderer over TestBackend:

  • a_dependency_with_no_known_version_says_unknown — reads the STATUS cell out of
    its column (not the whole screen) and requires it to be unknown.
  • a_dependency_with_no_known_version_is_never_looked_upselected_key() is
    None.
  • a_freshness_verdict_never_attaches_to_a_version_we_did_not_read — forces
    Ready { status: UpToDate, latest: 2.0.0 } into the store under the empty
    version and requires the cell to still read unknown and 2.0.0 to be absent.

I checked these are failing-first two ways. Replacing the unknown branch with
return None (i.e. master's blank cell) fails the first and third. Removing the
version check from status_badge entirely — the actual bug the issue describes —
renders serde … ok and fails the third with that exact screen in the message.

Plus json_reports_an_unread_version_as_null in output/tree.rs, and two graph-
level tests: a_manifest_only_graph_leaves_every_dependency_version_unknown
(over the sample-kotlin Gradle catalogs) and an assertion inside the existing
falls_back_to_shallow_graph_without_lockfile.

Judgement calls

  • LockedPackage::version follows Node::version. It did not have to: I
    could have kept String there and decoded "" to None inside
    DependencyGraph::from_resolved. I did not, because that relocates the sentinel
    rather than removing it, and because LockedPackage has four synthesizing
    producers of its own (shallow_graph's members and externals, direct_graph,
    with_root) that all mean "unknown". Reverses by restoring String and
    normalising empty-to-None at the one from_resolved boundary.

  • Declined to use parsed.items[..].version_constraint at tree.rs. The
    issue asks whether a declared constraint is a better answer than "unknown". It
    is not, and using it would reintroduce the reported bug: a constraint is not a
    resolved version, and check_version("*", versions, Some(constraint)) parses a
    range like [1.0,2.0) or ^1.2 as a Version, fails, falls back to
    latest_compatible, and answers UpToDate — the same wrong green ok, now
    from data that looks plausible. A constraint would need to be carried as a
    constraint and evaluated as one, which is a larger change than this issue.

  • root_version became Option<&str>. A manifest that declares no version of
    its own (a pom.xml inheriting from <parent>) now leaves the project root's
    version unknown instead of blank, which is the case the issue calls out as
    affecting every dependency in such a file.

  • Detail-pane wording. "version not resolved — no lockfile for this project"
    became "version not resolved — nothing read a version for this". The old clause
    was often false: a pom.xml with a <parent> has no lockfile at all, and
    pubspec.lock is read but records no edges. Why it could not be read is the
    project row's caveat to explain.

  • VERSION column stays blank for an unknown version, rather than repeating
    unknown. STATUS is where the row says it does not know; saying it twice on one
    line adds nothing.

Observed but out of scope

dedupe_workspaces in crates/dependable-tui/src/data.rs fingerprints a project
by its roots' name version. Two distinct projects of the same ecosystem whose
roots share an inferred name and both have unknown versions fingerprint
identically and one is dropped. That is pre-existing, is not what #96 reports,
and is not fixed here — the fingerprint keeps its current behaviour (unknown
renders as a fixed ?). Fixing it properly means folding the manifest path into
the fingerprint.

Validation

env -u FORCE_COLOR -u COLORTERM cargo test --workspace     851 passed, 0 failed, 20 ignored
env -u FORCE_COLOR -u COLORTERM cargo clippy --workspace --all-targets -- -D warnings    clean
cargo fmt --all --check                                    clean
convco check 9b22488..HEAD                                 no errors in 3 commits

The 20 ignored are the network-gated tests that run under mise run test:live.
FORCE_COLOR/COLORTERM are unset because they make owo-colors emit ANSI
inside tests and break output::tree::tests::ascii_points_a_member_at_its_own_tree
on every branch, master included.


Review repairs

An adversarial review built and ran both binaries against this branch and found
that the change did not fix the defect it named as the real one, and reopened
issue #96's own symptom through a narrower door. Both are confirmed with
side-by-side output and repaired in three further commits.

1. A workspace member's declared version was discarded (HIGH)

shallow_graph built every Cargo member from the (name, manifest content)
pair it had just parsed and passed None for the version, throwing away the
[package] version in that content. build_project_graph already reads
meta.literal_version() for non-Cargo roots; the Cargo path never did.

Reproduced with a Cargo.toml declaring name = "demo-app",
version = "0.4.2", one dependency, no Cargo.lock:

before after
--format json {"name":"demo-app","version":null,…} {"name":"demo-app","version":"0.4.2",…}
ascii demo-app (workspace) demo-app v0.4.2 (workspace)

null there was a worse answer than "" had been, not a better one. A member is
resolved against nothing — what its manifest declares is its version — so
there is no unresolved state to report, and the detail pane read "version not
resolved — nothing read a version for this" over a crate whose version sits one
line under its name. Its dependencies stay unknown, which is the distinction
the issue is actually about: a manifest declares a constraint, and nothing here
resolved it.

version.workspace = true is resolved against the root's [workspace.package]
table, which shallow_graph already holds — reading only the literal would
report every member of a version-inheriting workspace as unknown, which is the
majority shape for a Cargo workspace.

Test correction, not weakening. falls_back_to_shallow_graph_without_lockfile
asserted nodes().iter().all(|n| n.version.is_none()) under the message "a
manifest-only graph resolves no versions", over a fixture whose three members
each declare version = "0.1.0". It pinned the defect. It now asserts what is
true of that fixture: a, b, and c report 0.1.0; serde and gitdep
report nothing; no node carries the empty string. A new test,
a_member_inheriting_its_version_from_the_workspace_root_still_reports_one,
covers the inherited case. README.md documented the defect as intended and is
corrected too.

2. Some("") was never normalized, so issue #96's symptom was reachable again (HIGH)

master guarded with row.version.is_empty(), which covered both "no version"
and "empty version". This branch replaced it with an Option check and nothing
normalized Some("") → None. Four parsers can still emit it: cargo_lock_graph
from a hand-edited or generator-produced Cargo.lock with version = "",
mix_lock_graph from unquote("\"\""), composer_lock_graph from
"version": "" or a bare "v" through strip_v, and bun_lock_graph.

Reproduced on a Registry node whose version is Some(""):

before after
selected_key() Some((Rust, "serde", "")) — a real lookup is spawned None
STATUS cell ok unknown
detail pane latest 2.0.0 / status up to date version not resolved

That is issue #96 verbatim. The mechanism is in semver/checker.rs:
Version::parse("") fails, locked becomes None, current falls back to
latest_compatible under the * constraint, and the >= latest_available arm
answers UpToDate. age() was likewise unguarded.

Repaired at the parse boundary, not at the consumers. LockedPackage::new is
the single choke point every package entry passes through, parsed and synthesized
alike, and it now normalizes an empty version to None. Adding is_empty()
checks back at each consumer would have left the next consumer to rediscover the
rule — and an Option that still admits Some("") has not achieved what the
issue's Direction asked for, which is that the distinction cannot be lost by
accident. Node is only ever constructed inside DependencyGraph::from_resolved
from a LockedPackage, so the boundary covers the whole graph.

Three tests falsify it, each verified failing without the normalization:

  • an_empty_version_is_recorded_as_no_version_at_all (dependable-core) — a
    Cargo.lock recording version = "" parses to None, and so does a
    synthesized entry.
  • an_empty_version_string_is_treated_as_no_version_at_all_not_as_up_to_date
    (dependable-tui) — the row carries no version, selected_key() is None,
    and the STATUS cell reads unknown.
  • an_empty_version_in_a_lockfile_is_never_rendered_as_a_version
    (dependable) — no ascii, dot, or json output prints it as a version.

3. The renderers printed serde v (LOW)

output/tree.rs dropped its is_empty() checks for match … as_deref(), so
with a Cargo.lock carrying version = "" the branch rendered └── serde v
and n1 [label="serde v"] where master rendered └── serde and
n1 [label="serde"]. Fix 2 closes it — confirmed by rerunning the same
lockfile, which now renders └── serde and n1 [label="serde"] — and the
renderer test above pins it. The body's claim above that the ASCII and DOT
renderers are byte-for-byte unchanged held for None but not for Some(""); it
holds now.

4. The dependable-tui break, declared

dependable_tui::rows::Row::version is Option<String> where it was String.
rows is a pub mod and dependable-tui is published, so this is a public API
break alongside the dependable-core and JSON-schema ones listed above. It
landed in fix(tui): report a dependency with no known version as unknown, which
carries neither a ! nor a trailer; that commit is pushed and cannot be
reworded, so the break is declared here and carried as a BREAKING CHANGE:
trailer on fix(core)!: read an empty version as no version at all.

5. A doc claim contradicted by code in the same PR (LOW)

LockedPackage::version's doc said "A lockfile always records one, so a parsed
entry is always Some. None is what a synthesized entry carries."
package_lock_graph and bun_lock_graph both produce None for parsed
entries — an npm or Bun workspace link records a location
(workspace:packages/lib), not a version. The doc now says so, and states that
Some("") is unrepresentable.

Decisions taken

  • (a) A Cargo workspace member's declared [package] version is read, not
    unknown. It is reported, inherited versions included.
  • (b) Some("") is unrepresentable, normalized at the parse boundary rather
    than checked for at each consumer.
  • (c) tree --format json keeps null rather than omitting the key.
    Explicit beats absent, and the README documents it.
  • (d) LockedPackage::version stays an Option and new() normalizes —
    both, not either. The type states the intent; the constructor enforces it.
  • (e) A dependency whose declared constraint is an exact pin (a .csproj
    Version="1.2.3", a Gradle catalog pin) stays reported as unknown for now.
    That is outside issue fix(tui): a dependency whose version was never read renders as up to date #96, and the "a constraint is not a resolution" argument
    above does not cover a constraint admitting exactly one version. Filed as
    feat(fetch): report a dependency pinned to an exact version rather than leaving it unknown #107.
  • (f) The STATUS column reading unknown with a blank VERSION column, and
    the detail-pane wording, are kept as the body describes them.

Validation after the repairs

env -u FORCE_COLOR -u COLORTERM cargo test --workspace     856 passed, 0 failed, 20 ignored
env -u FORCE_COLOR -u COLORTERM cargo clippy --workspace --all-targets -- -D warnings    clean
cargo fmt --all --check                                    clean
convco check master..HEAD                                  no errors in 6 commits

No test was deleted or weakened. The one test that changed is the corrected
falls_back_to_shallow_graph_without_lockfile, which asserted the defect
described in repair 1; it now asserts more than it did, not less.


Repair: a nested independent workspace no longer inherits the outer root's version

The commit that taught the shallow graph to resolve version.workspace = true
resolved it for every collected member against the outer scan root's
[workspace.package]. But the member walk descends into every Cargo.toml
under the root — the skip list covers only target, node_modules, .git and
vendor — so a cargo fuzz, examples/ or xtask directory holding its own
independent [workspace] was swept in, and its crates took a version from a
root Cargo would never resolve them against. [workspace] exclude does not
save it either: nobody lists a nested workspace there, because Cargo already
ignores such a subtree.

Given this layout with no Cargo.lock:

root/Cargo.toml            [workspace] members = ["crates/a"]
                           [workspace.package] version = "1.0.0"
root/crates/a/Cargo.toml   [package] name = "a", version.workspace = true
root/fuzz/Cargo.toml       own [workspace], [workspace.package] version = "0.0.0"
                           [package] name = "a-fuzz", version.workspace = true
before:  a v1.0.0 (workspace)        after:  a v1.0.0 (workspace)
         a-fuzz v1.0.0 (workspace)           a-fuzz (workspace)

and in --format json, a-fuzz's "version" goes from "1.0.0" to null.
This reached ascii, dot and json alike, and it was worse than the state it
replaced — that node previously had no version at all, so the change turned
"unknown" into a confidently wrong number, which is the exact failure class this
branch exists to remove.

The member walk now carries whether the scan root is a crate's nearest
[workspace] ancestor, and an inherited scalar is resolved only when it is. A
crate inside a nested workspace reports no version rather than a borrowed one;
resolving it against its own root would mean introducing a second resolution
scope, which is a larger change than this. A version such a crate states
outright is unaffected and still reported. Bounded to the lockfile-less shallow
path — with a Cargo.lock the nested crate is not in the lock and never appears.

Covered by a_nested_independent_workspace_does_not_inherit_the_outer_roots_version,
which fails on the previous commit with left: Some("1.0.0"), right: None.

Validation after this repair

env -u FORCE_COLOR -u COLORTERM cargo test --workspace     857 passed, 0 failed, 20 ignored
env -u FORCE_COLOR -u COLORTERM cargo clippy --workspace --all-targets -- -D warnings    clean
cargo fmt --all --check                                    clean
convco check master..HEAD                                  no errors in 7 commits

857 is 856 plus the new test; nothing was deleted or weakened.

list --depth 4, list --format json, tree and tree --format json over
this repository and all 16 fixtures remain byte-identical to master. Those
targets all resolve a lockfile, so they do not exercise the changed path; a
second sweep over lockfile-less copies of sample-rust, sample-workspace,
sample-workspace-inherit and sample-monorepo plus the fuzz/ and
examples/inner/ layouts above, comparing this commit against its parent,
differs in exactly two places: the a-fuzz and a-example versions dropping to
null. Every other output on that corpus is unchanged.

A graph built from manifests alone resolves no versions, and both
`LockedPackage` and `Node` recorded that state as `String::new()`. An empty
string is a value, not an absence: every consumer downstream had to remember
that this particular string means "we never learned the version", and one that
forgot would read it as a version and act on it.

Both fields become `Option<String>`. A lockfile always records a version, so
every parsed entry is `Some`; `None` is carried only by the entries the builder
synthesizes — a shallow Cargo workspace, a manifest-only project graph, an npm
or Bun workspace link, and a project whose manifest declares no version of its
own (a `pom.xml` inheriting from a `<parent>`, a `*.csproj`).

BREAKING CHANGE: `dependable_core::LockedPackage::version` and
`dependable_core::Node::version` are now `Option<String>`, and
`LockedPackage::new` takes `Option<String>`. Both types are re-exported from
`dependable_fetch::core`.

Rendered output is unchanged: every existing consumer treated the empty string
as "unknown" already, so each one keeps that behaviour against `None`.
`tree --format json` is a schema other tools read. A node whose version was
never resolved was serialised as `"version": ""`, which a consumer can only
read as a package at the empty version. It is now `null`.

`null` rather than omitting the key: every node keeps the same shape, so a
consumer never has to tell an absent key from an absent version, and a schema
that drops a field on some rows is the harder one to parse.

The ASCII and DOT renderers are unchanged — both already printed the bare name
for a node with no version.

BREAKING CHANGE: `tree --format json` emits `"version": null` for a node whose
version was never read, where it previously emitted `""`. Every node in a
shallow tree (no `Cargo.lock`) is in that state.
A row in the tree carried its version as a `String` that was empty when nothing
had read one. Three separate `version.is_empty()` checks — in `selected_key`, in
`status_badge`, and in the detail pane — were what kept the tool from asserting
freshness about a package it knew nothing about. Delete any one of them and the
row silently gains a green `ok`: `check_version` reads an empty current version
as "not locked", falls back to the newest compatible release, and answers
`UpToDate`.

`Row::version` becomes an `Option<String>`, so the check is the type rather than
three copies of a convention. `age` — the one place that never had the check —
gets it as a consequence, and the detail pane's `resolved` version is now passed
down to the metadata block instead of being read back off the row, so the code
that formats a version cannot be reached without one.

The status column now reports `unknown` for such a row rather than leaving the
cell blank. A blank cell reads as "nothing to report about this package", which
is the opposite of what is true; `unknown` says the tool never learned what
version is declared. This is the state every Gradle, `pom.xml`, `*.csproj`, and
`mix.exs` project is in, along with any Cargo workspace with no `Cargo.lock`.

The detail pane's explanation drops its "no lockfile for this project" clause:
that is one of several reasons a version goes unread — a `pom.xml` inheriting
from a `<parent>` has one, and a `pubspec.lock` is read but records no edges —
and the project row's own caveat is where the reason belongs.

Closes #96
Replacing the `String::new()` sentinel with `Option<String>` did not on its own
close issue #96: nothing normalized `Some("")`, so the sentinel simply moved
inside the `Option`. Four parsers can still produce it — a hand-edited or
generator-produced `Cargo.lock` with `version = ""`, `unquote("\"\"")` in the
Mix reader, `"version": ""` or a bare `"v"` in the Composer reader, and the Bun
reader — and every consumer that had an `is_empty()` guard before now has only
an `Option` check.

The symptom is issue #96 verbatim. `Version::parse("")` fails, so the checker's
`locked` is `None`, `current` falls back to the latest compatible release under
a `*` constraint, and the `>= latest_available` arm answers `UpToDate`: a green
`ok` in the TUI status column and a "latest 2.0.0 / status up to date" detail
pane for a package whose version nobody ever read. The renderers say `serde v`
for the same node.

`LockedPackage::new` is the one choke point every package entry passes through,
parsed and synthesized alike, so an empty version is normalized to `None` there.
That makes `Some("")` unrepresentable rather than merely discouraged, which is
what issue #96 asked for — an `Option` that still admits the empty string has
not removed the sentinel, only re-typed it. Adding `is_empty()` checks back at
each consumer would leave the next consumer to rediscover the rule.

Three tests falsify it: the parser test that a `Cargo.lock` recording
`version = ""` yields `None`, the TUI test that such a node reports `unknown`
and is never looked up, and the renderer test that no `ascii`, `dot`, or `json`
output prints it as a version.

Also corrects the `LockedPackage::version` doc, which claimed a parsed entry is
always `Some` — the npm and Bun readers both parse workspace links, which record
a location rather than a version and yield `None`.

BREAKING CHANGE: `dependable_core::LockedPackage::new` no longer stores an empty
`version`; it is normalized to `None`, so `LockedPackage::version` and
`graph::Node::version` are never `Some("")`. This also declares a break that
landed unmarked earlier in this branch: `dependable_tui::rows::Row::version` is
`Option<String>` rather than `String`.
…file

`shallow_graph` builds every Cargo workspace member from the `(name, manifest
content)` pair it just parsed and passes `None` for the version, discarding the
`[package] version` sitting in that content. A `Cargo.toml` declaring
`version = "0.4.2"` with no `Cargo.lock` beside it renders as `demo-app
(workspace)` and reports `"version": null` in `--format json`.

That is a false claim, not merely a missing one. A member is resolved against
nothing: what its manifest declares *is* its version, whether or not a lockfile
exists, so there is no "unresolved" state to report. `None` on a member says no
version was ever read for it, which is more emphatically wrong than the empty
string was — the TUI detail pane reads "version not resolved — nothing read a
version for this" over a crate whose version is written one line below its name.

`build_project_graph` already reads `meta.literal_version()` for every non-Cargo
root; the Cargo path simply never did. This reads the member's version the same
way, and resolves `version.workspace = true` against the root's
`[workspace.package]` table, which is already in hand here — reading only the
literal would report every member of a version-inheriting workspace as unknown,
which is the majority shape for a Cargo workspace.

A member's *dependencies* stay unknown, which is the distinction issue #96 is
about: a manifest declares a constraint, and nothing here resolved it.

`falls_back_to_shallow_graph_without_lockfile` asserted the defect — every node
`version.is_none()` over a fixture whose three members each declare
`version = "0.1.0"`, under the message "a manifest-only graph resolves no
versions". It is corrected to assert what is true of that fixture: the members
report `0.1.0`, `serde` and `gitdep` report nothing, and no node carries the
empty string. A new test covers the inherited case.
The `tree` section documented the shallow graph as producing `null` "for every
node", which described a defect rather than the intended behaviour: a workspace
member carries the version its manifest declares, and only its dependencies are
unknown. It also claimed a version "is never the empty string", which was untrue
until the parse boundary normalized one away.

Both are now accurate, and the note covers the `ascii` and `dot` renderers as
well as `--format json`, since all three read the same absence.
…e crate

`shallow_graph` resolved every collected member's `version.workspace = true`
against the outer scan root's `[workspace.package]`. But `collect_members`
walks every `Cargo.toml` under the root, and `SKIP_DIRS` covers only `target`,
`node_modules`, `.git` and `vendor` — so a `cargo fuzz`, `examples/` or `xtask`
directory holding its own independent `[workspace]` is descended into, and its
crates took a version from a root Cargo would never resolve them against.
Nobody excludes such a subtree either, because Cargo already ignores it.

For the layout below with no `Cargo.lock`, `tree` reported `a-fuzz v1.0.0`:

    root/Cargo.toml            [workspace.package] version = "1.0.0"
    root/crates/a/Cargo.toml   version.workspace = true
    root/fuzz/Cargo.toml       own [workspace], version = "0.0.0"

That is worse than the state it replaced, where the node simply had no version:
it turns "unknown" into a confidently wrong number, the failure class this whole
branch exists to remove.

The walk now carries whether the scan root is a crate's nearest `[workspace]`
ancestor, and an inherited field is resolved only when it is. A crate in a
nested workspace reports no version rather than a borrowed one — resolving it
against its own root would mean a second resolution scope, which is a larger
change than this. A version such a crate states outright is still its own, and
still reported.
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.

fix(tui): a dependency whose version was never read renders as up to date

1 participant