fix(presets): seed constitution from preset constitution-template (#3272)#3276
fix(presets): seed constitution from preset constitution-template (#3272)#3276BenBtg wants to merge 3 commits into
Conversation
) The constitution is the only template materialized to a live file (.specify/memory/constitution.md) rather than resolved on demand, yet ensure_constitution_from_template hardcoded a copy from the core template and ignored PresetResolver. Combined with init seeding the constitution before preset installation, a preset's constitution-template (e.g. strategy: replace with a ratified constitution) could never go live. Changes: - ensure_constitution_from_template now resolves constitution-template through PresetResolver, so a preset/override/extension wins and core is the fallback. - init seeds the constitution after preset installation so init --preset uses the resolved stack. - install_from_directory re-seeds memory/constitution.md from the resolved preset template, guarded to only act when the memory file is missing or still contains generic placeholder tokens — authored constitutions are never overwritten. Covers preset add and install_from_zip. - Tests for preset seeding, placeholder re-seed, authored-constitution preservation, override resolution, and resolver-aware init seeding. Assisted-by: GitHub Copilot (model: Claude Opus 4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
+1 — we hit this independently. We maintain a spec-kit harness for a team with a fixed, ratified constitution authored upstream in a canonical repo and installed verbatim downstream. We read #3276's diff against our use case, and it handles our edge cases correctly:
One small maintainability suggestion: Our current stopgap is (Disclosure, per CONTRIBUTING: this comment was drafted with the assistance of Claude Code (an AI coding agent) and reviewed by a human before posting.) |
| template_constitution = PresetResolver(project_path).resolve( | ||
| "constitution-template", "template" | ||
| ) |
| resolved = PresetResolver(self.project_root).resolve( | ||
| "constitution-template", "template" | ||
| ) | ||
| if resolved is None or not resolved.exists(): | ||
| return | ||
|
|
||
| try: | ||
| memory_constitution.parent.mkdir(parents=True, exist_ok=True) | ||
| shutil.copy2(resolved, memory_constitution) |
Take on review feedback from Copilot and gglachant:
- constitution seeding previously copied the top layer file path verbatim
even when the winning layer used a composing strategy
(prepend/append/wrap), which could leave {CORE_TEMPLATE} unresolved.
- both seeding paths now inspect resolver layers and only copy verbatim for
replace; non-replace strategies materialize composed content via
PresetResolver.resolve_content().
- add regression tests for wrap strategy composition in both
PresetManager seeding and ensure_constitution_from_template.
- add a drift-guard test pinning _CONSTITUTION_PLACEHOLDER_TOKENS to the
placeholders in templates/constitution-template.md.
Assisted-by: GitHub Copilot (model: GPT-5.3-Codex, autonomous)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Addressed review feedback in f954e30.\n\n- Fixed constitution seeding to handle composing strategies (//) in both paths ( and ): we now materialize composed content via whenever the winning layer is non-.\n- Added regression tests that verify wrap composition is materialized (no unresolved ) in both seeding paths.\n- Added a drift-guard test that pins to placeholders present in the core , per gglachant’s maintainability suggestion.\n\nPosted on behalf of @BenBtg by GitHub Copilot (model: gpt-5.3-codex). |
|
Correction to my previous comment (shell quoting ate formatting). Addressed review feedback in f954e30.
Posted on behalf of @BenBtg by GitHub Copilot (model: gpt-5.3-codex). |
| memory_constitution = project_path / ".specify" / "memory" / "constitution.md" | ||
| template_constitution = ( | ||
| project_path / ".specify" / "templates" / "constitution-template.md" | ||
| ) | ||
| resolver = PresetResolver(project_path) | ||
| layers = resolver.collect_all_layers("constitution-template", "template") | ||
|
|
| if tracker: | ||
| tracker.add("constitution", "Constitution setup") | ||
| tracker.complete("constitution", "copied from template") |
| 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") | ||
| except OSError as exc: |
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>
|
Addressed the latest Copilot feedback in aa09272.
Posted on behalf of @BenBtg by GitHub Copilot (model: gpt-5.3-codex). |
Summary
Fixes #3272. A preset that ships a
type: templateconstitution-templateentry (e.g.strategy: replacewith a ratified constitution) was never applied to.specify/memory/constitution.md. The constitution is the only template materialized to a live file rather than resolved on demand, yetensure_constitution_from_templatehardcoded a copy from the core template and bypassedPresetResolver. Combined withinitseeding the constitution before preset installation, a preset's constitution could never go live.This is a provenance fix (resolve through the same priority stack every other template uses), not a copy-into-core fix.
Changes
commands/init.py—ensure_constitution_from_templatenow resolvesconstitution-templatethroughPresetResolver(project overrides → installed presets → extensions → core). Core remains the fallback when nothing overrides it. Skip-if-exists / not-found behavior preserved.commands/init.py— reordered so the constitution is seeded after preset installation, sospecify init --presetseeds from the resolved stack. No-op for non-preset init.presets/__init__.py—install_from_directoryre-seeds.specify/memory/constitution.mdfrom the resolved preset template, only when the memory file is missing or still contains generic placeholder tokens ([PROJECT_NAME]/[PRINCIPLE_1_NAME]). Authored constitutions are never overwritten. Coversspecify preset addandinstall_from_zip(which delegates here).CORE_TEMPLATE_NAMESoverride resolvesconstitution-templateto the preset file; resolver-aware init seeding (core fallback, preset override, existing-file preservation).Key guard
Placeholder-token detection (
_constitution_is_placeholder) prevents clobbering legitimately authored constitutions duringpreset addon an existing project.Testing
pytest tests/test_presets.py— 333 passed (7 new)This PR was authored autonomously by GitHub Copilot (model: Claude Opus 4.8). Each commit carries an
Assisted-by:trailer.