specfuse-loop 0.3.3__tar.gz → 0.3.5__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.3/specfuse_loop.egg-info → specfuse_loop-0.3.5}/PKG-INFO +1 -1
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/pyproject.toml +1 -1
- specfuse_loop-0.3.5/specfuse/loop/data/VERSION +1 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/loop.py +33 -10
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5/specfuse_loop.egg-info}/PKG-INFO +1 -1
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse_loop.egg-info/SOURCES.txt +1 -0
- specfuse_loop-0.3.5/tests/test_lint_plan_relative_import.py +57 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_orchestration.py +2 -2
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_prepare_feature.py +39 -1
- specfuse_loop-0.3.3/specfuse/loop/data/VERSION +0 -1
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/LICENSE +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/NOTICE +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/README.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/setup.cfg +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/__init__.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/_miniyaml.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/adopt_feature.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/LEARNINGS.template.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/docs/getting-started.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/docs/methodology.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/docs/skills.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/gitignore.snippet +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/roadmap.template.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/rules/correlation-ids.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/rules/never-touch.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/rules/result-contract.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/rules/security-boundaries.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/templates/GATE.template.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/templates/PLAN.template.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/templates/WU.template.md +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/verification.yml.example +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/gate_eval.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/gh_backend.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/gh_features.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/lint_plan.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/scaffold.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/validate_event.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse_loop.egg-info/dependency_links.txt +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse_loop.egg-info/entry_points.txt +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse_loop.egg-info/requires.txt +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse_loop.egg-info/top_level.txt +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_adopt_feature.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_arm_gate_edits_uncommitted.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_attempt_outcome_emission.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_autosync.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_autosync_consent.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_autosync_firstrun.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_autosync_plugin.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_backend.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_bookkeeping_commit_crash_run.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_closing_deliverable_guard.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_cost_tracking.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_deliverable_presence_gate.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_doctor.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_driver_integration.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_driver_lock.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_duration_tracking.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_empty_files_escalation.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_ensure_feature_branch.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_extra_gates.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_force_full_close.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_gate_eval.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_gate_eval_calibration.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_gate_eval_intermediate_wiring.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_gate_eval_terminal_wiring.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_gh_backend.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_gh_features.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_git_env_isolation.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_hashed_denylist.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_hashed_denylist_ci.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_init_integration.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_leak_findings_redaction.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_leak_scan.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_leak_scan_content.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_legacy_4wu_terminal_flips.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lifecycle_integration.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_bare_produces_path.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_close_intermediate.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_close_wu.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_correlation_id.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_oracle_env.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_plan_errors.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_plan_next_draft.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_produces_driver_helper.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_sections.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_task_graph_yaml_selection.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_auto_archive.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_caveman_preamble.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_close_intermediate.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_defaults_by_type.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_effort.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_failure_note_cap.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_files_changed_guard.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_gate_budget.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_model_alias.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_post_pass_invariant.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_reset_preserving_events.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_smoke_runner.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_unsandboxed.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_loop_zero_token_guard.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_migrate_legacy.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_miniyaml_equivalence.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_miniyaml_negative.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_planned_cost_lint.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_produces_field.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_result_block.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_roadmap_add_skill.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_roadmap_archive_skill.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_roadmap_row_parser.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_data_in_sync.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_docs.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_init.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_manifest.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_resources.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_seed_sanity.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_upgrade.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_scaffold_wiring.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_squash_commit_hook_crash.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_template_closing_shapes.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_terminal_flip_ownership.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_terminal_flips.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_untracked_feature_folder.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_upgrade_integration.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_validate_event.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_verdict_coupling.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_verify_empty_gate_set.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_version_consistency.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_version_skew.py +0 -0
- {specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/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.5"
|
|
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.5
|
|
@@ -62,7 +62,7 @@ SPECFUSE_DIR = Path(".specfuse")
|
|
|
62
62
|
REPO_ROOT = SPECFUSE_DIR.parent
|
|
63
63
|
FEATURES_DIR = SPECFUSE_DIR / "features"
|
|
64
64
|
VERIFICATION_PATH = SPECFUSE_DIR / "verification.yml"
|
|
65
|
-
DRIVER_VERSION = "0.3.
|
|
65
|
+
DRIVER_VERSION = "0.3.5"
|
|
66
66
|
# Oldest scaffold layout this driver can drive. init.sh stamps the scaffold's own
|
|
67
67
|
# version into `.specfuse/VERSION`; check_scaffold_version() fails loud at startup if
|
|
68
68
|
# the consumer's scaffold is older than this, pointing at `specfuse upgrade`. Bump
|
|
@@ -728,7 +728,8 @@ def _branch_prep_hint(feature_dir: "Path", feat_fm: dict, feature_id: str) -> st
|
|
|
728
728
|
lines = [
|
|
729
729
|
"",
|
|
730
730
|
"Easiest — let the loop create the branch and commit for you:",
|
|
731
|
-
" specfuse-loop --prepare",
|
|
731
|
+
" specfuse-loop --prepare # …then run",
|
|
732
|
+
" specfuse-loop --prepare-only # …then stop, so you can review first",
|
|
732
733
|
"",
|
|
733
734
|
"Or do it manually:",
|
|
734
735
|
]
|
|
@@ -926,9 +927,15 @@ def ensure_feature_branch(feat_fm: dict, feature_dir: "Path | None" = None) -> N
|
|
|
926
927
|
).returncode == 0
|
|
927
928
|
if not is_ancestor:
|
|
928
929
|
raise FeatureBranchError(
|
|
929
|
-
f"branch '{branch}' exists and
|
|
930
|
-
f"
|
|
931
|
-
f"
|
|
930
|
+
f"branch '{branch}' already exists and has diverged from your "
|
|
931
|
+
f"current branch '{current}' (it carries commits HEAD does not). "
|
|
932
|
+
f"Refusing to silently check it out — that could build on a stale "
|
|
933
|
+
f"base or lose work. Pick one, then re-run:\n"
|
|
934
|
+
f" - resume that branch's work: git checkout {branch}\n"
|
|
935
|
+
f" - rebase it onto here: git checkout {branch} && "
|
|
936
|
+
f"git rebase {current}\n"
|
|
937
|
+
f" - discard it (work not needed): git branch -D {branch} "
|
|
938
|
+
f"(the loop recreates it from HEAD)"
|
|
932
939
|
)
|
|
933
940
|
_checked_checkout(["checkout", branch], f"checkout of existing branch '{branch}'")
|
|
934
941
|
print(f"Switched to feature branch '{branch}' (was on '{current}').")
|
|
@@ -2974,6 +2981,7 @@ def run(
|
|
|
2974
2981
|
dry_run: bool,
|
|
2975
2982
|
force_full_close: str | None = None,
|
|
2976
2983
|
prepare: bool = False,
|
|
2984
|
+
prepare_only: bool = False,
|
|
2977
2985
|
) -> int:
|
|
2978
2986
|
# Fail-fast on a malformed verification.yml BEFORE we touch any WU state.
|
|
2979
2987
|
# The per-gate `verify()` call lazy-loads the same file; if it's malformed,
|
|
@@ -3027,10 +3035,16 @@ def run(
|
|
|
3027
3035
|
)
|
|
3028
3036
|
return 1
|
|
3029
3037
|
require_git_ready()
|
|
3030
|
-
# --prepare: create the feature branch + commit the
|
|
3031
|
-
# the guards below pass (the do-it-for-me path
|
|
3032
|
-
|
|
3038
|
+
# --prepare / --prepare-only: create the feature branch + commit the
|
|
3039
|
+
# folder up front so the guards below pass (the do-it-for-me path
|
|
3040
|
+
# draft-feature can't take). --prepare-only stops after, so you can
|
|
3041
|
+
# review the commit before the loop dispatches anything.
|
|
3042
|
+
if prepare or prepare_only:
|
|
3033
3043
|
prepare_feature(feat_fm, feature_dir, feature_id)
|
|
3044
|
+
if prepare_only:
|
|
3045
|
+
print("Prepared: feature is on its branch and committed. "
|
|
3046
|
+
"Re-run `specfuse-loop` to start the gate.")
|
|
3047
|
+
return 0
|
|
3034
3048
|
# Pre-flight guards — both run BEFORE ensure_feature_branch so the
|
|
3035
3049
|
# refusal happens before any branch mutation, and both protect against
|
|
3036
3050
|
# the per-attempt `git reset --hard` destroying uncommitted state:
|
|
@@ -3517,7 +3531,12 @@ def run(
|
|
|
3517
3531
|
# FEAT-2026-0018/T07 — plan-next-draft lint hook (warn-only v1)
|
|
3518
3532
|
if wu.type == "plan-next":
|
|
3519
3533
|
try:
|
|
3520
|
-
|
|
3534
|
+
# Package-relative + function-local: lint_plan
|
|
3535
|
+
# imports `from .loop import VERDICT_VALUES`, so a
|
|
3536
|
+
# module-top import would be circular; the old
|
|
3537
|
+
# flat (top-level) form broke once the driver
|
|
3538
|
+
# ships as a pip package (#100).
|
|
3539
|
+
from .lint_plan import lint_plan_next_draft
|
|
3521
3540
|
_warns = lint_plan_next_draft(feature_dir, gate.number)
|
|
3522
3541
|
except Exception as _exc:
|
|
3523
3542
|
_warns = [f"lint_plan_next_draft raised: {_exc}"]
|
|
@@ -4014,12 +4033,16 @@ def main() -> int:
|
|
|
4014
4033
|
"'branch:') and commit its folder, then run — the do-it-for-me "
|
|
4015
4034
|
"path after /draft-feature, which leaves the folder "
|
|
4016
4035
|
"uncommitted on the current branch.")
|
|
4036
|
+
ap.add_argument("--prepare-only", action="store_true",
|
|
4037
|
+
help="Like --prepare (create branch + commit the folder) but "
|
|
4038
|
+
"STOP afterwards without dispatching — review the commit, then "
|
|
4039
|
+
"re-run `specfuse-loop` to start.")
|
|
4017
4040
|
args = ap.parse_args()
|
|
4018
4041
|
if not FEATURES_DIR.exists():
|
|
4019
4042
|
sys.exit(f"No {FEATURES_DIR}. Run from your repo root.")
|
|
4020
4043
|
auto_sync(dry_run=args.dry_run, no_autosync=args.no_autosync)
|
|
4021
4044
|
return run(args.feature, args.dry_run, force_full_close=args.force_full_close,
|
|
4022
|
-
prepare=args.prepare)
|
|
4045
|
+
prepare=args.prepare, prepare_only=args.prepare_only)
|
|
4023
4046
|
|
|
4024
4047
|
|
|
4025
4048
|
if __name__ == "__main__":
|
|
@@ -79,6 +79,7 @@ tests/test_lint_correlation_id_close_intermediate.py
|
|
|
79
79
|
tests/test_lint_oracle_env.py
|
|
80
80
|
tests/test_lint_plan_errors.py
|
|
81
81
|
tests/test_lint_plan_next_draft.py
|
|
82
|
+
tests/test_lint_plan_relative_import.py
|
|
82
83
|
tests/test_lint_produces_driver_helper.py
|
|
83
84
|
tests/test_lint_sections.py
|
|
84
85
|
tests/test_lint_task_graph_yaml_selection.py
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Specfuse contributors
|
|
3
|
+
# Licensed under the Apache License, Version 2.0. See LICENSE.
|
|
4
|
+
#
|
|
5
|
+
"""Regression test for #100 — the plan-next-draft lint hook must use a
|
|
6
|
+
package-relative import.
|
|
7
|
+
|
|
8
|
+
`loop.py`'s plan-next hook imported `from lint_plan import lint_plan_next_draft`
|
|
9
|
+
(flat). That only resolved when the scripts were copied side-by-side (pre-pip);
|
|
10
|
+
in the installed package it raises `No module named 'lint_plan'`, so the
|
|
11
|
+
plan-next gate logs a warning and the draft lint never runs. The import is
|
|
12
|
+
function-local on purpose (lint_plan imports `from .loop import VERDICT_VALUES`,
|
|
13
|
+
so a module-top import would be circular) — the fix is to make the local import
|
|
14
|
+
package-relative, not to hoist it.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
import tempfile
|
|
22
|
+
import unittest
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
26
|
+
LOOP_SRC = REPO_ROOT / "specfuse" / "loop" / "loop.py"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TestLintPlanRelativeImport(unittest.TestCase):
|
|
30
|
+
|
|
31
|
+
def test_loop_uses_package_relative_lint_plan_import(self):
|
|
32
|
+
src = LOOP_SRC.read_text(encoding="utf-8")
|
|
33
|
+
self.assertNotIn(
|
|
34
|
+
"from lint_plan import", src,
|
|
35
|
+
"flat `from lint_plan import` breaks in the installed package (#100)",
|
|
36
|
+
)
|
|
37
|
+
self.assertIn(
|
|
38
|
+
"from .lint_plan import lint_plan_next_draft", src,
|
|
39
|
+
"the plan-next hook must import lint_plan_next_draft package-relative",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def test_lint_plan_next_draft_importable_outside_repo_root(self):
|
|
43
|
+
"""Resolvable as a package member even when CWD is not the repo root
|
|
44
|
+
(so a flat `lint_plan.py` on the path can't mask the bug)."""
|
|
45
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
46
|
+
r = subprocess.run(
|
|
47
|
+
[sys.executable, "-c",
|
|
48
|
+
"from specfuse.loop.lint_plan import lint_plan_next_draft; "
|
|
49
|
+
"print('ok')"],
|
|
50
|
+
capture_output=True, text=True, cwd=tmp,
|
|
51
|
+
)
|
|
52
|
+
self.assertEqual(r.returncode, 0, r.stderr)
|
|
53
|
+
self.assertIn("ok", r.stdout)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
if __name__ == "__main__":
|
|
57
|
+
unittest.main()
|
|
@@ -706,7 +706,7 @@ class TestMainArgparse(unittest.TestCase):
|
|
|
706
706
|
with patch.object(sys, "argv", ["loop", "--feature", "my-feature"]):
|
|
707
707
|
result = loop.main()
|
|
708
708
|
self.assertEqual(result, 0)
|
|
709
|
-
mock_run.assert_called_once_with("my-feature", False, force_full_close=None, prepare=False)
|
|
709
|
+
mock_run.assert_called_once_with("my-feature", False, force_full_close=None, prepare=False, prepare_only=False)
|
|
710
710
|
|
|
711
711
|
def test_dry_run_flag_forwarded_to_run(self):
|
|
712
712
|
"""--dry-run is forwarded as dry_run=True to run()."""
|
|
@@ -718,7 +718,7 @@ class TestMainArgparse(unittest.TestCase):
|
|
|
718
718
|
with patch.object(loop, "run", return_value=0) as mock_run:
|
|
719
719
|
with patch.object(sys, "argv", ["loop", "--dry-run"]):
|
|
720
720
|
loop.main()
|
|
721
|
-
mock_run.assert_called_once_with(None, True, force_full_close=None, prepare=False)
|
|
721
|
+
mock_run.assert_called_once_with(None, True, force_full_close=None, prepare=False, prepare_only=False)
|
|
722
722
|
|
|
723
723
|
def test_multi_active_error_propagates_from_run(self):
|
|
724
724
|
"""main() with no args propagates the find_feature multi-active SystemExit."""
|
|
@@ -36,6 +36,13 @@ def _init_git(root: Path) -> None:
|
|
|
36
36
|
subprocess.run(["git", "-C", str(root), "config", "commit.gpgSign", "false"],
|
|
37
37
|
check=True)
|
|
38
38
|
(root / "README.md").write_text("# fixture\n")
|
|
39
|
+
specfuse = root / ".specfuse"
|
|
40
|
+
specfuse.mkdir()
|
|
41
|
+
(specfuse / "verification.yml").write_text(
|
|
42
|
+
"code:\n - name: noop\n command: \"true\"\n"
|
|
43
|
+
"doc:\n - name: noop\n command: \"true\"\n"
|
|
44
|
+
"plannext:\n - name: noop\n command: \"true\"\n"
|
|
45
|
+
)
|
|
39
46
|
subprocess.run(["git", "-C", str(root), "add", "."], check=True)
|
|
40
47
|
subprocess.run(["git", "-C", str(root), "commit", "-q", "-m", "init"], check=True)
|
|
41
48
|
|
|
@@ -46,7 +53,10 @@ def _write_uncommitted_feature(root: Path) -> Path:
|
|
|
46
53
|
fdir.mkdir(parents=True)
|
|
47
54
|
(fdir / "PLAN.md").write_text(
|
|
48
55
|
f"---\nfeature_id: {_FEAT_ID}\ntitle: T\nbranch: {_BRANCH}\n"
|
|
49
|
-
f"roadmap_goal: t\nstatus: active\n---\n\n# Plan\n"
|
|
56
|
+
f"roadmap_goal: t\nstatus: active\n---\n\n# Plan\n\n```yaml\n"
|
|
57
|
+
f"gates:\n - gate: 1\n file: GATE-01.md\n work_units:\n"
|
|
58
|
+
f" - id: {_FEAT_ID}/T01\n file: WU-01.md\n"
|
|
59
|
+
f" depends_on: []\n```\n"
|
|
50
60
|
)
|
|
51
61
|
(fdir / "GATE-01.md").write_text("---\ngate: 1\nstatus: open\n---\n\n# Gate 1\n")
|
|
52
62
|
(fdir / "WU-01.md").write_text(
|
|
@@ -85,6 +95,34 @@ class TestPrepareFeature(unittest.TestCase):
|
|
|
85
95
|
capture_output=True, text=True).stdout
|
|
86
96
|
self.assertIn("PLAN.md", tracked)
|
|
87
97
|
|
|
98
|
+
def test_prepare_only_prepares_then_stops(self):
|
|
99
|
+
"""--prepare-only branches + commits but does NOT dispatch (rc=0)."""
|
|
100
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
101
|
+
root = Path(tmp)
|
|
102
|
+
_init_git(root)
|
|
103
|
+
_write_uncommitted_feature(root)
|
|
104
|
+
os.chdir(root)
|
|
105
|
+
|
|
106
|
+
called = {"dispatch": False}
|
|
107
|
+
|
|
108
|
+
def _no_dispatch(*a, **k):
|
|
109
|
+
called["dispatch"] = True
|
|
110
|
+
raise AssertionError("must not dispatch under --prepare-only")
|
|
111
|
+
|
|
112
|
+
orig = loop.dispatch
|
|
113
|
+
loop.dispatch = _no_dispatch
|
|
114
|
+
try:
|
|
115
|
+
rc = loop.run(None, dry_run=False, prepare_only=True)
|
|
116
|
+
finally:
|
|
117
|
+
loop.dispatch = orig
|
|
118
|
+
|
|
119
|
+
self.assertEqual(rc, 0)
|
|
120
|
+
self.assertFalse(called["dispatch"])
|
|
121
|
+
self.assertEqual(loop._current_branch(), _BRANCH)
|
|
122
|
+
# folder committed (tree clean)
|
|
123
|
+
rel = Path(".specfuse/features") / f"{_FEAT_ID}-thing"
|
|
124
|
+
self.assertEqual(loop.untracked_feature_files(rel), [])
|
|
125
|
+
|
|
88
126
|
def test_committed_guard_message_suggests_prepare_and_branch(self):
|
|
89
127
|
"""On the default branch, the untracked-folder refusal names --prepare
|
|
90
128
|
AND the `git checkout -b <branch>` step."""
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.3.3
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/specfuse/loop/data/docs/concepts/ralph-lineage.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{specfuse_loop-0.3.3 → specfuse_loop-0.3.5}/tests/test_lint_correlation_id_close_intermediate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|