specfuse-loop 0.3.21__tar.gz → 0.3.23__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.3.21/specfuse_loop.egg-info → specfuse_loop-0.3.23}/PKG-INFO +1 -1
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/pyproject.toml +1 -1
- specfuse_loop-0.3.23/specfuse/loop/data/VERSION +1 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/docs/methodology.md +12 -0
- specfuse_loop-0.3.23/specfuse/loop/data/rules/close-discipline.md +74 -0
- specfuse_loop-0.3.23/specfuse/loop/data/rules/planning-discipline.md +119 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/rules/result-contract.md +27 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/rules/verification-discipline.md +6 -0
- specfuse_loop-0.3.23/specfuse/loop/data/rules-local/README.md +43 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/templates/GATE.template.md +17 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/templates/PLAN.template.md +26 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/templates/WU.template.md +35 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/lint_plan.py +52 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/loop.py +12 -9
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/scaffold.py +84 -3
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23/specfuse_loop.egg-info}/PKG-INFO +1 -1
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse_loop.egg-info/SOURCES.txt +4 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_init_integration.py +5 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_close_intermediate.py +8 -0
- specfuse_loop-0.3.23/tests/test_lint_planning_sections.py +117 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_gate_budget.py +28 -4
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_data_in_sync.py +3 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_docs.py +7 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_init.py +5 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_resources.py +3 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_upgrade.py +89 -1
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_upgrade_integration.py +8 -0
- specfuse_loop-0.3.21/specfuse/loop/data/VERSION +0 -1
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/LICENSE +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/NOTICE +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/README.md +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/setup.cfg +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/__init__.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/_filelock.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/_miniyaml.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/adopt_feature.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/LEARNINGS.template.md +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/docs/getting-started.md +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/docs/skills.md +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/gitignore.snippet +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/roadmap.template.md +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/rules/correlation-ids.md +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/rules/never-touch.md +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/rules/security-boundaries.md +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/schemas/event.schema.json +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/verification.yml.example +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/gate_eval.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/gh_backend.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/gh_features.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/validate_event.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse_loop.egg-info/dependency_links.txt +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse_loop.egg-info/entry_points.txt +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse_loop.egg-info/requires.txt +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse_loop.egg-info/top_level.txt +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_adopt_feature.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_arm_gate_edits_uncommitted.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_attempt_outcome_emission.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_autoclose_deferral_visibility.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_autosync.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_autosync_consent.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_autosync_firstrun.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_autosync_plugin.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_backend.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_bash_routing.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_bookkeeping_commit_crash_run.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_bookkeeping_no_verify.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_claude_resolution.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_close_wu_self_reference.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_closing_deliverable_guard.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_console_encoding.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_cost_tracking.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_degraded_oracle.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_deliverable_presence_gate.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_doctor.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_driver_integration.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_driver_lock.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_duration_tracking.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_empty_files_escalation.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_ensure_feature_branch.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_ensure_feature_branch_base.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_events_redaction.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_extra_gates.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_filelock_portable.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_force_full_close.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_gate_eval.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_gate_eval_calibration.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_gate_eval_intermediate_wiring.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_gate_eval_terminal_wiring.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_gh_backend.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_gh_features.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_git_env_isolation.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_hashed_denylist.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_hashed_denylist_ci.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_interpreter_normalization.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_leak_findings_redaction.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_leak_scan.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_leak_scan_content.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_learnings_query.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_legacy_4wu_terminal_flips.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lifecycle_integration.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lifetime_accounting.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_bare_produces_path.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_close_wu.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_correlation_id.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_oracle_env.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_plan_errors.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_plan_next_draft.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_plan_relative_import.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_produces_driver_helper.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_sections.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_task_graph_yaml_selection.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_lint_verification_reference.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_attempt_evidence.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_auto_archive.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_caveman_preamble.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_close_intermediate.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_defaults_by_type.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_effort.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_failure_note_cap.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_files_changed_guard.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_model_alias.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_orchestration.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_post_pass_invariant.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_reset_preserving_events.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_smoke_runner.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_unsandboxed.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_loop_zero_token_guard.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_migrate_legacy.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_miniyaml_equivalence.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_miniyaml_negative.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_planned_cost_lint.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_prepare_feature.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_prepare_scaffold_sync.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_produces_field.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_produces_in_diff.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_redaction_windows_home.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_resolve_base.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_result_block.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_roadmap_add_skill.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_roadmap_archive_skill.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_roadmap_row_parser.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_doc_hygiene.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_manifest.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_seed_sanity.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_scaffold_wiring.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_skills_vendored_in_sync.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_spinning_rearm_gate.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_squash_commit_hook_crash.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_template_closing_shapes.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_terminal_flip_ownership.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_terminal_flips.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_timeout_kill_portable.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_untracked_feature_folder.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_untracked_file_absorption.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_upgrade_merge_gate.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_validate_event.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_verdict_coupling.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_verify_empty_gate_set.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_version_consistency.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_version_skew.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/tests/test_windows_gate_exec.py +0 -0
- {specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/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.3.
|
|
7
|
+
version = "0.3.23"
|
|
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 @@
|
|
|
1
|
+
0.3.23
|
|
@@ -147,6 +147,18 @@ WU's** frontmatter. The predicate keys on the implementation WUs' cost and
|
|
|
147
147
|
plan-conformance and cannot see what a close verifies, so a substantive close
|
|
148
148
|
must be marked must-run or its verification is silently skipped (#189).
|
|
149
149
|
|
|
150
|
+
**Hedged verdicts leave a trail** — a close that ends `met_locally` (everything
|
|
151
|
+
locally verifiable passed; something structurally could not be verified here)
|
|
152
|
+
leaves every terminal surface un-flipped (driver >= 0.3.21: gate
|
|
153
|
+
`awaiting_review`, roadmap row `active`, PLAN.md `active`) AND must produce a
|
|
154
|
+
follow-up record — in the gate review or `RETROSPECTIVE.md` — naming each
|
|
155
|
+
unmet criterion, why it was unverifiable in this environment, and the exact
|
|
156
|
+
re-run condition that would upgrade the verdict to `met`. Without the record,
|
|
157
|
+
`met_locally` is a dead end; with it, the feature is resumable by anyone. See
|
|
158
|
+
`.specfuse/rules/close-discipline.md` §2 for the record's shape and the other
|
|
159
|
+
close-time obligations (fresh oracle re-runs, consumer-visible-change
|
|
160
|
+
enumeration).
|
|
161
|
+
|
|
150
162
|
**Predicate-version transparency** — every `auto_close_decision` event in
|
|
151
163
|
`events.jsonl` carries a `predicate_version` field (e.g., `predicate_version:
|
|
152
164
|
v1`). Future revisions to the predicate constants increment this version, so
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Copyright 2026 Specfuse Contributors
|
|
3
|
+
Licensed under the Apache License, Version 2.0. See LICENSE.
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Rule: close discipline
|
|
7
|
+
|
|
8
|
+
Three obligations for `close` and `close-intermediate` WUs, binding at close
|
|
9
|
+
time. Sibling of [`planning-discipline.md`](planning-discipline.md) (plan/arm
|
|
10
|
+
time) and of the per-WU closing obligations in
|
|
11
|
+
[`result-contract.md`](result-contract.md). Provenance: FEAT-2026-0049
|
|
12
|
+
(the specfuse-generator dogfood), where three gates auto-closed at
|
|
13
|
+
`attempts: 0` and the one close forced to run caught two false-`done` WUs and
|
|
14
|
+
an unsatisfiable predicate — 48% of the feature's spend landed after the
|
|
15
|
+
terminal gate had already "auto-closed". A close that only writes prose
|
|
16
|
+
verifies nothing.
|
|
17
|
+
|
|
18
|
+
A close whose acceptance criteria include ANY obligation below is
|
|
19
|
+
**load-bearing**: the plan author sets `auto_close_disabled: true` in that
|
|
20
|
+
WU's frontmatter so the auto-close predicate cannot optimize it away (#189).
|
|
21
|
+
|
|
22
|
+
## 1. Oracles re-run fresh
|
|
23
|
+
|
|
24
|
+
The close re-runs every oracle the feature's acceptance criteria name — the
|
|
25
|
+
full test command(s), plus any compile/execution gates — fresh, in this
|
|
26
|
+
session, exit codes read directly. Never inherit a producing WU's
|
|
27
|
+
self-report: `done` is a claim, the re-run is the verification. When the
|
|
28
|
+
feature's criteria assert on generated artifacts, regenerate into a clean
|
|
29
|
+
output directory before asserting — stale output satisfies any assertion.
|
|
30
|
+
|
|
31
|
+
> **Provenance.** A WU reported `done` while its source was untouched and its
|
|
32
|
+
> oracle never ran; the driver-side produces-vs-diff guard now refuses that
|
|
33
|
+
> pass (specfuse-loop >= 0.3.21), but only the close's own fresh re-run
|
|
34
|
+
> catches the composite: all WUs individually green while the feature-level
|
|
35
|
+
> oracle fails.
|
|
36
|
+
|
|
37
|
+
## 2. Hedged-verdict follow-up record
|
|
38
|
+
|
|
39
|
+
On `met_locally`, the close must produce a named record — in the gate review
|
|
40
|
+
or `RETROSPECTIVE.md` — with one entry per unmet criterion:
|
|
41
|
+
|
|
42
|
+
- the criterion, verbatim;
|
|
43
|
+
- why it is unverifiable in this environment;
|
|
44
|
+
- the exact re-run condition that would upgrade the verdict to `met`.
|
|
45
|
+
|
|
46
|
+
The driver (>= 0.3.21) already guarantees the surfaces stay un-flipped on a
|
|
47
|
+
hedged verdict (gate `awaiting_review`, roadmap `active`, PLAN `active`);
|
|
48
|
+
this record is the other half: without it, `met_locally` is a dead end —
|
|
49
|
+
no artifact says what would make it `met`, and the honest hedged verdict
|
|
50
|
+
degrades into a polite synonym for "unknown".
|
|
51
|
+
|
|
52
|
+
## 3. Consumer-visible contract changes enumerated, human-acknowledged
|
|
53
|
+
|
|
54
|
+
The close enumerates every consumer-visible addition, removal, or rename the
|
|
55
|
+
feature makes across ALL its producing WUs — API surface, generated models,
|
|
56
|
+
published schemas, CLI flags, whatever contract consumers depend on — and
|
|
57
|
+
blocks on explicit human acknowledgment of the list. A feature with no such
|
|
58
|
+
changes writes exactly: `n/a — no consumer-visible contract change` (do not
|
|
59
|
+
fabricate an empty enumeration; the n/a line is the reviewed claim).
|
|
60
|
+
|
|
61
|
+
A close carrying this obligation is always load-bearing
|
|
62
|
+
(`auto_close_disabled: true`).
|
|
63
|
+
|
|
64
|
+
> **Provenance.** A breaking removal from a generated model survived four
|
|
65
|
+
> gates unreviewed because no close surface owned the breaking-change list.
|
|
66
|
+
> A silent breaking change is the most expensive false-done, and it is not
|
|
67
|
+
> language-specific — any generated or published contract has this surface.
|
|
68
|
+
|
|
69
|
+
## Split with project-local rules
|
|
70
|
+
|
|
71
|
+
These are the generic obligations. The concrete grounding — which command is
|
|
72
|
+
the oracle, which surface is "the API", project-specific regeneration
|
|
73
|
+
hazards — is per-project and belongs in that project's
|
|
74
|
+
`.specfuse/rules-local/` (never touched by upgrade), referencing this rule.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Copyright 2026 Specfuse Contributors
|
|
3
|
+
Licensed under the Apache License, Version 2.0. See LICENSE.
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Rule: planning discipline
|
|
7
|
+
|
|
8
|
+
Encodes hard-won discipline from feature retrospectives — the checks that, applied
|
|
9
|
+
at design and arming time, would have prevented the most expensive re-plans. Each
|
|
10
|
+
check below is cheap (a grep, a sentence, a table, one local run) and guards a
|
|
11
|
+
failure mode that cost multiple gates when skipped. Provenance is named per check so
|
|
12
|
+
the reasoning survives the rule (the FEAT-2026-0049 / F1–F4 references resolve in the
|
|
13
|
+
retrospective of the specfuse-generator dogfood feature that motivated these checks).
|
|
14
|
+
|
|
15
|
+
These are **binding at draft, plan-next, and arm time**. A gate armed in violation
|
|
16
|
+
of any check is not ready, regardless of what a WU's frontmatter says.
|
|
17
|
+
|
|
18
|
+
## 1. Existing-mechanism search — before designing any enforcement or measurement
|
|
19
|
+
|
|
20
|
+
Before an ADR designs a validation rule, a severity level, an enforcement gate, or a
|
|
21
|
+
measurement, it must first establish that the mechanism does not already exist. The
|
|
22
|
+
ADR is **incomplete** without this section.
|
|
23
|
+
|
|
24
|
+
- **Grep the inventory.** Search the directory that holds the mechanism's
|
|
25
|
+
siblings — for a validation rule, the project's rules directory (e.g.
|
|
26
|
+
`grep -rl <concept> <path/to/validation/rules/>`); for an artifact, the
|
|
27
|
+
tool's own inventory command (e.g. `<tool> templates`); for any other
|
|
28
|
+
check, the directory that holds its siblings.
|
|
29
|
+
- **Record the grep command and its verdict** in the ADR — the exact command, and
|
|
30
|
+
either "no existing mechanism, building new" or "found `<X>`, reusing / extending
|
|
31
|
+
it". A verdict of "reusing" that then builds new anyway must say why.
|
|
32
|
+
- **Read the javadoc/description of anything the grep surfaces.** A rule that already
|
|
33
|
+
covers the property may say so in its own words.
|
|
34
|
+
|
|
35
|
+
> **Provenance (FEAT-2026-0049 / F3).** The feature spent three gates building a
|
|
36
|
+
> `WARNING→ERROR` ladder on a new rule while the enforcement it needed already existed
|
|
37
|
+
> in `RelationshipSymmetryValidationRule` — whose javadoc (F-029, 2026-05-29) literally
|
|
38
|
+
> said the debt "surfaces as a Saturday-night surprise when the inference rule changes."
|
|
39
|
+
> A one-line grep of the rules directory at ADR time would have found it. Cost: two
|
|
40
|
+
> gates and a full pivot.
|
|
41
|
+
|
|
42
|
+
## 2. Escalation-predicate satisfiability — for any severity flip
|
|
43
|
+
|
|
44
|
+
Any plan that raises a check to `ERROR`, flips a `WARNING` to blocking, or asserts a
|
|
45
|
+
"zero issues" close predicate must answer, in writing, one question:
|
|
46
|
+
|
|
47
|
+
> **What does this rule report on a spec/input already in its intended final state?**
|
|
48
|
+
|
|
49
|
+
If the answer is anything other than **zero**, the predicate is **unsatisfiable** —
|
|
50
|
+
the rule fires on correct inputs, so "zero issues" can never be reached and the
|
|
51
|
+
severity flip forces wrong changes. **Stop and redesign** before arming: fix the rule
|
|
52
|
+
so a correct input reports zero, or route enforcement to a different mechanism (see §1).
|
|
53
|
+
|
|
54
|
+
> **Provenance (FEAT-2026-0049 / F3).** ADR D3 ("omission = intent — a non-owning
|
|
55
|
+
> reference is declared by omission") and D6 ("`ERROR` when the rule reports zero
|
|
56
|
+
> issues") contradicted each other inside the same document: post-flip, every correct
|
|
57
|
+
> omission still tripped the rule, so "zero" was unreachable. The contradiction was
|
|
58
|
+
> answerable by reading the ADR against itself. Separately, the *shipped* rule
|
|
59
|
+
> false-positived ~35 of 49 on real specs (#821) — a severity flip forcing wrong
|
|
60
|
+
> changes, exactly what this check prevents.
|
|
61
|
+
|
|
62
|
+
## 3. Flag-scope table — for any WU introducing or flipping a behavior flag
|
|
63
|
+
|
|
64
|
+
A WU that introduces, gates on, or flips a behavior flag must contain a **flag-scope
|
|
65
|
+
table**: every code path the flag is claimed to affect, marked *gated* / *not gated*,
|
|
66
|
+
with a one-line *why* per row. The arming review checks the feature's headline claim
|
|
67
|
+
("the flip retires inference", "the flag makes X strict") against the table — a claim
|
|
68
|
+
the table does not support is a scope mismatch, and scope mismatches surface gates
|
|
69
|
+
later as defects, not at arming.
|
|
70
|
+
|
|
71
|
+
| Code path | Gated by flag? | Why |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `<path/method>` | yes / no | <one line> |
|
|
74
|
+
|
|
75
|
+
> **Provenance (FEAT-2026-0049 / F1).** The plan enumerated both inference paths
|
|
76
|
+
> (bare-FK and `$ref`) on day one, but no document ever crossed "which paths does the
|
|
77
|
+
> `explicitRelationshipsOnly` flag gate" against "the flip retires inference." The flag
|
|
78
|
+
> was built FK-path-only, deliberately and correctly — but the mismatch with the
|
|
79
|
+
> headline claim surfaced two gates later (F1), and again as a runtime defect (F4).
|
|
80
|
+
|
|
81
|
+
## 4. Runtime probe before arming a default/severity flip
|
|
82
|
+
|
|
83
|
+
A gate whose WUs flip a **default value** or a **severity** may **not** be armed on
|
|
84
|
+
"mechanical, nothing design-open." Before arming, apply the change locally and run the
|
|
85
|
+
**exact command the WU's tests gate will run** (the full oracle, not a subset), and
|
|
86
|
+
paste the resulting failure list into the gate review. The probe is the arming
|
|
87
|
+
evidence; "nothing design-open" without it is a claim, not a fact.
|
|
88
|
+
|
|
89
|
+
- Run the *full* oracle. A subset hides failures in the paths the change touches that
|
|
90
|
+
the subset does not exercise.
|
|
91
|
+
- The probe's failure list becomes the WU's enumerated test surface — the WU no longer
|
|
92
|
+
discovers its own breakage attempt by attempt.
|
|
93
|
+
|
|
94
|
+
> **Provenance (FEAT-2026-0049 / F4).** Gate 4 was armed on "nothing is design-open"
|
|
95
|
+
> without ever applying the default flip and running the full suite once. The WU then
|
|
96
|
+
> spun three times (~$14) on a real defect that one local run exposed in seconds — and
|
|
97
|
+
> the first two diagnoses were made against a *subset*, missing failures the full suite
|
|
98
|
+
> showed. Contrast: an earlier gate's "measure before drafting" probe (a `WARNING` run
|
|
99
|
+
> over the real specs) worked and correctly shrank the next WU's scope.
|
|
100
|
+
|
|
101
|
+
## 5. Planning-WU cost floor
|
|
102
|
+
|
|
103
|
+
`plan-next` and `close` / `close-intermediate` WUs are systematically under-estimated.
|
|
104
|
+
When drafting their `planned_cost_usd`, use a **floor of $5.00** — never the $2–3 that
|
|
105
|
+
"it's just bookkeeping" suggests. A gate `cost_budget_usd` set to the sum of $2–3
|
|
106
|
+
planning estimates is a brake that fires by construction on the first real close.
|
|
107
|
+
|
|
108
|
+
> **Provenance (FEAT-2026-0049 + LEARNINGS [FEAT-2026-0044]).** Planning/close WUs ran
|
|
109
|
+
> 2.8–5.2× their $2–3 estimates: `plan-next` at $5.90 and $15.65, `close-intermediate`
|
|
110
|
+
> at $5.67, against $2–3 drafts. LEARNINGS `[FEAT-2026-0044]` had already predicted this
|
|
111
|
+
> systematic under-estimation; the floor makes the prediction binding.
|
|
112
|
+
|
|
113
|
+
## Why these live together
|
|
114
|
+
|
|
115
|
+
Each is a one-time check that trades a minute at design/arm time against a gate (or
|
|
116
|
+
three) of rework. They share a root: **automation and authors both trust a claim
|
|
117
|
+
without the cheap check that would test it** — a grep not run (§1), a contradiction not
|
|
118
|
+
read (§2), a scope not tabled (§3), a suite not run (§4), an estimate not calibrated
|
|
119
|
+
(§5). State the check, run it, record the verdict. Every plan, every arm.
|
|
@@ -107,6 +107,33 @@ blocked_reason: <present only when status is blocked>
|
|
|
107
107
|
5. **No secret-looking values in evidence.** The RESULT block is read by the driver
|
|
108
108
|
and may be archived. See [`security-boundaries.md`](security-boundaries.md).
|
|
109
109
|
|
|
110
|
+
## Closing obligations for implementation WUs (FEAT-2026-0049)
|
|
111
|
+
|
|
112
|
+
Four binding obligations before `status: complete`. Each guards a way a prior WU
|
|
113
|
+
closed on nothing:
|
|
114
|
+
|
|
115
|
+
1. **Diff against `produces:` first.** Every path in the WU's `produces:` list must
|
|
116
|
+
show a working-tree change, or the RESULT must justify each unchanged path
|
|
117
|
+
explicitly: "no change needed because X; here is the evidence the deliverable
|
|
118
|
+
already holds: `<command + output>`." Silence on an unchanged deliverable is not
|
|
119
|
+
a valid close. (The driver enforces this mechanically since specfuse-loop 0.3.21
|
|
120
|
+
— #198, outcome `produces_not_in_diff`; the obligation binds regardless of
|
|
121
|
+
driver version.)
|
|
122
|
+
2. **A plan-level contradiction is `blocked`, not `complete`.** If mid-WU you find
|
|
123
|
+
the plan cannot be delivered as written (a decision contradicts another, a
|
|
124
|
+
deliverable cannot exist), emit `status: blocked` with the finding in
|
|
125
|
+
`blocked_reason`. Never write the finding into a gate document and close
|
|
126
|
+
`complete`. `complete` means the deliverables exist; `blocked` means they cannot
|
|
127
|
+
or should not.
|
|
128
|
+
3. **Every `evidence:` cites an executed command** — its observed exit code/output.
|
|
129
|
+
Reading source, grepping for a string, or citing another WU's RESULT is not
|
|
130
|
+
verification. For a validation-rule or severity claim the evidence must include a
|
|
131
|
+
negative observation (the rule seen rejecting a purpose-built bad input); see
|
|
132
|
+
[`verification-discipline.md`](verification-discipline.md) §3.
|
|
133
|
+
4. **Analysis without edits is not a silent attempt.** If an attempt produces
|
|
134
|
+
analysis but no file edits, say so explicitly and end `blocked` (or with a
|
|
135
|
+
concrete next-edit plan). Do not exhaust the attempt on prose.
|
|
136
|
+
|
|
110
137
|
## Why this matters
|
|
111
138
|
|
|
112
139
|
The driver's trust model is that the RESULT block is an honest claim about what
|
{specfuse_loop-0.3.21 → specfuse_loop-0.3.23}/specfuse/loop/data/rules/verification-discipline.md
RENAMED
|
@@ -39,6 +39,12 @@ Two generic meanings, both apply unless the unit narrows the scope:
|
|
|
39
39
|
decide done. Run them yourself first, in declared order, with full output.
|
|
40
40
|
Generic verifications — "I assume the tests still pass" — are not verifications.
|
|
41
41
|
The commands are the verification.
|
|
42
|
+
- **Observe behavior, not source.** A claim that code does X is verified by making
|
|
43
|
+
X happen and reading the output — never by reading the source that looks like it
|
|
44
|
+
does X. A rule read as `ERROR` in source may be gated off and never fire; only a
|
|
45
|
+
run proves it. For any rule or severity claim, include a **negative observation**:
|
|
46
|
+
the rule seen rejecting a purpose-built bad input. A rule that fires on nothing
|
|
47
|
+
passes every test.
|
|
42
48
|
|
|
43
49
|
**Verification is the exit oracle.** The agent's own report is advisory; done is
|
|
44
50
|
decided by the checks passing, not by the agent's belief. If a check fails you are
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Copyright 2026 Specfuse Contributors
|
|
3
|
+
Licensed under the Apache License, Version 2.0. See LICENSE.
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Project-authored rules (`rules-local/`)
|
|
7
|
+
|
|
8
|
+
This directory belongs to **your project**, not to Specfuse. Put binding rules
|
|
9
|
+
here that are specific to this repository — its tools, its paths, its failure
|
|
10
|
+
modes — and that should survive every scaffold upgrade.
|
|
11
|
+
|
|
12
|
+
**The contract:** `specfuse upgrade` never writes, overwrites, or prunes
|
|
13
|
+
anything in `rules-local/`. That is the opposite of the sibling
|
|
14
|
+
`.specfuse/rules/` directory, which is versioned: upgrades overwrite its files
|
|
15
|
+
with the shipped versions and delete files the shipped scaffold no longer
|
|
16
|
+
carries. A project rule placed in `rules/` will be lost on upgrade; the same
|
|
17
|
+
rule placed here is permanent.
|
|
18
|
+
|
|
19
|
+
## When to write a rule here vs. upstream
|
|
20
|
+
|
|
21
|
+
- **Here:** the rule names your project's directories, commands, tools, or
|
|
22
|
+
conventions ("grep `src/main/java/.../validation/rules/` before designing a
|
|
23
|
+
rule", "the generator jar's `templates` command lists artifacts"). Upstream
|
|
24
|
+
would have to genericize away exactly the specifics that make it useful to
|
|
25
|
+
you.
|
|
26
|
+
- **Upstream (a PR to the scaffold's `rules/`):** the failure mode is
|
|
27
|
+
project-agnostic and other Specfuse projects would hit it too. Keep the
|
|
28
|
+
provenance; genericize the examples.
|
|
29
|
+
|
|
30
|
+
## Wiring a rule into the loop
|
|
31
|
+
|
|
32
|
+
Rules load via the `@`-references in your `.claude/CLAUDE.md` Specfuse rules
|
|
33
|
+
block. Add one line per rule you author:
|
|
34
|
+
|
|
35
|
+
```markdown
|
|
36
|
+
@.specfuse/rules-local/<your-rule>.md
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Follow the shipped rules' format: one failure mode per rule, the check stated
|
|
40
|
+
imperatively, provenance recorded so the reasoning survives the rule (see
|
|
41
|
+
`.specfuse/rules/planning-discipline.md` for the model).
|
|
42
|
+
|
|
43
|
+
This README is seeded once and never overwritten — edit it freely.
|
|
@@ -30,6 +30,23 @@ here for human review-and-arm: read the review artifact, accept or edit the draf
|
|
|
30
30
|
next-gate work units, flip the accepted ones to `pending`, set this gate's status to
|
|
31
31
|
`passed`, and re-run.
|
|
32
32
|
|
|
33
|
+
## Arming discipline (see `.specfuse/rules/planning-discipline.md`)
|
|
34
|
+
|
|
35
|
+
Before flipping this gate's next-gate WUs to `pending`:
|
|
36
|
+
|
|
37
|
+
- **Runtime probe for a default/severity flip (§4).** If any WU in the gate being armed
|
|
38
|
+
flips a **default value** or a **severity**, it may **not** be armed on "mechanical,
|
|
39
|
+
nothing design-open." Apply the change locally, run the **exact command the WU's tests
|
|
40
|
+
gate will run** (the full oracle, not a subset), and paste the failure list into
|
|
41
|
+
`GATE-NN-REVIEW.md`. That list becomes the WU's enumerated test surface. Arming such a
|
|
42
|
+
gate without a probe is what makes an implementation WU spin on a defect one local run
|
|
43
|
+
would have shown.
|
|
44
|
+
- **Flag-scope table (§3).** If a WU introduces or flips a behavior flag, confirm it
|
|
45
|
+
carries a flag-scope table and that the feature's headline claim matches it.
|
|
46
|
+
- **Escalation-predicate satisfiability (§2).** If a WU raises a check to `ERROR` or
|
|
47
|
+
asserts a "zero issues" predicate, confirm PLAN.md answered "what does the rule report
|
|
48
|
+
on a correct input" and that the answer is zero.
|
|
49
|
+
|
|
33
50
|
## Reflection notes
|
|
34
51
|
|
|
35
52
|
<Written by the human at review time. What surprised you, what you changed in the
|
|
@@ -23,6 +23,32 @@ each WU file owns its own status, and each GATE file owns its gate's status. Det
|
|
|
23
23
|
only as far as the next gate; plan-next drafts the gate after that from the
|
|
24
24
|
retrospective and lessons.
|
|
25
25
|
|
|
26
|
+
## Existing-mechanism search (mandatory — see `.specfuse/rules/planning-discipline.md` §1)
|
|
27
|
+
|
|
28
|
+
Before any ADR or gate designs a validation rule, a severity level, an enforcement
|
|
29
|
+
gate, or a measurement, establish that it does not already exist. **An ADR without
|
|
30
|
+
this section is incomplete.** If this feature designs no such mechanism, write
|
|
31
|
+
exactly `n/a — no enforcement or measurement designed` and stop — do not fabricate
|
|
32
|
+
a grep to fill the section.
|
|
33
|
+
|
|
34
|
+
- **Grep command run:** `<the exact command — e.g. grep -rl <concept>
|
|
35
|
+
src/main/java/.../validation/rules/>`
|
|
36
|
+
- **Verdict:** `<"no existing mechanism, building new" | "found <X>, reusing/extending">`
|
|
37
|
+
- **If reusing:** name `<X>` and quote the line of its javadoc/description that
|
|
38
|
+
confirms it covers the property. If building new despite a hit, say why the hit does
|
|
39
|
+
not suffice.
|
|
40
|
+
|
|
41
|
+
## Escalation-predicate satisfiability (mandatory for any severity flip — §2)
|
|
42
|
+
|
|
43
|
+
If this feature raises a check to `ERROR`, flips a `WARNING` to blocking, or asserts a
|
|
44
|
+
"zero issues" close predicate, answer here:
|
|
45
|
+
|
|
46
|
+
- **What does the rule report on an input already in its intended final state?**
|
|
47
|
+
`<answer>`
|
|
48
|
+
- If that answer is **not zero**, the predicate is unsatisfiable — the rule fires on
|
|
49
|
+
correct inputs. **Redesign before drafting gates:** fix the rule so a correct input
|
|
50
|
+
reports zero, or route enforcement to the mechanism found in the search above.
|
|
51
|
+
|
|
26
52
|
## Task graph
|
|
27
53
|
|
|
28
54
|
```yaml
|
|
@@ -28,6 +28,10 @@ AUTHOR-SET FIELDS — fill or override these at draft/arm time:
|
|
|
28
28
|
`xhigh` | `max`. Absent → type-keyed default. `low`/`medium` add a terseness directive.
|
|
29
29
|
- `planned_cost_usd` — OPTIONAL. Estimated USD at draft time. Compared against actual in close
|
|
30
30
|
WU's cost analysis. Lint WARN when absent on active/draft WUs (non-blocking).
|
|
31
|
+
**Planning-WU cost floor (`.specfuse/rules/planning-discipline.md` §5):** for `plan-next`,
|
|
32
|
+
`close`, and `close-intermediate` WUs, draft `planned_cost_usd` at a **floor of $5.00** — these
|
|
33
|
+
run 2.8–5.2× the $2–3 that "it's just bookkeeping" suggests. A gate `cost_budget_usd` set to
|
|
34
|
+
the sum of $2–3 planning estimates is a brake that fires by construction on the first real close.
|
|
31
35
|
- `generated_surfaces` — OPTIONAL. Paths to generated files this unit's acceptance depends on.
|
|
32
36
|
- `oracle_env` — OPTIONAL. Environment the verifying oracle runs in: `macos_local`,
|
|
33
37
|
`linux_docker`, `github_actions_ci`, or an operator-named string. Lint WARN when AC mentions
|
|
@@ -77,6 +81,37 @@ cheapest hollow-pass guard; see `/authoring-work-units` §12 for the contract an
|
|
|
77
81
|
the carve-outs (refactor, migration, pure-data → explicit `Red-test exempt:
|
|
78
82
|
<reason>` line in the WU body).
|
|
79
83
|
|
|
84
|
+
**Flag-scope table** (REQUIRED only when this WU introduces, gates on, or flips a
|
|
85
|
+
behavior flag; omit otherwise — see `.specfuse/rules/planning-discipline.md` §3). Every
|
|
86
|
+
code path the flag is claimed to affect, marked gated / not gated, with a one-line why.
|
|
87
|
+
The arming review checks the feature's headline claim ("the flip retires X") against
|
|
88
|
+
this table; a claim the table does not support is a scope mismatch that surfaces as a
|
|
89
|
+
defect gates later.
|
|
90
|
+
|
|
91
|
+
| Code path | Gated by flag? | Why |
|
|
92
|
+
|---|---|---|
|
|
93
|
+
| `<path/method>` | yes / no | <one line> |
|
|
94
|
+
|
|
95
|
+
**Close obligations** (REQUIRED only for `close` / `close-intermediate` WUs; omit
|
|
96
|
+
otherwise — see `.specfuse/rules/close-discipline.md`). The close's acceptance
|
|
97
|
+
criteria must cover:
|
|
98
|
+
|
|
99
|
+
1. **Oracles re-run fresh** (§1): every oracle the feature's criteria name,
|
|
100
|
+
full command(s), exit codes read directly — never a producing WU's
|
|
101
|
+
self-report; regenerate into a clean dir before asserting on generated
|
|
102
|
+
artifacts.
|
|
103
|
+
2. **Hedged follow-up record** (§2): on `met_locally`, a named record per
|
|
104
|
+
unmet criterion — criterion, why unverifiable here, exact re-run condition
|
|
105
|
+
that upgrades it to `met`.
|
|
106
|
+
3. **Consumer-visible contract changes** (§3): enumerate every addition /
|
|
107
|
+
removal / rename across the feature's producing WUs and block on human
|
|
108
|
+
acknowledgment, or write exactly `n/a — no consumer-visible contract
|
|
109
|
+
change`.
|
|
110
|
+
|
|
111
|
+
A close whose criteria include any of these is load-bearing — set
|
|
112
|
+
`auto_close_disabled: true` in this WU's frontmatter so the auto-close
|
|
113
|
+
predicate cannot skip it.
|
|
114
|
+
|
|
80
115
|
**Do not touch.** Generated directories (`_generated/`, `gen-src/`, or the repo's
|
|
81
116
|
declared equivalent), files owned by other work units in this gate, secrets,
|
|
82
117
|
`.git/`. The driver owns all git operations — you edit files only. See
|
|
@@ -335,6 +335,56 @@ def check_planned_cost(feature_dir: Path, plan_fm: dict, gates: list) -> None:
|
|
|
335
335
|
)
|
|
336
336
|
|
|
337
337
|
|
|
338
|
+
# Planning-discipline section names (#201, second half). Canonical since the
|
|
339
|
+
# FEAT-2026-0049 planning-discipline drop (PR #211): PLAN.md carries the two
|
|
340
|
+
# ADR sections; each gate file carries the arming-discipline section. Names
|
|
341
|
+
# are shared vocabulary with downstream planning tooling — do not rename here
|
|
342
|
+
# without a template/coordination change.
|
|
343
|
+
PLAN_DISCIPLINE_SECTIONS = (
|
|
344
|
+
"Existing-mechanism search",
|
|
345
|
+
"Escalation-predicate satisfiability",
|
|
346
|
+
)
|
|
347
|
+
GATE_ARMING_SECTION = "Arming discipline"
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def check_planning_sections(
|
|
351
|
+
feature_dir: Path, plan_fm: dict, plan_body: str, gates: list,
|
|
352
|
+
) -> None:
|
|
353
|
+
"""WARN when the planning-discipline sections are absent (#201).
|
|
354
|
+
|
|
355
|
+
Presence-only: the PLAN template's sections allow an explicit n/a line, so
|
|
356
|
+
content is the arming reviewer's job — this check only catches the section
|
|
357
|
+
never being written at all (pre-0.3.22 drafts, or a plan-next that dropped
|
|
358
|
+
it). Sealed features (plan status done/abandoned) are history and skipped.
|
|
359
|
+
WARN-only, exit code 0, like the rest of the planning-lint family.
|
|
360
|
+
"""
|
|
361
|
+
if plan_fm.get("status") in ("done", "abandoned"):
|
|
362
|
+
return
|
|
363
|
+
for section in PLAN_DISCIPLINE_SECTIONS:
|
|
364
|
+
if not _slice_section(plan_body, section).strip():
|
|
365
|
+
print(
|
|
366
|
+
f"WARN: {feature_dir}/PLAN.md: missing '{section}' section "
|
|
367
|
+
f"(planning-discipline; write it or an explicit 'n/a' line). "
|
|
368
|
+
f"See .specfuse/rules/planning-discipline.md."
|
|
369
|
+
)
|
|
370
|
+
for g in gates:
|
|
371
|
+
gate_file = g.get("file")
|
|
372
|
+
if not gate_file:
|
|
373
|
+
continue
|
|
374
|
+
gate_path = feature_dir / gate_file
|
|
375
|
+
if not gate_path.exists():
|
|
376
|
+
continue
|
|
377
|
+
gfm, gbody = read_frontmatter(gate_path)
|
|
378
|
+
if gfm.get("status") == "passed":
|
|
379
|
+
continue
|
|
380
|
+
if not _slice_section(gbody, GATE_ARMING_SECTION).strip():
|
|
381
|
+
print(
|
|
382
|
+
f"WARN: {gate_file}: missing '{GATE_ARMING_SECTION}' section "
|
|
383
|
+
f"(runtime-probe / flag-scope / predicate checks before "
|
|
384
|
+
f"arming). See .specfuse/rules/planning-discipline.md."
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
338
388
|
def lint(feature_dir: Path) -> list[str]:
|
|
339
389
|
errs: list[str] = []
|
|
340
390
|
plan = feature_dir / "PLAN.md"
|
|
@@ -623,6 +673,8 @@ def lint(feature_dir: Path) -> list[str]:
|
|
|
623
673
|
|
|
624
674
|
# Planned-cost capture: WARN on missing/divergent planned_cost_usd fields.
|
|
625
675
|
check_planned_cost(feature_dir, fm, gates)
|
|
676
|
+
# Planning-discipline section presence (#201): WARN-only.
|
|
677
|
+
check_planning_sections(feature_dir, fm, body, gates)
|
|
626
678
|
|
|
627
679
|
# Cross-gate mixed-shape check. Two directions of mix:
|
|
628
680
|
#
|
|
@@ -64,7 +64,7 @@ SPECFUSE_DIR = Path(".specfuse")
|
|
|
64
64
|
REPO_ROOT = SPECFUSE_DIR.parent
|
|
65
65
|
FEATURES_DIR = SPECFUSE_DIR / "features"
|
|
66
66
|
VERIFICATION_PATH = SPECFUSE_DIR / "verification.yml"
|
|
67
|
-
DRIVER_VERSION = "0.3.
|
|
67
|
+
DRIVER_VERSION = "0.3.23"
|
|
68
68
|
# Oldest scaffold layout this driver can drive. init.sh stamps the scaffold's own
|
|
69
69
|
# version into `.specfuse/VERSION`; check_scaffold_version() fails loud at startup if
|
|
70
70
|
# the consumer's scaffold is older than this, pointing at `specfuse upgrade`. Bump
|
|
@@ -1537,15 +1537,20 @@ def gate_budget_usd(gate_file: Path) -> float | None:
|
|
|
1537
1537
|
|
|
1538
1538
|
|
|
1539
1539
|
def gate_spent_usd(plan: dict, gate: dict, feature_dir: Path) -> float:
|
|
1540
|
-
"""Sum lifetime cost across the gate's
|
|
1540
|
+
"""Sum lifetime recorded cost across ALL of the gate's WUs.
|
|
1541
1541
|
|
|
1542
1542
|
Reads each WU file's frontmatter from `gate["work_units"]` and adds
|
|
1543
1543
|
`cost_usd` PLUS `cumulative_cost_usd` (prior re-arm cycles' spend, folded
|
|
1544
|
-
by fold_cumulative_on_rearm — #199)
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1544
|
+
by fold_cumulative_on_rearm — #199) regardless of WU status (#219):
|
|
1545
|
+
write_cost_to_wu records cost on every outcome — pass, blocked, spinning —
|
|
1546
|
+
so cost on a non-done WU is real spend, not an estimate. The pre-#219
|
|
1547
|
+
done-only filter left a blocked WU's re-arm burn invisible to the budget
|
|
1548
|
+
brake until the WU finally passed (FEAT-2026-0049/WU-06: $30.29 across 9
|
|
1549
|
+
attempts, contributing $0 while blocked). WUs whose frontmatter omits both
|
|
1550
|
+
fields — cost tracking off, never dispatched, or the attempt didn't record
|
|
1551
|
+
a cost — contribute 0.0. `plan` is the feature frontmatter dict and is
|
|
1552
|
+
accepted for signature symmetry with the broader gate-budget helpers; the
|
|
1553
|
+
spent total is derived from WU files alone.
|
|
1549
1554
|
"""
|
|
1550
1555
|
del plan # signature symmetry — sum is derived from WU files only
|
|
1551
1556
|
total = 0.0
|
|
@@ -1557,8 +1562,6 @@ def gate_spent_usd(plan: dict, gate: dict, feature_dir: Path) -> float:
|
|
|
1557
1562
|
if not wu_path.exists():
|
|
1558
1563
|
continue
|
|
1559
1564
|
fm, _ = read_frontmatter(wu_path)
|
|
1560
|
-
if fm.get("status") != "done":
|
|
1561
|
-
continue
|
|
1562
1565
|
# Lifetime spend, not per-cycle (#199): fold_cumulative_on_rearm
|
|
1563
1566
|
# moves each prior dispatch cycle's cost into cumulative_cost_usd and
|
|
1564
1567
|
# zeroes cost_usd, so summing cost_usd alone undercounts every
|