Skip to content

Bug: extensions core-command discovery is dead — off-by-one paths after the #3014 module move #3274

Description

@v-dhruv

Summary

specify_cli.extensions._load_core_command_names() never discovers the bundled command templates. Both candidate paths are off by one directory, so the function silently returns the hardcoded _FALLBACK_CORE_COMMAND_NAMES on every call — in both wheel installs and source checkouts.

Root cause

The function resolves its command dirs with bespoke Path(__file__) math:

candidate_dirs = [
    Path(__file__).parent / "core_pack" / "commands",
    Path(__file__).resolve().parent.parent.parent / "templates" / "commands",
]

These were correct when the code lived at src/specify_cli/extensions.py (anti-shadowing guard added in #1994). The refactor that moved it into the package at src/specify_cli/extensions/__init__.py (#3014, 826e193 — diff shows {extensions.py => extensions/__init__.py}) pushed the file one directory deeper but did not update the parent counts. They now resolve to:

  • wheel → specify_cli/extensions/core_pack/commands — real path is specify_cli/core_pack/commands
  • source → src/templates/commands — real path is repo-root templates/commands

Neither exists, so commands_dir.is_dir() is always false and the loop falls through to the fallback set.

This is the same off-by-one class @mnriem identified for the presets loader (parent.parent.parent after the #2826 presets.pypresets/__init__.py move, re #3086), but in the extensions module — which the preset-scoped fix does not touch.

Reproduction

From a source checkout:

from specify_cli.extensions import _load_core_command_names, _FALLBACK_CORE_COMMAND_NAMES
# Discovery is meant to read repo-root templates/commands. Instead:
assert _load_core_command_names() == _FALLBACK_CORE_COMMAND_NAMES  # passes — discovery is dead

Both candidate dirs resolve to non-existent paths (verifiable by printing them).

Impact (honest assessment)

Currently latent, not user-visible: _FALLBACK_CORE_COMMAND_NAMES happens to equal the 10 real command stems today, so nothing breaks right now.

The risk is silent drift. CORE_COMMAND_NAMES (derived from this function) guards extensions against shadowing core command names (#1994). With dynamic discovery dead, that guard depends entirely on someone hand-editing the fallback whenever a core command is added or removed — which has already happened: converge was manually appended to _FALLBACK_CORE_COMMAND_NAMES in #3001 (0c29d89). A future add/remove that forgets the fallback would silently desync the guard.

Proposed fix

Delegate path resolution to the canonical _assets resolvers (_locate_core_pack / _repo_root), exactly as presets/__init__.py already does. They are anchored to the package root, so discovery is immune to future module moves. Add regression tests that pin live discovery (they fail on the current code and pass after the fix).


Filed by @v-dhruv with assistance from GitHub Copilot (model: Claude Opus 4.8). The investigation and proposed patch are agent-generated and human-directed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions