specfuse-loop 0.7.0__tar.gz → 0.7.1__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 (249) hide show
  1. {specfuse_loop-0.7.0/specfuse_loop.egg-info → specfuse_loop-0.7.1}/PKG-INFO +1 -1
  2. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/pyproject.toml +1 -1
  3. specfuse_loop-0.7.1/specfuse/loop/_wu_sections.py +51 -0
  4. specfuse_loop-0.7.1/specfuse/loop/data/VERSION +1 -0
  5. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/templates/WU.template.md +3 -1
  6. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/lint_plan.py +308 -0
  7. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/loop.py +45 -16
  8. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1/specfuse_loop.egg-info}/PKG-INFO +1 -1
  9. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse_loop.egg-info/SOURCES.txt +4 -0
  10. specfuse_loop-0.7.1/tests/test_lint_boundary_consistency.py +194 -0
  11. specfuse_loop-0.7.1/tests/test_lint_boundary_extraction.py +228 -0
  12. specfuse_loop-0.7.1/tests/test_lint_produces_satisfiability.py +209 -0
  13. specfuse_loop-0.7.1/tests/test_produces_semantics_unified.py +190 -0
  14. specfuse_loop-0.7.0/specfuse/loop/_wu_sections.py +0 -44
  15. specfuse_loop-0.7.0/specfuse/loop/data/VERSION +0 -1
  16. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/LICENSE +0 -0
  17. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/NOTICE +0 -0
  18. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/README.md +0 -0
  19. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/setup.cfg +0 -0
  20. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/__init__.py +0 -0
  21. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/_filelock.py +0 -0
  22. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/_miniyaml.py +0 -0
  23. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/adopt_feature.py +0 -0
  24. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/closing_requirements.py +0 -0
  25. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/LEARNINGS.template.md +0 -0
  26. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
  27. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
  28. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/docs/getting-started.md +0 -0
  29. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/docs/methodology.md +0 -0
  30. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/docs/skills.md +0 -0
  31. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/gitignore.snippet +0 -0
  32. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/monitoring-secrets-checklist.md +0 -0
  33. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/monitoring.overrides.yml.example +0 -0
  34. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/monitoring.yml.example +0 -0
  35. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/roadmap.template.md +0 -0
  36. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/rules/close-discipline.md +0 -0
  37. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/rules/correlation-ids.md +0 -0
  38. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/rules/design-for-diagnosis.md +0 -0
  39. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/rules/never-touch.md +0 -0
  40. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/rules/operator-escalation.md +0 -0
  41. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/rules/planning-discipline.md +0 -0
  42. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/rules/result-contract.md +0 -0
  43. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/rules/security-boundaries.md +0 -0
  44. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/rules/verification-discipline.md +0 -0
  45. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/rules-local/README.md +0 -0
  46. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/schemas/event.schema.json +0 -0
  47. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
  48. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
  49. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
  50. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
  51. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/templates/GATE.template.md +0 -0
  52. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/templates/PLAN.template.md +0 -0
  53. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/verification.yml.example +0 -0
  54. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/data/workflows/specfuse-monitor.yml +0 -0
  55. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/escalation.py +0 -0
  56. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/events_stats.py +0 -0
  57. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/gate_eval.py +0 -0
  58. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/gh_backend.py +0 -0
  59. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/gh_features.py +0 -0
  60. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/labels.py +0 -0
  61. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/lint_closing.py +0 -0
  62. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/lint_monitoring.py +0 -0
  63. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/scaffold.py +0 -0
  64. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/loop/validate_event.py +0 -0
  65. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/monitor/__init__.py +0 -0
  66. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/monitor/adapters.py +0 -0
  67. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/monitor/artifact.py +0 -0
  68. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/monitor/cli.py +0 -0
  69. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/monitor/fingerprint.py +0 -0
  70. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/monitor/issues.py +0 -0
  71. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/monitor/providers/__init__.py +0 -0
  72. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/monitor/providers/_azure_auth.py +0 -0
  73. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/monitor/providers/azure_app_insights.py +0 -0
  74. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/monitor/providers/azure_service_bus.py +0 -0
  75. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/monitor/redaction.py +0 -0
  76. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse/monitor/schedule.py +0 -0
  77. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse_loop.egg-info/dependency_links.txt +0 -0
  78. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse_loop.egg-info/entry_points.txt +0 -0
  79. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse_loop.egg-info/requires.txt +0 -0
  80. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/specfuse_loop.egg-info/top_level.txt +0 -0
  81. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_accept_hedged_close_skill.py +0 -0
  82. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_adopt_feature.py +0 -0
  83. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_app_insights_adapters.py +0 -0
  84. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_arm_gate_edits_uncommitted.py +0 -0
  85. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_artifact_redaction.py +0 -0
  86. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_attempt_outcome_contract.py +0 -0
  87. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_attempt_outcome_emission.py +0 -0
  88. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_attention_nonwriting_guard.py +0 -0
  89. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_attention_skill_structure.py +0 -0
  90. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_autoclose_deferral_visibility.py +0 -0
  91. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_autosync.py +0 -0
  92. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_autosync_consent.py +0 -0
  93. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_autosync_firstrun.py +0 -0
  94. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_autosync_no_cwd_leak.py +0 -0
  95. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_autosync_plugin.py +0 -0
  96. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_azure_credential_resolution.py +0 -0
  97. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_backend.py +0 -0
  98. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_baseline_escalation_message.py +0 -0
  99. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_baseline_persistence.py +0 -0
  100. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_baseline_probe.py +0 -0
  101. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_bash_routing.py +0 -0
  102. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_bats_suites_gated.py +0 -0
  103. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_bookkeeping_commit_crash_run.py +0 -0
  104. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
  105. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_bookkeeping_no_verify.py +0 -0
  106. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_claude_resolution.py +0 -0
  107. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_close_wu_self_reference.py +0 -0
  108. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_closing_deliverable_guard.py +0 -0
  109. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_closing_guard_contracts.py +0 -0
  110. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_closing_guard_prediction.py +0 -0
  111. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_closing_requirements.py +0 -0
  112. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_console_encoding.py +0 -0
  113. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_cost_tracking.py +0 -0
  114. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_degraded_oracle.py +0 -0
  115. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_deliverable_presence_gate.py +0 -0
  116. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_derive_monitoring_discovery.py +0 -0
  117. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_derive_monitoring_skill_registration.py +0 -0
  118. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_design_for_diagnosis_rule.py +0 -0
  119. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_dispatch_skeleton.py +0 -0
  120. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_doctor.py +0 -0
  121. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_done_feature_gates.py +0 -0
  122. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_driver_integration.py +0 -0
  123. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_driver_lock.py +0 -0
  124. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_duration_tracking.py +0 -0
  125. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_empty_files_escalation.py +0 -0
  126. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_ensure_feature_branch.py +0 -0
  127. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_ensure_feature_branch_base.py +0 -0
  128. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_escalation_contract.py +0 -0
  129. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_escalation_emit.py +0 -0
  130. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_events_redaction.py +0 -0
  131. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_events_stats.py +0 -0
  132. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_extra_gates.py +0 -0
  133. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_failure_artifact_model.py +0 -0
  134. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_feature_id_resolution.py +0 -0
  135. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_filelock_portable.py +0 -0
  136. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_fingerprint.py +0 -0
  137. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_force_full_close.py +0 -0
  138. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_gate_eval.py +0 -0
  139. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_gate_eval_calibration.py +0 -0
  140. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_gate_eval_intermediate_wiring.py +0 -0
  141. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_gate_eval_terminal_wiring.py +0 -0
  142. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_gh_backend.py +0 -0
  143. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_gh_features.py +0 -0
  144. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_git_env_isolation.py +0 -0
  145. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_gitleaks_pinning.py +0 -0
  146. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_hashed_denylist.py +0 -0
  147. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_hashed_denylist_ci.py +0 -0
  148. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_heartbeat_adapter.py +0 -0
  149. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_init_integration.py +0 -0
  150. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_interpreter_normalization.py +0 -0
  151. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_label_registry.py +0 -0
  152. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_label_registry_covers_consumers.py +0 -0
  153. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_leak_findings_redaction.py +0 -0
  154. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_leak_scan.py +0 -0
  155. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_leak_scan_content.py +0 -0
  156. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_learnings_query.py +0 -0
  157. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_legacy_4wu_terminal_flips.py +0 -0
  158. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lifecycle_integration.py +0 -0
  159. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lifetime_accounting.py +0 -0
  160. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_bare_produces_path.py +0 -0
  161. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_close_intermediate.py +0 -0
  162. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_close_wu.py +0 -0
  163. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_closing.py +0 -0
  164. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_correlation_id.py +0 -0
  165. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
  166. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_monitoring.py +0 -0
  167. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_oracle_env.py +0 -0
  168. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_plan_errors.py +0 -0
  169. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_plan_next_draft.py +0 -0
  170. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_plan_relative_import.py +0 -0
  171. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_plan_verdict_exempt.py +0 -0
  172. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_planning_sections.py +0 -0
  173. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_produces_driver_helper.py +0 -0
  174. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_sections.py +0 -0
  175. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_task_graph_yaml_selection.py +0 -0
  176. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_lint_verification_reference.py +0 -0
  177. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_attempt_evidence.py +0 -0
  178. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_auto_archive.py +0 -0
  179. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_caveman_preamble.py +0 -0
  180. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_close_intermediate.py +0 -0
  181. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_defaults_by_type.py +0 -0
  182. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_effort.py +0 -0
  183. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_failure_note_cap.py +0 -0
  184. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_files_changed_guard.py +0 -0
  185. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_gate_budget.py +0 -0
  186. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_model_alias.py +0 -0
  187. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_orchestration.py +0 -0
  188. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_post_pass_invariant.py +0 -0
  189. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_reset_preserving_events.py +0 -0
  190. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_smoke_runner.py +0 -0
  191. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_unsandboxed.py +0 -0
  192. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_loop_zero_token_guard.py +0 -0
  193. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_migrate_legacy.py +0 -0
  194. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_miniyaml_equivalence.py +0 -0
  195. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_miniyaml_negative.py +0 -0
  196. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_monitor_cli.py +0 -0
  197. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_monitor_issue_lifecycle.py +0 -0
  198. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_monitor_runner_surfaces.py +0 -0
  199. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_monitoring_bootstrap_artifacts.py +0 -0
  200. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_monitoring_cron_dialect.py +0 -0
  201. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_monitoring_example.py +0 -0
  202. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_monitoring_fenced_blocks.py +0 -0
  203. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_monitoring_seed.py +0 -0
  204. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_operator_escalation_rule.py +0 -0
  205. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_planned_cost_lint.py +0 -0
  206. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_planning_cost_floor.py +0 -0
  207. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_prepare_feature.py +0 -0
  208. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_prepare_scaffold_sync.py +0 -0
  209. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_produces_field.py +0 -0
  210. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_produces_in_diff.py +0 -0
  211. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_provision_labels.py +0 -0
  212. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_queue_stalled_adapter.py +0 -0
  213. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_redaction_windows_home.py +0 -0
  214. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_resolve_base.py +0 -0
  215. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_result_block.py +0 -0
  216. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_roadmap_add_skill.py +0 -0
  217. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_roadmap_archive_skill.py +0 -0
  218. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_roadmap_row_parser.py +0 -0
  219. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_scaffold_data_in_sync.py +0 -0
  220. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_scaffold_doc_hygiene.py +0 -0
  221. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_scaffold_docs.py +0 -0
  222. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_scaffold_init.py +0 -0
  223. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_scaffold_label_provisioning.py +0 -0
  224. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_scaffold_manifest.py +0 -0
  225. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_scaffold_resources.py +0 -0
  226. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_scaffold_seed_sanity.py +0 -0
  227. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_scaffold_upgrade.py +0 -0
  228. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_scaffold_wiring.py +0 -0
  229. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_schedule_dialect.py +0 -0
  230. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_service_bus_dlq_adapter.py +0 -0
  231. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_skill_discovery_links.py +0 -0
  232. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_skills_vendored_in_sync.py +0 -0
  233. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_spinning_rearm_gate.py +0 -0
  234. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_squash_commit_hook_crash.py +0 -0
  235. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_template_closing_shapes.py +0 -0
  236. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_terminal_flip_ownership.py +0 -0
  237. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_terminal_flips.py +0 -0
  238. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_timeout_kill_portable.py +0 -0
  239. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_untracked_feature_folder.py +0 -0
  240. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_untracked_file_absorption.py +0 -0
  241. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_upgrade_integration.py +0 -0
  242. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_upgrade_merge_gate.py +0 -0
  243. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_validate_event.py +0 -0
  244. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_verdict_coupling.py +0 -0
  245. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_verify_empty_gate_set.py +0 -0
  246. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_version_consistency.py +0 -0
  247. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_version_skew.py +0 -0
  248. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/tests/test_windows_gate_exec.py +0 -0
  249. {specfuse_loop-0.7.0 → specfuse_loop-0.7.1}/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.7.0
