fix(deps): preserve nested package manifests during prune#2092
fix(deps): preserve nested package manifests during prune#2092danielmeppiel wants to merge 12 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a destructive edge case in dependency discovery where an installed package containing a nested apm.yml could be misclassified as a separate top-level package, causing apm deps list to report a false orphan and apm prune to delete the nested directory.
Changes:
- Apply the existing
_is_nested_under_package()ancestry guard inside the shared installed-package scanner so nested manifests are excluded from top-level results. - Remove the previous
SKILL.md-only gating so bothapm.ymlandSKILL.mdnested manifests are consistently treated as owned by their parent package indeps listanddeps tree. - Add regression tests covering nested-manifest behavior at the scanner layer and the CLI helper classification layer.
Show a summary per file
| File | Description |
|---|---|
| src/apm_cli/commands/deps/_utils.py | Filters installed-package scan results to exclude candidates nested under an existing package root. |
| src/apm_cli/commands/deps/cli.py | Aligns filesystem candidate scanning for deps list/tree to consistently skip nested manifests. |
| tests/unit/test_deps_utils.py | Adds unit coverage ensuring the shared scanner only returns the parent when a nested apm.yml exists. |
| tests/unit/commands/test_deps_cli_helpers.py | Adds unit coverage ensuring deps-list classification omits nested manifests as independent installed/orphan entries. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 1
- Review effort level: Low
| def test_nested_package_manifest_is_not_an_orphan(self, tmp_path): | ||
| """A nested subpackage belongs to its installed parent package.""" | ||
| modules_dir = tmp_path / APM_MODULES_DIR |
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 0 | 2 | Shared predicate is the simplest correct design; current bounded scan cost is acceptable. |
| CLI Logging Expert | 0 | 0 | 0 | No CLI output surface changed. |
| DevX UX Expert | 0 | 0 | 0 | No validated findings; legacy response shape failed schema validation twice. |
| Supply Chain Security Expert | 0 | 0 | 0 | Package boundaries tighten; deletion containment is unchanged. |
| OSS Growth Hacker | 0 | 0 | 0 | Required user-facing CHANGELOG entry is present. |
| Doc Writer | 0 | 0 | 0 | CHANGELOG and helper documentation are accurate. |
| Test Coverage Expert | 0 | 0 | 0 | Exact-HEAD lifecycle integration test passed. |
| Performance Expert | 0 | 0 | 0 | Bounded O(N*D) filesystem scan is acceptable. |
B = highest-severity signals, R = recommendations, N = nits. Counts are advisory signals.
Architecture
flowchart TD
A["install / update / prune / deps tree"] --> B["scan apm_modules"]
B --> C{"candidate has apm.yml or .apm?"}
C -- No --> D["skip"]
C -- Yes --> E{"ancestor has apm.yml or .apm?"}
E -- Yes --> F["skip: owned by parent"]
E -- No --> G["classify top-level package"]
G --> H{"still declared?"}
H -- Yes --> I["keep complete package"]
H -- No --> J["remove complete package directory"]
Folded in this run
- (Copilot) Renamed the CLI helper test so its name matches the asserted classification --
cec792e57. - (Panel) Added an empirical install/update/prune lifecycle test with unrelated-material preservation --
cec792e57. - (Panel) Aligned
.apm-only parent detection and added direct dependency-tree coverage --538527d63. - (Panel) Added the required CHANGELOG entry and corrected the package-boundary docstring --
f9a6fba37.
Regression-trap evidence
tests/integration/test_intra_package_cleanup.py::TestNestedPackagePrune::test_prune_preserves_nested_content_then_removes_whole_parent-- deleted the_scan_installed_packagesancestry guard; test failed because nested content was removed; guard restored.tests/unit/test_deps_utils.py::TestIsNestedUnderPackage::test_child_of_manifestless_package-- deleted the.apmparent condition; test failed; guard restored.tests/unit/commands/test_deps_cli_phase3w4.py::TestBuildDepTree::test_nested_package_manifest_skipped-- deleted the tree ancestry guard; test failed because the nested package appeared independently; guard restored.
Validation
- Exact head:
f9a6fba375ca64b322c7984a874a3179db47918a. - Relevant suite: 92 passed before the final documentation fold; 89 passed after it.
- Full lint mirror: ruff check and format silent; YAML I/O, file length, relative path, pylint R0801, and auth-signal guards passed.
- Exact-head CI: all 13 checks passed, including both test shards, coverage combine, lint, CodeQL, merge workflow, binary smoke, spec conformance, self-check, NOTICE, and CLA. Run: https://github.com/microsoft/apm/actions/runs/29062607630
Recommendation
Ready for maintainer review. No recommended follow-ups remain.
Full per-persona findings
Python Architect
- [nit] The ancestry walk is O(N*D), but package depth is bounded and measured impact is negligible.
- [nit] No additional design pattern is warranted; the shared predicate is the simplest correct shape.
All other active personas returned no findings. Auth Expert was inactive because the diff is authentication-neutral.
This panel is advisory; the maintainer makes the shipping decision.
Reuse the installed-package ancestry guard for nested apm.yml files so dependency listing and prune only treat the parent as installed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Exercise install, update, preservation, and full-parent pruning while proving unrelated package content survives. Also aligns the unit test name with its actual classification contract; addresses Copilot inline review and the shepherd hard gate. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Treat .apm-only parents consistently with scanner package detection and add direct tree-scan coverage for nested manifests. Addresses review-panel followups on package-boundary symmetry and tree behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Document the .apm package-boundary behavior and add the required Unreleased entry for the user-visible prune correction. Addresses final panel documentation followups. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the fixed tree cutoff with path-local cycle detection and add a hermetic eight-level CLI regression covering list, tree, and prune while excluding embedded parent-owned manifests. Addresses the operator follow-up for PR #2092. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 1 | 2 | Cycle-guarded traversal and ownership filtering are sound; remove call-stack limits iteratively. |
| CLI Logging Expert | 0 | 0 | 2 | Output is consistent; a cycle marker would improve diagnostics. |
| DevX UX Expert | 0 | 1 | 1 | The npm-like mental model is restored; one pre-existing recovery message remains vague. |
| Supply Chain Security Expert | 0 | 1 | 1 | Prune safety is stronger; iterative traversal avoids deep-lockfile stack exhaustion. |
| OSS Growth Hacker | 0 | 0 | 2 | Docs and changelog reinforce trust; lead with user pain. |
| Doc Writer | 0 | 1 | 0 | Full-depth docs are accurate; prune's orphan definition needs the lock graph. |
| Test Coverage Expert | 0 | 0 | 1 | Critical behavior is defended by E2E and mutation evidence. |
| Performance Expert | 0 | 0 | 1 | Typical graphs are fast; an iterative walk avoids per-frame overhead. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [Python Architect] Convert Rich and fallback tree rendering to iterative explicit-stack traversal -- eliminates Python recursion limits without introducing a depth cap.
- [Doc Writer] Define prune orphans against direct declarations and the lockfile-resolved graph -- current prose omits retained transitives.
- [CLI Logging Expert] Mark repeated ancestors as circular -- makes intentional cycle suppression visible.
- [DevX UX Expert] Improve the pre-existing unresolved-parent guidance -- gives users a concrete recovery path.
- [Test Coverage Expert] Strengthen no-Rich traversal coverage -- protects fallback parity.
Architecture
classDiagram
direction LR
class ScanInstalledPackages
class NestedPackageGuard
class BuildDependencyTree
class RichTreeRenderer
class TextTreeRenderer
class LockfileDependency
ScanInstalledPackages ..> NestedPackageGuard : filters
BuildDependencyTree ..> NestedPackageGuard : filters
BuildDependencyTree ..> LockfileDependency : reads
BuildDependencyTree ..> RichTreeRenderer : renders
BuildDependencyTree ..> TextTreeRenderer : renders
flowchart TD
CLI[apm deps list / tree / prune] --> Scan[scan apm_modules]
Scan --> Ownership{nested under package?}
Ownership -->|yes| Ignore[parent-owned content]
Ownership -->|no| Match[match canonical lock path]
Match --> Graph[resolved_by dependency graph]
Graph --> Cycle{ancestor repeated?}
Cycle -->|yes| Stop[stop this path]
Cycle -->|no| Render[render child and continue]
Recommendation
The exact-target regression and mutation evidence prove the user promise. Fold iterative no-cap traversal and the prune-doc correction in this PR; the remaining diagnostic and fallback items are small quality improvements on the same surface.
Full per-persona findings
Python Architect
- [recommended] Recursive traversal can hit Python's recursion limit on pathological graphs at
src/apm_cli/commands/deps/cli.py:73.
Convert to iterative DFS with an explicit stack; do not add a hard depth cap. - [nit] Rich and text rendering duplicate traversal logic.
- [nit] The optional ancestor sentinel is less explicit than a dedicated walk abstraction.
CLI Logging Expert
- [nit] Repeated ancestors are silently skipped at
src/apm_cli/commands/deps/cli.py:85. - [nit] The text fallback uses
click.echo; this is pre-existing and appropriate for structured output.
DevX UX Expert
- [recommended] The pre-existing
(could not determine parent)annotation lacks actionable recovery guidance. - [nit] Keep command-guide notes within its scannable table structure.
Supply Chain Security Expert
- [recommended] Recursive traversal permits stack exhaustion from a crafted very deep lockfile at
src/apm_cli/commands/deps/cli.py:89.
Use an iterative stack rather than the suggested hard cap. - [nit] Symlink resolution could strengthen ancestry checks, while the deletion chokepoint already enforces containment.
OSS Growth Hacker
- [nit] Lead the CHANGELOG entry with the phantom-orphan/prune pain.
- [nit] Contributor attribution can reinforce community norms when applicable.
Auth Expert -- inactive
Dependency inventory, tree, and prune behavior do not change authentication, token, credential, or host-selection surfaces.
Doc Writer
- [recommended] Prune docs omit lockfile-resolved transitives from the orphan definition at
docs/src/content/docs/reference/cli/prune.md:28.
Proof (passed):tests/integration/test_transitive_chain_e2e.py::test_deps_commands_follow_full_lock_graph_and_ignore_embedded_manifests-- exact target run passed in 3.73s.
Test Coverage Expert
- [nit] The no-Rich fallback is covered below the full E2E tier.
Proof (passed): exact 8-level CLI E2E passed in 3.06s; unit fallback coverage also passes.
Performance Expert
- [nit] Immutable ancestor-set copies add minor allocation cost at
src/apm_cli/commands/deps/cli.py:94; an iterative traversal can avoid call-stack growth.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
Fold review feedback by removing Python call-stack limits, marking cycles, clarifying recovery guidance, and aligning prune documentation with the resolved lock graph. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 0 | 2 | Architecture is sound; remove one vestigial parameter. |
| CLI Logging Expert | 0 | 0 | 1 | Rich and text output agree; the same parameter is dead. |
| DevX UX Expert | 0 | 0 | 1 | Tighten command help copy. |
| Supply Chain Security Expert | 0 | 0 | 0 | No current security concern. |
| OSS Growth Hacker | 0 | 0 | 0 | User-facing communication is clear. |
| Doc Writer | 0 | 0 | 0 | Changed docs are accurate and complete. |
| Test Coverage Expert | 0 | 0 | 1 | Critical contracts are covered; exact text prefixes could be explicit. |
| Performance Expert | 0 | 0 | 0 | Traversal cost is appropriate for CLI display. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 3 follow-ups
- [Python Architect] Remove
_add_tree_children(..., has_rich)-- the function is Rich-only and the false branch is unreachable. - [DevX UX Expert] Tighten list/tree help text -- describe manifest-plus-lock inventory and complete resolved-tree rendering.
- [Test Coverage Expert] Add one exact ASCII text-walker prefix assertion -- make fallback shape explicit without coupling E2E to Rich glyphs.
Architecture
flowchart TD
CLI[apm deps tree] --> Build[build direct and resolved_by indexes]
Build --> Walk[iterative depth-first walk]
Walk --> Cycle{ancestor repeated?}
Cycle -->|yes| Mark[render circular marker]
Cycle -->|no| Push[push children]
Mark --> Rich[Rich renderer]
Mark --> Text[text renderer]
Push --> Rich
Push --> Text
Recommendation
Fold the dead-parameter cleanup, help copy, and small text-prefix test. These are narrow changes on the current surface; the substantive behavior is already proven.
Full per-persona findings
Python Architect
- [nit]
_add_tree_childrenacceptshas_rich, but all production callers are inside the Rich branch; remove the false affordance. - [nit] The branch map retains completed entries; measured scale makes this non-actionable.
CLI Logging Expert
- [nit] Remove the dead
has_richbranch so the Rich-only contract is explicit.
DevX UX Expert
- [nit] List/tree
--helpcopy under-describes manifest-plus-lock and all-depth behavior.
Supply Chain Security Expert
No findings.
OSS Growth Hacker
No findings.
Auth Expert -- inactive
No authentication, credential, token, or host-classification surface changed.
Doc Writer
No findings.
Test Coverage Expert
- [nit] The E2E proves increasing nesting depth, while exact text connectors are only covered below E2E tier. The user promise is covered.
Performance Expert
No findings.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
Remove the dead Rich switch, align command help with resolved-graph behavior, and pin portable text nesting. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 0 | 1 | Implementation is sound; strengthen one element type. |
| CLI Logging Expert | 0 | 0 | 1 | Correct one stale orphan-warning parenthetical. |
| DevX UX Expert | 0 | 1 | 0 | Use user-intent list help wording. |
| Supply Chain Security Expert | 0 | 0 | 0 | No current concern. |
| OSS Growth Hacker | 0 | 0 | 0 | No in-PR communication concern. |
| Doc Writer | 0 | 1 | 0 | Remove an unsupported embedded-manifest exception. |
| Test Coverage Expert | 0 | 0 | 0 | Exact-target coverage is green. |
| Performance Expert | 0 | 0 | 0 | No material performance fault. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 4 follow-ups
- [Doc Writer] Remove the lockfile exception for embedded manifests -- scanners categorically treat nested source manifests as parent-owned.
- [CLI Logging Expert] Change
(not in apm.yml)to resolved-graph wording -- expected paths include lockfile transitives. - [DevX UX Expert] Use
List installed APM dependencies and their primitives-- describe user intent in help. - [Python Architect] Type tree children as
list[LockedDependency]-- make the shared walker contract explicit.
Recommendation
Fold these four small edits, rerun the exact-target evidence, and perform the terminal advisory pass. The required behavior is already proven.
Full per-persona findings
Python Architect
- [nit] Replace bare child lists with
list[LockedDependency]annotations.
CLI Logging Expert
- [nit] Orphan warnings say
(not in apm.yml)although lockfile-resolved transitives are also expected.
DevX UX Expert
- [recommended] List help uses implementation wording rather than the user's goal.
Supply Chain Security Expert
No findings.
OSS Growth Hacker
No in-PR findings; release narrative is release-prep scope.
Auth Expert -- inactive
No authentication surface changed.
Doc Writer
- [recommended] Changed docs describe a lockfile exception for nested manifests that the scanner does not implement.
Test Coverage Expert
No findings; exact-target tests pass.
Performance Expert
No findings.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
Clarify parent-owned embedded manifests, use resolved-graph orphan wording, and tighten tree-walker types. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep help and unresolved-parent recovery wording focused on the user's next action. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 0 | 0 | Shared iterative traversal is typed, cycle-safe, and unbounded by call depth. |
| CLI Logging Expert | 0 | 0 | 0 | Rich and text output are consistent and actionable. |
| DevX UX Expert | 0 | 0 | 0 | Help, recovery, inventory, and prune match the package-manager mental model. |
| Supply Chain Security Expert | 0 | 0 | 0 | No new deletion, credential, or integrity risk. |
| OSS Growth Hacker | 0 | 0 | 0 | Changed docs and changelog communicate the safety fix clearly. |
| Doc Writer | 0 | 0 | 0 | Dependency and prune references match runtime behavior. |
| Test Coverage Expert | 0 | 0 | 0 | Critical contracts have E2E, unit, and mutation regression traps. |
| Performance Expert | 0 | 0 | 0 | Iterative display traversal has no material performance fault. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Architecture
classDiagram
class LockedDependency
class TreeWalk
class RichRenderer
class TextRenderer
TreeWalk ..> LockedDependency : traverses
RichRenderer ..> TreeWalk : consumes
TextRenderer ..> TreeWalk : consumes
flowchart TD
Lock[apm.lock.yaml] --> Index[resolved_by index]
Index --> Walk[iterative depth-first walk]
Walk --> Cycle{ancestor repeated?}
Cycle -->|yes| Mark[render circular marker]
Cycle -->|no| Continue[push children]
Mark --> Output[Rich or text output]
Continue --> Output
Recommendation
All panel signals converge on ship_now. No current findings, deferred items, or follow-ups remain; the exact-target implementation and evidence are ready for maintainer review.
Full per-persona findings
Python Architect
No findings.
CLI Logging Expert
No findings after the final wording fold.
DevX UX Expert
No findings after the final help and recovery fold.
Supply Chain Security Expert
No findings.
OSS Growth Hacker
No in-PR findings.
Auth Expert -- inactive
No authentication, token, credential, or host-classification surface changed.
Doc Writer
No findings.
Test Coverage Expert
No findings; 112 exact-target tests pass and seven mutations were killed.
Performance Expert
No findings.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
Reservations carried from strategic alignment
- Reassess nested-package visibility after fix(deps): match local transitive dependencies by install path #2099 at arbitrary graph depth: addressed by the 8-node actual CLI lock-chain E2E, categorical parent-owned manifest filtering, and canonical local install-path assertions.
Folded in this run
- Iterative no-cap tree traversal and visible cycle markers (
2609eb7e1). - Resolved-graph prune definition, pain-first changelog, and recovery guidance (
2609eb7e1). - Rich-only renderer cleanup and exact ASCII fallback nesting (
8b61f259f). - Parent-owned manifest docs, resolved-graph orphan wording, and typed lock nodes (
69609ad15). - Concise tree help and unresolved-node recovery text (
67299472a).
Copilot signals reviewed
- Two fetch rounds found no Copilot inline comments. The summary-only review introduced no actionable signal.
Validation
- Final head:
6944af6b3cd505b6b1b19c0360f3a1301e1aad2d - Local lint: all seven CI-mirror gates passed (ruff check, format check, YAML I/O, 2100-line limit, portable relpaths, pylint R0801, auth signals).
- Regression suite: 112 focused tests passed; seven production mutations were killed and restored.
- Exact-SHA CI: all required checks completed successfully, including Lint, both Linux test shards, Coverage Combine, PR Binary Smoke, APM Self-Check, CodeQL, Merge Gate, spec conformance, docs build, NOTICE, and CLA.
| PR | Head | Panel stance | Iterations | Folds | Deferrals | Copilot rounds | CI | Mergeable | Merge state | Notes |
|---|---|---|---|---|---|---|---|---|---|---|
| #2092 | 6944af6 |
ship_now |
4 | 10 | 0 | 2 | green | MERGEABLE | BLOCKED | pending required review |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
f9a6fba to
6944af6
Compare
…ckage-orphan-2067 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: aaed937e-6ff3-45da-a1ba-c217842423de
…e marker) Fold in-scope apm-review-panel recommendations on PR #2092: - CHANGELOG: relocate the #2092 entry from the frozen [0.24.1] released block into [Unreleased] > Fixed (split into the data-loss/orphan fix and the deps-tree full-depth + (circular) rendering beat); drop the merge-duplicated #2074/#2102 entries so [0.24.1] again matches main byte-for-byte (doc-writer, oss-growth-hacker). - prune: align the Click help= and docstring with the updated prune.md orphan definition -- "absent from the resolved dependency graph (neither declared in apm.yml nor retained as transitive nodes in apm.lock.yaml)" -- so the CLI --help surface matches the shipped docs (devx-ux-expert, cli-logging-expert). Help text only; no logic change. - tests: add pytestmark = pytest.mark.requires_apm_binary to test_transitive_chain_e2e.py, matching test_intra_package_cleanup.py and the tests.instructions.md convention (test-coverage-expert). Core full-depth cycle-safe traversal and the _is_nested_under_package parent-owned-manifest guard are unchanged. Seven-gate mutation-break proof re-run on this head: disabling the guard fails exactly the seven nested-guard/scanner/E2E/prune traps; restored -> all green. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: aaed937e-6ff3-45da-a1ba-c217842423de
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 0 | 1 | Ancestry guard reuse is the correct abstraction; iterative walker with frozenset cycle detection is sound and eliminates the depth-5 cap. |
| CLI Logging Expert | 0 | 0 | 2 | CLI output clean: ASCII-only tree prefixes, consistent circular/orphan messaging across Rich and non-Rich, proper CommandLogger routing. |
| DevX UX Expert | 0 | 1 | 2 | Nested-manifest ownership fix aligns with npm/pip mental models; prune --help drift folded on final head. |
| Supply Chain Security Expert | 0 | 0 | 0 | Ancestry guard and cycle detection are sound; prune classification errs conservative; no security regression. |
| OSS Growth Hacker | 0 | 0 | 2 | CHANGELOG entry ships the trust narrative; tree-depth lift is a quiet "we handle real graphs" signal. |
| Performance Expert | 0 | 0 | 3 | Iterative tree walk and per-candidate ancestry guard are both O(N*D) on realistic lock graphs; no user-visible regression. |
| Doc Writer | 0 | 2 | 0 | Docs prose accurate; CHANGELOG placement + merge-duplication findings both folded on final head. |
| Test Coverage Expert | 0 | 0 | 2 | All four critical user promises carry regression traps at unit and integration/E2E tiers. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 4 follow-ups
- [CLI Logging Expert] Orphan parenthetical could name apm.yml alongside "resolved dependency graph" for users unfamiliar with the term. -- Reduces cognitive load for first-time users encountering an orphan warning; low-cost copy change that does not affect correctness.
- [CLI Logging Expert] Circular marker inherits
[dim]styling; a[yellow](circular)[/yellow]infix would make cycle-stop nodes visually pop in Rich output. -- Improves scanability of deep trees with cycles; purely cosmetic, zero correctness impact. - [Python Architect] Optional NamedTuple graduation for the 4-tuple yielded by
_walk_tree_childrenif a third consumer appears. -- Improves readability and IDE support for tuple unpacking; defer until a third call-site materializes. - [Performance Expert] O(N*D) frozenset-union and tuple-key hashing costs are acceptable at production depths (<0.1ms at D=20); revisit only if lock graphs exceed hundreds of nodes. -- Informational note for future scaling; no action needed on current workloads.
Architecture
classDiagram
direction TB
class DepsUtils {
<<Module>>
+_scan_installed_packages(dir) list
+_is_nested_under_package(candidate, root) bool
+_count_primitives(path) dict
+_get_package_display_info(path) dict
}
class DepsCli {
<<Module>>
+_walk_tree_children(key, map) Iterator
+_add_tree_children(branch, key, map) None
+_echo_tree_children(key, map, prefix) None
+_resolve_scope_deps(dir, logger) tuple
+_build_dep_tree(dir) dict
}
class LockedDependency {
<<ValueObject>>
+repo_url str
+depth int
+resolved_by str
+get_unique_key() str
}
class APMPackage {
<<ValueObject>>
+name str
+version str
+get_apm_dependencies() list
}
class CommandLogger {
<<Lifecycle>>
+warning(msg) None
+info(msg) None
+error(msg) None
}
class PruneCommand {
<<Consumer>>
+prune(dry_run) None
}
class HelpersModule {
<<Module>>
+_check_orphaned_packages() list
+_expand_with_ancestors() set
}
DepsCli ..> DepsUtils : imports _is_nested_under_package
DepsCli ..> LockedDependency : traverses
DepsCli ..> APMPackage : reads manifest
DepsCli ..> CommandLogger : logs output
DepsUtils ..> APMPackage : reads manifest
PruneCommand ..> DepsUtils : _scan_installed_packages
PruneCommand ..> HelpersModule : _expand_with_ancestors
HelpersModule ..> DepsUtils : _scan_installed_packages
note for DepsUtils "Guard predicate: _is_nested_under_package walks up to root checking apm.yml or .apm"
note for DepsCli "Iterator pattern: _walk_tree_children stack-based DFS with frozenset cycle guard"
flowchart TD
CLI["apm deps list / tree / prune"]
CLI --> SOURCE{"source?"}
SOURCE -->|"deps tree + lockfile"| BUILD["_build_dep_tree parse lockfile deps by depth + resolved_by"]
SOURCE -->|"deps list or no lockfile"| RGLOB["[FS] rglob apm_modules/*"]
BUILD --> WALK["_walk_tree_children stack-based DFS, frozenset ancestors"]
WALK --> CYCLECHECK{"child_key in ancestors?"}
CYCLECHECK -->|"yes"| CIRCULAR["yield circular=True skip children push"]
CYCLECHECK -->|"no"| NOTCIRC["yield circular=False push grandchildren onto stack"]
NOTCIRC --> WALK
CIRCULAR --> RENDERTREE["_add_tree_children / _echo_tree_children append circular suffix"]
NOTCIRC --> RENDERTREE
RGLOB --> NESTED["[FS] _is_nested_under_package walk parent dirs for apm.yml or .apm"]
NESTED -->|"nested"| SKIP["skip: parent-owned content"]
NESTED -->|"top-level"| CANDIDATES["scanned_candidates"]
CANDIDATES --> ORPHAN["orphan = key not in declared_with_ancestors"]
ORPHAN --> RENDERLIST["render table + orphan warnings"]
BUILD --> NESTED2["[FS] _is_nested_under_package fallback scan when no lockfile"]
NESTED2 -->|"nested"| SKIP2["skip nested"]
NESTED2 -->|"top-level"| SCAN["scanned_packages for tree fallback"]
Recommendation
Merge to main and include in the next release with a "reliability and visibility" section in the announcement. The single highest-signal follow-up to track post-merge is the orphan-message wording nit (cli-logging-expert): adding "apm.yml" to the parenthetical so unfamiliar users know exactly which file governs ownership. File as a low-priority issue and pick up in the next polish pass.
Full per-persona findings
Python Architect
- [nit] Design patterns assessment; optional NamedTuple graduation for the 4-tuple yielded by
_walk_tree_childrenif a third consumer appears atsrc/apm_cli/commands/deps/cli.py:75
Iterator/Generator (traversal separated from rendering) and Guard/Predicate (_is_nested_under_packageapplied uniformly across three scan sites) are the correct patterns at this scope. The bare 4-tuple is appropriate at two consumers with clean destructuring.
CLI Logging Expert
- [nit] Circular marker inherits
[dim]styling in Rich tree atsrc/apm_cli/commands/deps/cli.py:124
In a large tree every transitive node is dim, so the cycle-stop marker blends in. A[yellow](circular)[/yellow]infix inside the[dim]block would let the cycle annotation pop without breaking the dim convention. Purely cosmetic. - [nit] Orphan parenthetical could name apm.yml alongside the graph for unfamiliar users at
src/apm_cli/commands/deps/cli.py:399
"not in resolved dependency graph" is accurate but may puzzle a user who does not know what the resolved dependency graph is; naming the concrete artifacts (apm.yml, lockfile) is a friendlier middle ground.
DevX UX Expert
- [recommended] prune
--helpsaid "not listed in apm.yml" while deps list says "not in resolved dependency graph" -- FOLDED on final head atsrc/apm_cli/commands/prune.py:24
The--helpsurface is the primary discoverability mechanism and should match the docs this PR updated. Resolved on the final head by aligning the Clickhelp=and docstring to the prune.md resolved-graph orphan definition (help text only; no logic change). - [nit] deps list help could mention nested-manifest filtering for discoverability at
src/apm_cli/commands/deps/cli.py:454
A user hitting the old bug might search--helpfor "nested" or "orphan"; a brief note would make the fix discoverable from the CLI itself. Minor since behavior is now correct by default. - [nit] Positive signal: "could not place in tree; run apm install to resolve" names the recovery action (Failure-mode-is-the-product); no change needed.
Supply Chain Security Expert
No findings.
OSS Growth Hacker
- [nit] CHANGELOG bullet packed a data-loss fix and a tree-rendering improvement into one sentence -- FOLDED (split into two bullets on final head) at
CHANGELOG.md:50
Users scanning for "did prune delete my stuff?" locate the fix faster if it stands alone; a separate bullet for the deps-tree depth + cycle-marking upgrade also lets release-note writers craft two story beats. - [nit] Orphan warning changed to graph jargon at
src/apm_cli/commands/deps/cli.py:255
"not declared in apm.yml or reached transitively" keeps the actionable pointer to the manifest for first-time users.
Performance Expert
- [nit]
_walk_tree_childrencarries O(D^2) per-node cost from frozenset union and tuple unpacking on deep chains atsrc/apm_cli/commands/deps/cli.py:101
Noise in production (<0.1ms at D=20). If profiling ever shows this as a hot spot, switch the per-node frozenset to a mutable set with add/discard around the stack loop and use a mutable list for the path. No action needed now. - [nit]
branchesdict keys on growing tuples -- O(N*D) hashing, acceptable atsrc/apm_cli/commands/deps/cli.py:120
At N=100, D=10 that is ~1000 hash operations (microseconds). A flat integer id would be cheaper only if the tree grows to thousands of nodes. - [nit]
_is_nested_under_packageissues 2 stat() per ancestor level for each candidate -- O(N*D) but bounded by realistic layouts
Typical N<50 at depths 2-4 = ~200-400 stat() calls per scan (~0.2-2ms on warm VFS cache). No regression; the guard now applies to all matching candidates, widening the count slightly but still noise.
Doc Writer
- [recommended] fix(deps): preserve nested package manifests during prune #2092 changelog entry sat under released
[0.24.1]not[Unreleased]-- FOLDED atCHANGELOG.md:50
Keep-a-Changelog freezes dated release sections; unreleased changes belong under[Unreleased]. Resolved on the final head by relocating the entry (split into the data-loss/orphan fix and the deps-tree full-depth +(circular)beat). - [recommended] Merge duplicated fix(install): actionable hint when self-hosted GitLab host swallows subpath #2074 and fix: retain dev dependencies (supersedes #2042, closes #2033) #2102 entries inside
[0.24.1]-- FOLDED atCHANGELOG.md:53
Single-source changelog hygiene: the same fix must appear once. Resolved on the final head by dropping the merge-added duplicates so[0.24.1]again matches main byte-for-byte.
Test Coverage Expert
- [nit] PR-body Scenario Evidence row 2 + mutation block cited a stale test name -- FOLDED at
tests/unit/commands/test_deps_cli_helpers.py:163
Citedtest_nested_package_manifest_is_not_an_orphan; actual istest_nested_package_manifest_is_not_listed_independently. Corrected in both places in the PR body. - [nit]
test_transitive_chain_e2e.pylacked module-levelrequires_apm_binarypytestmark -- FOLDED attests/integration/test_transitive_chain_e2e.py:1
Addedpytestmark = pytest.mark.requires_apm_binary, matchingtest_intra_package_cleanup.pyand the tests.instructions.md convention.
Auth Expert -- inactive
PR touches deps scan/prune logic, reference docs, CHANGELOG, and unit/integration tests; no authentication, token management, credential resolution, AuthResolver, host classification, or remote-host fallback surface is modified.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
fix(deps): preserve nested package manifests during prune
TL;DR
Installed packages can contain nested
apm.ymlfiles that belong to the parent package. This change reuses the existing package-ancestry guard across dependency scans so nested manifests are not reported as top-level orphans and cannot be pruned independently.Important
Closes #2067.
Problem (WHY)
_scan_installed_packages()returned both_local/packageand_local/package/sub-package, even though the consumer lockfile contained only the parent (reported reproduction).apm pruneconsumed that scan result and deleted the nested source directory as an orphan (reported prune output).apm deps listapplied the nested-package guard only toSKILL.md-only directories, so a nestedapm.ymlremained visible as an orphan (reported root cause).The destructive and advisory paths must classify the same on-disk package tree consistently. The issue's expected contract is: "A directory that lives inside an already-installed package is part of that package, not a separate top-level dependency."
Approach (WHAT)
_is_nested_under_package()in the shared installed-package scanner.Implementation (HOW)
src/apm_cli/commands/deps/_utils.py- filters candidates whose ancestor already hasapm.yml, using the existing ancestry helper. The scanner still supports GitHub, ADO, and standalone subdirectory package layouts.src/apm_cli/commands/deps/cli.py- removes theSKILL.md-only restriction from both manual filesystem scans so nested package manifests and nested skill manifests follow one ownership rule.tests/unit/test_deps_utils.py- proves the shared scanner returns only_local/packagewhen a nested subpackage also hasapm.yml.tests/unit/commands/test_deps_cli_helpers.py- proves dependency listing omits the nested candidate from both installed and orphan classifications.No user-facing command, flag, manifest schema, or documented contract changed, so no documentation or README update is included.
Diagrams
Legend: the dashed stage is the reused guard that now protects every filesystem scan before orphan classification.
flowchart LR subgraph Scan[Filesystem scan] S1["Find apm.yml or package marker"] end subgraph Classify[Package ownership] C1["Check ancestor apm.yml"] C2["Keep top-level package"] C3["Skip nested package"] end subgraph Consume[Consumers] D1["apm deps list"] D2["apm deps tree"] P1["apm prune"] end S1 --> C1 C1 -->|"no package ancestor"| C2 C1 -->|"package ancestor exists"| C3 C2 --> D1 C2 --> D2 C2 --> P1 classDef new stroke-dasharray: 5 5; class C1 new;Trade-offs
apm deps listandapm pruneinconsistent.Benefits
apm.ymlproduces one installed-package entry instead of two.apm deps listno longer reports the nested subpackage as a direct orphan.apm pruneno longer receives the nested subpackage as a deletion candidate.Validation
Relevant unit and integration suites:
Full CI lint mirror
Mutation-break proof:
Scenario Evidence
tests/unit/test_deps_utils.py::TestScanInstalledPackages::test_nested_package_manifest_is_part_of_parent(regression-trap for #2067)apm deps listdoes not report the nested subpackage as an orphan.tests/unit/commands/test_deps_cli_helpers.py::TestResolveScopeDepsWithPackages::test_nested_package_manifest_is_not_listed_independently(regression-trap for #2067)tests/integration/test_virtual_package_orphan_detection.pyHow to test
apm_modules/_local/package/apm.ymlandapm_modules/_local/package/sub-package/apm.yml.apm deps listand confirm only_local/packageis listed.apm prune --dry-runand confirm_local/package/sub-packageis not a deletion candidate.Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com