Skip to content

fix(deps): normalize case-insensitive package identity#2098

Merged
danielmeppiel merged 4 commits into
mainfrom
fix/package-owner-case-normalization
Jul 10, 2026
Merged

fix(deps): normalize case-insensitive package identity#2098
danielmeppiel merged 4 commits into
mainfrom
fix/package-owner-case-normalization

Conversation

@danielmeppiel

Copy link
Copy Markdown
Collaborator

fix(deps): normalize case-insensitive package identity

TL;DR

GitHub and package-registry owner/repository paths now become lowercase at a shared identity boundary. Mixed-case references therefore share canonical strings, lock/cache keys, publish paths, and install directories, while unknown git hosts retain their original path casing.

Note

This closes #2073 and automatically rewrites legacy mixed-case GitHub lock entries on the next lockfile emission.

Problem (WHY)

  • Owner/example-package and owner/example-package previously produced distinct identities, lock/cache keys, and apm_modules/ paths, as reproduced in [BUG] Packages are case sensitive but they should not be #2073.
  • apm publish --package Owner/Repo addressed a different case-sensitive registry URL path than install used for the same logical package.
  • [!] Lowercasing every git URL would break unknown self-hosted backends whose repository paths may be case-sensitive.

The regression tests turn the issue contract into executable evidence: "Grounding outputs in deterministic tool execution transforms probabilistic generation into verifiable action."

Approach (WHAT)

# Fix Principle Source
1 Centralize owner/repository normalization in one identity helper. "Favor small, chainable primitives over monolithic frameworks." PROSE
2 Apply that boundary when dependency and lock models are created, and when registry publish IDs are parsed. "Context arrives just-in-time, not just-in-case." PROSE
3 Preserve unknown-host repository casing and prove the exception. "Add what the agent lacks, omit what it knows" Agent Skills

Implementation (HOW)

File Change
src/apm_cli/models/dependency/identity.py Adds the shared normalization policy: lowercase GitHub/default-host, registry, and registry-proxy identities; preserve local, unresolved marketplace, and unknown-host paths.
src/apm_cli/models/dependency/reference.py Normalizes once in DependencyReference.__post_init__, before canonical, dedup, cache, display, and install-path consumers run.
src/apm_cli/deps/lockfile.py Applies the same boundary to new and loaded LockedDependency values, migrating mixed-case GitHub lock entries on serialization.
src/apm_cli/commands/publish.py Canonicalizes registry OWNER/REPO before constructing publish API paths.
tests/test_apm_package_models.py Adds regression traps for identity, canonical form, dedup key, install path, and unknown-host case preservation.
tests/test_lockfile.py Proves legacy mixed-case lock entries converge and serialize lowercase.
tests/unit/commands/test_publish_registry_archive.py Proves publish package IDs use the same lowercase registry identity.
tests/unit/integration/test_skill_integrator.py Updates the integration-level diagnostic contract to consume the normalized key.
docs/src/content/docs/consumer/manage-dependencies.md Documents normalization and the unknown-host exception.
packages/apm-guide/.apm/skills/apm-usage/dependencies.md Keeps the bundled dependency reference guidance synchronized.
CHANGELOG.md Records the issue fix under Unreleased.

Diagrams

Legend: the dashed node is the new shared boundary; downstream consumers only receive its canonical result.

flowchart LR
    subgraph Inputs[Package identity inputs]
        I1[DependencyReference]
        I2[LockedDependency]
        I3[Registry publish ID]
    end
    subgraph Normalize[Identity boundary]
        N1[normalize_package_repo_url]
        N2{GitHub or registry?}
    end
    subgraph Outputs[Identity consumers]
        O1[Canonical string]
        O2[Lock and cache key]
        O3[Install and publish path]
    end
    I1 --> N1
    I2 --> N1
    I3 --> N1
    N1 --> N2
    N2 -->|yes| L1[Lowercase owner and repo]
    N2 -->|no| P1[Preserve repository path case]
    L1 --> O1
    L1 --> O2
    L1 --> O3
    P1 --> O1
    P1 --> O2
    P1 --> O3
    classDef new stroke-dasharray: 5 5;
    class N1 new;
Loading

Trade-offs

  • Normalize at model construction, not at every consumer. This prevents drift among canonical strings, lock/cache keys, and paths; repeated consumer-specific lowercasing was rejected.
  • Preserve unknown-host casing. Universal lowercasing was rejected because generic Git servers can define case-sensitive repository paths.
  • Rewrite legacy values in place. A lock schema bump was rejected because existing entries can be normalized safely when loaded and re-emitted.