3
+ Version: 0.7.1
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.7.0"
7
+ version = "0.7.1"
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,51 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ # Copyright 2026 Specfuse contributors
4
+ # Licensed under the Apache License, Version 2.0. See LICENSE.
5
+ #
6
+ """
7
+ Dependency-free slicers for work-unit body sections.
8
+
9
+ Shared by `loop.py` and `lint_plan.py`, which cannot import each other
10
+ (`lint_plan.py` imports `VERDICT_VALUES` from `loop.py`, so the reverse
11
+ import would be a cycle). This module imports nothing from either.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import re
17
+
18
+ _AC_END_RE = re.compile(r"(?m)^(?:\*\*|#{1,6}\s)")
19
+
20
+
21
+ def slice_wu_section(body: str, section_name: str) -> str:
22
+ """Return content between a named section heading and the next heading.
23
+
24
+ Handles both section-heading shapes a WU body uses: the canonical
25
+ bold-preamble form (`**Section name.** content starting on the same
26
+ line`, what `.specfuse/templates/WU.template.md` prescribes and what
27
+ 327/327 real WU bodies in this repo use) and the ATX form (`## Section
28
+ name`, content starting on the next line). For the bold form, content
29
+ immediately following the closing `**` — on the label line itself — is
30
+ part of the section; discarding it (as a next-line-only slice would)
31
+ silently drops whatever a WU author wrote inline after the label.
32
+ """
33
+ escaped = re.escape(section_name)
34
+ heading_re = re.compile(
35
+ rf"(?mi)^#{{1,6}}\s*{escaped}\b|^\*\*{escaped}[^\n*]*\*\*\.?"
36
+ )
37
+ m = heading_re.search(body)
38
+ if not m:
39
+ return ""
40
+ if m.group(0).startswith("#"):
41
+ nl = body.find("\n", m.end())
42
+ after = body[nl + 1:] if nl != -1 else ""
43
+ else:
44
+ after = body[m.end():]
45
+ em = _AC_END_RE.search(after)
46
+ return after[:em.start()] if em else after
47
+
48
+
49
+ def slice_acceptance_criteria(body: str) -> str:
50
+ """Return the text of the Acceptance criteria section only (bold-preamble or ATX)."""
51
+ return slice_wu_section(body, "Acceptance criteria")
@@ -0,0 +1 @@
1
+ 0.7.1
@@ -45,7 +45,9 @@ AUTHOR-SET FIELDS — fill or override these at draft/arm time:
45
45
  (`loop.py`, `lint_plan.py`, adjacent scripts). Lint WARN when body mentions driver-wiring
46
46
  keywords but field is absent. See FEAT-2026-0017.
47
47
  - `produces` — OPTIONAL. File path(s) this WU must produce; machine-enforced by the driver's
48
- presence gate (FEAT-2026-0022). Lint WARN when absent on `implementation` WUs.
48
+ presence gate (FEAT-2026-0022). Unified contract: a literal path must exist non-empty; a glob
49
+ needs ≥1 existing non-empty match — either form satisfies both the presence gate and the
50
+ in-diff gate. Lint WARN when absent on `implementation` WUs.
49
51
 
50
52
  DRIVER-OWNED FIELDS - the driver writes these at outcome time; authors leave them absent:
51
53
  <!-- driver-owned: attempts, cost_usd, input_tokens, output_tokens, duration_seconds,
@@ -28,6 +28,7 @@ Usage: specfuse-lint .specfuse/features/FEAT-XXXX-slug
28
28
 
29
29
  from __future__ import annotations
30
30
 
31
+ import fnmatch
31
32
  import re
32
33
  from pathlib import Path
33
34
 
@@ -441,6 +442,311 @@ def check_closing_guard_literals(feature_dir: Path, gates: list) -> None:
441
442
 
442
443
  _AUTOCLOSE_DEBT_MARKER_RE = re.compile(r"<!--\s*specfuse:autoclose-debt\s+gate=(\d+)")
443
444
 
445
+ _PRODUCES_DISPATCHABLE_STATUSES = {"draft", "pending", "ready"}
446
+ _PRODUCES_EXEMPT_PATHS = {"events.jsonl"}
447
+
448
+
449
+ def check_produces_satisfiability(feature_dir: Path, gates: list) -> None:
450
+ """WARN when a dispatchable WU's `produces:` path was already delivered.
451
+
452
+ Exact string match only against a `done` WU's `produces:` entries in the
453
+ same feature — a glob is compared literally, not expanded (expansion
454
+ semantics belong to the presence gate, not this lint). Parallel drafts
455
+ sharing a surface are fine (the earlier WU must be `done`), and a WU's own
456
+ file / `events.jsonl` never count as a clash. WARN-only; never appends to
457
+ the errors list. See FEAT-2026-0055/T01, FEAT-2026-0066/T04.
458
+ """
459
+ records = [] # (wid, wfile, status, produces_entries)
460
+ for gate in gates:
461
+ for entry in gate.get("work_units") or []:
462
+ wid, wfile = entry.get("id"), entry.get("file")
463
+ if not wid or not wfile:
464
+ continue
465
+ wpath = feature_dir / wfile
466
+ if not wpath.exists():
467
+ continue
468
+ wfm, _ = read_frontmatter(wpath)
469
+ produces_raw = wfm.get("produces")
470
+ if not produces_raw:
471
+ continue
472
+ raw_entries = produces_raw if isinstance(produces_raw, list) else [produces_raw]
473
+ cleaned = set()
474
+ for p in raw_entries:
475
+ p_s = str(p).strip()
476
+ if not p_s or p_s in _PRODUCES_EXEMPT_PATHS or p_s == wfile:
477
+ continue
478
+ cleaned.add(p_s)
479
+ if cleaned:
480
+ records.append((wid, wfile, wfm.get("status", ""), cleaned))
481
+
482
+ done_paths: dict = {} # path -> (wid, wfile), first done WU declaring it
483
+ for wid, wfile, status, entries in records:
484
+ if status != "done":
485
+ continue
486
+ for p in entries:
487
+ done_paths.setdefault(p, (wid, wfile))
488
+
489
+ for wid, wfile, status, entries in records:
490
+ if status not in _PRODUCES_DISPATCHABLE_STATUSES:
491
+ continue
492
+ for p in entries:
493
+ match = done_paths.get(p)
494
+ if match is None or match[0] == wid:
495
+ continue
496
+ done_wid, done_wfile = match
497
+ print(
498
+ f"WARN: {wfile}: {wid} declares produces path {p!r}, but "
499
+ f"done WU {done_wid} ({done_wfile}) already delivered it. "
500
+ f"Drop the path, or state the incremental edit this WU makes "
501
+ f"to it in the body."
502
+ )
503
+
504
+
505
+ _DNT_CARVEOUT_RE = re.compile(r"(?i)\bexcept\b")
506
+ _DNT_ALLOW_ENUM_RE = re.compile(
507
+ r"(?i)\b(?:files?|paths?)(?:\s+that)?\s+change\b|\badds?\b|\bnew\b"
508
+ )
509
+ _DNT_AMBIGUOUS_PHRASE_RE = re.compile(
510
+ r"(?i)\bexisting\b|\bgenerated (?:by|from)\b|\boutput of\b"
511
+ r"|\bhand-edit\b|\bdirect edit\b"
512
+ )
513
+ _DNT_CONTAINER_TOKEN_RE = re.compile(r"(?i)\bin\s*`([^`]+)`")
514
+ _BACKTICK_TOKEN_RE = re.compile(r"`([^`]+)`")
515
+
516
+
517
+ def _split_dnt_clauses(dnt_text: str) -> list[str]:
518
+ """Split Do-not-touch prose into clauses on ';' and sentence-ending '.'.
519
+
520
+ Physical lines are joined first (a wrapped sentence spans them; the
521
+ bold-preamble form's canonical body is prose, not one clause per line),
522
+ and a delimiter inside backticks never splits a clause — a path like
523
+ `SKILL.md` must not fracture on its own period.
524
+ """
525
+ joined = " ".join(
526
+ line.strip().lstrip("-*").strip()
527
+ for line in dnt_text.splitlines()
528
+ if line.strip()
529
+ )
530
+ clauses: list[str] = []
531
+ buf: list[str] = []
532
+ in_backtick = False
533
+ for ch in joined:
534
+ if ch == "`":
535
+ in_backtick = not in_backtick
536
+ buf.append(ch)
537
+ continue
538
+ if not in_backtick and ch in ";.":
539
+ clause = "".join(buf).strip()
540
+ if clause:
541
+ clauses.append(clause)
542
+ buf = []
543
+ continue
544
+ buf.append(ch)
545
+ tail = "".join(buf).strip()
546
+ if tail:
547
+ clauses.append(tail)
548
+ return clauses
549
+
550
+
551
+ def _extract_do_not_touch_patterns(dnt_text: str) -> tuple[list[str], list[str], set[str]]:
552
+ """Return (prohibit_patterns, ambiguous_patterns, allowed_literals) from a
553
+ Do-not-touch body.
554
+
555
+ Only backtick-quoted tokens containing '/', '*', or a file extension count
556
+ as binding path patterns — a plain-prose mention of a path never fires the
557
+ boundary check (no semantic judgment, no false ERROR on illustrative
558
+ prose). Extraction is prohibition-scoped, per clause (split on ';' and
559
+ sentence-ending '.'):
560
+
561
+ - A clause carrying an "except" carve-out (the shape FEAT-2026-0066/T04's
562
+ re-armed body used) contributes no patterns at all.
563
+ - A clause carrying an allow-enumeration signal ("These/Paths that
564
+ change:" / "adds" / "new" — the FEAT-2026-0023/T01 shape, where the
565
+ WU's own deliverables are listed, not forbidden) contributes no
566
+ patterns to either bucket. Its backtick tokens are instead recorded as
567
+ `allowed_literals`: exact paths the WU has explicitly declared it is
568
+ creating or changing. A later clause's *pattern* may still coincide
569
+ with one of these paths (e.g. "any other `.github/workflows/*` file"
570
+ after enumerating the one workflow file this WU adds) — the caller
571
+ treats an exact `allowed_literals` hit as never a match, regardless of
572
+ which clause the pattern came from, since the enumeration is a
573
+ definite allow-list for that literal path.
574
+ - A clause carrying a semantic qualifier cannot be resolved without
575
+ knowing something about the WU's own edit that the lint has no way to
576
+ check. Its path-like tokens go in `ambiguous_patterns` — the caller
577
+ downgrades a match to WARN, never ERROR, per this WU's design rule:
578
+ ERROR only on certainty. Two qualifier shapes, both drawn from the
579
+ retrospective's real fixtures:
580
+ - "existing" (FEAT-2026-0070/T08: "every existing `check_*`
581
+ function") — is a given produces path pre-existing or newly
582
+ created by this WU?
583
+ - "generated by/output of/hand-edit/direct edit" (FEAT-2026-0069/T08,
584
+ FEAT-2026-0070/T03: "`.../SKILL.md` as a direct edit — it is an
585
+ output of `scripts/sync-scaffold.sh`") — the prohibition is on
586
+ *manual* editing; a produces declaration doesn't say whether the
587
+ WU reaches the path by hand or by running the generator.
588
+ - A token immediately preceded by "in" (e.g. "`discover_components()`
589
+ and `_STACK_A_PATTERNS` in `tests/test_x.py`" — FEAT-2026-0069/T03)
590
+ names a container, not the prohibition itself: the forbidden things
591
+ are the symbols named before "in", and the file may otherwise be
592
+ touched freely. Also goes to `ambiguous_patterns` — the lint can spot
593
+ the grammatical shape but not verify the symbols went untouched.
594
+ - Everything else is an unqualified prohibition and goes in
595
+ `prohibit_patterns`.
596
+ """
597
+ prohibit: list[str] = []
598
+ ambiguous: list[str] = []
599
+ allowed_literals: set[str] = set()
600
+ for clause in _split_dnt_clauses(dnt_text):
601
+ if _DNT_CARVEOUT_RE.search(clause):
602
+ continue
603
+ if _DNT_ALLOW_ENUM_RE.search(clause):
604
+ for tok in _BACKTICK_TOKEN_RE.findall(clause):
605
+ tok = tok.strip()
606
+ if tok:
607
+ allowed_literals.add(tok)
608
+ continue
609
+ clause_ambiguous = bool(_DNT_AMBIGUOUS_PHRASE_RE.search(clause))
610
+ container_tokens = {
611
+ m.group(1).strip() for m in _DNT_CONTAINER_TOKEN_RE.finditer(clause)
612
+ }
613
+ for tok in _BACKTICK_TOKEN_RE.findall(clause):
614
+ tok = tok.strip()
615
+ if not tok:
616
+ continue
617
+ if "/" in tok or "*" in tok or re.search(r"\.[A-Za-z0-9]+$", tok.rstrip("/")):
618
+ bucket = (
619
+ ambiguous if (clause_ambiguous or tok in container_tokens)
620
+ else prohibit
621
+ )
622
+ bucket.append(tok)
623
+ return prohibit, ambiguous, allowed_literals
624
+
625
+
626
+ def _match_dnt_boundary(
627
+ surface: str,
628
+ prohibit_patterns: list[str],
629
+ ambiguous_patterns: list[str],
630
+ allowed_literals: set[str],
631
+ ) -> tuple[str, str] | None:
632
+ """Return (severity, pattern) for the first pattern `surface` matches —
633
+ ERROR if prohibited, WARN if only ambiguous. A surface the WU has
634
+ explicitly enumerated as its own new/changed deliverable never matches,
635
+ even against a pattern found in an unrelated clause."""
636
+ if surface in allowed_literals:
637
+ return None
638
+ for pat in prohibit_patterns:
639
+ if fnmatch.fnmatch(surface, pat):
640
+ return ("ERROR", pat)
641
+ for pat in ambiguous_patterns:
642
+ if fnmatch.fnmatch(surface, pat):
643
+ return ("WARN", pat)
644
+ return None
645
+
646
+
647
+ def check_produces_boundary(feature_dir: Path, gates: list) -> list[str]:
648
+ """ERROR when a WU's own `produces:` path (or `produces_driver_helper`
649
+ surface) falls inside its own Do-not-touch section's paths.
650
+
651
+ A structural deadlock, not a review nit: the WU cannot both deliver the
652
+ path and honor its own boundary, and `assert_produces_in_diff` only
653
+ catches this AFTER a full dispatch attempt — the cost FEAT-2026-0066/T04
654
+ paid (3 attempts + an operator re-arm) for a conjunction this lint makes
655
+ un-armable up front. See FEAT-2026-0070 (earlier-enforcer-names-the-
656
+ later-one): the ERROR names the post-attempt guard it preempts so the
657
+ author isn't left to rediscover the connection.
658
+ """
659
+ found: list[str] = []
660
+ for gate in gates:
661
+ for entry in gate.get("work_units") or []:
662
+ wid, wfile = entry.get("id"), entry.get("file")
663
+ if not wid or not wfile:
664
+ continue
665
+ wpath = feature_dir / wfile
666
+ if not wpath.exists():
667
+ continue
668
+ wfm, wbody = read_frontmatter(wpath)
669
+ dnt_text = _slice_section(wbody, "Do not touch")
670
+ if not dnt_text.strip():
671
+ continue
672
+ prohibit_patterns, ambiguous_patterns, allowed_literals = (
673
+ _extract_do_not_touch_patterns(dnt_text)
674
+ )
675
+ if not prohibit_patterns and not ambiguous_patterns:
676
+ continue
677
+
678
+ produces_raw = wfm.get("produces")
679
+ produces_entries = (
680
+ produces_raw if isinstance(produces_raw, list)
681
+ else [produces_raw] if produces_raw else []
682
+ )
683
+ for p in produces_entries:
684
+ p_s = str(p).strip()
685
+ if not p_s:
686
+ continue
687
+ hit = _match_dnt_boundary(
688
+ p_s, prohibit_patterns, ambiguous_patterns, allowed_literals
689
+ )
690
+ if hit is None:
691
+ continue
692
+ severity, pat = hit
693
+ if severity == "ERROR":
694
+ found.append(
695
+ f"ERROR: {wfile}: {wid} declares produces path "
696
+ f"{p_s!r}, which its own Do-not-touch section "
697
+ f"forbids via {pat!r}. This is a structural "
698
+ f"deadlock — assert_produces_in_diff would refuse "
699
+ f"it after a full dispatch attempt. Drop the path "
700
+ f"from produces, narrow the Do-not-touch pattern, "
701
+ f"or add an explicit 'except' carve-out. See "
702
+ f"FEAT-2026-0066/T04, FEAT-2026-0070."
703
+ )
704
+ else:
705
+ print(
706
+ f"WARN: {wfile}: {wid} declares produces path "
707
+ f"{p_s!r}, which matches Do-not-touch pattern "
708
+ f"{pat!r} — the boundary's wording ('existing', "
709
+ f"'generated by', 'in <file>', or similar) makes "
710
+ f"this pattern's scope ambiguous without semantic "
711
+ f"judgment this lint does not have. Confirm by hand "
712
+ f"before arming, or reword the boundary to name the "
713
+ f"protected surface unambiguously."
714
+ )
715
+
716
+ pdh_raw = wfm.get("produces_driver_helper")
717
+ if pdh_raw:
718
+ pdh_s = str(pdh_raw).strip()
719
+ surface = pdh_s.split("—")[0].split("--")[0].strip().strip("`\"'")
720
+ if surface:
721
+ hit = _match_dnt_boundary(
722
+ surface, prohibit_patterns, ambiguous_patterns, allowed_literals
723
+ )
724
+ if hit is not None:
725
+ severity, pat = hit
726
+ if severity == "ERROR":
727
+ found.append(
728
+ f"ERROR: {wfile}: {wid} declares "
729
+ f"produces_driver_helper surface {surface!r}, "
730
+ f"which its own Do-not-touch section forbids "
731
+ f"via {pat!r}. This is a structural deadlock — "
732
+ f"assert_produces_in_diff would refuse it "
733
+ f"after a full dispatch attempt. Drop/narrow "
734
+ f"the boundary, or add an explicit 'except' "
735
+ f"carve-out. See FEAT-2026-0066/T04, "
736
+ f"FEAT-2026-0070."
737
+ )
738
+ else:
739
+ print(
740
+ f"WARN: {wfile}: {wid} declares "
741
+ f"produces_driver_helper surface {surface!r}, "
742
+ f"which matches Do-not-touch pattern {pat!r} "
743
+ f"— the boundary's wording makes this "
744
+ f"pattern's scope ambiguous without semantic "
745
+ f"judgment this lint does not have. Confirm "
746
+ f"by hand before arming."
747
+ )
748
+ return found
749
+
444
750
 
445
751
  def check_autoclose_debt_prediction(feature_dir: Path, gates: list) -> None:
446
752
  """WARN when a terminal close WU's body never instructs reconciling a
@@ -890,6 +1196,8 @@ def lint(feature_dir: Path) -> list[str]:
890
1196
  check_planning_sections(feature_dir, fm, body, gates)
891
1197
  check_closing_guard_literals(feature_dir, gates)
892
1198
  check_autoclose_debt_prediction(feature_dir, gates)
1199
+ check_produces_satisfiability(feature_dir, gates)
1200
+ errs.extend(check_produces_boundary(feature_dir, gates))
893
1201
  errs.extend(check_done_feature_gates(feature_dir, fm))
894
1202
 
895
1203
  # Cross-gate mixed-shape check. Two directions of mix:
@@ -42,6 +42,7 @@ from __future__ import annotations
42
42
  import argparse
43
43
  import datetime as dt
44
44
  import fnmatch
45
+ import glob
45
46
  import hashlib
46
47
  import json
47
48
  import logging
@@ -88,7 +89,7 @@ SPECFUSE_DIR = Path(".specfuse")
88
89
  REPO_ROOT = SPECFUSE_DIR.parent
89
90
  FEATURES_DIR = SPECFUSE_DIR / "features"
90
91
  VERIFICATION_PATH = SPECFUSE_DIR / "verification.yml"
91
- DRIVER_VERSION = "0.7.0"
92
+ DRIVER_VERSION = "0.7.1"
92
93
  # Oldest scaffold layout this driver can drive. init.sh stamps the scaffold's own
93
94
  # version into `.specfuse/VERSION`; check_scaffold_version() fails loud at startup if
94
95
  # the consumer's scaffold is older than this, pointing at `specfuse upgrade`. Bump
@@ -4517,27 +4518,55 @@ def assert_implementation_touched_files(
4517
4518
 
4518
4519
 
4519
4520
  def assert_declared_deliverables(wu: WorkUnit) -> tuple[bool, str]:
4520
- """Deliverable-presence gate (FEAT-2026-0022/T02).
4521
-
4522
- Verify every path the WU declared in ``produces:`` exists on disk and is
4523
- non-empty (``test -s`` semantics: ``Path(p).exists()`` and
4524
- ``Path(p).stat().st_size > 0``). Returns ``(True, "")`` when ``wu.produces``
4525
- is empty — the opt-out: an undeclared ``produces:`` means no gate, exactly
4526
- as ``verify_files_changed``'s absence opt-out (loop.py:994) — or when every
4527
- declared path exists and is non-empty. On the first offending path returns
4528
- ``(False, summary)`` naming that path and whether it was absent or empty.
4529
-
4530
- A path that exists but is zero-length is treated as missing: an empty
4531
- deliverable is a hollow deliverable. This catches the partial-bundle hollow
4532
- pass (FEAT-2026-0020/T12: SECURITY.md present, bundled CODE_OF_CONDUCT.md
4533
- absent). The check is file-level only; symbol-level checks are out of scope
4534
- (PLAN Scope OUT).
4521
+ """Deliverable-presence gate (FEAT-2026-0022/T02), unified with
4522
+ ``assert_produces_in_diff``'s literal/glob contract (FEAT-2026-0055/T03).
4523
+
4524
+ Returns ``(True, "")`` when ``wu.produces`` is empty — the opt-out: an
4525
+ undeclared ``produces:`` means no gate, exactly as ``verify_files_changed``'s
4526
+ absence opt-out (loop.py:994). Otherwise every declared entry must satisfy
4527
+ one of two forms:
4528
+
4529
+ - **Literal path** (no ``fnmatch`` metacharacter — none of ``* ? [``):
4530
+ must exist and be non-empty (``test -s`` semantics). Unchanged from the
4531
+ original presence gate.
4532
+ - **Glob** (contains an ``fnmatch`` metacharacter): at least one existing,
4533
+ non-empty file must match, via ``glob.glob`` (same pattern syntax
4534
+ ``assert_produces_in_diff`` matches against the squash diff with
4535
+ ``fnmatch.fnmatch``, so a pattern that satisfies one satisfies the other).
4536
+
4537
+ A path that is, or resolves to, a directory is refused outright with an
4538
+ ``ERROR``-worthy message naming the unified contract: directories were
4539
+ never valid produces: entries (a directory always passed this presence
4540
+ gate while failing ``assert_produces_in_diff``'s diff match — the
4541
+ literal-vs-glob split this WU exists to close); the refusal now says why
4542
+ instead of silently passing one gate and failing the other.
4543
+
4544
+ On the first offending entry returns ``(False, summary)`` naming it.
4535
4545
  """
4536
4546
  if not wu.produces:
4537
4547
  return True, ""
4538
4548
  for raw in wu.produces:
4539
4549
  path = str(raw)
4540
4550
  p = Path(path)
4551
+ if path.endswith("/") or p.is_dir():
4552
+ return False, (
4553
+ f"declared deliverable is a directory: {path} — directories "
4554
+ "are not valid produces: entries under the unified "
4555
+ "literal/glob contract (a directory always passed presence "
4556
+ "while failing assert_produces_in_diff's diff match); declare "
4557
+ "the specific file(s) or a glob instead"
4558
+ )
4559
+ if any(ch in path for ch in "*?["):
4560
+ matches = [
4561
+ m for m in glob.glob(path)
4562
+ if Path(m).is_file() and Path(m).stat().st_size > 0
4563
+ ]
4564
+ if not matches:
4565
+ return False, (
4566
+ f"declared deliverable glob matched no existing "
4567
+ f"non-empty file: {path}"
4568
+ )
4569
+ continue
4541
4570
  if not p.exists():
4542
4571
  return False, f"declared deliverable absent: {path}"
4543
4572
  if p.stat().st_size == 0:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfuse-loop
3
- Version: 0.7.0
3
+ Version: 0.7.1
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
@@ -150,6 +150,8 @@ tests/test_legacy_4wu_terminal_flips.py
150
150
  tests/test_lifecycle_integration.py
151
151
  tests/test_lifetime_accounting.py
152
152
  tests/test_lint_bare_produces_path.py
153
+ tests/test_lint_boundary_consistency.py
154
+ tests/test_lint_boundary_extraction.py
153
155
  tests/test_lint_close_intermediate.py
154
156
  tests/test_lint_close_wu.py
155
157
  tests/test_lint_closing.py
@@ -163,6 +165,7 @@ tests/test_lint_plan_relative_import.py
163
165
  tests/test_lint_plan_verdict_exempt.py
164
166
  tests/test_lint_planning_sections.py
165
167
  tests/test_lint_produces_driver_helper.py
168
+ tests/test_lint_produces_satisfiability.py
166
169
  tests/test_lint_sections.py
167
170
  tests/test_lint_task_graph_yaml_selection.py
168
171
  tests/test_lint_verification_reference.py
@@ -200,6 +203,7 @@ tests/test_prepare_feature.py
200
203
  tests/test_prepare_scaffold_sync.py
201
204
  tests/test_produces_field.py
202
205
  tests/test_produces_in_diff.py
206
+ tests/test_produces_semantics_unified.py
203
207
  tests/test_provision_labels.py
204
208
  tests/test_queue_stalled_adapter.py
205
209
  tests/test_redaction_windows_home.py