@@ -8,17 +8,23 @@ class PromptBuilder:
88 def build_system_prompt () -> str :
99 """Return the system prompt that establishes Guardian's reviewer persona."""
1010 return (
11- "You are the CGIS Guardian — a Senior Software Architect doing a code review. "
12- "Your goal is HIGH PRECISION: every finding you report must be a real problem "
13- "that exists in the actual diff. A false positive wastes engineering time just "
14- "as much as a missed bug. Quality of findings beats quantity. "
11+ "You are the CGIS Guardian finder — a Senior Software Architect hunting for "
12+ "defects in a code review. You are the FIRST of two stages: a separate skeptic "
13+ "verifier runs after you and removes false positives. Because of that division "
14+ "of labour, your job is RECALL — surface every plausible real defect. A missed "
15+ "bug is the expensive error here; a borderline finding is cheap, because the "
16+ "skeptic filters it. Surface a finding whenever you can name a CONCRETE failure "
17+ "scenario — a specific input, state, timing, or platform that makes the code "
18+ "wrong. Do not self-censor because you are unsure: hand the uncertainty to the "
19+ "skeptic with your reasoning, do not suppress it. "
1520 "You prioritise: (1) Logic correctness — wrong output, crashes, data corruption; "
1621 "(2) Library boundary contracts — convention mismatches at third-party API calls; "
1722 "(3) Missing test coverage for real edge cases in the diff; "
1823 "(4) Type safety violations that mypy strict would catch; "
1924 "(5) Ontology compliance — wrong NodeType/EdgeType mappings corrupt the graph. "
20- "You do NOT flag style preferences, naming conventions, or design disagreements "
21- "unless they cause a concrete defect. If the code is correct and tested, say so."
25+ "You still do NOT flag style preferences, naming conventions, or design "
26+ "disagreements that have no concrete failure scenario. If the code is correct "
27+ "and tested, say so."
2228 )
2329
2430 @staticmethod
@@ -62,42 +68,79 @@ def build_user_prompt(context: dict[str, str]) -> str:
6268{ drift }
6369"""
6470
65- return f"""Review the following Pull Request diff for real defects.
66-
67- ### 1. ENGINEERING STANDARDS (from CONTRIBUTING.md)
71+ contributing_section = ""
72+ if contributing :
73+ contributing_section = f""" ### 1. ENGINEERING STANDARDS (from CONTRIBUTING.md)
6874{ contributing }
6975
70- ### 2. PROJECT ONTOLOGY (from docs/ontology/)
76+ """
77+
78+ ontology_section = ""
79+ if ontology :
80+ ontology_section = f"""### 2. PROJECT ONTOLOGY (from docs/ontology/)
7181{ ontology }
7282
73- ### 3. CHANGES TO REVIEW (git diff)
83+ """
84+
85+ return f"""Review the following Pull Request diff for real defects.
86+
87+ { contributing_section } { ontology_section } ### 3. CHANGES TO REVIEW (git diff)
7488{ diff }
7589{ graph_section } { full_files_section } { drift_section }
7690---
77- ### PRECISION RULES — read before writing a single finding:
91+ ### HUNTING RULES — read before writing a single finding:
7892
79- 1. **Evidence first.** Quote the exact line(s) from the diff that prove the problem.
80- The quoted text MUST appear verbatim in section 3. If you cannot find it, do not raise it.
93+ 1. **Evidence first.** Quote the exact line(s) from the diff that the finding sits on.
94+ The quoted text MUST appear verbatim in section 3 (it positions the inline comment).
95+ If you cannot find the line, do not raise it.
8196
82- 2. **Confidence gate.** Before writing a finding, ask yourself:
83- "Am I at least 80% confident this is a real defect in this diff?"
84- If not — omit it entirely. Uncertain findings are not helpful.
97+ 2. **Surface on a nameable failure scenario.** Before writing a finding, ask yourself:
98+ "Can I describe one concrete input, state, timing, or platform where this code
99+ misbehaves?" If yes — REPORT it, and put your honest `confidence` in the field.
100+ Confidence does NOT gate inclusion: a 40%-confident finding with a real failure
101+ scenario is worth surfacing, because the skeptic verifier decides what to keep.
102+ Only drop a candidate when you cannot construct ANY failure scenario for it.
85103
861043. **No ghost issues.** If the code already handles a case, acknowledge it and move on.
87- Do not flag something as missing when it is present.
105+ Do not flag something as missing when it is present (this is a wrong finding, not
106+ an uncertain one — the skeptic cannot rescue precision from a fabricated claim).
88107
891084. **No invented rules.** Only cite standards explicitly written in CONTRIBUTING.md or the
90109 ontology files provided above. Do not apply rules from outside this context.
91110
92- 5. **Cap at 5 findings.** Report only the 5 most important real issues.
93- If you find fewer real issues, report fewer. Zero is a valid answer.
111+ 5. **No finding cap.** Report every real candidate you find — more genuine findings is
112+ strictly better here, since recall is your job and the skeptic trims the list. Order
113+ them most-severe first. Zero is still a valid answer when the diff is clean.
114+
115+ 6. **Reason per changed function before deciding.** For each function the diff touches,
116+ briefly walk these before you write findings for it:
117+ - What are its inputs and where do they come from (caller, config/YAML/JSON, env,
118+ request)? Is any of them external/untrusted?
119+ - What happens on the awkward inputs: empty / None / zero / a non-dict where a dict
120+ is assumed / a scalar where an iterable is assumed / unsorted / duplicate / oversized?
121+ - Did a deleted or changed line hold an invariant (a guard, a validation, an error
122+ path)? Is it re-established?
123+ Only after that walk do you decide what to surface. Skipping this walk is the main
124+ reason subtle defects (unvalidated data, exact-equality, dropped guards) go unseen.
94125
95126---
96127### WHAT TO LOOK FOR (focus areas):
97128
98129**Logic bugs** — inputs that produce wrong output, division by zero, off-by-one errors,
99130incorrect algorithm behaviour. Think: empty collections, None values, boundary conditions.
100131
132+ **Unvalidated external data** — a value read from config/YAML/JSON, env, or a request is
133+ used as a `dict`/`list`/`set`/iterable (subscripted, iterated, passed to `set()`/`dict()`,
134+ `.items()`, `for x in value`) without first checking its type or presence. A YAML key the
135+ author expects to be a mapping can legally be a scalar or a list; a `value or {{}}` idiom
136+ catches `None` but lets a non-dict truthy value through to operations that then misbehave
137+ silently rather than erroring. Flag each such use that lacks a type/shape guard.
138+
139+ **Exact-equality on floats / money** — bare `==` or `!=` comparing floating-point or
140+ Decimal values, *including in test assertions* (`assert x == 0.3`). Floating-point
141+ rounding makes these flaky or wrong; they should use a tolerance compare. Check changed
142+ test files for this too.
143+
101144**Missing test coverage** — code paths in the diff that have no test. Focus on edge cases
102145that could silently return wrong results (not just "coverage for coverage's sake").
103146
@@ -116,6 +159,32 @@ def build_user_prompt(context: dict[str, str]) -> str:
116159**Ontology compliance** — wrong NodeType/EdgeType assignments, FQNs not derived from file
117160paths, unresolved calls not using `raw_call:` prefix.
118161
162+ ---
163+ ### WORKED EXAMPLES (how the per-function walk turns into a finding):
164+
165+ These show the kind of subtle, borderline defect that is easy to skip but worth surfacing.
166+ Do not look for these exact lines — learn the *pattern* and apply it to the diff above.
167+
168+ Example A — unvalidated config value used as a mapping:
169+ ```
170+ def layers_for(self, name: str) -> set[str]:
171+ cfg = self._patterns.get(name) # cfg comes from a YAML file
172+ return set(cfg) # <-- assumes cfg is iterable-of-str
173+ ```
174+ Walk: input `cfg` is external (YAML); the author assumes a list/mapping, but YAML lets
175+ that key be a scalar (`name: layered`) → `set("layered")` silently yields `{{'l','a',...}}`,
176+ not an error. → FINDING: `set(cfg)` lacks a type/shape guard on external config data.
177+ (confidence ~50 — borderline, but it has a concrete failure scenario, so surface it.)
178+
179+ Example B — exact-equality on floats in a test:
180+ ```
181+ def test_drift_score():
182+ assert scorer.score(fp) == 0.3 # <-- bare == on a float result
183+ ```
184+ Walk: `score()` returns a computed float; `== 0.3` is exact float equality → rounding can
185+ make this assert flaky/false. → FINDING: float compared with bare `==` in a test; use a
186+ tolerance (`pytest.approx` / `math.isclose`). (confidence ~55.)
187+
119188---
120189### OUTPUT FORMAT:
121190
@@ -140,8 +209,9 @@ def build_user_prompt(context: dict[str, str]) -> str:
140209 diff in section 3 (no paraphrasing, no `+`/`-` marker). It is used to position the inline
141210 comment deterministically — if your "line" guess is off, a correct "anchor" still lands the
142211 comment on the right line. Use null only for genuinely file-level findings.
143- - "confidence" must be >= 80 to include a finding (the gate above).
144- - max 5 findings; fewer is fine; an empty list means LGTM.
212+ - "confidence" is your honest 0-100 estimate; it does NOT gate inclusion — the skeptic
213+ verifier uses it to prioritise, so report findings below 80 too.
214+ - no finding cap; report every real candidate, most-severe first; an empty list means LGTM.
145215- "summary" is mandatory; for an LGTM it lists what you checked and found correct.
146216
147217Example LGTM response: {{"findings": [], "summary": "Checked diff for logic and types; ok."}}"""
0 commit comments