Benefits

  1. Two GitHub references differing only by casing now produce exactly one identity and one lock/cache key.
  2. The same pair now resolves to exactly one lowercase apm_modules/owner/repo directory.
  3. Registry publish and install use the same lowercase owner/repository URL path.
  4. Unknown self-hosted Git paths remain byte-for-byte case-preserving.

Validation

uv run --extra dev pytest <six casing regression tests> -q:

......                                                                   [100%]
6 passed in 2.11s

Mutation-break proof (replace both normalization returns with the unmodified input, then run the five normalization traps):

mutation killed by regression tests
Full unit suite and CI lint mirror

uv run --extra dev pytest tests/unit -q --tb=short:

18164 passed, 2 skipped, 21 xfailed, 19 warnings, 84 subtests passed in 164.33s (0:02:44)

CI lint mirror:

All checks passed!
1410 files already formatted
Your code has been rated at 10.00/10
[+] auth-signal lint clean
YAML, file-length, and relative_to guards passed

Scenario Evidence

# Scenario (user promise) Principle(s) Test(s) proving it Type
1 Installing Owner/Repo and owner/repo creates one package identity, lock/cache key, and directory. Governed by policy, DevX (pragmatic as npm) tests/test_apm_package_models.py::TestDependencyReference::test_github_owner_repo_casing_has_one_identity
::test_github_owner_repo_casing_has_one_dedup_key
::test_github_owner_repo_casing_has_one_install_path (regression-trap for #2073)
unit
2 Loading an older mixed-case GitHub lock entry rewrites it to the canonical lowercase key. Governed by policy tests/test_lockfile.py::TestLockedDependency::test_github_owner_repo_casing_migrates_to_one_lock_key unit
3 Publishing a mixed-case package ID addresses the same registry package that install resolves. Governed by policy, DevX (pragmatic as npm) tests/unit/commands/test_publish_registry_archive.py::TestResolvePackageId::test_package_identity_is_lowercase unit
4 Installing from an unknown self-hosted Git server preserves its case-sensitive repository path. Vendor-neutral tests/test_apm_package_models.py::TestDependencyReference::test_generic_git_host_preserves_case_sensitive_repo_path unit
5 Integrator diagnostics report the normalized current package key. Multi-harness support, Governed by policy tests/unit/integration/test_skill_integrator.py::TestNativeSkillIntegration::test_native_skill_collision_diagnostic_package_is_current_key integration

How to test

  • Run the six casing regression tests above; expect 6 passed.
  • Run uv run --extra dev pytest tests/unit -q; expect all unit tests to pass.
  • Run the CI lint mirror; expect Ruff, duplication, auth, YAML, file-length, and relative-path guards to pass.

Co-authored-by: Copilot 223556219+Copilot@users.noreply.github.com

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 9, 2026 22:25
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes GitHub and registry owner/repo package identities case-insensitive by normalizing them to lowercase at a shared model boundary, ensuring consistent lock/cache keys, canonical strings, publish API paths, and install directories while preserving casing for unknown git hosts.

Changes:

  • Introduces a shared normalize_package_repo_url() helper and applies it in dependency reference and lockfile models.
  • Canonicalizes apm publish --package parsing to align publish identity with install identity.
  • Adds/updates regression tests and updates docs to document the normalization behavior.
Show a summary per file
File Description
src/apm_cli/models/dependency/identity.py Adds shared normalization helper for repo identity casing.
src/apm_cli/models/dependency/reference.py Normalizes repo_url in DependencyReference.__post_init__ so downstream identity consumers are consistent.
src/apm_cli/deps/lockfile.py Normalizes LockedDependency.repo_url at construction/load time to converge legacy mixed-case entries.
src/apm_cli/commands/publish.py Ensures --package IDs are normalized before constructing publish paths.
tests/test_apm_package_models.py Adds regression tests covering identity/key/install-path convergence and unknown-host case preservation.
tests/test_lockfile.py Adds regression test proving mixed-case GitHub lock entries normalize on load/serialize.
tests/unit/commands/test_publish_registry_archive.py Adds test ensuring publish package IDs normalize to lowercase.
tests/unit/integration/test_skill_integrator.py Updates expected integration diagnostic package key to the normalized form.
docs/src/content/docs/consumer/manage-dependencies.md Documents new normalization behavior and unknown-host exception.
packages/apm-guide/.apm/skills/apm-usage/dependencies.md Syncs bundled dependency guidance with the normalization behavior.
CHANGELOG.md Adds an Unreleased “Fixed” entry describing the behavior change.

Review details

  • Files reviewed: 11/11 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment thread CHANGELOG.md Outdated
Comment on lines +12 to +15
- GitHub and package-registry references now normalize owner/repository casing
before deriving identity, lock/cache keys, canonical strings, publish paths,
and install directories, while unknown git hosts retain case-sensitive paths.
(closes #2073)
Add integration-level install, resolution, collision, install-path, and lockfile evidence while documenting the canonical identity boundary and publish behavior. Addresses review-panel follow-ups for PR #2098.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel

Copy link
Copy Markdown
Collaborator Author

APM Review Panel: ship_now

Case-insensitive GitHub package identity prevents duplicate installs and lockfile collisions by enforcing normalization at the shared model boundary.

cc @sergio-sisternes-epam -- a fresh advisory pass is ready for your review.

All active panel lenses converge on shipping. DependencyReference and LockedDependency normalize through one idempotent helper before canonical strings, dedup keys, install paths, cache identity, and lockfile serialization are derived. The integration regression trap drives mixed-case inputs through manifest addition, dependency resolution, collision handling, install-path selection, and lockfile write/read. Removing the GitHub lowercase guard makes that test fail.

Security suggestions about ADO casing and raw .git suffix handling are separate host/URL semantics beyond this GitHub and registry casing change. Logging suggestions would couple pure identity models to CLI output. Neither identifies an unresolved issue in this PR.

Aligned with: portable manifests through deterministic lock keys; secure defaults by eliminating identity confusion; npm-style package ergonomics.

Panel summary

Persona B R N Takeaway
Python Architect 0 0 0 One normalization boundary now owns package casing.
CLI Logging Expert 0 0 0 No output regression; canonical names remain clear.
DevX UX Expert 0 0 0 Mixed-case references now behave as one package.
Supply Chain Security Expert 0 0 0 Identity confusion is closed for the stated hosts.
OSS Growth Hacker 0 0 0 The changelog now leads with the user-visible fix.
Auth Expert 0 0 0 Auth and token resolution remain unchanged.
Doc Writer 0 0 0 Install, lockfile, publish, and bundled guidance agree.
Test Coverage Expert 0 0 0 Unit and integration evidence cover the critical promise.
Performance Expert 0 0 0 Constant-time string normalization is negligible and improves cache convergence.

B = blocking-severity findings, R = recommended, N = nits.
Counts are signal strength, not gates. The maintainer ships.

Folded in this run

  • (panel) Added an empirical install/resolution integration test covering deduplication, zero collisions, one lowercase install path, and lockfile roundtrip -- resolved in 926a13fe1287d400e1ed224b34e2a6410137e798.
  • (panel) Documented lockfile and publish normalization in the Starlight references and bundled APM usage guide -- resolved in 926a13fe1287d400e1ed224b34e2a6410137e798.
  • (panel) Documented the single casing-normalization boundary and removed a redundant default-host lookup -- resolved in 926a13fe1287d400e1ed224b34e2a6410137e798.
  • (panel) Reframed the changelog around duplicate identity, lock/cache, and install-path prevention -- resolved in 926a13fe1287d400e1ed224b34e2a6410137e798.

Copilot signals reviewed

Copilot produced no inline findings on the final revision.

Regression-trap evidence (mutation-break gate)

  • tests/integration/test_package_identity_case_install.py::test_mixed_case_install_resolves_once_without_collision -- removed the GitHub lowercase return in normalize_package_repo_url; the test failed because two case variants survived as distinct installs; the guard was restored and the test passed.

Lint contract

Ruff check and format, YAML I/O safety, file length, portable relative paths, pylint R0801, and auth-signal checks all passed before push. The full unit suite also passed.

CI

All checks passed on exact head 926a13fe1287d400e1ed224b34e2a6410137e798: https://github.com/microsoft/apm/actions/runs/29063285281

Mergeability status

PR head SHA CEO stance iters folds defers Copilot rounds CI mergeable mergeStateStatus notes
#2098 926a13f ship_now 1 4 0 1 green MERGEABLE BLOCKED awaiting required review

Recommendation

All in-scope findings are folded, the mutation is killed, and exact-head CI is green. Ready for maintainer review.


Full per-persona findings

The final pass found no unresolved in-scope findings across architecture, CLI logging, DevX, supply-chain security, growth, auth, documentation, test coverage, or performance.

This panel is advisory. Re-apply the panel-review label after addressing feedback to re-run.

@danielmeppiel danielmeppiel merged commit b0b41d8 into main Jul 10, 2026
12 checks passed
@danielmeppiel danielmeppiel deleted the fix/package-owner-case-normalization branch July 10, 2026 04:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Packages are case sensitive but they should not be

2 participants