diff --git a/src/bonfire/engine/gates.py b/src/bonfire/engine/gates.py index b19838d7..5bc8addc 100644 --- a/src/bonfire/engine/gates.py +++ b/src/bonfire/engine/gates.py @@ -157,14 +157,14 @@ async def evaluate(self, envelope: Envelope, context: GateContext) -> GateResult class MergePreflightGate: """Gate adapter for :class:`MergePreflightHandler` envelopes. - Per Sage memo bon-519-sage-20260428T033101Z.md §D-CL.1 lines 845-848, - §D-CL.6 #5 (line 1071), and §A Q6 (ALLOW-WITH-ANNOTATION ratified). + Implements the ratified allow-with-annotation policy for pre-existing + test debt. Severity table: - COMPLETED + clean metadata -> ``passed=True, severity="info"`` - COMPLETED + ``META_PREFLIGHT_TEST_DEBT_NOTED is True`` - -> ``passed=True, severity="warning"`` (Q6) + -> ``passed=True, severity="warning"`` (allow-with-annotation) - FAILED with ``error_type`` ∈ {cross_wave_interaction, pure_warrior_bug, pytest_collection_error, merge_conflict} -> ``passed=False, severity="error"`` diff --git a/src/bonfire/git/scratch.py b/src/bonfire/git/scratch.py index e1e499d2..2b6388e5 100644 --- a/src/bonfire/git/scratch.py +++ b/src/bonfire/git/scratch.py @@ -8,11 +8,11 @@ ephemeral branches with an 8-hex random suffix (race-safety), and ALWAYS get torn down on context exit (try/finally guarantee). -Per Sage memo bon-519-sage-20260428T033101Z.md §D3 (lines 298-350): +Design contract: - Path format: ``/.bonfire-worktrees/preflight/pr--<8-hex>/`` - Branch format: ``bonfire/preflight-pr--<8-hex>`` - Reuses ``_run_git`` from ``bonfire.git.workflow`` (no new subprocess). - - Reuses ``WORKTREE_DIR`` from ``bonfire.git.worktree`` (line 21). + - Reuses ``WORKTREE_DIR`` from ``bonfire.git.worktree``. - ``__aexit__`` MUST swallow exceptions during cleanup (logs only). Otherwise a cleanup failure masks the original handler error. diff --git a/src/bonfire/github/client.py b/src/bonfire/github/client.py index 98f98351..c7713d3d 100644 --- a/src/bonfire/github/client.py +++ b/src/bonfire/github/client.py @@ -38,9 +38,9 @@ class PRInfo(BaseModel, frozen=True, extra="forbid"): class PRSummary(BaseModel, frozen=True, extra="forbid"): """Lightweight open-PR summary used by the merge-preflight stage. - Sage memo bon-519-sage-20260428T033101Z.md §D5 lines 498-504. Returned - by :py:meth:`GitHubClient.list_open_prs`. Frozen so the sibling-batch - detector can store instances inside ``frozenset`` keys without worry. + Returned by :py:meth:`GitHubClient.list_open_prs`. Frozen so the + sibling-batch detector can store instances inside ``frozenset`` keys + without worry. Fields map to ``gh pr list --json number,headRefName,title,files``: ``headRefName -> head_branch`` and ``files[].path -> file_paths``. @@ -253,7 +253,6 @@ async def list_open_prs( ) -> list[PRSummary]: """List open PRs targeting *base*. Optionally exclude a PR number. - Sage memo bon-519-sage-20260428T033101Z.md §D5 lines 478-491. Invokes ``gh pr list -R --base --state open --json number,headRefName,title,files``. Returns parsed :class:`PRSummary` instances. diff --git a/src/bonfire/github/mock.py b/src/bonfire/github/mock.py index 9294a6df..bb2c24a4 100644 --- a/src/bonfire/github/mock.py +++ b/src/bonfire/github/mock.py @@ -130,8 +130,7 @@ async def get_pr_files(self, number: int) -> list[dict]: def set_open_prs(self, *, base: str, prs: list[dict]) -> None: """Configure canned open-PR data for ``list_open_prs``. - Sage memo bon-519-sage-20260428T033101Z.md §D5 line 506: tests - pre-populate the mock with N synthetic PRs each with a + Tests pre-populate the mock with N synthetic PRs, each with a deterministic file set. Parameters diff --git a/src/bonfire/handlers/__init__.py b/src/bonfire/handlers/__init__.py index 76051856..6070f6dc 100644 --- a/src/bonfire/handlers/__init__.py +++ b/src/bonfire/handlers/__init__.py @@ -21,8 +21,7 @@ correction stage (``SageCorrectionBounceHandler``) IS in the map — it binds the ``sage_correction_bounce`` stage stem to ``AgentRole.SYNTHESIZER`` so the display layer can resolve its gamified -name through the same path as the other handlers. See Sage memo -``bon-519-sage-20260428T033101Z.md`` §A Q1 Path beta + §D10 line 745. +name through the same path as the other handlers. """ from __future__ import annotations diff --git a/src/bonfire/handlers/merge_preflight.py b/src/bonfire/handlers/merge_preflight.py index 8340a2b4..03a63ba5 100644 --- a/src/bonfire/handlers/merge_preflight.py +++ b/src/bonfire/handlers/merge_preflight.py @@ -7,26 +7,21 @@ ``gh pr merge``. Detects cross-wave interactions between sibling PRs (the enum-widening incident from S007). -Per Sage memo bon-519-sage-20260428T033101Z.md: - - §A Q1 Path β (lines 16-39): module at ``bonfire.handlers.merge_preflight`` - with module-level ``ROLE: AgentRole = AgentRole.VERIFIER``. NOT in - ``HANDLER_ROLE_MAP`` (deterministic handler bypasses gamified-display map). - - §A Q4 (lines 79-122): 6-verdict deterministic classifier; first-match-wins - ordering (collection-error -> green -> pre-existing-debt -> cross-wave - -> pure-warrior-bug; merge-conflict produced by handler shell, not the - pure classifier). - - §A Q5 (lines 124-142): sibling-batch detection via +Design contract: + - Module at ``bonfire.handlers.merge_preflight`` with module-level + ``ROLE: AgentRole = AgentRole.VERIFIER``. NOT in ``HANDLER_ROLE_MAP`` + (deterministic handler bypasses the gamified-display map). + - Six-verdict deterministic classifier; first-match-wins ordering + (collection-error -> green -> pre-existing-debt -> cross-wave + -> pure-warrior-bug; merge-conflict produced by the handler shell, not + the pure classifier). + - Sibling-batch detection via ``client.list_open_prs(base, exclude=current_pr_number)``. - - §A Q6 (lines 144-156): ratified ALLOW-WITH-ANNOTATION for pre-existing - debt; classifier returns the verdict, handler downstream marks - ``META_PREFLIGHT_TEST_DEBT_NOTED``. - - §D1 (lines 196-225): module shape, public ``__all__``. - - §D2 (lines 229-294): handler signature + ``handle()`` flow pseudocode. - - §D4 (lines 383-470): classifier function signatures + edge case table. - - §D5 (lines 473-522): gh client extension + sibling detection. - - §D-CL.4 (lines 962-989): Warrior B fills the algorithmic body - (``classify_pytest_run``, ``parse_pytest_junit_xml``, - ``parse_pytest_stdout_fallback``, ``detect_sibling_prs``). + - Allow-with-annotation for pre-existing debt: the classifier returns the + verdict, the handler downstream marks ``META_PREFLIGHT_TEST_DEBT_NOTED``. + - The algorithmic body lives in ``classify_pytest_run``, + ``parse_pytest_junit_xml``, ``parse_pytest_stdout_fallback``, and + ``detect_sibling_prs``. The module exposes ``ROLE: AgentRole = AgentRole.VERIFIER`` for generic- vocabulary discipline. Display translation (verifier -> "Cleric") happens @@ -631,24 +626,23 @@ async def _classify_preflight_run( ) -> PreflightClassification: """Live body: apply diff, run pytest, classify. - Per Sage memo bon-519-sage-20260428T033101Z.md §D2 lines 273-291. Supersedes the prior v0.1 stub that returned GREEN unconditionally; this method now drives the full subprocess pipeline (current PR diff -> sibling diffs -> pytest -> JUnit parse -> baseline cache -> deterministic classifier). - Step ordering mirrors the §D2 pseudocode exactly: + Step ordering: 5. Apply current PR diff in scratch (``git apply --3way``). 6. Apply sibling-batch diffs in ascending PR-number order - (Sage §D-CL.7 #4: later PR's diff takes precedence on - conflict via ``--3way``). - 7. Run pytest with ``--junit-xml=`` (§D-CL.7 #3). + (a later PR's diff takes precedence on conflict via + ``--3way``). + 7. Run pytest with ``--junit-xml=``. 8. Parse failures from JUnit XML; fall back to stdout regex if XML is empty AND returncode != 0. 9. Compute / cache baseline failures on ``origin/``. - 10. Call :py:func:`classify_pytest_run` (Warrior B's pure fn). + 10. Call :py:func:`classify_pytest_run` (the pure classifier). - Envelope-size discipline (§D-CL.7 #6): + Envelope-size discipline: - ``pytest_stdout_tail`` is truncated to 2KB. - ``failing_tests`` is truncated to 100 entries; on overflow a sentinel ``FailingTest`` with ``file_path=''`` diff --git a/src/bonfire/workflow/standard.py b/src/bonfire/workflow/standard.py index 9de50a69..b681958c 100644 --- a/src/bonfire/workflow/standard.py +++ b/src/bonfire/workflow/standard.py @@ -45,8 +45,7 @@ def standard_build() -> WorkflowSpec: Three on_gate_failure bounces target the warrior (from prover, sage_correction_bounce, and wizard). MergePreflight runs full-suite - pytest against the simulated merged tip before the merge button - (Sage memo ``bon-519-sage-20260428T033101Z.md`` §D6 lines 530-544). + pytest against the simulated merged tip before the merge button. """ return WorkflowSpec( name="standard_build", diff --git a/tests/unit/test_no_bon_ref_in_src_sweep.py b/tests/unit/test_no_bon_ref_in_src_sweep.py index ad4d0712..035fa1ff 100644 --- a/tests/unit/test_no_bon_ref_in_src_sweep.py +++ b/tests/unit/test_no_bon_ref_in_src_sweep.py @@ -42,10 +42,10 @@ _REPO_ROOT = Path(__file__).resolve().parents[2] _SRC_DIR = _REPO_ROOT / "src" / "bonfire" -# Regex matching the canonical ticket-ref shape (e.g. ``BON-338``). -# Note: ``BON-W5.3``-style refs do NOT match (W is not a digit) — those -# are intentionally out of scope for this sweep. -_BON_REF = re.compile(r"BON-\d+") +# Regex matching the canonical ticket-ref shape (e.g. ``BON-338``). ``BON-W5.3`` +# refs do NOT match (W not a digit) — out of scope. IGNORECASE: tracker rot also +# ships lowercase (``bon-NNN-...`` filenames) that a case-sensitive regex missed. +_BON_REF = re.compile(r"BON-\d+", re.IGNORECASE) # --------------------------------------------------------------------------- # Allowlist