Don't flag parent dirs of subdirectory packages as orphaned#1052
Don't flag parent dirs of subdirectory packages as orphaned#1052danielmeppiel merged 28 commits intomicrosoft:mainfrom
Conversation
When a dependency uses dict-form (git: + path:) pointing at a nested subdirectory (e.g., .apm/skills/skill-name), the intermediate owner/repo/ directory contains .apm/ and is detected by _scan_installed_packages. The orphan check now recognizes these directories as ancestors of expected install paths rather than orphaned packages. The fix is applied consistently across all three orphan-detection sites: compile (_helpers.py), prune, and deps list. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…rphaned Ensures the ancestor-check fix does not regress orphan detection for standard owner/repo shorthand dependencies (the most common case). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Exercises _resolve_scope_deps to ensure owner/repo is not flagged as orphaned when the declared dependency points at a nested subdirectory package beneath it. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Fixes false-positive orphan detection when dict-form subdirectory dependencies create intermediate parent directories that were previously flagged as orphaned.
Changes:
- Add an “ancestor of expected install path” check to orphan detection in
apm prune,apm deps list, and_check_orphaned_packages. - Add unit tests covering subdirectory dependency parent directory handling and regression cases.
- Document the fix in the changelog.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_deps_list_tree_info.py | Adds a CLI-level regression test ensuring deps list output doesn’t report the parent directory as orphaned. |
| tests/unit/test_command_helpers.py | Adds unit tests validating _check_orphaned_packages behavior for subdirectory ancestors and real orphans. |
| src/apm_cli/commands/prune.py | Updates prune orphan filtering to ignore directories that are ancestors of expected install paths. |
| src/apm_cli/commands/deps/cli.py | Updates deps list orphan detection to ignore ancestors of declared subdirectory install paths. |
| src/apm_cli/commands/_helpers.py | Updates _check_orphaned_packages to ignore installed paths that are ancestors of expected install paths. |
| CHANGELOG.md | Notes the bug fix for false orphan detection of parent directories. |
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md # src/apm_cli/commands/_helpers.py # src/apm_cli/commands/prune.py
Match the specific 'orphaned package(s) found' header rather than the generic 'orphan' substring, per Copilot review nit. This avoids a future false-positive match if output ever contains 'orphan' in a non-error context. Other Copilot comments on the PR were resolved earlier (centralized ancestor expansion via _expand_with_ancestors, materialized iterables, import wrapping, ASCII header). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel Verdict: REJECT
Required before merge (1 item)
Nits (9 items, skip if you want)
CEO arbitrationThe panel reached consensus on the core correctness story: this PR fixes a genuine false-positive in orphan detection and is a net improvement. Four of five active panelists produced only nits. The single blocking finding comes from devx-ux-expert and it is legitimate: The python-architect and cli-logging-expert nits are housekeeping that should accompany any rework: tighten the return type to The CHANGELOG entry should be rewritten before merge. The current text is accurate but written for maintainers. The user-facing framing should open with the symptom and close with the outcome. This matters: mono-repo skill authors are early adopters and evangelists, and a prune command that silently destroys their layout is high-severity friction for the segment most likely to drive word-of-mouth. Dissent resolved: No panelist disagreed on required vs nit classification. Supply-chain-security-expert and devx-ux-expert both identified the unconditional ancestor whitelist as the central risk but framed it differently -- devx as a user-visible false negative, supply-chain as a structural invariant dependency. The two framings are complementary, not in conflict. The devx required finding subsumes the supply-chain nit; resolving it resolves both. Growth/positioning note: Mono-repo skill authoring -- a single repo containing multiple skills installed via subdirectory paths -- is a power-user pattern that unlocks significant workflow density. Once the false-negative guard is in place, the next release post should include a one-line callout aimed at this segment: "Mono-repo skill authors: apm prune is now safe to run in CI." This is a concrete, verifiable claim that costs nothing to add and directly addresses the credibility gap that early adopters report when recommending APM to their teams. Per-persona findings (full)Python ArchitectclassDiagram
direction LR
class helpers_module {
<<Module>>
+_build_expected_install_paths(declared_deps, lockfile, dir) set
+_scan_installed_packages(apm_modules_dir) list
+_check_orphaned_packages() list
+_expand_with_ancestors(paths) set
}
class prune_cmd {
<<IOBoundary>>
+prune(ctx, dry_run) None
}
class deps_cli_mod {
<<IOBoundary>>
+_resolve_scope_deps(apm_dir, logger) dict
}
class helpers_module:::touched
classDef touched fill:#fff3b0,stroke:#d47600
note for helpers_module "_expand_with_ancestors: Pure Function\nstateless, no I/O, 3 call sites (rule: abstract when 3+)"
prune_cmd ..> helpers_module : imports
deps_cli_mod ..> helpers_module : imports
flowchart TD
A["apm prune / apm deps list / apm compile"] --> B["_build_expected_install_paths\n_helpers.py [I/O]"]
B --> C["_scan_installed_packages\n_helpers.py [FS] rglob apm_modules/"]
C --> D["_expand_with_ancestors(expected)\n_helpers.py [Pure] NEW"]
D --> E["expected_with_ancestors: set[str]\n= leaf paths + all 2+-segment prefixes"]
E --> F{"p in expected_with_ancestors?"}
F -- "yes (leaf or ancestor)" --> G["skip: recognized install or intermediate dir"]
F -- "no" --> H["append to orphaned_packages list"]
H --> I["prune: safe_rmtree / deps: mark source=orphaned"]
Design patterns
Nits:
CLI Logging ExpertNits:
DevX UX ExpertRequired:
Nits:
Supply Chain Security ExpertNo required findings. Nits:
Auth ExpertInactive -- No auth-related files touched; the PR modifies only orphan-detection path expansion in OSS Growth HackerNo required findings. Nits:
Verdict computed deterministically: 1 required finding across 5 active panelists. APPROVE iff N == 0. Push a new commit to clear this verdict label automatically. Note 🔒 Integrity filter blocked 2 itemsThe following items were blocked because they don't meet the GitHub integrity level.
To allow these resources, lower tools:
github:
min-integrity: approved # merged | approved | unapproved | none
|
…er/repo root + panel nits Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel Verdict: REJECT
Required before merge (9 items)
Nits (14 items, skip if you want)
CEO arbitrationThree panelists independently flagged the missing The CLI-logging-expert correctly identifies that the orphan warning block in The growth hacker's two required items -- CHANGELOG voice and monorepo release framing -- are legitimate positioning observations but fall outside the specialist blocking threshold per the OSS Growth Hacker persona contract ("you do not block specialist findings"). They are reclassified as nits for purposes of merge decisions. The CHANGELOG rewrite suggestion is concrete and low-cost and should be adopted before the release post ships. Dissent resolved: DevX-UX-expert classified the Growth/positioning note: This fix is quietly the most important positioning commit in recent history: apm now correctly handles monorepo and multi-skill repo layouts without false prune prompts. That is table-stakes functionality that competing package managers get wrong constantly. A short post demonstrating a two-skill monorepo workflow would convert this bug fix into a community acquisition event. Do not bury it in a patch entry. Per-persona findings (full)Python Architectflowchart TD
A["declared deps + lockfile"] --> B["_build_expected_install_paths()\n-> expected: set[str]"]
B --> C["_expand_with_ancestors(expected, standalone_installed)\n-> expected_with_ancestors: set[str]"]
D["apm_modules/ filesystem"] --> E["_scan_installed_packages()\n-> installed: list[str]"]
E --> F["standalone_installed\n(filter: has apm.yml)"]
F --> C
E --> G["orphaned = installed - expected_with_ancestors"]
C --> G
G --> H{"caller"}
H -->|"_check_orphaned_packages"| I["compile warning"]
H -->|"prune (NEEDS FIX)"| J["rm -rf"]
H -->|"deps/cli"| K["display tree"]
flowchart LR
subgraph correct ["Correct (real-orphan safe)"]
direction TB
COH["_check_orphaned_packages"] -->|"passes standalone_installed"| EWA1["_expand_with_ancestors"]
end
subgraph missing ["Missing guard"]
direction TB
PR["prune.py"] -->|"NO installed= arg"| EWA2["_expand_with_ancestors"]
DC["deps/cli.py"] -->|"NO installed= arg"| EWA3["_expand_with_ancestors"]
end
Design patterns
Required:
Nits:
CLI Logging ExpertRequired:
Nits:
DevX UX ExpertNo findings required. Nits:
Supply Chain Security ExpertRequired:
Nits:
Auth ExpertInactive -- No auth-related files changed; PR only modifies orphan-detection logic in _helpers.py, deps/cli.py, and prune.py, which do not touch authentication, token management, credential resolution, or host classification. OSS Growth HackerRequired (reclassified as nits by CEO per persona contract):
Nits:
Verdict computed deterministically: 9 required findings across 4 active panelists. APPROVE iff N == 0. Push a new commit to clear this verdict label automatically. Note 🔒 Integrity filter blocked 2 itemsThe following items were blocked because they don't meet the GitHub integrity level.
To allow these resources, lower tools:
github:
min-integrity: approved # merged | approved | unapproved | none
|
…expansion Fold panel round-2 findings (microsoft#1052): - prune.py: pass standalone_installed (lockfile-membership + apm.yml fallback) to _expand_with_ancestors so the destructive command behaves identically to its advisory display path. Closes the real-orphan deletion false-negative when a sibling subdir dep shares the same owner/repo install root. - deps/cli.py: thread the same standalone_installed guard through _resolve_scope_deps; route both orphan-warning blocks (Rich and fallback) through CommandLogger.warning() instead of raw console.print/click.echo. Removes the latent Rich MarkupError risk from the '[!]' literal and unifies output behaviour with prune.py. - _helpers.py: normalise backslashes before the '..' split guard in _expand_with_ancestors (closes the backslash-traversal bypass); cap ancestor emission at depth 3 (ADO install-root depth) to bound the orphan-suppression surface; replace the apm.yml-only heuristic with a combined lockfile-membership + apm.yml signal via new _standalone_installed_packages helper (tamper-evident determination); sort the orphan list for deterministic output. - CHANGELOG: rewrite the Fixed entry in user-voice per CEO-adopted growth-hacker suggestion; surface the multi-skill / monorepo positioning angle and the no-migration-needed reassurance. - Tests: add regression coverage for (a) prune CLI deleting a real owner/repo orphan when a sibling subdir dep is declared, (b) backslash-traversal guard, (c) installed= guard semantic, (d) depth-cap; update the deps-list parent-not-orphaned test to use the simpler intermediary-only fixture per panel nit. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel Verdict: REJECT
Required before merge (4 items)
Nits (15 items, skip if you want)
CEO arbitrationThis PR solves a genuine and concrete bug: parent directories of legitimately installed subdirectory packages were being flagged as orphans, creating false positives in a destructive command ( The python-architect nits are internally consistent and well-reasoned but none individually rise to required status. The depth-cap note from supply-chain-security-expert overlaps with the architect's and the correct resolution is to name the constant AND tie its value to the ADO root-depth invariant. The UX nits around bullet character change and CHANGELOG entry quality are valid and easy to fold into the fix pass. Strategic read: this is a solid fix whose implementation needs one focused pass to close the security and logging gaps. The concept is low-risk and high-value for the target segment. The panel's required findings are actionable and bounded; no specialist is asking for a design reversal. Dissent resolved: cli-logging-expert and devx-ux-expert agree Growth/positioning note: This fix quietly enables a first-class layout ( Per-persona findings (full)Python ArchitectclassDiagram
direction LR
class _helpers {
<<Module / IOBoundary>>
+_build_expected_install_paths(declared, lockfile, dir) set
+_scan_installed_packages(dir) list
+_check_orphaned_packages() list
}
class _expand_with_ancestors {
<<Pure>>
+__call__(paths, installed) set
}
class _standalone_installed_packages {
<<Pure>>
+__call__(installed, dir, lockfile) list
}
class _resolve_scope_deps {
<<IOBoundary>>
+__call__(apm_dir, logger, insecure_only) tuple
}
class prune_command {
<<IOBoundary>>
+run(apm_dir, logger) None
}
class LockFile {
<<ValueObject>>
+dependencies dict
+read(path) LockFile
}
class CommandLogger {
<<Base>>
+warning(msg) None
+progress(msg) None
}
_helpers *-- _expand_with_ancestors : contains
_helpers *-- _standalone_installed_packages : contains
_helpers ..> LockFile : reads
_resolve_scope_deps ..> _expand_with_ancestors : calls
_resolve_scope_deps ..> _standalone_installed_packages : calls
_resolve_scope_deps ..> CommandLogger : emits via
prune_command ..> _expand_with_ancestors : calls
prune_command ..> _standalone_installed_packages : calls
prune_command ..> CommandLogger : emits via
flowchart TD
A([apm deps list / apm prune]) --> B[_resolve_scope_deps / prune.run]
B --> C["[FS] apm_modules_path.rglob - scan candidates"]
C --> D{candidate valid?}
D -- skip --> C
D -- keep --> E["scanned_candidates.append"]
E --> C
C --> F["[I/O] get_lockfile_path + LockFile.read"]
F --> G{lockfile exists?}
G -- no --> H[lockfile_for_check = None]
G -- yes --> I[lockfile_for_check = LockFile]
H --> J[_standalone_installed_packages]
I --> J
J --> K{lockfile key present?}
K -- yes --> L[mark as standalone]
K -- no --> M{"[FS] apm.yml exists?"}
M -- yes --> L
M -- no --> N[skip - filesystem intermediary]
L --> O[standalone_installed_for_check]
N --> O
O --> P[_expand_with_ancestors]
P --> Q{path contains .. after backslash-norm?}
Q -- yes --> R[keep original, skip expansion]
Q -- no --> S["range(2, min(4, len(parts))): emit ancestors up to depth 3"]
S --> T{ancestor in installed_set AND not in declared?}
T -- yes --> U[skip - would mask real orphan]
T -- no --> V[add ancestor to expanded set]
V --> W[declared_with_ancestors]
R --> W
U --> W
W --> X["for each scanned candidate: org_repo_name not in declared_with_ancestors?"]
X -- orphaned --> Y[orphaned_packages.append]
X -- expected --> Z[installed_packages.append]
Y --> AA["[OUT] logger.warning / logger.progress"]
Z --> AA
No required findings. Nits:
CLI Logging ExpertRequired:
Nits:
DevX UX ExpertRequired:
Nits:
Supply Chain Security ExpertRequired:
Nits:
Auth ExpertInactive -- No auth-sensitive files changed; PR modifies orphan detection path logic in OSS Growth HackerNo required findings. Nits:
Side-channel note: This fix is a quiet trust-builder for a high-value segment -- teams distributing skills from monorepos (common in the GitHub AI ecosystem). False positives in safety commands like Verdict computed deterministically: 4 required findings across 5 active panelists. APPROVE iff N == 0. Push a new commit to clear this verdict label automatically. Note 🔒 Integrity filter blocked 2 itemsThe following items were blocked because they don't meet the GitHub integrity level.
To allow these resources, lower tools:
github:
min-integrity: approved # merged | approved | unapproved | none
|
Persona-gated round 3 fold for microsoft#1052. 4 findings: - validate_path_segments replaces ad-hoc .. check in _helpers.py - _standalone_installed_packages no longer swallows lockfile errors - prune.py + deps/cli.py orphan surface unified on logger.warning - builtins.set / builtins.list shims removed (redundant) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🔬 APM Review Panel — PR #1052
SummaryThis PR fixes a correctness bug where 🛡️ Security PanelistScore: Approved with commendation The PR addresses a non-trivial supply-chain surface and does so correctly:
✅ Correctness PanelistScore: Approved The logic is correct across all scenarios:
The The 🧪 Testing PanelistScore: Approved with commendation Test coverage is exemplary for a security-sensitive change:
One minor nit: 🔧 Maintainability PanelistScore: Approved
Panel ConclusionAll four panelists approve. The implementation is correct, security-hardened, and well-tested. The PR closes a real usability bug (spurious prune prompts in multi-skill/monorepo setups) while tightening the orphan-detection safety net for real orphans. No blocking issues found. → Recommend merge. Note 🔒 Integrity filter blocked 2 itemsThe following items were blocked because they don't meet the GitHub integrity level.
To allow these resources, lower tools:
github:
min-integrity: approved # merged | approved | unapproved | none
|
Description
Fix false-positive orphan detection when dict-form dependencies (
git:+path:) point at nested subdirectory packages. When a dependency like.apm/skills/commit-conventionsis installed, the intermediateowner/repo/directory contains a.apm/subdirectory as a side effect. The orphan scanner picks this up as an "installed package," but since it's not directly in the expected set (only the deeper path is), it gets falsely flagged as orphaned.The fix adds an ancestor check: a scanned directory is not considered orphaned if any expected install path starts with it as a path prefix. Applied consistently across all three orphan-detection sites (
apm compile,apm prune,apm deps list).Fixes #1050
Type of change
Testing