diff --git a/core.lock b/core.lock index fa640cb..474e072 100644 --- a/core.lock +++ b/core.lock @@ -1,6 +1,6 @@ # GENERATED by dotfiles-core sync-core.sh — vendored Core provenance (B1). # Regenerate after a manual 'git subtree pull' with: make core-lock -core_version=4.5.0 -core_sha=bea73f6080b04704ffd206bd935073e38781aec9 -core_branch=bea73f6080b04704ffd206bd935073e38781aec9 -core_tag=v4.5.0 +core_version=4.6.0 +core_sha=f1780a40031ef75368d49e866d6274a34f4f3a75 +core_branch=f1780a40031ef75368d49e866d6274a34f4f3a75 +core_tag=v4.6.0 diff --git a/core/.pre-commit-config.yaml b/core/.pre-commit-config.yaml index 58bdb7e..1c7869c 100644 --- a/core/.pre-commit-config.yaml +++ b/core/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace exclude: '\.md$' @@ -34,7 +34,7 @@ repos: - id: check-shebang-scripts-are-executable - repo: https://github.com/koalaman/shellcheck-precommit - rev: v0.10.0 + rev: v0.11.0 hooks: - id: shellcheck args: ["-x"] @@ -44,7 +44,7 @@ repos: # shellcheck/zsh -n/toml-yaml never inspect — so lint it at author time too. # Shares .markdownlint.jsonc with the audit's markdown section (one rule config). - repo: https://github.com/DavidAnson/markdownlint-cli2 - rev: v0.22.1 + rev: v0.23.2 hooks: - id: markdownlint-cli2 diff --git a/core/CHANGELOG.md b/core/CHANGELOG.md index 306100d..efcb9f7 100644 --- a/core/CHANGELOG.md +++ b/core/CHANGELOG.md @@ -13,6 +13,66 @@ commit (`git tag -a vX.Y.Z -m vX.Y.Z`). ## [Unreleased] +## [v4.6.0] - 2026-07-30 + +### Added + +- **`:checkhealth gerrrt` now reports LSP / formatter / linter readiness.** The built-in + `:checkhealth vim.lsp` only lists clients _attached to the live session_, so run from the + dashboard it reads "No active clients" and says nothing about whether your configured tools + are installed. Three new sections in `nvim/lua/gerrrt/health.lua` report per-tool state: + - **LSP servers** — every wanted server (from `nvim/lua/gerrrt/servers/init.lua`) as + attached / enabled-idle / pending-enable / binary-missing / override-failed, via a new + read-only `M.status()` export that reuses the module's own wanted-list and + `binary_available()`. It tracks _registered_ (our override loaded) and _enabled_ (we called + `vim.lsp.enable`) as **distinct** facts — not inferred from `vim.lsp.config[name]`, which + also resolves upstream lspconfig defaults — so a failed override or an installed-but-not-yet- + enabled binary is reported accurately. + - **Formatters (conform)** — rendered from conform's own `list_all_formatters()` availability. + - **Linters (nvim-lint)** — from `linters_by_ft` + the SAST `semgrep`, checking each linter's + real builtin `cmd`. + + All three are **side-effect-free**: they observe `package.loaded` and never `require()` a plugin + (which would force-load it and, for the LSP stack, register/enable servers before nvim-lspconfig's + defaults are on the runtimepath), so from the dashboard they say "open a file, then re-run" rather + than mutating the session. Missing binaries are info (not warnings) on `DOTFILES_OFFLINE` boxes. + Covered by new assertions in `scripts/test-core.sh` (the D4 registry probe exercises `status()`; + the checkhealth probe asserts all four sections render). + (`nvim/lua/gerrrt/health.lua`, `nvim/lua/gerrrt/servers/init.lua`, `scripts/test-core.sh`) + +### Changed + +- **Dev toolchain and plugin pins rolled forward.** Routine freshness sweep. The gate + toolchain was bumped in `scripts/tool-versions.env` (the single source — `ci.yml` and + `make setup` read it, and the audit's consistency gate enforces the `.pre-commit-config.yaml` + revs match it): **shellcheck** `0.10.0 → 0.11.0` (with its `SHELLCHECK_SHA256` recomputed + via `scripts/update-tool-checksums.sh`), **markdownlint-cli2** `0.22.1 → 0.23.2`, and + **pre-commit-hooks** `v5.0.0 → v6.0.0`. Plugin pins were rolled to upstream: the zsh + `zsh-transient-prompt` pin (`zsh/45-plugins.zsh`) and four Neovim plugins in + `nvim/lazy-lock.json` (`nvim-lspconfig`, `package-info.nvim`, `rainbow-delimiters.nvim`, + `schemastore.nvim`). (`scripts/tool-versions.env`, `.pre-commit-config.yaml`, + `zsh/45-plugins.zsh`, `nvim/lazy-lock.json`) + +### Fixed + +- **`:checkhealth gerrrt` clipboard false alarm on native Windows.** On the Windows host + (where `dotfiles-Windows` vendors only `nvim/` and never runs Core's bootstrap), the + clipboard section warned "Core's cross-OS clipboard scripts are not on PATH (clip: found, + clip-paste: missing)" — misleading, because `clip` only "found" as Windows' built-in + `clip.exe` and the Unix/WSL `clip`/`clip-paste` ladder simply does not apply there: + `config/clipboard.lua` wires an OS-appropriate provider instead (the `clip-windows` + provider — `clip.exe` copy + PowerShell paste — when `clip.exe` is present, else the OSC52 + fallback). `nvim/lua/gerrrt/health.lua` now detects native Windows (`has("win32")`, false + under WSL), records that the Unix probe is inapplicable, and defers to `:checkhealth + vim.provider` for the live backend rather than running the ladder or re-deriving the + provider itself. (`nvim/lua/gerrrt/health.lua`) +- **noice.nvim cmdline regex highlighting.** Added the `regex` Tree-sitter parser to + `ensure_installed` in `nvim/lua/gerrrt/plugins/nvim-treesitter.lua`. noice runs a + floating command line (`cmdline_popup`), which uses the `regex` parser to syntax- + highlight the pattern in `:s/…/` substitutions and searches; without it, `:checkhealth + noice` warned "`regex` parser is not installed. Highlighting of the cmdline for `regex` + might be broken." It installs on next launch via the existing `ensure_installed` diff. + ## [v4.5.0] - 2026-07-28 ### Added diff --git a/core/core.version b/core/core.version index a84947d..6016e8a 100644 --- a/core/core.version +++ b/core/core.version @@ -1 +1 @@ -4.5.0 +4.6.0 diff --git a/core/nvim/lazy-lock.json b/core/nvim/lazy-lock.json index 1986faa..3c8c8b2 100644 --- a/core/nvim/lazy-lock.json +++ b/core/nvim/lazy-lock.json @@ -31,7 +31,7 @@ "nvim-dap": { "branch": "master", "commit": "9e848e09a697ee95302a3ef2dd43fd6eb709e570" }, "nvim-dap-python": { "branch": "master", "commit": "1808458eba2b18f178f990e01376941a42c7f93b" }, "nvim-lint": { "branch": "master", "commit": "a219b2c9e5b4765e5c845aba119dad55806fcaf1" }, - "nvim-lspconfig": { "branch": "master", "commit": "7ab79bb1b57160a537f427ac8a6026102f12b701" }, + "nvim-lspconfig": { "branch": "master", "commit": "b7b920947f21339ee41fbb38c79d6445e12900aa" }, "nvim-tree.lua": { "branch": "master", "commit": "4213bd6eabac38b16dd6615002b6243b23cf3bf6" }, "nvim-treesitter": { "branch": "main", "commit": "61df84986b4b4ec469ee745a182e433d49f8c27e" }, "nvim-treesitter-context": { "branch": "master", "commit": "b311b30818951d01f7b4bf650521b868b3fece16" }, @@ -39,14 +39,14 @@ "nvim-ufo": { "branch": "main", "commit": "ab3eb124062422d276fae49e0dd63b3ad1062cfc" }, "nvim-web-devicons": { "branch": "master", "commit": "2ae6958df7ced50baac5035cec0c15799eedfbf7" }, "oil.nvim": { "branch": "master", "commit": "b73018b75affd13fa38e2fc94ef753b465f770d7" }, - "package-info.nvim": { "branch": "master", "commit": "24099acd58f8aab06795c92ddea59f5ba5cd6eeb" }, + "package-info.nvim": { "branch": "master", "commit": "9e40ba35e1f91df3686f4b4b6619dc2ef92561fa" }, "persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" }, "plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" }, "promise-async": { "branch": "main", "commit": "119e8961014c9bfaf1487bf3c2a393d254f337e2" }, - "rainbow-delimiters.nvim": { "branch": "master", "commit": "a798325b7f36acc62741d1029930a7b96d4dd4bf" }, + "rainbow-delimiters.nvim": { "branch": "master", "commit": "012f1480cd9a5fc99fce7678e0a536421a53fc46" }, "render-markdown.nvim": { "branch": "main", "commit": "f422cb5c6855f150e2ddcfaf44e7157b98b34f6a" }, "rustaceanvim": { "branch": "main", "commit": "f69c85a955b9a83d495e3dc8d43adb6364a0b765" }, - "schemastore.nvim": { "branch": "main", "commit": "3d2c297f7152208daf4a382ea7123f9e4ebf9da1" }, + "schemastore.nvim": { "branch": "main", "commit": "19e40cef03a380bfb578793a08222826118664dd" }, "todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" }, "tokyonight.nvim": { "branch": "main", "commit": "cdc07ac78467a233fd62c493de29a17e0cf2b2b6" }, "treesj": { "branch": "main", "commit": "79aedb401bbdc7e4202f7881eab5f6feb2105b0a" }, diff --git a/core/nvim/lua/gerrrt/health.lua b/core/nvim/lua/gerrrt/health.lua index bd62a9d..009b228 100644 --- a/core/nvim/lua/gerrrt/health.lua +++ b/core/nvim/lua/gerrrt/health.lua @@ -1,20 +1,41 @@ -- nvim/lua/gerrrt/health.lua -- ───────────────────────────────────────────────────────────────────────────── -- `:checkhealth gerrrt` — a precise, actionable report for Core's Neovim bits that --- otherwise fail with a generic message. The clipboard is the prime case: when no --- backend exists, `"+y` / `"+p` surface only Neovim's opaque "clipboard provider" --- error. Here we run the SAME `clip` / `clip-paste` ladder the provider uses and say --- exactly what's missing and how to fix it, instead of leaving the user guessing. +-- otherwise fail with a generic message. Two sections: +-- • clipboard — when no backend exists, `"+y` / `"+p` surface only Neovim's opaque +-- "clipboard provider" error. Here we run the SAME `clip` / `clip-paste` ladder the +-- provider uses and say exactly what's missing and how to fix it. +-- • LSP servers — the built-in `:checkhealth vim.lsp` only lists clients ATTACHED to the +-- current session, so from the dashboard it reads "No active clients" and tells you +-- nothing about whether your configured servers are installed. This section reports +-- every WANTED server (from gerrrt/servers) as installed/enabled/attached instead. -- -- This module is loaded ONLY by :checkhealth (Neovim discovers lua/**/health.lua) — -- it is never required at startup, so it adds nothing to load time. -- ───────────────────────────────────────────────────────────────────────────── local M = {} -function M.check() - local h = vim.health +local function check_clipboard(h) h.start("dotfiles-core: clipboard") + -- Native Windows (NOT WSL — there has("win32") is false, it's Linux) never runs Core's + -- bootstrap and has no clip/clip-paste scripts: dotfiles-Windows vendors only nvim/, so the + -- unified Unix/WSL provider simply does not apply here. Worse, `clip` spuriously resolves to + -- Windows' built-in clip.exe, so the generic ladder below reports "clip: found, clip-paste: + -- missing" and warns about a probe the host was never meant to run. clipboard.lua instead wires + -- an OS-appropriate provider on this host (the clip-windows provider — clip.exe copy + PowerShell + -- paste — when clip.exe is present, else the OSC52 fallback). We deliberately do NOT re-derive + -- which of those is live here: :checkhealth vim.provider is the single authority on the active + -- backend. So just record that the Unix probe is inapplicable and defer to it. + if vim.fn.has("win32") == 1 then + h.ok("native Windows — Core's Unix/WSL clip/clip-paste probe does not apply here") + h.info( + "clipboard.lua wires the host provider instead (clip-windows via clip.exe + PowerShell, " + .. "else OSC52). See :checkhealth vim.provider for the live clipboard backend." + ) + return + end + local have_clip = vim.fn.executable("clip") == 1 local have_paste = vim.fn.executable("clip-paste") == 1 @@ -55,4 +76,184 @@ function M.check() end end +local function check_lsp(h) + h.start("dotfiles-core: LSP servers") + + -- READ-ONLY by construction: we peek `package.loaded` rather than require()-ing the registry. + -- Requiring it here would REGISTER server configs and call vim.lsp.enable() as a side effect of + -- running a health check — and, on the dashboard, it would do so BEFORE nvim-lspconfig's + -- lsp/*.lua defaults are on the runtimepath (that plugin loads on `User FilePost`), caching an + -- incomplete config for the rest of the session. So we only report once a code file has loaded + -- the stack the normal way (correct order), and otherwise tell the user to open one. + local servers = package.loaded["gerrrt.servers"] + if type(servers) ~= "table" or type(servers.status) ~= "function" then + h.info("LSP stack not loaded yet — open any code file (that loads it in the right order), then re-run :checkhealth gerrrt") + return + end + + local offline = vim.g.dotfiles_offline + local attached, idle, pending, missing, broken = 0, 0, 0, 0, 0 + + for _, s in ipairs(servers.status()) do + if not s.registered then + -- Our servers/.lua override failed to load. The server may still run on nvim-lspconfig's + -- upstream defaults, but our deliberate local overrides are silently lost — flag it. + broken = broken + 1 + h.error(("%s — config override failed to load (running on lspconfig defaults, if any)"):format(s.name), { + "See :messages for the gerrrt.servers error.", + }) + elseif s.clients > 0 then + attached = attached + 1 + h.ok(("%s — attached (%d client%s)"):format(s.name, s.clients, s.clients == 1 and "" or "s")) + elseif s.enabled then + idle = idle + 1 + h.ok(("%s — enabled (no client attached right now)"):format(s.name)) + elseif s.available then + -- Binary is present but the enable pass never ran for it (installed after startup without + -- firing the Mason re-enable hook, e.g. `uv tool install`). It can't attach until enabled. + pending = pending + 1 + h.warn(("%s — binary present but not enabled yet"):format(s.name), { + "Restart Neovim (or re-run the Mason install) to enable it; it won't attach until then.", + }) + elseif offline then + -- DOTFILES_OFFLINE boxes intentionally omit tooling; a missing binary is expected, not a + -- defect — keep the section green (mirrors the startup-notify suppression in servers/init.lua). + missing = missing + 1 + h.info(("%s — binary not found (DOTFILES_OFFLINE: expected)"):format(s.name)) + else + missing = missing + 1 + h.warn(("%s — binary not found, server not enabled"):format(s.name), { + "Install via :Mason (except ruff/ty via `uv tool install`, rust via rustup).", + }) + end + end + + h.info( + ("%d attached · %d enabled-idle · %d pending-enable · %d missing · %d broken"):format( + attached, + idle, + pending, + missing, + broken + ) + .. "\n'attached' is a snapshot of THIS moment — servers attach per-filetype, so open a file of that" + .. " language to watch its server start." + ) +end + +local function sorted_keys(set) + local t = {} + for k in pairs(set) do + t[#t + 1] = k + end + table.sort(t) + return t +end + +local function check_formatters(h) + h.start("dotfiles-core: formatters (conform)") + + -- READ-ONLY: peek package.loaded, never require() (that would force-load conform from a health + -- check). list_all_formatters() is conform's own public API — the exact one :ConformInfo renders + -- — so there's no second formatter list here to drift from conform.lua. + local conform = package.loaded["conform"] + if type(conform) ~= "table" or type(conform.list_all_formatters) ~= "function" then + h.info("conform.nvim not loaded yet — save a file or run :ConformInfo, then re-run :checkhealth gerrrt") + return + end + + local list = conform.list_all_formatters() + table.sort(list, function(a, b) + return a.name < b.name + end) + + local offline = vim.g.dotfiles_offline + local installed, missing = 0, 0 + for _, f in ipairs(list) do + if f.available then + installed = installed + 1 + h.ok(("%s — installed"):format(f.name)) + else + missing = missing + 1 + local why = (f.available_msg and f.available_msg ~= "") and (" (" .. f.available_msg .. ")") or "" + if offline then + h.info(("%s — not found%s (DOTFILES_OFFLINE: expected)"):format(f.name, why)) + else + h.warn(("%s — binary not found%s"):format(f.name, why), { + "Install via :Mason; ruff via `uv tool install ruff`; forge_fmt/zigfmt/terraform_fmt need foundry/zig/terraform on PATH.", + }) + end + end + end + + h.info(("%d installed · %d missing"):format(installed, missing) + .. "\nconform silently skips a formatter whose binary is missing (that filetype just falls back to" + .. " LSP formatting, or none) — a miss here is degraded formatting, never an error.") +end + +local function check_linters(h) + h.start("dotfiles-core: linters (nvim-lint)") + + -- READ-ONLY: peek package.loaded, never require() (that would force-load nvim-lint, whose config + -- also runs an initial lint pass — a side effect a health check must not trigger). + local lint = package.loaded["lint"] + if type(lint) ~= "table" then + h.info("nvim-lint not loaded yet — open any code file, then re-run :checkhealth gerrrt") + return + end + + -- Union of every linter wired per-filetype, plus semgrep (attached via the SAST filetype set in + -- nvim-lint.lua, not linters_by_ft). We read the plugin's own registries, so the wanted set can't + -- drift from the config. Each linter's REAL binary is its builtin `.cmd` (e.g. golangcilint→ + -- "golangci-lint"), which is more accurate than guessing from the map key. + local set = { semgrep = true } + for _, names in pairs(lint.linters_by_ft or {}) do + for _, n in ipairs(names) do + set[n] = true + end + end + + local offline = vim.g.dotfiles_offline + local installed, missing, dynamic = 0, 0, 0 + for _, name in ipairs(sorted_keys(set)) do + local okd, def = pcall(function() + return lint.linters[name] + end) + local cmd = okd and type(def) == "table" and def.cmd or nil + + if type(cmd) == "function" then + -- A few builtins compute cmd at runtime; probing it here would mean calling it out of context. + dynamic = dynamic + 1 + h.info(("%s — dynamic command, not probed"):format(name)) + elseif type(cmd) == "string" then + if vim.fn.executable(cmd) == 1 then + installed = installed + 1 + h.ok(("%s — installed (%s)"):format(name, cmd)) + elseif offline then + missing = missing + 1 + h.info(("%s — %s not found (DOTFILES_OFFLINE: expected)"):format(name, cmd)) + else + missing = missing + 1 + h.warn(("%s — `%s` not found on PATH"):format(name, cmd), { + "Install via :Mason; some (checkstyle/phpstan/rubocop/…) also need their language runtime present.", + }) + end + else + h.info(("%s — could not resolve its command"):format(name)) + end + end + + h.info(("%d installed · %d missing · %d dynamic"):format(installed, missing, dynamic) + .. "\nseveral linters are config-gated — installed here, but they only run in a project that ships" + .. " their config (see plugins/nvim-lint.lua). Python lint is ruff's LSP, not listed here.") +end + +function M.check() + local h = vim.health + check_clipboard(h) + check_lsp(h) + check_formatters(h) + check_linters(h) +end + return M diff --git a/core/nvim/lua/gerrrt/plugins/nvim-treesitter.lua b/core/nvim/lua/gerrrt/plugins/nvim-treesitter.lua index fcc89d3..7f864d8 100644 --- a/core/nvim/lua/gerrrt/plugins/nvim-treesitter.lua +++ b/core/nvim/lua/gerrrt/plugins/nvim-treesitter.lua @@ -36,6 +36,7 @@ return { "diff", -- diffview.nvim + git diff buffers "gitcommit", -- commit message buffers (you write these via fugitive/lazygit) "vimdoc", -- :help and plugin docs + "regex", -- noice.nvim floating cmdline: highlights the regex in :s/… and searches -- ── added-language parsers (LSP + formatter + linter wired in the other manifests) ── "ruby", "java", diff --git a/core/nvim/lua/gerrrt/servers/init.lua b/core/nvim/lua/gerrrt/servers/init.lua index d36c3f6..77bd704 100644 --- a/core/nvim/lua/gerrrt/servers/init.lua +++ b/core/nvim/lua/gerrrt/servers/init.lua @@ -67,6 +67,12 @@ local servers = { "terraformls", -- Terraform/HCL } +-- Which servers/.lua LOCAL modules actually loaded. Tracked explicitly because +-- `vim.lsp.config[name]` is NOT a reliable "did our override load" signal: once nvim-lspconfig is +-- on the runtimepath it resolves an UPSTREAM default table for a name even when our local module +-- errored — so a failed leaf would masquerade as configured. status() consults this instead. +local registered = {} + -- Register each server's config. pcall'd per module so one broken or missing server file degrades -- to "that one server is unconfigured" instead of taking the whole LSP stack — and the editor — -- down with it. @@ -74,6 +80,7 @@ for _, name in ipairs(servers) do local okm, cfg = pcall(require, "gerrrt.servers." .. name) if okm and type(cfg) == "table" then vim.lsp.config(name, cfg) + registered[name] = true else vim.schedule(function() vim.notify( @@ -153,6 +160,13 @@ end local M = {} +-- Names we have actually called vim.lsp.enable() on. This is the AUTHORITATIVE "is it wired to +-- spawn" state, distinct from "binary is present": a binary that appears AFTER this pass (e.g. +-- `uv tool install ruff`, which doesn't fire the Mason re-enable hook) is available but NOT enabled +-- until the next enable pass or a restart. Accumulated (enable is idempotent) so status() can tell +-- the two apart instead of equating them. +local enabled_set = {} + -- Enable every WANTED server whose binary is currently present; return the list still missing. -- Safe to call REPEATEDLY: vim.lsp.enable is idempotent and, on 0.11+, attaches a newly-enabled -- server to already-open matching buffers. That is what lets the post-install hook in @@ -170,9 +184,38 @@ function M.enable_available() end end vim.lsp.enable(to_enable) + for _, name in ipairs(to_enable) do + enabled_set[name] = true + end return missing end +-- Per-server readiness snapshot for `:checkhealth gerrrt` (see gerrrt/health.lua). Reuses the +-- WANTED `servers` list and the SAME `binary_available()` decision the enable pass uses, so the +-- health report can never disagree with what actually got enabled — no second server list to drift. +-- Reports FOUR independent facts per server, deliberately kept separate so the report is honest: +-- • registered : our servers/.lua override loaded (tracked above — NOT inferred from +-- vim.lsp.config[name], which also resolves upstream lspconfig defaults) +-- • enabled : we actually called vim.lsp.enable() on it (from enabled_set — the real "wired +-- to spawn" state, which "binary present" alone does not imply) +-- • available : its binary is on PATH (or a project-local node_modules/.bin) +-- • clients : clients attached RIGHT NOW (a point-in-time snapshot; servers attach per-filetype) +-- This function is READ-ONLY (no config registration, no enable) so a health check that calls it +-- cannot mutate the session — see the package.loaded guard in gerrrt/health.lua. +function M.status() + local out = {} + for _, name in ipairs(servers) do + out[#out + 1] = { + name = name, + registered = registered[name] == true, + enabled = enabled_set[name] == true, + available = binary_available(name), + clients = #vim.lsp.get_clients({ name = name }), + } + end + return out +end + -- Initial pass at load. Surface (once) which servers were skipped so a missing binary is -- discoverable, not silent. Suppressed on engagement/offline boxes (DOTFILES_OFFLINE=1, see -- config/globals.lua), where tools are intentionally not installed and the warning would be noise. diff --git a/core/scripts/test-core.sh b/core/scripts/test-core.sh index 053c474..b863150 100755 --- a/core/scripts/test-core.sh +++ b/core/scripts/test-core.sh @@ -378,13 +378,82 @@ LUA -c 'checkhealth gerrrt' \ -c 'execute "write!" fnameescape($CORE_CK_REP)' \ -c 'qa!' >/dev/null 2>"$ckerr" - if grep -q "dotfiles-core" "$ckrep" 2>/dev/null; then - pass "checkhealth gerrrt ran (health report rendered)" + # Assert ALL FOUR sections rendered — each helper's h.start() runs before any early return, so a + # header proves that helper ran without throwing (a bad vim.health call in any of them would drop + # its header). The LSP/formatters/linters sections show their "not loaded — open a file" info here + # (hermetic: no plugins, no file opened), which is the correct side-effect-free behavior. + if grep -q "dotfiles-core: clipboard" "$ckrep" 2>/dev/null \ + && grep -q "dotfiles-core: LSP servers" "$ckrep" 2>/dev/null \ + && grep -q "dotfiles-core: formatters" "$ckrep" 2>/dev/null \ + && grep -q "dotfiles-core: linters" "$ckrep" 2>/dev/null; then + pass "checkhealth gerrrt ran (clipboard + LSP + formatters + linters sections rendered)" else - fail "checkhealth gerrrt did not render its section (check() missing or threw):" + fail "checkhealth gerrrt did not render all sections (a check() helper missing or threw):" [[ -s "$ckrep" ]] && sed 's/^/ /' "$ckrep" >&2 [[ -s "$ckerr" ]] && sed 's/^/ /' "$ckerr" >&2 fi + + # Native-Windows clipboard branch (headless, has("win32") stubbed). The Neovim CI matrix is + # Ubuntu/macOS, so check_clipboard's has("win32") early return never runs under the gate — a + # regression in it (running the Unix clip/clip-paste probe on the host, or a bad vim.health + # call) would otherwise pass the full audit. This stubs vim.fn.has→win32 and CAPTURES the + # vim.health calls (rather than rendering a report), asserting the clipboard section reports OK + # (never warn/error) AND that the branch skips the executable()/system() probe entirely. The + # whole body is pcall-guarded so even a bad stub cquits (fails) instead of hanging on a prompt. + winprobe="$SANDBOX/nvim-health-win32.lua" + cat >"$winprobe" <<'LUA' +local function run() + local calls = {} + vim.health = { + start = function(s) calls[#calls + 1] = { "start", s } end, + ok = function(s) calls[#calls + 1] = { "ok", s } end, + warn = function(s) calls[#calls + 1] = { "warn", s } end, + info = function(s) calls[#calls + 1] = { "info", s } end, + error = function(s) calls[#calls + 1] = { "error", s } end, + } + -- Force the native-Windows branch; trip a flag if the Unix probe is ever run. + local probed = false + vim.fn.has = function(f) return (f == "win32") and 1 or 0 end + vim.fn.executable = function(_) probed = true; return 0 end + vim.fn.system = function(_) probed = true; return "" end + assert(vim.fn.has("win32") == 1, "stub failed: vim.fn.has('win32') did not return 1") + + local M = dofile(vim.env.CORE_HEALTH_LUA) + assert(type(M) == "table" and type(M.check) == "function", "health.lua did not return a module with check()") + M.check() + + -- The clipboard section's calls run from its start() up to the next start(). + local in_clip, saw_start, saw_ok = false, false, false + for _, c in ipairs(calls) do + local kind, text = c[1], c[2] or "" + if kind == "start" then + in_clip = text:find("dotfiles%-core: clipboard", 1) ~= nil + if in_clip then saw_start = true end + elseif in_clip then + assert(kind ~= "warn" and kind ~= "error", "clipboard section emitted a " .. kind .. " on native Windows: " .. text) + if kind == "ok" then saw_ok = true end + end + end + assert(saw_start, "clipboard section did not run (no start)") + assert(saw_ok, "clipboard section did not report OK on native Windows") + assert(not probed, "native-Windows branch called executable()/system() — it must skip the Unix probe") +end + +local ok, err = pcall(run) +if not ok then + io.stderr:write(tostring(err) .. "\n") + vim.cmd("cquit 1") +end +vim.cmd("quitall!") +LUA + win_err="$SANDBOX/nvim-health-win32.err" + if CORE_HEALTH_LUA="$HERE/nvim/lua/gerrrt/health.lua" \ + nvim --headless -u "$winprobe" -i NONE -n +qa >/dev/null 2>"$win_err"; then + pass "checkhealth gerrrt: native-Windows clipboard branch skips the Unix probe (has('win32') stubbed)" + else + fail "checkhealth gerrrt native-Windows clipboard branch probe failed:" + [[ -s "$win_err" ]] && sed 's/^/ /' "$win_err" >&2 + fi else skip "nvim config load (nvim not installed — runs in CI)" fi @@ -609,11 +678,30 @@ vim.lsp.enable = function(names) for _, n in ipairs(type(names) == "table" and names or { names }) do enabled[#enabled + 1] = n end end -local ok, err = pcall(require, "gerrrt.servers") +local ok, mod = pcall(require, "gerrrt.servers") + +-- Exercise the read-only status() export (feeds :checkhealth gerrrt) WHILE the stubs are still +-- active, so the binary gate stays pinned and get_clients() is the real (empty, headless) surface. +-- Asserts the states health.lua renders: a broken override reports registered=false (NOT masked by +-- an upstream default); a good server reports registered+enabled+available; clients is a count. +local st_ok, st_gopls_reg, st_luals_reg, st_luals_en, st_luals_av, st_luals_cl = false +if ok and type(mod) == "table" and type(mod.status) == "function" then + local oks, rows = pcall(mod.status) + if oks and type(rows) == "table" then + st_ok = true + for _, s in ipairs(rows) do + if s.name == "gopls" then st_gopls_reg = s.registered end + if s.name == "lua_ls" then + st_luals_reg, st_luals_en, st_luals_av, st_luals_cl = s.registered, s.enabled, s.available, s.clients + end + end + end +end + vim.lsp.config, vim.lsp.enable = real_config, real_enable vim.fn.executable = real_executable if not ok then - io.stderr:write("require gerrrt.servers → " .. tostring(err) .. "\n") + io.stderr:write("require gerrrt.servers → " .. tostring(mod) .. "\n") vim.cmd("cquit 1") end @@ -628,13 +716,20 @@ for name, cfg in pairs(registered) do end -- The broken module registered nothing, so its cmd resolves to nil and it bypasses the gate too. if registered["gopls"] == nil then n_nocmd = n_nocmd + 1 end -io.stdout:write(("wildcard=%s caps=%s servers=%d broken_registered=%s enabled=%d nocmd=%d\n"):format( +io.stdout:write(("wildcard=%s caps=%s servers=%d broken_registered=%s enabled=%d nocmd=%d"):format( tostring(registered["*"] ~= nil), tostring(registered["*"] and registered["*"].capabilities and registered["*"].capabilities.STUB_CAPS or false), n_servers, tostring(registered["gopls"] ~= nil), #enabled, n_nocmd)) +io.stdout:write((" status_ok=%s st_gopls_reg=%s st_luals_reg=%s st_luals_en=%s st_luals_av=%s st_luals_cl=%s\n"):format( + tostring(st_ok), + tostring(st_gopls_reg), + tostring(st_luals_reg), + tostring(st_luals_en), + tostring(st_luals_av), + tostring(st_luals_cl))) vim.cmd("qa!") LUA reg_err="$SANDBOX/nvim-registry.err" @@ -656,6 +751,21 @@ LUA [[ -s "$reg_err" ]] && sed 's/^/ /' "$reg_err" >&2 fi + # status() export (feeds :checkhealth gerrrt). The broken gopls override must report + # registered=false — the whole point of tracking it separately from vim.lsp.config[name], which + # would resolve an upstream default and hide the failure. A good server (lua_ls) must report + # registered + enabled + available, and clients must be a number (0 in this headless probe). + if [[ "$(_reg_field "$reg_a" status_ok)" == "true" \ + && "$(_reg_field "$reg_a" st_gopls_reg)" == "false" \ + && "$(_reg_field "$reg_a" st_luals_reg)" == "true" \ + && "$(_reg_field "$reg_a" st_luals_en)" == "true" \ + && "$(_reg_field "$reg_a" st_luals_av)" == "true" \ + && "$(_reg_field "$reg_a" st_luals_cl)" == "0" ]]; then + pass "LSP registry: status() separates registered/enabled/available (broken override → registered=false)" + else + fail "LSP registry status() contract broken — got '$reg_a' (expected status_ok, gopls reg=false, lua_ls reg/en/av=true, clients=0)" + fi + # B. no binary present. Only configs that bypass the gate by design may remain — those with no # literal `cmd` list, which binary_available() deliberately does not second-guess. Asserted # against the probe's own count rather than a magic number, so adding a server can't silently diff --git a/core/scripts/tool-versions.env b/core/scripts/tool-versions.env index a7dbea9..2d31f0d 100644 --- a/core/scripts/tool-versions.env +++ b/core/scripts/tool-versions.env @@ -20,10 +20,10 @@ # Plain KEY=VALUE, no spaces around `=`, no `v` prefix — so this both `source`s in # bash AND appends straight to $GITHUB_ENV. # ────────────────────────────────────────────────────────────────────────────── -SHELLCHECK_VERSION=0.10.0 +SHELLCHECK_VERSION=0.11.0 LUACHECK_VERSION=1.2.0 NVIM_VERSION=0.12.3 -MARKDOWNLINT_VERSION=0.22.1 +MARKDOWNLINT_VERSION=0.23.2 ACTIONLINT_VERSION=1.7.8 GITLEAKS_VERSION=8.30.1 # shfmt is pinned here for the setup-core-tools action's OS-repo consumers (MacBook @@ -31,7 +31,7 @@ GITLEAKS_VERSION=8.30.1 # shfmt — this pin exists so the shared action has one verified shfmt to install. SHFMT_VERSION=3.8.0 PRECOMMIT_VERSION=4.0.1 -PRECOMMIT_HOOKS_VERSION=5.0.0 +PRECOMMIT_HOOKS_VERSION=6.0.0 # Claude Code CLI for the scheduled maintenance bots (.github/workflows/claude-routines.yml). # Pinned like every other tool here (the workflow installs this exact version); bump deliberately. CLAUDE_CODE_VERSION=2.1.185 @@ -43,7 +43,7 @@ CLAUDE_CODE_VERSION=2.1.185 # `scripts/update-tool-checksums.sh` recomputes them and the audit fails any pinned # version that lacks a 64-hex hash. (luacheck = luarocks build, markdownlint/ # pre-commit/claude = npm/pip/registry installs — not raw downloads, so no hash.) -SHELLCHECK_SHA256=6c881ab0698e4e6ea235245f22832860544f17ba386442fe7e9d629f8cbedf87 +SHELLCHECK_SHA256=8c3be12b05d5c177a04c29e3c78ce89ac86f1595681cab149b65b97c4e227198 ACTIONLINT_SHA256=be92c2652ab7b6d08425428797ceabeb16e31a781c07bc388456b4e592f3e36a GITLEAKS_SHA256=551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb NVIM_SHA256=c441b547142860bf01bcce39e36cbed185c41112813e15443b16e5237750724d diff --git a/core/zsh/45-plugins.zsh b/core/zsh/45-plugins.zsh index 77c92ba..3ba352e 100644 --- a/core/zsh/45-plugins.zsh +++ b/core/zsh/45-plugins.zsh @@ -48,7 +48,7 @@ typeset -gA ZPLUGIN_PINS=( zsh-users/zsh-syntax-highlighting 1d85c692615a25fe2293bdd44b34c217d5d2bf04 Aloxaf/fzf-tab 24105b15714bfec37989ed5c5b6e60f572253019 MichaelAquilina/zsh-you-should-use 5f3d129864ee4505043d88c3486224f1d75b692e - olets/zsh-transient-prompt ba98fe847ffb31c5529441e14c5a228a74903a75 + olets/zsh-transient-prompt bdd5917f38db4b4995134e8fb0251ce01cd9a068 ) # Show first-run install progress with Core's spinner WHEN 05-ui.zsh is loaded; fall