Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
7 changes: 7 additions & 0 deletions .watchflow/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,10 @@ rules:
event_types: ["pull_request"]
parameters:
block_on_unresolved_comments: true

- description: "PR description must accurately reflect the actual code changes in the diff."
enabled: true
severity: "medium"
event_types: ["pull_request"]
parameters:
require_description_diff_alignment: true
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added

- **Description-diff alignment** -- `DescriptionDiffAlignmentCondition` uses
the configured AI provider (OpenAI / Bedrock / Vertex AI) to verify that
the PR description semantically matches the actual code changes. First
LLM-backed condition in Watchflow; adds ~1-3s latency. Gracefully skips
(no violation) if the LLM is unavailable.

## [2026-03-01] -- PR #59

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Rules are **description + event_types + parameters**. The engine matches paramet
| **Push** | `no_force_push: true` | push | Reject force pushes. |
| **Files** | `max_file_size_mb: 1` | pull_request | No single file > N MB. |
| **Files** | `pattern` + `condition_type: "files_match_pattern"` | pull_request | Changed files must (or must not) match glob/regex. |
| **PR** | `require_description_diff_alignment: true` | pull_request | Description must match code changes (LLM-assisted). |
| **Time** | `allowed_hours`, `days`, weekend | deployment / workflow | Restrict when actions can run. |

Rules are read from the **default branch** (e.g. `main`). Each webhook delivery is deduplicated by `X-GitHub-Delivery` so handler and processor both run; comments and check runs stay in sync.
Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ graph TD
### Condition registry

- Maps parameter names to condition classes (e.g. `require_linked_issue` → `RequireLinkedIssueCondition`, `max_lines` → `MaxPrLocCondition`, `require_code_owner_reviewers` → `RequireCodeOwnerReviewersCondition`).
- Supported conditions: linked issue, title pattern, description length, labels, approvals, PR size (lines), CODEOWNERS (path has owner, require owners as reviewers), protected branches, no force push, file size, file pattern, diff pattern scanning, security pattern detection, unresolved comments, test coverage, comment response SLA, signed commits, changelog required, self-approval prevention, cross-team approval, time/deploy rules. See [Configuration](../getting-started/configuration.md).
- Supported conditions: linked issue, title pattern, description length, labels, approvals, PR size (lines), CODEOWNERS (path has owner, require owners as reviewers), protected branches, no force push, file size, file pattern, diff pattern scanning, security pattern detection, unresolved comments, test coverage, comment response SLA, signed commits, changelog required, self-approval prevention, cross-team approval, description-diff alignment (LLM-assisted), time/deploy rules. See [Configuration](../getting-started/configuration.md).

### PR enricher

Expand Down
6 changes: 6 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ Rules are **description + event_types + parameters**. The engine matches **param
| `require_signed_commits: true` | SignedCommitsCondition | All commits must be cryptographically signed (GPG/SSH/S/MIME). |
| `require_changelog_update: true` | ChangelogRequiredCondition | Source changes must include a CHANGELOG or `.changeset` update. |

### LLM-assisted

| Parameter | Condition | Description |
|-----------|-----------|-------------|
| `require_description_diff_alignment: true` | DescriptionDiffAlignmentCondition | PR description must semantically match the code diff (uses LLM; ~1-3s latency). |

---

## Repository analysis → one-click rules PR
Expand Down
11 changes: 11 additions & 0 deletions docs/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,17 @@ parameters:

PRs that modify source code must include a corresponding `CHANGELOG.md` or `.changeset/` update. Docs, tests, and `.github/` paths are excluded.

### LLM-assisted conditions

**Description-diff alignment**

```yaml
parameters:
require_description_diff_alignment: true
```

Uses the configured AI provider to check whether the PR description semantically reflects the actual code changes. Flags mismatches like "description says fix login but diff only touches billing code." Adds ~1-3s latency per evaluation. If the LLM is unavailable (provider not configured, rate limit), the condition gracefully skips without blocking the PR.

