specfuse-loop 0.9.0__tar.gz → 0.9.2__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.
- {specfuse_loop-0.9.0/specfuse_loop.egg-info → specfuse_loop-0.9.2}/PKG-INFO +1 -1
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/pyproject.toml +1 -1
- specfuse_loop-0.9.2/specfuse/loop/data/CHANGELOG.seed.md +27 -0
- specfuse_loop-0.9.2/specfuse/loop/data/VERSION +1 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/correlation-ids.md +0 -11
- specfuse_loop-0.9.2/specfuse/loop/gate_commands.py +94 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/lint_plan.py +93 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/loop.py +98 -11
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/scaffold.py +27 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/validate_event.py +5 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2/specfuse_loop.egg-info}/PKG-INFO +1 -1
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/SOURCES.txt +6 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_changelog_collection.py +40 -0
- specfuse_loop-0.9.2/tests/test_ci_runs_every_gate.py +74 -0
- specfuse_loop-0.9.2/tests/test_correlation_id_two_surfaces.py +79 -0
- specfuse_loop-0.9.2/tests/test_produces_shape.py +160 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_data_in_sync.py +1 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_resources.py +1 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_wiring.py +81 -0
- specfuse_loop-0.9.2/tests/test_unterminated_frontmatter.py +139 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_upgrade_merge_gate.py +103 -0
- specfuse_loop-0.9.0/specfuse/loop/data/VERSION +0 -1
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/LICENSE +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/NOTICE +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/README.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/setup.cfg +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/__init__.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/_filelock.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/_miniyaml.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/_wu_sections.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/adopt_feature.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/arm_eval.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/arm_sweep.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/arm_txn.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/changelog.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/closing_requirements.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/cost.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/LEARNINGS.template.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/concepts/adopting-auto-mode.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/concepts/autonomy-stop-classes.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/getting-started.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/methodology.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/docs/skills.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/gitignore.snippet +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/monitoring-secrets-checklist.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/monitoring.overrides.yml.example +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/monitoring.yml.example +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/roadmap.template.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/close-discipline.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/design-for-diagnosis.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/never-touch.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/operator-escalation.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/planning-discipline.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/result-contract.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/security-boundaries.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules/verification-discipline.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/rules-local/README.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/driver-event.schema.json +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/event.schema.json +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/templates/GATE.template.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/templates/LEARNINGS-pending.template.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/templates/PLAN.template.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/templates/WU.template.md +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/verification.yml.example +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/data/workflows/specfuse-monitor.yml +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/escalation.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/events_stats.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/gate_eval.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/gh_backend.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/gh_features.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/labels.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/lint_closing.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/lint_monitoring.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/lint_roadmap.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/loop/plan_baseline.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/__init__.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/adapters.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/artifact.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/autofix.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/autofix_invoke.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/autofix_run.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/autofix_state.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/cli.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/diagnose_cli.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/diagnosis.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/fingerprint.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/issues.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/providers/__init__.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/providers/_azure_auth.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/providers/azure_app_insights.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/providers/azure_service_bus.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/redaction.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse/monitor/schedule.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/dependency_links.txt +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/entry_points.txt +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/requires.txt +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/specfuse_loop.egg-info/top_level.txt +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_accept_hedged_close_headline.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_accept_hedged_close_skill.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_adopt_feature.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_app_insights_adapters.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_eval.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_eval_lint_class.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_eval_wiring.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_gate_edits_uncommitted.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_sweep.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_sweep_gate.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_txn.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_arm_wiring.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_artifact_redaction.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_attempt_outcome_contract.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_attempt_outcome_emission.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_attention_nonwriting_guard.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_attention_skill_structure.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autoclose_deferral_visibility.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autofix_invoke.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autofix_live.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autofix_predicate.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autofix_run.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autofix_state.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autosync.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autosync_consent.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autosync_firstrun.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autosync_no_cwd_leak.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_autosync_plugin.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_azure_credential_resolution.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_backend.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_baseline_escalation_message.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_baseline_persistence.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_baseline_probe.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_bash_routing.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_bats_suites_gated.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_bookkeeping_commit_crash_run.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_bookkeeping_no_verify.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_changelog_release_wiring.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_changelog_schema.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_claude_resolution.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_close_wu_self_reference.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_closing_deliverable_guard.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_closing_guard_contracts.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_closing_guard_prediction.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_closing_requirements.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_console_encoding.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_correlation_id_override.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_cost_lifetime.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_cost_tracking.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_degraded_oracle.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_deliverable_presence_gate.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_derive_monitoring_discovery.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_derive_monitoring_skill_registration.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_design_for_diagnosis_rule.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_diagnose_headless.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_diagnose_issue_skill.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_diagnosis.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_diagnosis_roundtrip_live.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_dispatch_skeleton.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_doctor.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_done_feature_gates.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_driver_event_registry_covers_emitters.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_driver_integration.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_driver_lock.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_duration_tracking.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_empty_files_escalation.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_ensure_feature_branch.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_ensure_feature_branch_base.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_escalation_contract.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_escalation_emit.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_event_gate_full_envelope.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_events_redaction.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_events_stats.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_extra_gates.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_failure_artifact_model.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_feature_id_resolution.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_feature_review_accumulation.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_filelock_portable.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_fingerprint.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_fix_bug_headless.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_force_full_close.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gate_budget.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gate_budget_post_dispatch.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gate_eval.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gate_eval_calibration.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gate_eval_intermediate_wiring.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gate_eval_terminal_wiring.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gate_report_verdict.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gh_backend.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gh_features.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_git_env_isolation.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_gitleaks_pinning.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_hashed_denylist.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_hashed_denylist_ci.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_heartbeat_adapter.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_hedged_kind_contract.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_init_integration.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_interpreter_normalization.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_label_registry.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_label_registry_covers_consumers.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_leak_findings_redaction.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_leak_scan.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_leak_scan_content.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_learnings_query.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_learnings_staging.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_legacy_4wu_terminal_flips.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lifecycle_integration.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lifetime_accounting.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_bare_produces_path.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_boundary_consistency.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_boundary_extraction.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_close_intermediate.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_close_wu.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_closing.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_correlation_id.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_monitoring.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_oracle_env.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_plan_contract_fields.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_plan_errors.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_plan_next_draft.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_plan_relative_import.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_plan_verdict_exempt.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_planning_sections.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_produces_driver_helper.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_produces_satisfiability.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_roadmap.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_sections.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_task_graph_yaml_selection.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_lint_verification_reference.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_attempt_evidence.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_auto_archive.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_caveman_preamble.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_close_intermediate.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_defaults_by_type.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_effort.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_failure_note_cap.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_files_changed_guard.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_gate_budget.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_model_alias.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_orchestration.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_post_pass_invariant.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_reset_preserving_events.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_smoke_runner.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_unsandboxed.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_loop_zero_token_guard.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_migrate_legacy.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_miniyaml_equivalence.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_miniyaml_negative.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitor_cli.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitor_issue_lifecycle.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitor_runner_surfaces.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitoring_bootstrap_artifacts.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitoring_cron_dialect.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitoring_example.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitoring_fenced_blocks.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_monitoring_seed.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_no_pytest_subprocess.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_operator_escalation_rule.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_plan_baseline.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_planned_cost_lint.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_planning_cost_floor.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_prepare_feature.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_prepare_scaffold_sync.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_produces_field.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_produces_in_diff.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_produces_semantics_unified.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_provision_labels.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_queue_stalled_adapter.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_redaction_windows_home.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_resolve_base.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_result_block.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_roadmap_add_skill.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_roadmap_archive_skill.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_roadmap_link_gate.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_roadmap_row_parser.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_routed_finding_tracking.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_doc_hygiene.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_docs.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_init.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_label_provisioning.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_manifest.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_seed_sanity.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_scaffold_upgrade.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_schedule_dialect.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_service_bus_dlq_adapter.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_skill_discovery_links.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_skills_vendored_in_sync.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_spinning_rearm_gate.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_squash_commit_hook_crash.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_template_closing_shapes.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_terminal_flip_ownership.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_terminal_flips.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_timeout_kill_portable.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_untracked_feature_folder.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_untracked_file_absorption.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_upgrade_integration.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_validate_event.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_validate_event_driver_types.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_verdict_coupling.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_verify_empty_gate_set.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_version_consistency.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_version_skew.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_windows_gate_exec.py +0 -0
- {specfuse_loop-0.9.0 → specfuse_loop-0.9.2}/tests/test_wu_execution_metadata.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specfuse-loop"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.2"
|
|
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,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Consumer-visible changes to this project, newest first.
|
|
4
|
+
|
|
5
|
+
<!--
|
|
6
|
+
How entries get here. Two collection points, because bugs have no close
|
|
7
|
+
ceremony:
|
|
8
|
+
|
|
9
|
+
- a feature's close appends its "Consumer-visible contract changes"
|
|
10
|
+
enumeration (close-discipline.md §3) to Unreleased, one entry per change;
|
|
11
|
+
- `fix-bug` appends one line carrying the issue number before opening its PR.
|
|
12
|
+
|
|
13
|
+
Every entry carries a `FEAT-YYYY-NNNN` or `#<issue>` trace in trailing
|
|
14
|
+
parentheses, and sits under one of four class headings: Breaking, Added,
|
|
15
|
+
Changed, Fixed. An entry without a trace is a lint finding — a changelog line
|
|
16
|
+
nobody can trace back to the work that produced it is a line nobody can act on.
|
|
17
|
+
|
|
18
|
+
No backfill. This file starts empty on purpose. Reconstructing history from
|
|
19
|
+
commit subjects produces entries that read plausibly and describe the wrong
|
|
20
|
+
thing; the enumeration a close already wrote is the source, and work that
|
|
21
|
+
predates this file has none to lift.
|
|
22
|
+
|
|
23
|
+
Cutting a release stamps Unreleased with the version and freezes it. Appends
|
|
24
|
+
only ever go to Unreleased.
|
|
25
|
+
-->
|
|
26
|
+
|
|
27
|
+
## [Unreleased]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.9.2
|
|
@@ -145,17 +145,6 @@ The session running `plan-next` mints the next gate's substantive IDs as drafts;
|
|
|
145
145
|
the human arms them. When adding a new closing WU type to `lint_plan.py`, also
|
|
146
146
|
add its `<NAME>` segment to `CORRELATION_ID_RE` so the new IDs pass validation.
|
|
147
147
|
|
|
148
|
-
**Two enforcement surfaces, one contract.** `lint_plan.py`'s `CORRELATION_ID_RE`
|
|
149
|
-
enforces this format on `PLAN.md` graphs and WU frontmatter. The event envelope
|
|
150
|
-
(`specfuse/loop/data/schemas/event.schema.json`) enforces a narrower vendored
|
|
151
|
-
`correlation_id` pattern that does not yet know the closing-sequence or hygiene
|
|
152
|
-
shapes; the loop widens it without editing the vendored file, via a driver-local
|
|
153
|
-
registry (`specfuse/loop/data/schemas/driver-event.schema.json`'s `correlation_id`
|
|
154
|
-
key, read by `validate_event.py`'s `load_validator`) — the same deep-copy
|
|
155
|
-
fall-through FEAT-2026-0060 established for `event_type` (FEAT-2026-0073). When
|
|
156
|
-
adding a new `<NAME>` segment, update `CORRELATION_ID_RE` **and** that registry's
|
|
157
|
-
`closing_names` so both surfaces admit the new shape together.
|
|
158
|
-
|
|
159
148
|
Do not reuse an ordinal even after a unit is abandoned. Once an ID has appeared in
|
|
160
149
|
the event log, it is spent. Reusing it would make history ambiguous.
|
|
161
150
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Specfuse Contributors
|
|
3
|
+
# Licensed under the Apache License, Version 2.0. See LICENSE.
|
|
4
|
+
#
|
|
5
|
+
"""Read the gate sets out of `.specfuse/verification.yml` (#592).
|
|
6
|
+
|
|
7
|
+
`verification.yml` is the source of truth for what gets verified. Anything
|
|
8
|
+
that runs gates should derive its list from here rather than keeping a copy.
|
|
9
|
+
|
|
10
|
+
`scripts/smoke-test.sh` used to carry a hand-maintained mirror under the
|
|
11
|
+
comment "Keep in sync". It drifted by six gates, and two of them
|
|
12
|
+
(`roadmap-link-gate`, `arm-sweep-gate`) shipped with features that week and
|
|
13
|
+
were never once executed by CI. A declared-but-unrun gate is worse than an
|
|
14
|
+
absent one, because the declaration reads as coverage.
|
|
15
|
+
|
|
16
|
+
Deliberately not a YAML dependency: `_miniyaml` already parses this project's
|
|
17
|
+
files, and adding a third-party parser to a path CI depends on would trade one
|
|
18
|
+
drift risk for an install risk.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import re
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
_SET_RE = re.compile(r"^([a-z_][a-z0-9_]*):\s*$", re.M)
|
|
27
|
+
_NAME_RE = re.compile(r"^ - name:\s*(\S+)\s*$", re.M)
|
|
28
|
+
_COMMAND_RE = re.compile(r'^\s+command:\s*"(.*?)"\s*$', re.M | re.S)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _set_block(text: str, set_name: str) -> str:
|
|
32
|
+
"""Return the raw text of one top-level gate set, without its siblings."""
|
|
33
|
+
starts = [(m.group(1), m.start()) for m in _SET_RE.finditer(text)]
|
|
34
|
+
for i, (name, pos) in enumerate(starts):
|
|
35
|
+
if name != set_name:
|
|
36
|
+
continue
|
|
37
|
+
end = starts[i + 1][1] if i + 1 < len(starts) else len(text)
|
|
38
|
+
return text[pos:end]
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def iter_code_gates(
|
|
43
|
+
verification_yml: "Path | str", set_name: str = "code"
|
|
44
|
+
) -> list[tuple[str, str]]:
|
|
45
|
+
"""Yield `(name, command)` for every gate in *set_name*, in declared order.
|
|
46
|
+
|
|
47
|
+
Order is preserved because gate order is a real signal: the cheap,
|
|
48
|
+
fast-failing gates are declared first so a broken build reports in seconds
|
|
49
|
+
rather than after the full suite.
|
|
50
|
+
"""
|
|
51
|
+
text = Path(verification_yml).read_text(encoding="utf-8")
|
|
52
|
+
block = _set_block(text, set_name)
|
|
53
|
+
if not block:
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
gates: list[tuple[str, str]] = []
|
|
57
|
+
entries = re.split(r"^ - name:\s*", block, flags=re.M)[1:]
|
|
58
|
+
for entry in entries:
|
|
59
|
+
name = entry.split("\n", 1)[0].strip()
|
|
60
|
+
m = _COMMAND_RE.search(entry)
|
|
61
|
+
command = m.group(1).strip() if m else ""
|
|
62
|
+
gates.append((name, command))
|
|
63
|
+
return gates
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def code_gate_names(
|
|
67
|
+
verification_yml: "Path | str", set_name: str = "code"
|
|
68
|
+
) -> list[str]:
|
|
69
|
+
"""Names only, for parity checks."""
|
|
70
|
+
text = Path(verification_yml).read_text(encoding="utf-8")
|
|
71
|
+
return _NAME_RE.findall(_set_block(text, set_name))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main() -> int:
|
|
75
|
+
"""Print `name<TAB>command` per gate, for shell consumption.
|
|
76
|
+
|
|
77
|
+
Used by `scripts/smoke-test.sh`, which substitutes its own interpreter for
|
|
78
|
+
a leading `python3`: the declared commands name `python3` because that is
|
|
79
|
+
what a target project's driver invokes, while the smoke test must use the
|
|
80
|
+
virtualenv interpreter that actually has the dependencies installed.
|
|
81
|
+
"""
|
|
82
|
+
import argparse
|
|
83
|
+
|
|
84
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
85
|
+
ap.add_argument("verification_yml")
|
|
86
|
+
ap.add_argument("--set", default="code", dest="set_name")
|
|
87
|
+
args = ap.parse_args()
|
|
88
|
+
for name, command in iter_code_gates(args.verification_yml, args.set_name):
|
|
89
|
+
print(f"{name}\t{command}")
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
raise SystemExit(main())
|
|
@@ -84,6 +84,20 @@ MODEL_ALIASES = frozenset({"sonnet", "opus", "haiku"})
|
|
|
84
84
|
VALID_EFFORT = frozenset({"low", "medium", "high", "xhigh", "max"})
|
|
85
85
|
FULL_MODEL_ID_RE = re.compile(r"^claude-\w[\w.-]*$")
|
|
86
86
|
|
|
87
|
+
# Two enforcement surfaces, one contract. This pattern governs PLAN.md graphs
|
|
88
|
+
# and WU frontmatter. The event envelope enforces its own, narrower
|
|
89
|
+
# `correlation_id` pattern (`data/schemas/event.schema.json`, vendored from the
|
|
90
|
+
# methodology core and never edited here); the driver widens it via the
|
|
91
|
+
# driver-local registry `data/schemas/driver-event.schema.json`, read by
|
|
92
|
+
# `validate_event.load_validator` on a deep copy — the fall-through
|
|
93
|
+
# FEAT-2026-0060 established for `event_type` and FEAT-2026-0073 extended to
|
|
94
|
+
# correlation IDs.
|
|
95
|
+
#
|
|
96
|
+
# ADDING A NEW `<NAME>` SEGMENT: update this pattern AND that registry's
|
|
97
|
+
# `closing_names`, or IDs the linter accepts will be rejected by envelope
|
|
98
|
+
# validation. This note lives here, at the change site, rather than in
|
|
99
|
+
# `rules/correlation-ids.md` — that file is vendored from core, so a loop-local
|
|
100
|
+
# addition to it is reverted by the next `sync-scaffold.sh` run (#581).
|
|
87
101
|
CORRELATION_ID_RE = re.compile(
|
|
88
102
|
r"^(FEAT-\d{4}-\d{4}(/(T\d{2}(H\d*)?|G\d+-(RETRO|LESSONS|DOCS|PLAN|CLOSE-INTERMEDIATE|CLOSE)))?|"
|
|
89
103
|
r"INIT-\d{4}-\d{4}/F\d{2}(/(T\d{2}(H\d*)?|G\d+-(RETRO|LESSONS|DOCS|PLAN|CLOSE-INTERMEDIATE|CLOSE)))?)$"
|
|
@@ -226,6 +240,15 @@ def read_frontmatter(path: Path) -> tuple[dict, str]:
|
|
|
226
240
|
j = 1
|
|
227
241
|
while j < len(lines) and not FM.match(lines[j]):
|
|
228
242
|
j += 1
|
|
243
|
+
if j >= len(lines):
|
|
244
|
+
# See loop.read_frontmatter: an opening `---` with no closing one made
|
|
245
|
+
# the scan run off the end and parse the document as frontmatter,
|
|
246
|
+
# reporting the failure against an arbitrary body line (#306).
|
|
247
|
+
raise _miniyaml.MiniYAMLError(
|
|
248
|
+
f"{path}: frontmatter block opened with `---` on line 1 but is "
|
|
249
|
+
f"never closed — add a `---` line to end it. Every line after it "
|
|
250
|
+
f"was read as frontmatter, so no check could evaluate this file."
|
|
251
|
+
)
|
|
229
252
|
return _miniyaml.parse("\n".join(lines[1:j])) or {}, "\n".join(lines[j + 1:])
|
|
230
253
|
|
|
231
254
|
|
|
@@ -645,6 +668,60 @@ def _match_dnt_boundary(
|
|
|
645
668
|
return None
|
|
646
669
|
|
|
647
670
|
|
|
671
|
+
def check_produces_shape(feature_dir: Path, gates: list) -> list[str]:
|
|
672
|
+
"""ERROR when a WU's `produces:` entry is a directory rather than a file
|
|
673
|
+
or glob.
|
|
674
|
+
|
|
675
|
+
Same family as `check_produces_boundary` below, and the same argument: the
|
|
676
|
+
driver refuses this outright, but only after a full `claude -p` session.
|
|
677
|
+
Since the agent cannot edit its own frontmatter, every retry is
|
|
678
|
+
byte-identical -- a real feature paid $6.42 and 20.6 minutes across three
|
|
679
|
+
of them before `spinning_detected` fired, while `specfuse-lint` reported
|
|
680
|
+
`OK - structurally valid` throughout (#593).
|
|
681
|
+
|
|
682
|
+
Every input is static, so this belongs in the pre-dispatch checklist. The
|
|
683
|
+
message is rendered by `loop.produces_shape_error`, the same function the
|
|
684
|
+
driver's guards call, so all three surfaces read identically rather than
|
|
685
|
+
drifting into three descriptions of one rule.
|
|
686
|
+
|
|
687
|
+
Applies to every WU with a `produces:` entry regardless of status: a `done`
|
|
688
|
+
WU with a directory entry is a folder authored against the pre-0.9.0
|
|
689
|
+
contract, and reporting it is how a migrating project finds them all at
|
|
690
|
+
once instead of one dispatch at a time.
|
|
691
|
+
"""
|
|
692
|
+
from .loop import produces_shape_error
|
|
693
|
+
|
|
694
|
+
found: list[str] = []
|
|
695
|
+
for gate in gates:
|
|
696
|
+
for entry in gate.get("work_units") or []:
|
|
697
|
+
wid, wfile = entry.get("id"), entry.get("file")
|
|
698
|
+
if not wid or not wfile:
|
|
699
|
+
continue
|
|
700
|
+
wpath = feature_dir / wfile
|
|
701
|
+
if not wpath.exists():
|
|
702
|
+
continue
|
|
703
|
+
wfm, _ = read_frontmatter(wpath)
|
|
704
|
+
produces_raw = wfm.get("produces")
|
|
705
|
+
if not produces_raw:
|
|
706
|
+
continue
|
|
707
|
+
raw_entries = (
|
|
708
|
+
produces_raw if isinstance(produces_raw, list) else [produces_raw]
|
|
709
|
+
)
|
|
710
|
+
for p in raw_entries:
|
|
711
|
+
p_s = str(p).strip()
|
|
712
|
+
if not p_s:
|
|
713
|
+
continue
|
|
714
|
+
err = produces_shape_error(p_s)
|
|
715
|
+
if err:
|
|
716
|
+
found.append(
|
|
717
|
+
f"ERROR: {wfile}: {wid} {err}. assert_declared_"
|
|
718
|
+
f"deliverables refuses this after a full dispatch "
|
|
719
|
+
f"attempt, and a retry cannot help — the agent cannot "
|
|
720
|
+
f"edit its own frontmatter. See #593."
|
|
721
|
+
)
|
|
722
|
+
return found
|
|
723
|
+
|
|
724
|
+
|
|
648
725
|
def check_produces_boundary(feature_dir: Path, gates: list) -> list[str]:
|
|
649
726
|
"""ERROR when a WU's own `produces:` path (or `produces_driver_helper`
|
|
650
727
|
surface) falls inside its own Do-not-touch section's paths.
|
|
@@ -879,6 +956,21 @@ def check_planning_sections(
|
|
|
879
956
|
|
|
880
957
|
|
|
881
958
|
def lint(feature_dir: Path) -> list[str]:
|
|
959
|
+
"""Lint a feature folder, converting an unparseable file into a finding.
|
|
960
|
+
|
|
961
|
+
A malformed WU used to abort the whole run with a traceback, which made
|
|
962
|
+
the folder unevaluable by *every* check rather than one (#306). The lint's
|
|
963
|
+
job is to report what is wrong with a folder; a file it cannot parse is
|
|
964
|
+
the most basic thing that can be wrong with one, so it is a finding like
|
|
965
|
+
any other. Callers still get a non-empty error list and a non-zero exit.
|
|
966
|
+
"""
|
|
967
|
+
try:
|
|
968
|
+
return _lint_impl(feature_dir)
|
|
969
|
+
except _miniyaml.MiniYAMLError as exc:
|
|
970
|
+
return [f"ERROR: unparseable frontmatter — {exc}"]
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def _lint_impl(feature_dir: Path) -> list[str]:
|
|
882
974
|
errs: list[str] = []
|
|
883
975
|
plan = feature_dir / "PLAN.md"
|
|
884
976
|
if not plan.exists():
|
|
@@ -1198,6 +1290,7 @@ def lint(feature_dir: Path) -> list[str]:
|
|
|
1198
1290
|
check_closing_guard_literals(feature_dir, gates)
|
|
1199
1291
|
check_autoclose_debt_prediction(feature_dir, gates)
|
|
1200
1292
|
check_produces_satisfiability(feature_dir, gates)
|
|
1293
|
+
errs.extend(check_produces_shape(feature_dir, gates))
|
|
1201
1294
|
errs.extend(check_produces_boundary(feature_dir, gates))
|
|
1202
1295
|
errs.extend(check_done_feature_gates(feature_dir, fm))
|
|
1203
1296
|
|
|
@@ -60,7 +60,7 @@ from . import _filelock
|
|
|
60
60
|
from . import _miniyaml
|
|
61
61
|
from . import _wu_sections
|
|
62
62
|
from . import scaffold as _scaffold
|
|
63
|
-
from .changelog import parse_changelog
|
|
63
|
+
from .changelog import ENTRY_CLASSES, parse_changelog
|
|
64
64
|
from .closing_requirements import (
|
|
65
65
|
AUTOCLOSE_DEBT_MARKER_RE,
|
|
66
66
|
CHANGELOG_PATH,
|
|
@@ -105,7 +105,7 @@ SPECFUSE_DIR = Path(".specfuse")
|
|
|
105
105
|
REPO_ROOT = SPECFUSE_DIR.parent
|
|
106
106
|
FEATURES_DIR = SPECFUSE_DIR / "features"
|
|
107
107
|
VERIFICATION_PATH = SPECFUSE_DIR / "verification.yml"
|
|
108
|
-
DRIVER_VERSION = "0.9.
|
|
108
|
+
DRIVER_VERSION = "0.9.2"
|
|
109
109
|
# Oldest scaffold layout this driver can drive. init.sh stamps the scaffold's own
|
|
110
110
|
# version into `.specfuse/VERSION`; check_scaffold_version() fails loud at startup if
|
|
111
111
|
# the consumer's scaffold is older than this, pointing at `specfuse upgrade`. Bump
|
|
@@ -245,6 +245,17 @@ def read_frontmatter(path: Path) -> tuple[dict, str]:
|
|
|
245
245
|
j = 1
|
|
246
246
|
while j < len(lines) and not FM.match(lines[j]):
|
|
247
247
|
j += 1
|
|
248
|
+
if j >= len(lines):
|
|
249
|
+
# Unterminated block: an opening `---` with no closing one. Without
|
|
250
|
+
# this check the scan runs off the end, the whole document is parsed
|
|
251
|
+
# as frontmatter, and the failure is reported against whatever body
|
|
252
|
+
# line happens to be first — which sent #306 chasing an HTML comment
|
|
253
|
+
# that was never the problem. Name the actual defect instead.
|
|
254
|
+
raise _miniyaml.MiniYAMLError(
|
|
255
|
+
f"{path}: frontmatter block opened with `---` on line 1 but is "
|
|
256
|
+
f"never closed — add a `---` line to end it. Every line after it "
|
|
257
|
+
f"was read as frontmatter, so no check could evaluate this file."
|
|
258
|
+
)
|
|
248
259
|
try:
|
|
249
260
|
fm = _miniyaml.parse("\n".join(lines[1:j])) or {}
|
|
250
261
|
except _miniyaml.MiniYAMLError as exc:
|
|
@@ -4674,7 +4685,12 @@ def assert_changelog_entry_for_contract_changes(
|
|
|
4674
4685
|
return (
|
|
4675
4686
|
False,
|
|
4676
4687
|
f"assert_changelog_entry_for_contract_changes: '{CONSUMER_VISIBLE_HEADING}' "
|
|
4677
|
-
f"names a real change but {CHANGELOG_PATH} does not exist"
|
|
4688
|
+
f"names a real change but {CHANGELOG_PATH} does not exist. Create it at "
|
|
4689
|
+
f"the repo root with a '## [Unreleased]' heading, then append this "
|
|
4690
|
+
f"feature's enumeration under one of: "
|
|
4691
|
+
f"{', '.join(sorted(ENTRY_CLASSES))}. Projects scaffolded before "
|
|
4692
|
+
f"{CHANGELOG_PATH} was seeded will not have one; `specfuse upgrade` "
|
|
4693
|
+
f"creates it without touching an existing file (#575)",
|
|
4678
4694
|
)
|
|
4679
4695
|
result = parse_changelog(changelog.read_text())
|
|
4680
4696
|
unreleased = result.unreleased()
|
|
@@ -4795,6 +4811,52 @@ def assert_implementation_touched_files(
|
|
|
4795
4811
|
)
|
|
4796
4812
|
|
|
4797
4813
|
|
|
4814
|
+
def produces_shape_error(path: str) -> "str | None":
|
|
4815
|
+
"""Return why *path* is an invalid ``produces:`` entry, or None if it is fine.
|
|
4816
|
+
|
|
4817
|
+
Shape only -- never presence. A path that does not exist yet is perfectly
|
|
4818
|
+
valid at draft time; whether it was actually produced is the dispatch
|
|
4819
|
+
gate's question, asked after the session.
|
|
4820
|
+
|
|
4821
|
+
Split out of ``assert_declared_deliverables`` so the one fact that needs no
|
|
4822
|
+
session output can be checked by every surface that has the frontmatter:
|
|
4823
|
+
``lint_plan`` at draft/arm/conformance time, and the driver before it
|
|
4824
|
+
spends a token (#593). A real feature paid $6.42 across three byte-identical
|
|
4825
|
+
refusals for a ``Path.is_dir()`` call, with ``specfuse-lint`` reporting
|
|
4826
|
+
``OK - structurally valid`` the whole time.
|
|
4827
|
+
|
|
4828
|
+
The three surfaces must read identically, so all render this one string.
|
|
4829
|
+
"""
|
|
4830
|
+
if path.endswith("/") or Path(path).is_dir():
|
|
4831
|
+
return (
|
|
4832
|
+
f"declared deliverable is a directory: {path} — directories "
|
|
4833
|
+
"are not valid produces: entries under the unified "
|
|
4834
|
+
"literal/glob contract (a directory always passed presence "
|
|
4835
|
+
"while failing assert_produces_in_diff's diff match); declare "
|
|
4836
|
+
"the specific file(s) or a glob instead"
|
|
4837
|
+
)
|
|
4838
|
+
return None
|
|
4839
|
+
|
|
4840
|
+
|
|
4841
|
+
def assert_produces_shape(wu: WorkUnit) -> tuple[bool, str]:
|
|
4842
|
+
"""Pre-dispatch half of ``assert_declared_deliverables`` (#593).
|
|
4843
|
+
|
|
4844
|
+
Every input is static: the WU's own frontmatter and the filesystem. None
|
|
4845
|
+
of it depends on what a session did, so refusing here converts a $2.46
|
|
4846
|
+
attempt into a $0.00 refusal -- the lever ``planning-discipline.md`` §5
|
|
4847
|
+
already names, where ``assert_verdict_well_formed`` fired 10 times for
|
|
4848
|
+
nothing because it is checked before the agent spends anything.
|
|
4849
|
+
|
|
4850
|
+
The agent cannot edit its own frontmatter, so a WU that fails this cannot
|
|
4851
|
+
be fixed by retrying it; escalating immediately is the only honest move.
|
|
4852
|
+
"""
|
|
4853
|
+
for raw in wu.produces or []:
|
|
4854
|
+
err = produces_shape_error(str(raw))
|
|
4855
|
+
if err:
|
|
4856
|
+
return False, err
|
|
4857
|
+
return True, ""
|
|
4858
|
+
|
|
4859
|
+
|
|
4798
4860
|
def assert_declared_deliverables(wu: WorkUnit) -> tuple[bool, str]:
|
|
4799
4861
|
"""Deliverable-presence gate (FEAT-2026-0022/T02), unified with
|
|
4800
4862
|
``assert_produces_in_diff``'s literal/glob contract (FEAT-2026-0055/T03).
|
|
@@ -4826,14 +4888,11 @@ def assert_declared_deliverables(wu: WorkUnit) -> tuple[bool, str]:
|
|
|
4826
4888
|
for raw in wu.produces:
|
|
4827
4889
|
path = str(raw)
|
|
4828
4890
|
p = Path(path)
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
4833
|
-
|
|
4834
|
-
"while failing assert_produces_in_diff's diff match); declare "
|
|
4835
|
-
"the specific file(s) or a glob instead"
|
|
4836
|
-
)
|
|
4891
|
+
# Shape refusal is shared with the pre-dispatch guard and lint_plan so
|
|
4892
|
+
# all three surfaces render one message (#593).
|
|
4893
|
+
shape_err = produces_shape_error(path)
|
|
4894
|
+
if shape_err:
|
|
4895
|
+
return False, shape_err
|
|
4837
4896
|
if any(ch in path for ch in "*?["):
|
|
4838
4897
|
matches = [
|
|
4839
4898
|
m for m in glob.glob(path)
|
|
@@ -5511,6 +5570,34 @@ def run(
|
|
|
5511
5570
|
blocked = True
|
|
5512
5571
|
continue
|
|
5513
5572
|
|
|
5573
|
+
# produces: shape gate (#593). Same posture as the re-arm gate
|
|
5574
|
+
# above: fires BEFORE any dispatch state change, so a WU the
|
|
5575
|
+
# driver is guaranteed to refuse costs $0.00 instead of a full
|
|
5576
|
+
# session. Every input is static frontmatter plus the
|
|
5577
|
+
# filesystem, and the agent cannot edit its own frontmatter --
|
|
5578
|
+
# so retrying is provably useless. A real feature paid $6.42
|
|
5579
|
+
# across three byte-identical refusals before spinning fired.
|
|
5580
|
+
shape_ok, shape_reason = assert_produces_shape(wu)
|
|
5581
|
+
if not shape_ok:
|
|
5582
|
+
backend.set_wu(wu, "status", "blocked_human")
|
|
5583
|
+
flush_events(events_path, [build_event(
|
|
5584
|
+
"human_escalation", wu.wu_id, {
|
|
5585
|
+
"reason": "produces_shape_invalid",
|
|
5586
|
+
"blocked_reason": shape_reason,
|
|
5587
|
+
"attempts": 0,
|
|
5588
|
+
"failure_class": "guard_refusal",
|
|
5589
|
+
"failure_signature": "assert_produces_shape",
|
|
5590
|
+
})])
|
|
5591
|
+
commit_bookkeeping(
|
|
5592
|
+
[wu.file, events_path],
|
|
5593
|
+
f"chore(loop): {wu.wu_id} blocked "
|
|
5594
|
+
f"(produces: shape invalid)"
|
|
5595
|
+
f"\n\nFeature: {wu.wu_id}",
|
|
5596
|
+
)
|
|
5597
|
+
print(f" BLOCKED before dispatch (no cost) — {shape_reason}")
|
|
5598
|
+
blocked = True
|
|
5599
|
+
continue
|
|
5600
|
+
|
|
5514
5601
|
head_before = git("rev-parse", "HEAD")
|
|
5515
5602
|
# Snapshot the operator's untracked files alongside head_before:
|
|
5516
5603
|
# squash_commit must not absorb WIP that pre-dates this dispatch
|
|
@@ -13,6 +13,9 @@ from pathlib import Path
|
|
|
13
13
|
|
|
14
14
|
from . import _miniyaml
|
|
15
15
|
from . import labels as _labels
|
|
16
|
+
# The one name for the file close-k reads; scaffold seeds what that guard
|
|
17
|
+
# requires, so both must agree on the path (#575).
|
|
18
|
+
from .closing_requirements import CHANGELOG_PATH
|
|
16
19
|
|
|
17
20
|
try:
|
|
18
21
|
from importlib.resources.abc import Traversable # Python 3.11+
|
|
@@ -163,8 +166,11 @@ _SEED_RENAME: dict[str, str] = {
|
|
|
163
166
|
|
|
164
167
|
# Seed files handled by other work units; skip during init.
|
|
165
168
|
# - gitignore.snippet: handled by T05 (wire_claude)
|
|
169
|
+
# - CHANGELOG.seed.md: a repo-root file, not a .specfuse/ one; wire_claude
|
|
170
|
+
# writes it to the project root (#575)
|
|
166
171
|
_SKIP_SEEDS: frozenset[str] = frozenset({
|
|
167
172
|
"gitignore.snippet",
|
|
173
|
+
"CHANGELOG.seed.md",
|
|
168
174
|
})
|
|
169
175
|
|
|
170
176
|
|
|
@@ -260,6 +266,26 @@ def _write_gitignore(target_path: Path) -> None:
|
|
|
260
266
|
gitignore.write_text(existing + snippet, encoding="utf-8")
|
|
261
267
|
|
|
262
268
|
|
|
269
|
+
def _write_changelog(target_path: Path) -> None:
|
|
270
|
+
"""Seed a root CHANGELOG.md when the project has none (#575).
|
|
271
|
+
|
|
272
|
+
`close-k` fails a close whose §3 enumeration names a real consumer-visible
|
|
273
|
+
change unless `CHANGELOG.md`'s Unreleased gained a matching entry. Before
|
|
274
|
+
this, `specfuse init` never created the file, so a fresh project's first
|
|
275
|
+
such feature failed at its terminal close — the most expensive point in a
|
|
276
|
+
feature to be blocked — on a file the scaffold owed it.
|
|
277
|
+
|
|
278
|
+
Create-only. An existing CHANGELOG.md is a project's own document and is
|
|
279
|
+
never read, merged, or overwritten: `upgrade` routes through `wire_claude`
|
|
280
|
+
too, and silently rewriting a consumer-facing file on upgrade is worse than
|
|
281
|
+
the gap this closes.
|
|
282
|
+
"""
|
|
283
|
+
changelog = target_path / CHANGELOG_PATH
|
|
284
|
+
if changelog.exists():
|
|
285
|
+
return
|
|
286
|
+
changelog.write_bytes(read_scaffold("CHANGELOG.seed.md"))
|
|
287
|
+
|
|
288
|
+
|
|
263
289
|
def _write_claude_md(target_path: Path) -> None:
|
|
264
290
|
claude_dir = target_path / ".claude"
|
|
265
291
|
claude_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -355,6 +381,7 @@ def wire_claude(target: str | Path) -> None:
|
|
|
355
381
|
"""
|
|
356
382
|
target_path = Path(target)
|
|
357
383
|
_write_gitignore(target_path)
|
|
384
|
+
_write_changelog(target_path)
|
|
358
385
|
_write_claude_md(target_path)
|
|
359
386
|
_write_settings_json(target_path)
|
|
360
387
|
refresh_claude_plugin_config(target_path)
|
|
@@ -146,6 +146,11 @@ def load_driver_correlation_patterns() -> dict | None:
|
|
|
146
146
|
A missing, unreadable, or structurally incomplete registry file degrades
|
|
147
147
|
to None (no widening — vendored-only validation) rather than raising,
|
|
148
148
|
matching ``load_driver_event_types``'s existing contract.
|
|
149
|
+
|
|
150
|
+
ADDING A NEW ``<NAME>`` SEGMENT: update ``closing_names`` here AND
|
|
151
|
+
``lint_plan.CORRELATION_ID_RE``, or the linter will accept IDs that
|
|
152
|
+
envelope validation rejects. The two surfaces enforce one contract and
|
|
153
|
+
must move together (#581).
|
|
149
154
|
"""
|
|
150
155
|
global _DRIVER_CORRELATION_PATTERNS_CACHE
|
|
151
156
|
if _DRIVER_CORRELATION_PATTERNS_CACHE is not _UNSET:
|
|
@@ -15,6 +15,7 @@ specfuse/loop/closing_requirements.py
|
|
|
15
15
|
specfuse/loop/cost.py
|
|
16
16
|
specfuse/loop/escalation.py
|
|
17
17
|
specfuse/loop/events_stats.py
|
|
18
|
+
specfuse/loop/gate_commands.py
|
|
18
19
|
specfuse/loop/gate_eval.py
|
|
19
20
|
specfuse/loop/gh_backend.py
|
|
20
21
|
specfuse/loop/gh_features.py
|
|
@@ -27,6 +28,7 @@ specfuse/loop/loop.py
|
|
|
27
28
|
specfuse/loop/plan_baseline.py
|
|
28
29
|
specfuse/loop/scaffold.py
|
|
29
30
|
specfuse/loop/validate_event.py
|
|
31
|
+
specfuse/loop/data/CHANGELOG.seed.md
|
|
30
32
|
specfuse/loop/data/LEARNINGS.template.md
|
|
31
33
|
specfuse/loop/data/VERSION
|
|
32
34
|
specfuse/loop/data/gitignore.snippet
|
|
@@ -128,6 +130,7 @@ tests/test_bookkeeping_no_verify.py
|
|
|
128
130
|
tests/test_changelog_collection.py
|
|
129
131
|
tests/test_changelog_release_wiring.py
|
|
130
132
|
tests/test_changelog_schema.py
|
|
133
|
+
tests/test_ci_runs_every_gate.py
|
|
131
134
|
tests/test_claude_resolution.py
|
|
132
135
|
tests/test_close_wu_self_reference.py
|
|
133
136
|
tests/test_closing_deliverable_guard.py
|
|
@@ -136,6 +139,7 @@ tests/test_closing_guard_prediction.py
|
|
|
136
139
|
tests/test_closing_requirements.py
|
|
137
140
|
tests/test_console_encoding.py
|
|
138
141
|
tests/test_correlation_id_override.py
|
|
142
|
+
tests/test_correlation_id_two_surfaces.py
|
|
139
143
|
tests/test_cost_lifetime.py
|
|
140
144
|
tests/test_cost_tracking.py
|
|
141
145
|
tests/test_degraded_oracle.py
|
|
@@ -256,6 +260,7 @@ tests/test_prepare_scaffold_sync.py
|
|
|
256
260
|
tests/test_produces_field.py
|
|
257
261
|
tests/test_produces_in_diff.py
|
|
258
262
|
tests/test_produces_semantics_unified.py
|
|
263
|
+
tests/test_produces_shape.py
|
|
259
264
|
tests/test_provision_labels.py
|
|
260
265
|
tests/test_queue_stalled_adapter.py
|
|
261
266
|
tests/test_redaction_windows_home.py
|
|
@@ -286,6 +291,7 @@ tests/test_template_closing_shapes.py
|
|
|
286
291
|
tests/test_terminal_flip_ownership.py
|
|
287
292
|
tests/test_terminal_flips.py
|
|
288
293
|
tests/test_timeout_kill_portable.py
|
|
294
|
+
tests/test_unterminated_frontmatter.py
|
|
289
295
|
tests/test_untracked_feature_folder.py
|
|
290
296
|
tests/test_untracked_file_absorption.py
|
|
291
297
|
tests/test_upgrade_integration.py
|
|
@@ -216,3 +216,43 @@ class TestChangelogCollection(unittest.TestCase):
|
|
|
216
216
|
|
|
217
217
|
if __name__ == "__main__":
|
|
218
218
|
unittest.main()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class TestMissingChangelogMessage(unittest.TestCase):
|
|
222
|
+
"""#575 — the absent-file halt must name the fix, not just the absence.
|
|
223
|
+
|
|
224
|
+
The bare message reported that CHANGELOG.md does not exist without saying
|
|
225
|
+
the operator should create one or that the scaffold now seeds it, so a
|
|
226
|
+
first-time user read a terminal-close halt as a broken install.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
def _reason_for_missing_changelog(self, root: Path) -> str:
|
|
230
|
+
fdir = root / "feature"
|
|
231
|
+
fdir.mkdir()
|
|
232
|
+
(fdir / "RETROSPECTIVE.md").write_text(
|
|
233
|
+
"# Retrospective\n\n"
|
|
234
|
+
"## Consumer-visible contract changes\n\n"
|
|
235
|
+
"1. **New CLI flag `--foo`.** Additive, no removal.\n\n"
|
|
236
|
+
"## Cost analysis\n\nAll cheap.\n"
|
|
237
|
+
)
|
|
238
|
+
wu = _make_wu(file=fdir / "WU-close.md", wu_id="FEAT-2026-9999/G1-CLOSE")
|
|
239
|
+
ok, reason = loop.assert_changelog_entry_for_contract_changes(
|
|
240
|
+
wu, fdir, root, DUMMY_HEAD,
|
|
241
|
+
)
|
|
242
|
+
self.assertFalse(ok, "a missing CHANGELOG.md must still fail close-k")
|
|
243
|
+
return reason
|
|
244
|
+
|
|
245
|
+
def test_message_names_the_remedy_and_the_legal_classes(self):
|
|
246
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
247
|
+
reason = self._reason_for_missing_changelog(Path(tmp))
|
|
248
|
+
self.assertIn("[Unreleased]", reason)
|
|
249
|
+
self.assertIn("specfuse upgrade", reason)
|
|
250
|
+
self.assertIn("#575", reason)
|
|
251
|
+
for cls in ("added", "changed", "fixed", "breaking"):
|
|
252
|
+
self.assertIn(cls, reason, f"message omits legal class {cls!r}")
|
|
253
|
+
|
|
254
|
+
def test_message_says_upgrade_preserves_an_existing_file(self):
|
|
255
|
+
"""Telling an operator to run `upgrade` is only safe if it cannot clobber."""
|
|
256
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
257
|
+
reason = self._reason_for_missing_changelog(Path(tmp))
|
|
258
|
+
self.assertIn("without touching an existing file", reason)
|