specfuse-loop 0.31.1__tar.gz → 0.32.0__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.31.1/specfuse_loop.egg-info → specfuse_loop-0.32.0}/PKG-INFO +1 -1
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/pyproject.toml +1 -1
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/arm_eval.py +2 -1
- specfuse_loop-0.32.0/specfuse/loop/changelog_fragments.py +232 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/closing_requirements.py +32 -2
- specfuse_loop-0.32.0/specfuse/loop/data/VERSION +1 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/docs/lifecycles.md +12 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/rules/close-discipline.md +7 -6
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/ids.py +4 -1
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/layout.py +54 -3
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/lint_closing.py +7 -12
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/lint_roadmap.py +65 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/loop.py +16 -25
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0/specfuse_loop.egg-info}/PKG-INFO +1 -1
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse_loop.egg-info/SOURCES.txt +4 -0
- specfuse_loop-0.32.0/tests/test_blocked_by_entry_anchor.py +163 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_caller_check_ratchet.py +0 -1
- specfuse_loop-0.32.0/tests/test_changelog_fragments.py +420 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_ids_migrate_all.py +21 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_judge_path_registry.py +2 -2
- specfuse_loop-0.32.0/tests/test_repo_changelog_opt_in.py +28 -0
- specfuse_loop-0.31.1/specfuse/loop/data/VERSION +0 -1
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/LICENSE +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/NOTICE +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/README.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/setup.cfg +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/__init__.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/budget.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/diagnose_invoke.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/drafting_answers.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/drafting_invoke.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/drafting_questions.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/driver_command.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/driver_invoke.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/invoke.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/monitoring_read.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/providers/__init__.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/providers/answers.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/providers/bugs.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/providers/feature.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/providers/findings_autofix.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/providers/findings_diagnose.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/providers/triage.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/queue_read.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/repo_detect.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/run.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/severity_backfill.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/state.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/triage_invoke.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/agent/worktree.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/__init__.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/_filelock.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/_miniyaml.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/_wu_sections.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/adopt_feature.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/agent_policy.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/arm_sweep.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/arm_txn.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/bug_lane.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/bug_lane_run.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/bug_lane_state.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/build_provenance.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/caller_check.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/changelog.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/cost.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/criteria_state.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/CHANGELOG.seed.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/LEARNINGS.template.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/docs/concepts/adopting-auto-mode.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/docs/concepts/autonomy-stop-classes.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/docs/concepts/monitoring-runners.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/docs/concepts/monitoring-schema.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/docs/concepts/monitoring-to-fix.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/docs/flows.html +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/docs/getting-started.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/docs/glossary.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/docs/methodology.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/docs/skills.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/gitignore.snippet +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/monitoring-secrets-checklist.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/monitoring.overrides.yml.example +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/monitoring.yml.example +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/roadmap.template.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/rules/correlation-ids.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/rules/design-for-diagnosis.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/rules/human-output.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/rules/never-touch.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/rules/operator-escalation.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/rules/planning-discipline.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/rules/result-contract.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/rules/security-boundaries.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/rules/verification-discipline.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/rules-local/README.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/schemas/driver-event.schema.json +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/schemas/event.schema.json +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/templates/DECISIONS.template.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/templates/GATE.template.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/templates/LEARNINGS-pending.template.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/templates/PLAN.template.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/templates/WU.template.md +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/verification.yml.example +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/data/workflows/specfuse-monitor.yml +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/decisions_format.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/driver_edit.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/escalation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/events_stats.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/gate_commands.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/gate_eval.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/gen_contract.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/gh_backend.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/gh_features.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/heartbeat.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/judge.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/labels.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/learnings_distil.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/learnings_query.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/lint_monitoring.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/lint_plan.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/notify.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/notify_escalation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/notify_sla.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/plan_baseline.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/policy_proposals.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/policy_review.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/prerun.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/prerun_capture.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/promotion.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/rearm_migration.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/red_on_base.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/replay_spin.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/roadmap_archive.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/scaffold.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/triage.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/upgrade_merge_gate.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/loop/validate_event.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/__init__.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/adapters.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/artifact.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/autofix.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/autofix_invoke.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/autofix_run.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/autofix_state.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/cli.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/diagnose_cli.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/diagnosis.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/durations.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/fingerprint.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/issues.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/providers/__init__.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/providers/_azure_auth.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/providers/azure_app_insights.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/providers/azure_service_bus.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/redaction.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse/monitor/schedule.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse_loop.egg-info/dependency_links.txt +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse_loop.egg-info/entry_points.txt +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse_loop.egg-info/requires.txt +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/specfuse_loop.egg-info/top_level.txt +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_abandon_feature_skill.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_abandoned_work_reported.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_abandoned_wu_strands_gate.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_adopt_feature.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_budget.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_bug_selection_dedup.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_default_features_root.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_driver_command.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_driver_invoke.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_driver_restart.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_escalation_on_issue.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_escalation_run_state.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_findings_seam.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_invoke_timeout.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_invoke_usage.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_item_worktree.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_lock.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_policy_key_ownership.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_policy_queue.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_policy_schema.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_policy_triage_dial.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_provider_answers.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_provider_bugs.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_provider_feature.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_provider_findings_autofix.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_provider_findings_diagnose.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_provider_isolation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_provider_triage.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_queue_read.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_relays_driver_output.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_repo_detect.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_run.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_run_progress.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_seam.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_snapshot_refresh.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_agent_state.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_answer_escalation_skill.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_app_insights_adapters.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_app_insights_harvest_window.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_app_insights_query_schema_check.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_app_insights_workspace_schema.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_archive_ref_reconciliation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_arm_eval.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_arm_eval_lint_class.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_arm_eval_terminal.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_arm_eval_wiring.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_arm_gate_edits_uncommitted.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_arm_sweep.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_arm_sweep_gate.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_arm_txn.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_arm_wiring.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_artifact_redaction.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_attempt_note_untracked_files.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_attempt_outcome_contract.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_attempt_outcome_emission.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_attention_nonwriting_guard.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_attention_silence_section.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_attention_skill_structure.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_attribution_halt_claims.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_autoclose_deferral_visibility.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_autoclose_gate_note.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_autoclose_stub_states_what_passed.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_autofix_invoke.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_autofix_live.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_autofix_predicate.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_autofix_run.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_autofix_state.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_autosync.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_autosync_consent.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_autosync_firstrun.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_autosync_no_cwd_leak.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_autosync_plugin.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_azure_credential_resolution.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_backend.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_baseline_escalation_message.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_baseline_persistence.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_baseline_probe.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_baseline_provenance.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_baseline_tree_hash_key.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bash_routing.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bats_suites_gated.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_binary_verdict.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_binding_block_advisory.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_binding_block_allocation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_binding_block_budget.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_blind_run_is_not_drained.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_block_feature_layout.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bookkeeping_commit_crash_run.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bookkeeping_no_verify.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_brief_covers_every_unit_escalation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_ci_pending.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_guardrails.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_label_nonfatal.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_labels_registered.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_min_severity.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_no_checks_pending.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_policy_contract.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_pr_number_carried.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_preflight_and_dial.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_run.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_spend.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_state.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_stopped_outcome_pr_number.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_superseded.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_bug_lane_test_paths.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_build_provenance.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_carried_green_invalidated_by_diff.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_carry_accounting_close_and_judge.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_changed_file_test_selection.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_changelog_append_only.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_changelog_collection.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_changelog_release_wiring.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_changelog_schema.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_ci_conclusion_wait.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_ci_runs_every_gate.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_claude_resolution.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_close_b_pending_learnings.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_close_records_feature_oracle.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_close_wu_self_reference.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_closed_escalation_labels.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_closing_deliverable_guard.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_closing_guard_contracts.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_closing_guard_prediction.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_closing_learnings_destination.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_closing_requirements.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_closing_sessions_on_sonnet.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_coauthor_trailer_allowlist.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_conditional_reflection.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_console_encoding.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_convergent_iteration.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_convergent_wu_lint.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_correlation_id_override.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_correlation_id_two_surfaces.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_cost_lifetime.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_cost_tracking.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_criteria_per_gate_lint.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_criteria_state.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_criteria_worklist.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_decision_citation_lint.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_decision_followups_discharged.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_decision_override_lint.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_decisions_format.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_default_severity_aliases.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_degraded_oracle.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_deliverable_missing_note.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_deliverable_presence_gate.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_derive_agent_policy_review_mode.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_derive_agent_policy_skill.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_derive_monitoring_discovery.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_derive_monitoring_skill_registration.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_design_for_diagnosis_rule.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_deterministic_refusal_repeat.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_diagnose_fenced_analysis.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_diagnose_headless.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_diagnose_issue_skill.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_diagnosis.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_diagnosis_roundtrip_live.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_diagnosis_severity_end_to_end.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_diagnosis_severity_rubric.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_dispatch_model_from_fix_scope.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_dispatch_preamble_rules_pointer.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_dispatch_skeleton.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_dispatch_skills_index.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_distilled_accept_step.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_doctor.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_doctor_command_preflight.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_done_feature_gates.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_draft_feature_answers_mode.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_drafting_answer_gate.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_drafting_invoke.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_drafting_question_issue.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_drafting_questions.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_drafting_reply_shape.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_driver_edit_detection.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_driver_event_registry_covers_emitters.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_driver_integration.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_driver_lock.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_driver_module_surface.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_driver_restart_halt_surfaced.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_driver_restart_halt_wiring.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_driver_restart_hold.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_driver_staleness_gate_summary.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_driver_staleness_warning.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_dry_run_no_branch_mutation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_duration_tracking.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_empty_files_escalation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_ensure_feature_branch.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_ensure_feature_branch_base.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_escalation_assignee_policy.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_escalation_contract.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_escalation_crash_containment.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_escalation_dedup_search.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_escalation_emit.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_event_gate_full_envelope.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_events_redaction.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_events_stats.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_extra_gates.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_failure_artifact_model.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_failure_signature_fail_prefix.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_failure_signature_names_the_test.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_failure_signature_skips_log_noise.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_feature_branch_auto_sync.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_feature_branch_squash_merge.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_feature_branch_stale_base.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_feature_id_resolution.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_feature_oracle_e2e.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_feature_provider_drafting_dispatch.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_feature_review_accumulation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_filelock_portable.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_files_changed_auto_repair.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_find_pr_no_search_index.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_fingerprint.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_fix_bug_cwd_worktree.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_fix_bug_diff_self_check.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_fix_bug_headless.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_fix_bug_reads_comments.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_fix_unit_insertion_bookkeeping.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_fix_unit_insertion_e2e.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_fix_unit_insertion_refused.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_floor_skip_summary.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_flows_page.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_fold_contract_documented.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_followups_artifact.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_force_full_close.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_frontmatter_scalar_quoting.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gate_broad_run.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gate_budget.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gate_budget_post_dispatch.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gate_commands_needs.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gate_eval.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gate_eval_calibration.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gate_eval_intermediate_wiring.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gate_eval_repo_flag.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gate_eval_terminal_wiring.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gate_log_size_budget.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gate_report_verdict.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gen_contract.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gh_backend.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gh_features.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_git_env_isolation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_gitleaks_pinning.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_groom_backlog_skill.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_guard_refusal_failure_class.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_guard_refusal_failure_excerpt.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_guard_repair_e2e.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_guard_repair_prompt.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_hashed_denylist.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_hashed_denylist_ci.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_headless_no_background_tasks.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_heartbeat.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_heartbeat_adapter.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_heartbeat_emission_contract.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_http_status_regression_adapter.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_http_status_regression_end_to_end.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_human_output_rule.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_human_work_unit.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_hygiene_boundary_guidance.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_hyphenated_gate_name.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_ids.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_ids_aliases.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_ids_call_sites.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_ids_migrate.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_ignored_cleanup_before_probe.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_init_integration.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_installed_copy_driver_e2e.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_interpreter_normalization.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_judge_close_path.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_judge_cost.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_judge_cost_fold_committed.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_judge_editing_terminal_docs.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_judge_module.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_judge_sees_dropped_produces.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_judge_terminal_flip_state.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_judged_event_points_at_findings.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_keyed_feature_ids_docs.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_keyed_feature_ids_end_to_end.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_label_description_length.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_label_provisioning_runner_contract.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_label_registry.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_label_registry_covers_consumers.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_language_aware_narrow_selection.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_layout.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_layout_end_to_end.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_layout_link.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_layout_no_layout_goldens.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_layout_validation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lazy_baseline_e2e.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_leak_findings_redaction.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_leak_scan.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_leak_scan_content.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_learnings_curate_vendored_promote.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_learnings_distil_cli.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_learnings_query.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_learnings_staging.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_learnings_suggest_signature_filter.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_learnings_weights.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_legacy_4wu_terminal_flips.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lifecycle_integration.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lifetime_accounting.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_ac_observable.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_bare_produces_path.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_boundary_consistency.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_boundary_extraction.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_close_intermediate.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_close_verdict_not_predecided.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_close_wu.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_closing.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_closing_criteria.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_closing_criteria_pristine.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_correlation_id.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_covers_load_wu_invariants.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_feature_oracle_declared.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_gate_proportionality.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_judge_edit_acknowledged.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_monitoring.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_oracle_env.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_plan_contract_fields.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_plan_errors.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_plan_next_draft.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_plan_relative_import.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_plan_verdict_exempt.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_planning_sections.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_produces_driver_helper.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_produces_satisfiability.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_roadmap.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_roadmap_unkeyed_rows.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_sections.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_task_graph_yaml_selection.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_lint_verification_reference.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_live_tests_opt_in.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_load_bearing_close_lint.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_attempt_evidence.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_auto_archive.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_caveman_preamble.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_close_intermediate.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_criteria_skeleton.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_criteria_survival.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_defaults_by_type.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_effort.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_failure_note_cap.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_files_changed_guard.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_gate_budget.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_gate_needs.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_model_alias.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_orchestration.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_post_pass_invariant.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_reset_preserving_events.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_smoke_runner.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_unsandboxed.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_worklist_injection.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_loop_zero_token_guard.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_make_repo_gpgsign_isolation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_marker_dual_shape.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_maven_failure_excerpt.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_max_attempts_configurable.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_max_open_prs.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_migrate_all_repo_stays_valid.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_migrate_all_scales.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_migrate_legacy.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_miniyaml_equivalence.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_miniyaml_negative.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitor_check_isolation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitor_cli.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitor_diagnose_auto_warning.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitor_durations.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitor_finding_context_end_to_end.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitor_finding_lifecycle_end_to_end.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitor_finding_meta.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitor_issue_lifecycle.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitor_one_issue_per_fingerprint.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitor_quiet_annotation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitor_runner_surfaces.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitoring_bootstrap_artifacts.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitoring_cron_dialect.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitoring_example.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitoring_fenced_blocks.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_monitoring_seed.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_narrow_fallback_warning.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_narrow_non_py_produces.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_needs_decision_outcome.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_no_pytest_subprocess.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_notify.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_notify_escalation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_notify_sla.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_operator_escalation_rule.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_oracle_set_declared.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_oracle_unrunnable_attribution.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_per_run_item_cap_disclosure.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_pick_feature_skill.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_pin_honesty_and_integrity.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_pin_marker_not_inherited.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_pin_marker_spawn_sites.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_plan_baseline.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_planned_cost_lint.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_planning_cost_floor.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_plugin_prose_cross_refs.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_plugin_qualified_dispatch.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_policy_budgets_applied.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_policy_proposals.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_policy_review.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_pr_checks_json_field.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_pr_reference_in_title.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_pre_existing_claims_cite_evidence.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_prep_halt_operator_surface.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_prepare_feature.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_prepare_scaffold_sync.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_prerun_capture.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_prerun_oracles.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_prerun_wiring.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_produced_document_verdict_guard.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_produces_amendment_e2e.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_produces_bracket_literal.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_produces_bracket_wildcard.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_produces_field.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_produces_in_diff.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_produces_justification.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_produces_recursive_glob.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_produces_repair_note.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_produces_semantics_unified.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_produces_shape.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_progress_forward_note.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_progress_lines_end_to_end.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_promoted_issue_not_redispatched.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_promotion_ledger.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_provider_escalation_traces.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_provision_labels.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_queue_stalled_adapter.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_rearm_clears_stale_criteria_state.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_rearm_close_broad_run.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_rearm_fold_marker.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_rearm_migration.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_rearm_retained_new_files.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_reclose_carries_narrow_greens_e2e.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_red_baseline_never_reused.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_red_on_base_fetch_and_reason.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_red_on_base_guardrail.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_red_on_base_overlay.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_red_on_base_probe.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_red_on_base_real_runner.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_redaction_windows_home.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_rediagnose_on_reopen.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_reescalation_posts_comment.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_reflection_required_signals.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_replan_end_to_end.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_replan_event_emission.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_replan_note_collision.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_replan_trigger.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_replan_turn_contract.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_replay_spin.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_replay_spin_failing_sets.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_required_checks.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_resolve_base.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_restart_cap_counts_progress.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_result_block.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_result_block_audience.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_result_block_near_miss.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_result_block_scalar.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_resume_command_build.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_retain_on_guard_refusal_docs.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_roadmap_add_skill.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_roadmap_archive_cli.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_roadmap_archive_shim.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_roadmap_archive_skill.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_roadmap_archive_skill_delegates.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_roadmap_link_gate.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_roadmap_row_parser.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_ruff_verdict_lines.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_rule_import_convergence.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_run_level_failure_breaker.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_scaffold_data_in_sync.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_scaffold_doc_hygiene.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_scaffold_docs.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_scaffold_init.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_scaffold_label_provisioning.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_scaffold_manifest.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_scaffold_resources.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_scaffold_seed_sanity.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_scaffold_skew_halts.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_scaffold_upgrade.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_scaffold_wiring.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_schedule_dialect.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_seed_documents_prerun_keys.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_service_bus_dlq_adapter.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_session_env_preflight.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_severity_aliases.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_severity_backfill_apply.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_severity_backfill_bugs_only.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_severity_backfill_end_to_end.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_severity_backfill_limit.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_severity_backfill_marker.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_severity_backfill_run.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_severity_label_projection.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_severity_reconciliation.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_severity_rubric.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_shipped_templates_parse.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_skill_discovery_links.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_skills_vendored_in_sync.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_spec_work_unit_rules.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_spend_counts_cache_tokens.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_spinning_rearm_gate.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_spinning_repeat_same_failing_set.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_spinout_brief_end_to_end.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_spinout_brief_replan_option.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_squash_commit_hook_crash.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_stop_rationale_captured.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_stranded_halt_cause.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_surefire_run_level_signature.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_surefire_verdict_lines.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_template_closing_shapes.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_terminal_auto_close_changelog.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_terminal_flip_ownership.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_terminal_flips.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_terminal_gate_message.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_terminal_judge_editing_end_to_end.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_tiered_verification_e2e.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_timeout_kill_portable.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_triage.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_triage_apply.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_triage_projects_severity_label.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_triage_reads_comments.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_triage_severity_classify.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_triage_severity_end_to_end.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_triage_severity_label_conflict.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_triage_severity_write.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_triage_skill_contract.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_triage_skips_agent_escalations.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_tsc_verdict_lines.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_umbrella_version_resolution.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_unreadable_pr_listing_holds_pr_work.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_unterminated_frontmatter.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_untracked_feature_folder.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_untracked_file_absorption.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_upgrade_gitignore_lines.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_upgrade_integration.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_upgrade_merge_gate.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_validate_event.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_validate_event_driver_types.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_validate_feature_dirs_only.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_verdict_coupling.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_verify_empty_gate_set.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_version_consistency.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_version_skew.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_windows_gate_exec.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_worktree_restore.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_wu_execution_metadata.py +0 -0
- {specfuse_loop-0.31.1 → specfuse_loop-0.32.0}/tests/test_wu_template_renders_lintable.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.
|
|
7
|
+
version = "0.32.0"
|
|
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"
|
|
@@ -110,6 +110,8 @@ JUDGE_MODULES = tuple(
|
|
|
110
110
|
"closing_requirements.py",
|
|
111
111
|
"criteria_state.py",
|
|
112
112
|
"lint_closing.py",
|
|
113
|
+
"changelog.py", # its parser is how close-k, lint --closing and the auto-close guard read Unreleased
|
|
114
|
+
"changelog_fragments.py", # the one reader/writer of changelog entries the close guards read
|
|
113
115
|
"lint_plan.py",
|
|
114
116
|
"ids.py", # supplies lint_plan's CORRELATION_ID_RE; an edit changes what the close's lint accepts
|
|
115
117
|
"plan_baseline.py", # the baseline retroactive_edits compares to
|
|
@@ -144,7 +146,6 @@ NON_JUDGE_MODULES = {
|
|
|
144
146
|
"caller_check.py": "reports public symbols whose only callers are "
|
|
145
147
|
"tests; a hygiene report, grants no verdict and no arm/close/"
|
|
146
148
|
"merge decision reads it",
|
|
147
|
-
"changelog.py": "parses and stamps CHANGELOG.md; no gate reads it",
|
|
148
149
|
"decisions_format.py": "parses DECISIONS.md into entries; not on the "
|
|
149
150
|
"arm/close/merge judge path — T02/T03 own the lint that reads it",
|
|
150
151
|
"driver_edit.py": "applies operator edits to a feature folder",
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Specfuse contributors
|
|
3
|
+
# Licensed under the Apache License, Version 2.0. See LICENSE.
|
|
4
|
+
#
|
|
5
|
+
"""Changelog fragments — FEAT-LOOP-2026-0249.
|
|
6
|
+
|
|
7
|
+
One writer for changelog entries. `add` routes by `changelog.mode` in
|
|
8
|
+
`.specfuse/layout.yml`: `fragments` writes `changelog.d/<trace>-<slug>.md`
|
|
9
|
+
(so concurrent PRs never touch the same file), `single` (the default) appends
|
|
10
|
+
to `CHANGELOG.md` through `changelog.append_entry`.
|
|
11
|
+
|
|
12
|
+
python3 -m specfuse.loop.changelog_fragments add --class C --trace T --summary S [--repo DIR]
|
|
13
|
+
python3 -m specfuse.loop.changelog_fragments collate [--repo DIR]
|
|
14
|
+
python3 -m specfuse.loop.changelog_fragments check [--repo DIR]
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from specfuse.loop import layout
|
|
25
|
+
from specfuse.loop.changelog import (
|
|
26
|
+
_TRACE_RE,
|
|
27
|
+
ENTRY_CLASSES,
|
|
28
|
+
ChangelogEntry,
|
|
29
|
+
append_entry,
|
|
30
|
+
parse_changelog,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
FRAGMENT_DIR = "changelog.d"
|
|
34
|
+
CHANGELOG_FILE = "CHANGELOG.md"
|
|
35
|
+
_SLUG_MAX = 40
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def normalize_trace(trace: str) -> str:
|
|
39
|
+
"""`#3563` -> `3563`, `FEAT-X/T01` -> `FEAT-X-T01`."""
|
|
40
|
+
return trace.lstrip("#").replace("/", "-")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def slugify(summary: str) -> str:
|
|
44
|
+
slug = re.sub(r"[^a-z0-9]+", "-", summary.lower()).strip("-")[:_SLUG_MAX].strip("-")
|
|
45
|
+
return slug or "entry"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _validate(entry_class: str, trace: str, summary: str) -> None:
|
|
49
|
+
if entry_class not in ENTRY_CLASSES:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
f"unrecognised entry class {entry_class!r} — must be one of "
|
|
52
|
+
f"{', '.join(ENTRY_CLASSES)}"
|
|
53
|
+
)
|
|
54
|
+
if not _TRACE_RE.match(trace):
|
|
55
|
+
raise ValueError(f"trace {trace!r} is not a FEAT-YYYY-NNNN or #<issue-number> trace")
|
|
56
|
+
if not summary.strip():
|
|
57
|
+
raise ValueError("summary must not be empty")
|
|
58
|
+
if "\n" in summary or "\r" in summary:
|
|
59
|
+
raise ValueError("summary must be a single line")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def read_fragment(path) -> ChangelogEntry | None:
|
|
63
|
+
"""The entry one fragment file carries, or None when it is not a valid fragment.
|
|
64
|
+
|
|
65
|
+
Frontmatter is read line by line, not through `_miniyaml`: `trace: #101`
|
|
66
|
+
is a comment to a YAML reader. Never raises on a malformed file.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
lines = Path(path).read_text(encoding="utf-8").splitlines()
|
|
70
|
+
except (OSError, UnicodeDecodeError):
|
|
71
|
+
return None
|
|
72
|
+
if not lines or lines[0].strip() != "---":
|
|
73
|
+
return None
|
|
74
|
+
try:
|
|
75
|
+
end = next(i for i in range(1, len(lines)) if lines[i].strip() == "---")
|
|
76
|
+
except StopIteration:
|
|
77
|
+
return None
|
|
78
|
+
fields: dict[str, str] = {}
|
|
79
|
+
for line in lines[1:end]:
|
|
80
|
+
key, sep, value = line.partition(":")
|
|
81
|
+
if not sep:
|
|
82
|
+
return None
|
|
83
|
+
fields[key.strip()] = value.strip()
|
|
84
|
+
entry_class, trace = fields.get("class", ""), fields.get("trace", "")
|
|
85
|
+
body = [ln.strip() for ln in lines[end + 1:] if ln.strip()]
|
|
86
|
+
if entry_class not in ENTRY_CLASSES or not _TRACE_RE.match(trace) or len(body) != 1:
|
|
87
|
+
return None
|
|
88
|
+
return ChangelogEntry(entry_class=entry_class, summary=body[0], trace=trace, line=end + 2)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def fragment_entries(repo_root) -> list:
|
|
92
|
+
"""Every valid fragment's entry under `changelog.d/`; malformed files are skipped."""
|
|
93
|
+
directory = Path(repo_root) / FRAGMENT_DIR
|
|
94
|
+
if not directory.is_dir():
|
|
95
|
+
return []
|
|
96
|
+
found = (read_fragment(p) for p in sorted(directory.glob("*.md")))
|
|
97
|
+
return [entry for entry in found if entry is not None]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def add(repo_root, *, entry_class: str, trace: str, summary: str) -> Path:
|
|
101
|
+
"""Write one entry in the project's mode; return the file written."""
|
|
102
|
+
_validate(entry_class, trace, summary)
|
|
103
|
+
summary = summary.strip()
|
|
104
|
+
root = Path(repo_root)
|
|
105
|
+
if layout.load(root).changelog.mode == "fragments":
|
|
106
|
+
target = root / FRAGMENT_DIR / f"{normalize_trace(trace)}-{slugify(summary)}.md"
|
|
107
|
+
if target.exists():
|
|
108
|
+
raise ValueError(f"{target.relative_to(root).as_posix()} already exists")
|
|
109
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
target.write_text(
|
|
111
|
+
f"---\nclass: {entry_class}\ntrace: {trace}\n---\n{summary}\n", encoding="utf-8"
|
|
112
|
+
)
|
|
113
|
+
return target
|
|
114
|
+
path = root / CHANGELOG_FILE
|
|
115
|
+
if not path.is_file():
|
|
116
|
+
raise ValueError(f"{CHANGELOG_FILE} not found in {root}")
|
|
117
|
+
text = path.read_text(encoding="utf-8")
|
|
118
|
+
path.write_text(
|
|
119
|
+
append_entry(text, entry_class=entry_class, summary=summary, trace=trace),
|
|
120
|
+
encoding="utf-8",
|
|
121
|
+
)
|
|
122
|
+
return path
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def invalid_fragments(repo_root) -> list:
|
|
126
|
+
"""One message per malformed fragment under `changelog.d/`, each naming the file."""
|
|
127
|
+
directory = Path(repo_root) / FRAGMENT_DIR
|
|
128
|
+
paths = sorted(directory.glob("*.md")) if directory.is_dir() else []
|
|
129
|
+
return [
|
|
130
|
+
f"{FRAGMENT_DIR}/{path.name} is not a valid fragment — expected "
|
|
131
|
+
"`class:` and `trace:` frontmatter and a single-line summary"
|
|
132
|
+
for path in paths
|
|
133
|
+
if read_fragment(path) is None
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def collate_text(repo_root, text: str) -> tuple:
|
|
138
|
+
"""Fold every fragment into `text`'s `Unreleased`; return `(new_text, fragment_paths)`.
|
|
139
|
+
|
|
140
|
+
Fragments fold in sorted filename order through `append_entry`. Writes and
|
|
141
|
+
deletes nothing: any malformed fragment, or a result that does not parse
|
|
142
|
+
clean, raises `ValueError` naming the file, so the caller's tree is untouched.
|
|
143
|
+
"""
|
|
144
|
+
directory = Path(repo_root) / FRAGMENT_DIR
|
|
145
|
+
paths = sorted(directory.glob("*.md")) if directory.is_dir() else []
|
|
146
|
+
problems = invalid_fragments(repo_root)
|
|
147
|
+
if problems:
|
|
148
|
+
raise ValueError(problems[0])
|
|
149
|
+
for path in paths:
|
|
150
|
+
entry = read_fragment(path)
|
|
151
|
+
text = append_entry(
|
|
152
|
+
text, entry_class=entry.entry_class, summary=entry.summary, trace=entry.trace
|
|
153
|
+
)
|
|
154
|
+
if paths:
|
|
155
|
+
result = parse_changelog(text)
|
|
156
|
+
if not result.ok:
|
|
157
|
+
raise ValueError(
|
|
158
|
+
f"collating {FRAGMENT_DIR}/ leaves {CHANGELOG_FILE} unparseable: "
|
|
159
|
+
+ "; ".join(str(f) for f in result.findings)
|
|
160
|
+
)
|
|
161
|
+
return text, paths
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def remove_fragments(paths) -> None:
|
|
165
|
+
"""Delete consumed fragments; drop `changelog.d/` when it is left empty."""
|
|
166
|
+
parents = {Path(p).parent for p in paths}
|
|
167
|
+
for path in paths:
|
|
168
|
+
Path(path).unlink()
|
|
169
|
+
for parent in parents:
|
|
170
|
+
try:
|
|
171
|
+
parent.rmdir()
|
|
172
|
+
except OSError:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def collate(repo_root) -> list:
|
|
177
|
+
"""Fold fragments into `CHANGELOG.md` and delete them; return the fragment paths."""
|
|
178
|
+
path = Path(repo_root) / CHANGELOG_FILE
|
|
179
|
+
if not path.is_file():
|
|
180
|
+
raise ValueError(f"{CHANGELOG_FILE} not found in {repo_root}")
|
|
181
|
+
new_text, paths = collate_text(repo_root, path.read_text(encoding="utf-8"))
|
|
182
|
+
if paths:
|
|
183
|
+
path.write_text(new_text, encoding="utf-8")
|
|
184
|
+
remove_fragments(paths)
|
|
185
|
+
return paths
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def main(argv=None) -> int:
|
|
189
|
+
parser = argparse.ArgumentParser(
|
|
190
|
+
prog="specfuse.loop.changelog_fragments", description="Write changelog entries."
|
|
191
|
+
)
|
|
192
|
+
parser.add_argument("--repo", default=".", help="repository root (default: cwd)")
|
|
193
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
194
|
+
addp = sub.add_parser("add", help="write one changelog entry in the project's mode")
|
|
195
|
+
addp.add_argument("--class", dest="entry_class", required=True)
|
|
196
|
+
addp.add_argument("--trace", required=True)
|
|
197
|
+
addp.add_argument("--summary", required=True)
|
|
198
|
+
sub.add_parser("collate", help="fold changelog.d/ fragments into CHANGELOG.md and delete them")
|
|
199
|
+
sub.add_parser("check", help="fail when any changelog.d/ fragment is malformed")
|
|
200
|
+
args = parser.parse_args(argv)
|
|
201
|
+
|
|
202
|
+
if args.command == "check":
|
|
203
|
+
problems = invalid_fragments(args.repo)
|
|
204
|
+
for problem in problems:
|
|
205
|
+
print(problem, file=sys.stderr)
|
|
206
|
+
return 2 if problems else 0
|
|
207
|
+
|
|
208
|
+
if args.command == "collate":
|
|
209
|
+
try:
|
|
210
|
+
folded = collate(args.repo)
|
|
211
|
+
except ValueError as exc:
|
|
212
|
+
print(str(exc), file=sys.stderr)
|
|
213
|
+
return 2
|
|
214
|
+
print(f"collated {len(folded)} fragment(s)")
|
|
215
|
+
return 0
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
target = add(
|
|
219
|
+
args.repo,
|
|
220
|
+
entry_class=args.entry_class,
|
|
221
|
+
trace=args.trace,
|
|
222
|
+
summary=args.summary,
|
|
223
|
+
)
|
|
224
|
+
except ValueError as exc:
|
|
225
|
+
print(str(exc), file=sys.stderr)
|
|
226
|
+
return 2
|
|
227
|
+
print(target)
|
|
228
|
+
return 0
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
if __name__ == "__main__":
|
|
232
|
+
sys.exit(main())
|
|
@@ -18,6 +18,7 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
import re
|
|
20
20
|
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
21
22
|
from typing import Optional
|
|
22
23
|
|
|
23
24
|
# --------------------------------------------------------------------------- #
|
|
@@ -232,6 +233,35 @@ def changelog_has_entry_for(unreleased_entries, feature_id: str) -> bool:
|
|
|
232
233
|
)
|
|
233
234
|
|
|
234
235
|
|
|
236
|
+
def changelog_exists(repo_root) -> bool:
|
|
237
|
+
"""True if the project has a changelog in either form: `CHANGELOG.md` or `changelog.d/*.md`."""
|
|
238
|
+
root = Path(repo_root)
|
|
239
|
+
return (root / CHANGELOG_PATH).exists() or any((root / "changelog.d").glob("*.md"))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def changelog_has_entry_for_feature(repo_root, feature_id: str) -> bool:
|
|
243
|
+
"""True if *feature_id* has an entry in `CHANGELOG.md`'s Unreleased section or a valid fragment.
|
|
244
|
+
|
|
245
|
+
The one reader behind close-k, `lint --closing` and the auto-close guard
|
|
246
|
+
(FEAT-LOOP-2026-0249). Both forms are read in every mode. A missing or
|
|
247
|
+
unparseable changelog and a malformed fragment count as no entry; this
|
|
248
|
+
never raises.
|
|
249
|
+
"""
|
|
250
|
+
from . import changelog_fragments
|
|
251
|
+
from .changelog import parse_changelog
|
|
252
|
+
|
|
253
|
+
root = Path(repo_root)
|
|
254
|
+
entries = list(changelog_fragments.fragment_entries(root))
|
|
255
|
+
changelog = root / CHANGELOG_PATH
|
|
256
|
+
if changelog.is_file():
|
|
257
|
+
try:
|
|
258
|
+
unreleased = parse_changelog(changelog.read_text(encoding="utf-8")).unreleased()
|
|
259
|
+
except Exception: # noqa: BLE001 - a guard reports a gap, never crashes on one
|
|
260
|
+
unreleased = None
|
|
261
|
+
entries += unreleased.entries if unreleased else []
|
|
262
|
+
return changelog_has_entry_for(entries, feature_id)
|
|
263
|
+
|
|
264
|
+
|
|
235
265
|
# --------------------------------------------------------------------------- #
|
|
236
266
|
# Requirement records #
|
|
237
267
|
# --------------------------------------------------------------------------- #
|
|
@@ -354,8 +384,8 @@ CLOSING_REQUIREMENTS: dict[str, list[Requirement]] = {
|
|
|
354
384
|
description=(
|
|
355
385
|
f"When the '{CONSUMER_VISIBLE_HEADING}' section (close-discipline.md "
|
|
356
386
|
f"§3) is not the '{CONSUMER_VISIBLE_NA_PHRASE}' line, "
|
|
357
|
-
f"{CHANGELOG_PATH}'s Unreleased section
|
|
358
|
-
"this feature's FEAT-ID — the same enumeration §3 already "
|
|
387
|
+
f"{CHANGELOG_PATH}'s Unreleased section or a valid changelog.d/ "
|
|
388
|
+
"fragment gains an entry carrying this feature's FEAT-ID — the same enumeration §3 already "
|
|
359
389
|
"requires, appended where a consumer reads it, not re-derived"
|
|
360
390
|
),
|
|
361
391
|
file=RETROSPECTIVE_FILENAME,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.32.0
|
|
@@ -175,6 +175,18 @@ hit stops the branch rather than shipping feature-scoped work as a bug fix.
|
|
|
175
175
|
**The `CHANGELOG.md` entry is written here or nowhere.** Bugs have no close
|
|
176
176
|
ceremony to collect through, so `/fix-bug` is the collection point.
|
|
177
177
|
|
|
178
|
+
`/fix-bug` records the entry with `python3 -m specfuse.loop.changelog_fragments add`
|
|
179
|
+
(trace `#<issue-number>`) and commits what it wrote with the fix.
|
|
180
|
+
|
|
181
|
+
**Opting in to fragments.** By default entries append to `CHANGELOG.md`. To
|
|
182
|
+
stop concurrent PRs colliding on that file, set `changelog.mode: fragments` in
|
|
183
|
+
`.specfuse/layout.yml`; `add` then writes one `changelog.d/<trace>-<slug>.md`
|
|
184
|
+
per entry. Add `python3 -m specfuse.loop.changelog_fragments check` as a gate
|
|
185
|
+
in `.specfuse/verification.yml` to fail on malformed fragments. A custom
|
|
186
|
+
release script must run `python3 -m specfuse.loop.changelog_fragments collate`
|
|
187
|
+
before stamping the release, so fragments fold into `CHANGELOG.md` and are
|
|
188
|
+
deleted first.
|
|
189
|
+
|
|
178
190
|
---
|
|
179
191
|
|
|
180
192
|
## 3. The autonomous lifecycle
|
|
@@ -138,18 +138,19 @@ A close carrying this obligation is always load-bearing
|
|
|
138
138
|
(`auto_close_disabled: true`).
|
|
139
139
|
|
|
140
140
|
**Also append it to `CHANGELOG.md`'s `Unreleased` section (FEAT-2026-0064).**
|
|
141
|
-
When this section is a real enumeration — not the `n/a` line —
|
|
142
|
-
|
|
143
|
-
`
|
|
144
|
-
|
|
141
|
+
When this section is a real enumeration — not the `n/a` line — run one
|
|
142
|
+
`python3 -m specfuse.loop.changelog_fragments add --class <class> --trace <FEAT-ID> --summary '<line>'`
|
|
143
|
+
per enumerated item, classified `added` / `changed` / `fixed` / `breaking`,
|
|
144
|
+
with the feature's `FEAT-YYYY-NNNN` ID as trace. Never hand-edit; `add`
|
|
145
|
+
routes by the project's `changelog.mode`. **This is the same material the
|
|
145
146
|
paragraph above already requires you to write, put where a consumer will
|
|
146
147
|
read it — not a second write.** An author who treats the changelog append as
|
|
147
148
|
new work will write the enumeration twice, badly; write it once, in both
|
|
148
149
|
places, from the same understanding. `closing_requirements.py`'s `close-k`
|
|
149
150
|
requirement (enforced by `assert_changelog_entry_for_contract_changes` in
|
|
150
151
|
`loop.py`, and pre-squash by `specfuse lint --closing`) fires when this
|
|
151
|
-
section names a real change and
|
|
152
|
-
|
|
152
|
+
section names a real change and no entry tracing to this feature's FEAT-ID
|
|
153
|
+
lives in `CHANGELOG.md` or a `changelog.d/` fragment.
|
|
153
154
|
|
|
154
155
|
**An `n/a` close appends nothing to `Unreleased`, and that is correct.** A
|
|
155
156
|
changelog padded with "no user-facing change" entries is noise that trains
|
|
@@ -399,7 +399,10 @@ def _rewrite_file(rel, text, renames, include_prose, plan, folders=()):
|
|
|
399
399
|
is_changelog = rel == "CHANGELOG.md"
|
|
400
400
|
lines = text.splitlines(keepends=True)
|
|
401
401
|
bare = [ln.rstrip("\r\n") for ln in lines]
|
|
402
|
-
|
|
402
|
+
# A changelog.d/ fragment is unreleased by definition: its `trace:` frontmatter
|
|
403
|
+
# is structural, so it follows the rename (the filename stays; frontmatter wins).
|
|
404
|
+
is_fragment = is_md and rel.startswith("changelog.d/")
|
|
405
|
+
fm_end = _frontmatter_end(bare) if is_md and (in_features or is_fragment) else -1
|
|
403
406
|
graph = _task_graph_lines(bare) if name == "PLAN.md" and in_features else set()
|
|
404
407
|
span = _unreleased_span(bare) if is_changelog else None
|
|
405
408
|
if is_changelog and span is None:
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
"""Project layout resolver — FEAT-2026-0224 (project-configurable layout).
|
|
7
7
|
|
|
8
8
|
Reads an optional `.specfuse/layout.yml` and answers `layout get <key>` for
|
|
9
|
-
`decisions.style`, `decisions.location`, `decisions.link
|
|
9
|
+
`decisions.style`, `decisions.location`, `decisions.link`, `decisions.entryKey`
|
|
10
|
+
and `ids.key`.
|
|
10
11
|
|
|
11
12
|
Contract: no file (or no `decisions:` block) means today's behaviour — style
|
|
12
13
|
`adr`, location `docs/adr`, link `{path}`. An `entries` style with `location`
|
|
@@ -41,12 +42,23 @@ _STYLE_DEFAULTS = {
|
|
|
41
42
|
}
|
|
42
43
|
_FALLBACK_DEFAULTS = _STYLE_DEFAULTS["adr"]
|
|
43
44
|
|
|
44
|
-
KEYS = (
|
|
45
|
+
KEYS = (
|
|
46
|
+
"decisions.style",
|
|
47
|
+
"decisions.location",
|
|
48
|
+
"decisions.link",
|
|
49
|
+
"decisions.entryKey",
|
|
50
|
+
"ids.key",
|
|
51
|
+
"changelog.mode",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
DEFAULT_CHANGELOG_MODE = "single"
|
|
55
|
+
VALID_CHANGELOG_MODES = ("single", "fragments")
|
|
45
56
|
|
|
46
57
|
VALID_STYLES = ("adr", "entries")
|
|
47
58
|
LINK_PLACEHOLDERS = ("location", "path", "id", "domain")
|
|
48
|
-
KNOWN_TOP_LEVEL = ("decisions", "ids")
|
|
59
|
+
KNOWN_TOP_LEVEL = ("decisions", "ids", "changelog")
|
|
49
60
|
_PLACEHOLDER_RE = re.compile(r"\{([^{}]*)\}")
|
|
61
|
+
_ENTRY_KEY_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_-]*$")
|
|
50
62
|
_DRIVE_RE = re.compile(r"^[A-Za-z]:")
|
|
51
63
|
|
|
52
64
|
|
|
@@ -55,6 +67,7 @@ class Decisions:
|
|
|
55
67
|
style: str
|
|
56
68
|
location: str
|
|
57
69
|
link: str
|
|
70
|
+
entry_key: Optional[str] = None
|
|
58
71
|
|
|
59
72
|
|
|
60
73
|
@dataclass(frozen=True)
|
|
@@ -62,10 +75,16 @@ class Ids:
|
|
|
62
75
|
key: Optional[str] = None
|
|
63
76
|
|
|
64
77
|
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class Changelog:
|
|
80
|
+
mode: str = DEFAULT_CHANGELOG_MODE
|
|
81
|
+
|
|
82
|
+
|
|
65
83
|
@dataclass(frozen=True)
|
|
66
84
|
class Layout:
|
|
67
85
|
decisions: Decisions
|
|
68
86
|
ids: Ids = Ids()
|
|
87
|
+
changelog: Changelog = Changelog()
|
|
69
88
|
|
|
70
89
|
|
|
71
90
|
def _normalise(location: str) -> str:
|
|
@@ -81,10 +100,12 @@ def _resolve_decisions(block: Optional[dict]) -> Decisions:
|
|
|
81
100
|
default_location, default_link = _STYLE_DEFAULTS.get(style, _FALLBACK_DEFAULTS)
|
|
82
101
|
location = block.get("location")
|
|
83
102
|
link = block.get("link")
|
|
103
|
+
entry_key = block.get("entryKey")
|
|
84
104
|
return Decisions(
|
|
85
105
|
style=style,
|
|
86
106
|
location=default_location if location is None else _normalise(str(location)),
|
|
87
107
|
link=default_link if link is None else str(link),
|
|
108
|
+
entry_key=None if entry_key is None else str(entry_key),
|
|
88
109
|
)
|
|
89
110
|
|
|
90
111
|
|
|
@@ -104,9 +125,12 @@ def load(repo_root) -> Layout:
|
|
|
104
125
|
block = data.get("decisions") if isinstance(data, dict) else None
|
|
105
126
|
ids_block = data.get("ids") if isinstance(data, dict) else None
|
|
106
127
|
key = ids_block.get("key") if isinstance(ids_block, dict) else None
|
|
128
|
+
cl_block = data.get("changelog") if isinstance(data, dict) else None
|
|
129
|
+
mode = cl_block.get("mode") if isinstance(cl_block, dict) else None
|
|
107
130
|
return Layout(
|
|
108
131
|
decisions=_resolve_decisions(block if isinstance(block, dict) else None),
|
|
109
132
|
ids=Ids(key=None if key is None else str(key)),
|
|
133
|
+
changelog=Changelog(mode=DEFAULT_CHANGELOG_MODE if mode is None else str(mode)),
|
|
110
134
|
)
|
|
111
135
|
|
|
112
136
|
|
|
@@ -168,6 +192,17 @@ def validate(repo_root) -> list:
|
|
|
168
192
|
"(2-6 characters, upper-case letters and digits, starting with a letter)",
|
|
169
193
|
)
|
|
170
194
|
|
|
195
|
+
cl_block = data.get("changelog")
|
|
196
|
+
if isinstance(cl_block, dict) and cl_block.get("mode") is not None:
|
|
197
|
+
mode = str(cl_block["mode"])
|
|
198
|
+
if mode not in VALID_CHANGELOG_MODES:
|
|
199
|
+
add(
|
|
200
|
+
SEVERITY_ERROR,
|
|
201
|
+
"mode",
|
|
202
|
+
f"changelog.mode {mode!r} is invalid; must be one of "
|
|
203
|
+
f"{VALID_CHANGELOG_MODES[0]!r} or {VALID_CHANGELOG_MODES[1]!r}",
|
|
204
|
+
)
|
|
205
|
+
|
|
171
206
|
block = data.get("decisions")
|
|
172
207
|
if not isinstance(block, dict):
|
|
173
208
|
return findings
|
|
@@ -227,6 +262,18 @@ def validate(repo_root) -> list:
|
|
|
227
262
|
f"decisions.link placeholder '{{{name}}}' is not allowed; "
|
|
228
263
|
"use {location}, {path}, {id} or {domain}",
|
|
229
264
|
)
|
|
265
|
+
|
|
266
|
+
entry_key = block.get("entryKey")
|
|
267
|
+
if entry_key is not None:
|
|
268
|
+
if not _ENTRY_KEY_RE.match(str(entry_key)):
|
|
269
|
+
add(
|
|
270
|
+
SEVERITY_ERROR,
|
|
271
|
+
"entryKey",
|
|
272
|
+
f"decisions.entryKey {str(entry_key)!r} is invalid; must match "
|
|
273
|
+
f"{_ENTRY_KEY_RE.pattern}",
|
|
274
|
+
)
|
|
275
|
+
if style is None or str(style) != "entries":
|
|
276
|
+
add(SEVERITY_ERROR, "entryKey", "decisions.entryKey applies only to style 'entries'")
|
|
230
277
|
return findings
|
|
231
278
|
|
|
232
279
|
|
|
@@ -239,6 +286,10 @@ def get(layout: Layout, key: str) -> str:
|
|
|
239
286
|
raise KeyError(key)
|
|
240
287
|
if key == "ids.key":
|
|
241
288
|
return layout.ids.key or ""
|
|
289
|
+
if key == "changelog.mode":
|
|
290
|
+
return layout.changelog.mode
|
|
291
|
+
if key == "decisions.entryKey":
|
|
292
|
+
return layout.decisions.entry_key or ""
|
|
242
293
|
return getattr(layout.decisions, key.split(".", 1)[1])
|
|
243
294
|
|
|
244
295
|
|
|
@@ -28,7 +28,6 @@ from pathlib import Path
|
|
|
28
28
|
from . import closing_requirements as creq
|
|
29
29
|
from . import criteria_state
|
|
30
30
|
from ._wu_sections import slice_wu_section
|
|
31
|
-
from .changelog import parse_changelog
|
|
32
31
|
from .judge import MEASUREMENTS_SECTION
|
|
33
32
|
from .loop import (
|
|
34
33
|
_gate_number_from_wu_id,
|
|
@@ -293,22 +292,18 @@ def _check_changelog_entry_for_contract_changes(req: creq.Requirement, ctx: Clos
|
|
|
293
292
|
return True, ""
|
|
294
293
|
if creq.consumer_visible_section_is_na(section):
|
|
295
294
|
return True, ""
|
|
296
|
-
|
|
297
|
-
if
|
|
295
|
+
feature_id = ctx.wu_id.rsplit("/", 1)[0]
|
|
296
|
+
if creq.changelog_has_entry_for_feature(ctx.repo_root, feature_id):
|
|
297
|
+
return True, ""
|
|
298
|
+
if not creq.changelog_exists(ctx.repo_root):
|
|
298
299
|
return False, (
|
|
299
300
|
f"'{creq.CONSUMER_VISIBLE_HEADING}' names a real change but "
|
|
300
301
|
f"{creq.CHANGELOG_PATH} does not exist"
|
|
301
302
|
)
|
|
302
|
-
result = parse_changelog(changelog.read_text())
|
|
303
|
-
unreleased = result.unreleased()
|
|
304
|
-
feature_id = ctx.wu_id.rsplit("/", 1)[0]
|
|
305
|
-
entries = unreleased.entries if unreleased else []
|
|
306
|
-
if creq.changelog_has_entry_for(entries, feature_id):
|
|
307
|
-
return True, ""
|
|
308
303
|
return False, (
|
|
309
|
-
f"'{creq.CONSUMER_VISIBLE_HEADING}' names a real change but "
|
|
310
|
-
f"{creq.CHANGELOG_PATH}'s Unreleased section
|
|
311
|
-
f"{feature_id}"
|
|
304
|
+
f"'{creq.CONSUMER_VISIBLE_HEADING}' names a real change but neither "
|
|
305
|
+
f"{creq.CHANGELOG_PATH}'s Unreleased section nor a valid changelog.d/ "
|
|
306
|
+
f"fragment has an entry tracing to {feature_id}"
|
|
312
307
|
)
|
|
313
308
|
|
|
314
309
|
|
|
@@ -44,6 +44,7 @@ from __future__ import annotations
|
|
|
44
44
|
from dataclasses import dataclass
|
|
45
45
|
from pathlib import Path
|
|
46
46
|
from urllib.parse import urlparse
|
|
47
|
+
import json
|
|
47
48
|
import re
|
|
48
49
|
|
|
49
50
|
from specfuse.loop import ids as _ids
|
|
@@ -160,6 +161,7 @@ def _find_anchors(lines):
|
|
|
160
161
|
return anchors
|
|
161
162
|
|
|
162
163
|
|
|
164
|
+
_ENTRY_FILE_SUFFIXES = (".yaml", ".yml", ".json")
|
|
163
165
|
_EXPLICIT_ANCHOR_RE = re.compile(r'<a\s+(?:id|name)\s*=\s*"([^"]+)"', re.IGNORECASE)
|
|
164
166
|
_MD_HEADING_RE = re.compile(r'^ {0,3}#{1,6}\s+(.*?)\s*#*\s*$')
|
|
165
167
|
|
|
@@ -193,6 +195,58 @@ def _collect_anchor_names(text: str) -> set:
|
|
|
193
195
|
return names
|
|
194
196
|
|
|
195
197
|
|
|
198
|
+
def _json_entry_ids(value, entry_key: str) -> set:
|
|
199
|
+
"""Every scalar `entry_key` value in a parsed JSON document, at any depth."""
|
|
200
|
+
ids = set()
|
|
201
|
+
if isinstance(value, dict):
|
|
202
|
+
for key, item in value.items():
|
|
203
|
+
if key == entry_key and isinstance(item, (str, int)) and not isinstance(item, bool):
|
|
204
|
+
ids.add(str(item))
|
|
205
|
+
ids |= _json_entry_ids(item, entry_key)
|
|
206
|
+
elif isinstance(value, list):
|
|
207
|
+
for item in value:
|
|
208
|
+
ids |= _json_entry_ids(item, entry_key)
|
|
209
|
+
return ids
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _collect_entry_ids(path: Path, entry_key: str):
|
|
213
|
+
"""Ids a YAML or JSON decision file answers to, or None when it cannot be read (#3567).
|
|
214
|
+
|
|
215
|
+
YAML is scanned by line rather than parsed: `_miniyaml` refuses block scalars
|
|
216
|
+
and flow mappings by design, and decision files hold long rationale. A line
|
|
217
|
+
inside a block scalar that looks like the key therefore counts as an id, which
|
|
218
|
+
can only let a typo pass, never fail a good link.
|
|
219
|
+
"""
|
|
220
|
+
try:
|
|
221
|
+
text = path.read_text(encoding="utf-8")
|
|
222
|
+
if path.suffix == ".json":
|
|
223
|
+
return _json_entry_ids(json.loads(text), entry_key)
|
|
224
|
+
except (OSError, UnicodeDecodeError, ValueError):
|
|
225
|
+
return None
|
|
226
|
+
line_re = re.compile(
|
|
227
|
+
rf"""^\s*(?:-\s+)*["']?{re.escape(entry_key)}["']?\s*:\s+"""
|
|
228
|
+
r"""(?:"([^"]*)"|'([^']*)'|([^\s#"'][^#]*?))\s*(?:\#.*)?$"""
|
|
229
|
+
)
|
|
230
|
+
ids = set()
|
|
231
|
+
for line in text.splitlines():
|
|
232
|
+
m = line_re.match(line)
|
|
233
|
+
if m:
|
|
234
|
+
ids.add(next(group for group in m.groups() if group is not None))
|
|
235
|
+
return ids
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _entry_anchor_problem(target: Path, link_path: str, fragment: str, entry_key: str):
|
|
239
|
+
"""Why `fragment` does not resolve in a YAML or JSON decision file, or None."""
|
|
240
|
+
ids = _collect_entry_ids(target, entry_key)
|
|
241
|
+
if ids is None:
|
|
242
|
+
return f"'{link_path}', which could not be read as YAML or JSON"
|
|
243
|
+
if not ids:
|
|
244
|
+
return f"'{link_path}', which has no entries under entryKey '{entry_key}'"
|
|
245
|
+
if fragment not in ids:
|
|
246
|
+
return f"'{link_path}', which has no anchor '{fragment}'"
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
|
|
196
250
|
def _declared_decisions_location(repo_root: Path):
|
|
197
251
|
"""Resolved `decisions.location` only when layout.yml declares a `decisions:` block."""
|
|
198
252
|
data = _layout._read(repo_root / _layout.LAYOUT_RELPATH)
|
|
@@ -419,6 +473,7 @@ def _find_blocked_by(section, lines):
|
|
|
419
473
|
def _check_blocked_by(roadmap_doc: _FileDoc, archive_doc: _FileDoc, rows, repo_root: Path) -> list:
|
|
420
474
|
findings = []
|
|
421
475
|
declared_location = _declared_decisions_location(repo_root)
|
|
476
|
+
entry_key = _layout.load(repo_root).decisions.entry_key
|
|
422
477
|
roadmap_sections = _detail_sections(roadmap_doc.lines)
|
|
423
478
|
archive_sections = _detail_sections(archive_doc.lines)
|
|
424
479
|
|
|
@@ -488,6 +543,16 @@ def _check_blocked_by(roadmap_doc: _FileDoc, archive_doc: _FileDoc, rows, repo_r
|
|
|
488
543
|
f"{row['id']}'s Blocked-by link '{href}' points at "
|
|
489
544
|
f"'{link_path}', which has no anchor '{fragment}'",
|
|
490
545
|
))
|
|
546
|
+
elif (
|
|
547
|
+
fragment and entry_key and target.is_file()
|
|
548
|
+
and target.suffix in _ENTRY_FILE_SUFFIXES
|
|
549
|
+
):
|
|
550
|
+
problem = _entry_anchor_problem(target, link_path, fragment, entry_key)
|
|
551
|
+
if problem:
|
|
552
|
+
findings.append(Finding(
|
|
553
|
+
SEVERITY_ERROR, doc.filename, blocked_by_line + 1,
|
|
554
|
+
f"{row['id']}'s Blocked-by link '{href}' points at {problem}",
|
|
555
|
+
))
|
|
491
556
|
if declared_location is not None:
|
|
492
557
|
location = (repo_root / declared_location).resolve()
|
|
493
558
|
try:
|