Skip to content

feat(cli): report the version from both command-line tools - #144

Merged
L4XB merged 1 commit into
mainfrom
feat/cli-version
Sep 15, 2026
Merged

L4XB merged 1 commit into
mainfrom
feat/cli-version

Conversation

@L4XB

@L4XB L4XB commented Sep 15, 2026

Copy link
Copy Markdown
Member

Summary

Neither command-line tool could say which version it was. sixsentences --version
exited 2 with the following arguments are required: command, and
six-community --version exited 2 with No such option: --version. The answer
to the first question in every bug report involved reading pyproject.toml
inside a container.

Both now report it, from the installed distribution metadata.

Behavior and compatibility

Command Before After
sixsentences --version exit 2, usage error sixsentences 0.2.0a1, exit 0
six-community --version exit 2, No such option six-community 0.2.0a1, exit 0

Two details the issue asked to check rather than assume:

  • argparse with required=True subcommands. --version works. argparse
    resolves an eager version action while it consumes the option string, so it
    exits before the required-subcommand check at the end of parsing. Pinned by
    test_version_flag_answers_before_a_subcommand_is_demanded.
  • Typer. It does need a root callback with is_eager=True; without one the
    option does not exist at the root at all. The callback carries no required
    parameters, so no_args_is_help still prints help and exits 2, and every
    subcommand is still reachable — test_the_root_callback_does_not_swallow_a_subcommand
    covers that. Its docstring now gives six-community --help a description line
    it did not have.

The second literal

services/api/src/sixsentences_server/__init__.py held __version__ = "0.2.0a1"
as a hard-coded string, and nothing validated it. release.py validate
checks version agreement across pyproject.toml, uv.lock, the API project and
its lock, package.json for web and extension, CITATION.cff, CHANGELOG.md,
the release notes and the Companion plist — but not that line. It reaches users
through GET /health, the MCP server identity, the methods paragraph a
researcher cites, and the generator field of exported documents, so a stale
value there is not cosmetic.

It now derives from the installed distribution, exactly as
sixsentences/__init__.py already did, and falls back to 0+unknown for a
source tree imported without installation — an honest answer rather than a
plausible wrong one. No new literal was added anywhere.

Public API, schema, migrations, rollback: none. --version is additive;
reverting the commit restores the previous behaviour with no state to undo.

Validation

Engine (Python 3.14 locally, CI covers 3.12–3.14):

$ uv run ruff check src tests .github/scripts          # All checks passed!
$ uv run ruff format --check src tests .github/scripts # 64 files already formatted
$ uv run mypy src/sixsentences                         # no issues found in 40 source files
$ uv run pytest -q                                     # 357 passed  (355 before)
$ uv build                                             # sdist + wheel
$ uv run sixsentences --version                        # sixsentences 0.2.0a1  (exit 0)
$ uv run sixsentences                                  # usage error (exit 2, unchanged)
$ uv run sixsentences query "a AND b"                  # exit 0, unchanged

API:

$ uv run --frozen --no-sync ruff check src scripts tests    # All checks passed!
$ uv run --frozen --no-sync mypy src                        # no issues found in 208 source files
$ uv run --frozen --no-sync pytest -q                       # 2904 passed, 1 skipped  (2901 before)
$ python scripts/audit_community_export.py services/api --refresh-manifest
  community export manifest refreshed: added:tests/test_cli.py,
  changed:src/sixsentences_server/__init__.py, changed:src/sixsentences_server/cli.py
$ python scripts/audit_community_export.py services/api
  community export audit passed: 405 files, 400 HTTP operations, 1 WebSocket, 89 tables
$ uv run --frozen --no-sync python scripts/check_web_contracts.py --require-complete
  Web contracts: 349/349 implemented; 0 missing.
$ uv run --frozen --no-sync six-community --version         # six-community 0.2.0a1  (exit 0)
$ uv run --frozen --no-sync six-community                   # help, exit 2, unchanged
$ uv run --frozen --no-sync six-community doctor            # unchanged output

Web, browser extension, macOS Companion and the self-hosting definition are
untouched by this change.

One thing worth splitting out

The audit command CONTRIBUTING.md documents cannot pass on a machine set up
the way CONTRIBUTING.md says to set it up. uv sync --project services/api
creates services/api/.venv, included_files() walks it, and the first symlink
it meets ends the run:

community export audit failed: symlink:.venv/bin/python