---

## Example rules
Expand Down
3 changes: 3 additions & 0 deletions src/rules/acknowledgment.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class RuleID(StrEnum):
CHANGELOG_REQUIRED = "changelog-required"
NO_SELF_APPROVAL = "no-self-approval"
CROSS_TEAM_APPROVAL = "cross-team-approval"
DESCRIPTION_DIFF_ALIGNMENT = "description-diff-alignment"


# Mapping from violation text patterns to RuleID
Expand All @@ -67,6 +68,7 @@ class RuleID(StrEnum):
"without a corresponding CHANGELOG": RuleID.CHANGELOG_REQUIRED,
"approved by its own author": RuleID.NO_SELF_APPROVAL,
"approvals from required teams": RuleID.CROSS_TEAM_APPROVAL,
"does not align with code changes": RuleID.DESCRIPTION_DIFF_ALIGNMENT,
}

# Mapping from RuleID to human-readable descriptions
Expand All @@ -91,6 +93,7 @@ class RuleID(StrEnum):
RuleID.CHANGELOG_REQUIRED: "Source code changes must include a CHANGELOG or .changeset update.",
RuleID.NO_SELF_APPROVAL: "PR authors cannot approve their own pull requests.",
RuleID.CROSS_TEAM_APPROVAL: "Pull requests require approvals from specified GitHub teams.",
RuleID.DESCRIPTION_DIFF_ALIGNMENT: "PR description must accurately reflect the actual code changes.",
}

