# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

Two deliverables: `nvchecker.toml` (a merged nvchecker config) and `nvtable` (a
Bash script that joins nvchecker's upstream results against the version
installed on this host and the version shipped by the SlackBuild repo that
packages it). No build step, no test framework, no dependencies to install.

## Verifying changes

There is no test suite. These are the checks that actually catch regressions:

```bash
bash -n nvtable                 # syntax only
./nvtable -n -C                 # default view, no state writes, no color
./nvtable -n -C -a              # every tracked package
```

**Always pass `-n` while developing.** Without it `nvtable` runs `nvtake`,
which rewrites `~/.config/nvchecker/old_ver.json` as a side effect of merely
displaying the table.

To validate the config without touching live state, rewrite `[__config__]` to
point at scratch paths and run there:

```bash
sed 's|^oldver.*|oldver = "/tmp/o.json"|; s|^newver.*|newver = "/tmp/n.json"|' \
    nvchecker.toml > /tmp/run.toml
echo '{"version":2,"data":{}}' > /tmp/o.json
nvchecker -c /tmp/run.toml 2>&1 | grep -oE '\] [^:]+: no-result'
```

A stanza that resolves prints `updated to <version>`; a broken one prints
`no-result`. The strongest check on a stanza is that the version it returns
equals the `VERSION=` in that package's `.info` — that caught a stanza pointing
at an entirely wrong upstream project (`mutagen-io/mutagen` vs
`quodlibet/mutagen`) that still resolved cleanly.

`nvtable` reads `new_ver.json` and never refreshes it. After renaming stanzas
the table reports the old names as untracked until `nvchecker` runs again.

## Live state this touches

The repo is self-contained, but the installed tool reaches outside it:

| Path | Role |
|------|------|
| `~/.config/nvchecker/nvchecker.toml` | the deployed copy of this config |
| `~/.config/nvchecker/{old,new}_ver.json` | nvchecker state; `nvtake` writes `old_ver` |
| `~/bin/nvtable` | symlink to the script in this repo |
| `~/.profile.d/qar.sh` | login script; calls `nvtable` where `nvcmp` used to run |

Because `qar.sh` runs on every interactive shell, a bug in `nvtable` shows up
in every new terminal. Test with `-n -C` before touching the deployed copy.

`NVTABLE_CONF` and `NVTABLE_NEWVER` override the config and state paths, which
is how to exercise the real script against scratch data.

## Architecture

`nvtable` is one pass with three joins. The subtlety is entirely in the joins,
not the control flow.

**Name resolution.** The design rule is that a stanza name equals the Slackware
package name, so most rows join with no mapping at all. Two alias maps cover
the rest, and they are separate because the mappings differ per direction:
`ALIAS` maps a stanza to installed package name(s) (`kvantum` →
`kvantum-qt5 kvantum-qt6`, one-to-many), while `REPO_ALIAS` maps a stanza to a
SlackBuild directory (`kitty` → `kitty-bin`). Adding one map for both breaks
the other direction.

**Installed version** resolves through three sources in order:
`/var/log/packages`, the appimage-updater state file, then `flatpak list`.

**Repo version** is the `VERSION=` line from
`<repo>/<category>/<pkg>/<pkg>.info`. Note the category level: `.info` files sit
at depth 3, not 2. The repositories scanned are the `REPOS` array at the top of
the script.

### Traps that produced real bugs here

These are non-obvious and each one silently produced wrong output:

- **A Slackware package filename cannot be parsed by counting dashes.** It is
  `PRGNAM-VERSION-ARCH-BUILD`, but VERSION may itself contain a dash
  (`solvespace-3.1-659215d`). Lookups anchor on the package name being searched
  for, and require the following character to be a digit so `kvantum` does not
  match `kvantum-qt5`.
- **Installed versions carry build metadata upstream never has** (`r8125` is
  `9.018.00_6.18.41`, version plus kernel). `vers_equal` treats installed as
  equal when it merely extends upstream at a component boundary; strict equality
  marks such packages permanently behind.
- **`read` with `IFS=$'\t'` collapses consecutive tabs**, so an empty field
  shifts every later field left. Row records are packed with `|` for this
  reason. Do not "simplify" it back to tabs.
- **Installed-behind and repo-behind are independent conditions.** A row can be
  both (🔴🟡). Collapsing the marker to a single worst-case state hides the
  packaging work.
- **Column padding must happen before color codes are added**, or `printf`
  counts escape bytes as width.

### nvtake

When a package is caught up, its name is collected and one `nvtake` call
records them all, moving `new_ver` → `old_ver` so nvchecker's state tracks this
system rather than only upstream history. `-n` disables it.

The call is wrapped in `flock -n` because `nvtake` does read-modify-write on
`old_ver.json` with no locking of its own, and `qar.sh` runs on every shell — so
several terminals opening at once means concurrent writers. On lock contention
the take is skipped rather than queued; the next run recomputes the same set.

## nvchecker.toml conventions

- **Stanza name = Slackware package name** wherever a package exists. Renaming a
  stanza silently orphans its history in `old_ver.json` (the old key stays,
  nothing matches the new one) and breaks any external consumer that looks
  entries up by name — `appimage-updater` reads this config and requires the
  entries `Feishin` and `cherrytree` verbatim.
- **`# repo=` comments** mark which repository ships each package (`sbo`, `my`,
  `sps`); entries with none are tracked-only.
- **A name containing a dot must be quoted.** `[llama.cpp-vulkan]` parses as a
  nested `llama` table and fails with `no source specified`.
- **`use_latest_release` on GitHub needs `prefix = "v"`** when the tags carry
  one, otherwise the version comes back as `v8.30.1` and never matches a `.info`
  VERSION.
- Prefer `use_latest_release` over `use_max_tag`: `use_max_tag` hits a tightly
  rate-limited endpoint.
- Per-repo `.extras/nvchecker.toml` files are deliberately kept in their own
  repositories for standalone sweeps. This config is the superset; they drift.

The GitHub token lives in `~/.config/nvchecker/keys.toml`, which `.gitignore`
excludes so it cannot be committed alongside the config.
