refactor(presets): unify constitution template materialization

Address latest Copilot feedback on the constitution seeding path:
- moved resolver/layer I/O behind the existing-memory fast path in init
- corrected tracker output for composed materialization
- deduplicated materialization logic shared by init and preset install seeding
  into presets._materialize_constitution_template()

Behavior is unchanged for replace strategies (copy verbatim) and remains
composed for prepend/append/wrap via resolve_content().

Assisted-by: GitHub Copilot (model: GPT-5.3-Codex, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Ben Buttigieg
2026-07-01 19:53:21 +01:00
parent f954e30a66
commit aa09272873
2 changed files with 47 additions and 38 deletions

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
import os
import shutil
import sys
from pathlib import Path
from typing import Any
@@ -41,11 +40,9 @@ def ensure_constitution_from_template(
constitution) can seed the memory file. When nothing overrides it, the
resolver falls through to the core template.
"""
from ..presets import PresetResolver
from ..presets import _materialize_constitution_template
memory_constitution = project_path / ".specify" / "memory" / "constitution.md"
resolver = PresetResolver(project_path)
layers = resolver.collect_all_layers("constitution-template", "template")
if memory_constitution.exists():
if tracker:
@@ -53,27 +50,21 @@ def ensure_constitution_from_template(
tracker.skip("constitution", "existing file preserved")
return
if not layers:
if tracker:
tracker.add("constitution", "Constitution setup")
tracker.error("constitution", "template not found")
return
try:
memory_constitution.parent.mkdir(parents=True, exist_ok=True)
top_layer = layers[0]
if top_layer["strategy"] == "replace":
shutil.copy2(top_layer["path"], memory_constitution)
else:
composed_content = resolver.resolve_content(
"constitution-template", "template"
)
if composed_content is None:
raise FileNotFoundError("constitution template not found")
memory_constitution.write_text(composed_content, encoding="utf-8")
materialization = _materialize_constitution_template(
project_path, memory_constitution
)
if materialization is None:
if tracker:
tracker.add("constitution", "Constitution setup")
tracker.error("constitution", "template not found")
return
if tracker:
tracker.add("constitution", "Constitution setup")
tracker.complete("constitution", "copied from template")
if materialization == "copied":
tracker.complete("constitution", "copied from template")
else:
tracker.complete("constitution", "composed from template")
else:
console.print("[cyan]Initialized constitution from template[/cyan]")
except Exception as e:

View File

@@ -45,6 +45,35 @@ def _constitution_is_placeholder(content: str) -> bool:
return any(token in content for token in _CONSTITUTION_PLACEHOLDER_TOKENS)
def _materialize_constitution_template(
project_root: Path,
memory_constitution: Path,
) -> str | None:
"""Materialize constitution-template content into memory/constitution.md.
Returns:
"copied" when the winning layer is ``replace`` and the source file is
copied verbatim; "composed" when a composing strategy is materialized
via ``resolve_content``; ``None`` when no constitution template resolves.
"""
resolver = PresetResolver(project_root)
layers = resolver.collect_all_layers("constitution-template", "template")
if not layers:
return None
memory_constitution.parent.mkdir(parents=True, exist_ok=True)
top_layer = layers[0]
if top_layer["strategy"] == "replace":
shutil.copy2(top_layer["path"], memory_constitution)
return "copied"
composed_content = resolver.resolve_content("constitution-template", "template")
if composed_content is None:
return None
memory_constitution.write_text(composed_content, encoding="utf-8")
return "composed"
def _substitute_core_template(
body: str,
cmd_name: str,
@@ -1664,23 +1693,12 @@ class PresetManager:
# Legitimately authored constitution; leave it untouched.
return
resolver = PresetResolver(self.project_root)
layers = resolver.collect_all_layers("constitution-template", "template")
if not layers:
return
try:
memory_constitution.parent.mkdir(parents=True, exist_ok=True)
top_layer = layers[0]
if top_layer["strategy"] == "replace":
shutil.copy2(top_layer["path"], memory_constitution)
else:
composed_content = resolver.resolve_content(
"constitution-template", "template"
)
if composed_content is None:
return
memory_constitution.write_text(composed_content, encoding="utf-8")
result = _materialize_constitution_template(
self.project_root, memory_constitution
)
if result is None:
return
except OSError as exc:
import warnings