specfuse-loop 0.3.8__tar.gz → 0.3.9__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.8/specfuse_loop.egg-info → specfuse_loop-0.3.9}/PKG-INFO +1 -1
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/pyproject.toml +1 -1
- specfuse_loop-0.3.9/specfuse/loop/data/VERSION +1 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/loop.py +1 -1
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9/specfuse_loop.egg-info}/PKG-INFO +1 -1
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_roadmap_add_skill.py +93 -5
- specfuse_loop-0.3.8/specfuse/loop/data/VERSION +0 -1
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/LICENSE +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/NOTICE +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/README.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/setup.cfg +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/__init__.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/_miniyaml.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/adopt_feature.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/LEARNINGS.template.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/docs/concepts/architecture-addendum-gates-and-iterative-planning.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/docs/concepts/ralph-lineage.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/docs/getting-started.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/docs/methodology.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/docs/skills.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/gitignore.snippet +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/roadmap.template.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/rules/correlation-ids.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/rules/never-touch.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/rules/result-contract.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/rules/security-boundaries.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/rules/verification-discipline.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/schemas/event.schema.json +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/schemas/events/initiative_created.schema.json +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/schemas/events/spec_issue_resolved.schema.json +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/schemas/events/spec_issue_routed.schema.json +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/schemas/events/spec_validated.schema.json +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/templates/GATE.template.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/templates/PLAN.template.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/templates/WU.template.md +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/verification.yml.example +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/gate_eval.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/gh_backend.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/gh_features.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/lint_plan.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/scaffold.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/validate_event.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse_loop.egg-info/SOURCES.txt +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse_loop.egg-info/dependency_links.txt +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse_loop.egg-info/entry_points.txt +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse_loop.egg-info/requires.txt +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse_loop.egg-info/top_level.txt +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_adopt_feature.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_arm_gate_edits_uncommitted.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_attempt_outcome_emission.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_autosync.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_autosync_consent.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_autosync_firstrun.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_autosync_plugin.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_backend.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_bookkeeping_commit_crash_run.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_bookkeeping_commit_hook_crash.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_closing_deliverable_guard.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_cost_tracking.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_deliverable_presence_gate.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_doctor.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_driver_integration.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_driver_lock.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_duration_tracking.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_empty_files_escalation.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_ensure_feature_branch.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_events_redaction.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_extra_gates.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_force_full_close.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_gate_eval.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_gate_eval_calibration.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_gate_eval_intermediate_wiring.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_gate_eval_terminal_wiring.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_gh_backend.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_gh_features.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_git_env_isolation.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_hashed_denylist.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_hashed_denylist_ci.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_init_integration.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_leak_findings_redaction.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_leak_scan.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_leak_scan_content.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_learnings_query.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_legacy_4wu_terminal_flips.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_lifecycle_integration.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_lint_bare_produces_path.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_lint_close_intermediate.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_lint_close_wu.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_lint_correlation_id.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_lint_correlation_id_close_intermediate.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_lint_oracle_env.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_lint_plan_errors.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_lint_plan_next_draft.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_lint_plan_relative_import.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_lint_produces_driver_helper.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_lint_sections.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_lint_task_graph_yaml_selection.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_auto_archive.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_caveman_preamble.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_close_intermediate.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_defaults_by_type.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_effort.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_failure_note_cap.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_files_changed_guard.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_gate_budget.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_model_alias.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_orchestration.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_post_pass_invariant.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_reset_preserving_events.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_smoke_runner.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_unsandboxed.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_loop_zero_token_guard.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_migrate_legacy.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_miniyaml_equivalence.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_miniyaml_negative.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_planned_cost_lint.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_prepare_feature.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_produces_field.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_result_block.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_roadmap_archive_skill.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_roadmap_row_parser.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_scaffold_data_in_sync.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_scaffold_docs.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_scaffold_init.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_scaffold_manifest.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_scaffold_resources.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_scaffold_seed_sanity.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_scaffold_upgrade.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_scaffold_wiring.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_skills_vendored_in_sync.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_squash_commit_hook_crash.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_template_closing_shapes.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_terminal_flip_ownership.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_terminal_flips.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_untracked_feature_folder.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_upgrade_integration.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_upgrade_merge_gate.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_validate_event.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_verdict_coupling.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_verify_empty_gate_set.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_version_consistency.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/tests/test_version_skew.py +0 -0
- {specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/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.9"
|
|
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.9
|
|
@@ -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.9"
|
|
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
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"""Reference implementation and tests for the roadmap-add skill algorithm.
|
|
6
6
|
|
|
7
7
|
The skill appends a new planned feature's table row and detail section to
|
|
8
|
-
roadmap.md, auto-picking the next FEAT-YYYY-NNNN by scanning
|
|
8
|
+
roadmap.md, auto-picking the next FEAT-YYYY-NNNN by scanning four sources
|
|
9
|
+
(the fourth — GitHub issue/PR-reserved IDs — added for specfuse#16).
|
|
9
10
|
|
|
10
11
|
Exercises:
|
|
11
12
|
(a) headless mode appends a row and detail section in the right places
|
|
@@ -33,19 +34,29 @@ FEAT_ROW_RE = re.compile(r"^\| (FEAT-\d{4}-\d{4})")
|
|
|
33
34
|
PLAN_FEAT_RE = re.compile(r"^feature_id:\s*(FEAT-\d{4}-\d{4})")
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
def scan_feat_ids(
|
|
37
|
-
|
|
37
|
+
def scan_feat_ids(
|
|
38
|
+
roadmap_path: Path,
|
|
39
|
+
features_dir: Path,
|
|
40
|
+
extra_paths: list[Path],
|
|
41
|
+
github_reserved: dict[str, str] | None = None,
|
|
42
|
+
) -> dict:
|
|
43
|
+
"""Scan FEAT-YYYY-NNNN IDs from four sources.
|
|
38
44
|
|
|
39
45
|
Source (a): table rows in roadmap_path (lines matching ``^| FEAT-``).
|
|
40
46
|
Source (b): ``feature_id:`` lines in features_dir/*/PLAN.md.
|
|
41
47
|
Source (c): any occurrence in extra_paths (LEARNINGS.md, RETROSPECTIVE.md).
|
|
48
|
+
Source (d): GitHub issue/PR titles+bodies — modelled here as an injected
|
|
49
|
+
``github_reserved`` map ``{feat_id: "GitHub issue/PR #<n>"}`` so the
|
|
50
|
+
fold-into-max + collision logic is exercised hermetically (no network).
|
|
51
|
+
``None`` models GitHub being unreachable: the skill degrades gracefully
|
|
52
|
+
and simply omits source (d).
|
|
42
53
|
|
|
43
|
-
Returns dict mapping feat_id (str) -> (
|
|
54
|
+
Returns dict mapping feat_id (str) -> (location_str, line_no) for the
|
|
44
55
|
first occurrence of each ID seen.
|
|
45
56
|
"""
|
|
46
57
|
sources: dict[str, tuple[str, int]] = {}
|
|
47
58
|
|
|
48
|
-
def record(feat_id: str, filepath
|
|
59
|
+
def record(feat_id: str, filepath, lineno: int) -> None:
|
|
49
60
|
if feat_id not in sources:
|
|
50
61
|
sources[feat_id] = (str(filepath), lineno)
|
|
51
62
|
|
|
@@ -71,6 +82,13 @@ def scan_feat_ids(roadmap_path: Path, features_dir: Path, extra_paths: list[Path
|
|
|
71
82
|
for m in FEAT_RE.finditer(line):
|
|
72
83
|
record(m.group(0), path, lineno)
|
|
73
84
|
|
|
85
|
+
# (d) GitHub issue/PR-reserved IDs (None = GitHub unreachable, skip source).
|
|
86
|
+
if github_reserved:
|
|
87
|
+
for feat_id, ref in github_reserved.items():
|
|
88
|
+
# location is the issue/PR ref; lineno 0 (not a file line)
|
|
89
|
+
if feat_id not in sources:
|
|
90
|
+
sources[feat_id] = (ref, 0)
|
|
91
|
+
|
|
74
92
|
return sources
|
|
75
93
|
|
|
76
94
|
|
|
@@ -449,6 +467,40 @@ class TestNextFeatId(unittest.TestCase):
|
|
|
449
467
|
self.assertIn("gap", str(ctx.exception).lower())
|
|
450
468
|
self.assertIn("0002", str(ctx.exception))
|
|
451
469
|
|
|
470
|
+
# ── Source (d): GitHub issue/PR-reserved IDs (specfuse#16) ──────────────
|
|
471
|
+
|
|
472
|
+
def test_github_reserved_id_raises_max(self):
|
|
473
|
+
"""A FEAT ID reserved only on a GitHub issue (no roadmap row) must be
|
|
474
|
+
folded into the max so the next ID does not collide with it."""
|
|
475
|
+
# roadmap/plan/learnings top out at 0010; GitHub reserves 0011.
|
|
476
|
+
gh = {"FEAT-2026-0011": "GitHub issue #345"}
|
|
477
|
+
sources = scan_feat_ids(
|
|
478
|
+
self.roadmap, self.features_dir, [self.learnings], github_reserved=gh
|
|
479
|
+
)
|
|
480
|
+
feat_id, ordinal = next_feat_id(2026, sources)
|
|
481
|
+
self.assertEqual(feat_id, "FEAT-2026-0012")
|
|
482
|
+
self.assertEqual(ordinal, 12)
|
|
483
|
+
|
|
484
|
+
def test_github_unreachable_degrades_gracefully(self):
|
|
485
|
+
"""github_reserved=None models GitHub being unreachable: the scan omits
|
|
486
|
+
source (d) and proceeds on the three local sources — no hard failure."""
|
|
487
|
+
sources = scan_feat_ids(
|
|
488
|
+
self.roadmap, self.features_dir, [self.learnings], github_reserved=None
|
|
489
|
+
)
|
|
490
|
+
feat_id, _ = next_feat_id(2026, sources)
|
|
491
|
+
self.assertEqual(feat_id, "FEAT-2026-0011") # local-only max=0010
|
|
492
|
+
|
|
493
|
+
def test_github_reserved_beyond_local_max_surfaces_gap(self):
|
|
494
|
+
"""A reserved ID beyond the local max exposes the intervening ordinals
|
|
495
|
+
as genuine gaps (roadmap→0010, issue reserves 0016 → 0011–0015 gap)."""
|
|
496
|
+
gh = {"FEAT-2026-0016": "GitHub issue #345"}
|
|
497
|
+
sources = scan_feat_ids(
|
|
498
|
+
self.roadmap, self.features_dir, [self.learnings], github_reserved=gh
|
|
499
|
+
)
|
|
500
|
+
with self.assertRaises(ValueError) as ctx:
|
|
501
|
+
next_feat_id(2026, sources)
|
|
502
|
+
self.assertIn("gap", str(ctx.exception).lower())
|
|
503
|
+
|
|
452
504
|
|
|
453
505
|
class TestCollisionRejection(unittest.TestCase):
|
|
454
506
|
"""(c) ID collision is rejected with a message naming the conflicting source."""
|
|
@@ -530,6 +582,42 @@ class TestCollisionRejection(unittest.TestCase):
|
|
|
530
582
|
# Caller would not invoke add_feature on collision; verify snapshot unchanged
|
|
531
583
|
self.assertEqual(self.roadmap.read_text(), snapshot)
|
|
532
584
|
|
|
585
|
+
def test_collision_detected_for_github_reserved_id(self):
|
|
586
|
+
"""specfuse#16: an ID reserved only on a GitHub issue/PR (no local
|
|
587
|
+
source) must be caught by the collision check and name the GitHub ref."""
|
|
588
|
+
gh = {"FEAT-2026-0016": "GitHub issue #345"}
|
|
589
|
+
sources = scan_feat_ids(
|
|
590
|
+
self.roadmap, self.features_dir, [self.learnings], github_reserved=gh
|
|
591
|
+
)
|
|
592
|
+
collides, filepath, lineno = check_collision("FEAT-2026-0016", sources)
|
|
593
|
+
self.assertTrue(collides, "GitHub-reserved ID must collide")
|
|
594
|
+
self.assertIn("GitHub", filepath)
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class TestSkillDocumentsGitHubSource(unittest.TestCase):
|
|
598
|
+
"""The skill prose must document source (d) + graceful offline degradation
|
|
599
|
+
(specfuse#16). Guards against the four-source scan silently regressing to
|
|
600
|
+
three in the authored SKILL.md."""
|
|
601
|
+
|
|
602
|
+
def setUp(self):
|
|
603
|
+
repo_root = Path(__file__).resolve().parent.parent
|
|
604
|
+
self.skill = (
|
|
605
|
+
repo_root / "plugins" / "specfuse" / "skills"
|
|
606
|
+
/ "roadmap-add" / "SKILL.md"
|
|
607
|
+
).read_text()
|
|
608
|
+
|
|
609
|
+
def test_declares_four_sources(self):
|
|
610
|
+
self.assertIn("four sources", self.skill)
|
|
611
|
+
|
|
612
|
+
def test_documents_github_source(self):
|
|
613
|
+
self.assertIn("Source (d)", self.skill)
|
|
614
|
+
self.assertIn("gh search issues", self.skill)
|
|
615
|
+
|
|
616
|
+
def test_documents_graceful_degradation(self):
|
|
617
|
+
# A WARN path so offline runs continue rather than hard-failing.
|
|
618
|
+
self.assertIn("WARN", self.skill)
|
|
619
|
+
self.assertIn("offline", self.skill.lower())
|
|
620
|
+
|
|
533
621
|
|
|
534
622
|
if __name__ == "__main__":
|
|
535
623
|
unittest.main()
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.3.8
|
|
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.8 → specfuse_loop-0.3.9}/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
|
{specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/specfuse/loop/data/rules/verification-discipline.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
|
{specfuse_loop-0.3.8 → specfuse_loop-0.3.9}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|