fix(cli): head an unread dependency list as unread in list too - #115
Open
justin13888 wants to merge 5 commits into
Open
fix(cli): head an unread dependency list as unread in list too#115justin13888 wants to merge 5 commits into
justin13888 wants to merge 5 commits into
Conversation
`list` counted the items it held, so a SwiftPM project with no readable `Package.resolved` was headed "(0 dependencies)" — an inventory reporting an empty inventory for a list it never opened. `check` and the HTML report already say "dependency list unread" here; `list` did not, because `run_list` discarded the bool `report_lockfile_notices` returns. Carry that bool onto `ProjectReport` and use it in both surfaces `list` owns: the table heading gets the same words `check` and the HTML report use, and `--format json` gains a per-project `dependencies_unread`. The JSON half matters on its own — nothing on a `list` project object separated "declares nothing" from "nothing was read", and `lockfile: null` does not answer it because `--no-lock-file` produces that too. The `items.is_empty()` conjunct is load-bearing, and matches the two existing consumers: a `Package.resolved` that parses to zero pins really does declare nothing and is still counted, as is a partially read list. An added key stays within `dependable.list/v1`: a consumer that reads the fields it knows is unaffected by one it never looks at, and bumping the version would break every consumer pinning `v1` in order to protect none of them. README worded the pin as "which fields exist", which an added field contradicts, so it now states the policy it actually follows. `--format text` is unchanged: its contract is one record per dependency at fixed arity, and a manifest-level record has no place in it. Refs #109
…-unread-dependency-list Brings `PackageSource::Locked` and `PackageSource::Unidentified`, and the two `source_token` arms they need, onto the branch that adds `dependencies_unread`. Git merged `output/list.rs` cleanly because the three changes sit in different parts of the file; the properties were checked rather than assumed, and all three hold together: - a Swift project with no `Package.resolved` still heads its list "dependency list unread" and reports `dependencies_unread: true`; - a lockfile pin still reports `"source": "locked"`; - a POM entry whose coordinate the file never states still reports `"source": "unidentified"` and annotates as `(unidentified)`. Neither new variant falls through `_ => "unknown"`.
This was referenced Sep 6, 2026
The `assert_ne!` over the two `list --format json` documents discriminated nothing. It stripped `root`, but the two runs also differ in `lockfile`: the unread project has no `Package.resolved` to name, so its `lockfile` is null, while the empty one names the file. The assertion fired on that difference alone — it passed unchanged with `dependencies_unread` absent from `ProjectDto` entirely — so its message stated something untrue. The shape came from the `check --format json` twin, where it is load-bearing: that document carries no per-manifest `lockfile` key, so those two really were byte-identical before `manifests_unread`. The premise does not transfer to a document that carries one. Strip `lockfile` too, for the same reason `root` was already stripped. With the field removed from `ProjectDto` the two documents are now byte-identical and the assertion fails, which is what it always claimed to be testing.
`dependencies_unread` has no `skip_serializing_if`, so `list --format json` emits it on every project, not only a SwiftPM one. The README example omitted it, which made it the single field a decoder written from that example — the artefact people copy — would reject on a document the tool actually produces. The example is otherwise a faithful superset of the schema: every field without `skip_serializing_if`, plus the two optional ones. Restore that property.
Its return value is load-bearing in three places — the `list` heading, `list --format json`, and the `--fail-on any` exit code — its own doc comment says the caller has to carry it, and every function it delegates to (`lockfile_notices`, `locate_lockfile`, `lockfile_items`, `swift_package_resolved_items`) already carries the attribute. This wrapper was the one unmarked link in that chain, and CLAUDE.md asks for `#[must_use]` on important return types. Be honest about what it buys: `#[must_use]` would not have caught the bug this branch fixes, because `let _ = f()` is exactly the spelling that silences the lint and exactly what the base code had. It guards the bare-statement spelling only. Add it anyway — the convention is stated and the attribute is free.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #109.
listcounted the items it held, so a SwiftPM project with no readablePackage.resolvedwas headed(0 dependencies)— an inventory reporting an empty inventory for a list it never opened.checkand the HTML report have saiddependency list unreadhere since the Swift work landed;listdid not, becauserun_listdiscarded the booleanreport_lockfile_noticesalready returns:so
ProjectReportnever carried the fact and neither oflist's own surfaces could state it.What changed
ProjectReportgainspub dependencies_unread: bool,run_listbinds the boolean instead of discarding it.report.items.is_empty() && report.dependencies_unreadyields the literal stringdependency list unread, character for character the phraseoutput/table.rsand the HTML template already use. A user comparingcheckandlistsees one phrase, not two.--format json— a per-projectdependencies_unreadboolean, immediately beforedependenciesbecause it qualifies that array. Plain, always emitted, noskip_serializing_if: the answer is always known. Named afterManifestReport,ManifestResultsand the HTML model rather than inventing a list-local name.list --format jsonexample gains"dependencies_unread": falsein the same positionfn jsonemits it. Because the field has noskip_serializing_ifthe tool emits it on every project, so an example without it was the one place a decoder written from the README — the artefact people copy — would reject a real document. The example is otherwise a faithful superset of the schema, and decision 4 argues from exactly that consistency.#[must_use]onreport_lockfile_notices— the discarded return value above is what caused this bug, the value is load-bearing in three places, and every function it delegates to already carries the attribute. It is worth being precise about what this buys: it would not have caught this bug, becauselet _ = f()is exactly the spelling that silences the lint and exactly what the base code had. It guards the bare-statement spelling only. Added because CLAUDE.md states the convention and the attribute is free, not because it closes the hole.The
items.is_empty()conjunct is load-bearing rather than decorative, and matches both existing consumers: aPackage.resolvedthat parses to zero pins really does declare nothing and is still headed(0 dependencies); a partially read list is still counted rather than disclaimed. Both cases are asserted.Before this, the only thing separating
list --format jsonover an unread project from the same command over a genuinely empty one waslockfile—nullagainst"Package.resolved". An earlier revision of this body claimed the two documents were identical apart from the root path; that was wrong, and the test written from it was wrong in the same way.lockfileanswers a different question, since--no-lock-fileproduces the samenullfor a project whose list was read perfectly well. Setlockfileaside — as the test now does explicitly — and the two documents were byte-identical, which is the defect. The test asserts they now differ once it is set aside, and asserts why the field is needed rather than a count:summary.dependenciesis0in both.Decisions taken
1. Deliverable boundary — how much of the issue closes here
Taken: the heading and the per-project
list --format jsonfield. Both sentences of the issue's own Direction paragraph.listproject object separates unread from empty today, andlockfile: nullis not a substitute because--no-lock-fileproduces it too.projects_unreadis inconsistent withcheck'smanifests_unread;manifests_unreaduses a wordlist's summary uses nowhere else), and that choice deserves its own issue rather than being taken in passing here.dependencies_unreadfromProjectDtoand its line infn json; drop the JSON test.--format textgap from decision 3.2. The JSON surface and what it does to
dependable.list/v1Taken: add the field, retain
dependable.list/v1, and amend the README's schema paragraph so it states the policy it actually follows — v1 permits added fields and forbids removed, renamed or retyped ones.dependable.list/v2. Every consumer pinning== "dependable.list/v1"breaks because of the bump, not because of the field. The bump causes the breakage it exists to prevent, and it sets the precedent that any added key costs a version.SCHEMAinoutput/list.rstodependable.list/v2, updating the assertion intests/cli_list.rs.3.
--format textand whether anything is printed under the headingTaken: heading only.
--format textunchanged, and no explanatory body line under the table heading.check's own table adds no such line, so "exactly as check does" argues against it, andlist's table has no manifest-level body line for any zero-dependency project today.--format text. That format's contract is one record per dependency at fixed arity, documented in-code as deliberate, and any sentinel in the name column is a string a real package could hold.println!under the heading infn table, or a manifest-level record infn text.Known cost, disclosed rather than hidden:
list --format textemits nothing at all for an unread manifest — strictly worse than the "0 dependencies" this issue objects to. That is a real gap, it is not closed here, and it is written up in #114.4. Taken inside the manifest, during implementation
Taken: the
SCHEMAdoc comment inoutput/list.rsis amended alongside the README's, and the README documents the newdependencies_unreadkey beside its siblings.The doc comment carried the same "which fields exist and what type each holds" wording the README did, and ends with "The README states the same policy for readers who never open this file" — so amending one and not the other would have left the two statements of one policy disagreeing. The key documentation follows the file's own precedent:
version_inherited,inheritedandlockfileare each described in that section, and leaving one sibling undocumented is an inconsistency rather than a smaller diff. Both edits are inside files already in scope.Reverses: revert the two README hunks and the
SCHEMAdoc hunk; the code is unaffected.5. Was
dependable.list/v1always meant to permit added fields?Taken: yes — additive-within-v1 is the contract, and the previous wording was imprecise rather than a different promise.
check --format jsonalready addedmanifests_unreadadditively, with an in-tree doc comment making that exact argument, and no golden-file snapshot oflist --format jsonexists in CI to be broken by a new key.dependable.list/v2. Every consumer pinning== "dependable.list/v1"breaks because of the bump rather than because of the field, so the bump causes the breakage it exists to prevent. It also sets the precedent that any added key costs a version.SCHEMAtodependable.list/v2and update the assertion intests/cli_list.rs.6. Is "unread implies empty" an invariant or a coincidence?
Taken: an invariant, enforced by
adoptable_fromconfining a dependency source to the manifest's own directory. Theitems.is_empty()conjunct in the two heading consumers is defensive documentation of that invariant. The JSON field deliberately carries the raw fact without the conjunct, because a machine consumer wants the fact rather than a rendering decision.items.is_empty() && dependencies_unread.7. Should the JSON document carry the cause, not just the fact?
Taken: no — stderr-only, for now.
dependencies_unread: trueis emitted identically for "noPackage.resolved", "it exists but could not be read", and "it is malformed"; the distinguishing reason stays on stderr, where every--formatsees it.ProjectDto.Verified behaviour
Package.resolved(dependency list unread), JSONtrue, warning still on stderr, exit 0Package.resolved={"pins":[],"version":2}(0 dependencies), JSONfalselist --no-lock-file, resolved file presentfalselist --no-lock-file, no resolved filedependencies_unread: false; the one visible change to non-Swift output--features/--licenses&mut [ProjectReport]and never construct one; unaffectedTests
Two tests in
crates/dependable/tests/fixture_swift.rs, beside thecheck-heading, JSON and HTML twins for this exact fact — only a SwiftPM project can set the bit, so that is where the assertions belong.the_list_heading_says_the_list_went_unread_rather_than_counting_zero— also asserts the warning on stderr survives: the heading joins it, it does not replace it.list_json_distinguishes_an_unread_dependency_list_from_an_empty_one— asserts the two documents now differ, the field's two values,summary.dependencies == 0in both, andschema == "dependable.list/v1".The comparison strips both
root(a scratch path) andlockfilebefore comparing, so it discriminates ondependencies_unreadalone. Stripping onlyroot— as the first revision of this test did, copied from thecheck --format jsontwin where the shape is load-bearing because that document carries no per-manifestlockfilekey — left the assertion firing on thelockfiledifference and passing whether or not the new field existed at all. Falsified deliberately: withdependencies_unreadremoved fromProjectDtoand its line infn json, the repairedassert_ne!fails on two byte-identical documents, and the field was then restored.crates/dependable/tests/cli_list.rsis deliberately untouched and passes as-is: it already pinsschema == "dependable.list/v1"and thenpm (4 dependencies)heading, both of which this change must not disturb.Validation
cargo test -p dependable --test fixture_swift(24 passed, 1 ignored),--test cli_list(13 passed, untouched),mise run test(full workspace green),mise run fmt:check,mise run lint,convco check— all clean.An earlier revision of this body reported three
dependable-fetchtests/checker.rstests failing locally against a populated~/.cache/dependableleft by live runs. That did not reproduce on the latest run: with the cache still present (884K),tests/checker.rspassed 23/23 andmise run testexited 0 with no failures anywhere in the workspace. The note is retained only to record that the earlier observation was environmental and is not a property of this branch; nothing in this diff touches that crate, and CI is green.Residuals
Not fixed here, recorded so it is not lost:
listprints "1 dependencies" wherecheckprints "1 dependency". Pre-existing: this diff refactors that literal into ascopebinding but the pluralisation gap predates it and is untouched by it. Deliberately left alone, because fixing it would touch an assertion intests/cli_list.rs, which this change keeps untouched on purpose as an independent check that the schema and heading contracts still hold. A candidate for the fix(core): a bare NuGet Version reports UpToDate whatever the registry publishes #113/fix(cli): the two halves of "dependency list unread" that list still cannot say #114 remainder set.Base
Based on
refactor/98-locked-package-source(#103), the tip of the pom.xml/Swift stack, because thedependency_list_unreadnotice andcheck's heading only exist there. Merge, do not rebase, to take base updates.