diff options
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/superpowers/plans/2026-10-20-sb-builder-phase4.md | 328 |
1 files changed, 171 insertions, 157 deletions
diff --git a/docs/superpowers/plans/2026-10-20-sb-builder-phase4.md b/docs/superpowers/plans/2026-10-20-sb-builder-phase4.md index 2608459..1d15e1f 100644 --- a/docs/superpowers/plans/2026-10-20-sb-builder-phase4.md +++ b/docs/superpowers/plans/2026-10-20-sb-builder-phase4.md @@ -2,7 +2,7 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. -**Goal:** Manage slackrepo hints from sb-builder: upstream check on the desktop, hint update/new/delete/fix-current as helper jobs, diff and revert after each. +**Goal:** Manage slackrepo hints from sb-builder: upstream check on the desktop, hint update/new/delete on the VM through the helper, diff and revert after each. **Architecture:** mkhint's hint logic is ported into `remote/sbb-remote` as text edits (stdlib: `re`, `urllib`, `hashlib`), run as job kinds whose argv is the helper itself (`_hint`). The GUI runs `nvchecker` locally over a repo-tracked `nvchecker.toml` (`sb_builder/upstream.py`), shows a Hints screen (`sb_builder/hints_tab.py`) and a diff/revert dialog when a hint job ends. @@ -11,7 +11,7 @@ Spec: `docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md`. Every code step below was run before this plan was written: the diffs apply -to `master` at `8ad8b0b` with `git apply` and the suite ends at 113 tests, +to `master` at `8ad8b0b` with `git apply` and the suite ends at 111 tests, OK. Two `ResourceWarning: subprocess ... is still running` lines are expected (detached `_run`). @@ -19,8 +19,8 @@ expected (detached `_run`). | File | Change | |---|---| -| `remote/sbb-remote` | hint text functions, hint job kinds, `_hint`, `hint-diff`, `hint-revert`, `hint-delete`, `job-result`, config keys `phantom_deps`, `manifest_packages`, hyphenated job ids, VERSION 0.5.0 | -| `remote/config.example.toml` | the two new keys | +| `remote/sbb-remote` | hint text functions, hint job kinds, `_hint`, `hint-diff`, `hint-revert`, `hint-delete`, `job-result`, config key `manifest_packages`, hyphenated job ids, VERSION 0.5.0 | +| `remote/config.example.toml` | the new key | | `sb_builder/__init__.py` | VERSION 0.5.0 | | `nvchecker.toml` | new: the VM's 64 stanzas | | `sb_builder/upstream.py` | new: nvchecker config, JSON log parsing, states, `Checker` | @@ -50,7 +50,7 @@ and old `.bak`; `hint-revert` without `.bak` deletes (reverting a new hint). ```diff diff --git c/tests/test_remote.py i/tests/test_remote.py -index db3d0a1..3e546ef 100644 +index db3d0a1..14622b8 100644 --- c/tests/test_remote.py +++ i/tests/test_remote.py @@ -18,6 +18,9 @@ @@ -71,7 +71,7 @@ index db3d0a1..3e546ef 100644 import tempfile import threading import time -@@ -179,6 +183,275 @@ class KindsTest(Base): +@@ -179,6 +183,255 @@ class KindsTest(Base): sbb.cmd_job_start(self.cfg, "rm", "-rf", "/") @@ -110,16 +110,13 @@ index db3d0a1..3e546ef 100644 + self.assertEqual(sbb.hint_drop(text, r"(?:A|DOWNLOAD\w*)"), "B=plain\n") + + def test_new_hint(self): -+ self.assertEqual(sbb.new_hint(FLATPAK_INFO, ["ostree", "rust-opt"]), ++ self.assertEqual(sbb.new_hint(FLATPAK_INFO), + 'VERSION="1.16.0"\nDOWNLOAD="https://example.org/flatpak-1.16.0.tar.xz"\n' + 'MD5SUM="bdf866646fb312a341f84669f625488b"\n' + 'DOWNLOAD_x86_64=""\nMD5SUM_x86_64=""\n' -+ '#REQUIRES="bubblewrap ostree"\nARCH="x86_64"\nDELREQUIRES="ostree"\n') -+ -+ def test_merge_delrequires(self): -+ self.assertEqual(sbb.merge_delrequires('DELREQUIRES="a"\n', ["b", "a"]), 'DELREQUIRES="a b"\n') -+ self.assertIsNone(sbb.merge_delrequires('DELREQUIRES="a b"\n', ["b"])) -+ self.assertEqual(sbb.merge_delrequires("", ["b"]), 'DELREQUIRES="b"\n') ++ '#REQUIRES="bubblewrap ostree"\nARCH="x86_64"\n') ++ with self.assertRaisesRegex(sbb.Error, "no VERSION"): ++ sbb.new_hint('PRGNAM="x"\n') + + +class Served: @@ -201,7 +198,6 @@ index db3d0a1..3e546ef 100644 + def setUp(self): + super().setUp() + self.www = Served(self, {"x-2.0.tgz": b"two"}) -+ self.cfg["phantom_deps"] = ["rust-opt", "ostree"] + self.info = (f'PRGNAM="x"\nVERSION="1.0"\nDOWNLOAD="{self.www.url}/x-1.0.tgz"\nMD5SUM="old"\n' + 'DOWNLOAD_x86_64=""\nMD5SUM_x86_64=""\nREQUIRES="rust-opt a"\n') + self.hint = os.path.join(self.cfg["hintdir"], "x.hint") @@ -228,7 +224,6 @@ index db3d0a1..3e546ef 100644 + self.assertEqual(sbb.KINDS["hint-update"](self.cfg, ["x=2.0", "--no-dl"]), + (me + ["hint-update", "x=2.0", "--no-dl"], {})) + self.assertEqual(sbb.KINDS["hint-new"](self.cfg, ["x", "2.0"]), (me + ["hint-new", "x", "2.0"], {})) -+ self.assertEqual(sbb.KINDS["fix-current"](self.cfg, []), (me + ["fix-current"], {})) + self.assertFalse(sbb.signs("hint-update", ["x=2.0"])) + + def test_kinds_reject(self): @@ -249,8 +244,7 @@ index db3d0a1..3e546ef 100644 + ("hint-update", ["shadowed=1"], "not managed"), + ("hint-new", [], "hint-new <pkg>"), + ("hint-new", ["x", "1", "2"], "hint-new <pkg>"), -+ ("hint-new", ["neovim", "1"], "bundled deps"), -+ ("fix-current", ["x"], "no arguments")): ++ ("hint-new", ["neovim", "1"], "bundled deps")): + with self.subTest(kind=kind, args=args), self.assertRaisesRegex(sbb.Error, why): + sbb.KINDS[kind](self.cfg, args) + sbb.KINDS["hint-new"](self.cfg, ["neovim"]) # no version: no bundled deps to touch @@ -281,29 +275,15 @@ index db3d0a1..3e546ef 100644 + rc, result, _ = self.run_hint("hint-update", "x=2.0", "--no-dl") + self.assertEqual((rc, result), (0, {"ok": ["x"], "failed": []})) + self.assertEqual(self.read(self.hint), 'VERSION="2.0"\n#REQUIRES="rust-opt a"\nARCH="x86_64"\n' -+ 'DELREQUIRES="rust-opt"\nDOWNLOAD="no"\n') ++ 'DOWNLOAD="no"\n') + self.assertFalse(os.path.exists(self.hint + ".bak")) # revert = delete + + def test_new(self): + self.make_pkg("misc/x", self.info) + rc, result, _ = self.run_hint("hint-new", "x") + self.assertEqual((rc, result), (0, {"ok": ["x"], "failed": []})) -+ self.assertIn('DELREQUIRES="rust-opt"', self.read(self.hint)) -+ self.assertIn('MD5SUM="old"', self.read(self.hint)) -+ -+ def test_fix_current(self): -+ self.make_pkg("misc/x", self.info, hint='DELREQUIRES="b"\n') -+ self.make_pkg("misc/z", 'VERSION="1"\nREQUIRES="ostree"\n') -+ self.make_pkg("misc/w", 'VERSION="1"\nREQUIRES="a"\n') -+ rc, result, _ = self.run_hint("fix-current") -+ self.assertEqual((rc, result), (0, {"ok": ["x", "z"], "failed": []})) -+ self.assertEqual(self.read(self.hint), 'DELREQUIRES="b rust-opt"\n') -+ z = os.path.join(self.cfg["hintdir"], "z.hint") -+ self.assertEqual(self.read(z), 'DELREQUIRES="ostree"\n') -+ self.assertFalse(os.path.exists(z + ".bak")) -+ rc, result, _ = self.run_hint("fix-current") -+ self.assertEqual(result, {"ok": [], "failed": []}) -+ self.assertEqual(self.read(self.hint + ".bak"), 'DELREQUIRES="b"\n') ++ self.assertIn('#REQUIRES="rust-opt a"', self.read(self.hint)) ++ self.assertIn('MD5SUM="old"', self.read(self.hint)) # no version given: no download + + def test_diff_revert_delete(self): + self.make_pkg("misc/x", self.info, hint="A=1\n") @@ -355,17 +335,16 @@ index db3d0a1..3e546ef 100644 ```diff diff --git c/remote/config.example.toml i/remote/config.example.toml -index d275401..a489bff 100644 +index d275401..a77ace9 100644 --- c/remote/config.example.toml +++ i/remote/config.example.toml -@@ -12,3 +12,5 @@ hintdir = "/etc/slackrepo/SBo-danix/hintfiles" +@@ -12,3 +12,4 @@ hintdir = "/etc/slackrepo/SBo-danix/hintfiles" default_hintdir = "" # searched after hintdir when set genreprc = "/root/.genreprc" # REPOSOWNERGPG names the signing key slackrepo_logdir = "/var/log/slackrepo/SBo-danix" -+phantom_deps = [] # deps unneeded on -current, DELREQUIREd by hint-new, fix-current +manifest_packages = ["neovim", "openvino"] # bundled deps: hint-update refuses them (phase 4b) diff --git c/remote/sbb-remote i/remote/sbb-remote -index 25427c0..230dde6 100755 +index 25427c0..2674ed6 100755 --- c/remote/sbb-remote +++ i/remote/sbb-remote @@ -25,6 +25,7 @@ its arguments, this file validates them and builds the argv itself. @@ -387,11 +366,10 @@ index 25427c0..230dde6 100755 DEFAULTS = { "state_dir": "/var/lib/sb-builder", "repo_dir": "/var/lib/sbopkg/SBo-danix", -@@ -51,11 +53,14 @@ DEFAULTS = { +@@ -51,11 +53,13 @@ DEFAULTS = { "default_hintdir": "", "genreprc": "/root/.genreprc", "slackrepo_logdir": "/var/log/slackrepo/SBo-danix", -+ "phantom_deps": [], # deps unneeded on -current: hint-new and fix-current DELREQUIRES them + "manifest_packages": ["neovim", "openvino"], # bundled deps: left to mkhint until phase 4b } FLAGS = ("--yes", "--no") @@ -403,19 +381,19 @@ index 25427c0..230dde6 100755 ACTIONS = ("build", "rebuild", "update", "remove") PRESET = "/usr/libexec/gpg-preset-passphrase" ITEM = re.compile(r"^[A-Za-z0-9._+-]+/[A-Za-z0-9._+-]+$") # category/pkg -@@ -85,8 +90,9 @@ def load_config(path=None): +@@ -85,8 +89,9 @@ def load_config(path=None): if type(v) is not type(DEFAULTS[k]): raise Error(f"config: {k}: expected {type(DEFAULTS[k]).__name__}") cfg[k] = v - if not all(isinstance(o, str) for o in cfg["overlays"]): - raise Error("config: overlays: expected list of strings") -+ for k in ("overlays", "phantom_deps", "manifest_packages"): ++ for k in ("overlays", "manifest_packages"): + if not all(isinstance(o, str) for o in cfg[k]): + raise Error(f"config: {k}: expected list of strings") return cfg -@@ -138,8 +144,71 @@ def kind_publish(cfg, args): +@@ -138,8 +143,67 @@ def kind_publish(cfg, args): return ["sb-publish"], {} @@ -472,11 +450,7 @@ index 25427c0..230dde6 100755 +def _self_job(kind): + """A job kind run by this file itself (_hint), argv-free like the others.""" + def build(cfg, args): -+ if kind == "fix-current": -+ if args: -+ raise Error("fix-current takes no arguments") -+ else: -+ hint_args(cfg, kind, args) ++ hint_args(cfg, kind, args) + return [sys.executable, os.path.abspath(__file__), "_hint", kind, *args], {} + return build + @@ -484,11 +458,11 @@ index 25427c0..230dde6 100755 KINDS = {"sbpull": kind_sbpull, "sbshadow": kind_sbshadow, - "slackrepo": kind_slackrepo, "publish": kind_publish} + "slackrepo": kind_slackrepo, "publish": kind_publish, -+ **{k: _self_job(k) for k in ("hint-update", "hint-new", "fix-current")}} ++ **{k: _self_job(k) for k in ("hint-update", "hint-new")}} def signs(kind, args): -@@ -549,6 +618,234 @@ def cmd_inventory(cfg): +@@ -549,6 +613,212 @@ def cmd_inventory(cfg): return {"columns": COLUMNS, "rows": rows} @@ -562,23 +536,17 @@ index 25427c0..230dde6 100755 + return "".join(ln.rstrip("\n") + "\n" for ln in lines) + + -+def new_hint(info, phantom): -+ """mkhint's hint from an .info: the pin, REQUIRES commented, ARCH, phantom deps.""" ++def new_hint(info): ++ """mkhint's hint from an .info: the pin, REQUIRES commented, ARCH. ++ ++ No phantom deps: a package that has them already has a hint, the ++ DELREQUIRES one sbshadow's mkhint -F writes after every sync. ++ """ + text = _pin(info) -+ requires = hint_tokens(info, "REQUIRES") + m = _assignment("REQUIRES").search(info) + if m: + text += "#" + m.group(0).rstrip("\n") + "\n" -+ text += 'ARCH="x86_64"\n' -+ deps = [d for d in phantom if d in requires] -+ return text + f'DELREQUIRES="{" ".join(deps)}"\n' if deps else text -+ -+ -+def merge_delrequires(text, deps): -+ """text with deps added to DELREQUIRES, or None when all are there already.""" -+ have = hint_tokens(text, "DELREQUIRES") -+ add = [d for d in dict.fromkeys(deps) if d not in have] -+ return hint_set(text, "DELREQUIRES", " ".join(have + add)) if add else None ++ return text + 'ARCH="x86_64"\n' + + +def _md5_url(url): @@ -644,41 +612,25 @@ index 25427c0..230dde6 100755 + +def cmd_hint(cfg, kind=None, *args): + """Body of the hint jobs: log lines, then SBB-RESULT {"ok": [...], "failed": [[pkg, why]]}.""" ++ if kind not in ("hint-update", "hint-new"): ++ raise Error(f"unknown hint job: {kind}") + ok, failed = [], [] + pkgs = _pkgmap(cfg) -+ if kind == "fix-current": -+ for pkg, path in sorted(pkgs.items()): ++ pairs, no_dl = hint_args(cfg, kind, list(args)) ++ for pkg, ver in pairs: ++ try: ++ path, target = _hint_target(cfg, pkg, pkgs) + info = _read(os.path.join(cfg["repo_dir"], path, pkg + ".info")) or "" -+ deps = [d for d in cfg["phantom_deps"] if d in hint_tokens(info, "REQUIRES")] -+ if not deps: -+ continue -+ try: -+ _, target = _hint_target(cfg, pkg, pkgs) -+ text = merge_delrequires(_read(target) or "", deps) -+ if text is not None: -+ _save(target, text) -+ print(f"{pkg}: DELREQUIRES {' '.join(deps)}", flush=True) -+ ok.append(pkg) -+ except (Error, OSError) as e: -+ failed.append([pkg, str(e)]) -+ elif kind in ("hint-update", "hint-new"): -+ pairs, no_dl = hint_args(cfg, kind, list(args)) -+ for pkg, ver in pairs: -+ try: -+ path, target = _hint_target(cfg, pkg, pkgs) -+ info = _read(os.path.join(cfg["repo_dir"], path, pkg + ".info")) or "" -+ text = _read(target) -+ if text is None: -+ text = new_hint(info, cfg["phantom_deps"]) -+ _save(target, apply_version(text, info, ver, no_dl, -+ lambda s, p=pkg: print(f"{p}: {s}", flush=True))) -+ print(f"{pkg}: ok", flush=True) -+ ok.append(pkg) -+ except (Error, OSError) as e: -+ print(f"{pkg}: failed: {e}", flush=True) -+ failed.append([pkg, str(e)]) -+ else: -+ raise Error(f"unknown hint job: {kind}") ++ text = _read(target) ++ if text is None: ++ text = new_hint(info) ++ _save(target, apply_version(text, info, ver, no_dl, ++ lambda s, p=pkg: print(f"{p}: {s}", flush=True))) ++ print(f"{pkg}: ok", flush=True) ++ ok.append(pkg) ++ except (Error, OSError) as e: ++ print(f"{pkg}: failed: {e}", flush=True) ++ failed.append([pkg, str(e)]) + print("SBB-RESULT " + json.dumps({"ok": ok, "failed": failed}), flush=True) + if failed: + raise Error(f"{len(failed)} failed") @@ -723,7 +675,7 @@ index 25427c0..230dde6 100755 # ── signing key ───────────────────────────────────────────────────────────── def _signing_key(cfg): -@@ -626,7 +923,12 @@ COMMANDS = { +@@ -626,7 +896,12 @@ COMMANDS = { "build-log": cmd_build_log, "gpg-status": cmd_gpg_status, "gpg-unlock": cmd_gpg_unlock, @@ -748,7 +700,7 @@ index e0f577e..21a93ab 100644 +VERSION = "0.5.0" # must match remote/sbb-remote VERSION ``` -- [ ] **Step 4: Run the suite, expect pass.** Run `python3 -m unittest discover -s tests`. Expected: `OK` (104 tests at this point). +- [ ] **Step 4: Run the suite, expect pass.** Run `python3 -m unittest discover -s tests`. Expected: `OK` (102 tests at this point). - [ ] **Step 5: Commit.** @@ -756,7 +708,7 @@ index e0f577e..21a93ab 100644 git add remote/sbb-remote remote/config.example.toml sb_builder/__init__.py tests/test_remote.py git commit -m "sbb-remote 0.5.0: hint jobs replace mkhint's hint editing -hint-update, hint-new and fix-current run the helper's own _hint command: +hint-update and hint-new run the helper's own _hint command: version bumps as mkhint does them (also '-' and dotless URL forms, never inside MD5SUM values), md5 of the first URL of each DOWNLOAD pair, --no-dl as DOWNLOAD=\"no\", .bak before each write and a final SBB-RESULT line @@ -1051,7 +1003,7 @@ index 0000000..d936706 + p.start("nvchecker", ["-c", write_config(tmp.name, keyfile, path), "--logger", "json"]) ``` -- [ ] **Step 5: Run the suite, expect pass.** Run `python3 -m unittest discover -s tests`. Expected: `OK` (110 tests). +- [ ] **Step 5: Run the suite, expect pass.** Run `python3 -m unittest discover -s tests`. Expected: `OK` (108 tests). - [ ] **Step 6: Commit.** @@ -1174,10 +1126,10 @@ index 0da4243..aa9c03b 100644 ```diff diff --git c/sb_builder/hints_tab.py i/sb_builder/hints_tab.py new file mode 100644 -index 0000000..abe6eb9 +index 0000000..6be57fc --- /dev/null +++ i/sb_builder/hints_tab.py -@@ -0,0 +1,283 @@ +@@ -0,0 +1,277 @@ +# sb-builder - Hints tab +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> +# @@ -1213,7 +1165,7 @@ index 0000000..abe6eb9 +STATE_COLOR = {"outdated": STATUS["aborted"], "current": STATUS["ok"], + "nvchecker error": STATUS["failed"]} +COLUMNS = ("", "package", "hint", ".info", "upstream", "state") -+HINT_JOBS = ("hint-update", "hint-new", "fix-current") ++HINT_JOBS = ("hint-update", "hint-new") + + +class HintsTab(QWidget): @@ -1264,10 +1216,9 @@ index 0000000..abe6eb9 + bar.addStretch() + for w in (self.check_btn, self.update_btn, self.no_dl): + bar.addWidget(w) -+ for label, slot in (("Fix current", self.fix_current), ("Delete hint", self.delete)): -+ b = QPushButton(label) -+ b.clicked.connect(slot) -+ bar.addWidget(b) ++ delete = QPushButton("Delete hint") ++ delete.clicked.connect(self.delete) ++ bar.addWidget(delete) + + layout = QVBoxLayout(self) + layout.addWidget(self.query) @@ -1358,11 +1309,6 @@ index 0000000..abe6eb9 + if pairs: + self.start_requested.emit(["hint-update", *pairs] + (["--no-dl"] if self.no_dl.isChecked() else [])) + -+ def fix_current(self): -+ if QMessageBox.question(self, "Fix current", "Add DELREQUIRES for the -current phantom deps " -+ "to every package that needs them?") == QMessageBox.StandardButton.Yes: -+ self.start_requested.emit(["fix-current"]) -+ + def delete(self): + name = self.current() + if name and QMessageBox.question(self, "Delete hint", f"Delete the hint of {name}? " @@ -1652,7 +1598,7 @@ index 34ce7f5..146db51 100644 + *(["--no-dl"] if no_dl else [])]) ``` -- [ ] **Step 4: Run the suite, expect pass.** Run `python3 -m unittest discover -s tests`. Expected: `OK` (113 tests). +- [ ] **Step 4: Run the suite, expect pass.** Run `python3 -m unittest discover -s tests`. Expected: `OK` (111 tests). - [ ] **Step 5: Look at it.** Render the screens offscreen and read the PNGs (checked while writing this plan: table, chips, button bar, diff dialog with the failure line under the split, New hint under the detail pane): @@ -1681,8 +1627,7 @@ git add sb_builder/hints_tab.py sb_builder/mainwindow.py sb_builder/packages.py git commit -m "Hints screen: upstream check, hint jobs, diff and revert Check upstream fills the upstream column and ticks the outdated hints, -Update selected starts hint-update, Fix current and Delete hint act on -the VM. When a hint job ends its SBB-RESULT opens a dialog with each +Update selected starts hint-update, Delete hint acts on the VM. When a hint job ends its SBB-RESULT opens a dialog with each changed hint's diff, Revert per package (once) and Build these. The Packages detail pane gets New hint and Open in Hints. @@ -1703,7 +1648,7 @@ unbuilt items; 64 stanzas, not 65; pinned hints do carry ```diff diff --git c/AGENTS.md i/AGENTS.md -index 9113e34..eda2dd8 100644 +index 9113e34..9a8918c 100644 --- c/AGENTS.md +++ i/AGENTS.md @@ -17,7 +17,8 @@ Design and phasing: `docs/superpowers/specs/`. Phase 1 (core) is @@ -1716,15 +1661,16 @@ index 9113e34..eda2dd8 100644 - Trust boundary: the helper runs only a fixed table of job kinds and builds argv itself from validated args. Never pass shell strings or paths from the GUI to the helper. -@@ -32,10 +33,15 @@ Design and phasing: `docs/superpowers/specs/`. Phase 1 (core) is +@@ -32,10 +33,16 @@ Design and phasing: `docs/superpowers/specs/`. Phase 1 (core) is - Hints are parsed (`shlex`), never sourced. `sb_builder/hints.py` mirrors slackrepo's own rules (`../GITHUB/slackrepo/libexec/functions.d/parsefunctions.sh`); check that file when changing it. -+- Hint editing lives in the helper (`hint-update`, `hint-new`, `fix-current` -+ jobs run the helper's own `_hint` command): text edits, `.bak` before each ++- Hint editing lives in the helper (`hint-update`, `hint-new` jobs run the ++ helper's own `_hint` command): text edits, `.bak` before each + write, a final `SBB-RESULT {json}` log line read back with `job-result`. -+ It replaces mkhint, which stays on the VM only for neovim and openvino -+ (bundled deps, `manifest_packages`) until phase 4b. ++ It replaces mkhint, which stays on the VM for neovim and openvino ++ (bundled deps, `manifest_packages`) and for the `DELREQUIRES` hints ++ sbshadow regenerates with `mkhint -F` after every sync, until phase 4b. - Jobs never get input. Any tool run as a job must be non-interactive (`--yes`/`--no` style flags). If a tool needs a flag it lacks, add it in - that tool's own repo (e.g. `slackrepo_setup` for sbshadow, `mkhintfile` @@ -1735,7 +1681,7 @@ index 9113e34..eda2dd8 100644 ## Conventions diff --git c/README.md i/README.md -index e6434c9..63ebd1a 100644 +index e6434c9..e1f93cc 100644 --- c/README.md +++ i/README.md @@ -1,8 +1,8 @@ @@ -1761,7 +1707,7 @@ index e6434c9..63ebd1a 100644 ## Install -@@ -58,6 +57,20 @@ Every key is optional. `File > Reload config` rereads the GUI config. +@@ -58,6 +57,19 @@ Every key is optional. `File > Reload config` rereads the GUI config. the package's last attempt (slackrepo keeps one per command: `build.log`, `update.log`, `rebuild.log`, `remove.log`), where it reports the failure, with its `resource.log` (load, memory, tmp over the build). @@ -1771,9 +1717,8 @@ index e6434c9..63ebd1a 100644 + it by hand to track a package; `hints.nvchecker_keyfile` gives it a GitHub + token) and ticks the outdated packages. **Update selected** rewrites their + hints on the VM (version, URLs, md5 of the downloaded source; **no download** -+ writes `DOWNLOAD="no"` instead). **Fix current** adds `DELREQUIRES` for the -+ -current phantom deps (`phantom_deps` in the VM config). **Delete hint** -+ keeps a `.bak`. The Packages detail pane has **New hint** for a package ++ writes `DOWNLOAD="no"` instead; a hint sbshadow wrote, `DELREQUIRES` only, ++ gets the `.info` version lines added). **Delete hint** keeps a `.bak`. The Packages detail pane has **New hint** for a package + without one. +- After a hint job a dialog shows the diff of each changed hint, with + **Revert** per package and **Build these** (a slackrepo update preview). @@ -1783,10 +1728,10 @@ index e6434c9..63ebd1a 100644 (status bar 🔒), the GUI asks for its passphrase first and hands it to the VM's gpg-agent; it is never stored. diff --git c/docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md i/docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md -index df360eb..4b659c7 100644 +index df360eb..c45c720 100644 --- c/docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md +++ i/docs/superpowers/specs/2026-10-20-sb-builder-phase4-design.md -@@ -1,7 +1,7 @@ +@@ -1,15 +1,15 @@ # sb-builder phase 4: hints Date: 2026-10-20 @@ -1795,9 +1740,22 @@ index df360eb..4b659c7 100644 Builds on: `2026-10-19-sb-builder-phase1-design.md` (helper, job model, GUI shell), `2026-10-19-sb-builder-phase2-design.md` (inventory, hint view, slackrepo jobs) -@@ -25,7 +25,9 @@ Verified on the VM, in the mkhint source (`~/Programming/GIT/mkhintfile/mkhint`, + ## Goal + + Manage slackrepo hint files from sb-builder: check upstream versions, update +-version hints, create and delete hints, sweep `DELREQUIRES` for -current +-phantom deps, and see a diff of every change with a way to revert it. ++version hints, create and delete hints, and see a diff of every change ++with a way to revert it. + + Direction set by the user: the repo is run from the desktop, the VM shell is + for upgrading Slackware only. The VM-side tools were built for a terminal; a +@@ -23,9 +23,11 @@ Verified on the VM, in the mkhint source (`~/Programming/GIT/mkhintfile/mkhint`, + 1.4.0, 2291 lines) and in slackrepo (`../GITHUB/slackrepo`): + - The VM has 607 hints, all in `hintdir`, none in subdirectories and none in - the SBo tree. 594 carry `DELREQUIRES` (from `mkhint -F`), 13 carry +- the SBo tree. 594 carry `DELREQUIRES` (from `mkhint -F`), 13 carry ++ the SBo tree. 594 carry only `DELREQUIRES`, 13 carry `VERSION` + `ARCH` + `DOWNLOAD` + `MD5SUM`, one carries `OPTIONS`. No hint - carries `NODOWNLOAD` or a `DOWNLOAD_x86_64`/`MD5SUM_x86_64` line. + carries `NODOWNLOAD`. Pinned hints carry `DOWNLOAD_x86_64`/`MD5SUM_x86_64` @@ -1806,11 +1764,17 @@ index df360eb..4b659c7 100644 - slackrepo reads `DOWNLOAD="no"` as "no download" and ignores `NODOWNLOAD`. `DOWNLOAD_$ARCH` replaces `DOWNLOAD` when set (`libexec/functions.d/parsefunctions.sh:457`), so `DOWNLOAD="no"` next to a -@@ -45,11 +47,17 @@ Verified on the VM, in the mkhint source (`~/Programming/GIT/mkhintfile/mkhint`, +@@ -44,12 +46,24 @@ Verified on the VM, in the mkhint source (`~/Programming/GIT/mkhintfile/mkhint`, + - mkhint's `-F`: for every `.info` in the repo whose `REQUIRES` contains a phantom dep, merge those deps into the hint's `DELREQUIRES` (union, first seen order, `.bak` only when something changes, minimal hint created when - absent). +- absent). -- The VM's nvchecker config has 65 stanzas named after the packages, plus a ++ absent). It edits only the `DELREQUIRES` line. sbshadow ++ (slackrepo_setup) runs it after every sync: that is where the 594 ++ DELREQUIRES-only hints come from, and why they exist (slackrepo must not ++ build deps -current does not need). Until now sbshadow ran it on `--no` ++ previews too. +- The VM's nvchecker config has 64 stanzas named after the packages, plus a `[__config__]` with oldver, newver and keyfile. nvchecker v2.22 is installed on the desktop, with a GitHub token in @@ -1821,13 +1785,33 @@ index df360eb..4b659c7 100644 + result, `{"name", "error", "level": "error"}` per failure; it exits 0 when + entries fail and needs no oldver or newver to run. +- A DELREQUIRES-only hint has no `VERSION`; mkhint's update cannot bump it. -+ Updating one pins it from the `.info` first. ++ Updating one pins it from the `.info` first; the next `mkhint -F` keeps the ++ pin. A package with a phantom dep always has such a hint, so `hint-new` ++ never sees one. +- slackrepo `update <item>` builds an item that exists, built or not + (`update_command` calls `build_command`). - The phantom deps list on the VM (`/root/.config/mkhint/phantom-deps`) has 18 entries. - Two packages use bundled-dep manifests: neovim (url mode) and openvino -@@ -70,20 +78,22 @@ editing, migration of `NODOWNLOAD=yes` lines (none exist). +@@ -57,33 +71,37 @@ Verified on the VM, in the mkhint source (`~/Programming/GIT/mkhintfile/mkhint`, + + ## Scope + +-In: upstream check, version update, new hint, delete hint, fix-current, ++In: upstream check, version update, new hint, delete hint, + `--no-dl` as `DOWNLOAD="no"`, diff and revert. + + Out, for good (the Hints screen covers them): mkhint `--list`, `--versions`, + `--review`, `--strip-version`, `--info`, `--clean`. + +-Out, later: bundled-dep reconcile for neovim and openvino (phase 4b, which +-then removes mkhint and nvchecker from the VM), generating nvchecker stanzas ++Out, later (phase 4b, which then removes mkhint and nvchecker from the VM): ++bundled-dep reconcile for neovim and openvino, and a helper `fix-current` ++that sbshadow calls in place of `mkhint -F`, with the phantom deps list ++moved into the helper config as its single source. Also out: generating nvchecker stanzas + for new packages (add them to `nvchecker.toml` by hand), free text hint + editing, migration of `NODOWNLOAD=yes` lines (none exist). ## Part 1: upstream check, on the desktop @@ -1858,7 +1842,17 @@ index df360eb..4b659c7 100644 `~/.config/nvchecker/keys.toml` (expanded). A missing keyfile is not an error: nvchecker runs without a token. -@@ -108,27 +118,38 @@ the 18 entries at deploy. `remote/config.example.toml` shows the key. +@@ -91,8 +109,7 @@ error: nvchecker runs without a token. + + ### Config + +-New key `phantom_deps`, list of strings, default `[]`. The VM config gets +-the 18 entries at deploy. `remote/config.example.toml` shows the key. ++New key `manifest_packages` (below). `remote/config.example.toml` shows it. + + ### Arguments + +@@ -108,27 +125,36 @@ the 18 entries at deploy. `remote/config.example.toml` shows the key. ### Job kinds @@ -1877,13 +1871,14 @@ index df360eb..4b659c7 100644 + `SBB-RESULT {"ok": [pkg...], "failed": [[pkg, why]...]}`. Exit status 1 + when any package failed. - `hint-new <pkg> [<ver>] [--no-dl]`: refuses when the hint exists. Builds - the hint from the `.info` as mkhint does, phantom deps from +- the hint from the `.info` as mkhint does, phantom deps from - `phantom_deps`, then applies the version when given. No `.bak` (nothing to - back up); revert means delete. -+ `phantom_deps`, then applies the version when given. No `.bak` (a stale +-- `fix-current`: mkhint's `-F` with `phantom_deps`. Logs one line per +- changed hint, then `SBB-RESULT {"ok": [...], "failed": []}`. ++ the hint from the `.info` as mkhint does, without phantom deps (see the ++ facts), then applies the version when given. No `.bak` (a stale + one is removed); revert means delete. - - `fix-current`: mkhint's `-F` with `phantom_deps`. Logs one line per - changed hint, then `SBB-RESULT {"ok": [...], "failed": []}`. -These jobs never sign, so they do not check the signing key. +These jobs never sign, so they do not check the signing key. Their argv is @@ -1910,7 +1905,7 @@ index df360eb..4b659c7 100644 3. Without `--no-dl`: for `DOWNLOAD`/`MD5SUM` and `DOWNLOAD_x86_64`/`MD5SUM_x86_64`, split the values on whitespace (hint values may continue over lines with `\`). Skip a pair whose value is empty, -@@ -141,9 +162,8 @@ These jobs never sign, so they do not check the signing key. +@@ -141,9 +167,8 @@ These jobs never sign, so they do not check the signing key. written, because `DOWNLOAD_$ARCH` would override it. Hint text is edited as text, not regenerated, so comments, order and other @@ -1922,7 +1917,18 @@ index df360eb..4b659c7 100644 ### Commands (answer at once, not jobs) -@@ -187,9 +207,10 @@ When a `hint-update`, `hint-new` or `fix-current` job ends, the GUI reads +@@ -177,22 +202,21 @@ convention. + - **Update selected**: starts `hint-update pkg=ver ...` for the checked + rows that have an upstream version; a **No download** checkbox adds + `--no-dl`. +- - **Fix current**: starts `fix-current`. + - **Delete hint**: confirm dialog, then `hint-delete` for the current row. + - Jobs go through the Jobs tab like every other job. + + ### After a hint job + +-When a `hint-update`, `hint-new` or `fix-current` job ends, the GUI reads ++When a `hint-update` or `hint-new` job ends, the GUI reads its `SBB-RESULT` line and opens a dialog with one entry per changed package: a unified diff (`difflib`) of `hint-diff`, and a **Revert** button per package. Failed packages are listed with their reason. The dialog ends with @@ -1934,10 +1940,21 @@ index df360eb..4b659c7 100644 +**Close**. Revert is offered once per package: a second one would delete +the restored hint. After Revert or Delete the inventory is reloaded. - For `fix-current` the dialog lists the packages without opening every diff - (it can touch hundreds); clicking one shows its diff. -@@ -224,8 +245,9 @@ For `fix-current` the dialog lists the packages without opening every diff - changes nothing and writes no `.bak`), new -> revert, update -> diff -> +-For `fix-current` the dialog lists the packages without opening every diff +-(it can touch hundreds); clicking one shows its diff. ++Each diff is fetched when its package is selected. + + ### Packages detail pane + +@@ -219,13 +243,13 @@ For `fix-current` the dialog lists the packages without opening every diff + touched), multi-line `DOWNLOAD`/`MD5SUM` with continuation md5s kept, + `UNSUPPORTED` skipped, `--no-dl` removing `DOWNLOAD_x86_64` and writing + `DOWNLOAD="no"`, a download failure restoring the hint while the next +- package still updates, `SBB-RESULT` contents, `hint-new` from `.info` +- with phantom deps, `fix-current` merge and idempotence (second run +- changes nothing and writes no `.bak`), new -> revert, update -> diff -> ++ package still updates, `SBB-RESULT` contents, `hint-new` from `.info`, ++ a DELREQUIRES-only hint pinned from the `.info`, new -> revert, update -> diff -> revert, delete -> revert, manifest package refusal, unmanaged hint path refusal, bad version and package rejection. -- `test_upstream.py`: generated config text, newver parsing from a fixture, @@ -1948,6 +1965,14 @@ index df360eb..4b659c7 100644 - `test_gui.py`: Hints screen with a fake remote and injected upstream results (states, checked rows, Update selected argv), diff dialog Revert call. +@@ -233,6 +257,6 @@ For `fix-current` the dialog lists the packages without opening every diff + ## Deploy (asks first) + + 1. `./install.sh --remote` with helper 0.5.0. +-2. Add `phantom_deps` (the VM's 18 entries) to `/etc/sb-builder/config.toml`. ++2. slackrepo_setup's sbshadow, which no longer runs `mkhint -F` on `--no`. + 3. mkhint, nvchecker and `/root/.config/{mkhint,nvchecker}` stay on the VM + until phase 4b. ``` - [ ] **Step 2: Commit and push.** @@ -1964,17 +1989,6 @@ git log --format='%h %G? %s' -5 # every line G ### Task 5: deploy (ask the user first) and check on the VM - [ ] **Step 1: Ask**, then deploy the helper: `./install.sh --remote`. Expected last line: `{"version": "0.5.0", ...}`. -- [ ] **Step 2: Phantom deps into the helper config.** Build the line locally (a JSON array of strings is a TOML array), show it and the VM config to the user, then append it: - -```bash -line=$(ssh buildsystem cat /root/.config/mkhint/phantom-deps | python3 -c ' -import json, sys -deps = [l.split("#")[0].strip() for l in sys.stdin] -print("phantom_deps = " + json.dumps([d for d in deps if d]))') -echo "$line"; ssh buildsystem cat /etc/sb-builder/config.toml -printf '%s\n' "$line" | ssh buildsystem 'cat >> /etc/sb-builder/config.toml && sbb-remote version' -``` - -Expected: an 18-entry array, then the version JSON (a config error would print `{"error": ...}`). +- [ ] **Step 2: sbshadow.** slackrepo_setup `f5b14ff` (`--no` skips `mkhint -F`): show the user the VM copy's diff, then `scp ~/Programming/GIT/slackrepo_setup/sbshadow buildsystem:$(ssh buildsystem command -v sbshadow)` and `ssh buildsystem 'sbshadow --no | tail -2'`, expecting `sbshadow: preview, phantom-dep hintfiles not regenerated.` - [ ] **Step 3: Read-only check on real data:** `ssh buildsystem sbb-remote hint-diff flatpak` returns `{"bak": null, "current": "VERSION=..."}` (or the `.bak` mkhint left). -- [ ] **Step 4: User's manual run** in the GUI, reported back: Hints > Check upstream fills the column; Update selected on one outdated package, dialog shows the diff, Revert restores it; Update again and Build these opens the slackrepo preview; Fix current reports no changes (`mkhint -F` already ran); New hint and Delete hint on a throwaway package, each reverted. +- [ ] **Step 4: User's manual run** in the GUI, reported back: Hints > Check upstream fills the column; Update selected on one outdated package, dialog shows the diff, Revert restores it; Update again and Build these opens the slackrepo preview; New hint and Delete hint on a throwaway package, each reverted. |
