Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/bonfire/engine/gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"``
Expand Down
4 changes: 2 additions & 2 deletions src/bonfire/git/scratch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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: ``<repo>/.bonfire-worktrees/preflight/pr-<N>-<8-hex>/``
- Branch format: ``bonfire/preflight-pr-<N>-<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.

Expand Down
7 changes: 3 additions & 4 deletions src/bonfire/github/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
Expand Down Expand Up @@ -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 <repo> --base <base> --state open --json
number,headRefName,title,files``. Returns parsed
:class:`PRSummary` instances.
Expand Down
3 changes: 1 addition & 2 deletions src/bonfire/github/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions src/bonfire/handlers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 20 additions & 26 deletions src/bonfire/handlers/merge_preflight.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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=<known-path>`` (§D-CL.7 #3).
(a later PR's diff takes precedence on conflict via
``--3way``).
7. Run pytest with ``--junit-xml=<known-path>``.
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/<base>``.
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='<overflow>'``
Expand Down
3 changes: 1 addition & 2 deletions src/bonfire/workflow/standard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions tests/unit/test_no_bon_ref_in_src_sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading