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 @@ -2565,7 +2565,9 @@ All remaining open issues are enhancements (no bugs as of 2026-07-18). Prioritiz
### #291 — SpecKit-style preset composition (layered template resolution)
**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.
**Slice 2 COMPLETE (PR #371, 2026-07-18)**: `mapify preset remove <id>` (with `--yes` bypass), `mapify preset enable/disable <id>` (persisted to `.map/presets/<id>/.state.json`), `mapify preset resolve <template>` (3-tier: project overrides → enabled presets → core templates; `--json`). 45 tests total in `tests/test_preset_commands.py`.

**Next slice (Slice 3)**: The composition engine — when a preset's `templates/<name>` exists, apply the preset's strategy (`prepend`/`append`/`wrap`/`replace`) at render time. Add `mapify preset set-priority <id> <n>` to control ordering. Key constraint: must not bypass the `make check-render` single-source invariant — presets should compose at render time, not by editing generated trees. The composition should be a separate render pass (`mapify preset render <template>`) that layers preset content on top of the Jinja-rendered core output.

### #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
146 changes: 146 additions & 0 deletions src/mapify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2068,6 +2068,152 @@ def preset_resolve(
console.print(f" {i}. tier={tier}{preset_str}{strategy_str}{enabled_str} → {layer['path']}")


# ---------------------------------------------------------------------------
# Preset composition helpers (Slice 3)
# ---------------------------------------------------------------------------

_COMPOSITION_STRATEGIES = frozenset({"replace", "prepend", "append", "wrap"})
_WRAP_PLACEHOLDER = "{CORE_TEMPLATE}"


def _preset_priority(preset_dir: Path) -> int:
state = _read_preset_state(preset_dir)
return int(state.get("priority", 50))


def _compose_template(core_content: str, layer_content: str, strategy: str) -> str:
"""Apply a composition strategy to produce the final template content."""
if strategy == "replace":
return layer_content
if strategy == "prepend":
return layer_content + "\n" + core_content
if strategy == "append":
return core_content + "\n" + layer_content
if strategy == "wrap":
if _WRAP_PLACEHOLDER in layer_content:
return layer_content.replace(_WRAP_PLACEHOLDER, core_content)
return layer_content + "\n" + core_content
return core_content


def _build_resolution_order(presets_root: Path, template_name: str) -> list[dict[str, Any]]:
"""Return enabled preset layers for a template, sorted by priority descending."""
layers: list[dict[str, Any]] = []
if not presets_root.is_dir():
return layers
for entry in presets_root.iterdir():
if not entry.is_dir() or not _is_preset_enabled(entry):
continue
template_path = entry / "templates" / template_name
if not template_path.is_file():
continue
manifest = _read_preset_manifest(entry)
strategy = (manifest or {}).get("strategies", {}).get(template_name, "append")
layers.append({
"preset_id": entry.name,
"path": template_path,
"strategy": strategy,
"priority": _preset_priority(entry),
})
layers.sort(key=lambda x: x["priority"], reverse=True)
return layers


@preset_app.command("render")
def preset_render(
template_name: str = typer.Argument(..., help="Template name to render (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 rendered content as JSON."),
dry_run: bool = typer.Option(False, "--dry-run", help="Print composed content without writing to disk."),
) -> None:
"""Compose a template by layering enabled presets over the core template.

Strategies (applied highest-priority first):
replace — preset content replaces the core entirely
prepend — preset content is inserted above the core
append — preset content is inserted below the core
wrap — preset content wraps the core via {CORE_TEMPLATE} placeholder
"""
target = project_path or Path.cwd()
presets_root = _presets_dir(target)

# Start from project override if present, else core template
project_override = target / ".map" / "overrides" / template_name
if project_override.is_file():
composed = project_override.read_text(encoding="utf-8")
source = f"project-override:{project_override}"
else:
try:
core_path = get_templates_dir() / template_name
if core_path.is_file():
composed = core_path.read_text(encoding="utf-8")
source = f"core:{core_path}"
else:
composed = ""
source = "core:(not found)"
except Exception:
composed = ""
source = "core:(error)"

layers = _build_resolution_order(presets_root, template_name)
applied: list[str] = []
for layer in layers:
layer_content = Path(layer["path"]).read_text(encoding="utf-8")
strategy: str = layer["strategy"]
if strategy not in _COMPOSITION_STRATEGIES:
strategy = "append"
composed = _compose_template(composed, layer_content, strategy)
applied.append(f"{layer['preset_id']}({strategy})")

if output_json:
typer.echo(json.dumps({
"template": template_name,
"source": source,
"applied_layers": applied,
"content": composed,
}))
return

if dry_run or True:
console.print(f"[bold]Composed:[/bold] [cyan]{template_name}[/cyan]")
if applied:
console.print(f"[dim]Layers applied:[/dim] {' → '.join(applied)}")
else:
console.print("[dim]No preset layers matched; showing core/override content.[/dim]")
console.print()
console.print(composed)


@preset_app.command("set-priority")
def preset_set_priority(
preset_id: str = typer.Argument(..., help="ID of the preset to reprioritize."),
priority: int = typer.Argument(..., help="Priority value (higher = applied first). Default: 50."),
project_path: Optional[Path] = typer.Argument(
None,
help="Project root directory (defaults to current directory).",
),
) -> None:
"""Set the composition priority of an installed preset.

Higher priority presets are applied first in the composition stack.
When two presets target the same template, the one with higher priority
has its strategy applied first, then lower-priority presets layer on top.
"""
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["priority"] = priority
_write_preset_state(preset_dir, state)
console.print(f"[green]Preset '{preset_id}'[/green] priority set to {priority}.")


# Research localization eval commands


Expand Down
172 changes: 172 additions & 0 deletions tests/test_preset_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -511,3 +511,175 @@ def test_resolve_strategy_shown(self, tmp_path: Path):
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"


# ---------------------------------------------------------------------------
# PC12 — preset render: composition engine
# ---------------------------------------------------------------------------


class TestPc12PresetRender:
def _install_preset_with_template(self, project: Path, name: str, template: str,
content: str, strategy: str = "append", priority: int = 50) -> 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: strategy}}), encoding="utf-8"
)
(dest / "templates" / template).write_text(content, encoding="utf-8")
if priority != 50:
(dest / ".state.json").write_text(
json.dumps({"enabled": True, "priority": priority}), encoding="utf-8"
)

