fix: virtual subdirectory deps marked as orphaned, skipping instruction processing#100
Conversation
…on processing Virtual subdirectory packages (owner/repo/subdir) were incorrectly flattened to owner/virtual-pkg-name in both dependency discovery and orphan detection, causing: - apm deps list: dependency shown as 'orphaned' - apm deps tree: dependency shown as 'unknown' - apm compile: Instruction Processing 0.0ms (no primitives found) Fixed get_dependency_declaration_order() and orphan detection to use natural path (owner/repo/subdir) for is_virtual_subdirectory() deps, matching DependencyReference.get_install_path() semantics. Refs: microsoft#99
There was a problem hiding this comment.
Pull request overview
This PR fixes a critical bug where virtual subdirectory packages (e.g., owner/repo/skills/azure-naming) were incorrectly flattened using get_virtual_package_name(), causing them to be marked as orphaned in dependency views and skipping instruction processing during compilation.
Changes:
- Fixed path resolution in
get_dependency_declaration_order()to preserve natural path structure for virtual subdirectory packages instead of flattening them - Fixed orphan detection logic in
deps listcommand to correctly recognize virtual subdirectory packages - Added regression tests for virtual subdirectory package handling
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/apm_cli/primitives/discovery.py |
Updated get_dependency_declaration_order() to check is_virtual_subdirectory() and return natural paths (owner/repo/subdir) instead of flattened names |
src/apm_cli/commands/deps.py |
Updated orphan detection to use subdirectory-aware logic when building declared_deps set |
tests/integration/test_virtual_package_orphan_detection.py |
Added _find_installed_subdirectory_packages() helper and two regression tests for virtual subdirectory orphan detection and declaration order |
- scan_dependency_primitives: use Path.joinpath(*parts) to handle variable-length paths (4+ parts for nested sub-skills) - test helper: change threshold to >= 4 to avoid overlap with 3-level ADO packages already found by _find_installed_packages - test: simplify orphan detection with unified installed set instead of fragile patch-on-top approach Refs: microsoft#99
danielmeppiel
left a comment
There was a problem hiding this comment.
Good root cause analysis and clean fix for discovery.py and deps.py — the is_virtual_subdirectory() branching is the right approach, and the joinpath(*parts) fix in the follow-up commit is solid.
Two remaining gaps before this can be merged:
-
cli.pyprune command (line ~764) has the identical unfixed pattern —get_virtual_package_name()for all virtual packages without anis_virtual_subdirectory()check. This meansapm prunewould incorrectly remove subdirectory packages. -
deps.py:list_packages()scanner only walks 2–3 directory levels underapm_modules/, so it won't discover subdirectory packages at 4+ levels (e.g.,owner/repo/skills/azure-naming). The test validates via helpers that do handle deep traversal, but the production scanner doesn't.
Both are detailed in inline comments.
- cli.py prune: add is_virtual_subdirectory() branch to expected_installed
so prune won't incorrectly remove subdirectory packages
- cli.py prune: replace 2-3 level scanner with rglob to discover packages
at any depth (4+ levels for nested sub-skills)
- deps.py list: replace 2-3 level scanner with rglob for same reason
Both scanners now use rglob('*') to find any directory containing apm.yml,
regardless of nesting depth, matching the install path structure.
Refs: microsoft#99
There was a problem hiding this comment.
Thanks for addressing the previous feedback — both the cli.py prune fix and the rglob scanner refactor look right.
However, the rglob refactor in deps.py introduced an ADO source label regression. The old code had separate branches for GitHub (2-level) and ADO (3-level) packages, setting 'source': 'github' or 'source': 'azure-devops' respectively. The unified rglob scanner lost this distinction and hardcodes 'github' for all non-orphaned packages.
Failing tests (verified locally):
test_ado_e2e::TestADODepsAndPrune::test_deps_list_shows_correct_path— expectsSource: azure-devopstest_ado_e2e::TestMixedDependencies::test_mixed_deps_list— expects ADO packages labeledazure-devops
Note: These tests weren't caught in CI due to a test harness bug on our side (test binary resolution bypassed the PR artifact). We've fixed that in PR #98 — not your fault, but the regression still needs fixing.
Suggested fix: In the scanner loop, detect ADO packages and set the source accordingly - see my following comment
Suggested fix for ADO source regressionFile: The problemLine 122 hardcodes # Line 122
'source': 'orphaned' if is_orphaned else 'github',The old code had separate scanner branches for GitHub (2-level) and ADO (3-level), each setting the correct source. The Important: a path-depth heuristic ( The fix (2 changes, same file)Change 1 — Line 58: Replace # Line 58 — before:
declared_deps = set()
# Line 58 — after:
declared_sources = {} # dep_path → 'github' | 'azure-devops'Then every # Example — lines 72-75, before:
declared_deps.add(
f"{repo_parts[0]}/{repo_parts[1]}/{repo_parts[2]}/{dep.virtual_path}"
)
# After:
source = 'azure-devops' if dep.is_azure_devops() else 'github'
declared_sources[
f"{repo_parts[0]}/{repo_parts[1]}/{repo_parts[2]}/{dep.virtual_path}"
] = sourceSame pattern for every other Change 2 — Lines 116 and 122: Look up source from dict: # Line 116 — before:
is_orphaned = org_repo_name not in declared_deps
# Line 122 — before:
'source': 'orphaned' if is_orphaned else 'github',
# Line 116 — after:
is_orphaned = org_repo_name not in declared_sources
# Line 122 — after:
'source': 'orphaned' if is_orphaned else declared_sources.get(org_repo_name, 'github'),That's it — no new imports, no new functions, no heuristics. |
The rglob refactor unified GitHub/ADO scanner branches but hardcoded
'github' as the source for all non-orphaned packages. This broke ADO
packages which should show 'azure-devops'.
Fix: replace declared_deps set with declared_sources dict that maps
each dep path to its source label ('github' | 'azure-devops'), derived
from dep.is_azure_devops(). The scanner looks up the source instead of
hardcoding it.
Refs: microsoft#99
|
Fixed in bede7f9. Replaced We didn't catch this locally because the ADO e2e tests ( |
Problem
Virtual subdirectory packages (
owner/repo/subdir) are incorrectly handled in two places, causing:apm deps list: dependency shown as orphanedapm deps tree: dependency shown as unknownapm compile --verbose:Instruction Processing: 0.0ms— no primitives discovered from the dependencyRoot Cause
Both
get_dependency_declaration_order()indiscovery.pyand orphan detection indeps.pyflatten all virtual dependencies usingget_virtual_package_name(), producingowner/virtual-pkg-name. However,DependencyReference.get_install_path()correctly differentiates virtual subdirectory packages (owner/repo/subdir) from virtual file/collection packages (owner/virtual-pkg-name).This path-shape mismatch means the dependency name returned by discovery doesn't match the actual installed directory, so:
scan_dependency_primitives()can't resolve the dependency path → no primitives foundFix
discovery.py:get_dependency_declaration_order()now checksis_virtual_subdirectory()and returns the natural path (owner/repo/subdir) instead of the flattened name.deps.py: Orphan detection uses the same subdirectory-aware logic when building thedeclared_depsset.test_virtual_package_orphan_detection.py.Validation
apm deps listno longer shows orphaned;apm compile --verboseshowsInstruction Processing: 5.4ms(was0.0ms)Refs: #99