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