@@ -2597,6 +2597,40 @@ def install_self_test_preset(manager: PresetManager, speckit_version: str = "0.1
25972597 return manager .install_from_directory (SELF_TEST_PRESET_DIR , speckit_version )
25982598
25992599
2600+ def _make_convention_constitution_preset (temp_dir : Path ) -> Path :
2601+ """Create a preset whose constitution is found by convention, not its manifest."""
2602+ preset_dir = temp_dir / "convention-constitution"
2603+ (preset_dir / "templates" ).mkdir (parents = True )
2604+ (preset_dir / "templates" / "constitution-template.md" ).write_text (
2605+ "# Convention Constitution\n "
2606+ )
2607+ (preset_dir / "templates" / "spec-template.md" ).write_text ("# Spec\n " )
2608+ (preset_dir / "preset.yml" ).write_text (
2609+ yaml .dump (
2610+ {
2611+ "schema_version" : "1.0" ,
2612+ "preset" : {
2613+ "id" : "convention-constitution" ,
2614+ "name" : "Convention Constitution" ,
2615+ "version" : "1.0.0" ,
2616+ "description" : "Convention-based constitution for testing" ,
2617+ },
2618+ "requires" : {"speckit_version" : ">=0.1.0" },
2619+ "provides" : {
2620+ "templates" : [
2621+ {
2622+ "type" : "template" ,
2623+ "name" : "spec-template" ,
2624+ "file" : "templates/spec-template.md" ,
2625+ }
2626+ ]
2627+ },
2628+ }
2629+ )
2630+ )
2631+ return preset_dir
2632+
2633+
26002634class TestSelfTestPreset :
26012635 """Tests using the self-test preset that ships with the repo.
26022636
@@ -2809,13 +2843,15 @@ def test_self_test_seeds_constitution_when_memory_absent(self, project_dir):
28092843
28102844 def test_self_test_reseeds_exact_core_constitution (self , project_dir ):
28112845 """An unchanged core constitution is re-seeded from the preset template."""
2812- core = "# [PROJECT_NAME] Constitution \n \n ### [PRINCIPLE_1_NAME] \n "
2813- ( project_dir / ".specify" / "templates" / "constitution-template.md" ). write_text (
2814- core
2846+ resolver = PresetResolver ( project_dir )
2847+ bundled_core = resolver . _find_bundled_core (
2848+ "constitution-template" , "template" , ".md"
28152849 )
2850+ assert bundled_core is not None
2851+ core = bundled_core .read_bytes ()
28162852 memory = project_dir / ".specify" / "memory" / "constitution.md"
28172853 memory .parent .mkdir (parents = True , exist_ok = True )
2818- memory .write_text (core )
2854+ memory .write_bytes (core )
28192855
28202856 manager = PresetManager (project_dir )
28212857 install_self_test_preset (manager )
@@ -2824,6 +2860,69 @@ def test_self_test_reseeds_exact_core_constitution(self, project_dir):
28242860 assert "preset:self-test" in content , "placeholder constitution was not re-seeded"
28252861 assert "[PROJECT_NAME]" not in content
28262862
2863+ def test_self_test_preserves_mutable_project_core_copy (self , project_dir ):
2864+ """A project template copy does not establish generated provenance."""
2865+ authored = "# Acme Organization Constitution\n \n Organization policy.\n "
2866+ project_template = (
2867+ project_dir / ".specify" / "templates" / "constitution-template.md"
2868+ )
2869+ project_template .write_text (authored )
2870+ memory = project_dir / ".specify" / "memory" / "constitution.md"
2871+ memory .parent .mkdir (parents = True , exist_ok = True )
2872+ memory .write_text (authored )
2873+
2874+ manager = PresetManager (project_dir )
2875+ install_self_test_preset (manager )
2876+
2877+ assert memory .read_text () == authored
2878+ assert not (memory .parent / ".constitution-template.json" ).exists ()
2879+
2880+ def test_core_prefixed_preset_does_not_establish_generated_provenance (
2881+ self , project_dir , temp_dir
2882+ ):
2883+ """A preset ID beginning with core is not an immutable core source."""
2884+ authored = "# Acme Organization Constitution\n \n Organization policy.\n "
2885+ memory = project_dir / ".specify" / "memory" / "constitution.md"
2886+ memory .parent .mkdir (parents = True , exist_ok = True )
2887+ memory .write_text (authored )
2888+
2889+ preset_dir = temp_dir / "core-company"
2890+ (preset_dir / "templates" ).mkdir (parents = True )
2891+ (preset_dir / "templates" / "constitution-template.md" ).write_text (authored )
2892+ (preset_dir / "preset.yml" ).write_text (
2893+ yaml .safe_dump (
2894+ {
2895+ "schema_version" : "1.0" ,
2896+ "preset" : {
2897+ "id" : "core-company" ,
2898+ "name" : "Core Company" ,
2899+ "version" : "1.0.0" ,
2900+ "description" : "Company constitution preset" ,
2901+ "author" : "Test Author" ,
2902+ "repository" : "https://github.com/test/core-company" ,
2903+ "license" : "MIT" ,
2904+ },
2905+ "requires" : {"speckit_version" : ">=0.1.0" },
2906+ "provides" : {
2907+ "templates" : [
2908+ {
2909+ "type" : "template" ,
2910+ "name" : "constitution-template" ,
2911+ "file" : "templates/constitution-template.md" ,
2912+ "description" : "Company constitution" ,
2913+ "replaces" : "constitution-template" ,
2914+ }
2915+ ]
2916+ },
2917+ }
2918+ )
2919+ )
2920+
2921+ PresetManager (project_dir ).install_from_directory (preset_dir , "0.1.5" )
2922+
2923+ assert memory .read_text () == authored
2924+ assert not (memory .parent / ".constitution-template.json" ).exists ()
2925+
28272926 def test_self_test_preserves_authored_constitution_with_placeholder (
28282927 self , project_dir
28292928 ):
@@ -2958,6 +3057,48 @@ def test_constitution_follows_priority_when_winning_preset_removed(
29583057
29593058 assert "preset:self-test" in memory .read_text ()
29603059
3060+ def test_convention_constitution_removal_restores_remaining_layer (
3061+ self , project_dir , temp_dir
3062+ ):
3063+ """Removing a convention layer rematerializes the remaining resolver layer."""
3064+ from specify_cli .commands .init import ensure_constitution_from_template
3065+
3066+ manager = PresetManager (project_dir )
3067+ install_self_test_preset (manager )
3068+ manager .install_from_directory (
3069+ _make_convention_constitution_preset (temp_dir ), "0.1.5" , priority = 1
3070+ )
3071+
3072+ memory = project_dir / ".specify" / "memory" / "constitution.md"
3073+ memory .unlink ()
3074+ ensure_constitution_from_template (project_dir )
3075+ assert memory .read_text () == "# Convention Constitution\n "
3076+
3077+ manager .remove ("convention-constitution" )
3078+
3079+ assert "preset:self-test" in memory .read_text ()
3080+
3081+ def test_convention_constitution_removal_preserves_edited_content (
3082+ self , project_dir , temp_dir
3083+ ):
3084+ """Removing a convention layer does not overwrite edited generated content."""
3085+ from specify_cli .commands .init import ensure_constitution_from_template
3086+
3087+ templates_dir = project_dir / ".specify" / "templates"
3088+ (templates_dir / "constitution-template.md" ).write_text ("# Core Constitution\n " )
3089+ manager = PresetManager (project_dir )
3090+ manager .install_from_directory (
3091+ _make_convention_constitution_preset (temp_dir ), "0.1.5"
3092+ )
3093+ ensure_constitution_from_template (project_dir )
3094+ memory = project_dir / ".specify" / "memory" / "constitution.md"
3095+ edited = memory .read_text () + "\n ## Authored amendment\n "
3096+ memory .write_text (edited )
3097+
3098+ manager .remove ("convention-constitution" )
3099+
3100+ assert memory .read_text () == edited
3101+
29613102 def test_constitution_seed_rejects_symlinked_memory_directory (
29623103 self , project_dir , temp_dir
29633104 ):
0 commit comments