def _install_core_override(self, project: Path, template: str, content: str) -> None:
(project / ".map" / "overrides").mkdir(parents=True, exist_ok=True)
(project / ".map" / "overrides" / template).write_text(content, encoding="utf-8")

def test_render_no_presets_exits_zero(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
result = runner.invoke(app, ["preset", "render", "map-efficient.md", str(project)])
assert result.exit_code == 0

def test_render_append_strategy(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
self._install_core_override(project, "map-efficient.md", "CORE")
self._install_preset_with_template(project, "lean", "map-efficient.md", "PRESET", strategy="append")
result = runner.invoke(app, ["preset", "render", "map-efficient.md", str(project), "--json"])
assert result.exit_code == 0
data = json.loads(result.output)
content = data["content"]
assert content.index("CORE") < content.index("PRESET")

def test_render_prepend_strategy(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
self._install_core_override(project, "map-efficient.md", "CORE")
self._install_preset_with_template(project, "lean", "map-efficient.md", "PRESET", strategy="prepend")
result = runner.invoke(app, ["preset", "render", "map-efficient.md", str(project), "--json"])
data = json.loads(result.output)
content = data["content"]
assert content.index("PRESET") < content.index("CORE")

def test_render_replace_strategy(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
self._install_core_override(project, "map-efficient.md", "CORE")
self._install_preset_with_template(project, "lean", "map-efficient.md", "REPLACED", strategy="replace")
result = runner.invoke(app, ["preset", "render", "map-efficient.md", str(project), "--json"])
data = json.loads(result.output)
assert data["content"].strip() == "REPLACED"
assert "CORE" not in data["content"]

def test_render_wrap_strategy_with_placeholder(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
self._install_core_override(project, "map-efficient.md", "CORE")
wrap_content = "BEFORE\n{CORE_TEMPLATE}\nAFTER"
self._install_preset_with_template(project, "lean", "map-efficient.md", wrap_content, strategy="wrap")
result = runner.invoke(app, ["preset", "render", "map-efficient.md", str(project), "--json"])
data = json.loads(result.output)
content = data["content"]
assert "BEFORE" in content
assert "CORE" in content
assert "AFTER" in content
assert content.index("BEFORE") < content.index("CORE") < content.index("AFTER")

def test_render_json_has_required_fields(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
result = runner.invoke(app, ["preset", "render", "any.md", str(project), "--json"])
data = json.loads(result.output)
for key in ("template", "source", "applied_layers", "content"):
assert key in data, f"missing key: {key}"

def test_render_applied_layers_listed(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
self._install_core_override(project, "map-efficient.md", "CORE")
self._install_preset_with_template(project, "lean", "map-efficient.md", "L", strategy="append")
result = runner.invoke(app, ["preset", "render", "map-efficient.md", str(project), "--json"])
data = json.loads(result.output)
assert len(data["applied_layers"]) == 1
assert "lean" in data["applied_layers"][0]

def test_render_disabled_preset_skipped(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
self._install_core_override(project, "map-efficient.md", "CORE")
self._install_preset_with_template(project, "lean", "map-efficient.md", "PRESET", strategy="append")
runner.invoke(app, ["preset", "disable", "lean", str(project)])
result = runner.invoke(app, ["preset", "render", "map-efficient.md", str(project), "--json"])
data = json.loads(result.output)
assert data["applied_layers"] == []
assert "PRESET" not in data["content"]

def test_render_project_override_used_as_base(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
self._install_core_override(project, "map-efficient.md", "OVERRIDE_BASE")
result = runner.invoke(app, ["preset", "render", "map-efficient.md", str(project), "--json"])
data = json.loads(result.output)
assert "OVERRIDE_BASE" in data["content"]
assert "project-override" in data["source"]


# ---------------------------------------------------------------------------
# PC13 — preset set-priority
# ---------------------------------------------------------------------------


class TestPc13PresetSetPriority:
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_set_priority_writes_state(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
dest = self._install(project, "lean")
result = runner.invoke(app, ["preset", "set-priority", "lean", "100", str(project)])
assert result.exit_code == 0
state = json.loads((dest / ".state.json").read_text())
assert state["priority"] == 100

def test_set_priority_nonexistent_exits_nonzero(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
result = runner.invoke(app, ["preset", "set-priority", "missing", "100", str(project)])
assert result.exit_code != 0

def test_priority_affects_composition_order(self, tmp_path: Path):
project = tmp_path / "project"
project.mkdir()
(project / ".map" / "overrides").mkdir(parents=True)
(project / ".map" / "overrides" / "t.md").write_text("CORE", encoding="utf-8")

for name, content, prio in [("alpha", "ALPHA", 10), ("beta", "BETA", 90)]:
dest = project / ".map" / "presets" / name
(dest / "templates").mkdir(parents=True)
(dest / "manifest.json").write_text(
json.dumps({"id": name, "title": name, "version": "1.0",
"strategies": {"t.md": "append"}}), encoding="utf-8"
)
(dest / "templates" / "t.md").write_text(content, encoding="utf-8")
(dest / ".state.json").write_text(
json.dumps({"enabled": True, "priority": prio}), encoding="utf-8"
)

result = runner.invoke(app, ["preset", "render", "t.md", str(project), "--json"])
data = json.loads(result.output)
content = data["content"]
# beta (priority 90) applied first → appended first after core
# alpha (priority 10) applied second → appended after that
beta_pos = content.index("BETA")
alpha_pos = content.index("ALPHA")
core_pos = content.index("CORE")
assert core_pos < beta_pos < alpha_pos