Skip to content

Make --version report install provenance - #85

Merged
blooop merged 2 commits into
mainfrom
wayfinder/devlaunch-84
Aug 7, 2026
Merged

blooop merged 2 commits into
mainfrom
wayfinder/devlaunch-84

Conversation

@blooop

@blooop blooop commented Aug 7, 2026

Copy link
Copy Markdown
Owner

The problem

dl on PATH is the released pixi-global build; dl-next is an editable install of a working tree. Both print dl 0.0.9 — the version came from importlib.metadata.version, which is identical for the two. So the only thing distinguishing a released run from a half-finished working tree was the name typed to invoke it, and any transcript, bug report, or CI log recording just --version output was ambiguous about which build produced it.

The change

get_version() now appends an install-provenance clause when the installed dist reports itself as editable:

$ dl --version                     # released
dl 0.0.9

$ dl-next --version                # editable install of a tree
dl 0.0.9 (dev, editable from /path/to/checkout)

Provenance is read from the dist's own PEP 610 direct_url.json via importlib.metadata. dir_info.editable == true is the discriminator and url supplies the tree to name. The url is a file:// URI and is parsed as one (urlparse + url2pathname), not string-stripped, so a percent-encoded path decodes correctly.

Nothing here stats a path, pattern-matches an install location, or hardcodes a /home/<user> path — the fact comes from metadata the installer wrote, so it holds for any user and any install prefix.

aid --version inherits this with no change of its own, since it renders the same version string under its own name.

Non-fatal by construction

A broken --version is worse than an ambiguous one, so provenance detection can only ever add to the output, never prevent it. Every failure mode falls back to today's bare <version>:

  • no direct_url.json at all (plain wheel/sdist install)
  • direct_url.json present but not parseable as JSON
  • parseable but missing url, or dir_info not a mapping
  • the metadata reader itself raising
  • PackageNotFoundError from the version lookup — the pre-existing "unknown" handling at that call site is untouched

Scope

Version string only. No change to any other command, to how versions are assigned, or to packaging.

Docs kept in sync per CLAUDE.md: the README's global-commands table and a short worked example of both outputs, and the dl --help line for --version. AGENTS.md/CLAUDE.md carried a now-false claim that dl-next --version "reports the package version, not its provenance" — corrected.

Tests

Seven tests in TestVersionProvenance, all driving get_version() with the installed-dist metadata reader stubbed at that system boundary (no devlaunch function of ours is mocked):

  • an editable install is named as dev and its tree path appears in the output
  • a percent-encoded tree path is decoded rather than string-stripped
  • a non-editable install returns the bare version, unchanged
  • absent direct-url metadata → bare version
  • malformed JSON → bare version
  • editable metadata with no url → bare version
  • a metadata reader that raises → bare version

The pre-existing PackageNotFoundError test and the CLI-level --version tests still pass unmodified.

Verified / not verified

Verified against real metadata on a machine that has both installs: the editable path was confirmed end to end by actually running --version (and aid --version) out of an editable install and seeing it name the right tree; the released path was confirmed by feeding the released build's real direct_url.json bytes through the new code and getting a bare 0.0.9.

Not verified: a released build carrying this code has not been built and run — the released side was exercised through its real metadata, not through a real conda-channel install. The degradation cases are covered by tests only; no install genuinely lacking direct_url.json was constructed to confirm in the wild.

pixi run ci green locally: 815 passed, 8 deselected; ruff format 40 files unchanged; ruff check all passed; pylint 10.00/10; ty all checks passed; coverage 89% total, devlaunch/dl.py 90%.

Closes #84

Summary by Sourcery

Make the CLI version output include install provenance for editable dev installs while preserving existing behavior for released builds.

Enhancements:

  • Add provenance detection based on PEP 610 direct_url metadata so editable installs report their source tree in the version string.
  • Update CLI help text and README to document the enhanced --version behavior and clarify output differences between released and editable installs.
  • Adjust internal version retrieval to append provenance information when present without affecting error handling for missing package versions.

Documentation:

  • Document the new --version output for editable installs in README and clarify behavior in AGENTS guidance.

Tests:

  • Add a dedicated test suite for version provenance covering editable installs, URL decoding, absent or malformed metadata, missing keys, and metadata reader failures.

Both builds report the same version, so `dl --version` could not say which
one had just run — the name on PATH was the only distinguishing thing, and a
`dl` and a `dl-next` of the same version printed identical output.

Read the installed dist's PEP 610 `direct_url.json`: `dir_info.editable`
marks an editable install and `url` names the tree it resolves to, decoded as
the file:// URI it is. No path is stat'd and no install location is
pattern-matched, so nothing here assumes where either build lives.

