fix(install): persist --skill filter to apm.yml (#1395)#1442
fix(install): persist --skill filter to apm.yml (#1395)#1442sergio-sisternes-epam wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Fixes a regression where apm install ... --skill ... computed a skill filter but failed to persist it into apm.yml, causing later integrations to deploy all skills in a bundle instead of the selected subset.
Changes:
- Thread
skill_subsetthrough_validate_and_add_packages_to_apm_yml()into_resolve_package_references()and ensure structuredapm.ymlentries are written when a subset is active. - Extend
InstallContext(and CLI->pipeline wiring) to carryskill_subsetand whether it originated from the CLI. - Add unit regression tests covering persistence behavior with and without
--skill.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
src/apm_cli/commands/install.py |
Wires skill_subset through validation/persistence and into the install pipeline context. |
tests/unit/commands/test_install_skill_subset.py |
Adds regression tests asserting apm.yml uses dict form with skills: when --skill is provided. |
tests/unit/commands/test_install_context.py |
Updates the InstallContext structural field contract to include the new fields. |
fa40f28 to
76a3b97
Compare
APM Review Panel:
|
| Persona | B | R | N | Takeaway |
|---|---|---|---|---|
| Python Architect | 0 | 2 | 1 | Clean bug fix with correct dataclass extension; normalization should be hoisted out of the per-package loop to avoid redundant work. |
| CLI Logging Expert | 0 | 1 | 1 | Skill-subset normalization is entirely silent; users get no feedback when names are dropped or when apm.yml entry shape changes. |
| DevX UX Expert | 0 | 2 | 1 | Skill-subset persistence restores install-idempotency; replace-vs-merge semantics for repeated --skill should be documented. |
| Supply Chain Security Expert | 0 | 1 | 0 | Read path validates skill names; write path does not. No exploit on current code paths but defense-in-depth missing. |
| OSS Growth Hacker | 0 | 1 | 1 | Community-contributed regression fix is well-structured; add CHANGELOG entry crediting @sergio-sisternes-epam to compound contributor-funnel signal. |
| Test Coverage Expert | 0 | 1 | 1 | Unit coverage is thorough (33 tests pass on persistence helpers); live integration test exists, but no hermetic integration test exercises the install->persist round-trip without network. |
B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.
Top 5 follow-ups
- [Test Coverage Expert] Add a hermetic (non-@LiVe) integration test exercising install --skill -> apm.yml round-trip with a local bare git fixture. -- The PR's unit tests mock the persistence boundary they claim to prove; a hermetic integration test closes the regression-trap gap without requiring network or GITHUB_APM_PAT.
- [OSS Growth Hacker] Add CHANGELOG entry under [Unreleased] Fixed crediting @sergio-sisternes-epam for [BUG] #1395. -- Named credit in CHANGELOG converts merge into a visible contributor-funnel signal; cheap win that compounds repeat contributions.
- [DevX UX Expert] Document replace-vs-merge semantics in --skill help text so users know re-running with a different list overwrites. -- No prior art in package managers for subset selectors; without documentation, users expecting merge semantics will be surprised when the previous subset vanishes.
- [CLI Logging Expert] Emit a warning when normalization silently drops empty or duplicate skill names from --skill input. -- A typo'd skill name currently vanishes without trace; one diagnostic line would catch user errors before they persist bad state to apm.yml.
- [Supply Chain Security Expert] Add write-path validation (validate_skill_name or validate_path_segments) in the normalization loop before persisting to apm.yml. -- Defense-in-depth: reject malformed skill names at ingestion time rather than relying solely on the read-path parser to catch traversal sequences on next install.
Architecture
classDiagram
direction LR
class InstallContext {
<<Dataclass>>
+skill_subset tuple
+skill_subset_from_cli bool
}
class DependencyReference {
<<ValueObject>>
+skill_subset list
+to_apm_yml_entry()
}
class install {
<<CLIEntryPoint>>
}
class _validate_and_add_packages_to_apm_yml
class _resolve_package_references
class _install_apm_packages
install ..> _validate_and_add_packages_to_apm_yml : passes skill_subset
install ..> InstallContext : constructs
_validate_and_add_packages_to_apm_yml ..> _resolve_package_references : forwards
_resolve_package_references ..> DependencyReference : mutates skill_subset
_install_apm_packages ..> InstallContext : reads
class InstallContext:::touched
class _resolve_package_references:::touched
class _validate_and_add_packages_to_apm_yml:::touched
classDef touched fill:#fff3b0,stroke:#d47600
flowchart TD
A["install() L1312: _skill_subset = tuple(skill_names)"] --> B["_validate_and_add_packages_to_apm_yml(skill_subset)"]
B --> C["_resolve_package_references(skill_subset)"]
C --> D["for package in packages"]
D --> E["resolve_parsed_dependency_reference -> dep_ref"]
E --> F{"skill_subset truthy?"}
F -->|Yes| G["Normalize strip dedupe; dep_ref.skill_subset = normalized"]
F -->|No| H["dep_ref.skill_subset unchanged"]
G --> I{"marketplace or gitlab?"}
H --> I
I -->|Yes| J["_apm_yml_entries set via dependency_reference_to_yaml_entry"]
I -->|No| K{"_validate_package_exists"}
K -->|Pass| L{"skill_subset and canonical not in entries?"}
L -->|Yes| M["Safety branch L489: _apm_yml_entries[canonical] = to_apm_yml_entry"]
L -->|No| N["fallback plain canonical string"]
J --> O["_merge_packages_into_yml writes apm.yml"]
M --> O
N --> O
A --> P["InstallContext(skill_subset, skill_subset_from_cli)"]
P --> Q["_install_apm_packages reads ctx"]
Recommendation
Merge this PR promptly -- it restores a tier-1 install-fidelity promise broken since v0.11.0, is well-tested at the unit boundary, and carries zero blocking findings from six active panelists. Track the hermetic integration test as the highest-signal follow-up (prevents future regression-trap drift on the persistence contract); CHANGELOG credit can land in the merge commit or a fast-follow.
Full per-persona findings
Python Architect
- [recommended] Normalization block re-executes identically on every loop iteration at
src/apm_cli/commands/install.py:446
skill_subset is invariant across the for-package loop in_resolve_package_references. The strip/dedupe/filter logic runs N times producing the same _normalized list each time. O(packages * skills) wasted work and clutters the hot loop with input-sanitization concerns that belong at the call boundary.
Suggested: Normalize once before the for-package loop, or normalize at L1317 when_skill_subsetis first constructed from CLI args. - [recommended] Safety branch at L489 papers over missing unified
_apm_yml_entriespopulation atsrc/apm_cli/commands/install.py:489
The normal (github, non-insecure) path never sets an entry, relying on_merge_packages_into_ymlfalling back to the plain canonical string. The new safety branch adds a fourth conditional site -- chain-of-special-cases anti-pattern. Future-state: unconditionally call_apm_yml_entries[canonical] = dep_ref.to_apm_yml_entry()after dep_ref is fully configured.
Suggested: Track refactor: unconditionally populate_apm_yml_entries[canonical] = dep_ref.to_apm_yml_entry()once per resolved dep_ref. - [nit] Type annotation: prefer
tuple[str, ...]overbuiltins.tuple[str, ...]atsrc/apm_cli/commands/install.py:229
PEP 585 allowstuple[str, ...]directly. The codebase already usesstr | None(PEP 604).builtins.tupleis unconventional.
Suggested:skill_subset: tuple[str, ...] | None = None
CLI Logging Expert
- [recommended] Silent drop of empty/duplicate skill names violates Name-the-thing and Include-the-fix rules at
src/apm_cli/commands/install.py:446
When the user passes--skill ' ,skill-a,skill-a', the normalization silently strips and dedupes. NoDiagnosticCollectorentry, no_rich_warning. A typo'd skill name vanishes without trace. Message-writing rules require the CLI to name the alteration and how to inspect it (--verbose).
Suggested: After the normalization loop, comparelen(skill_subset)vslen(_normalized). If fewer: emit[!] Dropped N empty/duplicate skill name(s); use --verbose to list.In--verbose, list the dropped values. - [nit] No verbose-only info line signals apm.yml entry promotion from string to dict form at
src/apm_cli/commands/install.py
Progressive disclosure: when apm.yml goes from'org/repo'to{git, skills:[...]}, a one-line verbose info aids users diffing their manifest.
DevX UX Expert
- [recommended] Replace-vs-merge semantics for repeated
--skillinvocations are undocumented atsrc/apm_cli/commands/install.py:1038
Code doesdep_ref.skill_subset = _normalized(overwrite, npm-like). But--skillresembles cargo--featureswhich MERGES. No prior art in package managers for a subset selector; a user who runsinstall --skill footheninstall --skill barwill be surprised that foo was dropped.
Suggested: Amend--skillhelp text: "Re-running with a different --skill list REPLACES the previous subset. Use --skill * (or omit --skill) to reset to all skills." - [recommended] No CLI feedback when
--skilloverwrites an existing subset in apm.yml
When apm.yml recordsskills:[foo]and the user runsinstall --skill bar, the subset is silently replaced. Mental model: install should acknowledge mutations. EmitUpdated skill subset for org/repo: [foo] -> [bar].
Suggested: One-line info message on subset overwrite. Implementation overlaps with cli-logging-expert silent-drop finding. - [nit] Help example uses
--skillbefore package (positional order) atsrc/apm_cli/commands/install.py:1125
npm/pip/cargo always put the package first in examples. Reordering toapm install org/bundle --skill my-skillreinforces natural reading order.
Supply Chain Security Expert
- [recommended] Write-path normalization does not call
validate_skill_name/validate_path_segmentsbefore persisting to apm.yml atsrc/apm_cli/commands/install.py:449
The read-path parser atreference.py:657callsvalidate_path_segmentsand rejects traversal sequences, so currently a malicious--skill ../../etc/passwdwould be caught when re-reading the manifest._promote_sub_skillsuses skill names as set-membership filters against on-disk directory names, so path construction is safe. However defense-in-depth says: reject bad input at ingestion (write time), not only at consumption.
Suggested: Inside the normalization loop, callvalidate_skill_name(s)(or at minimumvalidate_path_segments(s, context='--skill')) and raise a clear ValueError before persisting to apm.yml.
OSS Growth Hacker
- [recommended] Missing CHANGELOG entry under [Unreleased] Fixed for [BUG] #1395 at
CHANGELOG.md
Named credit in CHANGELOG converts merge into a visible signal that community PRs ship fast; pattern from httpie/bun/uv compounds repeat contributions and external mentions.
Suggested: Add under [Unreleased] Fixed:- Install: --skill subset filter is now persisted to apm.yml across reinstalls, fixing a regression since v0.11.0. (#1395, thanks @sergio-sisternes-epam) - [nit] Release-note angle: frame as install-idempotency promise restored
Install round-trip idempotency is a tier-1 package-manager credibility promise. The release narrative can use "install fidelity restored: your --skill filters now survive reinstalls" as a repostable line.
Test Coverage Expert
- [recommended] No hermetic integration test exercises install --skill -> apm.yml persistence without live network at
tests/integration/test_skill_bundle_live.py:358
Install pipeline surface floor is integration-with-fixtures.tests/integration/test_skill_bundle_live.py::test_skill_subset_persists_to_apm_ymldoes the full round-trip but is@pytest.mark.live(needs GITHUB_APM_PAT).tests/unit/test_skill_subset_persistence.pycoversto_apm_yml_entryandset_skill_subset_for_entryat unit tier with real objects. The PR's newtest_install_skill_subset.pymocks_validate_package_exists,resolve_parsed_dependency_reference, andto_apm_yml_entryitself -- it validates wiring but if realto_apm_yml_entryregressed, these mocked tests would still pass.
Suggested: Add a non-@LiVe integration test intests/integration/that sets up a local bare git repo fixture, runs_validate_and_add_packages_to_apm_ymlwithskill_subset=['foo'], and asserts on-disk apm.yml contains{git: 'owner/repo', skills: ['foo']}.
Proof (unknown at):tests/integration/test_skill_bundle_live.py::test_skill_subset_persists_to_apm_yml-- proves: apm install --skill persists the skills: field to apm.yml on disk after a real install [devx,portability-by-manifest]
assert isinstance(entry, dict); assert 'skills' in entry; assert target_skill in entry['skills'] - [nit] PR's new
test_install_skill_subset.pymocks the persistence boundary it claims to test attests/unit/commands/test_install_skill_subset.py
5 tests in the new file mockdep_ref.to_apm_yml_entryvia a lambda + mock the validation/resolve boundary. They prove orchestration wiring but not emitted YAML shape. Real persistence proof lives intest_skill_subset_persistence.py(33 unit tests passing on real DependencyReference). Tier gap is covered elsewhere; informational only.
Proof (passed):tests/unit/test_skill_subset_persistence.py::TestDependencyReferenceSkillSubset::test_round_trip_parse_emit-- proves: DependencyReference round-trips skill_subset through to_apm_yml_entry and parse_from_dict correctly [devx,portability-by-manifest]
ref2 = DependencyReference.parse_from_dict(emitted); assert ref2.skill_subset == ['cli', 'web']
Auth Expert -- inactive
PR touches only src/apm_cli/commands/install.py; no AuthResolver, token_manager, github_downloader, or remote-host classification code paths changed.
Doc Writer -- inactive
No README, CHANGELOG, MANIFESTO, docs/src/content/docs/, .apm/skills/, or instruction files changed in the diff.
Performance Expert -- inactive
PR touches src/apm_cli/commands/install.py only; no cache/, deps/, install/phases/, install/pipeline.py, install/resolve.py changes; no perf claim in PR body.
This panel is advisory. It does not block merge. Re-apply the panel-review label after addressing feedback to re-run.
Folds 6 RECOMMENDED follow-ups from the apm-review-panel pass on PR #1442 without changing the core regression fix. Test coverage (top-priority follow-up): - Add tests/integration/test_install_skill_subset_hermetic.py: hermetic end-to-end coverage of the install --skill -> apm.yml round-trip using REAL DependencyReference (no network, no mocked to_apm_yml_entry). Closes the regression-trap gap the existing unit tests had (they mocked the persistence boundary they claimed to prove). Python architecture: - Extract _normalize_skill_subset into apm_cli/install/skill_subset.py so the per-package normalization loop runs ONCE per invocation (was O(packages * skills)) and install.py stays within its LOC budget. - Use PEP 585 'tuple[str, ...]' on InstallContext.skill_subset. Supply-chain security (defense-in-depth): - Call validate_path_segments(name, context='--skill <name>') on the write path. The read-path parser already rejects traversal at parse_from_dict; this guarantees a poisoned skills: entry never reaches apm.yml even if a future regression bypasses the parser. CLI logging UX: - Emit a warning when --skill input has empty or duplicate names that were silently dropped; list them with --verbose. DevX UX: - Document replace-vs-merge semantics in --skill help text. - Reorder docstring example to put the package first (npm/pip/cargo convention). OSS growth: - CHANGELOG entry under [Unreleased] Fixed crediting the original author. Deferred (separable refactors, not folded): - Unconditional _apm_yml_entries population (cross-cutting structural change to four conditional sites). - Verbose info on entry shape promotion (nit, low-signal polish). - Diff-and-announce on --skill overwrite (needs apm.yml read-and-compare). Co-authored-by: Sergio Sisternes <sergio_sisternes@epam.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Folds 6 RECOMMENDED follow-ups from the apm-review-panel pass on PR #1442 without changing the core regression fix. Test coverage (top-priority follow-up): - Add tests/integration/test_install_skill_subset_hermetic.py: hermetic end-to-end coverage of the install --skill -> apm.yml round-trip using REAL DependencyReference (no network, no mocked to_apm_yml_entry). Closes the regression-trap gap the existing unit tests had (they mocked the persistence boundary they claimed to prove). Python architecture: - Extract _normalize_skill_subset into apm_cli/install/skill_subset.py so the per-package normalization loop runs ONCE per invocation (was O(packages * skills)) and install.py stays within its LOC budget. - Use PEP 585 'tuple[str, ...]' on InstallContext.skill_subset. Supply-chain security (defense-in-depth): - Call validate_path_segments(name, context='--skill <name>') on the write path. The read-path parser already rejects traversal at parse_from_dict; this guarantees a poisoned skills: entry never reaches apm.yml even if a future regression bypasses the parser. CLI logging UX: - Emit a warning when --skill input has empty or duplicate names that were silently dropped; list them with --verbose. DevX UX: - Document replace-vs-merge semantics in --skill help text. - Reorder docstring example to put the package first (npm/pip/cargo convention). OSS growth: - CHANGELOG entry under [Unreleased] Fixed crediting the original author. Deferred (separable refactors, not folded): - Unconditional _apm_yml_entries population (cross-cutting structural change to four conditional sites). - Verbose info on entry shape promotion (nit, low-signal polish). - Diff-and-announce on --skill overwrite (needs apm.yml read-and-compare). Co-authored-by: Sergio Sisternes <sergio_sisternes@epam.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
151c880 to
b48f073
Compare
Panel follow-ups folded (advisory shepherd)Folded 8 of 11 RECOMMENDED follow-ups from the apm-review-panel into commit Folded
Normalization extracted to Deferred (separable, opened-for-follow-up rather than gated here)
Evidence
This comment is advisory. Maintainer + author retain merge authority. |
cd9cf35 to
abb0662
Compare
The --skill CLI argument was computed but never passed to the apm.yml persistence layer. Wire skill_subset through _validate_and_add_packages_to_apm_yml and _resolve_package_references so DependencyReference.to_apm_yml_entry() emits the dict form with skills: key. Also pass skill_subset to _install_apm_dependencies so the integration pipeline honours the filter. Added skill_subset/skill_subset_from_cli fields to InstallContext so the values flow through _install_apm_packages without needing a separate parameter on every function boundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
abb0662 to
7e3a000
Compare
Description
When installing a package with
--skillto select a subset of skills, the filter was computed but never threaded into the apm.yml persistence layer. The_skill_subsetvariable was effectively dead code —DependencyReference.to_apm_yml_entry()already handlesskill_subsetcorrectly (emitting{"git": "...", "skills": [...]}), butdep_ref.skill_subsetwas never populated.Fixes
Fixes #1395
Type of change
Changes
_resolve_package_references— addedskill_subsetparameter; setsdep_ref.skill_subsetand populates_apm_yml_entriesviato_apm_yml_entry()_validate_and_add_packages_to_apm_yml— forwardsskill_subsetto resolveInstallContext— addedskill_subsetandskill_subset_from_clifields to carry the filter through the pipeline_skill_subsetinto both the validate and install pathsTesting
test_install_skill_subset.py(with/without --skill)