CI does not hit it because the api job sets
UV_PROJECT_ENVIRONMENT=/tmp/sixsentences-api-env, which puts the environment
outside the audited tree. .venv is simply missing from IGNORED_DIRS
alongside .mypy_cache, .pytest_cache, .ruff_cache and __pycache__.

Filed separately rather than fixed here: it is a behaviour change in a gate, and
it has nothing to do with --version.

  • Engine checks pass.
  • API, migration, worker, and web-contract checks pass.
  • Web type-check, tests, and production build pass, or the web app is unaffected. — unaffected
  • Browser-extension contracts and a deployment-bound build pass, or the extension is unaffected. — unaffected
  • macOS Companion boundary check, locked resolution, tests, and release build pass on the pinned Xcode toolchains—or the Companion is unaffected. — unaffected
  • Self-hosting tests and container builds pass, or deployment is unaffected. — the image installs the server as a distribution, so metadata is present; CI builds both images
  • User-facing behavior has a focused test or the omission is explained.

Review boundaries

  • Security and privacy effects were reviewed. A version string is already
    public in GET /health; nothing new is exposed, and no credential, path or
    configuration value is printed. Authentication, tenancy, capability URLs,
    uploads, retention, deletion, browser state and logs are untouched.
  • New network calls and processors are operator-configurable, fail closed, and document data egress, cost, retention, and failure behavior—or none were added. — none
  • Dependencies and bundled assets are justified, locked, and redistribution-compatible—or none were added. — none; importlib.metadata is stdlib
  • Native-client changes include explicit origin, local-retention/deletion, permission, signing, update, and binary-distribution implications. — none
  • Research-method assumptions, limitations, and provenance remain visible—or
    no research-facing behavior changed. The methods paragraph and export
    generator field now carry the installed version instead of a literal that
    nothing checked, which makes provenance more accurate, not less.
  • Accessibility and keyboard behavior were reviewed for UI changes—or no UI changed. — no UI
  • Browser permissions, capture bounds, pairing callbacks, extension storage, and generated host access were reviewed—or the extension is unaffected. — unaffected

Source-release hygiene

  • No secret, private key, production configuration, customer/participant data, user upload, database dump, log, private prompt, or non-redistributable research content is included.
  • The change belongs in the community stack; payment, subscription, commercial-plan, hosted-administration, and marketing-site code remains separate.
  • Public behavior and limitations are documented.
  • CHANGELOG.md is updated for user-visible changes.
  • Every commit carries my own matching DCO Signed-off-by trailer.
  • I have read CLA.md and posted its exact acceptance sentence as a standalone pull-request comment.
  • I have read and will follow the Code of Conduct.

Visual evidence

No UI change.

Closes #89

`sixsentences --version` and `six-community --version` both failed, so the
first thing a bug report asks for was only reachable by reading
`pyproject.toml` inside a container.

Both now read the installed distribution metadata. argparse resolves an eager
`version` action while it consumes the option, so the engine answers before its
required subcommand is enforced; Typer needs a root callback with an eager
option to do the same.

`sixsentences_server.__version__` was a second hard-coded literal that nothing
validated — `release.py validate` checks the version across pyproject files,
lockfiles, the changelog, the citation metadata and the Companion, but not that
string. It now derives from the installed distribution the same way the engine
already did, so there is one source and nothing to keep in step by hand.

Closes #89

Signed-off-by: L4XB <L4XB@users.noreply.github.com>
@L4XB

L4XB commented Sep 15, 2026

Copy link
Copy Markdown
Member Author

I have read and agree to the SixSentences CLA v1.0.

@L4XB

L4XB commented Sep 15, 2026

Copy link
Copy Markdown
Member Author

Self-review

Recorded under the founding-maintainer exception in GOVERNANCE.md. Every
changed hunk, in diff order.

CHANGELOG.md — replaces the Nothing yet. placeholder under
[Unreleased] / Added and opens a Changed subsection. Keep a Changelog order
(Added before Changed) is preserved. release.py validate only requires the
## [<public version>] - <date> heading for the version being released, so an
[Unreleased] section with real content does not affect the release gate.

services/api/COMMUNITY_EXPORT_MANIFEST.json — three entries, all expected
and all accounted for: src/sixsentences_server/__init__.py (83 → 316 bytes),
src/sixsentences_server/cli.py (48,653 → 49,196), and the new
tests/test_cli.py (1,095). Nothing else moved, which is the useful part of
reading this file: it confirms no unintended file entered or left the audited
tree. Regenerated with --refresh-manifest, which runs every other gate first
and rewrites only after they pass.

