Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ All notable changes to this project will be documented in this file.

---

## [0.34.0] - 2026-02-18

### Added

- **Init module discovery alignment** (backlog-core-01): `specfact init` now uses the same module discovery roots as command registration (`discover_all_package_metadata()`), so `--list-modules`, `--enable-module`, and `--disable-module` operate on all discovered modules including workspace-level ones (e.g. `modules/backlog-core/`). Closes [#116](https://github.com/nold-ai/specfact-cli/issues/116) scope for init-module-discovery-alignment.

### Changed

- `specfact init` module state and validation now build from `discover_all_package_metadata()` instead of `discover_package_metadata(get_modules_root())`, aligning enable/disable and list-modules with runtime command discovery.

---

## [0.33.0] - 2026-02-17

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,3 +347,29 @@ Enrich GitHub/ADO provider outputs so dependency graph analysis gets relationshi
- Added integration coverage for full stage sequence `plan -> develop -> review -> release -> monitor`.
- `test_project_devops_flow_complete_stage_sequence` validates all stage/action paths execute end-to-end with deterministic stubs.
- Integration command file now passes fully (`4 passed`).

## Scope (0.5 Init module discovery alignment)

Align `specfact init` with command registration so workspace-level modules appear in `--list-modules`, `--enable-module`, and `--disable-module`.

### Pre-Implementation Failing Run (0.5)

- Timestamp: 2026-02-18
- Command:
- `hatch run pytest tests/unit/specfact_cli/registry/test_init_module_lifecycle_ux.py::test_init_enable_workspace_level_module_succeeds -v`
- Result: **FAIL** (before code change)
- Failure summary: Init used `discover_package_metadata(get_modules_root())` for validation, so enabling a module only present in `SPECFACT_MODULES_ROOTS` was blocked ("module not found"); exit_code == 1.

### Implementation (0.5)

- Tests added: `test_init_list_modules_includes_workspace_level_modules`, `test_init_enable_workspace_level_module_succeeds` in `tests/unit/specfact_cli/registry/test_init_module_lifecycle_ux.py`.
- Production change: `src/specfact_cli/modules/init/src/commands.py` β€” replaced `discover_package_metadata(get_modules_root())` with `discover_all_package_metadata()` for building `packages` and `discovered_list`.
- Updated mocks in existing init tests from `discover_package_metadata` to `discover_all_package_metadata` (no-arg lambda).

### Post-Implementation Passing Run (0.5)

- Timestamp: 2026-02-18
- Command:
- `hatch run pytest tests/unit/specfact_cli/registry/test_init_module_lifecycle_ux.py -v`
- Result: **PASS** (11 passed)
- Verification summary: All init lifecycle UX tests pass; workspace-level module list and enable flows succeed.
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@

### 0.5 Init module discovery alignment (workspace-level modules)

- [ ] 0.5.1 In `src/specfact_cli/modules/init/src/commands.py`, replace use of `discover_package_metadata(get_modules_root())` for building `packages` and `discovered_list` with `discover_all_package_metadata()` so init sees all discovery roots (built-in + repo-root `modules/` + `SPECFACT_MODULES_ROOTS`).
- [ ] 0.5.2 Derive `discovered_list` from the same `packages` result (e.g. `[(meta.name, meta.version) for _dir, meta in packages]`) so enable/disable validation and merge use the full discovered set.
- [ ] 0.5.3 Add unit test (e.g. in `tests/unit/specfact_cli/registry/test_module_packages.py` or `tests/unit/specfact_cli/modules/init/`) that when repo-root `modules/` contains a module (or when `get_modules_roots()` returns multiple roots and a second root has a package), `specfact init --list-modules` output includes that module.
- [ ] 0.5.4 Run `hatch run format`, `hatch run type-check`, `hatch run smart-test-unit` and confirm tests pass.
- [x] 0.5.1 In `src/specfact_cli/modules/init/src/commands.py`, replace use of `discover_package_metadata(get_modules_root())` for building `packages` and `discovered_list` with `discover_all_package_metadata()` so init sees all discovery roots (built-in + repo-root `modules/` + `SPECFACT_MODULES_ROOTS`).
- [x] 0.5.2 Derive `discovered_list` from the same `packages` result (e.g. `[(meta.name, meta.version) for _dir, meta in packages]`) so enable/disable validation and merge use the full discovered set.
- [x] 0.5.3 Add unit test (e.g. in `tests/unit/specfact_cli/registry/test_module_packages.py` or `tests/unit/specfact_cli/modules/init/`) that when repo-root `modules/` contains a module (or when `get_modules_roots()` returns multiple roots and a second root has a package), `specfact init --list-modules` output includes that module.
- [x] 0.5.4 Run `hatch run format`, `hatch run type-check`, `hatch run smart-test-unit` and confirm tests pass.

## 1. Phase 1: Backlog Dependency Analysis (v0.26.0)

Expand Down
12 changes: 9 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "specfact-cli"
version = "0.33.0"
version = "0.34.0"
description = "The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with validation and contract enforcement for new projects and long-lived codebases."
readme = "README.md"
requires-python = ">=3.11"
Expand Down Expand Up @@ -582,8 +582,8 @@ disable = [
"C0115", # missing-class-docstring
"C0116", # missing-function-docstring
"C0103", # invalid-name (too restrictive for some cases)
"C0330", # bad-continuation (handled by ruff format)
"C0326", # bad-whitespace (handled by ruff format)
# C0330, C0326 removed in pylint 3.x (handled by ruff format)

"R0903", # too-few-public-methods
"R0913", # too-many-arguments (too restrictive for APIs)
"R0912", # too-many-branches
Expand All @@ -592,6 +592,12 @@ disable = [
"C0413", # wrong-import-position (handled by isort)
"E0401", # unable-to-import (false positives for local modules in pylint)
"E0611", # no-name-in-module (false positives for local modules in pylint)
"E1101", # no-member (false positives for Pydantic/FieldInfo, rich.Progress.tasks)
"E0601", # used-before-assignment (false positives in try/except UTC, tomllib)
"E1126", # invalid-sequence-index (rich TaskID)
"E0603", # undefined-all-variable
"E1136", # unsubscriptable-object
"E0110", # abstract-class-instantiated
]

[tool.pylint.master]
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
if __name__ == "__main__":
_setup = setup(
name="specfact-cli",
version="0.33.0",
version="0.34.0",
description=(
"The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with "
"validation and contract enforcement for new projects and long-lived codebases."
Expand Down
2 changes: 1 addition & 1 deletion src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
"""

# Package version: keep in sync with pyproject.toml, setup.py, src/specfact_cli/__init__.py
__version__ = "0.32.1"
__version__ = "0.34.0"
2 changes: 1 addition & 1 deletion src/specfact_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
- Supporting agile ceremonies and team workflows
"""

__version__ = "0.33.0"
__version__ = "0.34.0"

__all__ = ["__version__"]
5 changes: 2 additions & 3 deletions src/specfact_cli/modules/init/src/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,10 @@
from specfact_cli.modules import module_io_shim
from specfact_cli.registry.help_cache import run_discovery_and_write_cache
from specfact_cli.registry.module_packages import (
discover_package_metadata,
discover_all_package_metadata,
expand_disable_with_dependents,
expand_enable_with_dependencies,
get_discovered_modules_for_state,
get_modules_root,
merge_module_state,
validate_disable_safe,
validate_enable_safe,
Expand Down Expand Up @@ -563,7 +562,7 @@ def init(
if selected:
module_management_requested = True

packages = discover_package_metadata(get_modules_root())
packages = discover_all_package_metadata()
discovered_list = [(meta.name, meta.version) for _package_dir, meta in packages]
state = read_modules_state()

Expand Down
51 changes: 46 additions & 5 deletions tests/unit/specfact_cli/registry/test_init_module_lifecycle_ux.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ def test_init_disable_module_does_not_run_ide_setup(tmp_path: Path, monkeypatch)
lambda disable_ids, packages, enabled_map: {},
)
monkeypatch.setattr(
"specfact_cli.modules.init.src.commands.discover_package_metadata",
lambda modules_root: [],
"specfact_cli.modules.init.src.commands.discover_all_package_metadata",
list,
)

def _fail_copy(*args, **kwargs):
Expand Down Expand Up @@ -190,7 +190,7 @@ def test_init_force_disable_cascades_to_dependents(tmp_path: Path, monkeypatch)
ModulePackageMetadata(name="sync", version="0.1.0", commands=["sync"], module_dependencies=[]),
),
]
monkeypatch.setattr("specfact_cli.modules.init.src.commands.discover_package_metadata", lambda root: packages)
monkeypatch.setattr("specfact_cli.modules.init.src.commands.discover_all_package_metadata", lambda: packages)
monkeypatch.setattr("specfact_cli.modules.init.src.commands.read_modules_state", dict)
monkeypatch.setattr("specfact_cli.modules.init.src.commands.run_discovery_and_write_cache", lambda version: None)

Expand Down Expand Up @@ -230,7 +230,7 @@ def test_init_force_enable_cascades_to_dependencies(tmp_path: Path, monkeypatch)
ModulePackageMetadata(name="sync", version="0.1.0", commands=["sync"], module_dependencies=[]),
),
]
monkeypatch.setattr("specfact_cli.modules.init.src.commands.discover_package_metadata", lambda root: packages)
monkeypatch.setattr("specfact_cli.modules.init.src.commands.discover_all_package_metadata", lambda: packages)
monkeypatch.setattr("specfact_cli.modules.init.src.commands.read_modules_state", dict)
monkeypatch.setattr("specfact_cli.modules.init.src.commands.run_discovery_and_write_cache", lambda version: None)

Expand Down Expand Up @@ -270,7 +270,7 @@ def test_init_enable_without_force_blocks_when_dependency_disabled(tmp_path: Pat
ModulePackageMetadata(name="sync", version="0.1.0", commands=["sync"], module_dependencies=[]),
),
]
monkeypatch.setattr("specfact_cli.modules.init.src.commands.discover_package_metadata", lambda root: packages)
monkeypatch.setattr("specfact_cli.modules.init.src.commands.discover_all_package_metadata", lambda: packages)
monkeypatch.setattr(
"specfact_cli.modules.init.src.commands.read_modules_state", lambda: {"sync": {"enabled": False}}
)
Expand All @@ -280,3 +280,44 @@ def test_init_enable_without_force_blocks_when_dependency_disabled(tmp_path: Pat
assert result.exit_code == 1
assert "Cannot enable 'plan'" in result.stdout
assert "--force" in result.stdout


def test_init_list_modules_includes_workspace_level_modules(tmp_path: Path, monkeypatch) -> None:
"""specfact init --list-modules includes modules from SPECFACT_MODULES_ROOTS (init-module-discovery-alignment)."""
modules_root = tmp_path / "ws_modules"
modules_root.mkdir()
extra_dir = modules_root / "extra_ws"
extra_dir.mkdir()
(extra_dir / "module-package.yaml").write_text(
"name: extra_ws\nversion: '0.1.0'\ncommands: [dummy]\n", encoding="utf-8"
)
monkeypatch.setenv("SPECFACT_MODULES_ROOTS", str(modules_root))
reg_dir = tmp_path / "registry"
reg_dir.mkdir()
monkeypatch.setenv("SPECFACT_REGISTRY_DIR", str(reg_dir))

result = runner.invoke(app, ["init", "--repo", str(tmp_path), "--list-modules"])

assert result.exit_code == 0
assert "extra_ws" in result.stdout


def test_init_enable_workspace_level_module_succeeds(tmp_path: Path, monkeypatch) -> None:
"""init --enable-module for a workspace-level module succeeds when discovery uses all roots."""
modules_root = tmp_path / "ws_modules"
modules_root.mkdir()
extra_dir = modules_root / "extra_ws"
extra_dir.mkdir()
(extra_dir / "module-package.yaml").write_text(
"name: extra_ws\nversion: '0.1.0'\ncommands: [dummy]\n", encoding="utf-8"
)
monkeypatch.setenv("SPECFACT_MODULES_ROOTS", str(modules_root))
reg_dir = tmp_path / "registry"
reg_dir.mkdir()
monkeypatch.setenv("SPECFACT_REGISTRY_DIR", str(reg_dir))
monkeypatch.setattr("specfact_cli.modules.init.src.commands.is_non_interactive", lambda: True)
monkeypatch.setattr("specfact_cli.modules.init.src.commands.run_discovery_and_write_cache", lambda version: None)

result = runner.invoke(app, ["init", "--repo", str(tmp_path), "--enable-module", "extra_ws"])

assert result.exit_code == 0, result.output
Loading