# Comment markers that indicate an acknowledgment comment
Expand Down
3 changes: 3 additions & 0 deletions src/rules/conditions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
MaxPrLocCondition,
TestCoverageCondition,
)
from src.rules.conditions.llm_assisted import DescriptionDiffAlignmentCondition
from src.rules.conditions.pull_request import (
DiffPatternCondition,
MinDescriptionLengthCondition,
Expand Down Expand Up @@ -71,6 +72,8 @@
# Compliance
"SignedCommitsCondition",
"ChangelogRequiredCondition",
# LLM-assisted
"DescriptionDiffAlignmentCondition",
# Temporal
"AllowedHoursCondition",
"CommentResponseTimeCondition",
Expand Down
146 changes: 146 additions & 0 deletions src/rules/conditions/llm_assisted.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""LLM-assisted conditions for semantic rule evaluation.

This module contains conditions that use an LLM to perform evaluations
that cannot be expressed as deterministic checks. These conditions are
opt-in and clearly documented as having LLM latency in the evaluation path.
"""

import logging
from typing import Any

from pydantic import BaseModel, Field

from src.core.models import Severity, Violation
from src.rules.conditions.base import BaseCondition

logger = logging.getLogger(__name__)


class AlignmentVerdict(BaseModel):
"""Structured LLM response for description-diff alignment evaluation."""

is_aligned: bool = Field(description="Whether the PR description accurately reflects the code changes")
reason: str = Field(description="Brief explanation of the alignment or mismatch")
how_to_fix: str | None = Field(
description="Actionable suggestion for improving the description (only if misaligned)", default=None
)

Comment thread
coderabbitai[bot] marked this conversation as resolved.

_SYSTEM_PROMPT = """\
You are a senior code reviewer evaluating whether a pull request description \
accurately reflects the actual code changes shown in the diff.

Guidelines:
- A description is "aligned" if it describes the INTENT and SCOPE of the \
changes, even if it does not list every file.
- Minor omissions are acceptable (e.g., not mentioning a test file that \
accompanies a feature). Focus on whether the description would mislead a reviewer.
- Flag clear mismatches: description says "fix login bug" but diff only touches \
billing code; description claims refactoring but diff adds a new feature; \
description is entirely generic ("update code") with no mention of what changed.
- If the description is empty or trivially short (e.g. "fix", "update"), treat \
it as misaligned.
- Respond with structured output only. Do NOT include markdown or extra text."""

_HUMAN_PROMPT_TEMPLATE = """\
## PR title
{title}

## PR description
{description}

## Diff summary (top changed files)
{diff_summary}

## Changed file list
{file_list}

Evaluate whether the PR description aligns with the actual code changes."""


class DescriptionDiffAlignmentCondition(BaseCondition):
"""Validates that the PR description semantically matches the code diff.

This is the first LLM-backed condition in Watchflow. It uses the configured
AI provider (OpenAI / Bedrock / Vertex AI) to compare the PR description
against the diff summary and flag mismatches. Because it calls an LLM, it
adds latency (~1-3s) compared to deterministic conditions.

The condition gracefully degrades: if the LLM call fails (provider not
configured, rate limit, network error), it logs a warning and returns no
violation rather than blocking the PR.
"""

name = "description_diff_alignment"
description = "Validates that the PR description accurately reflects the actual code changes."
parameter_patterns = ["require_description_diff_alignment"]
event_types = ["pull_request"]
examples = [{"require_description_diff_alignment": True}]

async def evaluate(self, context: Any) -> list[Violation]:
"""Evaluate description-diff alignment using an LLM."""
parameters = context.get("parameters", {})
event = context.get("event", {})

if not parameters.get("require_description_diff_alignment"):
return []

pr_details = event.get("pull_request_details", {})
title = pr_details.get("title", "")
description_body = pr_details.get("body") or ""
diff_summary = event.get("diff_summary", "")
changed_files = event.get("changed_files", [])

# Nothing to compare against
if not changed_files:
return []

file_list = "\n".join(
f"- {f.get('filename', '?')} ({f.get('status', '?')}, "
f"+{f.get('additions', 0)}/-{f.get('deletions', 0)})"
for f in changed_files[:20]
)

human_prompt = _HUMAN_PROMPT_TEMPLATE.format(
title=title or "(no title)",
description=description_body or "(empty)",
diff_summary=diff_summary or "(no diff summary available)",
file_list=file_list,
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

try:
from langchain_core.messages import HumanMessage, SystemMessage

from src.integrations.providers import get_chat_model
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

llm = get_chat_model(
temperature=0.0,
max_tokens=512,
)
structured_llm = llm.with_structured_output(AlignmentVerdict, method="function_calling")

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
messages = [
SystemMessage(content=_SYSTEM_PROMPT),
HumanMessage(content=human_prompt),
]

verdict: AlignmentVerdict = await structured_llm.ainvoke(messages)

if not verdict.is_aligned:
return [
Violation(
rule_description=self.description,
severity=Severity.MEDIUM,
message=f"PR description does not align with code changes: {verdict.reason}",
how_to_fix=verdict.how_to_fix
or "Update the PR description to accurately summarize the intent and scope of the code changes.",
)
]

except Exception:
logger.warning(
"LLM call failed for description-diff alignment check; skipping.",
exc_info=True,
)

return []
3 changes: 3 additions & 0 deletions src/rules/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
MaxPrLocCondition,
TestCoverageCondition,
)
from src.rules.conditions.llm_assisted import DescriptionDiffAlignmentCondition
from src.rules.conditions.pull_request import (
DiffPatternCondition,
MinApprovalsCondition,
Expand Down Expand Up @@ -73,6 +74,7 @@
RuleID.CHANGELOG_REQUIRED: ChangelogRequiredCondition,
RuleID.NO_SELF_APPROVAL: NoSelfApprovalCondition,
RuleID.CROSS_TEAM_APPROVAL: CrossTeamApprovalCondition,
RuleID.DESCRIPTION_DIFF_ALIGNMENT: DescriptionDiffAlignmentCondition,
}

# Reverse map: condition class -> RuleID (for populating rule_id on violations)
Expand Down Expand Up @@ -106,6 +108,7 @@
CrossTeamApprovalCondition,
SignedCommitsCondition,
ChangelogRequiredCondition,
DescriptionDiffAlignmentCondition,
]


Expand Down
Loading
Loading