The addition is strictly non-fatal. A dist with no direct-url metadata, with
metadata that does not parse, or with metadata missing the keys, falls back
to the bare version rather than raising — a broken `--version` is worse than
an ambiguous one. A non-editable install's output is unchanged.

`aid --version` inherits this, since it renders the same version string.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @blooop, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

Add install-provenance reporting to get_version() so editable dev installs are distinguishable from released builds, backed by PEP 610 direct_url.json metadata, with robust degradation, tests, and documentation updates.

Sequence diagram for get_version install-provenance reporting

sequenceDiagram
    actor User
    participant dl as dl_cli
    participant get_version
    participant _install_provenance
    participant importlib_metadata as importlib_metadata
    participant direct_url_json as direct_url_json

    User->>dl: run --version
    dl->>get_version: get_version()
    get_version->>importlib_metadata: pkg_version(devlaunch)
    alt PackageNotFoundError
        get_version-->>dl: unknown
    else Version found
        get_version->>_install_provenance: _install_provenance()
        _install_provenance->>importlib_metadata: distribution(devlaunch)
        importlib_metadata->>direct_url_json: read_text(direct_url.json)
        _install_provenance->>_install_provenance: json.loads(raw)
        alt [dir_info.editable is true and url is file]
            _install_provenance-->>get_version: dev, editable from <tree>
            get_version-->>dl: <version> (dev, editable from <tree>)
        else No usable provenance
            _install_provenance-->>get_version: None
            get_version-->>dl: <version>
        end
    end
    dl-->>User: print version string
Loading

File-Level Changes

Change Details Files
Version reporting now includes install provenance for editable dev installs, derived from PEP 610 direct_url.json metadata, while preserving existing behavior for released and non-editable installs.
  • Introduce a helper that reads distribution metadata, parses direct_url.json, and returns a human-readable description of editable installs using file:// URI decoding.
  • Update the version retrieval function to append the provenance string when available and to fall back cleanly to the base version on any metadata error.
  • Ensure all error and edge cases (missing file, malformed JSON, non-editable or missing URL, metadata reader failures) degrade to bare version output instead of raising.
devlaunch/dl.py
Add targeted tests around install provenance handling to validate editable/non-editable behavior and all degradation paths for get_version().
  • Create a metadata reader stub that simulates distribution.read_text('direct_url.json') returning content or None.
  • Add a focused test class verifying editable installs are marked as dev and include the decoded tree path in the version output.
  • Add tests covering non-editable installs, absent or malformed direct-url metadata, missing URL keys, and metadata reader exceptions all returning the bare version.
test/test_dl.py
Update user-facing documentation and help text to describe the new provenance-aware --version behavior for dl and aid, and correct previous statements about dl-next.
  • Revise the CLI help line for --version to mention editable installs naming their tree.
  • Expand README with an example comparing released vs editable --version output and clarifying that provenance comes from PEP 610 metadata.
  • Adjust AGENTS.md to reflect that dl-next --version now reports the editable tree path, replacing the prior claim that it only shows the package version.
README.md
AGENTS.md

Assessment against linked issues

Issue Objective Addressed Explanation
#84 Implement install-provenance reporting in dl --version (and thus aid --version) using PEP 610 direct_url.json, ensuring editable installs are distinguished from released installs without breaking --version.
#84 Add tests that cover editable vs non-editable installs and the specified degradation/edge cases (no direct_url.json, malformed or missing keys, metadata reader failures), all falling back to bare version output.
#84 Update documentation (README and CLI help text) to reflect the new --version behavior and example outputs, in line with CLAUDE.md’s docs rule.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.59259% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.28%. Comparing base (b304cb6) to head (12fb310).

Files with missing lines Patch % Lines
devlaunch/dl.py 92.59% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #85      +/-   ##
==========================================
+ Coverage   89.24%   89.28%   +0.04%     
==========================================
  Files          12       12              
  Lines        1636     1661      +25     
==========================================
+ Hits         1460     1483      +23     
- Misses        176      178       +2     
Files with missing lines Coverage Δ
devlaunch/dl.py 90.51% <92.59%> (+0.05%) ⬆️

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@blooop blooop left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during review.

Reviewed at head 12fb3101b2ff2094d3bad44cc0a5beabe349067e against the merge-base with main (b304cb6), in a fresh context that did not write the code. Two axes run independently and not merged. Findings were checked by execution in a scratch clone, not by reading — including mutation-testing the implementation against the new tests, and running the new code against the real direct_url.json of both installs on a machine that has them.


Standards

Ruff, pylint and ty pass; broad-exception-caught is disabled repo-wide (pyproject.toml:115) with precedent in devlaunch/worktree/branch_manager.py:117 and devlaunch/dl.py:1111, so the bare except Exception is not a finding on its own. CLAUDE.md is a symlink to AGENTS.md, so the one edit covers both. No /home/<user> path anywhere in the diff — the tests use /srv/checkouts. Docstring density matches repo precedent.

