specfuse-loop 0.3.21__tar.gz → 0.3.22__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. {specfuse_loop-0.3.21/specfuse_loop.egg-info → specfuse_loop-0.3.22}/PKG-INFO +1 -1
  2. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/pyproject.toml +1 -1
  3. specfuse_loop-0.3.22/specfuse/loop/data/VERSION +1 -0
  4. specfuse_loop-0.3.22/specfuse/loop/data/rules/planning-discipline.md +119 -0
  5. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/rules/result-contract.md +27 -0
  6. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/rules/verification-discipline.md +6 -0
  7. specfuse_loop-0.3.22/specfuse/loop/data/rules-local/README.md +43 -0
  8. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/templates/GATE.template.md +17 -0
  9. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/templates/PLAN.template.md +26 -0
  10. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/templates/WU.template.md +15 -0
  11. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/loop.py +1 -1
  12. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/scaffold.py +11 -0
  13. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22/specfuse_loop.egg-info}/PKG-INFO +1 -1
  14. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse_loop.egg-info/SOURCES.txt +2 -0
  15. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_init_integration.py +3 -0
  16. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_scaffold_data_in_sync.py +2 -0
  17. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_scaffold_init.py +3 -0
  18. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_scaffold_resources.py +2 -0
  19. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_scaffold_upgrade.py +34 -0
  20. specfuse_loop-0.3.21/specfuse/loop/data/VERSION +0 -1
  21. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/LICENSE +0 -0
  22. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/NOTICE +0 -0
  23. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/README.md +0 -0
  24. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/setup.cfg +0 -0
  25. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/__init__.py +0 -0
  26. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/_filelock.py +0 -0
  27. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/_miniyaml.py +0 -0
  28. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/adopt_feature.py +0 -0
  29. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  30. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  31. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  32. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/docs/getting-started.md +0 -0
  33. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/docs/methodology.md +0 -0
  34. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/docs/skills.md +0 -0
  35. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/gitignore.snippet +0 -0
  36. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/roadmap.template.md +0 -0
  37. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  38. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/rules/never-touch.md +0 -0
  39. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  40. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/schemas/event.schema.json +0 -0
  41. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  42. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  43. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  44. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  45. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/data/verification.yml.example +0 -0
  46. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/gate_eval.py +0 -0
  47. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/gh_backend.py +0 -0
  48. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/gh_features.py +0 -0
  49. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/lint_plan.py +0 -0
  50. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse/loop/validate_event.py +0 -0
  51. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  52. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse_loop.egg-info/entry_points.txt +0 -0
  53. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse_loop.egg-info/requires.txt +0 -0
  54. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/specfuse_loop.egg-info/top_level.txt +0 -0
  55. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_adopt_feature.py +0 -0
  56. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  57. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_attempt_outcome_emission.py +0 -0
  58. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_autoclose_deferral_visibility.py +0 -0
  59. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_autosync.py +0 -0
  60. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_autosync_consent.py +0 -0
  61. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_autosync_firstrun.py +0 -0
  62. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_autosync_plugin.py +0 -0
  63. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_backend.py +0 -0
  64. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_bash_routing.py +0 -0
  65. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  66. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  67. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_bookkeeping_no_verify.py +0 -0
  68. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_claude_resolution.py +0 -0
  69. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_close_wu_self_reference.py +0 -0
  70. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_closing_deliverable_guard.py +0 -0
  71. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_console_encoding.py +0 -0
  72. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_cost_tracking.py +0 -0
  73. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_degraded_oracle.py +0 -0
  74. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_deliverable_presence_gate.py +0 -0
  75. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_doctor.py +0 -0
  76. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_driver_integration.py +0 -0
  77. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_driver_lock.py +0 -0
  78. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_duration_tracking.py +0 -0
  79. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_empty_files_escalation.py +0 -0
  80. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_ensure_feature_branch.py +0 -0
  81. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_ensure_feature_branch_base.py +0 -0
  82. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_events_redaction.py +0 -0
  83. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_extra_gates.py +0 -0
  84. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_filelock_portable.py +0 -0
  85. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_force_full_close.py +0 -0
  86. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_gate_eval.py +0 -0
  87. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_gate_eval_calibration.py +0 -0
  88. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  89. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_gate_eval_terminal_wiring.py +0 -0
  90. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_gh_backend.py +0 -0
  91. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_gh_features.py +0 -0
  92. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_git_env_isolation.py +0 -0
  93. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_hashed_denylist.py +0 -0
  94. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_hashed_denylist_ci.py +0 -0
  95. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_interpreter_normalization.py +0 -0
  96. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_leak_findings_redaction.py +0 -0
  97. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_leak_scan.py +0 -0
  98. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_leak_scan_content.py +0 -0
  99. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_learnings_query.py +0 -0
  100. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  101. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lifecycle_integration.py +0 -0
  102. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lifetime_accounting.py +0 -0
  103. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lint_bare_produces_path.py +0 -0
  104. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lint_close_intermediate.py +0 -0
  105. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lint_close_wu.py +0 -0
  106. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lint_correlation_id.py +0 -0
  107. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  108. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lint_oracle_env.py +0 -0
  109. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lint_plan_errors.py +0 -0
  110. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lint_plan_next_draft.py +0 -0
  111. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lint_plan_relative_import.py +0 -0
  112. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lint_produces_driver_helper.py +0 -0
  113. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lint_sections.py +0 -0
  114. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  115. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_lint_verification_reference.py +0 -0
  116. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_attempt_evidence.py +0 -0
  117. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_auto_archive.py +0 -0
  118. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_caveman_preamble.py +0 -0
  119. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_close_intermediate.py +0 -0
  120. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_defaults_by_type.py +0 -0
  121. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_effort.py +0 -0
  122. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_failure_note_cap.py +0 -0
  123. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_files_changed_guard.py +0 -0
  124. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_gate_budget.py +0 -0
  125. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_model_alias.py +0 -0
  126. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_orchestration.py +0 -0
  127. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_post_pass_invariant.py +0 -0
  128. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_reset_preserving_events.py +0 -0
  129. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_smoke_runner.py +0 -0
  130. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_unsandboxed.py +0 -0
  131. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_loop_zero_token_guard.py +0 -0
  132. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_migrate_legacy.py +0 -0
  133. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_miniyaml_equivalence.py +0 -0
  134. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_miniyaml_negative.py +0 -0
  135. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_planned_cost_lint.py +0 -0
  136. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_prepare_feature.py +0 -0
  137. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_prepare_scaffold_sync.py +0 -0
  138. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_produces_field.py +0 -0
  139. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_produces_in_diff.py +0 -0
  140. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_redaction_windows_home.py +0 -0
  141. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_resolve_base.py +0 -0
  142. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_result_block.py +0 -0
  143. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_roadmap_add_skill.py +0 -0
  144. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_roadmap_archive_skill.py +0 -0
  145. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_roadmap_row_parser.py +0 -0
  146. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_scaffold_doc_hygiene.py +0 -0
  147. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_scaffold_docs.py +0 -0
  148. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_scaffold_manifest.py +0 -0
  149. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_scaffold_seed_sanity.py +0 -0
  150. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_scaffold_wiring.py +0 -0
  151. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_skills_vendored_in_sync.py +0 -0
  152. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_spinning_rearm_gate.py +0 -0
  153. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_squash_commit_hook_crash.py +0 -0
  154. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_template_closing_shapes.py +0 -0
  155. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_terminal_flip_ownership.py +0 -0
  156. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_terminal_flips.py +0 -0
  157. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_timeout_kill_portable.py +0 -0
  158. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_untracked_feature_folder.py +0 -0
  159. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_untracked_file_absorption.py +0 -0
  160. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_upgrade_integration.py +0 -0
  161. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_upgrade_merge_gate.py +0 -0
  162. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_validate_event.py +0 -0
  163. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_verdict_coupling.py +0 -0
  164. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_verify_empty_gate_set.py +0 -0
  165. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_version_consistency.py +0 -0
  166. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_version_skew.py +0 -0
  167. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_windows_gate_exec.py +0 -0
  168. {specfuse_loop-0.3.21 → specfuse_loop-0.3.22}/tests/test_wu_execution_metadata.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.3.21
