fix(deps): support meta-packages and apm.yml inside collections/ subpaths#1097
Conversation
There was a problem hiding this comment.
Pull request overview
This PR fixes dependency resolution and validation for monorepo subpaths by removing the /collections/ path-segment heuristic, making virtual-package classification extension-based, and introducing a first-class META_PACKAGE type for dependency-only apm.yml packages.
Changes:
- Make virtual package type detection extension-only (explicit
.collection.yml/.yaml=>COLLECTION; otherwiseSUBDIRECTORY) and adjust naming/install-path expectations accordingly. - Add
PackageType.META_PACKAGEand validation support for dependency-onlyapm.ymlpackages with no.apm/. - Update GitHub downloader validation/probing and add a legacy fallback dispatcher so implicit
collections/<name>can still resolve to a sibling<name>.collection.yml.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/test_script_runner.py | Updates virtual collection ref in script-runner tests to explicit .collection.yml. |
| tests/unit/test_package_identity.py | Aligns install-path expectations with new extension-only classification; adds SUBDIRECTORY natural-layout test. |
| tests/unit/test_meta_package.py | Adds unit coverage for META_PACKAGE detection and validation behavior. |
| tests/unit/test_generic_git_urls.py | Updates Bitbucket virtual collection parsing test to explicit .collection.yml. |
| tests/unit/test_artifactory_support.py | Updates Artifactory virtual collection parsing test to explicit .collection.yml. |
| tests/unit/test_ado_path_structure.py | Updates ADO virtual collection parsing tests to explicit .collection.yml and adjusts expected virtual_path. |
| tests/unit/deps/test_github_downloader_validation.py | Adds regression tests for probe order, explicit extension handling, and legacy collection fallback routing. |
| tests/test_apm_package_models.py | Updates model parsing tests for explicit collection refs and new SUBDIRECTORY semantics for implicit /collections/<name>. |
| tests/integration/test_collection_install.py | Adjusts integration parsing expectations for implicit vs explicit collection refs under the new rules. |
| src/apm_cli/models/validation.py | Introduces META_PACKAGE, detection helper, and meta-package validation path. |
| src/apm_cli/models/dependency/reference.py | Implements extension-only virtual type classification and updates virtual-path acceptance and naming. |
| src/apm_cli/install/sources.py | Adds user-facing label for META_PACKAGE. |
| src/apm_cli/deps/github_downloader_validation.py | Fixes COLLECTION probe double-extension and reorders SUBDIRECTORY probes to prioritize apm.yml. |
| src/apm_cli/deps/github_downloader.py | Adds _is_legacy_collection_fallback dispatcher and relaxes collection downloader preconditions to support legacy SUBDIRECTORY refs. |
Comments suppressed due to low confidence (1)
src/apm_cli/models/dependency/reference.py:662
- The InvalidVirtualPackageExtensionError message only lists
VIRTUAL_FILE_EXTENSIONS, but this code path now also recognizes explicit collection manifests (.collection.yml/.collection.yaml). If a user typos a collection URL (e.g.,.collection.ym), the error currently suggests only the file extensions, which is misleading. Please update the message to mention the recognized collection-manifest extensions (or list the combined recognized extensions) so users get an actionable hint.
# Accept any path ending in a recognised virtual extension
# (file or collection-manifest). Reject other dotted final
# segments so typos like `prompts/file.txt` fail fast instead
# of silently mis-classifying as a subdirectory.
recognised_exts = cls.VIRTUAL_FILE_EXTENSIONS + cls.VIRTUAL_COLLECTION_EXTENSIONS
if any(virtual_path.endswith(ext) for ext in recognised_exts):
pass
else:
last_segment = virtual_path.split("/")[-1]
if "." in last_segment:
raise InvalidVirtualPackageExtensionError(
f"Invalid virtual package path '{virtual_path}'. "
f"Individual files must end with one of: {', '.join(cls.VIRTUAL_FILE_EXTENSIONS)}. "
f"For subdirectory packages, the path should not have a file extension."
)
|
Collection is legacy. This should be solved by transitive steps (even local) which APM already resolves. We should remove collections support |
Got it — happy to go this direction. One scope question before I rework: drop the new collection-related code I added (the The latter would be a breaking change for anyone with |
…aths apm install rejected meta-package layouts in two ways: paths containing /collections/ hard-routed to the .collection.yml parser even when an apm.yml lived inside, and a dependency-only apm.yml without a .apm/ directory failed validation. Both stem from the same architectural error: package type was inferred from path segments rather than content. Drop the /collections/ path heuristic in virtual_type in favor of extension-only classification (.collection.yml signals COLLECTION; everything else is SUBDIRECTORY, resolved at fetch time). Probe order in validate_virtual_package_exists puts apm.yml before the legacy .collection.yml fallback so a fixed-but-mis-routed collections/<name>/ apm.yml is recognised correctly. Recognise META_PACKAGE as a first-class PackageType so dependency-only aggregators no longer need an empty .apm/.gitkeep placeholder. Legacy /collections/<name> URLs that resolve to a sibling .collection.yml still work via a content-only fallback in the dispatcher. Fixes microsoft#1094
Address review feedback:
* Require `dependencies.{apm,mcp}` (and `devDependencies.{apm,mcp}`)
to be a non-empty list before classifying a package as META_PACKAGE.
A truthy non-list value (e.g. `apm: "foo"` or `apm: {}`) is malformed
per the schema; treat it as INVALID so the legacy diagnostic still
fires instead of silently reclassifying. Adds two regression tests.
* Update `validate_apm_package` docstring to list META_PACKAGE among
the supported package types.
* Update user-facing docs (manifest-schema.md, dependencies.md) to
describe the extension-only classification rule and remove the old
"Collection (dir)" entry; mention the meta_package value in the
apm.lock.yaml `package_type` enumeration.
…y apm.yml (microsoft#1094) Reworks PR microsoft#1097 per the apm-review-panel synthesis. The conceptual fix for issue microsoft#1094 is a one-line guard relaxation in `_validate_apm_package_with_yml`: a curated dependency aggregator (apm.yml with declared deps and no .apm/) is now a valid APM_PACKAGE, removing the need for users to commit an empty `.apm/.gitkeep`. The original PR also introduced a parallel `.collection.yml` / `VirtualPackageType.COLLECTION` codepath that overlapped with the existing SUBDIRECTORY virtual-package handling and added a latent production bug (`download_virtual_collection_package` was referenced from `script_runner.py` but never existed on the downloader). The panel agreed: delete the collection-virtual-package machinery wholesale, since the relaxed validation makes it redundant. Lockfiles are unaffected -- the COLLECTION enum value was never persisted. Changes: - Validation cascade collapses to: any apm.yml -> APM classification. With .apm/ OR declared dependencies, APM_PACKAGE; otherwise INVALID (preserving the standard "missing .apm/" diagnostic). The dep-list detection requires at least one parseable str/dict entry to guard against malformed manifests like `apm: [123]`. - DependencyReference.parse() now raises ValueError at parse time for any URL ending in `.collection.yml` / `.collection.yaml`, with a migration message pointing at the dependencies guide. - Removed: VirtualPackageType.COLLECTION, is_virtual_collection(), download_collection_package, normalize_collection_path, _is_legacy_collection_fallback, the collection probe in github_downloader_validation, collection_parser.py, the META_PACKAGE install-source label. - Docs: manifest-schema.md, dependencies.md, CHANGELOG.md updated with migration callouts. - Tests: 178 new lines covering dep-only detection (incl. malformed shapes), 6 regression-trap tests for the migration error path, 6 test files adapted to the SUBDIRECTORY-only world; integration test_collection_install.py and unit test_meta_package.py removed. Closes microsoft#1094. Co-authored-by: xuyuanhao <aa9736195201@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
42c2edf to
f1b8109
Compare
Rework: drop collection-virtual-package, accept dep-only
|
| Layer | Change |
|---|---|
| Validation cascade | apm.yml present -> APM classification. With .apm/ OR declared deps, APM_PACKAGE. Otherwise INVALID (preserves the standard "missing .apm/" diagnostic). |
| Dep-list detection | _apm_yml_declares_dependencies requires at least one parseable str/dict entry under dependencies.{apm,mcp} or devDependencies.{apm,mcp} -- rejects malformed shapes like apm: [123]. |
| Parser | DependencyReference.parse() raises ValueError at parse time for any URL ending in .collection.yml / .collection.yaml, with a migration message. |
| Surface deletion | VirtualPackageType.COLLECTION, is_virtual_collection(), download_collection_package, normalize_collection_path, _is_legacy_collection_fallback, the collection probe in github_downloader_validation, collection_parser.py, and the META_PACKAGE install-source label all gone. |
Implementation (HOW)
src/apm_cli/models/validation.py-- cascade collapsed to 7 steps (was 8). New_apm_yml_declares_dependencies()helper._validate_apm_package_with_yml.apm/guard relaxed to accept dep-only.src/apm_cli/models/dependency/reference.py-- newREMOVED_COLLECTION_EXTENSIONSconstant, parse-timeValueError,is_virtual_collection()deleted. Thehas_collection = "collections" in path_segmentsheuristic is preserved on purpose: it letsgitlab.com/owner/repo/collections/foo(no extension) parse as aSUBDIRECTORYvirtual package on generic hosts.src/apm_cli/models/dependency/types.py--VirtualPackageType.COLLECTIONenum value removed.src/apm_cli/deps/github_downloader.py--normalize_collection_path,download_collection_package,_is_legacy_collection_fallbackdeleted; dispatcher collapsed toif is_virtual_file(): file else: subdirectory(with Artifactory modes).src/apm_cli/deps/github_downloader_validation.py--is_virtual_collection()probe block deleted;marker_pathscleaned up (apm.yml first, then SKILL.md / plugin.json / README.md).src/apm_cli/core/script_runner.py-- collection branch deleted (also kills the latentdownload_virtual_collection_packageAttributeError).src/apm_cli/install/sources.py--META_PACKAGElabel entry deleted.src/apm_cli/integration/skill_integrator.py-- comment cleanup.src/apm_cli/deps/collection_parser.py-- entire file deleted.- Docs --
manifest-schema.md,dependencies.md,CHANGELOG.mdupdated with migration callouts.
Diagrams
Validation cascade after the rework -- the dep-only branch is the single new edge:
flowchart TD
A[detect_package_type] --> B{plugin.json or .claude-plugin/?}
B -- yes --> P[MARKETPLACE_PLUGIN]
B -- no --> C{root SKILL.md + apm.yml?}
C -- yes --> H[HYBRID]
C -- no --> D{root SKILL.md only?}
D -- yes --> CS[CLAUDE_SKILL]
D -- no --> E{nested skills/x/SKILL.md?}
E -- yes --> SB[SKILL_BUNDLE]
E -- no --> F{apm.yml present?}
F -- yes --> G{.apm/ OR declared deps?}
G -- yes --> AP[APM_PACKAGE]
G -- no --> I1[INVALID]
F -- no --> J{hooks/*.json?}
J -- yes --> HP[HOOK_PACKAGE]
J -- no --> I2[INVALID]
Trade-offs
- Breaking change to URL surface. Any user who pinned
.collection.ymlURLs inapm.ymlwill hit the migrationValueError. Mitigation: the error names the offending path and links to the dependencies guide. Lockfile is unaffected -- the COLLECTION enum was never persisted, so existingapm.lock.yamlfiles keep working. - Two-step migration for collection authors. They convert
.collection.ymlto anapm.ymlwithdependencies:AND change the URL form. The accepted alternative -- silent translation of.collection.yml->apm.yml-- was rejected because it would resurrect the parallel codepath this PR removes. - The
has_collection = "collections" in path_segmentsheuristic survives. It is parsing convenience for generic-host SUBDIRECTORY URLs, not a type classification. Removing it would breakgitlab.com/owner/repo/collections/foo-style URLs.
Validation
$ uv run --extra dev ruff check src/ tests/ && uv run --extra dev ruff format --check src/ tests/
All checks passed!
620 files already formatted
$ uv run --extra dev pytest tests/unit tests/test_*.py --ignore=tests/unit/test_audit_report.py
3 failed, 7513 passed, 2 skipped
Note
The 3 failures (test_resolve_git_reference_commit, test_clone_fallback_respects_enterprise_host, test_credential_fill_used_when_no_env_token) reproduce on origin/main as well -- they are environment-specific git-auth tests, unrelated to this diff.
Scenario evidence
| User promise | Test |
|---|---|
Dep-only apm.yml (no .apm/) installs without .gitkeep workaround |
tests/unit/test_dep_only_package.py::TestDepOnlyPackageDetection (12 tests) |
Validation still rejects malformed apm.yml (e.g. apm: [123]) |
tests/unit/test_dep_only_package.py::test_apm_list_with_only_non_parseable_entries_is_invalid (and mcp / devDependencies siblings) |
.collection.yml URLs surface a clear migration error at parse time |
tests/unit/test_collection_migration_error.py (6 tests) |
Implicit collections/<name> paths still resolve correctly as SUBDIRECTORY |
tests/unit/test_package_identity.py::test_collections_path_subdirectory_uses_natural_layout, test_ado_virtual_collection_subdirectory |
Existing FILE virtual packages (prompts/*.prompt.md, agents/*.agent.md) unaffected |
tests/test_github_downloader.py::test_yaml_with_colon_in_* |
How to test
git checkout fix/issue-1094-collections-meta-packagefrom edenfunf's fork.uv run --extra dev ruff check src/ tests/ && uv run --extra dev ruff format --check src/ tests/-- both silent.uv run --extra dev pytest tests/unit tests/test_*.py --ignore=tests/unit/test_audit_report.py-- expect 7513 pass plus the 3 pre-existing env failures noted above.- Author a
tmp/apm.ymlwith onlydependencies.apm: [microsoft/apm-sample-package]and no.apm/; runapm install-- expect success (was failing onmain). - Add a dep entry like
owner/repo/collections/foo.collection.ymlto anyapm.ymland runapm install-- expect a clearValueErrorpointing at the dependencies guide.
Per test-coverage-expert review: add the missing integration + CLI seam tests for the dep-only apm.yml fix and .collection.yml migration error. - tests/integration/test_apm_dependencies.py: test_dep_only_project_installs_dependencies_without_dot_apm -- end-to-end proof against real microsoft/apm-sample-package that a root project with declared deps but NO .apm/ resolves, downloads, and integrates without the .gitkeep workaround. Closes the microsoft#1094 user promise at the install-pipeline seam (unit tests only covered the detection layer). - tests/unit/test_collection_migration_error.py: TestCollectionMigrationErrorPropagation -- 2 tests asserting the parse-time ValueError survives the re-wrap inside APMPackage._parse_dependency_dict for both dependencies.apm and devDependencies.apm. The actionable migration text reaches the install caller intact. - tests/unit/test_install_command.py: test_install_collection_yml_argument_surfaces_migration_message -- CLI-argument seam: `apm install owner/repo/.../foo.collection.yml` routes the parse-time ValueError through _resolve_package_references -> invalid_outcomes -> "All packages failed validation" with the migration text in the user-visible output. Verified locally: all 3 new tests pass (integration test runs against real GitHub). Full sweep: 7516 passed, 2 skipped, 3 pre-existing env failures unrelated to this diff. Co-authored-by: xuyuanhao <aa9736195201@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Follow-up: test coverage gaps closedPushed Why the trailer reads
|
| Gap | Test added | Why it matters |
|---|---|---|
Dep-only apm.yml end-to-end (no .apm/ on root) |
tests/integration/test_apm_dependencies.py::test_dep_only_project_installs_dependencies_without_dot_apm |
Real install against microsoft/apm-sample-package proves the entire #1094 user story works through the install pipeline -- not just the detection layer. Picked up by scripts/test-integration.sh in CI. |
.collection.yml migration error survives APMPackage.from_apm_yml re-wrap |
tests/unit/test_collection_migration_error.py::TestCollectionMigrationErrorPropagation (2 tests, dependencies.apm + devDependencies.apm) |
The parse-time ValueError is re-raised with a "Invalid APM dependency '...': " prefix. Without this trap, a future refactor could strip the actionable migration text and no test would notice. |
apm install owner/repo/.../foo.collection.yml CLI-argument path |
tests/unit/test_install_command.py::test_install_collection_yml_argument_surfaces_migration_message |
The CLI surfaces the migration message via _resolve_package_references -> invalid_outcomes -> "All packages failed validation". Without this trap, that catch-and-surface chain could be silently swallowed by a future refactor. |
Verification
$ uv run --extra dev ruff check src/ tests/ && uv run --extra dev ruff format --check src/ tests/
All checks passed!
620 files already formatted
$ uv run --extra dev pytest tests/unit tests/test_*.py --ignore=tests/unit/test_audit_report.py
3 failed, 7516 passed, 2 skipped (3 pre-existing env failures, unrelated)
$ GITHUB_APM_PAT=$(gh auth token) uv run --extra dev pytest \
tests/integration/test_apm_dependencies.py::TestAPMDependenciesIntegration::test_dep_only_project_installs_dependencies_without_dot_apm -m integration
1 passed in 1.79s
The integration test fetches microsoft/apm-sample-package over real network to a temp dir, asserts not (test_dir / ".apm").exists() as a precondition, then runs the actual APMPackage.from_apm_yml + GitHubPackageDownloader.download_package chain. That's the closest we can get to "it works for users" without spinning up a fixture repo.
…icrosoft#1094) Adds a boundary integration test for the INVALID leaf of the validation cascade (apm.yml present, no .apm/, no declared deps). The test exposed a UX gap: the cascade-exit error message at validation.py:368-373 did not mention the dep-only escape hatch added in this PR, so users hitting the fence would not discover that declaring dependencies in apm.yml is now a valid alternative to .apm/. Updates the cascade-exit message to surface all three valid recovery paths (add .apm/, declare dependencies, add SKILL.md), keeping it consistent with the validator-level message at validation.py:747-752. Co-authored-by: xuyuanhao <aa9736195201@gmail.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cascade boundary coverage closed + UX fix surfacedFollowing on from the test-coverage audit (previous comment), I ran the test-coverage-expert persona against all 9 leaves of the validation cascade diagram. The persona's ruthless prioritization identified one remaining high-value gap: the INVALID negative boundary (apm.yml present, no Closing it surfaced a real UX bug. What landed in
|
| Leaf | Outcome |
|---|---|
| MARKETPLACE_PLUGIN | Covered by units (deferred) |
| HYBRID | Silent-drift candidate — follow-up issue |
| CLAUDE_SKILL | Covered by units (deferred) |
| SKILL_BUNDLE | Real-GH live test exists |
APM_PACKAGE w/ .apm/ |
Real-GH integration covered |
| APM_PACKAGE dep-only | Just added in 65e8a796 |
INVALID (apm.yml + no .apm/ + no deps) |
Just added in 56688ff2 (this commit) |
| HOOK_PACKAGE | Covered by units — follow-up |
| INVALID (empty) | Covered by units (deferred) |
Verification
- New integration test:
PASSEDagainst realmicrosoft/apm-sample-package(1.81s) - Existing
test_dep_only_project_installs_dependencies_without_dot_apm:PASSED - Lint contract: clean
- Full unit sweep:
7516 passed(3 pre-existing env failures unrelated)
Co-authored with @edenfunf via xuyuanhao <aa9736195201@gmail.com> trailer.
The panelist-return-schema's evidence.outcome had no tier dimension, so a unit-tier 'passed' and an integration-tier 'passed' were indistinguishable to the CEO synthesizer. This let the test-coverage-expert silently certify critical user-promise surfaces (install pipeline, auth resolution, lockfile determinism, hooks, marketplace download) on the basis of unit tests with mocked boundaries plus a ruff lint pass -- the exact failure mode that surfaced on PR #1097, where a unit-only 'passed' could have shipped the cascade-exit rework without any fixture-grade proof of the new dep-only escape hatch. Diagnosed via the genesis skill. Root cause is a PROSE Reduced Scope violation: one outcome axis collapsing cheap proof (unit, mocks at boundary) with expensive proof (integration with real fixtures). Persona body fused two lenses (PRESENCE + TIER) but only PRESENCE leaked into the schema, so TIER advice was silently dropped at synthesis. Schema: - evidence.tier required, enum {unit, integration-with-fixtures, e2e, manual-only, static}. - evidence.run_evidence optional, captures pytest invocation + pass/fail line + duration when an integration test was actually executed in-session. - outcome and evidence descriptions updated to spell out tier semantics (passed at tier X certifies tier X only, not tiers above it). Persona (test-coverage-expert): - New 'Tier floor by surface' matrix mapping 8 critical-promise surfaces to a minimum tier floor. CLI surface, install pipeline, lockfile determinism, auth resolution, hook execution, marketplace download + integrity, and cross-module integration all require integration-with-fixtures as the floor; only error-wording string-shape stays at unit. - TWO evidence rows on sub-floor coverage: when only unit coverage exists for a surface above unit floor, persona MUST emit 'passed/unit' AND 'missing/integration-with-fixtures' rows. Cheap proof does not silence integration-tier ask. - S7 PROBE RULE: a 'passed' at integration-with-fixtures or e2e on a critical surface MUST have run in-session with pytest output recorded in evidence.run_evidence. Reading a test is not running it. Skip-condition (e.g. missing creds) downgrades outcome to 'unknown' instead of certifying. - Two new anti-patterns added: 'Reading a test instead of running it' and 'Collapsing tier under one outcome'. Fixtures (evals shape references) updated for the new contract. Schema validates with jsonschema; render_eval renders both fixtures clean. Co-authored-by: Copilot <copilot-rework@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore(release): cut 0.12.0 Promotes [Unreleased] -> [0.12.0] - 2026-05-03 and bumps pyproject.toml + uv.lock to 0.12.0. Sanitization: - Filled the residual (#PR_NUMBER) placeholder on the local-bundle UnboundLocalError fix entry -> (#1108) - Preserved an empty [Unreleased] section above 0.12.0 so the next contributor PR can append entries without re-introducing the heading Version-bump rationale: 0.12.0 (minor bump) chosen over 0.11.1 because this release ships TWO BREAKING changes: - 'apm pack' now produces a Claude Code plugin directory by default; the legacy bundle layout requires --format apm (#1061) - Dropped support for .collection.yml / .collection.yaml virtual packages (#1097) plus several net-new features (Windsurf target, Claude Code MCP install target, --target agent-skills, apm install <local-bundle>, apm compile -t copilot, marketplace add HTTPS/nested URLs, slash-command argument hints in Claude). Strict semver in 0.x: minor for features-with-break, patch only for bugfixes. 44 commits since v0.11.0. Validation: - ruff check src/ tests/ -- silent - ruff format --check src/ tests/ -- silent - uv lock -- regenerated cleanly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(install): align local-bundle hash format with compute_file_hash integrate_local_bundle() recorded bare hex hashes in local_deployed_file_hashes, but cleanup.py provenance check compares against compute_file_hash() which returns 'sha256:<hex>'. The mismatch caused stale-cleanup of local-bundle files to skip every file as 'user-edited' instead of removing files no longer in the bundle. - services.py: write 'sha256:<hex>' on real deploy and dry-run paths - cleanup.py: defensively normalize both sides of the equality check (handles legacy 0.12.0-rc lockfiles with bare hex) - regression tests cover both the format consistency and the cross-flow cleanup interaction Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: wire pack/compile/transitive integration tests into CI These three integration test files exist and pass locally but were not enumerated in scripts/test-integration.sh, so CI silently skipped them and could not catch regressions in: - apm pack default format (0.12.0 flipped from 'apm' to 'plugin') - apm compile --target copilot (.github/copilot-instructions.md) - transitive local_path anchoring across multi-level local chains Surfaced by the test-coverage review of PR #1112. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Daniel Meppiel <copilot-rework@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description
Fixes two related gaps that prevented a natural meta-package mental model
(a sub-path inside a monorepo whose
apm.ymldeclares dependencies on otherskills/MCPs/packages):
/collections/path segment hard-routed to the.collection.ymlparser,shadowing any
apm.ymlthat lived at<repo>/collections/<name>/apm.yml.apm.yml(no.apm/directory) failed validationeven after transitive deps had been resolved and installed.
Both stem from the same architectural error: package type was inferred from
path segments rather than content. The fix removes the URL-level path
heuristic and adds
META_PACKAGEas a first-classPackageType.Fixes #1094
Type of change
What changed
models/dependency/reference.pyvirtual_typeis now extension-only: a path ending in.collection.yml/
.collection.yamlisCOLLECTION; otherwise (no recognised fileextension) it is
SUBDIRECTORY, and the actual on-disk shape is resolvedat fetch time. Path segments like
/collections/no longer carry semanticweight.
VIRTUAL_FILE_EXTENSIONS + VIRTUAL_COLLECTION_EXTENSIONS, so explicit.collection.ymlURLs areaccepted regardless of whether the path is under
/collections/.deps/github_downloader_validation.pySUBDIRECTORYbranch putsapm.ymlbefore the legacy<vpath>.collection.ymlfallback socollections/<name>/apm.ymlisrecognised as an APM package, not as a missing collection manifest.
COLLECTIONprobe(
{vpath}.collection.yml.collection.yml) exposed by the newextension-only classification.
deps/github_downloader.py_is_legacy_collection_fallbackhelper detects the legacy/collections/<name>URL form whose actual content is a sibling<name>.collection.yml, and routes the install todownload_collection_packageso existing consumers keep working withoutthe path heuristic.
download_collection_packageprecondition relaxed to acceptSUBDIRECTORY-typed refs (the legacy fallback path).models/validation.pyPackageType.META_PACKAGEforapm.ymlthat declares dependenciesbut contributes no own primitives.
detect_package_typecascaderecognises this shape (
apm.ymlexists, no.apm/, no nested skills,declared deps in
dependenciesordevDependencies). Validation parsesapm.ymland returns cleanly; transitive resolution and integrationhappen on the declared dependencies as before.
apm.ymlwith no deps and no.apm/still resolves toINVALIDsothe existing helpful error wording is preserved.
install/sources.pyMETA_PACKAGE.Why
The fix removes a class of bugs rather than adding a special case. Path
segments and package shape were two orthogonal concerns conflated by the
old
/collections/heuristic; oncevirtual_typeis extension-only thetwo gaps disappear together. A first-class
META_PACKAGEtype avoids theempty-
.apm/.gitkeepworkaround the issue described as a wart.The legacy implicit
/collections/<name>URL form (where the repo onlycontains a sibling
<name>.collection.yml) is preserved via the_is_legacy_collection_fallbackdispatcher: classification stayscontent-based, but a path-shape hint (
/collections/segment) narrowsthe search space so
SUBDIRECTORYrefs outside that convention skip 2-3otherwise-wasted HTTP probes per install.
How (testing)
Unit tests: 7040 passed locally, including:
tests/unit/test_meta_package.py(new) -- 12 tests covering theMETA_PACKAGEdetection cascade and validation, including dev-onlyaggregators, conflicts with
SKILL_BUNDLE/HYBRID, and the negativecase (
apm.ymlwith no deps and no.apm/->INVALID).tests/unit/deps/test_github_downloader_validation.py-- newregression tests for probe order (
apm.ymlpriority over.collection.yml), the_is_legacy_collection_fallbackdispatcher(path-hint short-circuit, both
.yml/.yamlextensions), and thedouble-extension bug.
(URLs that asserted
is_virtual_collection() is Trueforcollections/<name>without extension now use the explicit form, withseparate tests added for the new
SUBDIRECTORYsemantics).Manual end-to-end: against a fixture repo, all five package shapes
under
/collections/<name>/install correctly (APM_PACKAGE,META_PACKAGE,SKILL_BUNDLE,HYBRID,MARKETPLACE_PLUGIN). Theconflict case (sibling
apm.ymland.collection.ymlboth present)correctly resolves to the
apm.yml-defined package. Backwards-compatflows verified: explicit
.collection.ymlURL form, legacy implicit/collections/<name>form (CLI and viaapm.yml), pinned refs (#main,commit SHA), and lockfile round-trip stability.
Testing