Skip to content

Pin-aware audit rendering with notes column#78

Merged
CybotTM merged 8 commits into
mainfrom
feat/audit-pins-and-notes
Apr 21, 2026
Merged

Pin-aware audit rendering with notes column#78
CybotTM merged 8 commits into
mainfrom
feat/audit-pins-and-notes

Conversation

@CybotTM
Copy link
Copy Markdown
Member

@CybotTM CybotTM commented Apr 21, 2026

Summary

Three gaps surfaced in make audit:

  1. Install method invisible. installed_method (apt/cargo/uv/pipx/nvm/brew/manual) was captured per-tool but never rendered.
  2. Pinning invisible and broken. catalog.is_pinned() read a pinned_version catalog field that zero JSON files set, while the real pin store at ~/.config/cli-audit/pins.json was never read by the Python side.
  3. Canned hints. Every not-installed / outdated multi-version row carried Install python 3.13: check your package manager or version manager — hardcoded, tautological, adding no information over tool name + state.

Changes

Atomic commits, each independently revertable:

  1. refactor(catalog) — remove unused pinned_version field + is_pinned/get_pinned_version/should_skip methods. Documentation rewritten to point at the real pin store and shell helpers.
  2. feat(pins) — new cli_audit/pins.py that reads ~/.config/cli-audit/pins.json (flat and nested schemas, tool@cycle, \"never\" sentinel). Rewire the one non-render caller in audit.py.
  3. refactor(audit) — delete the hint plumbing end-to-end: generators in audit.py, CLI_AUDIT_HINTS env var, Makefile.d/user.mk overrides, three doc files.
  4. feat(audit) — pin-aware rendering:
    • Header: state|tool|installed|latest_upstream|notes (5 columns, up from 4; smoke test's 6-column assertion was silently passing against a 4-column reality).
    • Notes: method · auto.
    • Pins render in the installed column next to the version they constrain: 8.5.3 [PIN:8.5.3], [PIN:never] on absent rows.
    • Status respects pins: only when installed matches pin (regardless of upstream latest); ⚠️ CONFLICT when pin is violated; PIN:never+absent is (intent honored) instead of .
    • print_summary uses the same pin-aware status so the readiness tallies match the icons.
  5. docs(adr) — ADR-009 proposes first-class multi-install support. Implementation is follow-up.

Before / after (rows from this repo)

before:
❌  python@3.13                  3.13.13    PIN:never · auto · Install python 3.13: check your package manager or version manager
⬆   python@3.12  3.12            3.12.13    apt · PIN:3.12 · auto · Upgrade python 3.12: 3.12 → 3.12.13
✅  php@8.5      8.5.5            8.5.5      apt · PIN:8.5.3           ← green but pin was violated

after:
✅  python@3.13           [PIN:never]  3.13.13    auto
✅  python@3.12  3.12 [PIN:3.12]      3.12.13    apt · auto
⚠️   php@8.5     8.5.5 [PIN:8.5.3]    8.5.5      apt

Test plan

  • 546 pytest tests pass (unchanged count).
  • ./scripts/test_smoke.sh passes against the new 5-column header.
  • make audit — spot-checked regular tools (ripgrep, jq, cargo tools), multi-version runtimes (python@, php@, node@, ruby@, go@), and the readiness summary tallies.
  • flake8 clean on changed files.
  • Reviewer: verify your own pins.json renders as expected — [PIN:…] next to installed, PIN:never honored on absent rows.

CybotTM added 5 commits April 21, 2026 14:09
The ``pinned_version`` field on ``ToolCatalogEntry`` and the
``is_pinned`` / ``get_pinned_version`` / ``should_skip`` methods on
``ToolCatalog`` were never populated — zero ``catalog/*.json`` files
set them, so the ``[PINNED]`` marker in the audit renderer could
never fire from real data. Pins live per-user in
``~/.config/cli-audit/pins.json`` (managed by the shell helpers under
``scripts/``); the catalog-level mechanism was a dead parallel path.

Documentation rewritten to point at the real pin store and the
``scripts/pin_version.sh`` / ``unpin_version.sh`` helpers.

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Add ``cli_audit/pins.py`` that reads
``~/.config/cli-audit/pins.json`` — the file the shell helpers
(``scripts/pin_version.sh``, ``unpin_version.sh``,
``reset_pins.sh``) already populate. The Python side now understands:

- flat ``{"tool": "version"}`` pins
- nested ``{"tool": {"cycle": "version"}}`` for multi-version runtimes
- the ``"never"`` sentinel meaning "do not install or update"
- ``tool@cycle`` lookup for names like ``python@3.13``

``audit.py`` (update path) switches from the now-removed catalog
methods to the new API.

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
``make audit`` was emitting hardcoded hints like ``Install python 3.13:
check your package manager or version manager`` on every not-installed
and outdated multi-version row. They added no information — the tool
name and state already tell the user what action is possible — so rip
them out everywhere:

- ``audit.py``: stop generating the canned strings in
  ``audit_multi_version_tool`` and in the local-only update path;
  delete the ``CLI_AUDIT_HINTS`` env var and ``SHOW_HINTS`` plumbing.
  Catalog-provided ``hint`` values are still honored.
- ``Makefile.d/user.mk``: drop ``CLI_AUDIT_HINTS=1`` from every audit
  target (it was overriding the env default anyway).
- ``docs/``: remove ``CLI_AUDIT_HINTS`` from CLI / quick / deployment
  references.

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Three rendering gaps surfaced in ``make audit``:

1. Install method (apt / cargo / uv / nvm / …) was collected but never
   shown.
2. Version pins from ``pins.json`` were invisible.
3. Auto-update flags from ``config.yml`` were invisible.

Fixes:

- Header grows from 4 to 5 columns, adding ``notes`` =
  ``method · auto``. Smoke test updated to match (it was asserting
  6 columns against a 4-column reality — silent pass via ``|| true``).
- PIN marker renders next to the version it constrains (installed
  column), e.g. ``3.12.7 [PIN:3.12.7]`` or ``[PIN:never]`` on
  not-installed rows.
- Status icon computation now respects pins: a tool pinned to X with
  installed == X is ``✅`` regardless of upstream latest; installed != X
  is ``⚠️ CONFLICT`` (pin being violated); ``PIN:never`` + not installed
  is ``✅`` (intent honored). Previously ``✅`` could render on rows
  whose installed version diverged from the pin.
- Summary tallies use the same pin-aware status so "Readiness:
  N outdated, N missing" matches the visible icons.
- ``status_icon`` consults ``status`` before the empty-installed
  short-circuit so ``PIN:never`` rows don't show red-X for absence
  that is the intended state.

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Propose treating each discovered installation of a tool as
individually addressable. Current asymmetry: detection finds every
binary on ``PATH``, ``choose_highest()`` discards everything but one
before snapshot write, and only ``reconcile.py`` re-detects the rest
— purely to delete them.

ADR-009 spells out the data model (``Installation`` dataclass, stable
``tool#method`` keys), snapshot schema bump, per-install pins
(``"ripgrep#apt": "14.0.0"``), per-install upgrade, the narrowed role
for ``reconcile``, open questions, and a phased rollout.

Status: Proposed — companion to the rendering work landed in the
earlier commits of this branch; implementation is follow-up.

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Copilot AI review requested due to automatic review settings April 21, 2026 12:11
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 21, 2026

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 21, 2026

Codecov Report

❌ Patch coverage is 84.42623% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.10%. Comparing base (7c9af6e) to head (d864d81).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
cli_audit/render.py 69.23% 16 Missing ⚠️
cli_audit/pins.py 95.65% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #78      +/-   ##
==========================================
+ Coverage   66.78%   69.10%   +2.32%     
==========================================
  Files          21       22       +1     
  Lines        3230     3301      +71     
==========================================
+ Hits         2157     2281     +124     
+ Misses       1073     1020      -53     
Flag Coverage Δ
unittests 69.10% <84.42%> (+2.32%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

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

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the version pinning system by moving pin configurations from the tool catalog to a user-local JSON file, supported by a new cli_audit.pins module. It simplifies the audit output by removing redundant remediation hints and updating the table rendering to include a notes column for installation methods and auto-update status. Additionally, a new Architecture Decision Record (ADR-009) is introduced to propose support for multiple tool installations. Feedback identifies the need for better error reporting during pin file parsing, the removal of an unused parameter in the status logic, and a fix for a conflict detection bug that fails when ANSI color codes are present.

Comment thread cli_audit/pins.py Outdated
Comment thread cli_audit/render.py Outdated
Comment thread cli_audit/render.py Outdated
Self-review of the PR surfaced three things worth addressing before
a reviewer sees them:

- ``_apply_pin_to_status`` lived in ``render.py`` but it's pin logic,
  not display logic. Moved into ``pins.py`` as public
  ``apply_pin_to_status`` (no ``latest`` arg — it was unused) and
  exported from the package API.
- Inline ``from cli_audit.pins import …`` statements inside the
  ``cmd_update`` loop and ``_render_tool_row`` hot path promoted to
  module-level imports.
- ``tests/test_pins.py``: 30 tests covering ``load_pins``,
  ``lookup_pin``, ``is_pinned``/``is_never``, ``should_skip``, and the
  state matrix for ``apply_pin_to_status``. One test caught a real
  ambiguity — looking up ``tool@cycle`` against a flat ``{tool:
  "x.y"}`` pin returned the flat value; shape-mismatched lookups now
  return empty instead, and the docstring says so.

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the audit output to be pin-aware and more informative by rendering user pins from ~/.config/cli-audit/pins.json, adding a notes column (install method + auto-update flag), and removing the previously hardcoded “hints” plumbing.

Changes:

  • Add a Python pins reader (cli_audit/pins.py) and rewire pin usage away from the catalog.
  • Update audit rendering to 5 columns with pin-aware status icons and summary tallies.
  • Remove the CLI_AUDIT_HINTS feature and update docs/smoke test accordingly.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
scripts/test_smoke.sh Updates the smoke test to assert the new 5-column header and notes column.
docs/adr/ADR-009-first-class-multi-install.md Adds an ADR proposing first-class multi-install support (follow-up work).
docs/QUICK_REFERENCE.md Removes references to the deleted CLI_AUDIT_HINTS env var.
docs/DEPLOYMENT.md Removes references to the deleted CLI_AUDIT_HINTS env var.
docs/CLI_REFERENCE.md Removes CLI_AUDIT_HINTS from the env var reference table.
docs/CATALOG_GUIDE.md Updates pinning docs to point to pins.json and shell helpers (needs a small rendering-location fix).
docs/API_REFERENCE.md Removes pinned_version from the catalog entry API docs.
cli_audit/render.py Adds notes column, pin suffix rendering, and pin-aware status/summary logic.
cli_audit/pins.py New module for reading pins from the user-local pins file.
cli_audit/catalog.py Removes unused catalog pin fields/methods (pinned_version, is_pinned, should_skip, etc.).
cli_audit/init.py Exposes pins helpers from the package top-level API.
audit.py Removes hints env var usage, wires rendering to new signature, and uses pins for update progress markers.
Makefile.d/user.mk Removes CLI_AUDIT_HINTS from targets (needs smart_column alignment + help-text tweak).
Comments suppressed due to low confidence (1)

cli_audit/render.py:282

  • installed_display.startswith("CONFLICT:") will never match when colors are enabled, because installed_display has ANSI color prefixes after colorize(). Strip the CONFLICT: prefix from the raw installed string before colorizing (or strip ANSI escapes before checking) so conflict messages render cleanly in the default colored output.
    parts = [f"{total} tools", f"{outdated} outdated", f"{missing} missing"]
    if conflicts > 0:
        parts.append(f"{conflicts} conflicts")

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cli_audit/pins.py
Comment thread cli_audit/pins.py
Comment thread docs/CATALOG_GUIDE.md Outdated
Comment thread Makefile.d/user.mk Outdated
Comment thread Makefile.d/user.mk Outdated
Comment thread cli_audit/render.py
CybotTM added 2 commits April 21, 2026 14:19
Three issues raised on the initial review:

1. ``pins.py`` silently swallowed ``json.JSONDecodeError`` and
   top-level non-dict payloads. A malformed ``pins.json`` would render
   as if no pins existed with zero diagnostic feedback. Now each
   failure mode logs a ``WARNING`` naming the file and the failure
   reason.

2. ``render.py`` had an unused ``latest`` parameter on the
   now-relocated ``apply_pin_to_status``. Dropped (was already gone
   in ``pins.apply_pin_to_status``; this just removes the stale
   reviewer hit on the earlier function-local copy).

3. ``render.py``: the ``CONFLICT:`` sentinel strip ran against the
   already-colorized ``installed_display`` (``\\033[33mCONFLICT:…``),
   so the ``startswith`` check never matched when color was enabled —
   i.e. always under ``make audit``. Strip the sentinel from the raw
   ``installed`` string before colorization.

Tests:
- ``tests/test_pins.py`` gains warning-log assertions for malformed
  JSON and non-dict payloads.
- ``tests/test_render.py`` added, including a regression test for the
  CONFLICT-prefix / ANSI interaction.

585 tests pass; new files lint clean.

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
Copilot's review raised three additional issues:

1. ``Makefile.d/user.mk``: ``smart_column.py --right 3,5`` was
   right-aligning the new ``notes`` text column and leaving the
   ``latest_upstream`` version column left-aligned. Swap to ``3,4``
   so both version columns (installed, latest) right-align and the
   notes column stays left-aligned. Applied to every audit target.

2. ``Makefile.d/user.mk``: the ``audit-offline`` help string still
   said "with hints" after canned hints were removed. Updated to
   "fast local scan, no network".

3. ``docs/CATALOG_GUIDE.md``: the "Pinning Versions" section said
   pins render in the ``notes`` column, but the final layout puts
   ``[PIN:…]`` in the ``installed`` column (next to the version the
   pin constrains). Docs corrected to match.

Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
@CybotTM CybotTM merged commit 219495e into main Apr 21, 2026
18 checks passed
@CybotTM CybotTM deleted the feat/audit-pins-and-notes branch April 21, 2026 12:26
CybotTM added a commit that referenced this pull request Apr 21, 2026
## Summary

Follow-up to #78 (merged). Three issues surfaced once users saw real pin
data in the audit table:

1. **Cycle vs patch pins were indistinguishable.**
`~/.config/cli-audit/pins.json` stores per-cycle pins as `{cycle:
value}` but `value` can mean either "hold this cycle, any patch"
(`"3.12"`) or "hold this exact patch" (`"3.12.7"`). The previous code
treated every non-`never` pin as exact-version, flagging valid
cycle-holds as conflicts.
2. **Stale skip-markers shouted as conflicts.** `guide.sh` pins to
`$installed` or `$latest` from several code paths (pin-on-failure
prompt, `s` skip). When the world moved past those pins, my rendering
escalated them to `⚠️` even though the pin no longer applies
meaningfully.
3. **Auto-update state was invisible.** User-configured `auto_update:
true` existed in `config.yml` but had no marker on rows.

## Changes

Atomic commits:

1. `feat(pins)` — new `classify_pin(pin, cycle)` returns
`none|never|cycle|version`. `apply_pin_to_status` grows a `cycle`
parameter: cycle-holds are honored when installed is within-cycle;
multi-version rows with stale patch-pins pass their upstream status
through (no CONFLICT escalation); single-version tools still conflict on
mismatch. New `pin_label()` produces `PIN:never` / `CYCLE:3.12` /
`PIN:x` / `PIN:x stale` — one source of truth for the rendered suffix.
2. `feat(render)` — cycle threaded into status computation; pin + AUTO
combined in a single bracketed suffix on the installed column (`3.14.4
[AUTO]`, `3.12.7 [CYCLE:3.12 AUTO]`, `8.5.5 [PIN:8.5.3 stale]`); AUTO
suppressed when pin is `never` (contradictory state). Notes column slims
to just the install method.
3. `feat(scripts)` — `scripts/reset_pins.sh --stale [--dry-run]` removes
only stale patch-level pins (installed present, pin != installed, pin !=
cycle, pin != `never`). Cycle-holds, `never` pins, and honored
patch-holds are preserved.

## Before / after on this repo

```
before:                                              after:
⚠  php@8.5   8.5.5 [PIN:8.5.3]      apt            ✓  php@8.5   8.5.5                 apt
⚠  php@8.4   8.4.20 [PIN:8.4.18]    apt            ✓  php@8.4   8.4.20                apt
✓  python@3.12  3.12 [PIN:3.12]     apt            ✓  python@3.12  3.12 [CYCLE:3.12 AUTO]  apt
✓  python@3.14  3.14.4              manual · auto  ✓  python@3.14  3.14.4 [AUTO]      manual
⚠  ruby@3.3  3.3.6 [PIN:never AUTO] manual · auto  ⚠  ruby@3.3  3.3.6 [PIN:never]     manual
```

Stale pins in the user's `pins.json` (`php@8.5: 8.5.3`, `php@8.4:
8.4.18`) cleaned with the new `--stale` flag; config is now internally
consistent.

## Test plan

- [x] `uv run pytest` — 608 passing (up from 585; 23 new tests: 17 in
`test_pins.py` covering `classify_pin` / cycle-aware
`apply_pin_to_status` / `pin_label`, 6 in `test_render.py` covering
cycle-hold / stale / single-version conflict / AUTO visibility /
base-tool inheritance).
- [x] `./scripts/test_smoke.sh` passes.
- [x] `flake8` clean on changed files.
- [x] `scripts/reset_pins.sh --stale --dry-run` dry-runs correctly
against the real snapshot.
- [ ] Reviewer: verify your `pins.json` classifies as expected — run
`bash scripts/reset_pins.sh --stale --dry-run`.
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.

2 participants