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
4 changes: 3 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2563,7 +2563,9 @@ cat .map/workflow_logs/feat_auth_20251023_143022.json | jq '.subtasks[].agents.e
All remaining open issues are enhancements (no bugs as of 2026-07-18). Prioritized by concreteness:

### #291 — SpecKit-style preset composition (layered template resolution)
Adds `.map/presets/` directory, 4-tier resolution (project → preset → extension → core), composition strategies (prepend/append/wrap/replace), `mapify preset add/remove/enable/disable/list/resolve` commands, and extension hook lifecycle. First slice: directory structure, YAML manifest format, and `mapify preset list`. Key constraint: must not bypass the `make check-render` single-source invariant — presets should compose at render time, not by editing generated trees.
**Slice 1 COMPLETE (PR #370, 2026-07-18)**: `mapify preset` sub-group with `list` and `add --from <path>` commands; `.map/presets/<id>/manifest.json` format (required keys: `id`, `title`, `version`); path-traversal guards; `--json` flag; `--force` overwrite; 29 tests in `tests/test_preset_commands.py`.

**Next slice (Slice 2)**: `mapify preset remove <id>`, `mapify preset enable/disable <id>` (via `.enabled` flag in manifest or sidecar), and `mapify preset resolve <template>` (show which layer wins for a template). No composition engine yet — that's Slice 3. Key constraint: must not bypass the `make check-render` single-source invariant — presets should compose at render time, not by editing generated trees.

### #363 — Architecture deepening report (`/map-architecture` skill)
New opt-in skill that ranks codebase areas by recent git hotspot + design friction, generates a candidate report (HTML or Markdown+Mermaid under `.map/<branch>/architecture-report/`), and holds off implementation until the user picks a candidate. First slice: create `src/mapify_cli/templates_src/skills/map-architecture/SKILL.md.jinja` + register in skill-rules.json. No Python code in slice 1 — just the workflow instructions.
Expand Down
161 changes: 161 additions & 0 deletions src/mapify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1907,6 +1907,167 @@ def preset_add(
)


def _preset_state_path(preset_dir: Path) -> Path:
return preset_dir / ".state.json"


def _read_preset_state(preset_dir: Path) -> dict[str, Any]:
path = _preset_state_path(preset_dir)
if not path.is_file():
return {}
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {}


def _write_preset_state(preset_dir: Path, state: dict[str, Any]) -> None:
_preset_state_path(preset_dir).write_text(json.dumps(state, indent=2), encoding="utf-8")


def _is_preset_enabled(preset_dir: Path) -> bool:
return _read_preset_state(preset_dir).get("enabled", True)


def _resolve_installed_preset(presets_root: Path, preset_id: str) -> Path | None:
candidate = presets_root / preset_id
return candidate if candidate.is_dir() else None


@preset_app.command("remove")
def preset_remove(
preset_id: str = typer.Argument(..., help="ID of the preset to remove."),
project_path: Optional[Path] = typer.Argument(
None,
help="Project root directory (defaults to current directory).",
),
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
) -> None:
"""Remove an installed MAP preset from .map/presets/."""
target = project_path or Path.cwd()
preset_dir = _resolve_installed_preset(_presets_dir(target), preset_id)
if preset_dir is None:
console.print(f"[red]Error:[/red] Preset '{preset_id}' is not installed.")
raise typer.Exit(1)

if not yes:
confirm = typer.confirm(f"Remove preset '{preset_id}'?", default=False)
if not confirm:
console.print("[dim]Aborted.[/dim]")
raise typer.Exit(0)

shutil.rmtree(preset_dir)
console.print(f"[green]Preset '{preset_id}' removed.[/green]")


@preset_app.command("enable")
def preset_enable(
preset_id: str = typer.Argument(..., help="ID of the preset to enable."),
project_path: Optional[Path] = typer.Argument(
None,
help="Project root directory (defaults to current directory).",
),
) -> None:
"""Enable a disabled MAP preset."""
target = project_path or Path.cwd()
preset_dir = _resolve_installed_preset(_presets_dir(target), preset_id)
if preset_dir is None:
console.print(f"[red]Error:[/red] Preset '{preset_id}' is not installed.")
raise typer.Exit(1)

state = _read_preset_state(preset_dir)
state["enabled"] = True
_write_preset_state(preset_dir, state)
console.print(f"[green]Preset '{preset_id}' enabled.[/green]")


@preset_app.command("disable")
def preset_disable(
preset_id: str = typer.Argument(..., help="ID of the preset to disable."),
project_path: Optional[Path] = typer.Argument(
None,
help="Project root directory (defaults to current directory).",
),
) -> None:
"""Disable a MAP preset without uninstalling it."""
target = project_path or Path.cwd()
preset_dir = _resolve_installed_preset(_presets_dir(target), preset_id)
if preset_dir is None:
console.print(f"[red]Error:[/red] Preset '{preset_id}' is not installed.")
raise typer.Exit(1)

state = _read_preset_state(preset_dir)
state["enabled"] = False
_write_preset_state(preset_dir, state)
console.print(f"[yellow]Preset '{preset_id}' disabled.[/yellow]")


@preset_app.command("resolve")
def preset_resolve(
template_name: str = typer.Argument(..., help="Template name to resolve (e.g. 'map-efficient.md')."),
project_path: Optional[Path] = typer.Argument(
None,
help="Project root directory (defaults to current directory).",
),
output_json: bool = typer.Option(False, "--json", help="Output as JSON."),
) -> None:
"""Show which preset layers contribute to a template in resolution order.

Resolution order (highest priority first): project overrides → enabled presets → core templates.
"""
target = project_path or Path.cwd()
presets_root = _presets_dir(target)

layers: list[dict[str, Any]] = []

# Tier 1: project overrides
project_override = target / ".map" / "overrides" / template_name
if project_override.is_file():
layers.append({"tier": "project-override", "path": str(project_override), "enabled": True})

# Tier 2: installed presets (sorted alphabetically for determinism)
if presets_root.is_dir():
for entry in sorted(presets_root.iterdir()):
if not entry.is_dir():
continue
enabled = _is_preset_enabled(entry)
template_path = entry / "templates" / template_name
if template_path.is_file():
manifest = _read_preset_manifest(entry)
strategy = (manifest or {}).get("strategies", {}).get(template_name, "append")
layers.append({
"tier": "preset",
"preset_id": entry.name,
"path": str(template_path),
"strategy": strategy,
"enabled": enabled,
})

# Tier 3: core template (shipped by mapify)
try:
core_path = get_templates_dir() / template_name
if core_path.is_file():
layers.append({"tier": "core", "path": str(core_path), "enabled": True})
except Exception:
pass

if output_json:
typer.echo(json.dumps({"template": template_name, "layers": layers}))
return

if not layers:
console.print(f"[dim]No layers found for template '{template_name}'.[/dim]")
return

console.print(f"[bold]Resolution layers for:[/bold] [cyan]{template_name}[/cyan]")
for i, layer in enumerate(layers, 1):
tier = layer["tier"]
enabled_str = "" if layer.get("enabled", True) else " [dim](disabled)[/dim]"
strategy_str = f" strategy=[cyan]{layer['strategy']}[/cyan]" if "strategy" in layer else ""
preset_str = f" preset=[cyan]{layer['preset_id']}[/cyan]" if "preset_id" in layer else ""
console.print(f" {i}. tier={tier}{preset_str}{strategy_str}{enabled_str} → {layer['path']}")


# Research localization eval commands


Expand Down
181 changes: 181 additions & 0 deletions tests/test_preset_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,184 @@ def test_duplicate_does_not_corrupt_existing_preset(self, tmp_path: Path):
runner.invoke(app, ["preset", "add", "--from", str(src2), str(project)])
data = json.loads((_installed_preset(project, "lean") / "manifest.json").read_text())
assert data["version"] == "1.0.0"


# ---------------------------------------------------------------------------
# PC9 — preset remove
# ---------------------------------------------------------------------------


class TestPc9PresetRemove:
def _install(self, project: Path, name: str) -> None:
dest = project / ".map" / "presets" / name
dest.mkdir(parents=True)
(dest / "manifest.json").write_text(
json.dumps({"id": name, "title": name.title(), "version": "1.0.0"}), encoding="utf-8"
)

def test_remove_existing_preset(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
self._install(project, "lean")
result = runner.invoke(app, ["preset", "remove", "lean", str(project), "--yes"])
assert result.exit_code == 0
assert not (project / ".map" / "presets" / "lean").exists()

def test_remove_success_message(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
self._install(project, "lean")
result = runner.invoke(app, ["preset", "remove", "lean", str(project), "--yes"])
assert "lean" in result.output
assert "removed" in result.output.lower()

def test_remove_nonexistent_exits_nonzero(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
result = runner.invoke(app, ["preset", "remove", "nothere", str(project), "--yes"])
assert result.exit_code != 0

def test_remove_aborted_without_yes(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
self._install(project, "lean")
runner.invoke(app, ["preset", "remove", "lean", str(project)], input="n\n")
assert (project / ".map" / "presets" / "lean").exists()

def test_remove_does_not_affect_other_presets(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
for name in ("lean", "enterprise"):
self._install(project, name)
runner.invoke(app, ["preset", "remove", "lean", str(project), "--yes"])
assert (project / ".map" / "presets" / "enterprise").is_dir()


# ---------------------------------------------------------------------------
# PC10 — preset enable / disable
# ---------------------------------------------------------------------------


class TestPc10PresetEnableDisable:
def _install(self, project: Path, name: str) -> Path:
dest = project / ".map" / "presets" / name
dest.mkdir(parents=True)
(dest / "manifest.json").write_text(
json.dumps({"id": name, "title": name.title(), "version": "1.0.0"}), encoding="utf-8"
)
return dest

def test_disable_preset_writes_state(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
dest = self._install(project, "lean")
result = runner.invoke(app, ["preset", "disable", "lean", str(project)])
assert result.exit_code == 0
state = json.loads((dest / ".state.json").read_text())
assert state["enabled"] is False

def test_enable_preset_writes_state(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
dest = self._install(project, "lean")
runner.invoke(app, ["preset", "disable", "lean", str(project)])
result = runner.invoke(app, ["preset", "enable", "lean", str(project)])
assert result.exit_code == 0
state = json.loads((dest / ".state.json").read_text())
assert state["enabled"] is True

def test_disable_nonexistent_exits_nonzero(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
result = runner.invoke(app, ["preset", "disable", "missing", str(project)])
assert result.exit_code != 0

def test_enable_nonexistent_exits_nonzero(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
result = runner.invoke(app, ["preset", "enable", "missing", str(project)])
assert result.exit_code != 0

def test_preset_enabled_by_default_no_state_file(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
dest = self._install(project, "lean")
assert not (dest / ".state.json").exists()
result = runner.invoke(app, ["preset", "list", str(project), "--json"])
data = json.loads(result.output)
assert len(data["presets"]) == 1


# ---------------------------------------------------------------------------
# PC11 — preset resolve
# ---------------------------------------------------------------------------


class TestPc11PresetResolve:
def _install_preset_with_template(self, project: Path, name: str, template: str, content: str = "# content") -> None:
dest = project / ".map" / "presets" / name
(dest / "templates").mkdir(parents=True, exist_ok=True)
(dest / "manifest.json").write_text(
json.dumps({"id": name, "title": name.title(), "version": "1.0.0",
"strategies": {template: "append"}}), encoding="utf-8"
)
(dest / "templates" / template).write_text(content, encoding="utf-8")

def test_resolve_no_layers_exits_zero(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
result = runner.invoke(app, ["preset", "resolve", "nonexistent.md", str(project)])
assert result.exit_code == 0
assert "No layers" in result.output

def test_resolve_preset_layer_found(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
self._install_preset_with_template(project, "lean", "map-efficient.md")
result = runner.invoke(app, ["preset", "resolve", "map-efficient.md", str(project)])
assert result.exit_code == 0
assert "lean" in result.output
assert "preset" in result.output

def test_resolve_project_override_shown_first(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
(project / ".map" / "overrides").mkdir(parents=True)
(project / ".map" / "overrides" / "map-efficient.md").write_text("# override", encoding="utf-8")
self._install_preset_with_template(project, "lean", "map-efficient.md")
result = runner.invoke(app, ["preset", "resolve", "map-efficient.md", str(project)])
lines = [line for line in result.output.splitlines() if line.strip().startswith(("1.", "2."))]
assert any("project-override" in line for line in lines)

def test_resolve_json_output(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
self._install_preset_with_template(project, "lean", "map-efficient.md")
result = runner.invoke(app, ["preset", "resolve", "map-efficient.md", str(project), "--json"])
assert result.exit_code == 0
data = json.loads(result.output)
assert "template" in data
assert "layers" in data
assert data["template"] == "map-efficient.md"
assert any(layer["tier"] == "preset" for layer in data["layers"])

def test_resolve_disabled_preset_still_shows(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
self._install_preset_with_template(project, "lean", "map-efficient.md")
runner.invoke(app, ["preset", "disable", "lean", str(project)])
result = runner.invoke(app, ["preset", "resolve", "map-efficient.md", str(project), "--json"])
data = json.loads(result.output)
lean_layer = next((layer for layer in data["layers"] if layer.get("preset_id") == "lean"), None)
assert lean_layer is not None
assert lean_layer["enabled"] is False

def test_resolve_strategy_shown(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
self._install_preset_with_template(project, "lean", "map-efficient.md")
result = runner.invoke(app, ["preset", "resolve", "map-efficient.md", str(project), "--json"])
data = json.loads(result.output)
lean_layer = next((layer for layer in data["layers"] if layer.get("preset_id") == "lean"), None)
assert lean_layer is not None
assert lean_layer.get("strategy") == "append"