3
+ Version: 0.3.22
4
4
  Summary: Local-first executor for the Specfuse Plan + Work Unit gate-cycle methodology.
5
5
  Author: Specfuse contributors
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "specfuse-loop"
7
- version = "0.3.21"
7
+ version = "0.3.22"
8
8
  description = "Local-first executor for the Specfuse Plan + Work Unit gate-cycle methodology."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1 @@
1
+ 0.3.22
@@ -0,0 +1,119 @@
1
+ <!--
2
+ Copyright 2026 Specfuse Contributors
3
+ Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ -->
5
+
6
+ # Rule: planning discipline
7
+
8
+ Encodes hard-won discipline from feature retrospectives — the checks that, applied
9
+ at design and arming time, would have prevented the most expensive re-plans. Each
10
+ check below is cheap (a grep, a sentence, a table, one local run) and guards a
11
+ failure mode that cost multiple gates when skipped. Provenance is named per check so
12
+ the reasoning survives the rule (the FEAT-2026-0049 / F1–F4 references resolve in the
13
+ retrospective of the specfuse-generator dogfood feature that motivated these checks).
14
+
15
+ These are **binding at draft, plan-next, and arm time**. A gate armed in violation
16
+ of any check is not ready, regardless of what a WU's frontmatter says.
17
+
18
+ ## 1. Existing-mechanism search — before designing any enforcement or measurement
19
+
20
+ Before an ADR designs a validation rule, a severity level, an enforcement gate, or a
21
+ measurement, it must first establish that the mechanism does not already exist. The
22
+ ADR is **incomplete** without this section.
23
+
24
+ - **Grep the inventory.** Search the directory that holds the mechanism's
25
+ siblings — for a validation rule, the project's rules directory (e.g.
26
+ `grep -rl <concept> <path/to/validation/rules/>`); for an artifact, the
27
+ tool's own inventory command (e.g. `<tool> templates`); for any other
28
+ check, the directory that holds its siblings.
29
+ - **Record the grep command and its verdict** in the ADR — the exact command, and
30
+ either "no existing mechanism, building new" or "found `<X>`, reusing / extending
31
+ it". A verdict of "reusing" that then builds new anyway must say why.
32
+ - **Read the javadoc/description of anything the grep surfaces.** A rule that already
33
+ covers the property may say so in its own words.
34
+
35
+ > **Provenance (FEAT-2026-0049 / F3).** The feature spent three gates building a
36
+ > `WARNING→ERROR` ladder on a new rule while the enforcement it needed already existed
37
+ > in `RelationshipSymmetryValidationRule` — whose javadoc (F-029, 2026-05-29) literally
38
+ > said the debt "surfaces as a Saturday-night surprise when the inference rule changes."
39
+ > A one-line grep of the rules directory at ADR time would have found it. Cost: two
40
+ > gates and a full pivot.
41
+
42
+ ## 2. Escalation-predicate satisfiability — for any severity flip
43
+
44
+ Any plan that raises a check to `ERROR`, flips a `WARNING` to blocking, or asserts a
45
+ "zero issues" close predicate must answer, in writing, one question:
46
+
47
+ > **What does this rule report on a spec/input already in its intended final state?**
48
+
49
+ If the answer is anything other than **zero**, the predicate is **unsatisfiable** —
50
+ the rule fires on correct inputs, so "zero issues" can never be reached and the
51
+ severity flip forces wrong changes. **Stop and redesign** before arming: fix the rule
52
+ so a correct input reports zero, or route enforcement to a different mechanism (see §1).
53
+
54
+ > **Provenance (FEAT-2026-0049 / F3).** ADR D3 ("omission = intent — a non-owning
55
+ > reference is declared by omission") and D6 ("`ERROR` when the rule reports zero
56
+ > issues") contradicted each other inside the same document: post-flip, every correct
57
+ > omission still tripped the rule, so "zero" was unreachable. The contradiction was
58
+ > answerable by reading the ADR against itself. Separately, the *shipped* rule
59
+ > false-positived ~35 of 49 on real specs (#821) — a severity flip forcing wrong
60
+ > changes, exactly what this check prevents.
61
+
62
+ ## 3. Flag-scope table — for any WU introducing or flipping a behavior flag
63
+
64
+ A WU that introduces, gates on, or flips a behavior flag must contain a **flag-scope
65
+ table**: every code path the flag is claimed to affect, marked *gated* / *not gated*,
66
+ with a one-line *why* per row. The arming review checks the feature's headline claim
67
+ ("the flip retires inference", "the flag makes X strict") against the table — a claim
68
+ the table does not support is a scope mismatch, and scope mismatches surface gates
69
+ later as defects, not at arming.
70
+
71
+ | Code path | Gated by flag? | Why |
72
+ |---|---|---|
73
+ | `<path/method>` | yes / no | <one line> |
74
+
75
+ > **Provenance (FEAT-2026-0049 / F1).** The plan enumerated both inference paths
76
+ > (bare-FK and `$ref`) on day one, but no document ever crossed "which paths does the
77
+ > `explicitRelationshipsOnly` flag gate" against "the flip retires inference." The flag
78
+ > was built FK-path-only, deliberately and correctly — but the mismatch with the
79
+ > headline claim surfaced two gates later (F1), and again as a runtime defect (F4).
80
+
81
+ ## 4. Runtime probe before arming a default/severity flip
82
+
83
+ A gate whose WUs flip a **default value** or a **severity** may **not** be armed on
84
+ "mechanical, nothing design-open." Before arming, apply the change locally and run the
85
+ **exact command the WU's tests gate will run** (the full oracle, not a subset), and
86
+ paste the resulting failure list into the gate review. The probe is the arming
87
+ evidence; "nothing design-open" without it is a claim, not a fact.
88
+
89
+ - Run the *full* oracle. A subset hides failures in the paths the change touches that
90
+ the subset does not exercise.
91
+ - The probe's failure list becomes the WU's enumerated test surface — the WU no longer
92
+ discovers its own breakage attempt by attempt.
93
+
94
+ > **Provenance (FEAT-2026-0049 / F4).** Gate 4 was armed on "nothing is design-open"
95
+ > without ever applying the default flip and running the full suite once. The WU then
96
+ > spun three times (~$14) on a real defect that one local run exposed in seconds — and
97
+ > the first two diagnoses were made against a *subset*, missing failures the full suite
98
+ > showed. Contrast: an earlier gate's "measure before drafting" probe (a `WARNING` run
99
+ > over the real specs) worked and correctly shrank the next WU's scope.
100
+
101
+ ## 5. Planning-WU cost floor
102
+
103
+ `plan-next` and `close` / `close-intermediate` WUs are systematically under-estimated.
104
+ When drafting their `planned_cost_usd`, use a **floor of $5.00** — never the $2–3 that
105
+ "it's just bookkeeping" suggests. A gate `cost_budget_usd` set to the sum of $2–3
106
+ planning estimates is a brake that fires by construction on the first real close.
107
+
108
+ > **Provenance (FEAT-2026-0049 + LEARNINGS [FEAT-2026-0044]).** Planning/close WUs ran
109
+ > 2.8–5.2× their $2–3 estimates: `plan-next` at $5.90 and $15.65, `close-intermediate`
110
+ > at $5.67, against $2–3 drafts. LEARNINGS `[FEAT-2026-0044]` had already predicted this
111
+ > systematic under-estimation; the floor makes the prediction binding.
112
+
113
+ ## Why these live together
114
+
115
+ Each is a one-time check that trades a minute at design/arm time against a gate (or
116
+ three) of rework. They share a root: **automation and authors both trust a claim
117
+ without the cheap check that would test it** — a grep not run (§1), a contradiction not
118
+ read (§2), a scope not tabled (§3), a suite not run (§4), an estimate not calibrated
119
+ (§5). State the check, run it, record the verdict. Every plan, every arm.
@@ -107,6 +107,33 @@ blocked_reason: <present only when status is blocked>
107
107
  5. **No secret-looking values in evidence.** The RESULT block is read by the driver
108
108
  and may be archived. See [`security-boundaries.md`](security-boundaries.md).
109
109
 
110
+ ## Closing obligations for implementation WUs (FEAT-2026-0049)
111
+
112
+ Four binding obligations before `status: complete`. Each guards a way a prior WU
113
+ closed on nothing:
114
+
115
+ 1. **Diff against `produces:` first.** Every path in the WU's `produces:` list must
116
+ show a working-tree change, or the RESULT must justify each unchanged path
117
+ explicitly: "no change needed because X; here is the evidence the deliverable
118
+ already holds: `<command + output>`." Silence on an unchanged deliverable is not
119
+ a valid close. (The driver enforces this mechanically since specfuse-loop 0.3.21
120
+ — #198, outcome `produces_not_in_diff`; the obligation binds regardless of
121
+ driver version.)
122
+ 2. **A plan-level contradiction is `blocked`, not `complete`.** If mid-WU you find
123
+ the plan cannot be delivered as written (a decision contradicts another, a
124
+ deliverable cannot exist), emit `status: blocked` with the finding in
125
+ `blocked_reason`. Never write the finding into a gate document and close
126
+ `complete`. `complete` means the deliverables exist; `blocked` means they cannot
127
+ or should not.
128
+ 3. **Every `evidence:` cites an executed command** — its observed exit code/output.
129
+ Reading source, grepping for a string, or citing another WU's RESULT is not
130
+ verification. For a validation-rule or severity claim the evidence must include a
131
+ negative observation (the rule seen rejecting a purpose-built bad input); see
132
+ [`verification-discipline.md`](verification-discipline.md) §3.
133
+ 4. **Analysis without edits is not a silent attempt.** If an attempt produces
134
+ analysis but no file edits, say so explicitly and end `blocked` (or with a
135
+ concrete next-edit plan). Do not exhaust the attempt on prose.
136
+
110
137
  ## Why this matters
111
138
 
112
139
  The driver's trust model is that the RESULT block is an honest claim about what
@@ -39,6 +39,12 @@ Two generic meanings, both apply unless the unit narrows the scope:
39
39
  decide done. Run them yourself first, in declared order, with full output.
40
40
  Generic verifications — "I assume the tests still pass" — are not verifications.
41
41
  The commands are the verification.
42
+ - **Observe behavior, not source.** A claim that code does X is verified by making
43
+ X happen and reading the output — never by reading the source that looks like it
44
+ does X. A rule read as `ERROR` in source may be gated off and never fire; only a
45
+ run proves it. For any rule or severity claim, include a **negative observation**:
46
+ the rule seen rejecting a purpose-built bad input. A rule that fires on nothing
47
+ passes every test.
42
48
 
43
49
  **Verification is the exit oracle.** The agent's own report is advisory; done is
44
50
  decided by the checks passing, not by the agent's belief. If a check fails you are
@@ -0,0 +1,43 @@
1
+ <!--
2
+ Copyright 2026 Specfuse Contributors
3
+ Licensed under the Apache License, Version 2.0. See LICENSE.
4
+ -->
5
+
6
+ # Project-authored rules (`rules-local/`)
7
+
8
+ This directory belongs to **your project**, not to Specfuse. Put binding rules
9
+ here that are specific to this repository — its tools, its paths, its failure
10
+ modes — and that should survive every scaffold upgrade.
11
+
12
+ **The contract:** `specfuse upgrade` never writes, overwrites, or prunes
13
+ anything in `rules-local/`. That is the opposite of the sibling
14
+ `.specfuse/rules/` directory, which is versioned: upgrades overwrite its files
15
+ with the shipped versions and delete files the shipped scaffold no longer
16
+ carries. A project rule placed in `rules/` will be lost on upgrade; the same
17
+ rule placed here is permanent.
18
+
19
+ ## When to write a rule here vs. upstream
20
+
21
+ - **Here:** the rule names your project's directories, commands, tools, or
22
+ conventions ("grep `src/main/java/.../validation/rules/` before designing a
23
+ rule", "the generator jar's `templates` command lists artifacts"). Upstream
24
+ would have to genericize away exactly the specifics that make it useful to
25
+ you.
26
+ - **Upstream (a PR to the scaffold's `rules/`):** the failure mode is
27
+ project-agnostic and other Specfuse projects would hit it too. Keep the
28
+ provenance; genericize the examples.
29
+
30
+ ## Wiring a rule into the loop
31
+
32
+ Rules load via the `@`-references in your `.claude/CLAUDE.md` Specfuse rules
33
+ block. Add one line per rule you author:
34
+
35
+ ```markdown
36
+ @.specfuse/rules-local/<your-rule>.md
37
+ ```
38
+
39
+ Follow the shipped rules' format: one failure mode per rule, the check stated
40
+ imperatively, provenance recorded so the reasoning survives the rule (see
41
+ `.specfuse/rules/planning-discipline.md` for the model).
42
+
43
+ This README is seeded once and never overwritten — edit it freely.
@@ -30,6 +30,23 @@ here for human review-and-arm: read the review artifact, accept or edit the draf
30
30
  next-gate work units, flip the accepted ones to `pending`, set this gate's status to
31
31
  `passed`, and re-run.
32
32
 
33
+ ## Arming discipline (see `.specfuse/rules/planning-discipline.md`)
34
+
35
+ Before flipping this gate's next-gate WUs to `pending`:
36
+
37
+ - **Runtime probe for a default/severity flip (§4).** If any WU in the gate being armed
38
+ flips a **default value** or a **severity**, it may **not** be armed on "mechanical,
39
+ nothing design-open." Apply the change locally, run the **exact command the WU's tests
40
+ gate will run** (the full oracle, not a subset), and paste the failure list into
41
+ `GATE-NN-REVIEW.md`. That list becomes the WU's enumerated test surface. Arming such a
42
+ gate without a probe is what makes an implementation WU spin on a defect one local run
43
+ would have shown.
44
+ - **Flag-scope table (§3).** If a WU introduces or flips a behavior flag, confirm it
45
+ carries a flag-scope table and that the feature's headline claim matches it.
46
+ - **Escalation-predicate satisfiability (§2).** If a WU raises a check to `ERROR` or
47
+ asserts a "zero issues" predicate, confirm PLAN.md answered "what does the rule report
48
+ on a correct input" and that the answer is zero.
49
+
33
50
  ## Reflection notes
34
51
 
35
52
  <Written by the human at review time. What surprised you, what you changed in the
@@ -23,6 +23,32 @@ each WU file owns its own status, and each GATE file owns its gate's status. Det
23
23
  only as far as the next gate; plan-next drafts the gate after that from the
24
24
  retrospective and lessons.
25
25
 
26
+ ## Existing-mechanism search (mandatory — see `.specfuse/rules/planning-discipline.md` §1)
27
+
28
+ Before any ADR or gate designs a validation rule, a severity level, an enforcement
29
+ gate, or a measurement, establish that it does not already exist. **An ADR without
30
+ this section is incomplete.** If this feature designs no such mechanism, write
31
+ exactly `n/a — no enforcement or measurement designed` and stop — do not fabricate
32
+ a grep to fill the section.
33
+
34
+ - **Grep command run:** `<the exact command — e.g. grep -rl <concept>
35
+ src/main/java/.../validation/rules/>`
36
+ - **Verdict:** `<"no existing mechanism, building new" | "found <X>, reusing/extending">`
37
+ - **If reusing:** name `<X>` and quote the line of its javadoc/description that
38
+ confirms it covers the property. If building new despite a hit, say why the hit does
39
+ not suffice.
40
+
41
+ ## Escalation-predicate satisfiability (mandatory for any severity flip — §2)
42
+
43
+ If this feature raises a check to `ERROR`, flips a `WARNING` to blocking, or asserts a
44
+ "zero issues" close predicate, answer here:
45
+
46
+ - **What does the rule report on an input already in its intended final state?**
47
+ `<answer>`
48
+ - If that answer is **not zero**, the predicate is unsatisfiable — the rule fires on
49
+ correct inputs. **Redesign before drafting gates:** fix the rule so a correct input
50
+ reports zero, or route enforcement to the mechanism found in the search above.
51
+
26
52
  ## Task graph
27
53
 
28
54
  ```yaml
@@ -28,6 +28,10 @@ AUTHOR-SET FIELDS — fill or override these at draft/arm time:
28
28
  `xhigh` | `max`. Absent → type-keyed default. `low`/`medium` add a terseness directive.
29
29
  - `planned_cost_usd` — OPTIONAL. Estimated USD at draft time. Compared against actual in close
30
30
  WU's cost analysis. Lint WARN when absent on active/draft WUs (non-blocking).
31
+ **Planning-WU cost floor (`.specfuse/rules/planning-discipline.md` §5):** for `plan-next`,
32
+ `close`, and `close-intermediate` WUs, draft `planned_cost_usd` at a **floor of $5.00** — these
33
+ run 2.8–5.2× the $2–3 that "it's just bookkeeping" suggests. A gate `cost_budget_usd` set to
34
+ the sum of $2–3 planning estimates is a brake that fires by construction on the first real close.
31
35
  - `generated_surfaces` — OPTIONAL. Paths to generated files this unit's acceptance depends on.
32
36
  - `oracle_env` — OPTIONAL. Environment the verifying oracle runs in: `macos_local`,
33
37
  `linux_docker`, `github_actions_ci`, or an operator-named string. Lint WARN when AC mentions
@@ -77,6 +81,17 @@ cheapest hollow-pass guard; see `/authoring-work-units` §12 for the contract an
77
81
  the carve-outs (refactor, migration, pure-data → explicit `Red-test exempt:
78
82
  <reason>` line in the WU body).
79
83
 
84
+ **Flag-scope table** (REQUIRED only when this WU introduces, gates on, or flips a
85
+ behavior flag; omit otherwise — see `.specfuse/rules/planning-discipline.md` §3). Every
86
+ code path the flag is claimed to affect, marked gated / not gated, with a one-line why.
87
+ The arming review checks the feature's headline claim ("the flip retires X") against
88
+ this table; a claim the table does not support is a scope mismatch that surfaces as a
89
+ defect gates later.
90
+
91
+ | Code path | Gated by flag? | Why |
92
+ |---|---|---|
93
+ | `<path/method>` | yes / no | <one line> |
94
+
80
95
  **Do not touch.** Generated directories (`_generated/`, `gen-src/`, or the repo's
81
96
  declared equivalent), files owned by other work units in this gate, secrets,
82
97
  `.git/`. The driver owns all git operations — you edit files only. See
@@ -64,7 +64,7 @@ SPECFUSE_DIR = Path(".specfuse")
64
64
  REPO_ROOT = SPECFUSE_DIR.parent
65
65
  FEATURES_DIR = SPECFUSE_DIR / "features"
66
66
  VERIFICATION_PATH = SPECFUSE_DIR / "verification.yml"
67
- DRIVER_VERSION = "0.3.21"
67
+ DRIVER_VERSION = "0.3.22"
68
68
  # Oldest scaffold layout this driver can drive. init.sh stamps the scaffold's own
69
69
  # version into `.specfuse/VERSION`; check_scaffold_version() fails loud at startup if
70
70
  # the consumer's scaffold is older than this, pointing at `specfuse upgrade`. Bump
@@ -166,6 +166,9 @@ _RULES_BLOCK = (
166
166
  "@.specfuse/rules/never-touch.md\n"
167
167
  "@.specfuse/rules/security-boundaries.md\n"
168
168
  "@.specfuse/rules/verification-discipline.md\n"
169
+ "<!-- Project-authored rules live in .specfuse/rules-local/ (never touched\n"
170
+ " by `specfuse upgrade`). Add one @.specfuse/rules-local/<rule>.md line\n"
171
+ " per rule below. See .specfuse/rules-local/README.md. -->\n"
169
172
  )
170
173
 
171
174
  _RULES_SENTINEL = "@.specfuse/rules/result-contract.md"
@@ -321,6 +324,8 @@ def upgrade_specfuse(
321
324
  Overwrites versioned files (templates/, rules/, verification.yml.example, VERSION),
322
325
  prunes versioned files the seed no longer ships (scoped to templates/ and rules/),
323
326
  seeds missing user-authored files from templates, and refreshes .claude wiring.
327
+ rules-local/ is project-owned: never overlaid, never pruned; only its README
328
+ is seeded when absent.
324
329
 
325
330
  Returns the sorted list of .specfuse/ relpaths written.
326
331
  Raises ScaffoldDowngradeError if the target VERSION is newer than the installed seed.
@@ -372,6 +377,12 @@ def upgrade_specfuse(
372
377
  ("LEARNINGS.md", "LEARNINGS.template.md"),
373
378
  ("roadmap.md", "roadmap.template.md"),
374
379
  ("verification.yml", "verification.yml.example"),
380
+ # rules-local/ is the project-authored rules directory: outside every
381
+ # versioned overlay prefix and prune dir BY CONSTRUCTION, so upgrade
382
+ # never overwrites or deletes its contents. The README is seeded once
383
+ # (create-if-absent, like the other user seeds) and then owned by the
384
+ # project.
385
+ ("rules-local/README.md", "rules-local/README.md"),
375
386
  ]
376
387
  for target_rel, seed_rel in user_seeds:
377
388
  dest = specfuse_dir / target_rel
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.3.21
3
+ Version: 0.3.22
4
4
  Summary: Local-first executor for the Specfuse Plan + Work Unit gate-cycle methodology.
5
5
  Author: Specfuse contributors
6
6
  License: Apache-2.0
@@ -25,9 +25,11 @@ specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-plann
25
25
  specfuse/loop/data/docs/concepts/ralph-lineage.md
26
26
  specfuse/loop/data/rules/correlation-ids.md
27
27
  specfuse/loop/data/rules/never-touch.md
28
+ specfuse/loop/data/rules/planning-discipline.md
28
29
  specfuse/loop/data/rules/result-contract.md
29
30
  specfuse/loop/data/rules/security-boundaries.md
30
31
  specfuse/loop/data/rules/verification-discipline.md
32
+ specfuse/loop/data/rules-local/README.md
31
33
  specfuse/loop/data/schemas/event.schema.json
32
34
  specfuse/loop/data/schemas/events/initiative_created.schema.json
33
35
  specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json
@@ -36,6 +36,8 @@ _EXPECTED_SPECFUSE_TREE = {
36
36
  "templates/WU.template.md",
37
37
  "rules/correlation-ids.md",
38
38
  "rules/never-touch.md",
39
+ "rules/planning-discipline.md",
40
+ "rules-local/README.md",
39
41
  "rules/result-contract.md",
40
42
  "rules/security-boundaries.md",
41
43
  "rules/verification-discipline.md",
@@ -117,6 +119,7 @@ class TestInitFullLayout(unittest.TestCase):
117
119
  for rel in (
118
120
  "rules/correlation-ids.md",
119
121
  "rules/never-touch.md",
122
+ "rules/planning-discipline.md",
120
123
  "rules/result-contract.md",
121
124
  "rules/security-boundaries.md",
122
125
  "rules/verification-discipline.md",
@@ -25,6 +25,8 @@ TRACKED = {
25
25
  "templates/WU.template.md",
26
26
  "rules/correlation-ids.md",
27
27
  "rules/never-touch.md",
28
+ "rules/planning-discipline.md",
29
+ "rules-local/README.md",
28
30
  "rules/result-contract.md",
29
31
  "rules/security-boundaries.md",
30
32
  "rules/verification-discipline.md",
@@ -18,6 +18,8 @@ _EXPECTED_TREE = {
18
18
  "templates/WU.template.md",
19
19
  "rules/correlation-ids.md",
20
20
  "rules/never-touch.md",
21
+ "rules/planning-discipline.md",
22
+ "rules-local/README.md",
21
23
  "rules/result-contract.md",
22
24
  "rules/security-boundaries.md",
23
25
  "rules/verification-discipline.md",
@@ -45,6 +47,7 @@ _VERSIONED_SEED_TO_TARGET = {
45
47
  "templates/WU.template.md": "templates/WU.template.md",
46
48
  "rules/correlation-ids.md": "rules/correlation-ids.md",
47
49
  "rules/never-touch.md": "rules/never-touch.md",
50
+ "rules/planning-discipline.md": "rules/planning-discipline.md",
48
51
  "rules/result-contract.md": "rules/result-contract.md",
49
52
  "rules/security-boundaries.md": "rules/security-boundaries.md",
50
53
  "rules/verification-discipline.md": "rules/verification-discipline.md",
@@ -20,6 +20,8 @@ _EXPECTED_RELPATHS = {
20
20
  "templates/WU.template.md",
21
21
  "rules/correlation-ids.md",
22
22
  "rules/never-touch.md",
23
+ "rules/planning-discipline.md",
24
+ "rules-local/README.md",
23
25
  "rules/result-contract.md",
24
26
  "rules/security-boundaries.md",
25
27
  "rules/verification-discipline.md",
@@ -212,6 +212,40 @@ class TestUpgradeOverlayAndPreserve(unittest.TestCase):
212
212
  upgrade_specfuse(self.target)
213
213
  self.assertTrue(user_file.exists())
214
214
 
215
+ def test_upgrade_preserves_rules_local_files(self):
216
+ """The rules-local/ contract: project-authored rules survive upgrade
217
+ byte-identical — never overlaid, never pruned."""
218
+ sentinel = b"# Project rule\n\nGrep our own rules dir before designing.\n"
219
+ rule = self.specfuse / "rules-local" / "our-project-rule.md"
220
+ rule.parent.mkdir(exist_ok=True)
221
+ rule.write_bytes(sentinel)
222
+
223
+ upgrade_specfuse(self.target)
224
+
225
+ self.assertTrue(rule.exists(),
226
+ "project-authored rules-local file must survive upgrade")
227
+ self.assertEqual(rule.read_bytes(), sentinel,
228
+ "project-authored rules-local file must be untouched")
229
+
230
+ def test_upgrade_seeds_rules_local_readme_when_absent(self):
231
+ readme = self.specfuse / "rules-local" / "README.md"
232
+ if readme.exists():
233
+ readme.unlink()
234
+ readme.parent.rmdir()
235
+ written = upgrade_specfuse(self.target)
236
+ self.assertTrue(readme.exists())
237
+ self.assertIn("rules-local/README.md", written)
238
+
239
+ def test_upgrade_never_overwrites_edited_rules_local_readme(self):
240
+ sentinel = b"project-edited readme"
241
+ readme = self.specfuse / "rules-local" / "README.md"
242
+ readme.parent.mkdir(exist_ok=True)
243
+ readme.write_bytes(sentinel)
244
+ upgrade_specfuse(self.target)
245
+ self.assertEqual(readme.read_bytes(), sentinel,
246
+ "seeded README is project-owned after init — "
247
+ "upgrade must not overwrite it")
248
+
215
249
  def test_upgrade_prune_does_not_touch_scripts_dir(self):
216
250
  # init.sh-legacy scripts/ dir must be left alone (migration prune out of scope)
217
251
  scripts = self.specfuse / "scripts"
@@ -1 +0,0 @@
1
- 0.3.21
File without changes
File without changes
File without changes
File without changes