Skip to content

feat(policy): fold config.anti_policies into the review prompt (#46) - #59

Closed
DYNOSuprovo wants to merge 1 commit into
exactml:masterfrom
DYNOSuprovo:feat/anti-policies
Closed

DYNOSuprovo wants to merge 1 commit into
exactml:masterfrom
DYNOSuprovo:feat/anti-policies

Conversation

@DYNOSuprovo

Copy link
Copy Markdown

Summary

Closes #46.

This PR adds support for config.anti_policies in .marginal/config.yaml. Anti-policies allow repositories to document deliberate, accepted tradeoffs or intentional patterns that the AI reviewer must never flag as issues.

Changes

  1. Config schema (marginal/config/schema.py):
    • Added anti_policies: list[str] = Field(default_factory=list) to MarginalConfig.
  2. Review CLI & Prompt Construction (marginal/cli/review.py):
    • In run_review, loads config.anti_policies via marginal.policy.load_policies(path, config.anti_policies) alongside config.policies, reusing the existing skip-and-warn handling for missing files.
    • In _build_finding_prompt, folds anti-policy content into the prompt under a distinct "do not flag" instruction framing:
      Do not flag any issues matching this repository's anti-policies below -- these are deliberate, accepted tradeoffs or intentional patterns that must never be reported as findings:
      
    • When no anti-policies are configured (default), the prompt remains byte-for-byte unchanged.
  3. Tests:
    • tests/test_config.py: verified default anti_policies == [] and valid parsing of configured anti_policies.
    • tests/test_cli.py:
      • verified default behavior leaves prompt unchanged.
      • verified configured anti-policy content is folded in under the "do not flag / never" instruction framing ahead of the diff.
      • verified both policies and anti_policies fold together in expected order ahead of the diff.
      • verified end-to-end marginal review with configured anti-policy reaches the LLM prompt.
      • verified missing anti-policy file warns to stderr without crashing the review.
  4. Changelog (CHANGELOG.md):
    • Added entry under ### New Features for ISSUE-46.

Verification

  • pytest -v: All 112 tests passed in 21.96s
  • ruff check .: All checks passed
  • ruff format --check .: 37 files already formatted

@exactml exactml self-assigned this Sep 11, 2026
@exactml exactml added backend backend dev feature labels Sep 11, 2026

@exactml exactml left a comment •

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DYNOSuprovo Really appreciate the contribution, especially since this is one of the first contributions to marginal! 🙌

I went through the changes and left a few comments around consistency and documentation. Nothing major; once those are addressed, I’d be happy to get this merged.

Thanks again for taking the time to contribute and help shape the project from the early days!

Comment thread marginal/cli/review.py
finding: Finding | None = None
if reviewer_model is not None:
policies = load_policies(path, config.policies)
anti_policies = load_policies(path, config.anti_policies)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering if we could also add a matching scaffold for anti_policies here. Currently, marginal init creates .marginal/policies/ via POLICIES_DIR, but there’s no equivalent directory for anti-policies.

Without it, users running marginal init from scratch may not realize this feature exists unless they happen to check the CHANGELOG or source code. Having something like .marginal/anti-policies/ would give both features a similar onboarding experience. What do you think?

Comment thread marginal/cli/review.py Outdated
Comment on lines +46 to +57
the content of any `config.policies` and `config.anti_policies` files
(loaded via `marginal.policy.load_policies`, relative to `path`; a
missing file is skipped with a warning rather than failing the review),
then runs it through `marginal.review.filter_findings`: dropped
outright if its self-reported `confidence` is below
`config.review.confidence_threshold`, otherwise capped alongside any
others at `config.review.max_comments` (highest-confidence first). A
finding that survives appends to the printed summary as a
severity+confidence badge (e.g. `🔴 **Critical** · 92% confidence`)
followed by its message. Without `models.reviewer`, or if the finding
gets filtered out, the summary stays metadata-only, same as if nothing
was generated.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to document anti_policies alongside policies here as well. The Configuration and “What it reviews for” sections currently explain .marginal/policies/ in detail, but don’t mention anti_policies.

Since this is a user-facing configuration option, giving it the same documentation treatment would make it much easier to discover, especially for users who aren’t looking through the source code.

Comment thread marginal/cli/review.py
Comment on lines +208 to +212
def _build_finding_prompt(
files: list[dict[str, object]],
policies: list[tuple[str, str]],
anti_policies: list[tuple[str, str]] | None = None,
) -> str:

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed that anti_policies defaults to None in _generate_finding / _build_finding_prompt (marginal/cli/review.py), while policies is required and has no default. Since the two are conceptually parallel, it feels a little odd to have them behave differently here.

I understand that the current default keeps the existing 2-arg test call working, but I think it might be cleaner either to give policies the same default for consistency.

Signed-off-by: DYNOSuprovo <DYNOSuprovo@users.noreply.github.com>
@DYNOSuprovo

Copy link
Copy Markdown
Author

@exactml Thanks for the thoughtful review and warm welcome! 🙌

I've addressed all the feedback and rebased onto the latest master:

  1. Scaffold for anti_policies in marginal init: Added .marginal/anti-policies/ directory and .marginal/anti-policies/README.md template scaffolding with guidance and suggested files (tradeoffs.md, patterns.md, exceptions.md), matching .marginal/policies/.
  2. Documentation: Documented anti_policies alongside policies across the README (under "What it reviews for", "Quickstart", and "Configuration" sections).
  3. Consistency of defaults: Updated _generate_findings and _build_finding_prompt so both policies and anti_policies consistently default to None.
  4. Rebase & conflict resolution: Resolved merge conflicts with recent code-graph additions on master, updated test mocks to wrap responses in _findings_response, and verified that all 181 unit tests pass cleanly along with ruff check.

The PR is now clean and MERGEABLE.

@DYNOSuprovo DYNOSuprovo closed this Oct 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add anti-policies: patterns marginal must never flag

2 participants