Skip to content

Update checker flags every commit; filter out noise files (README, CI, LICENSE) to reduce false positives #338

@bguidolim

Description

@bguidolim

Summary

UpdateChecker.checkPackUpdates() (Sources/mcs/Core/UpdateChecker.swift:173) compares the remote HEAD SHA (via git ls-remote) against the locally recorded entry.commitSHA. Any difference surfaces as "pack update available" — regardless of what actually changed.

In practice, upstream pack repos accrue commits that have zero effect on what mcs sync installs: README tweaks, LICENSE updates, CHANGELOG edits, CI workflow changes, .github/ config, etc. Users get update notifications (including the STOP. Before doing ANY work… hook banner) for commits that would produce an identical install. This trains users to ignore the notification, which is the worst outcome for an update checker.

This is also connected to a second annoyance: mcs pack validate emits is not referenced by any component or template warnings (PackHeuristics.checkUnreferencedFiles, PackHeuristics.swift:108) for exactly these same files. Today pack authors have no way to quiet those warnings short of deleting the files, which they legitimately want to keep in the repo.

Both problems share a root cause: the engine has no concept of "files that exist in the pack but aren't material to installation".

Proposed solution

Apply a deny-list filter after the SHA-change trigger, reusing heuristics that already exist in the codebase.

Phase 1 — Engine-side filter

When git ls-remote reports a new SHA, do a shallow git fetch --depth 1 + git diff --name-only localSHA..FETCH_HEAD against the existing pack checkout, then filter the changed-path list against:

  • PackHeuristics.ignoredDirectories (.git, .github, .gitlab, .vscode, node_modules, __pycache__, .build)
  • PackHeuristics.infrastructureFiles minus techpack.yaml (see invariant below) — README.md, README, LICENSE, LICENSE.md, CHANGELOG.md, CONTRIBUTING.md, .gitignore, .editorconfig, package.json, package-lock.json, requirements.txt, Makefile, Dockerfile, .dockerignore

If the filtered set is empty → suppress the notification AND advance entry.commitSHA to the new SHA so the same no-op commits don't re-trigger on every cooldown window.

If the filtered set is non-empty → show the notification as today (optionally include the material paths in the message — "Dev Essentials updated: hooks/session-start.sh").

Why deny-list, not allow-list: we considered intersecting changes with PackHeuristics.referencedPaths(from:) (PackHeuristics.swift:87), which enumerates copyPackFile.source, settingsFile.source, template.contentFile, and configureProject.script. Rejected: that set does not include shellCommand script paths or a configure script's transitive dependencies, so an allow-list would produce false negatives (hiding real updates). Deny-list treats unknown files as material — safe default.

Phase 1a — Hard invariant: techpack.yaml is always material

A manifest edit can change the install surface entirely: new copyPackFile sources, different shell: actions, added/removed hooks, different MCP server commands. Silently suppressing a manifest-only commit is a supply-chain attack vector — a pack author (malicious or compromised) could swap hook scripts or MCP commands without the user ever seeing a notification.

Therefore:

  • techpack.yaml is unconditionally material. Any commit that touches it surfaces the update notification, regardless of ignore lists, built-in or author-supplied.
  • The update-check deny-list uses infrastructureFiles \ {techpack.yaml}, NOT the raw set. Today PackHeuristics.infrastructureFiles at PackHeuristics.swift:171 contains Constants.ExternalPacks.manifestFilename because it is intentional infrastructure for the checkRootLevelContentFiles use case. Reusing the set as-is for update-check filtering would silently suppress manifest changes. The two call sites need distinct effective sets — worth naming explicitly in code so a future reader doesn't "deduplicate" them.

Phase 2 — Pack-author ignore: field

Add an optional ignore: list to techpack.yaml:

identifier: my-pack
displayName: My Pack
ignore:
  - docs/
  - examples/
  - diagrams/*.png

Semantics:

  • Entries merge with the built-in ignore lists (authors extend, they don't replace)
  • Used by both UpdateChecker (for the filter above) AND PackHeuristics.checkUnreferencedFiles (to silence the validation warning)
  • Glob support via fnmatch to handle docs/*.md-style patterns

Phase 2a — Validation of ignore: entries (belt-and-suspenders)

The ignore: list cannot contain anything the engine relies on. Enforce at two layers:

  1. mcs pack validate rejects as an error (publish-time guard) — authors catch mistakes before shipping. Rejection rules:
    • techpack.yaml is forbidden (manifest is always tracked).
    • Any path in PackHeuristics.referencedPaths(from: manifest) is forbidden — you cannot silence a file the manifest claims to use. Error shape:
      ignore: entry 'hooks/session-start.sh' is referenced by component 'session-hook'.
      Remove it from ignore: or remove the component.
      
  2. Runtime loader strips forbidden entries silently with a warn log (user-safety guard) — if a malformed manifest somehow reaches a user machine (older mcs version, hand-edited file), their install doesn't break. Worst case: the author's invalid suppression silently doesn't apply; they figure it out when notifications keep firing.

Why both: blocking alone risks breaking working installs when authors publish bad manifests; stripping alone hides the error from authors who wonder why their ignore entry "doesn't work". Together: loud feedback at publish time, safe fallback at runtime.

Phase 3 — Remediation hint in mcs pack validate

When checkUnreferencedFiles emits warnings, append a hint:

Add these paths to the ignore: field in techpack.yaml if they are intentionally not installed (docs, examples, assets).

This closes the loop: users discover the field naturally through validation, and they're motivated to add entries because it quiets both the validation warning AND the spurious update notifications downstream.

Cost / tradeoffs

  • One shallow git fetch per genuinely-changed pack per cooldown window (24h). For the typical case where 0–1 packs have new commits at a time, this is negligible vs. the current ls-remote-only path.
  • Network-failure fallback: if the deep check fails (offline, rate-limited), surface the notification anyway. The filter can only suppress, never manufacture silence on error — preserves the "never hide a real update" invariant.
  • Cache invalidation: UpdateChecker.loadCache() (UpdateChecker.swift:122) already invalidates on CLI version change; may also want to invalidate when a pack's manifest ignore: list changes (hash the list, store in CachedResult).

Scope

  • Sources/mcs/Core/UpdateChecker.swift — add post-SHA-change filter and baseline advancement
  • Sources/mcs/ExternalPack/ExternalPackManifest.swift — add ignore: [String]? field; validate rejects forbidden entries
  • Sources/mcs/ExternalPack/PackHeuristics.swift — respect manifest ignore: in checkUnreferencedFiles, add remediation hint, expose infrastructureFilesForUpdateCheck or equivalent so the \ {techpack.yaml} distinction is explicit
  • Sources/mcs/Commands/ValidatePackCommand.swift — wire through the new hint

Test coverage

  • Unit: filter-only-noise commits → suppressed + baseline advances
  • Unit: commit touching techpack.yaml only → always surfaced, never suppressed, even if author put techpack.yaml in ignore:
  • Unit: mixed commits (README + hook script) → surfaced as "hook script changed"
  • Unit: manifest ignore: entries extend built-in sets (union, not replace)
  • Unit: glob matching for ignore: patterns
  • Unit: mcs pack validate rejects ignore: containing techpack.yaml or a referenced path, with actionable error
  • Unit: runtime loader strips forbidden ignore: entries with a warn log
  • Integration: mcs pack validate on a pack with ignore: entries does not emit unreferenced-file warnings for those paths

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions