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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "postil-cli"
version = "0.8.18"
version = "0.8.19"
edition = "2024"
description = "Postil: a low-noise AI review gate. Silent on clean PRs, hard gate on real risk."
license = "Apache-2.0"
Expand Down
24 changes: 20 additions & 4 deletions src/llm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,7 @@ struct RawFinding {
severity: String,
#[serde(default)]
kind: Option<String>,
#[serde(default)]
repository_context: RawRepositoryContext,
#[serde(default = "default_confidence")]
confidence: f64,
Expand All @@ -523,6 +524,7 @@ struct RawFinding {
#[derive(Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
struct RawRepositoryContext {
#[serde(default)]
claim: RawRepositoryClaimKind,
#[serde(default)]
resources: Vec<String>,
Expand Down Expand Up @@ -8568,17 +8570,31 @@ mod tests {
}

#[test]
fn rejects_findings_without_explicit_repository_context() {
let missing = r#"{
fn defaults_omitted_repository_context_to_diff_local() {
let omitted = r#"{
"summary": "",
"findings": [{
"path": "config.yaml",
"line": 1,
"severity": "warn",
"body": "The counterpart remains on the old version."
"body": "The changed value is invalid."
}]
}"#;
assert!(parse_review(missing).is_err());
let parsed = parse_review(omitted).unwrap();
assert!(matches!(
parsed.findings[0].repository_context.claim,
RawRepositoryClaimKind::None
));

let empty = omitted.replace(
"\"body\": \"The changed value is invalid.\"",
"\"repositoryContext\": {},\n \"body\": \"The changed value is invalid.\"",
);
let parsed = parse_review(&empty).unwrap();
assert!(matches!(
parsed.findings[0].repository_context.claim,
RawRepositoryClaimKind::None
));

let explicit = r#"{
"summary": "",
Expand Down
39 changes: 19 additions & 20 deletions src/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,15 @@ pub fn review_contract(cfg: &Config) -> String {
`.postil/change-metadata`. Findings citing other lines are discarded as \
ungrounded.\n\
\n\
Every finding MUST include `repositoryContext`. Use `none` whenever the exact cited \
changed line is sufficient to establish the defect, even if unchanged code elsewhere \
could also be relevant. Use `absence` only when the conclusion depends on a construct \
missing from the complete reviewed head. Use `mismatch` only when the conclusion depends \
on a repository target having an expected value not established by the cited changed \
line; name that target in resources, paths, or identifiers and the expected value in \
values or versions. Populated arrays are conjunctive and refute only when matched in one \
file. Repository claims require the complete reviewed head. Public \
`repositoryContext` is optional and defaults to a diff-local finding. Omit it whenever \
the exact cited changed line is sufficient to establish the defect, even if unchanged \
code elsewhere could also be relevant. Include it only when the conclusion depends on \
repository-wide evidence, using `claim: absence` for a construct missing from the \
complete reviewed head or `claim: mismatch` for a repository target whose expected \
value is not established by the cited changed line. When included, name the target in \
`resources`, `paths`, or `identifiers` and the expected value in `values` or `versions`; \
include all five arrays even when empty. Populated arrays are conjunctive and refute \
only when matched in one file. Repository claims require the complete reviewed head. Public \
text names the concrete construct \
and correction, never review-input boundaries such as `in the diff`, retrieval mechanics, \
delegated evidence collection, or guessed files.\n",
Expand Down Expand Up @@ -223,8 +224,6 @@ pub fn system_prompt(cfg: &Config, current_utc_date: Date) -> String {
\"findings\": [{\"path\": \"file path from the diff\", \"line\": <new-file line>,\n \
\"endLine\": <optional>, \"severity\": \"info|warn|error\",\n \
\"kind\": \"risk|humanEscalation|guardrail|uncertainty|contentPolicy\", \"confidence\": <0..1>,\n \
\"repositoryContext\": {\"claim\": \"none|absence|mismatch\", \"resources\": [], \"values\": [],\n \
\"versions\": [], \"paths\": [], \"identifiers\": []},\n \
\"title\": \"short imperative title\", \"body\": \"specific, evidence-based markdown\",\n \
\"evidence\": \"exact non-empty new-side text from the cited line\"}]}\n\
\n\
Expand Down Expand Up @@ -546,17 +545,19 @@ mod tests {
}

#[test]
fn generator_reserves_repository_claims_for_repository_dependent_conclusions() {
fn generator_omits_repository_claims_for_diff_local_conclusions() {
let prompt = system_prompt(&Config::default(), trusted_date());
assert!(
prompt.contains("`repositoryContext` is optional and defaults to a diff-local finding")
);
assert!(prompt.contains(
"Use `none` whenever the exact cited changed line is sufficient to establish the defect"
));
assert!(prompt.contains(
"Use `absence` only when the conclusion depends on a construct missing from the complete reviewed head"
));
assert!(prompt.contains(
"Use `mismatch` only when the conclusion depends on a repository target having an expected value"
"Omit it whenever the exact cited changed line is sufficient to establish the defect"
));
assert!(
prompt.contains(
"Include it only when the conclusion depends on repository-wide evidence"
)
);
}

#[test]
Expand Down Expand Up @@ -649,8 +650,6 @@ mod tests {
\"findings\": [{\"path\": \"file path from the diff\", \"line\": <new-file line>,\n \
\"endLine\": <optional>, \"severity\": \"info|warn|error\",\n \
\"kind\": \"risk|humanEscalation|guardrail|uncertainty|contentPolicy\", \"confidence\": <0..1>,\n \
\"repositoryContext\": {\"claim\": \"none|absence|mismatch\", \"resources\": [], \"values\": [],\n \
\"versions\": [], \"paths\": [], \"identifiers\": []},\n \
\"title\": \"short imperative title\", \"body\": \"specific, evidence-based markdown\",\n \
\"evidence\": \"exact non-empty new-side text from the cited line\"}]}\n\
\n\
Expand Down
10 changes: 5 additions & 5 deletions src/review.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3297,12 +3297,12 @@ mod tests {
let below_floor = format!("Benchmark pull request{}", "x".repeat(442));

let local_budgets = batch_budgets_for_title(&local_edge);
assert_eq!(local_budgets.synthesis, 4_177);
assert_eq!(local_budgets.source, 4_178);
assert_eq!(local_budgets.synthesis, 4_227);
assert_eq!(local_budgets.source, 4_228);
assert!(review_batch_budgets_are_usable(local_budgets));
let ci_budgets = batch_budgets_for_title(&ci_edge);
assert_eq!(ci_budgets.synthesis, 4_161);
assert_eq!(ci_budgets.source, 4_162);
assert_eq!(ci_budgets.synthesis, 4_211);
assert_eq!(ci_budgets.source, 4_212);
assert!(review_batch_budgets_are_usable(ci_budgets));
assert_eq!(
ci_budgets.stabilized_for_rendering(),
Expand All @@ -3314,7 +3314,7 @@ mod tests {
let below_floor_budgets = batch_budgets_for_title(&below_floor);
assert_eq!(
below_floor_budgets.synthesis,
diff::MIN_REVIEW_BATCH_BYTES - 319
diff::MIN_REVIEW_BATCH_BYTES - 269
);
assert!(!review_batch_budgets_are_usable(below_floor_budgets));
}
Expand Down