fix(cli): reconcile scoped-closure and reduced-lockfile freshness checks

Merge follow-up on top of the salvaged commits: fold the npm>=10 reduced
hidden-lockfile comparison (intersection of non-null fields) and the
annotation-field exclusions into a single entries_differ() helper, keeping
the workspace-closure scoping intact.
This commit is contained in:
Teknium
2026-08-25 04:57:56 -07:00
parent def7bdc638
commit 76d8f876f3
+25 -27
View File
@@ -2246,19 +2246,28 @@ def _tui_need_npm_install(root: Path) -> bool:
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
return lock.stat().st_mtime > marker.stat().st_mtime
def comparable(pkg: dict) -> dict:
# npm >= 10/11 writes a reduced hidden lockfile that omits declarative
# fields (`version`, `dependencies`, `dev`, `engines`, `bin`, ...) or
# stores them as null. Comparing every key field-by-field then flags
# nearly every installed package as "changed". Compare only the keys
# both sides actually record with a non-null value (`resolved`,
# `integrity`, ...): a genuinely stale install (root lockfile bumped
# while node_modules is behind) still differs on those keys, while the
# reduced-lockfile field omissions no longer false-positive.
def entries_differ(pkg: dict, installed_pkg: dict) -> bool:
# Only compare keys present in *both* lockfiles with non-null values.
# npm's hidden .package-lock.json intentionally omits many metadata
# fields the root lock records (version, dependencies, license,
# engines, bin, ...), and npm >= 10/11 writes a further *reduced*
# hidden lockfile that stores some of them as null. Missing- or
# null-on-one-side is a normal npm artefact, not a real skew. The
# authoritative fields "resolved" and "integrity" are present in both
# locks for installed packages, so a genuinely stale install (root
# lockfile bumped while node_modules is behind) still differs on them.
a = {k: v for k, v in pkg.items() if k not in _NPM_LOCK_RUNTIME_KEYS}
b = {k: v for k, v in installed_pkg.items() if k not in _NPM_LOCK_RUNTIME_KEYS}
common = a.keys() & b.keys()
return {k: a[k] for k in common if a[k] is not None and b[k] is not None}
b = {
k: v
for k, v in installed_pkg.items()
if k not in _NPM_LOCK_RUNTIME_KEYS
}
for k in a.keys() & b.keys():
if a[k] is None or b[k] is None:
continue
if a[k] != b[k]:
return True
return False
# In a shared workspace checkout the launch install is scoped to the ui-tui
# workspace (plus its child packages/* workspaces on Termux), so only that
@@ -2295,21 +2304,10 @@ def _tui_need_npm_install(root: Path) -> bool:
continue
return True
if isinstance(installed[name], dict):
w = comparable(pkg)
i = comparable(installed[name])
# Only compare keys present in *both* lockfiles. npm's hidden
# .package-lock.json intentionally omits many metadata fields
# (version, dependencies, license, engines, dev, optional, etc.)
# that the root lock records. Missing-on-one-side is a normal
# npm artefact, not a real skew. The authoritative fields
# "resolved" and "integrity" are always present for installed
# packages, so a real version/dependency change will still
# be caught through them.
common = set(w) & set(i)
for k in common:
if w[k] != i[k]:
return True
if isinstance(installed[name], dict) and entries_differ(
pkg, installed[name]
):
return True
return False