specfuse-loop 0.9.2__tar.gz → 0.9.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {specfuse_loop-0.9.2/specfuse_loop.egg-info → specfuse_loop-0.9.3}/PKG-INFO +1 -1
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/pyproject.toml +1 -1
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/cost.py +21 -16
- specfuse_loop-0.9.3/specfuse/loop/data/VERSION +1 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/templates/WU.template.md +9 -1
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/loop.py +38 -9
- specfuse_loop-0.9.3/specfuse/loop/rearm_migration.py +267 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3/specfuse_loop.egg-info}/PKG-INFO +1 -1
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse_loop.egg-info/SOURCES.txt +4 -0
- specfuse_loop-0.9.3/tests/test_fold_contract_documented.py +127 -0
- specfuse_loop-0.9.3/tests/test_rearm_fold_marker.py +143 -0
- specfuse_loop-0.9.3/tests/test_rearm_migration.py +306 -0
- specfuse_loop-0.9.2/specfuse/loop/data/VERSION +0 -1
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/LICENSE +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/NOTICE +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/README.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/setup.cfg +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/__init__.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/_filelock.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/_miniyaml.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/_wu_sections.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/adopt_feature.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/arm_eval.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/arm_sweep.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/arm_txn.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/changelog.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/closing_requirements.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/CHANGELOG.seed.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/LEARNINGS.template.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/docs/concepts/adopting-auto-mode.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/docs/concepts/autonomy-stop-classes.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/docs/getting-started.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/docs/methodology.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/docs/skills.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/gitignore.snippet +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/monitoring-secrets-checklist.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/monitoring.overrides.yml.example +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/monitoring.yml.example +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/roadmap.template.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/close-discipline.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/correlation-ids.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/design-for-diagnosis.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/never-touch.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/operator-escalation.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/planning-discipline.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/result-contract.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/security-boundaries.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules/verification-discipline.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/rules-local/README.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/schemas/driver-event.schema.json +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/schemas/event.schema.json +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/templates/GATE.template.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/templates/LEARNINGS-pending.template.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/templates/PLAN.template.md +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/verification.yml.example +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/data/workflows/specfuse-monitor.yml +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/escalation.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/events_stats.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/gate_commands.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/gate_eval.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/gh_backend.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/gh_features.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/labels.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/lint_closing.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/lint_monitoring.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/lint_plan.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/lint_roadmap.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/plan_baseline.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/scaffold.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/loop/validate_event.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/__init__.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/adapters.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/artifact.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/autofix.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/autofix_invoke.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/autofix_run.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/autofix_state.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/cli.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/diagnose_cli.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/diagnosis.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/fingerprint.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/issues.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/providers/__init__.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/providers/_azure_auth.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/providers/azure_app_insights.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/providers/azure_service_bus.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/redaction.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse/monitor/schedule.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse_loop.egg-info/dependency_links.txt +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse_loop.egg-info/entry_points.txt +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse_loop.egg-info/requires.txt +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/specfuse_loop.egg-info/top_level.txt +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_accept_hedged_close_headline.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_accept_hedged_close_skill.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_adopt_feature.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_app_insights_adapters.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_eval.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_eval_lint_class.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_eval_wiring.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_gate_edits_uncommitted.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_sweep.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_sweep_gate.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_txn.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_arm_wiring.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_artifact_redaction.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_attempt_outcome_contract.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_attempt_outcome_emission.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_attention_nonwriting_guard.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_attention_skill_structure.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autoclose_deferral_visibility.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autofix_invoke.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autofix_live.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autofix_predicate.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autofix_run.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autofix_state.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autosync.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autosync_consent.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autosync_firstrun.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autosync_no_cwd_leak.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_autosync_plugin.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_azure_credential_resolution.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_backend.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_baseline_escalation_message.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_baseline_persistence.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_baseline_probe.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_bash_routing.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_bats_suites_gated.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_bookkeeping_commit_crash_run.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_bookkeeping_no_verify.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_changelog_collection.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_changelog_release_wiring.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_changelog_schema.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_ci_runs_every_gate.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_claude_resolution.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_close_wu_self_reference.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_closing_deliverable_guard.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_closing_guard_contracts.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_closing_guard_prediction.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_closing_requirements.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_console_encoding.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_correlation_id_override.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_correlation_id_two_surfaces.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_cost_lifetime.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_cost_tracking.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_degraded_oracle.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_deliverable_presence_gate.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_derive_monitoring_discovery.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_derive_monitoring_skill_registration.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_design_for_diagnosis_rule.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_diagnose_headless.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_diagnose_issue_skill.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_diagnosis.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_diagnosis_roundtrip_live.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_dispatch_skeleton.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_doctor.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_done_feature_gates.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_driver_event_registry_covers_emitters.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_driver_integration.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_driver_lock.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_duration_tracking.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_empty_files_escalation.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_ensure_feature_branch.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_ensure_feature_branch_base.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_escalation_contract.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_escalation_emit.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_event_gate_full_envelope.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_events_redaction.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_events_stats.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_extra_gates.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_failure_artifact_model.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_feature_id_resolution.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_feature_review_accumulation.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_filelock_portable.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_fingerprint.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_fix_bug_headless.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_force_full_close.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gate_budget.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gate_budget_post_dispatch.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gate_eval.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gate_eval_calibration.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gate_eval_intermediate_wiring.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gate_eval_terminal_wiring.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gate_report_verdict.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gh_backend.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gh_features.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_git_env_isolation.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_gitleaks_pinning.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_hashed_denylist.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_hashed_denylist_ci.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_heartbeat_adapter.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_hedged_kind_contract.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_init_integration.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_interpreter_normalization.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_label_registry.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_label_registry_covers_consumers.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_leak_findings_redaction.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_leak_scan.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_leak_scan_content.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_learnings_query.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_learnings_staging.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_legacy_4wu_terminal_flips.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lifecycle_integration.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lifetime_accounting.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_bare_produces_path.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_boundary_consistency.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_boundary_extraction.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_close_intermediate.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_close_wu.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_closing.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_correlation_id.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_monitoring.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_oracle_env.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_plan_contract_fields.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_plan_errors.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_plan_next_draft.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_plan_relative_import.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_plan_verdict_exempt.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_planning_sections.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_produces_driver_helper.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_produces_satisfiability.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_roadmap.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_sections.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_task_graph_yaml_selection.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_lint_verification_reference.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_attempt_evidence.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_auto_archive.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_caveman_preamble.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_close_intermediate.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_defaults_by_type.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_effort.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_failure_note_cap.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_files_changed_guard.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_gate_budget.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_model_alias.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_orchestration.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_post_pass_invariant.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_reset_preserving_events.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_smoke_runner.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_unsandboxed.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_loop_zero_token_guard.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_migrate_legacy.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_miniyaml_equivalence.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_miniyaml_negative.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitor_cli.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitor_issue_lifecycle.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitor_runner_surfaces.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitoring_bootstrap_artifacts.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitoring_cron_dialect.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitoring_example.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitoring_fenced_blocks.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_monitoring_seed.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_no_pytest_subprocess.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_operator_escalation_rule.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_plan_baseline.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_planned_cost_lint.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_planning_cost_floor.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_prepare_feature.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_prepare_scaffold_sync.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_produces_field.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_produces_in_diff.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_produces_semantics_unified.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_produces_shape.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_provision_labels.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_queue_stalled_adapter.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_redaction_windows_home.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_resolve_base.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_result_block.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_roadmap_add_skill.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_roadmap_archive_skill.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_roadmap_link_gate.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_roadmap_row_parser.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_routed_finding_tracking.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_data_in_sync.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_doc_hygiene.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_docs.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_init.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_label_provisioning.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_manifest.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_resources.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_seed_sanity.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_upgrade.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_scaffold_wiring.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_schedule_dialect.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_service_bus_dlq_adapter.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_skill_discovery_links.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_skills_vendored_in_sync.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_spinning_rearm_gate.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_squash_commit_hook_crash.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_template_closing_shapes.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_terminal_flip_ownership.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_terminal_flips.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_timeout_kill_portable.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_unterminated_frontmatter.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_untracked_feature_folder.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_untracked_file_absorption.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_upgrade_integration.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_upgrade_merge_gate.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_validate_event.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_validate_event_driver_types.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_verdict_coupling.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_verify_empty_gate_set.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_version_consistency.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_version_skew.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_windows_gate_exec.py +0 -0
- {specfuse_loop-0.9.2 → specfuse_loop-0.9.3}/tests/test_wu_execution_metadata.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "specfuse-loop"
|
|
7
|
-
version = "0.9.
|
|
7
|
+
version = "0.9.3"
|
|
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"
|
|
@@ -5,22 +5,27 @@
|
|
|
5
5
|
"""One lifetime-cost reader shared by both cost-gated consumers.
|
|
6
6
|
|
|
7
7
|
`wu_lifetime_cost_usd` answers "what has this work unit cost over its whole
|
|
8
|
-
life
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
8
|
+
life." FEAT-2026-0067 converged the fold onto one path: every re-arm now
|
|
9
|
+
unconditionally folds the prior cycle's spend into `cumulative_cost_usd`
|
|
10
|
+
(driven by an explicit `folded_through_re_arm` marker, not by inferring state
|
|
11
|
+
from a value), so `cumulative_cost_usd` is the lifetime accumulator and
|
|
12
|
+
`re_arm_history[].prior_cost_usd` is a pure audit record of what each cycle
|
|
13
|
+
cost — not a second place the fold might or might not have run.
|
|
14
|
+
|
|
15
|
+
Before that migration, some already-re-armed WUs had a prior cycle's spend
|
|
16
|
+
survive only in `re_arm_history[].prior_cost_usd` with `cumulative_cost_usd`
|
|
17
|
+
absent — a pre-migration legacy shape that this module's frontmatter fallback
|
|
18
|
+
still tolerates for projects that have not run the FEAT-2026-0067 migration,
|
|
19
|
+
not an ongoing design a reader needs to branch on.
|
|
20
|
+
|
|
21
|
+
Summing frontmatter fields cannot distinguish "already folded" from "never
|
|
22
|
+
folded" without replaying fold history, and naive summation double-counts a
|
|
23
|
+
folded record. Instead this module sums `payload.cost_usd` across every
|
|
24
|
+
`attempt_outcome` event for the work unit in `events.jsonl` — a source that
|
|
25
|
+
already carries every dispatch cycle's cost exactly once, independent of fold
|
|
26
|
+
shape. Frontmatter is used only as a fallback for the units this repo's
|
|
27
|
+
corpus shows have no usable event history (see FEAT-2026-0062 criterion 9's
|
|
28
|
+
measurement).
|
|
24
29
|
|
|
25
30
|
Deliberately dependency-light: this module must not import `loop.py` or
|
|
26
31
|
`arm_eval.py` (see `arm_eval.py`'s module docstring on the `loop -> arm_eval`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.9.3
|
|
@@ -62,10 +62,18 @@ AUTHOR-SET FIELDS — fill or override these at draft/arm time:
|
|
|
62
62
|
DRIVER-OWNED FIELDS - the driver writes these at outcome time; authors leave them absent:
|
|
63
63
|
<!-- driver-owned: attempts, cost_usd, input_tokens, output_tokens, duration_seconds,
|
|
64
64
|
cumulative_cost_usd, cumulative_duration_seconds, cumulative_input_tokens,
|
|
65
|
-
cumulative_output_tokens, re_arm_count, re_arm_history -->
|
|
65
|
+
cumulative_output_tokens, re_arm_count, re_arm_history, folded_through_re_arm -->
|
|
66
66
|
<!-- driver-stamped at dispatch (resolved execution metadata, visible in this .md):
|
|
67
67
|
model, effort (override or type default), gate_set (the verification.yml set
|
|
68
68
|
that is this WU's exit oracle), driver_version, started_at (UTC ISO). -->
|
|
69
|
+
<!-- `folded_through_re_arm` (FEAT-2026-0067): the `re_arm_count` value already
|
|
70
|
+
folded into the `cumulative_*` accumulators. On every re-arm the driver folds
|
|
71
|
+
the prior cycle's cost/duration/token spend into `cumulative_cost_usd`,
|
|
72
|
+
`cumulative_duration_seconds`, `cumulative_input_tokens`, and
|
|
73
|
+
`cumulative_output_tokens` — unconditionally, one fold path, including a
|
|
74
|
+
re-arm whose prior cycle cost nothing. `cumulative_*` is the lifetime
|
|
75
|
+
accumulator across every re-arm; `re_arm_history[].prior_*` is a pure audit
|
|
76
|
+
record of what each cycle cost, not a second place the fold might live. -->
|
|
69
77
|
<!-- Full field semantics in docs/methodology.md §2 and events.jsonl outcome payloads. -->
|
|
70
78
|
|
|
71
79
|
Dependencies live in PLAN.md's `gates[].work_units[].depends_on` graph, not
|
|
@@ -105,7 +105,7 @@ SPECFUSE_DIR = Path(".specfuse")
|
|
|
105
105
|
REPO_ROOT = SPECFUSE_DIR.parent
|
|
106
106
|
FEATURES_DIR = SPECFUSE_DIR / "features"
|
|
107
107
|
VERIFICATION_PATH = SPECFUSE_DIR / "verification.yml"
|
|
108
|
-
DRIVER_VERSION = "0.9.
|
|
108
|
+
DRIVER_VERSION = "0.9.3"
|
|
109
109
|
# Oldest scaffold layout this driver can drive. init.sh stamps the scaffold's own
|
|
110
110
|
# version into `.specfuse/VERSION`; check_scaffold_version() fails loud at startup if
|
|
111
111
|
# the consumer's scaffold is older than this, pointing at `specfuse upgrade`. Bump
|
|
@@ -1672,20 +1672,35 @@ def write_cost_to_wu(backend, wu: WorkUnit, cum_usage: dict) -> None:
|
|
|
1672
1672
|
|
|
1673
1673
|
|
|
1674
1674
|
def detect_rearm_dispatch(wu: WorkUnit) -> bool:
|
|
1675
|
-
"""Return True when wu is a re-arm dispatch whose prior cycle
|
|
1676
|
-
|
|
1675
|
+
"""Return True when wu is a re-arm dispatch whose prior cycle has not yet
|
|
1676
|
+
been folded into the cumulative accumulators.
|
|
1677
|
+
|
|
1678
|
+
Reads re_arm_count and folded_through_re_arm from the WU's on-disk
|
|
1679
|
+
frontmatter because load_wu does not load those fields into the
|
|
1680
|
+
WorkUnit object. A fold is owed when re_arm_count > folded_through_re_arm;
|
|
1681
|
+
fold_cumulative_on_rearm stamps folded_through_re_arm = re_arm_count as
|
|
1682
|
+
part of the same write set it uses for the accumulators, so this is a
|
|
1683
|
+
marker comparison, not an inference from a value that a fold can also
|
|
1684
|
+
produce as a side effect (FEAT-2026-0067/T01).
|
|
1677
1685
|
|
|
1678
|
-
Reads re_arm_count and cost_usd from the WU's on-disk frontmatter because
|
|
1679
|
-
load_wu does not load those fields into the WorkUnit object.
|
|
1680
1686
|
Returns False for first-time dispatches (re_arm_count absent or 0) and for
|
|
1681
|
-
re-arms
|
|
1687
|
+
re-arms already folded (folded_through_re_arm == re_arm_count).
|
|
1688
|
+
|
|
1689
|
+
Exposure left for FEAT-2026-0067/T02: a WU re-armed under the prior
|
|
1690
|
+
cost_usd-based guard has no folded_through_re_arm field at all, so it
|
|
1691
|
+
reads as 0 here — indistinguishable from "never folded" even when it was.
|
|
1692
|
+
This WU does not add a value-inferring fallback to paper over that; T02
|
|
1693
|
+
owns the migration of existing WU shapes.
|
|
1682
1694
|
"""
|
|
1683
1695
|
fm, _ = read_frontmatter(wu.file)
|
|
1684
1696
|
re_arm_count = fm.get("re_arm_count", 0)
|
|
1685
|
-
if not isinstance(re_arm_count, int) or re_arm_count
|
|
1697
|
+
if not isinstance(re_arm_count, int) or isinstance(re_arm_count, bool) \
|
|
1698
|
+
or re_arm_count <= 0:
|
|
1686
1699
|
return False
|
|
1687
|
-
|
|
1688
|
-
|
|
1700
|
+
folded_through = fm.get("folded_through_re_arm", 0)
|
|
1701
|
+
if not isinstance(folded_through, int) or isinstance(folded_through, bool):
|
|
1702
|
+
folded_through = 0
|
|
1703
|
+
return re_arm_count > folded_through
|
|
1689
1704
|
|
|
1690
1705
|
|
|
1691
1706
|
def rearm_reproduction_gate(wu: WorkUnit) -> tuple[bool, str]:
|
|
@@ -1744,6 +1759,12 @@ def fold_cumulative_on_rearm(wu: WorkUnit, backend: Backend) -> None:
|
|
|
1744
1759
|
|
|
1745
1760
|
Backward-compatible: existing WUs with no cumulative_* fields initialise
|
|
1746
1761
|
from 0 — no KeyError on first re-arm of a WU that pre-dates this contract.
|
|
1762
|
+
|
|
1763
|
+
Idempotent: a second call for the same re-arm reads cost_usd/duration_seconds/
|
|
1764
|
+
input_tokens/output_tokens back as 0 (this call already reset them), so it
|
|
1765
|
+
adds 0 to each cumulative_* accumulator and re-stamps the same
|
|
1766
|
+
folded_through_re_arm value. Two calls for one re-arm produce the same
|
|
1767
|
+
accumulators as one (FEAT-2026-0067/T01).
|
|
1747
1768
|
"""
|
|
1748
1769
|
fm, _ = read_frontmatter(wu.file)
|
|
1749
1770
|
prior_cost = float(fm.get("cost_usd") or 0)
|
|
@@ -1783,6 +1804,14 @@ def fold_cumulative_on_rearm(wu: WorkUnit, backend: Backend) -> None:
|
|
|
1783
1804
|
backend.set_wu(wu, "input_tokens", 0)
|
|
1784
1805
|
backend.set_wu(wu, "output_tokens", 0)
|
|
1785
1806
|
|
|
1807
|
+
# Explicit fold marker (FEAT-2026-0067/T01): stamped in the same write set
|
|
1808
|
+
# as the accumulators above so detect_rearm_dispatch can tell "folded" from
|
|
1809
|
+
# "cost was zero" without inferring it from cost_usd's value.
|
|
1810
|
+
re_arm_count = fm.get("re_arm_count", 0)
|
|
1811
|
+
if not isinstance(re_arm_count, int) or isinstance(re_arm_count, bool):
|
|
1812
|
+
re_arm_count = 0
|
|
1813
|
+
backend.set_wu(wu, "folded_through_re_arm", re_arm_count)
|
|
1814
|
+
|
|
1786
1815
|
|
|
1787
1816
|
def gate_budget_usd(gate_file: Path) -> float | None:
|
|
1788
1817
|
"""Return the optional cumulative-cost ceiling declared on a GATE.md.
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Specfuse contributors
|
|
3
|
+
# Licensed under the Apache License, Version 2.0. See LICENSE.
|
|
4
|
+
#
|
|
5
|
+
"""Stamp `folded_through_re_arm` onto every already-re-armed work unit
|
|
6
|
+
(FEAT-2026-0067/T02).
|
|
7
|
+
|
|
8
|
+
T01 (`detect_rearm_dispatch`, `specfuse/loop/loop.py`) reads an absent marker
|
|
9
|
+
as `0`. Six WUs in this repository were folded under the pre-T01 `cost_usd >
|
|
10
|
+
0` guard and carry no marker at all — indistinguishable, under T01's rule,
|
|
11
|
+
from "never folded". This module closes that gap by giving every re-armed WU
|
|
12
|
+
an explicit marker, without re-running the fold logic T01 owns.
|
|
13
|
+
|
|
14
|
+
Two shapes, two treatments:
|
|
15
|
+
|
|
16
|
+
* **fold-ran** (`cumulative_cost_usd` present) — the fold already moved the
|
|
17
|
+
prior cycle's spend. Stamping `folded_through_re_arm = re_arm_count` is
|
|
18
|
+
the whole fix; `cumulative_*` is read, never written.
|
|
19
|
+
* **fold-never-ran** (`cumulative_cost_usd` absent, `re_arm_history[]`
|
|
20
|
+
populated) — the prior cycle's spend survives only as
|
|
21
|
+
`re_arm_history[].prior_cost_usd` / `prior_duration_seconds`. This module
|
|
22
|
+
folds those two recorded fields into `cumulative_cost_usd` /
|
|
23
|
+
`cumulative_duration_seconds` before stamping the marker, so every
|
|
24
|
+
re-armed WU ends up the same shape (PLAN.md's "converge" decision,
|
|
25
|
+
applied retroactively). No number is invented: only figures already
|
|
26
|
+
present in `re_arm_history` are summed. `prior_input_tokens` /
|
|
27
|
+
`prior_output_tokens` were never recorded on either fold-never-ran WU in
|
|
28
|
+
this repository, so `cumulative_input_tokens` / `cumulative_output_tokens`
|
|
29
|
+
are left unset rather than backfilled from `events.jsonl` — that would be
|
|
30
|
+
a second, undeclared source of truth for the same accumulator.
|
|
31
|
+
|
|
32
|
+
**A value with two meanings, read as if it had one.** `cost_usd` may be
|
|
33
|
+
folded safely by `fold_cumulative_on_rearm` because that fold runs *at
|
|
34
|
+
dispatch*, where `cost_usd` provably holds the prior cycle's spend. This
|
|
35
|
+
module runs *offline*, where `cost_usd` means "the prior cycle" **or**
|
|
36
|
+
"the current cycle" depending on whether a re-dispatch ever happened —
|
|
37
|
+
the exact defect class this feature exists to remove, reproduced by its
|
|
38
|
+
own fix. When a WU was re-armed and never re-dispatched, `cost_usd`
|
|
39
|
+
still holds the prior cycle's spend, and folding `re_arm_history`'s copy
|
|
40
|
+
of that same spend into `cumulative_cost_usd` without resetting
|
|
41
|
+
`cost_usd` double-counts it. The fold-never-ran branch below detects this
|
|
42
|
+
case — `cost_usd` agrees with the `re_arm_history` prior-cost sum within
|
|
43
|
+
`_COST_TOLERANCE_USD` — and resets `cost_usd` / `duration_seconds` to
|
|
44
|
+
`0.0` in the same write set, rather than adding a second copy of money
|
|
45
|
+
that is already accounted for in `cumulative_*`. Anyone adding a new
|
|
46
|
+
offline reader of `cost_usd` should assume it needs the same treatment.
|
|
47
|
+
|
|
48
|
+
A WU never re-armed (`re_arm_count` absent or `0`) is untouched: no read,
|
|
49
|
+
no write, not even a no-op frontmatter round-trip.
|
|
50
|
+
|
|
51
|
+
Writes go through `specfuse.loop.loop.write_frontmatter_field`, the same
|
|
52
|
+
single-key, no-reflow writer `fold_cumulative_on_rearm` uses — every other
|
|
53
|
+
line in the file, marker or not, keeps its original text.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
import json
|
|
59
|
+
from dataclasses import dataclass, field
|
|
60
|
+
from pathlib import Path
|
|
61
|
+
|
|
62
|
+
from . import loop as _loop
|
|
63
|
+
|
|
64
|
+
_COST_TOLERANCE_USD = 0.02
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _int_or_zero(value: object) -> int:
|
|
68
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
69
|
+
return 0
|
|
70
|
+
return value
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _classify(fm: dict) -> str:
|
|
74
|
+
"""Return "not_rearmed", "fold_ran", or "fold_never_ran"."""
|
|
75
|
+
re_arm_count = fm.get("re_arm_count", 0)
|
|
76
|
+
if not isinstance(re_arm_count, int) or isinstance(re_arm_count, bool) or re_arm_count <= 0:
|
|
77
|
+
return "not_rearmed"
|
|
78
|
+
if "cumulative_cost_usd" in fm:
|
|
79
|
+
return "fold_ran"
|
|
80
|
+
return "fold_never_ran"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _history_sums(fm: dict) -> tuple[float, float]:
|
|
84
|
+
"""Sum `prior_cost_usd` and `prior_duration_seconds` across every
|
|
85
|
+
`re_arm_history` entry. Non-numeric or missing fields contribute 0."""
|
|
86
|
+
history = fm.get("re_arm_history") or []
|
|
87
|
+
cost = 0.0
|
|
88
|
+
duration = 0.0
|
|
89
|
+
if isinstance(history, list):
|
|
90
|
+
for entry in history:
|
|
91
|
+
if not isinstance(entry, dict):
|
|
92
|
+
continue
|
|
93
|
+
pc = entry.get("prior_cost_usd")
|
|
94
|
+
if isinstance(pc, (int, float)) and not isinstance(pc, bool):
|
|
95
|
+
cost += float(pc)
|
|
96
|
+
pd = entry.get("prior_duration_seconds")
|
|
97
|
+
if isinstance(pd, (int, float)) and not isinstance(pd, bool):
|
|
98
|
+
duration += float(pd)
|
|
99
|
+
return cost, duration
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _events_cost_before(
|
|
103
|
+
events_path: Path, wu_id: str, cutoff_iso: str
|
|
104
|
+
) -> tuple[float, bool]:
|
|
105
|
+
"""Sum `attempt_outcome` `cost_usd` for `wu_id` at or before `cutoff_iso`.
|
|
106
|
+
|
|
107
|
+
Returns `(total, seen)`; `seen` is False when the events file is
|
|
108
|
+
missing/unreadable or carries no matching `attempt_outcome` for this WU
|
|
109
|
+
— that means "nothing to cross-check against", not "prior cost was
|
|
110
|
+
zero", so the caller must not treat it as a disagreement. ISO-8601
|
|
111
|
+
timestamps with a fixed-offset suffix compare correctly as strings, so
|
|
112
|
+
this avoids a datetime dependency.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
lines = events_path.read_text().splitlines()
|
|
116
|
+
except OSError:
|
|
117
|
+
return 0.0, False
|
|
118
|
+
total = 0.0
|
|
119
|
+
seen = False
|
|
120
|
+
for line in lines:
|
|
121
|
+
line = line.strip()
|
|
122
|
+
if not line:
|
|
123
|
+
continue
|
|
124
|
+
try:
|
|
125
|
+
event = json.loads(line)
|
|
126
|
+
except (json.JSONDecodeError, ValueError):
|
|
127
|
+
continue
|
|
128
|
+
if not isinstance(event, dict):
|
|
129
|
+
continue
|
|
130
|
+
if event.get("event_type") != "attempt_outcome":
|
|
131
|
+
continue
|
|
132
|
+
if event.get("correlation_id") != wu_id:
|
|
133
|
+
continue
|
|
134
|
+
timestamp = event.get("timestamp")
|
|
135
|
+
if not isinstance(timestamp, str) or timestamp > cutoff_iso:
|
|
136
|
+
continue
|
|
137
|
+
seen = True
|
|
138
|
+
payload = event.get("payload")
|
|
139
|
+
if not isinstance(payload, dict):
|
|
140
|
+
continue
|
|
141
|
+
cost = payload.get("cost_usd")
|
|
142
|
+
if isinstance(cost, (int, float)) and not isinstance(cost, bool):
|
|
143
|
+
total += float(cost)
|
|
144
|
+
return total, seen
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class CensusResult:
|
|
149
|
+
fold_ran: list[Path] = field(default_factory=list)
|
|
150
|
+
fold_never_ran: list[Path] = field(default_factory=list)
|
|
151
|
+
|
|
152
|
+
def __str__(self) -> str:
|
|
153
|
+
return (
|
|
154
|
+
f"re-armed WUs: fold ran = {len(self.fold_ran)}"
|
|
155
|
+
f" fold never ran = {len(self.fold_never_ran)}"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def census(root: Path) -> CensusResult:
|
|
160
|
+
"""Classify every re-armed WU under `root` by fold shape.
|
|
161
|
+
|
|
162
|
+
`root` is a directory searched recursively for `WU-*.md` files (the
|
|
163
|
+
repository's `.specfuse/features/` tree, or a fixture directory in
|
|
164
|
+
tests).
|
|
165
|
+
"""
|
|
166
|
+
result = CensusResult()
|
|
167
|
+
for wu_path in sorted(Path(root).glob("**/WU-*.md")):
|
|
168
|
+
fm, _ = _loop.read_frontmatter(wu_path)
|
|
169
|
+
shape = _classify(fm)
|
|
170
|
+
if shape == "fold_ran":
|
|
171
|
+
result.fold_ran.append(wu_path)
|
|
172
|
+
elif shape == "fold_never_ran":
|
|
173
|
+
result.fold_never_ran.append(wu_path)
|
|
174
|
+
return result
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class PriorCostDisagreement(RuntimeError):
|
|
178
|
+
"""Raised when a fold-never-ran WU's recorded prior cost disagrees with
|
|
179
|
+
its own `events.jsonl` total by more than a rounding tolerance."""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class MigrationOutcome:
|
|
184
|
+
path: Path
|
|
185
|
+
action: str # "not_rearmed" | "fold_ran_stamped" | "fold_never_ran_migrated" | "already_migrated"
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def migrate_file(wu_path: Path, *, events_path: Path | None = None) -> MigrationOutcome:
|
|
189
|
+
"""Migrate one WU file in place. Idempotent: a second call for an
|
|
190
|
+
already-migrated file is a no-op read, no writes.
|
|
191
|
+
|
|
192
|
+
`events_path` defaults to `events.jsonl` next to `wu_path` (the feature
|
|
193
|
+
folder layout `correlation-ids.md` documents); pass explicitly for a
|
|
194
|
+
fixture that does not follow that layout.
|
|
195
|
+
"""
|
|
196
|
+
wu_path = Path(wu_path)
|
|
197
|
+
fm, _ = _loop.read_frontmatter(wu_path)
|
|
198
|
+
shape = _classify(fm)
|
|
199
|
+
|
|
200
|
+
if shape == "not_rearmed":
|
|
201
|
+
return MigrationOutcome(wu_path, "not_rearmed")
|
|
202
|
+
|
|
203
|
+
re_arm_count = _int_or_zero(fm.get("re_arm_count", 0))
|
|
204
|
+
folded_through = fm.get("folded_through_re_arm", 0)
|
|
205
|
+
if not isinstance(folded_through, int) or isinstance(folded_through, bool):
|
|
206
|
+
folded_through = 0
|
|
207
|
+
|
|
208
|
+
if shape == "fold_ran":
|
|
209
|
+
if folded_through == re_arm_count:
|
|
210
|
+
return MigrationOutcome(wu_path, "already_migrated")
|
|
211
|
+
_loop.write_frontmatter_field(wu_path, "folded_through_re_arm", re_arm_count)
|
|
212
|
+
return MigrationOutcome(wu_path, "fold_ran_stamped")
|
|
213
|
+
|
|
214
|
+
# fold_never_ran
|
|
215
|
+
if folded_through == re_arm_count and "cumulative_cost_usd" in fm:
|
|
216
|
+
return MigrationOutcome(wu_path, "already_migrated")
|
|
217
|
+
|
|
218
|
+
prior_cost, prior_duration = _history_sums(fm)
|
|
219
|
+
|
|
220
|
+
wu_id = fm.get("id")
|
|
221
|
+
history = fm.get("re_arm_history") or []
|
|
222
|
+
last_entry_ts = None
|
|
223
|
+
if isinstance(history, list):
|
|
224
|
+
for entry in history:
|
|
225
|
+
if isinstance(entry, dict) and isinstance(entry.get("timestamp"), str):
|
|
226
|
+
last_entry_ts = entry["timestamp"]
|
|
227
|
+
if isinstance(wu_id, str) and wu_id and last_entry_ts:
|
|
228
|
+
resolved_events_path = events_path or (wu_path.parent / "events.jsonl")
|
|
229
|
+
events_total, seen = _events_cost_before(
|
|
230
|
+
resolved_events_path, wu_id, last_entry_ts
|
|
231
|
+
)
|
|
232
|
+
if seen and abs(events_total - prior_cost) > _COST_TOLERANCE_USD:
|
|
233
|
+
raise PriorCostDisagreement(
|
|
234
|
+
f"{wu_path}: re_arm_history prior_cost_usd sums to "
|
|
235
|
+
f"{prior_cost:.6f} but events.jsonl attempt_outcome total "
|
|
236
|
+
f"through {last_entry_ts} is {events_total:.6f} "
|
|
237
|
+
f"(tolerance {_COST_TOLERANCE_USD})"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
cost_usd = fm.get("cost_usd")
|
|
241
|
+
money_already_present = (
|
|
242
|
+
isinstance(cost_usd, (int, float))
|
|
243
|
+
and not isinstance(cost_usd, bool)
|
|
244
|
+
and abs(float(cost_usd) - prior_cost) <= _COST_TOLERANCE_USD
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
_loop.write_frontmatter_field(
|
|
248
|
+
wu_path, "cumulative_cost_usd", round(prior_cost, 6)
|
|
249
|
+
)
|
|
250
|
+
_loop.write_frontmatter_field(
|
|
251
|
+
wu_path, "cumulative_duration_seconds", round(prior_duration, 3)
|
|
252
|
+
)
|
|
253
|
+
if money_already_present:
|
|
254
|
+
# Never re-dispatched: `cost_usd` still holds the same spend now
|
|
255
|
+
# folded into `cumulative_cost_usd`. Reset it in this write set so
|
|
256
|
+
# the two fields don't both carry a copy of one cycle's spend.
|
|
257
|
+
_loop.write_frontmatter_field(wu_path, "cost_usd", 0.0)
|
|
258
|
+
_loop.write_frontmatter_field(wu_path, "duration_seconds", 0.0)
|
|
259
|
+
_loop.write_frontmatter_field(wu_path, "folded_through_re_arm", re_arm_count)
|
|
260
|
+
return MigrationOutcome(wu_path, "fold_never_ran_migrated")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def migrate_repo(root: Path) -> list[MigrationOutcome]:
|
|
264
|
+
"""Migrate every `WU-*.md` file under `root`. Returns one outcome per
|
|
265
|
+
file that was inspected (including untouched `not_rearmed` files, so
|
|
266
|
+
callers can audit that nothing else was written)."""
|
|
267
|
+
return [migrate_file(wu_path) for wu_path in sorted(Path(root).glob("**/WU-*.md"))]
|
|
@@ -26,6 +26,7 @@ specfuse/loop/lint_plan.py
|
|
|
26
26
|
specfuse/loop/lint_roadmap.py
|
|
27
27
|
specfuse/loop/loop.py
|
|
28
28
|
specfuse/loop/plan_baseline.py
|
|
29
|
+
specfuse/loop/rearm_migration.py
|
|
29
30
|
specfuse/loop/scaffold.py
|
|
30
31
|
specfuse/loop/validate_event.py
|
|
31
32
|
specfuse/loop/data/CHANGELOG.seed.md
|
|
@@ -173,6 +174,7 @@ tests/test_feature_review_accumulation.py
|
|
|
173
174
|
tests/test_filelock_portable.py
|
|
174
175
|
tests/test_fingerprint.py
|
|
175
176
|
tests/test_fix_bug_headless.py
|
|
177
|
+
tests/test_fold_contract_documented.py
|
|
176
178
|
tests/test_force_full_close.py
|
|
177
179
|
tests/test_gate_budget.py
|
|
178
180
|
tests/test_gate_budget_post_dispatch.py
|
|
@@ -263,6 +265,8 @@ tests/test_produces_semantics_unified.py
|
|
|
263
265
|
tests/test_produces_shape.py
|
|
264
266
|
tests/test_provision_labels.py
|
|
265
267
|
tests/test_queue_stalled_adapter.py
|
|
268
|
+
tests/test_rearm_fold_marker.py
|
|
269
|
+
tests/test_rearm_migration.py
|
|
266
270
|
tests/test_redaction_windows_home.py
|
|
267
271
|
tests/test_resolve_base.py
|
|
268
272
|
tests/test_result_block.py
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Specfuse contributors
|
|
3
|
+
# Licensed under the Apache License, Version 2.0. See LICENSE.
|
|
4
|
+
#
|
|
5
|
+
"""FEAT-2026-0067/T03 — the written contract agrees with the converged fold.
|
|
6
|
+
|
|
7
|
+
Asserts the WU template documents `folded_through_re_arm` and the
|
|
8
|
+
unconditional lifetime-accumulator contract, that both template copies stay
|
|
9
|
+
byte-identical, that `cost.py`'s module docstring names the pre-migration
|
|
10
|
+
fallback as legacy rather than an ongoing second shape, and that
|
|
11
|
+
`wu_lifetime_cost_usd`'s behaviour is unchanged by this documentation-only WU.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import tempfile
|
|
18
|
+
import unittest
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from specfuse.loop import cost
|
|
22
|
+
from specfuse.loop.cost import wu_lifetime_cost_usd
|
|
23
|
+
|
|
24
|
+
_REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
+
_CANONICAL_TEMPLATE = _REPO_ROOT / "specfuse/loop/data/templates/WU.template.md"
|
|
26
|
+
_VENDORED_TEMPLATE = _REPO_ROOT / ".specfuse/templates/WU.template.md"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _write(path: Path, text: str) -> None:
|
|
30
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
31
|
+
path.write_text(text)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _write_events(path: Path, events: list[dict]) -> None:
|
|
35
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
path.write_text("\n".join(json.dumps(e) for e in events) + "\n")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _attempt_outcome(correlation_id: str, cost_usd) -> dict:
|
|
40
|
+
return {
|
|
41
|
+
"correlation_id": correlation_id,
|
|
42
|
+
"event_type": "attempt_outcome",
|
|
43
|
+
"payload": {"cost_usd": cost_usd},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TestFoldContract(unittest.TestCase):
|
|
48
|
+
def test_template_documents_cumulative_as_lifetime(self):
|
|
49
|
+
text = _CANONICAL_TEMPLATE.read_text()
|
|
50
|
+
self.assertIn("folded_through_re_arm", text)
|
|
51
|
+
self.assertIn("every re-arm", text)
|
|
52
|
+
self.assertIn(
|
|
53
|
+
"whose prior cycle cost nothing",
|
|
54
|
+
text,
|
|
55
|
+
"template must state cumulative_* folds even a zero-cost prior cycle",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def test_vendored_template_documents_cumulative_as_lifetime(self):
|
|
59
|
+
text = _VENDORED_TEMPLATE.read_text()
|
|
60
|
+
self.assertIn("folded_through_re_arm", text)
|
|
61
|
+
self.assertIn("every re-arm", text)
|
|
62
|
+
self.assertIn("whose prior cycle cost nothing", text)
|
|
63
|
+
|
|
64
|
+
def test_template_copies_are_byte_identical(self):
|
|
65
|
+
self.assertEqual(
|
|
66
|
+
_CANONICAL_TEMPLATE.read_bytes(),
|
|
67
|
+
_VENDORED_TEMPLATE.read_bytes(),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def test_cost_docstring_names_migration_not_two_live_shapes(self):
|
|
71
|
+
doc = cost.__doc__ or ""
|
|
72
|
+
self.assertIn("FEAT-2026-0067", doc)
|
|
73
|
+
self.assertIn("pre-migration", doc)
|
|
74
|
+
self.assertNotIn("fold-never-ran", doc)
|
|
75
|
+
self.assertNotIn("fold-ran", doc)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestLifetimeCostUnchanged(unittest.TestCase):
|
|
79
|
+
"""Criterion 5: same reader behaviour before and after this WU's edits."""
|
|
80
|
+
|
|
81
|
+
def setUp(self):
|
|
82
|
+
self._tmp = tempfile.TemporaryDirectory()
|
|
83
|
+
self.addCleanup(self._tmp.cleanup)
|
|
84
|
+
self.root = Path(self._tmp.name)
|
|
85
|
+
self.wu_path = self.root / "WU-07-x.md"
|
|
86
|
+
self.events_path = self.root / "events.jsonl"
|
|
87
|
+
|
|
88
|
+
def test_events_bearing_wu_unchanged(self):
|
|
89
|
+
_write(
|
|
90
|
+
self.wu_path,
|
|
91
|
+
"""---
|
|
92
|
+
id: FEAT-2026-0053/WU-07
|
|
93
|
+
type: implementation
|
|
94
|
+
status: done
|
|
95
|
+
cost_usd: 4.281823
|
|
96
|
+
---
|
|
97
|
+
body
|
|
98
|
+
""",
|
|
99
|
+
)
|
|
100
|
+
_write_events(
|
|
101
|
+
self.events_path,
|
|
102
|
+
[
|
|
103
|
+
_attempt_outcome("FEAT-2026-0053/WU-07", 1.0),
|
|
104
|
+
_attempt_outcome("FEAT-2026-0053/WU-07", 2.0),
|
|
105
|
+
],
|
|
106
|
+
)
|
|
107
|
+
result = wu_lifetime_cost_usd(self.wu_path, self.events_path)
|
|
108
|
+
self.assertAlmostEqual(result, 3.0, delta=0.01)
|
|
109
|
+
|
|
110
|
+
def test_frontmatter_only_wu_unchanged(self):
|
|
111
|
+
_write(
|
|
112
|
+
self.wu_path,
|
|
113
|
+
"""---
|
|
114
|
+
id: FEAT-2026-0099/WU-01
|
|
115
|
+
cost_usd: 1.5
|
|
116
|
+
cumulative_cost_usd: 2.5
|
|
117
|
+
---
|
|
118
|
+
body
|
|
119
|
+
""",
|
|
120
|
+
)
|
|
121
|
+
_write_events(self.events_path, [])
|
|
122
|
+
result = wu_lifetime_cost_usd(self.wu_path, self.events_path)
|
|
123
|
+
self.assertAlmostEqual(result, 4.0, delta=0.01)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
unittest.main()
|