services/api/src/sixsentences_server/__init__.py — the literal becomes a
metadata lookup. Reviewed for three risks:

  • Circular import. None. The module imports only importlib.metadata, and
    cli.py imports the name from it rather than the reverse.
  • __all__ on a package with submodules. from sixsentences_server import jobs and import sixsentences_server are unaffected; __all__ only governs
    import *, and there is none in the tree (checked).
  • The fallback. PackageNotFoundError is caught narrowly. A source tree run
    without installation now reports 0+unknown where it previously reported
    0.2.0a1 — this is the honest direction: /health, the MCP identity, the
    methods paragraph and the export generator field would otherwise state a
    version nothing had verified. The image installs the distribution
    (uv sync --no-editable), so deployments report the real version.

services/api/src/sixsentences_server/cli.py

  • The import is placed in the existing first-party block; ruff's isort rules
    pass, so ordering is not a matter of taste here.
  • _print_version is invoked by Click on every run, including with
    requested=False. The guard returns immediately; no work is done on the
    normal path.
  • root() never reads its version parameter. That is the Typer idiom — the
    effect lives in callback= — and it reads as dead code at a glance. Accepted
    because the alternative (reading it in the body) would run after subcommand
    dispatch, which is exactly the behaviour is_eager=True exists to avoid.
  • raise typer.Exit() from inside a parameter callback exits 0. Verified by
    running it, not by reading Click's source.
  • The callback's docstring becomes the application help description. six-community --help gained a line it did not have; it says nothing that was not already
    true.
  • no_args_is_help still applies: bare six-community prints help and exits 2,
    unchanged. Verified.

services/api/tests/test_cli.py (new) — three tests. The middle one is the
one that matters long-term: it fails if anyone reintroduces a literal that
drifts from pyproject.toml, which is the failure mode this change removes and
which release.py validate does not cover.

src/sixsentences/cli.py--version registered before
add_subparsers(..., required=True). The comment states why that works, and
test_version_flag_answers_before_a_subcommand_is_demanded pins it rather than
trusting the comment. %(prog)s keeps the printed name in step with the parser
name.

tests/test_cli.py — two tests, pytest and tomllib imports added, no
existing test touched. tomllib is stdlib on every supported version
(3.12–3.14).

Checks

Every required context is green. The one failing check, Publish CLA status, is
a cancelled run, not a failure — it is the subject of #146 and PR #148, and it is
not a required context. The required CLA / acceptance status is green.

Merging.

@L4XB
L4XB merged commit fc02478 into main Sep 15, 2026
22 of 23 checks passed
@L4XB
L4XB deleted the feat/cli-version branch September 15, 2026 11:04
L4XB added a commit that referenced this pull request Sep 15, 2026
Every pull request showed a failed check named "Publish CLA status" beside the
green "CLA / acceptance" it had just produced:

    Canceling since a higher priority waiting request for cla-144 exists
    CLA acceptance verified for @L4XB on pull request #144.
    ##[error]The operation was canceled.

The run had already done its work. It was cancelled during cleanup, and GitHub
reports a cancelled run as a failed check.

Two events seconds apart share the concurrency group: opening the pull request
fires `pull_request_target`, and posting the acceptance sentence fires
`issue_comment`. That is not an unlucky ordering — CONTRIBUTING.md instructs the
author to post that sentence on the pull request they just opened, so the
comment reliably lands mid-run. The workflow cancelled itself on the one
sequence the documentation asks every contributor to perform, and showed a red X
next to the word CLA to the people least equipped to read it.

The runs queue now, as issue-claims.yml already does for the same reason. They
are idempotent: each reads the head commit and the comments live and publishes a
status for that exact commit, so serialized runs converge rather than fight, and
the reminder stays behind its marker check. A test plays the real sequence —
open, remind, remind again, accept, re-run — and asserts one reminder and a
stable verdict.

Queueing is also the safer of the two. Cancelling left a window where a
half-finished run could publish a stale verdict; serialized runs mean the one
that finishes last is the one that saw the newest state.

Closes #146

Signed-off-by: L4XB <L4XB@users.noreply.github.com>
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.

cli: report the version from both command-line tools

1 participant