Nits

  1. Four guards are unreachable and produce nothing the blanket handler doesn't. devlaunch/dl.py:89 (isinstance(info, dict)), :93 (isinstance(url, str)), :96 (scheme != "file"), :99 (if not tree). Each returns None — exactly what except Exception at :102 already yields for the same input. Proven by mutation: deleting :93-94, :96-97 or :99-100 individually leaves all 12 version tests green. Coverage confirms :97 and :100 are never executed by any test. There is repo precedent for isinstance-checking parsed JSON (devlaunch/worktree/storage.py:104,143,153), but it does not transfer — there the check sits under a narrow except (OSError, ValueError) and produces a distinct action. Speculative Generality; :86 and the dir_info.get("editable") check at :90 are the two that describe the real PEP 610 schema.
  2. The format string is assembled in two places. devlaunch/dl.py:101 returns the fragment "dev, editable from {tree}"; :113 supplies the parens. Neither location owns the rendered string, and the fragment is only well-formed inside the parens its caller adds. Returning the tree path and letting get_version write f"{base} (dev, editable from {tree})" puts the format in one place.
  3. aid --help was left behind. devlaunch/aid.py:169 still reads --version Show version while dl's help line was elaborated at devlaunch/dl.py:1171, and README.md:237 asserts "aid --version reports the same thing under its own name." Given CLAUDE.md's docs-in-sync rule, this is the one line now out of step with behavior the PR itself changed.
  4. Placement. devlaunch/dl.py is 1569 lines and already owns orchestration, cache, help and dispatch. Version+provenance is now ~45 self-contained lines with its own dependencies (importlib.metadata, urllib) and a cross-module consumer at devlaunch/aid.py:194. A devlaunch/version.py would match the layout of completion.py / gh_auth.py / workspace_id.py and stop aid importing the 1569-line module for one string. Not obligatory — but this PR is the moment "version" stopped being one line.
  5. 0.0.9 is hardcoded in both worked examples (README.md:231, AGENTS.md:42). They go stale at the next bump; dl <version> (dev, editable from …) reads the same and cannot drift. Related: README.md:225-239 and AGENTS.md:41-44 are now near-duplicate prose that must move together.
  6. The dist name "devlaunch" is a literal in two placesdevlaunch/dl.py:85 and :109. Minor, but they must agree or the provenance describes a different dist than the version.

Spec

Spec is #84. Every "Done when" bullet is met.

Spec line Result
"A test asserts an editable install (dir_info.editable == true) yields output naming it as dev and containing the tree path from url" Met — test/test_dl.py:817-828
"A test asserts a non-editable install yields the current bare dl <version> output, unchanged" Met — test/test_dl.py:838-843
"Tests cover the three degradation cases above, each falling back to bare output rather than raising" Met, plus a fourth (reader raises) — test/test_dl.py:845-875
"pixi run ci is green" All GitHub checks green on head; 313 tests pass in the scratch checkout
"README updated if it shows --version output" Met — README.md:223-238; AGENTS.md:41-44 corrected, and it previously asserted the opposite

