Skip to content

Commit 9eb9557

Browse files
committed
Add follow-up change proposals for marketplace
1 parent 28dfcaf commit 9eb9557

File tree

20 files changed

+3678
-2
lines changed

20 files changed

+3678
-2
lines changed

openspec/CHANGE_ORDER.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ These are derived extensions of the same 2026-02-15 plan and are required to ope
7575
| marketplace | 01 | ✅ marketplace-01-central-module-registry (implemented 2026-02-22; archived) | [#214](https://github.com/nold-ai/specfact-cli/issues/214) | #208 |
7676
| marketplace | 02 | marketplace-02-advanced-marketplace-features | [#215](https://github.com/nold-ai/specfact-cli/issues/215) | #214 |
7777

78+
### Module migration (UX grouping and extraction)
79+
80+
| Module | Order | Change folder | GitHub # | Blocked by |
81+
|--------|-------|---------------|----------|------------|
82+
| module-migration | 01 | module-migration-01-categorize-and-group | TBD | #215 (marketplace-02) |
83+
| module-migration | 02 | module-migration-02-bundle-extraction | TBD | module-migration-01 |
84+
| module-migration | 03 | module-migration-03-core-slimming | TBD | module-migration-02 |
85+
7886
### Cross-cutting foundations (no hard dependencies — implement early)
7987

8088
| Module | Order | Change folder | GitHub # | Blocked by |
@@ -305,13 +313,16 @@ Dependencies flow left-to-right; a wave may start once all its hard blockers are
305313
- backlog-scrum-02, backlog-scrum-03, backlog-scrum-04 (need backlog-core-01)
306314
- backlog-kanban-01, backlog-safe-01 (need backlog-core-01)
307315

308-
- **Wave 3 — Higher-order backlog + marketplace** (needs Wave 2):
316+
- **Wave 3 — Higher-order backlog + marketplace + module migration** (needs Wave 2):
309317
- marketplace-02 (needs marketplace-01)
310318
- backlog-scrum-01 ✅ (needs backlog-core-01; benefits from policy-engine-01 + patch-mode-01)
311319
- backlog-safe-02 (needs backlog-safe-01; integrates with scrum/kanban via bridge registry)
320+
- module-migration-01-categorize-and-group (needs marketplace-02; adds category metadata + group commands)
321+
- module-migration-02-bundle-extraction (needs module-migration-01; moves module source to bundle packages, publishes to marketplace registry)
312322

313-
- **Wave 4 — Ceremony layer** (needs Wave 3):
323+
- **Wave 4 — Ceremony layer + module slimming** (needs Wave 3):
314324
- ceremony-cockpit-01 ✅ (probes installed backlog-* modules at runtime; no hard deps but best after Wave 3)
325+
- module-migration-03-core-package-slimming (needs module-migration-02; removes bundled modules from core)
315326

316327
- **Wave 5 — Foundations for business-first chain** (architecture integration):
317328
- profile-01
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
schema: spec-driven
2+
created: 2026-02-27
Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
1+
# Design: Module Grouping and Category Command Groups
2+
3+
## Context
4+
5+
SpecFact CLI ships 21 modules as flat top-level commands in `src/specfact_cli/modules/`. The marketplace foundation (marketplace-01 archived, marketplace-02 in progress) enables signed, versioned, installable module packages. This change introduces the grouping layer that organises those 21 modules into 5 workflow-domain categories, exposes them through category umbrella commands, and replaces the first-run experience with VS Code-style bundle selection.
6+
7+
**Current State:**
8+
9+
- 21 flat top-level commands registered in bootstrap.py
10+
- No category concept in module-package.yaml
11+
- `specfact init` performs workspace setup with no bundle selection
12+
- `specfact --help` is overwhelming for new users
13+
14+
**Constraints:**
15+
16+
- Must not break existing `specfact <module>` invocations during migration window
17+
- Must work offline (no cloud dependency for grouping logic)
18+
- Must remain backward-compatible with CI/CD pipelines that call flat commands
19+
- Must depend on marketplace-02 dependency resolver for bundle-level dep graph (spec → project, govern → project)
20+
- All new public APIs must carry `@icontract` and `@beartype` decorators
21+
22+
## Goals / Non-Goals
23+
24+
**Goals:**
25+
26+
- Add category metadata to all 21 module-package.yaml files (Phase 1)
27+
- Implement 5 category group Typer apps in `src/specfact_cli/groups/` (Phase 2)
28+
- Update bootstrap.py to mount groups with compat shims for flat commands (Phase 2)
29+
- Add `category_grouping_enabled` config flag (default `true`)
30+
- Add first-run bundle selection to `specfact init` with `--profile` and `--install` flags
31+
- Preserve all existing command paths via deprecation shims
32+
33+
**Non-Goals:**
34+
35+
- Extracting module source code to separate packages (module-migration-02)
36+
- Removing bundled modules from pyproject.toml (module-migration-03)
37+
- Publishing bundles to the marketplace registry (module-migration-02)
38+
- Removing backward-compat shims (module-migration-03)
39+
40+
## Decisions
41+
42+
### Decision 1: Category group architecture — separate `groups/` layer
43+
44+
**Options:**
45+
46+
- **A**: Embed grouping logic directly in bootstrap.py
47+
- **B**: New `src/specfact_cli/groups/` package with one file per category
48+
49+
**Choice: B (dedicated `groups/` layer)**
50+
51+
**Rationale:**
52+
53+
- Clean separation of concerns — bootstrap orchestrates, groups define aggregation
54+
- Each group file is independently testable
55+
- Mirrors the `modules/` structure, making the pattern predictable
56+
- Easier to remove in module-migration-03 (delete groups/ directory)
57+
58+
**Structure:**
59+
60+
```text
61+
src/specfact_cli/groups/
62+
__init__.py
63+
project_group.py # aggregates: project, plan, import_cmd, sync, migrate
64+
backlog_group.py # aggregates: backlog, policy_engine
65+
codebase_group.py # aggregates: analyze, drift, validate, repro
66+
spec_group.py # aggregates: contract, spec (as 'api'), sdd, generate
67+
govern_group.py # aggregates: enforce, patch_mode
68+
```
69+
70+
**Group file pattern:**
71+
72+
```python
73+
import typer
74+
from beartype import beartype
75+
from icontract import require
76+
77+
app = typer.Typer(name="code", help="Codebase quality commands.")
78+
79+
@require(lambda: True) # placeholder; real contracts on member loaders
80+
@beartype
81+
def _register_members() -> None:
82+
"""Lazy-register member module sub-apps."""
83+
...
84+
```
85+
86+
### Decision 2: Backward-compat shim strategy
87+
88+
**Options:**
89+
90+
- **A**: Register both flat and grouped commands permanently
91+
- **B**: Register flat commands as shims that delegate to grouped commands
92+
- **C**: Remove flat commands immediately (breaking)
93+
94+
#### Choice: B (shim delegation)
95+
96+
**Rationale:**
97+
98+
- Zero breaking changes for existing scripts
99+
- Deprecation warning in Copilot mode trains users to migrate
100+
- Silent in CI/CD mode (detected from environment)
101+
- Clean removal path: delete shim registrations in module-migration-03
102+
103+
**Implementation:**
104+
105+
```python
106+
# In bootstrap.py, after mounting category groups:
107+
def _register_compat_shims(app: typer.Typer) -> None:
108+
"""Register flat-command shims that delegate to category group equivalents."""
109+
from specfact_cli.common.modes import is_cicd_mode
110+
...
111+
```
112+
113+
### Decision 3: Spec module name collision resolution
114+
115+
The `spec` module's command name (`specfact spec`) collides with the `spec` category group command.
116+
117+
**Choice: Mount the `spec` module as `api` sub-command within the `spec` group**
118+
119+
**Rationale:**
120+
121+
- `specfact spec api validate` is semantically clear (API spec validation)
122+
- The flat shim `specfact spec validate` continues to work during migration window
123+
- Avoids any namespace recursion (spec inside spec group)
124+
125+
**Manifest change in `modules/spec/module-package.yaml`:**
126+
127+
```yaml
128+
bundle_sub_command: api
129+
```
130+
131+
### Decision 4: `category_grouping_enabled` config flag storage
132+
133+
**Choice: Stored in `~/.specfact/config.yaml` under key `category_grouping_enabled: true`**
134+
135+
**Rationale:**
136+
137+
- Consistent with existing specfact config conventions
138+
- Easy to disable per-machine during rollout
139+
- Read once at CLI startup, passed down to bootstrap
140+
141+
### Decision 5: First-run detection mechanism
142+
143+
#### Choice: Check whether any category bundle is installed; if none, treat as first-run
144+
145+
**Rationale:**
146+
147+
- Simple and reliable — no additional state file needed
148+
- Idempotent: re-running init after bundles are installed skips selection
149+
- Compatible with `--install all` legacy flag (bypasses first-run UI)
150+
151+
### Decision 6: Bundle-level dependency resolution at init time
152+
153+
#### Choice: Delegate to marketplace-02 dependency resolver
154+
155+
**Rationale:**
156+
157+
- marketplace-02 owns the dependency resolution contract
158+
- Avoids duplicating resolution logic in init
159+
- At init time, spec and govern bundles automatically pull project bundle as dep
160+
- If marketplace-02 is not yet complete, init can warn and skip dep resolution (graceful degradation)
161+
162+
## Architecture
163+
164+
### Data flow: CLI startup with grouping enabled
165+
166+
```text
167+
specfact <command> <args>
168+
169+
├─ cli.py: cli_main()
170+
│ └─ loads root typer.Typer app
171+
172+
├─ registry/bootstrap.py: bootstrap_cli(app)
173+
│ ├─ reads category_grouping_enabled from config
174+
│ ├─ if enabled:
175+
│ │ ├─ groups/codebase_group.app → app.add_typer(name="code")
176+
│ │ ├─ groups/backlog_group.app → app.add_typer(name="backlog")
177+
│ │ ├─ groups/project_group.app → app.add_typer(name="project")
178+
│ │ ├─ groups/spec_group.app → app.add_typer(name="spec")
179+
│ │ ├─ groups/govern_group.app → app.add_typer(name="govern")
180+
│ │ └─ _register_compat_shims(app) ← flat shims
181+
│ └─ core modules always mounted flat: init, auth, module, upgrade
182+
183+
└─ registry/registry.py: lazy-load member module on invocation
184+
```
185+
186+
### Category group file structure (example: codebase_group.py)
187+
188+
```python
189+
"""Codebase quality category group."""
190+
import typer
191+
from beartype import beartype
192+
from icontract import ensure, require
193+
194+
from specfact_cli.registry.registry import get_module_app
195+
196+
app = typer.Typer(
197+
name="code",
198+
help="Codebase quality commands: analyze, drift, validate, repro.",
199+
no_args_is_help=True,
200+
)
201+
202+
_MEMBERS = ("analyze", "drift", "validate", "repro")
203+
204+
@require(lambda: True)
205+
@ensure(lambda result: result is None)
206+
@beartype
207+
def _register_members() -> None:
208+
for name in _MEMBERS:
209+
member_app = get_module_app(name)
210+
if member_app is not None:
211+
app.add_typer(member_app, name=name)
212+
213+
_register_members()
214+
```
215+
216+
### module-package.yaml additions
217+
218+
```yaml
219+
# Example: modules/validate/module-package.yaml (additions only)
220+
category: codebase
221+
bundle: specfact-codebase
222+
bundle_group_command: code
223+
bundle_sub_command: validate
224+
```
225+
226+
### Compat shim pattern (bootstrap.py)
227+
228+
```python
229+
def _make_shim(category_group_cmd: str, sub_cmd: str) -> Callable[..., Any]:
230+
"""Return a callback that delegates to the category group sub-command."""
231+
from specfact_cli.common.modes import is_cicd_mode
232+
from specfact_cli.common import get_bridge_logger
233+
234+
logger = get_bridge_logger(__name__)
235+
236+
def shim(*args: Any, **kwargs: Any) -> None:
237+
if not is_cicd_mode():
238+
console.print(
239+
f"[yellow]Note: `specfact {sub_cmd}` is deprecated. "
240+
f"Use `specfact {category_group_cmd} {sub_cmd}` instead.[/yellow]"
241+
)
242+
# delegate to the group sub-command
243+
...
244+
245+
return shim
246+
```
247+
248+
### First-run init flow
249+
250+
```text
251+
specfact init
252+
253+
├─ detect first-run: any category bundle installed? → No
254+
255+
├─ Copilot mode?
256+
│ ├─ Yes → show interactive multi-select UI (rich prompt)
257+
│ │ user picks bundles or profile preset
258+
│ │ confirm → install selected bundles via module_installer
259+
│ └─ No → skip selection (core-only install)
260+
261+
└─ --profile / --install flags?
262+
├─ --profile <name> → resolve canonical bundle list → install
263+
└─ --install <list> → parse comma-separated bundle names → install
264+
```
265+
266+
## Risks / Trade-offs
267+
268+
### Risk 1: Spec module name collision causes routing confusion
269+
270+
**Mitigation**: Mount spec module as `api` sub-command within spec group; flat shim `specfact spec <sub>` delegates correctly. Covered by explicit spec in `category-command-groups`.
271+
272+
### Risk 2: CLI startup time regression from group loading
273+
274+
**Mitigation**: Groups use the same lazy-loading pattern as the existing registry — member sub-apps are imported only on first invocation of a sub-command, not at startup.
275+
276+
### Risk 3: `sync``plan` circular-ish dependency across group files
277+
278+
**Mitigation**: Both `sync` and `plan` are in the `project` category group — no cross-group dependency. The circular import is intra-group and resolved by Typer's deferred registration.
279+
280+
### Risk 4: Compat shims add noise to `specfact --help`
281+
282+
**Mitigation**: Shim entries are marked `deprecated=True` in Typer; displayed with visual annotation. Users who want a clean help can set `category_grouping_enabled: true` (already default) and accept the new group layout.
283+
284+
### Risk 5: marketplace-02 not yet complete when this change is implemented
285+
286+
**Mitigation**: Phase 1 (metadata only) has no dependency on marketplace-02. Phase 2 (group commands) can be implemented and merged without marketplace-02 bundle dep resolution as long as the bundle dependency installation at init time gracefully degrades to a warning.
287+
288+
## Migration Plan
289+
290+
### Phase 1 — Metadata only (no code movement, no behavior change)
291+
292+
1. Add `category`, `bundle`, `bundle_group_command`, `bundle_sub_command` to all 21 module-package.yaml files
293+
2. Add manifest validation in registry/module_packages.py for new fields
294+
3. Run module signing gate: `hatch run ./scripts/verify-modules-signature.py --require-signature`
295+
4. Re-sign all 21 manifests
296+
297+
### Phase 2 — Category group commands
298+
299+
1. Create `src/specfact_cli/groups/` with 5 group files + `__init__.py`
300+
2. Update `bootstrap.py` to mount groups when `category_grouping_enabled` is `true`
301+
3. Add `_register_compat_shims()` for all 17 non-core flat commands
302+
4. Update `cli.py` to register category groups
303+
304+
### Phase 3 — First-run init enhancement
305+
306+
1. Add `--profile` and `--install` parameters to `specfact init`
307+
2. Implement first-run detection and interactive bundle selection UI
308+
3. Wire bundle installation through existing `module_installer`
309+
310+
### Rollback
311+
312+
1. Set `category_grouping_enabled: false` in `~/.specfact/config.yaml` (immediate, no code change)
313+
2. If code rollback needed: revert `bootstrap.py` (remove group mounting) and delete `groups/` directory
314+
315+
## Open Questions
316+
317+
**Q1: Should the migration window be one major version or time-boxed?**
318+
319+
- Recommendation: One major version (e.g., v0.x → v1.0 removes shims). Easier to communicate in changelog.
320+
321+
**Q2: Should `specfact --help` show shim commands alongside group commands by default?**
322+
323+
- Recommendation: Yes for the migration window, to avoid breaking muscle memory. Add deprecation annotation to shim entries.
324+
325+
**Q3: Should first-run selection be skippable with a `--no-interactive` flag?**
326+
327+
- Recommendation: CI/CD mode auto-detection handles this. Add `--no-interactive` as an explicit opt-out for edge cases.

0 commit comments

Comments
 (0)