From 0878025130124562a7b4c5c9a699a57599b5b8b2 Mon Sep 17 00:00:00 2001 From: mia106dev Date: Wed, 8 Jul 2026 18:44:58 +0900 Subject: [PATCH 1/2] fix(install): exclude self-entry from dry-run orphan preview `apm install --dry-run` over-reported the project's own includes:auto self-managed files under "Files that would be removed". LockFile.from_yaml synthesizes a virtual _SELF_KEY (".") dependency from local_deployed_files, and detect_orphans flagged every dependency key absent from the manifest- derived intended set as orphaned -- including "." which can never appear there. The real install path already guards _SELF_KEY on replay/cleanup and never removes these files, so this was a dry-run-preview-only false alarm. Skip _SELF_KEY in detect_orphans. Add unit tests covering both the stub and the real LockFile.from_yaml synthesis path. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 7 +++++ src/apm_cli/drift.py | 9 ++++++ tests/unit/test_drift_detection.py | 49 ++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 688e289ac..9a93cb596 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- `apm install --dry-run` no longer lists the project's own `includes: auto` + self-managed files under "Files that would be removed"; the orphan preview + now excludes the synthesized lockfile self-entry, matching the real install + which never removes them. — by @mia106dev (#XXXX) + ## [0.24.0] - 2026-07-05 ### Added diff --git a/src/apm_cli/drift.py b/src/apm_cli/drift.py index cc8462598..dc239877e 100644 --- a/src/apm_cli/drift.py +++ b/src/apm_cli/drift.py @@ -202,10 +202,19 @@ def detect_orphans( are no longer in the manifest. The caller is responsible for actually removing the files. """ + from apm_cli.deps.lockfile import _SELF_KEY + orphaned: builtins.set = builtins.set() if only_packages or not existing_lockfile: return orphaned for dep_key, dep in existing_lockfile.dependencies.items(): + # The synthesized self-entry (key ".") holds the project's own + # includes:auto content, which the real install always re-deploys + # (never removes). It can never appear in the manifest-derived + # intended set, so skip it to avoid over-counting self-includes as + # removals in the dry-run preview. + if dep_key == _SELF_KEY: + continue if dep_key not in intended_dep_keys: orphaned.update(dep.deployed_files) return orphaned diff --git a/tests/unit/test_drift_detection.py b/tests/unit/test_drift_detection.py index 4a8dfe02b..3fd81dafb 100644 --- a/tests/unit/test_drift_detection.py +++ b/tests/unit/test_drift_detection.py @@ -214,6 +214,55 @@ def test_none_only_packages_treated_as_empty(self): result = detect_orphans(lf, set(), only_packages=None) self.assertEqual(result, {"a.md"}) + def test_self_entry_never_orphaned(self): + """The synthesized self-entry (key ".") must never be reported as orphan. + + from_yaml() injects a virtual ``_SELF_KEY`` dependency carrying the + project's own includes:auto content. That key can never appear in the + manifest-derived intended set, and the real install re-deploys it + (drift.py replay guards ``local_path == _SELF_KEY``). Reporting it here + over-counts self-includes as removals in the dry-run preview. + """ + from apm_cli.deps.lockfile import _SELF_KEY + + self_dep = _LockedDep( + repo_url="", + source="local", + local_path=_SELF_KEY, + deployed_files=["self_a.md", "self_b.md"], + ) + orphan = _LockedDep(repo_url="owner/gone", deployed_files=["gone.md"]) + lf = _LockFile(dependencies={_SELF_KEY: self_dep, "owner/gone": orphan}) + result = detect_orphans(lf, set(), only_packages=[]) + # Only the genuinely dropped package is an orphan; self-includes are not. + self.assertEqual(result, {"gone.md"}) + + def test_self_include_survives_real_from_yaml_roundtrip(self): + """End-to-end guard using the real LockFile.from_yaml synthesis. + + The stub-based test above could drift from how from_yaml actually + synthesizes the self-entry. This exercises the real parse -> detect + path so a change to either side (self-key value, synthesis shape) + that reintroduces the dry-run false alarm fails here. from_yaml is a + pure string parse -- no I/O. + """ + from apm_cli.deps.lockfile import LockFile + + lock = LockFile.from_yaml( + "lockfile_version: '2'\n" + "generated_at: ''\n" + "dependencies:\n" + " - repo_url: owner/kept\n" + " source: git\n" + " deployed_files: ['.apm/x.md']\n" + "local_deployed_files:\n" + " - .claude/skills/self/SKILL.md\n" + ) + # Manifest now empty -> only the real dropped package is an orphan; + # the synthesized self-entry's includes are never reported. + result = detect_orphans(lock, set(), only_packages=[]) + self.assertEqual(result, {".apm/x.md"}) + # --------------------------------------------------------------------------- # detect_config_drift From 4c8070cdb16655389048ccc5f3abb7a697f54f13 Mon Sep 17 00:00:00 2001 From: mia106dev Date: Wed, 8 Jul 2026 19:03:44 +0900 Subject: [PATCH 2/2] docs(changelog): fill PR number for dry-run self-entry orphan fix Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a93cb596..0d4f7b9f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `apm install --dry-run` no longer lists the project's own `includes: auto` self-managed files under "Files that would be removed"; the orphan preview now excludes the synthesized lockfile self-entry, matching the real install - which never removes them. — by @mia106dev (#XXXX) + which never removes them. — by @mia106dev (#2069) ## [0.24.0] - 2026-07-05