Each hard constraint verified independently, by execution:

  • "Provenance detection is strictly additive: it must never make --version fail." Held. The probe you were most likely to have missed — distribution() itself raising — is covered: PackageNotFoundError subclasses ImportError subclasses Exception, so devlaunch/dl.py:102 catches it. Executed and confirmed bare for: PackageNotFoundError from distribution(), OSError from the reader, MemoryError, None, "", "{not json", [1,2,3], null, dir_info a string, url a dict, editable: 0. KeyboardInterrupt and SystemExit correctly still propagate. json is imported at devlaunch/dl.py:23, so there is no NameError path.
  • "a non-editable install yields the current bare output, unchanged." Held — and verified against reality, not just against a stub. The released conda build on this machine records {"dir_info": {}, "url": "file:///home/runner/work/devlaunch/devlaunch/output/bld/rattler-build_devlaunch_.../work"} (the recipe's pip install . at conda.recipe/recipe.yaml:19 does write PEP 610 metadata — it is present but non-editable, not absent). Fed through the new code it returns exactly 0.0.9. This closes the PR's own "not verified" caveat on the released path.
  • "do not stat paths or pattern-match against ~/.pixi or ~/.local, and do not hardcode any /home/<user> path." Held. devlaunch/dl.py:69-103 contains no stat/exists/is_dir/Path(, no .pixi/.local, no /home/.
  • "url supplies the tree to name", parsed not stripped. Held — and the test discriminates: swapping url2pathname(parsed.path) for url.replace("file://","") fails test/test_dl.py:830-835. Executed: %20→space, %C3%A5å, %23#.
  • "PackageNotFoundError is already handled at the existing call site; keep that behavior." Held — devlaunch/dl.py:110-112 short-circuits to "unknown" before provenance runs.

No pre-existing test was weakened. git diff b304cb6...HEAD -- test/ touches only test/test_dl.py and contains zero deletion lines. TestGetVersion (test/test_dl.py:782-798) is untouched, as is test/unit/test_aid.py — including test_version_matches_dl at :162, which still compares CLI output to a live get_version().

Nothing else parses --version output. Grepped scripts/, conda.recipe/, .github/, completions, pyproject.toml. Consumers are devlaunch/aid.py:194, dev.sh:108, dev-pixi.sh:20 — all print for humans. devlaunch/completions/dl.bash knows the flag name only.

Mutation testing — the tests are not vacuous. Two deliberate breaks in a scratch copy each turn a test red:

Mutation Caught by
drop the dir_info.get("editable") guard test_non_editable_install_reports_bare_version (test/test_dl.py:843)
url2pathname(parsed.path)url.replace("file://","") test_percent_encoded_tree_path_is_decoded (test/test_dl.py:835)

All 7 new tests also fail against the base devlaunch/dl.py.

Nits

  1. Red-before-green is not evidenced by the history. git log b304cb6..HEAD is two commits: fda3cb3 carries implementation and all 7 tests together, then 12fb310 is a codespell fixup. The tests are real and do go red against the base — but they fail there with AttributeError: module 'devlaunch.dl' does not have the attribute 'distribution', i.e. because the @patch target is missing, not because they observe the old behavior. That is a mechanically-red step, not a behaviorally-red one. The four degradation tests are additionally satisfied by any implementation that never adds provenance, which is inherent to negative tests, but worth naming.
  2. dir_info.get("editable") is a truthiness test (devlaunch/dl.py:90). A JSON "editable": "false" — a string — reports the install as dev. PEP 610 mandates a boolean and no real installer emits a string, so the risk is ~0; dir_info.get("editable") is True would be exact.
  3. parsed.netloc is discarded (devlaunch/dl.py:95-98). file://otherhost/srv/tree names /srv/tree as a local tree. Unreachable from a compliant installer, which writes an empty authority.
  4. A raw unencoded # or ? in the url truncates silently rather than falling back — file:///srv/a#b/devlaunchdev, editable from /srv/a, a wrong path. Not a spec breach (--version does not fail), and I confirmed pathname2url encodes both, so /srv/a#b/devlaunch is actually written as file://///srv/a%23b/devlaunch and decodes correctly. Unreachable in practice.
  5. Provenance describes the installed dist's metadata, not the module that actually got imported. Running python -m devlaunch.dl from a checkout inside an environment holding a released devlaunch prints the bare version while executing tree code — the exact ambiguity the ticket set out to remove. Closing it would mean comparing devlaunch.__file__ to the reported tree, which the ticket forbids ("do not stat paths"), so this is a consequence of the constraint rather than a defect. A line in the _install_provenance docstring naming the limit would be worth having.

Verdict

Approve (with nits). No blocking findings on either axis.

The mechanism is the right one: the fact comes from metadata the installer wrote, so it holds for any user and any prefix, and the CLAUDE.md path rule is respected without contortion. Every degradation path I could construct — including the one the review brief flagged as the likely miss, distribution() raising PackageNotFoundError — returns the bare version, and KeyboardInterrupt/SystemExit correctly still propagate. The released path, which the PR body listed as unverified, checks out against the real conda build's actual direct_url.json.

Nothing here needs to block the merge. The two I would actually spend a commit on:

  • Nit 3devlaunch/aid.py:169 help line, since it is a docs-sync gap this PR created and CLAUDE.md makes that rule explicit.
  • Nit 1 — dropping the three unreachable guards at devlaunch/dl.py:93,96,99, which no test pins and the blanket handler already covers.

Both are one-line changes and neither affects behavior.

@blooop
blooop merged commit beaa5b2 into main Aug 7, 2026
9 checks passed
@blooop
blooop deleted the wayfinder/devlaunch-84 branch August 7, 2026 22:31
blooop pushed a commit that referenced this pull request Aug 7, 2026
PR #85 merged into main after this release branch was cut, so the 0.0.10
section did not mention it. publish.yml parses these notes for the release,
so an omission here is an omission from the published release notes.
blooop pushed a commit that referenced this pull request Aug 7, 2026
PR #85 merged into main at 22:31Z; the 0.0.10 release branch had been cut at
22:15Z and merged without picking it up, so the shipped notes omit a change
that is in the shipped code.

Documentation only — no version bump, so publish.yml sees no version increase
and will not republish.
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.

Build: report install provenance in dl --version

1 participant