diff options
Diffstat (limited to 'docs/superpowers/plans/2026-07-07-bundled-dep-manifest.md')
| -rw-r--r-- | docs/superpowers/plans/2026-07-07-bundled-dep-manifest.md | 1297 |
1 files changed, 1297 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-07-07-bundled-dep-manifest.md b/docs/superpowers/plans/2026-07-07-bundled-dep-manifest.md new file mode 100644 index 0000000..7801194 --- /dev/null +++ b/docs/superpowers/plans/2026-07-07-bundled-dep-manifest.md @@ -0,0 +1,1297 @@ +# Bundled-dep Manifest Updates Implementation Plan + +> **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:** Reconcile a bundle package's extra `DOWNLOAD` lines against an upstream deps manifest (e.g. neovim's `cmake.deps/deps.txt`), opt-in per package, replacing the blind continuation-URL prompt for listed packages. + +**Architecture:** New pure helpers in `mkhint` parse a `<pkg> <url-template>` config and a `NAME_URL/NAME_SHA256` manifest, match hint download lines to manifest entries (repo-path first, basename-stem fallback), and rewrite matched lines + recompute md5. A `--check` two-phase flow runs manifest reconcile after the primary bump, gated by `--force` when the primary is unchanged. `--new` reports only. + +**Tech Stack:** bash, awk, perl (existing), pandoc for the man page. Tests via the existing mock harness in `tests/mkhint_test.sh` (mock dirs, fake `wget`, no network). + +--- + +## Reference facts (read before starting) + +- The spec is `docs/superpowers/specs/2026-07-07-bundled-dep-manifest-design.md`. Read it. +- The tool is a single bash file `mkhint`. Convention: single file, minimal deps, `set -e` at top (line 17). +- Phantom-dep block starts at `mkhint:338` (`# ── phantom-dep handling ──`). New helpers go **immediately after** the phantom-dep block (before `create_new_hint_file` at line ~423), in a `# ── bundled-dep manifest handling ──` block. +- `download_file <url>` (mkhint:319) downloads via `wget -O`, echoes the md5, deletes the temp. Non-zero on wget failure. +- `parse_multiline_var <var> <file>` (mkhint:595) prints one whitespace token per line for a `\`-continued hint variable. Reuse to read `DOWNLOAD` lines. +- `build_multiline_value <arrayname>` (mkhint:650) prints a quoted `\`-continued value from an array (nameref). Reuse to write `DOWNLOAD` back. +- The perl writeback idiom for a multiline var (mkhint:760): + ```bash + perl -i -0pe 'BEGIN{$v=shift} s|^DOWNLOAD="[^"]*(?:\\\n[^"]*)*"|DOWNLOAD="$v"|m' "$new_value" "$file" + ``` +- Test harness: `run_mkhint` (tests/mkhint_test.sh:193) sed-patches the path constants (`REPO_DIR`, `HINT_DIR`, `TMP_DIR`, `NVCHECKER_CONFIG`, `PHANTOM_DEPS_FILE`, `PACKAGES_DIR`, `MKHINT_CONFIG`) to mock dirs, then runs a copy. **It does NOT patch `BUNDLE_MANIFEST_FILE`** — a new sed line must be added (Task 1, Step included). +- Fake `wget` (tests/mkhint_test.sh:199) writes `FAKE_CONTENT_FOR_<url>` to the `-O` target. For manifest tests it must instead serve fixture `deps.txt` content when the URL is a manifest URL (Task 8 extends it). +- `_normalize_version` (mkhint:302) does `${1//-/_}`. Bug-1 dual-form sed lives in `update_hint_file` (mkhint:795) and `create_new_hint_file` (mkhint:~466). +- Colours/TTY: not relevant here; all reconcile output is plain text. +- Every commit is GPG-signed (`git commit -S`). Do not disable signing. +- After any `mkhint.1.md` edit, rebuild: `pandoc mkhint.1.md -s -t man -o mkhint.1 && gzip -9 -n -f mkhint.1`. Commit only `mkhint.1.gz` (the `.1` is gitignored). +- Run the whole suite with `bash tests/mkhint_test.sh`; it prints `Results: N passed, M failed` and exits non-zero on any failure. + +--- + +## File structure + +- **Modify `mkhint`:** add config var (Section 1), the manifest-handling helper block (Sections 2, 3, 4, 5), the `--force` flag in getopt + main, the `--new` hook, the `--check` Phase 2 wiring, the `--help` line. +- **Modify `mkhint.bash-completion`:** add `--force` to `all_flags`. +- **Modify `tests/mkhint_test.sh`:** extend fake wget for manifests, patch `BUNDLE_MANIFEST_FILE`, add unit + integration tests. +- **Create `bundle-manifests.example`** (repo root): commented sample config. +- **Modify docs:** `CLAUDE.md`, `mkhint.1.md` (+ rebuild `mkhint.1.gz`), `CHANGELOG.md`. + +--- + +## Task 1: Config var + test-harness patch + +**Files:** +- Modify: `mkhint` (after line 27, the `PHANTOM_DEPS_FILE` default) +- Modify: `tests/mkhint_test.sh:196-204` (the `run_mkhint` sed block) + +- [ ] **Step 1: Add the config default in `mkhint`** + +After the `PHANTOM_DEPS_FILE="$HOME/.config/mkhint/phantom-deps"` line (mkhint:27), add: + +```bash +# Packages whose extra DOWNLOAD lines are driven by an upstream deps manifest +# (e.g. neovim's cmake.deps/deps.txt). One entry per line: +# <pkgname> <deps-url-template-with-{VERSION}> +# Missing file = empty list = feature is a no-op. +BUNDLE_MANIFEST_FILE="$HOME/.config/mkhint/bundle-manifests" +``` + +- [ ] **Step 2: Patch `run_mkhint` so tests can point `BUNDLE_MANIFEST_FILE` at a mock file** + +In `tests/mkhint_test.sh`, inside the `sed` in `run_mkhint` (after the `PHANTOM_DEPS_FILE` line, ~line 201), add another `-e`: + +```bash + -e "s|PHANTOM_DEPS_FILE=\".*\"|PHANTOM_DEPS_FILE=\"$MOCK_BASE/phantom-deps\"|" \ + -e "s|BUNDLE_MANIFEST_FILE=\".*\"|BUNDLE_MANIFEST_FILE=\"$MOCK_BASE/bundle-manifests\"|" \ +``` + +- [ ] **Step 3: Verify syntax** + +Run: `bash -n mkhint && echo OK` +Expected: `OK` + +- [ ] **Step 4: Verify existing suite still green (no behavior change yet)** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep Results:` +Expected: `Results: 158 passed, 0 failed` + +- [ ] **Step 5: Commit** + +```bash +git add mkhint tests/mkhint_test.sh +git commit -S -m "feat: add BUNDLE_MANIFEST_FILE config var + +Opt-in per-package manifest for bundled-dep reconciliation. No behavior +yet; wires the config default and the test-harness path patch. + +Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" +``` + +--- + +## Task 2: `parse_manifest` helper + +**Files:** +- Modify: `mkhint` (new `# ── bundled-dep manifest handling ──` block, inserted after the phantom-dep block, before `create_new_hint_file`) +- Modify: `tests/mkhint_test.sh` (before the SUMMARY block) + +- [ ] **Step 1: Write the failing test** + +Add before the `# ─── SUMMARY ───` block in `tests/mkhint_test.sh`: + +```bash +# ── T-BM1: parse_manifest extracts URLs from NAME_URL/NAME_SHA256 pairs ────── +echo "" +echo "T-BM1: parse_manifest extracts URLs, ignores SHA256 lines" +cat > "$MOCK_BASE/deps.txt" << 'EOF' +LIBUV_URL https://github.com/libuv/libuv/archive/v1.52.1.tar.gz +LIBUV_SHA256 478baf2599bfbc +TREESITTER_URL https://github.com/tree-sitter/tree-sitter/archive/v0.26.7.tar.gz +TREESITTER_SHA256 4343107ad1097 +EOF +bm_out=$(bash -c 'source <(sed -n "/# ── bundled-dep manifest handling/,/# ── end bundled-dep/p" '"$SCRIPT"'); parse_manifest '"$MOCK_BASE"'/deps.txt') +echo "$bm_out" | grep -qx 'https://github.com/libuv/libuv/archive/v1.52.1.tar.gz' \ + && echo "$bm_out" | grep -qx 'https://github.com/tree-sitter/tree-sitter/archive/v0.26.7.tar.gz' \ + && ! echo "$bm_out" | grep -q '478baf' \ + && { echo " PASS: parse_manifest URLs only"; (( PASS++ )); } \ + || { echo " FAIL: parse_manifest: $bm_out"; (( FAIL++ )); ERRORS+=("T-BM1"); } +``` + +Note: the test sources just the helper block (delimited by the block header and an `# ── end bundled-dep manifest handling ──` marker) so helpers can be exercised without running `main`. Task 2 Step 3 adds both markers. + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM1|FAIL: parse'` +Expected: FAIL (block markers + `parse_manifest` don't exist yet) + +- [ ] **Step 3: Add the block header, the helper, and the end marker in `mkhint`** + +Insert immediately after the phantom-dep block ends (after `fix_current` closes at mkhint:~420, before `# Create new hint file`): + +```bash +# ── bundled-dep manifest handling ───────────────────────────────────────────── +# Reconcile a bundle package's extra DOWNLOAD lines against an upstream deps +# manifest (opt-in via BUNDLE_MANIFEST_FILE). See docs spec 2026-07-07. + +# parse_manifest <file> — print one download URL per line for each NAME_URL +# entry. SHA256 lines ignored (hints use MD5SUM; we re-download and md5). +parse_manifest() { + local file="$1" + [[ -f "$file" ]] || return 0 + awk '/_URL[[:space:]]/ { print $2 }' "$file" +} +# ── end bundled-dep manifest handling ───────────────────────────────────────── +``` + +(Subsequent tasks insert their helpers **above** the end marker.) + +- [ ] **Step 4: Run test to verify it passes** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep 'T-BM1' -A1` +Expected: `PASS: parse_manifest URLs only` + +- [ ] **Step 5: Commit** + +```bash +git add mkhint tests/mkhint_test.sh +git commit -S -m "feat: parse_manifest reads NAME_URL entries from a deps manifest + +Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" +``` + +--- + +## Task 3: `_url_stem` + `match_dep_url` + +**Files:** +- Modify: `mkhint` (above the end marker) +- Modify: `tests/mkhint_test.sh` (before SUMMARY) + +- [ ] **Step 1: Write the failing tests** + +Add before SUMMARY: + +```bash +# ── T-BM2: match_dep_url repo-path hit ─────────────────────────────────────── +echo "" +echo "T-BM2: match_dep_url matches by owner/repo path" +BM_SRC='source <(sed -n "/# ── bundled-dep manifest handling/,/# ── end bundled-dep/p" '"$SCRIPT"')' +manifest_urls='https://github.com/tree-sitter/tree-sitter/archive/v0.26.8.tar.gz +https://github.com/luvit/luv/archive/1.53.0-0.tar.gz' +r=$(bash -c "$BM_SRC; match_dep_url 'https://github.com/tree-sitter/tree-sitter/archive/v0.26.7/tree-sitter-0.26.7.tar.gz' \"\$1\"" _ "$manifest_urls") +[[ "$r" == 'https://github.com/tree-sitter/tree-sitter/archive/v0.26.8.tar.gz' ]] \ + && { echo " PASS: repo-path match"; (( PASS++ )); } \ + || { echo " FAIL: repo-path match got '$r'"; (( FAIL++ )); ERRORS+=("T-BM2"); } + +# ── T-BM3: match_dep_url stem fallback for blob host ───────────────────────── +echo "" +echo "T-BM3: match_dep_url stem fallback (neovim/deps/raw blob → lpeg)" +manifest_urls='https://github.com/neovim/deps/raw/deadbeef1234567/opt/lpeg-1.1.0.tar.gz' +r=$(bash -c "$BM_SRC; match_dep_url 'https://www.inf.puc-rio.br/~roberto/lpeg/lpeg-1.1.0.tar.gz' \"\$1\"" _ "$manifest_urls") +[[ "$r" == 'https://github.com/neovim/deps/raw/deadbeef1234567/opt/lpeg-1.1.0.tar.gz' ]] \ + && { echo " PASS: stem fallback match"; (( PASS++ )); } \ + || { echo " FAIL: stem fallback got '$r'"; (( FAIL++ )); ERRORS+=("T-BM3"); } + +# ── T-BM4: match_dep_url no false prefix match ─────────────────────────────── +echo "" +echo "T-BM4: match_dep_url does not match tree-sitter to tree-sitter-c" +manifest_urls='https://github.com/tree-sitter/tree-sitter-c/archive/v0.24.1.tar.gz' +r=$(bash -c "$BM_SRC; match_dep_url 'https://example.com/foo/tree-sitter-0.26.7.tar.gz' \"\$1\"" _ "$manifest_urls") +[[ -z "$r" ]] \ + && { echo " PASS: no false prefix match"; (( PASS++ )); } \ + || { echo " FAIL: false match got '$r'"; (( FAIL++ )); ERRORS+=("T-BM4"); } + +# ── T-BM5: match_dep_url no match → empty ──────────────────────────────────── +echo "" +echo "T-BM5: match_dep_url returns empty on no match" +manifest_urls='https://github.com/foo/bar/archive/v1.0.tar.gz' +r=$(bash -c "$BM_SRC; match_dep_url 'https://github.com/baz/qux/archive/v2.0.tar.gz' \"\$1\"" _ "$manifest_urls") +[[ -z "$r" ]] \ + && { echo " PASS: empty on no match"; (( PASS++ )); } \ + || { echo " FAIL: expected empty got '$r'"; (( FAIL++ )); ERRORS+=("T-BM5"); } +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM[2345]'` +Expected: FAILs (`match_dep_url` undefined) + +- [ ] **Step 3: Add `_url_stem` + `match_dep_url` above the end marker** + +```bash +# _url_stem <url> — reduce a download URL's basename to a comparison stem: +# drop a trailing archive extension, then a trailing -<version> or the version +# is the whole tail after the name. Version = [vV]?[0-9]... or a 7+ hex sha. +_url_stem() { + local base="${1##*/}" + base="${base%.tar.gz}"; base="${base%.tar.xz}"; base="${base%.tar.bz2}" + base="${base%.zip}"; base="${base%.tgz}" + # strip a trailing -<version> (version starts with optional v then a digit, + # or is a 7+ char hex sha) + if [[ "$base" =~ ^(.+)-[vV]?[0-9].*$ ]]; then + base="${BASH_REMATCH[1]}" + elif [[ "$base" =~ ^(.+)-[0-9a-f]{7,}$ ]]; then + base="${BASH_REMATCH[1]}" + fi + printf '%s\n' "$base" +} + +# _url_repo <url> — echo owner/repo for a github URL, empty otherwise. +_url_repo() { + [[ "$1" =~ github\.com/([^/]+)/([^/]+) ]] && printf '%s/%s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]%.git}" +} + +# match_dep_url <hint_url> <manifest_url_list> — echo the manifest URL that +# corresponds to hint_url, or nothing. Repo-path match first, then exact +# basename-stem fallback (for blob hosts where the repo path is ambiguous). +match_dep_url() { + local hint_url="$1" list="$2" + local hrepo; hrepo=$(_url_repo "$hint_url") + local m mrepo + # pass 1: repo-path (skip the shared-blob-host repo neovim/deps so a blob + # URL never wins on repo path; it must match on stem instead) + if [[ -n "$hrepo" && "$hrepo" != */deps ]]; then + while IFS= read -r m; do + [[ -z "$m" ]] && continue + mrepo=$(_url_repo "$m") + if [[ -n "$mrepo" && "$mrepo" != */deps && "$mrepo" == "$hrepo" ]]; then + printf '%s\n' "$m"; return 0 + fi + done <<< "$list" + fi + # pass 2: exact stem + local hstem; hstem=$(_url_stem "$hint_url") + while IFS= read -r m; do + [[ -z "$m" ]] && continue + [[ "$(_url_stem "$m")" == "$hstem" ]] && { printf '%s\n' "$m"; return 0; } + done <<< "$list" + return 1 +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM[2345]' -A1 | grep -E 'PASS|FAIL'` +Expected: four `PASS` lines + +- [ ] **Step 5: Commit** + +```bash +git add mkhint tests/mkhint_test.sh +git commit -S -m "feat: match_dep_url maps a hint download line to a manifest URL + +Repo-path match first, exact basename-stem fallback for blob hosts. Blob +host repo (owner/deps) is excluded from repo-path matching so it resolves +by stem. + +Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" +``` + +--- + +## Task 4: config loader + URL templater + +**Files:** +- Modify: `mkhint` (above the end marker) +- Modify: `tests/mkhint_test.sh` (before SUMMARY) + +- [ ] **Step 1: Write the failing tests** + +```bash +# ── T-BM6: load_bundle_manifests + manifest_url_for ────────────────────────── +echo "" +echo "T-BM6: manifest_url_for substitutes {VERSION}, unlisted pkg → non-zero" +cat > "$MOCK_BASE/bundle-manifests" << 'EOF' +# comment +neovim https://example.com/neovim/v{VERSION}/deps.txt +EOF +BM_SRC='source <(sed -n "/# ── bundled-dep manifest handling/,/# ── end bundled-dep/p" '"$SCRIPT"')' +r=$(bash -c "BUNDLE_MANIFEST_FILE='$MOCK_BASE/bundle-manifests'; $BM_SRC; load_bundle_manifests; manifest_url_for neovim 0.13.0") +[[ "$r" == 'https://example.com/neovim/v0.13.0/deps.txt' ]] \ + && { echo " PASS: templated URL"; (( PASS++ )); } \ + || { echo " FAIL: templated URL got '$r'"; (( FAIL++ )); ERRORS+=("T-BM6a"); } +bash -c "BUNDLE_MANIFEST_FILE='$MOCK_BASE/bundle-manifests'; $BM_SRC; load_bundle_manifests; manifest_url_for curl 8.0" >/dev/null 2>&1 +[[ $? -ne 0 ]] \ + && { echo " PASS: unlisted pkg non-zero"; (( PASS++ )); } \ + || { echo " FAIL: unlisted pkg should be non-zero"; (( FAIL++ )); ERRORS+=("T-BM6b"); } +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM6'` +Expected: FAIL (loader undefined) + +- [ ] **Step 3: Add loader + templater above the end marker** + +```bash +# Loaded map: pkg name -> URL template. Populated by load_bundle_manifests. +declare -A BUNDLE_MANIFESTS=() +load_bundle_manifests() { + BUNDLE_MANIFESTS=() + [[ -f "$BUNDLE_MANIFEST_FILE" ]] || return 0 + local line pkg url + while IFS= read -r line; do + line="${line%%#*}" + [[ -z "${line// }" ]] && continue + read -r pkg url <<< "$line" + [[ -n "$pkg" && -n "$url" ]] && BUNDLE_MANIFESTS["$pkg"]="$url" + done < "$BUNDLE_MANIFEST_FILE" +} + +# manifest_url_for <pkg> <version> — echo the manifest URL for pkg with +# {VERSION} substituted. Non-zero if pkg is not listed. +manifest_url_for() { + local pkg="$1" ver="$2" + local tmpl="${BUNDLE_MANIFESTS[$pkg]:-}" + [[ -z "$tmpl" ]] && return 1 + printf '%s\n' "${tmpl//\{VERSION\}/$ver}" +} + +# True if pkg has a bundle manifest configured. +pkg_has_manifest() { + [[ -n "${BUNDLE_MANIFESTS[$1]:-}" ]] +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM6' -A1 | grep -E 'PASS|FAIL'` +Expected: two `PASS` + +- [ ] **Step 5: Commit** + +```bash +git add mkhint tests/mkhint_test.sh +git commit -S -m "feat: load_bundle_manifests + manifest_url_for + +Parse the pkg->url-template config and substitute {VERSION}. + +Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" +``` + +--- + +## Task 5: `fetch_manifest` with dual-form retry + +**Files:** +- Modify: `mkhint` (above the end marker) +- Modify: `tests/mkhint_test.sh` (extend fake wget first — see note; then add test) + +- [ ] **Step 1: Extend the fake wget to serve manifest fixtures** + +In `tests/mkhint_test.sh` `mock_wget` (line 199), replace the body so a URL containing `deps.txt` serves the fixture file `$MOCK_BASE/manifest_fixture` (if it exists), else the old FAKE_CONTENT behavior: + +```bash + cat > "$MOCK_BASE/bin/wget" << EOF +#!/bin/bash +url="" +out="" +while [[ \$# -gt 0 ]]; do + case "\$1" in + -O) out="\$2"; shift 2 ;; + *) url="\$1"; shift ;; + esac +done +if [[ "\$url" == *deps.txt* && -f "$MOCK_BASE/manifest_fixture" ]]; then + cat "$MOCK_BASE/manifest_fixture" > "\$out" +elif [[ "\$url" == *deps.txt* ]]; then + exit 1 # simulate manifest fetch failure when no fixture set +else + echo "FAKE_CONTENT_FOR_\${url}" > "\$out" +fi +exit 0 +EOF +``` + +(Note: this is a `EOF` without quotes now so `$MOCK_BASE` expands; the inner `\$` escapes stay literal. Verify the heredoc still writes a valid script.) + +- [ ] **Step 2: Write the failing test** + +```bash +# ── T-BM7: fetch_manifest downloads to a temp file ─────────────────────────── +echo "" +echo "T-BM7: fetch_manifest returns a path with the fixture content" +cat > "$MOCK_BASE/manifest_fixture" << 'EOF' +LIBUV_URL https://github.com/libuv/libuv/archive/v1.52.1.tar.gz +LIBUV_SHA256 abc +EOF +BM_SRC='source <(sed -n "/# ── bundled-dep manifest handling/,/# ── end bundled-dep/p" '"$SCRIPT"')' +mpath=$(bash -c "TMP_DIR='$MOCK_TMP'; PATH='$MOCK_BASE/bin:\$PATH'; $BM_SRC; fetch_manifest 'https://example.com/v1/deps.txt'") +[[ -f "$mpath" ]] && grep -q 'LIBUV_URL' "$mpath" \ + && { echo " PASS: fetch_manifest content"; (( PASS++ )); } \ + || { echo " FAIL: fetch_manifest path='$mpath'"; (( FAIL++ )); ERRORS+=("T-BM7"); } +rm -f "$MOCK_BASE/manifest_fixture" +``` + +- [ ] **Step 3: Run test to verify it fails** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM7'` +Expected: FAIL (`fetch_manifest` undefined) + +- [ ] **Step 4: Add `fetch_manifest` above the end marker** + +```bash +# fetch_manifest <url> — download the manifest to a temp file, echo its path. +# Non-zero on wget failure. Caller cleans up (path is under TMP_DIR). +fetch_manifest() { + local url="$1" + local out="${TMP_DIR}/manifest" + rm -f "$out" + wget -O "$out" "$url" >&2 || return 1 + [[ -s "$out" ]] || return 1 + printf '%s\n' "$out" +} +``` + +- [ ] **Step 5: Run test to verify it passes** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM7' -A1 | grep -E 'PASS|FAIL'` +Expected: `PASS` + +- [ ] **Step 6: Verify full suite still green** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep Results:` +Expected: all passed, 0 failed (the fake-wget change must not break existing download tests — if any fail, the heredoc escaping is wrong) + +- [ ] **Step 7: Commit** + +```bash +git add mkhint tests/mkhint_test.sh +git commit -S -m "feat: fetch_manifest + fake-wget manifest fixture support + +Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" +``` + +--- + +## Task 6: `reconcile_bundle_deps` — the core + +This helper reads a hint's `DOWNLOAD` lines, matches each against a manifest, and for a given mode either **reports** (no write) or **applies** (rewrite matched changed lines + recompute md5). It also prints the manifest-only-deps FYI. + +**Files:** +- Modify: `mkhint` (above the end marker) +- Modify: `tests/mkhint_test.sh` (before SUMMARY) + +- [ ] **Step 1: Write the failing tests** + +```bash +# ── T-BM8: reconcile report mode leaves the hint unchanged ─────────────────── +echo "" +echo "T-BM8: reconcile_bundle_deps report mode: prints, does not rewrite" +cat > "$MOCK_BASE/manifest_fixture" << 'EOF' +NVIM_URL https://github.com/neovim/neovim/archive/v0.13.0.tar.gz +NVIM_SHA256 a +TREESITTER_URL https://github.com/tree-sitter/tree-sitter/archive/v0.26.8.tar.gz +TREESITTER_SHA256 b +WASMTIME_URL https://github.com/bytecodealliance/wasmtime/archive/v36.0.6.tar.gz +WASMTIME_SHA256 c +EOF +cat > "$MOCK_HINT/nvim.hint" << 'EOF' +VERSION="0.13.0" +DOWNLOAD="https://github.com/neovim/neovim/archive/v0.13.0/neovim-0.13.0.tar.gz \ + https://github.com/tree-sitter/tree-sitter/archive/v0.26.7/tree-sitter-0.26.7.tar.gz" +MD5SUM="aaa \ + bbb" +DOWNLOAD_x86_64="" +MD5SUM_x86_64="" +ARCH="x86_64" +EOF +cp "$MOCK_HINT/nvim.hint" "$MOCK_BASE/nvim.before" +BM_SRC='source <(sed -n "/# ── bundled-dep manifest handling/,/# ── end bundled-dep/p" '"$SCRIPT"')' +rep=$(bash -c "TMP_DIR='$MOCK_TMP'; PATH='$MOCK_BASE/bin:\$PATH'; source '$SCRIPT' 2>/dev/null; reconcile_bundle_deps nvim '$MOCK_HINT/nvim.hint' 'https://x/deps.txt' report" 2>&1) || true +# report mentions the changed dep and the wasmtime FYI, hint unchanged +echo "$rep" | grep -q 'tree-sitter' \ + && echo "$rep" | grep -q 'wasmtime' \ + && diff -q "$MOCK_HINT/nvim.hint" "$MOCK_BASE/nvim.before" >/dev/null \ + && { echo " PASS: report mode no rewrite"; (( PASS++ )); } \ + || { echo " FAIL: report mode. out=$rep"; (( FAIL++ )); ERRORS+=("T-BM8"); } + +# ── T-BM9: reconcile apply mode rewrites the changed dep + recomputes md5 ───── +echo "" +echo "T-BM9: reconcile_bundle_deps apply mode rewrites tree-sitter to v0.26.8" +bash -c "TMP_DIR='$MOCK_TMP'; PATH='$MOCK_BASE/bin:\$PATH'; source '$SCRIPT' 2>/dev/null; reconcile_bundle_deps nvim '$MOCK_HINT/nvim.hint' 'https://x/deps.txt' apply" >/dev/null 2>&1 +grep -q 'tree-sitter/archive/v0.26.8' "$MOCK_HINT/nvim.hint" \ + && ! grep -q 'v0.26.7' "$MOCK_HINT/nvim.hint" \ + && ! grep -q 'bbb' "$MOCK_HINT/nvim.hint" \ + && grep -q 'neovim-0.13.0' "$MOCK_HINT/nvim.hint" \ + && { echo " PASS: apply rewrote dep + md5, primary line untouched"; (( PASS++ )); } \ + || { echo " FAIL: apply mode:"; cat "$MOCK_HINT/nvim.hint"; (( FAIL++ )); ERRORS+=("T-BM9"); } +rm -f "$MOCK_BASE/manifest_fixture" +``` + +Note: these tests `source "$SCRIPT"` whole (guarded `2>/dev/null`); `main "$@"` runs at the bottom with no args, harmlessly (`getopt` with nothing → falls through, no command). If sourcing triggers unwanted output/exit, wrap the source: the tests already `|| true` and redirect. Confirm during Step 2. + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM[89]'` +Expected: FAIL (`reconcile_bundle_deps` undefined) + +- [ ] **Step 3: Implement `reconcile_bundle_deps` above the end marker** + +```bash +# reconcile_bundle_deps <pkg> <hintfile> <manifest_url> <mode> +# mode = report (print only) | apply (rewrite matched changed lines + md5). +# Reconciles the DOWNLOAD line's extra URLs (index >= 1) against the manifest. +# The primary URL (index 0) is never touched here. Guarded for set -e. +reconcile_bundle_deps() { + local pkg="$1" hint="$2" murl="$3" mode="$4" + + local mfile + if ! mfile=$(fetch_manifest "$murl"); then + echo " $pkg: manifest unavailable ($murl) — bundled deps left as-is (retry with --force)" + return 0 + fi + local -a manifest_urls + mapfile -t manifest_urls < <(parse_manifest "$mfile") + rm -f "$mfile" + if [[ ${#manifest_urls[@]} -eq 0 ]]; then + echo " $pkg: manifest empty/unrecognized — bundled deps left as-is" + return 0 + fi + local mlist; printf -v mlist '%s\n' "${manifest_urls[@]}" + + local -a urls + mapfile -t urls < <(parse_multiline_var "DOWNLOAD" "$hint") + (( ${#urls[@]} <= 1 )) && { echo " $pkg: no bundled deps in DOWNLOAD"; return 0; } + local -a md5s + mapfile -t md5s < <(parse_multiline_var "MD5SUM" "$hint") + + # Track which manifest URLs got matched (for the FYI of unmatched ones). + local -A matched_manifest=() + local -a new_urls=("${urls[@]}") + local -a changed_idx=() changed_from=() changed_to=() + local i m + for (( i=1; i<${#urls[@]}; i++ )); do + m=$(match_dep_url "${urls[$i]}" "$mlist") || m="" + if [[ -z "$m" ]]; then + echo " $pkg: $(_url_stem "${urls[$i]}") — no manifest match (left as-is)" + continue + fi + matched_manifest["$m"]=1 + if [[ "$m" != "${urls[$i]}" ]]; then + changed_idx+=("$i"); changed_from+=("${urls[$i]}"); changed_to+=("$m") + new_urls[$i]="$m" + fi + done + + # Manifest-only deps FYI (manifest URLs never matched by any hint line). + local -a extra=() + for m in "${manifest_urls[@]}"; do + [[ -z "${matched_manifest[$m]:-}" ]] && extra+=("$(_url_stem "$m")") + done + if [[ ${#extra[@]} -gt 0 ]]; then + echo "$pkg: manifest has ${#extra[@]} deps not in hint:" + printf '%s\n' "${extra[@]}" + fi + + if [[ ${#changed_idx[@]} -eq 0 ]]; then + echo " $pkg: bundled deps all current" + return 0 + fi + + echo "" + echo "$pkg bundled deps changed upstream:" + for (( i=0; i<${#changed_idx[@]}; i++ )); do + printf ' %s -> %s\n' "$(_url_stem "${changed_from[$i]}")" "$(_url_stem "${changed_to[$i]}")" + done + + [[ "$mode" == report ]] && return 0 + + # apply mode: recompute md5 only for changed lines, rewrite DOWNLOAD+MD5SUM. + local -a new_md5s=("${md5s[@]}") + local idx md5 + for idx in "${changed_idx[@]}"; do + echo "Downloading (bundled): ${new_urls[$idx]}" + if md5=$(download_file "${new_urls[$idx]}"); then + new_md5s[$idx]="$md5" + else + echo " download failed for ${new_urls[$idx]} — left as-is" + new_urls[$idx]="${urls[$idx]}" # revert this line + fi + done + + local new_dl new_md5 + new_dl=$(build_multiline_value new_urls); new_dl="${new_dl#\"}"; new_dl="${new_dl%\"}" + new_md5=$(build_multiline_value new_md5s); new_md5="${new_md5#\"}"; new_md5="${new_md5%\"}" + perl -i -0pe 'BEGIN{$v=shift} s|^DOWNLOAD="[^"]*(?:\\\n[^"]*)*"|DOWNLOAD="$v"|m' "$new_dl" "$hint" + perl -i -0pe 'BEGIN{$v=shift} s|^MD5SUM="[^"]*(?:\\\n[^"]*)*"|MD5SUM="$v"|m' "$new_md5" "$hint" +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM[89]' -A1 | grep -E 'PASS|FAIL'` +Expected: two `PASS` + +- [ ] **Step 5: Commit** + +```bash +git add mkhint tests/mkhint_test.sh +git commit -S -m "feat: reconcile_bundle_deps core (report/apply) + +Match each extra DOWNLOAD line to the manifest, rewrite changed lines and +recompute their md5 in apply mode, report only in report mode, and print +the manifest-only-deps FYI. Primary line untouched. + +Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" +``` + +--- + +## Task 7: `--new` hook (report mode) + +**Files:** +- Modify: `mkhint` `create_new_hint_file` (after the hint is generated, near mkhint:474 `echo "generated ..."`) +- Modify: `tests/mkhint_test.sh` (before SUMMARY) + +- [ ] **Step 1: Write the failing tests** + +Requires a `datedpkg`-style `.info` fixture for a listed package. Reuse a github `.info`. Add a `bmnvim` package fixture in `setup()` and list it. First, in `setup()` add the dir to the `mkdir -p` list: `"$MOCK_REPO/editors/bmnvim"`. Then add the `.info`: + +```bash + cat > "$MOCK_REPO/editors/bmnvim/bmnvim.info" << 'EOF' +PRGNAM="bmnvim" +VERSION="0.13.0" +HOMEPAGE="https://neovim.io" +DOWNLOAD="https://github.com/neovim/neovim/archive/v0.13.0/bmnvim-0.13.0.tar.gz \ + https://github.com/tree-sitter/tree-sitter/archive/v0.26.7/tree-sitter-0.26.7.tar.gz" +MD5SUM="aaa \ + bbb" +DOWNLOAD_x86_64="" +MD5SUM_x86_64="" +REQUIRES="" +MAINTAINER="Test" +EMAIL="test@test.com" +EOF +``` + +Test: + +```bash +# ── T-BM10: --new listed pkg prints reconcile report, does NOT rewrite ─────── +echo "" +echo "T-BM10: --new listed pkg → manifest report, hint not rewritten by manifest" +cat > "$MOCK_BASE/bundle-manifests" << 'EOF' +bmnvim https://example.com/bmnvim/v{VERSION}/deps.txt +EOF +cat > "$MOCK_BASE/manifest_fixture" << 'EOF' +NVIM_URL https://github.com/neovim/neovim/archive/v0.13.0.tar.gz +NVIM_SHA256 a +TREESITTER_URL https://github.com/tree-sitter/tree-sitter/archive/v0.26.8.tar.gz +TREESITTER_SHA256 b +EOF +rm -f "$MOCK_HINT/bmnvim.hint" +out=$(run_mkhint -n bmnvim 2>&1) +echo "$out" | grep -qi 'tree-sitter' \ + && grep -q 'v0.26.7' "$MOCK_HINT/bmnvim.hint" \ + && { echo " PASS: --new reported, kept .info dep version"; (( PASS++ )); } \ + || { echo " FAIL: --new manifest report. out=$out"; (( FAIL++ )); ERRORS+=("T-BM10"); } + +# ── T-BM11: --new non-listed pkg runs no manifest code ─────────────────────── +echo "" +echo "T-BM11: --new non-listed pkg → no manifest output" +: > "$MOCK_BASE/bundle-manifests" +rm -f "$MOCK_HINT/curl.hint" +out=$(run_mkhint -n curl 2>&1) +echo "$out" | grep -qi 'manifest' \ + && { echo " FAIL: unexpected manifest output for curl"; (( FAIL++ )); ERRORS+=("T-BM11"); } \ + || { echo " PASS: no manifest code for non-listed"; (( PASS++ )); } +rm -f "$MOCK_BASE/manifest_fixture" "$MOCK_BASE/bundle-manifests" +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM1[01]'` +Expected: T-BM10 FAILs (no report yet); T-BM11 may pass already. + +- [ ] **Step 3: Add the hook in `create_new_hint_file`** + +After the `echo "Check variables before using."` / before `add_nvchecker_section` call (mkhint:~475), add: + +```bash + load_bundle_manifests + if pkg_has_manifest "${normalized_file%.hint}"; then + local _bm_ver; _bm_ver=$(grep '^VERSION=' "$normalized_file" | sed 's/VERSION="//;s/"$//') + local _bm_url; _bm_url=$(manifest_url_for "${normalized_file%.hint}" "$_bm_ver") + echo "$( "${normalized_file%.hint}" )" >/dev/null 2>&1 || true # no-op guard + echo "${normalized_file%.hint}: bundled-dep manifest check" + reconcile_bundle_deps "${normalized_file%.hint}" "$normalized_file" "$_bm_url" report || true + fi +``` + +Remove the stray no-op guard line if it complicates; the essential calls are `load_bundle_manifests`, `pkg_has_manifest`, `manifest_url_for`, `reconcile_bundle_deps ... report`. + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM1[01]' -A1 | grep -E 'PASS|FAIL'` +Expected: two `PASS` + +- [ ] **Step 5: Commit** + +```bash +git add mkhint tests/mkhint_test.sh +git commit -S -m "feat: --new prints a manifest reconcile report for listed packages + +Report-only at creation time; the .info version is kept, upstream +disagreement is flagged. + +Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" +``` + +--- + +## Task 8: `--force` flag plumbing + +**Files:** +- Modify: `mkhint` main getopt (line 1028-1029), the option loop, the mutual-exclusion checks, and a `FORCE` var near the other flag vars (mkhint:44-55) +- Modify: `tests/mkhint_test.sh` (before SUMMARY) + +- [ ] **Step 1: Write the failing test (mutual exclusion)** + +```bash +# ── T-BM12: --force with --hintfile → exit 1 ───────────────────────────────── +echo "" +echo "T-BM12: --force is --check-only (errors with --hintfile/--new/--fix-current)" +set +e +run_mkhint --force -f curl -V 1.0 >/dev/null 2>&1; c1=$? +run_mkhint --force -n curl >/dev/null 2>&1; c2=$? +run_mkhint --force -F >/dev/null 2>&1; c3=$? +set -e +[[ $c1 -eq 1 && $c2 -eq 1 && $c3 -eq 1 ]] \ + && { echo " PASS: --force mutually exclusive"; (( PASS++ )); } \ + || { echo " FAIL: exits $c1 $c2 $c3"; (( FAIL++ )); ERRORS+=("T-BM12"); } +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM12'` +Expected: FAIL (`--force` unknown option → getopt error, likely exit 1 already for wrong reason; the test still meaningfully drives the flag once added) + +- [ ] **Step 3: Add `FORCE=0` var** + +After `NO_DL=0` (mkhint:55), add: + +```bash +FORCE=0 +``` + +- [ ] **Step 4: Add `--force` to getopt and the option loop** + +Change the getopt long list (mkhint:1029) to append `,force`: + +```bash + parsed=$(getopt -o vV:f:n:lcCdNhRF \ + --long version,set-version:,hintfile:,new:,list,clean,check,delete,no-dl,help,review,fix-current,force \ + -n 'mkhint' -- "$@") || { show_help; exit 1; } +``` + +Add a case in the option loop (after the `--no-dl|-N)` case, mkhint:~1065): + +```bash + --force) + FORCE=1 + shift + ;; +``` + +- [ ] **Step 5: Add mutual-exclusion check** + +After the existing `--fix-current` mutual-exclusion block (mkhint:~1120), add: + +```bash + if [[ $FORCE -eq 1 && "$COMMAND" != "check" ]]; then + echo "Error: --force is only valid with --check" >&2 + exit 1 + fi +``` + +- [ ] **Step 6: Run test to verify it passes** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM12' -A1 | grep -E 'PASS|FAIL'` +Expected: `PASS` + +- [ ] **Step 7: Commit** + +```bash +git add mkhint tests/mkhint_test.sh +git commit -S -m "feat: add --force flag (check-only, mutually exclusive elsewhere) + +Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" +``` + +--- + +## Task 9: `--check` Phase 2 wiring + +Wire the reconcile into `check_updates`: after the primary-update loop, for each listed target run Phase 2 in **apply** mode when the primary bumped this run OR `--force`; else skip. + +**Files:** +- Modify: `mkhint` `check_updates` (add a Phase 2 section after the `updated=()` slackrepo block, mkhint:~1012; and thread `FORCE` in) +- Modify: `tests/mkhint_test.sh` (before SUMMARY) + +- [ ] **Step 1: Write the failing tests** + +```bash +# ── T-BM13..17: --check Phase 2 behaviors ──────────────────────────────────── +# Shared fixtures: bmnvim listed, hint present, nvchecker seeded. +setup_bmnvim_check() { + cat > "$MOCK_BASE/bundle-manifests" << 'EOF' +bmnvim https://example.com/bmnvim/v{VERSION}/deps.txt +EOF + cat > "$MOCK_BASE/manifest_fixture" << 'EOF' +NVIM_URL https://github.com/neovim/neovim/archive/v0.13.0.tar.gz +NVIM_SHA256 a +TREESITTER_URL https://github.com/tree-sitter/tree-sitter/archive/v0.26.8.tar.gz +TREESITTER_SHA256 b +EOF + cat > "$MOCK_HINT/bmnvim.hint" << 'EOF' +VERSION="0.13.0" +DOWNLOAD="https://github.com/neovim/neovim/archive/v0.13.0/bmnvim-0.13.0.tar.gz \ + https://github.com/tree-sitter/tree-sitter/archive/v0.26.7/tree-sitter-0.26.7.tar.gz" +MD5SUM="aaa \ + bbb" +DOWNLOAD_x86_64="" +MD5SUM_x86_64="" +ARCH="x86_64" +EOF + # nvchecker keyfile says bmnvim latest == current (0.13.0) → primary unchanged + cat > "$MOCK_BASE/nvchecker_keyfile.json" << 'EOF' +{"version":2,"data":{"bmnvim":{"version":"0.13.0"}}} +EOF +} + +# T-BM13: primary unchanged, no --force → Phase 2 skipped (dep stays v0.26.7) +echo "" +echo "T-BM13: --check listed, primary current, no --force → deps untouched" +setup_bmnvim_check +run_mkhint -C bmnvim < <(printf 'n\n') >/dev/null 2>&1 +grep -q 'v0.26.7' "$MOCK_HINT/bmnvim.hint" \ + && { echo " PASS: Phase 2 skipped"; (( PASS++ )); } \ + || { echo " FAIL: dep changed without --force"; (( FAIL++ )); ERRORS+=("T-BM13"); } + +# T-BM14: primary unchanged, --force → Phase 2 runs, dep bumped on Y +echo "" +echo "T-BM14: --check --force listed, primary current → deps reconciled" +setup_bmnvim_check +run_mkhint -C --force bmnvim < <(printf 'Y\n') >/dev/null 2>&1 +grep -q 'tree-sitter/archive/v0.26.8' "$MOCK_HINT/bmnvim.hint" \ + && { echo " PASS: --force ran Phase 2"; (( PASS++ )); } \ + || { echo " FAIL: --force did not bump dep"; cat "$MOCK_HINT/bmnvim.hint"; (( FAIL++ )); ERRORS+=("T-BM14"); } + +# T-BM15: manifest fetch fails → primary stands, deps unchanged, retry msg +echo "" +echo "T-BM15: --check --force, manifest fetch fails → deps unchanged" +setup_bmnvim_check +rm -f "$MOCK_BASE/manifest_fixture" # fake wget returns exit 1 for deps.txt +out=$(run_mkhint -C --force bmnvim < <(printf 'Y\n') 2>&1) +grep -q 'v0.26.7' "$MOCK_HINT/bmnvim.hint" && echo "$out" | grep -qi 'manifest unavailable' \ + && { echo " PASS: fetch fail handled"; (( PASS++ )); } \ + || { echo " FAIL: fetch fail. out=$out"; (( FAIL++ )); ERRORS+=("T-BM15"); } + +# T-BM16: manifest-only deps FYI printed +echo "" +echo "T-BM16: --check --force prints manifest-only-deps FYI" +setup_bmnvim_check +cat > "$MOCK_BASE/manifest_fixture" << 'EOF' +NVIM_URL https://github.com/neovim/neovim/archive/v0.13.0.tar.gz +NVIM_SHA256 a +TREESITTER_URL https://github.com/tree-sitter/tree-sitter/archive/v0.26.7.tar.gz +TREESITTER_SHA256 b +WASMTIME_URL https://github.com/bytecodealliance/wasmtime/archive/v36.0.6.tar.gz +WASMTIME_SHA256 c +EOF +out=$(run_mkhint -C --force bmnvim < <(printf 'n\n') 2>&1) +echo "$out" | grep -q 'deps not in hint:' && echo "$out" | grep -q '^wasmtime' \ + && { echo " PASS: FYI list printed"; (( PASS++ )); } \ + || { echo " FAIL: FYI. out=$out"; (( FAIL++ )); ERRORS+=("T-BM16"); } +rm -f "$MOCK_BASE/manifest_fixture" "$MOCK_BASE/bundle-manifests" "$MOCK_HINT/bmnvim.hint" +``` + +Note on the nvchecker keyfile: `check_updates` reads the keyfile path from `NVCHECKER_CONFIG`'s `[__config__] newver`. The existing suite already seeds a mock `nvchecker.toml` + keyfile (see the `--check` tests near T23). Match that setup — reuse whatever the existing `--check` tests use to seed a keyfile/toml, adapting the JSON path to `$MOCK_BASE/nvchecker_keyfile.json`. If the existing tests use a different keyfile filename or a `newver` path, mirror that exactly instead of introducing a new file. + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM1[3-6]'` +Expected: FAILs (Phase 2 not wired) + +- [ ] **Step 3: Wire Phase 2 into `check_updates`** + +`check_updates` (mkhint:901) currently ends after the slackrepo split (mkhint:~1012). It also needs to know which packages bumped their primary this run (the `updated` array already tracks that) and honor `FORCE`. Insert **before** the final `}` of `check_updates`, after the slackrepo block: + +```bash + # Phase 2: bundled-dep manifest reconcile for listed packages. + load_bundle_manifests + local was_updated + for pkg in "${targets[@]}"; do + pkg_has_manifest "$pkg" || continue + local hintpath="${HINT_DIR%/}/${pkg}.hint" + [[ -f "$hintpath" ]] || continue + was_updated=0 + local u + for u in "${updated[@]}"; do [[ "$u" == "$pkg" ]] && was_updated=1; done + if [[ $was_updated -eq 0 && $FORCE -ne 1 ]]; then + continue # primary unchanged and not forced → skip Phase 2 + fi + local cur; cur=$(grep '^VERSION=' "$hintpath" | sed 's/VERSION="//;s/"$//') + local murl; murl=$(manifest_url_for "$pkg" "$cur") || continue + echo "" + echo "Bundled-dep reconcile: $pkg (manifest @ $cur)" + # report first, then confirm apply + reconcile_bundle_deps "$pkg" "$hintpath" "$murl" report || true + local ans + read -r -p "Apply bundled-dep updates for $pkg? [Y/n] " ans + ans="${ans:-Y}" + if [[ "$ans" =~ ^[Yy]$ ]]; then + reconcile_bundle_deps "$pkg" "$hintpath" "$murl" apply || true + fi + done +``` + +Note: `reconcile_bundle_deps` is called twice (report, then apply) — it fetches the manifest each time. That is one extra small GET; acceptable (ponytail: refetch, cache to a per-run temp if it ever matters). Alternatively fold report+confirm+apply into a single `interactive` mode. Keep the two-call form for now; it reuses the mode param cleanly. + +`targets` and `updated` are already locals in `check_updates`; `FORCE` is a global set in `main`. `pkg` is reused as the loop var (already declared local earlier in the function — reuse is fine). + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM1[3-6]' -A1 | grep -E 'PASS|FAIL'` +Expected: four `PASS` + +- [ ] **Step 5: Verify whole suite** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep Results:` +Expected: all passed, 0 failed + +- [ ] **Step 6: Commit** + +```bash +git add mkhint tests/mkhint_test.sh +git commit -S -m "feat: --check Phase 2 reconciles bundled deps for listed packages + +Runs after the primary bump; auto when the primary changed this run, +otherwise only with --force. Report then per-package apply confirm. + +Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" +``` + +--- + +## Task 9b: `--hintfile -V` parity (reconcile on the update path) + +The `update` command (mkhint:1166-1178) bumps a single hint via `update_hint_file` then `prompt_slackrepo`. For a **listed** package it must also run Phase 2 (apply) after the primary bump, so `mkhint -f neovim -V 0.13.0` reconciles bundled deps too. The old `prompt_continuation_urls` inside `update_hint_file` still fires for listed packages during the primary bump — that is acceptable: the extra lines it prompts for are then corrected by Phase 2. (Follow-up could suppress the prompt for listed packages; not required now.) + +**Files:** +- Modify: `mkhint` `main` `update)` case (mkhint:1166-1178) +- Modify: `tests/mkhint_test.sh` (before SUMMARY) + +- [ ] **Step 1: Write the failing test** + +```bash +# ── T-BM18: --hintfile -V on a listed pkg reconciles bundled deps ──────────── +echo "" +echo "T-BM18: -f listed pkg -V bumps primary AND reconciles bundled deps" +cat > "$MOCK_BASE/bundle-manifests" << 'EOF' +bmnvim https://example.com/bmnvim/v{VERSION}/deps.txt +EOF +cat > "$MOCK_BASE/manifest_fixture" << 'EOF' +NVIM_URL https://github.com/neovim/neovim/archive/v0.14.0.tar.gz +NVIM_SHA256 a +TREESITTER_URL https://github.com/tree-sitter/tree-sitter/archive/v0.26.8.tar.gz +TREESITTER_SHA256 b +EOF +cat > "$MOCK_HINT/bmnvim.hint" << 'EOF' +VERSION="0.13.0" +DOWNLOAD="https://github.com/neovim/neovim/archive/v0.13.0/bmnvim-0.13.0.tar.gz \ + https://github.com/tree-sitter/tree-sitter/archive/v0.26.7/tree-sitter-0.26.7.tar.gz" +MD5SUM="aaa \ + bbb" +DOWNLOAD_x86_64="" +MD5SUM_x86_64="" +ARCH="x86_64" +EOF +# stdin: (continuation prompt for the primary bump: keep) then (apply Y) then (slackrepo n) +run_mkhint -f bmnvim -V 0.14.0 < <(printf '\nY\nn\n') >/dev/null 2>&1 +grep -q 'VERSION="0.14.0"' "$MOCK_HINT/bmnvim.hint" \ + && grep -q 'tree-sitter/archive/v0.26.8' "$MOCK_HINT/bmnvim.hint" \ + && { echo " PASS: -f -V bumped primary + reconciled deps"; (( PASS++ )); } \ + || { echo " FAIL: -f -V parity:"; cat "$MOCK_HINT/bmnvim.hint"; (( FAIL++ )); ERRORS+=("T-BM18"); } +rm -f "$MOCK_BASE/manifest_fixture" "$MOCK_BASE/bundle-manifests" "$MOCK_HINT/bmnvim.hint" +``` + +Note: the exact stdin sequence depends on how many continuation URLs `update_hint_file`'s `prompt_continuation_urls` asks about (one extra line here → one blank "keep"). If the run consumes prompts differently, adjust the `printf` sequence so: blank-lines cover each continuation-URL keep, then `Y` for the reconcile apply, then `n` for slackrepo. Verify against the actual prompt order in Step 3. + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM18'` +Expected: FAIL (dep stays v0.26.7 — no Phase 2 on the update path) + +- [ ] **Step 3: Add the reconcile block to the `update)` case** + +In `main`, the `update)` case (mkhint:1166) currently ends with `prompt_slackrepo "$HINT_FILE"`. Insert **before** `prompt_slackrepo`, after the `update_hint_file` / `nvtake` lines: + +```bash + load_bundle_manifests + _bm_pkg="${HINT_FILE%.hint}" + if pkg_has_manifest "$_bm_pkg"; then + _bm_hint="${HINT_DIR%/}/${_bm_pkg}.hint" + _bm_cur=$(grep '^VERSION=' "$_bm_hint" | sed 's/VERSION="//;s/"$//') + _bm_url=$(manifest_url_for "$_bm_pkg" "$_bm_cur") + echo "" + echo "Bundled-dep reconcile: $_bm_pkg (manifest @ $_bm_cur)" + reconcile_bundle_deps "$_bm_pkg" "$_bm_hint" "$_bm_url" report || true + read -r -p "Apply bundled-dep updates for $_bm_pkg? [Y/n] " _bm_ans + _bm_ans="${_bm_ans:-Y}" + if [[ "$_bm_ans" =~ ^[Yy]$ ]]; then + reconcile_bundle_deps "$_bm_pkg" "$_bm_hint" "$_bm_url" apply || true + fi + fi +``` + +(`HINT_FILE` may or may not carry a `.hint` suffix depending on how the user invoked it; `update_hint_file` normalizes internally. Here strip a trailing `.hint` for the pkg name and rebuild the path via `HINT_DIR`, matching `update_hint_file`'s normalization.) + +- [ ] **Step 4: Run test to verify it passes** + +Run: `bash tests/mkhint_test.sh 2>&1 | grep -E 'T-BM18' -A1 | grep -E 'PASS|FAIL'` +Expected: `PASS` + +- [ ] **Step 5: Commit** + +```bash +git add mkhint tests/mkhint_test.sh +git commit -S -m "feat: --hintfile -V reconciles bundled deps for listed packages + +Parity with --check: the update path runs Phase 2 (report + apply) after +the primary bump. + +Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" +``` + +--- + +## Task 10: bash completion + example config + +**Files:** +- Modify: `mkhint.bash-completion:17` (`all_flags`) +- Create: `bundle-manifests.example` (repo root) + +- [ ] **Step 1: Add `--force` to completion flags** + +In `mkhint.bash-completion` line 17, append `--force` to `all_flags`: + +```bash + local all_flags="--version -v --set-version -V --hintfile -f --new -n --list -l --review -R --clean -c --check -C --fix-current -F --delete -d --no-dl -N --force --help -h" +``` + +- [ ] **Step 2: Verify completion sources cleanly** + +Run: `bash -n mkhint.bash-completion && echo OK` +Expected: `OK` + +- [ ] **Step 3: Create `bundle-manifests.example`** + +``` +# mkhint bundle-manifests — packages whose extra DOWNLOAD lines are driven by an +# upstream deps manifest. Copy to ~/.config/mkhint/bundle-manifests and edit. +# +# Format: <pkgname> <deps-url-template> +# {VERSION} is replaced with the hint's version at check time (both '_' and '-' +# forms are tried). One entry per line; '#' comments and blank lines ignored. +# +# Example (neovim publishes cmake.deps/deps.txt with pinned dep URLs): +# neovim https://raw.githubusercontent.com/neovim/neovim/v{VERSION}/cmake.deps/deps.txt +``` + +- [ ] **Step 4: Commit** + +```bash +git add mkhint.bash-completion bundle-manifests.example +git commit -S -m "feat: --force completion + bundle-manifests.example + +Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" +``` + +--- + +## Task 11: docs (help, man page, CLAUDE.md, changelog) + release v1.2.0 + +**Files:** +- Modify: `mkhint` `show_help` (mkhint:64-83), `MKHINT_VERSION` (mkhint:42) +- Modify: `mkhint.1.md` (OPTIONS + new section + title version), rebuild `mkhint.1.gz` +- Modify: `CLAUDE.md`, `CHANGELOG.md` + +- [ ] **Step 1: Add `--force` to `--help` options list** + +In `show_help` (after the `--no-dl` line, mkhint:75), add: + +```bash + --force With --check: reconcile bundled deps even if the primary version is unchanged +``` + +And add a line in the paths block (after the phantom-dep line, mkhint:81): + +```bash +Bundle manifests (for --check bundled-dep reconcile): $BUNDLE_MANIFEST_FILE +``` + +- [ ] **Step 2: Bump `MKHINT_VERSION`** + +Change mkhint:42 `readonly MKHINT_VERSION="1.1.3"` → `"1.2.0"`. + +- [ ] **Step 3: Add `--force` + a BUNDLED DEPENDENCIES section to `mkhint.1.md`** + +In `mkhint.1.md`, add to OPTIONS (option names `\--`-escaped for pandoc): + +``` +\--force +: Only meaningful with \--check. Reconcile a bundle package's extra download + lines against its upstream manifest even when the primary version has not + changed. Use to retry after a previous manifest fetch failed. +``` + +Add a new section after BASH COMPLETION: + +``` +# BUNDLED DEPENDENCIES + +Some SlackBuilds bundle several tarballs in one DOWNLOAD line: a primary source +plus pinned dependencies (neovim is the canonical case). Such packages can be +listed in the file named by BUNDLE_MANIFEST_FILE +(default \~/.config/mkhint/bundle-manifests), one entry per line: + + neovim https://raw.githubusercontent.com/neovim/neovim/v{VERSION}/cmake.deps/deps.txt + +The URL is an upstream, machine-readable deps manifest (NAME_URL / NAME_SHA256 +pairs); {VERSION} is substituted with the hint's version. For a listed package, +\--new prints a reconcile report (no changes), and \--check reconciles the extra +download lines against the manifest after the primary bump, rewriting changed +lines and recomputing their checksums. Bundled deps are never followed to their +own latest release; only the manifest's pinned URLs are used. Manifest entries +with no matching download line are reported but not added. +``` + +Update the title-line version: `% MKHINT(1) mkhint 1.2.0 | User Commands`. + +- [ ] **Step 4: Rebuild the man page** + +Run: +```bash +pandoc mkhint.1.md -s -t man -o mkhint.1 && gzip -9 -n -f mkhint.1 +zcat mkhint.1.gz | groff -man -Tascii -ww >/dev/null && echo "groff clean" +``` +Expected: `groff clean`, no warnings. + +- [ ] **Step 5: Add a Key Behavior entry + config note to `CLAUDE.md`** + +In the Configuration section, add `BUNDLE_MANIFEST_FILE` to the list of config-overridable vars. In Key Behaviors, add: + +```markdown +- `--check` bundled-dep reconcile: for packages listed in `BUNDLE_MANIFEST_FILE` + (`<pkg> <url-template-with-{VERSION}>`), after the primary bump mkhint fetches + the upstream deps manifest (`NAME_URL`/`NAME_SHA256` pairs, e.g. neovim's + `cmake.deps/deps.txt`), matches each extra `DOWNLOAD` line via + `match_dep_url` (repo-path first, exact basename-stem fallback for blob + hosts), and rewrites changed lines + recomputes their md5. Runs + automatically when the primary changed this run, otherwise only with + `--force` (check-only flag). `--new` on a listed package prints the reconcile + report without changing the hint. Manifest deps with no hint line are listed + as an FYI, never added (would need a SlackBuild change). Helpers: + `load_bundle_manifests`, `manifest_url_for`, `fetch_manifest`, + `parse_manifest`, `match_dep_url`, `reconcile_bundle_deps`. +``` + +- [ ] **Step 6: Move the CHANGELOG `[Unreleased]`/new section** + +Add at the top of `CHANGELOG.md` (above `## [1.1.3]`): + +```markdown +## [1.2.0] - 2026-07-07 + +### Added +- Manifest-driven bundled-dep updates. Packages listed in the new + `BUNDLE_MANIFEST_FILE` (`<pkg> <deps-url-template>`) have their extra + `DOWNLOAD` lines reconciled against an upstream deps manifest (e.g. neovim's + `cmake.deps/deps.txt`) during `--check`: matched lines are rewritten to the + manifest's pinned URLs and their checksums recomputed. `--new` reports the + reconcile without changing the hint. +- `--force` flag (check-only): run the bundled-dep reconcile even when the + primary version is unchanged (e.g. to retry after a failed manifest fetch). +``` + +- [ ] **Step 7: Verify version + whole suite** + +Run: +```bash +./mkhint -v +bash tests/mkhint_test.sh 2>&1 | grep Results: +``` +Expected: `mkhint 1.2.0`; all passed, 0 failed. + +- [ ] **Step 8: Commit the docs + release bump** + +```bash +git add mkhint mkhint.1.md mkhint.1.gz CLAUDE.md CHANGELOG.md +git commit -S -m "docs: document bundled-dep reconcile + --force; release v1.2.0 + +Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>" +``` + +- [ ] **Step 9: Tag the release** + +```bash +git tag -s -m "mkhint 1.2.0" v1.2.0 +git log --format='%h %G? %s' -1 +git tag -v v1.2.0 2>&1 | grep -i 'good signature' || true +``` +Expected: commit line shows `G`; tag verifies. + +--- + +## Task 12: push both remotes + deploy to VM + +**Files:** none (deployment). + +- [ ] **Step 1: Push master + tag to both remotes** + +```bash +git push origin master && git push origin v1.2.0 +``` +Expected: both `danix_git` and `slackware_forge` push URLs updated; new tag on both. + +- [ ] **Step 2: Deploy binary + man page to the VM** + +```bash +scp mkhint buildsystem:/usr/local/bin/mkhint +scp mkhint.1.gz buildsystem:/usr/local/man/man1/mkhint.1.gz +``` + +- [ ] **Step 3: Deploy the updated completion script (it changed this release)** + +```bash +scp mkhint.bash-completion buildsystem:/etc/bash_completion.d/mkhint +``` +(Note VM completion dir uses underscore: `/etc/bash_completion.d/`.) + +- [ ] **Step 4: Verify md5 + version on the VM** + +```bash +lb=$(md5sum mkhint | awk '{print $1}'); lm=$(md5sum mkhint.1.gz | awk '{print $1}') +ssh buildsystem "md5sum /usr/local/bin/mkhint /usr/local/man/man1/mkhint.1.gz; mkhint -v" +echo "local bin $lb man $lm" +``` +Expected: VM md5s match local; `mkhint -v` prints `mkhint 1.2.0`. + +- [ ] **Step 5: (manual, optional) seed the real bundle-manifests on the VM** + +Not automated. When ready, create `/root/.config/mkhint/bundle-manifests` on the VM with the neovim line from `bundle-manifests.example`, then `mkhint -C --force neovim` to reconcile. + +--- + +## Self-review notes + +- **Spec coverage:** Section 1 (config) → T1. Section 2 (parser/matcher) → T2, T3, T4, T5. Section 3 (--new) → T7. Section 4 (--check two-phase + --force gate) → T8, T9. Section 5 (errors, FYI, non-goals) → T6 (FYI + unmatched), T9 (fetch fail). Section 6 (tests) → distributed. Section 7 (layout/docs) → T10, T11, T12. All sections covered. +- **`--hintfile -V` on a listed package** (spec Section 4): covered by Task 9b, which mirrors the Phase 2 reconcile into the `update` case. Full spec parity. +- **Type/name consistency:** helper names match across tasks and the CLAUDE.md entry (`load_bundle_manifests`, `manifest_url_for`, `fetch_manifest`, `parse_manifest`, `match_dep_url`, `reconcile_bundle_deps`, plus internal `_url_stem`, `_url_repo`, `pkg_has_manifest`). +- **Sourcing helpers in unit tests:** relies on the block markers `# ── bundled-dep manifest handling ──` … `# ── end bundled-dep manifest handling ──` (added T2 Step 3). Later helper tests source between those markers; integration tests source the whole script. |
