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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Notable changes to JevGate. Versions follow [Semantic Versioning](https://semver

## [Unreleased]

- A shared-logic finding of a `--base` check or an agent's turn whose change left some copies untouched points at the change's own copy and names each untouched copy, saying whether its file is one the change edits. Its next step is to fix the change's copy, or mark the finding `later --note "#issue"` when sharing the logic would rewrite the untouched copies; the JSON report lists them under `untouched`. The agent hook holds a turn only for the change's copy. Fingerprints, and what fails the gate, are unchanged.

## [0.34.0] - 2026-10-03

A dismissal can say where it will be fixed: `baseline mark` takes a short `--note`, such as the issue that will fix a `later` finding, and `baseline list` prints the accepted findings by reason, as text, JSON or a Markdown checklist to paste into a cleanup issue. Findings, rules and fingerprints are unchanged.
Expand Down
95 changes: 92 additions & 3 deletions jevgate-baseline.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": 1,
"created_at": 1790976832,
Expand Down Expand Up @@ -709,14 +709,26 @@
"message": "This file may do several separate kinds of work, such as separate features, layers or integrations.",
"reason": "later"
},
{
"fingerprint": "7be216d324b25af08a32ae15ef77369d0bff302b27ea61aa8b48ddb0d7923986",
"rule": "maintainability/function-simplification",
"path": "src/evaluate.rs",
"line": 297,
"unit": "Session::evaluate",
"strength": "review",
"message": "`Session::evaluate` could likely be made simpler to read or change: it may be long, deeply nested, repetitive or mix separate jobs.",
"reason": "intended",
"note": "as on main; this PR only changes the compose_files call"
},
{
"fingerprint": "eb77e4f652658c21a450d81c491bebefa62143f89a1776ec4871d369f950a70a",
"rule": "maintainability/function-simplification",
"path": "src/evaluate.rs",
"line": 297,
"strength": "review",
"message": "`Session::evaluate` could likely be made simpler to read or change: it may be long, deeply nested, repetitive or mix separate jobs.",
"reason": "intended"
"reason": "intended",
"note": "as on main; this PR only changes the compose_files call"
},
{
"fingerprint": "f9f7ae0fc216c908682f9d8e1924d78e1bd15579a8ab2063b4f1c6c703c89493",
Expand Down Expand Up @@ -817,6 +829,17 @@
"message": "This test file may test several separate subjects.",
"reason": "later"
},
{
"fingerprint": "dae22b41c22054543983b0b7b16491335b908b0f6771909f4fa2f2598469ad57",
"rule": "maintainability/shared-logic",
"path": "src/hook/tests/mod.rs",
"line": 371,
"unit": "`a_copy_the_turn_left_untouched_never_holds_it_but_its_own_copy_does` (src/hook/tests/mod.rs:371) and `loader` (tests/cli/changes.rs:212)",
"strength": "review",
"message": "`a_copy_the_turn_left_untouched_never_holds_it_but_its_own_copy_does` (src/hook/tests/mod.rs:371) and `loader` (tests/cli/changes.rs:212) may repeat one piece of logic, so a change to it would have to be made in each place.",
"reason": "intended",
"note": "the binary's unit tests and the CLI test crate cannot share a fixture"
},
{
"fingerprint": "deb9de5dfe24d73e82b3e26115ff16eaf9c982aa964adf7ffd0edda5aec4000d",
"rule": "maintainability/function-simplification",
Expand Down Expand Up @@ -1198,7 +1221,19 @@
"line": 44,
"strength": "review",
"message": "`comment_findings` could likely be made simpler to read or change: it may be long, deeply nested, repetitive or mix separate jobs.",
"reason": "intended"
"reason": "intended",
"note": "as on main; this PR only adds the new empty untouched field"
},
{
"fingerprint": "0c5401505451733a846b4ef8cb1d253875e5638b656350ce74fdf1e843570218",
"rule": "maintainability/function-simplification",
"path": "src/units/compose/comments.rs",
"line": 44,
"unit": "comment_findings",
"strength": "review",
"message": "`comment_findings` could likely be made simpler to read or change: it may be long, deeply nested, repetitive or mix separate jobs.",
"reason": "intended",
"note": "as on main; this PR only adds the new empty untouched field"
},
{
"fingerprint": "0426cf29bc82927d5ba0d17e12226e080d2c849266ad2a8909303cd6fc633099",
Expand Down Expand Up @@ -1227,6 +1262,17 @@
"message": "`finding` could likely be made simpler to read or change: it may be long, deeply nested, repetitive or mix separate jobs.",
"reason": "intended"
},
{
"fingerprint": "c6aa055e22c707ebef59cf4a80880610aa6ca65847bfc395313b52f878a8e1c1",
"rule": "maintainability/function-simplification",
"path": "src/units/compose/mod.rs",
"line": 672,
"unit": "finding",
"strength": "review",
"message": "`finding` could likely be made simpler to read or change: it may be long, deeply nested, repetitive or mix separate jobs.",
"reason": "intended",
"note": "as on main; this PR only adds the new empty untouched field"
},
{
"fingerprint": "7b9c45a11bf97f35d12cf672422f6bb47560176fa5a5a91cd0618f98fed57922",
"rule": "maintainability/shared-logic",
Expand Down Expand Up @@ -1254,6 +1300,17 @@
"message": "`locates` could likely be made simpler to read or change: it may be long, deeply nested, repetitive or mix separate jobs.",
"reason": "intended"
},
{
"fingerprint": "32deda72f86d2190a95c4e251f268c89eacb26f4ee32ab2e0cf7910d52af0487",
"rule": "maintainability/shared-logic",
"path": "src/units/grouping.rs",
"line": 30,
"unit": "`group_repeats` (src/units/grouping.rs:30) and `move_findings` (src/units/untouched.rs:119)",
"strength": "review",
"message": "`group_repeats` (src/units/grouping.rs:30) and `move_findings` (src/units/untouched.rs:119) may repeat one piece of logic, so a change to it would have to be made in each place.",
"reason": "wrong",
"note": "a two-line loop calling the shared reorder helper"
},
{
"fingerprint": "368fb85e44e3230070b584f9350b9ab7288309c69c105e3b3e37d6064c3b5235",
"rule": "maintainability/function-simplification",
Expand Down Expand Up @@ -1389,6 +1446,17 @@
"message": "This file may do several separate kinds of work, such as separate features, layers or integrations.",
"reason": "later"
},
{
"fingerprint": "0fda0298cc8e2a6e108ca53c0f79d940f40c64d562ee309b132146e9b05fcb6e",
"rule": "maintainability/shared-logic",
"path": "src/units/plan/mod.rs",
"line": 196,
"unit": "`keep_changed` (src/units/plan/mod.rs:196) and `Changed::touches` (src/units/untouched.rs:35)",
"strength": "review",
"message": "`keep_changed` (src/units/plan/mod.rs:196) and `Changed::touches` (src/units/untouched.rs:35) may repeat one piece of logic, so a change to it would have to be made in each place.",
"reason": "wrong",
"note": "one lines.touch call on different spans: left-out code and a copy's location"
},
{
"fingerprint": "c5a9f017983841bd20189be39ba271545b3a7735726e4c7c6f31eb1f06be5621",
"rule": "maintainability/function-simplification",
Expand Down Expand Up @@ -1551,6 +1619,16 @@
"message": "`setup_hooks` could likely be made simpler to read or change: it may be long, deeply nested, repetitive or mix separate jobs.",
"reason": "wrong"
},
{
"fingerprint": "9d9627483b789c6a5777deb22e3d782551b8f364c26d9c14de4d3449ca88a3a5",
"rule": "maintainability/file-organization",
"path": "src/units/tests/changed.rs",
"line": 7,
"strength": "review",
"message": "This test file may test several separate subjects.",
"reason": "wrong",
"note": "every test checks what a --base check judges; the copy tests are about that too"
},
{
"fingerprint": "4eab3103044d7bfdeff38c62232428c8badd13b0db6ebfe106f74f49346b1b3a",
"rule": "maintainability/file-organization",
Expand Down Expand Up @@ -1693,7 +1771,18 @@
"line": 5,
"strength": "review",
"message": "This test file may test several separate subjects.",
"reason": "later"
"reason": "later",
"note": "as on main: the file mixes --base, --config and GitHub format tests; split by subject in a cleanup"
},
{
"fingerprint": "85a1fccf6670fd22ce4311acc7161f7fc94448188e5ef2180b68f13c042dd3e7",
"rule": "maintainability/file-organization",
"path": "tests/cli/changes.rs",
"line": 5,
"strength": "review",
"message": "This test file may test several separate subjects.",
"reason": "later",
"note": "as on main: the file mixes --base, --config and GitHub format tests; split by subject in a cleanup"
},
{
"fingerprint": "28ca0d566f63b03b435c74be6ae470766cc42b7c0c9cc1047e2bf48ca47e56ec",
Expand Down
2 changes: 1 addition & 1 deletion site/src/coding-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ finding and fix it when it is right; when it is mistaken, intended or left for
later, dismiss it with `jevgate baseline mark wrong|intended|later PATH:LINE`.
```

`--base` limits the review to what changed since that revision, uncommitted and untracked changes included: the functions, tests and comments on changed lines, and copies where either copy changed. A check asks about and reports only what the change touches, and cached answers make reruns free. The exit code says what to do next:
`--base` limits the review to what changed since that revision, uncommitted and untracked changes included: the functions, tests and comments on changed lines, and copies where either copy changed. A repeat points at the change's own copy and names the [copies it left untouched](rules/maintainability/shared-logic.md#copies-a-change-left-untouched), which never hold a turn: fix the change's copy, or mark the finding `later --note "#issue"`. A check asks about and reports only what the change touches, and cached answers make reruns free. The exit code says what to do next:

| Exit code | Meaning for the agent |
|---|---|
Expand Down
4 changes: 4 additions & 0 deletions site/src/rules/maintainability/shared-logic.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ A finding says two or more places may repeat one piece of logic, so a change to

Copies are compared within a package and across packages linked by a local dependency, copies inside example code are notes, which are not reported, and copies in code marked deprecated are not compared. A repeat whose every site lies inside tests a test-redundancy finding names is reported by that finding alone.

## Copies a change left untouched

With `--base`, and in an agent's turn, a repeat is asked about when the change touched at least one copy. When it left some copies untouched, the finding points at the change's own copy and names each untouched one, saying whether its file is one the change edits; the JSON report lists them under `untouched`, with `file_changed`. The change fixes its own copy, for example by reusing an untouched one; when sharing the logic would rewrite the untouched copies, it marks the finding `later --note "#issue"` and leaves them. The agent hook holds the end of a turn for the change's copy until it is fixed or marked, never for the untouched ones. The finding keeps the fingerprint its repeat has in a check of whole files, so a baseline accepts it in both.

## How it is measured

A look-here finding has not been labeled yet, so it says `Not yet measured.` and never fails the check by default. Each one a coding agent or person dismisses with a reason (`jevgate baseline mark wrong|intended|later PATH:LINE`) is counted by `jevgate baseline stats`, which is how its share of noise shows in daily use.
2 changes: 1 addition & 1 deletion src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ pub fn rule_version(key: &str) -> &'static str {
match key {
FILE_ORGANIZATION => "23",
FUNCTION_SIMPLIFICATION => "16",
SHARED_LOGIC => "23",
SHARED_LOGIC => "24",
TEST_VALUE => "7",
TEST_REDUNDANCY => "4",
INJECTION => "13",
Expand Down
1 change: 1 addition & 0 deletions src/changes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ mod tests {
gate: None,
precision: None,
preview: None,
untouched: Vec::new(),
}
}

Expand Down
15 changes: 12 additions & 3 deletions src/evaluate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ impl Session<'_> {
}
}
crate::progress::phase("composing findings");
compose_files(&plan, report);
compose_files(&plan, (inputs, self.args), report);
self.guard(&plan, report);
self.calibrate()?;
self.progress(report)
Expand Down Expand Up @@ -696,8 +696,13 @@ fn add_metrics(stage: &mut crate::schema::StageMetrics, m: &crate::schema::Stage
}

/// Compose each planned file's recorded judgments into dimensions and
/// findings, with the requests its units were first asked in.
fn compose_files(plan: &crate::units::Plan, report: &mut Report) {
/// findings, with the requests its units were first asked in. With a
/// change, a repeat whose copies it did not all touch points at its own.
fn compose_files(
plan: &crate::units::Plan,
(inputs, args): (&[Input], &CheckArgs),
report: &mut Report,
) {
let mut first = BTreeMap::<usize, Vec<&crate::units::Planned>>::new();
for planned in &plan.requests {
first.entry(planned.owner).or_default().push(planned);
Expand All @@ -716,6 +721,10 @@ fn compose_files(plan: &crate::units::Plan, report: &mut Report) {
}
crate::units::grouping::group_repeats(&mut report.files);
crate::units::compose::one_level(&mut report.files);
if args.changed_lines() {
let changed = crate::units::untouched::Changed::of(inputs);
crate::units::untouched::anchor(&changed, &mut report.files);
}
}

fn apply_classification(file: &mut FileResult, class: crate::file_kind::Classification) {
Expand Down
58 changes: 58 additions & 0 deletions src/hook/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,64 @@ fn a_turn_is_judged_on_what_it_changed_not_on_the_rest_of_its_files() {
assert_eq!(send(&project, &host, stop(false))["decision"], "block");
}

#[test]
fn a_copy_the_turn_left_untouched_never_holds_it_but_its_own_copy_does() {
// Two files repeat one loader; the turn edits `b.rs`'s copy and only
// the tail of `a.rs`, whose copy it leaves alone.
let load = |name: &str, field: &str, default: &str| {
format!(
"fn {name}(path: &str) -> Result<User> {{\n let text = std::fs::read_to_string(path)?;\n let value: Value = serde_json::from_str(&text)?;\n let name = value[\"{field}\"].as_str().unwrap_or(\"{default}\").trim().to_string();\n Ok(User {{ name }})\n}}\n"
)
};
let project = repository();
project.write("jevgate.toml", "rules = [\"shared-logic\"]\n");
project.write(
"a.rs",
&format!(
"{}\n{}",
load("load_user", "name", "anonymous"),
function("tail_a")
),
);
project.write("b.rs", &load("load_team", "title", "anonymous"));
project.commit_all();
let host = reviewing();
send(&project, &host, prompt("load teams"));
let tail = function("tail_a").replace("doubled + 1", "doubled + 2");
project.write(
"a.rs",
&format!("{}\n{tail}", load("load_user", "name", "anonymous")),
);
project.write("b.rs", &load("load_team", "title", "nobody"));
let blocked = send(&project, &host, stop(false));
let reason = blocked["reason"].as_str().unwrap_or_default();
assert!(
reason.contains("\n- b.rs:2 review maintainability/shared-logic: `load_team` (b.rs:2), which this change touched, may repeat logic that copies it left untouched also hold: `load_user` (a.rs:2, in a file this change edits)."),
"{blocked}"
);
assert!(
reason.contains("mark the finding `later --note"),
"{reason}"
);
// Marked for later, the change's own copy no longer holds the turn.
let marked = crate::baseline::mark(
&project.0,
&crate::baseline::Mark {
reason: crate::options::Disposition::Later,
note: Some(Some("#192".into())),
targets: &["b.rs:2".into()],
rules: &[],
},
);
assert_eq!(marked.unwrap(), 1);
let stopped = send(&project, &host, stop(true));
assert!(stopped.get("decision").is_none(), "{stopped}");
assert!(
message(&stopped).contains("b.rs:2 maintainability/shared-logic as later (#192)"),
"{stopped}"
);
}

#[test]
fn a_new_review_blocks_until_it_is_fixed_or_dismissed_with_a_reason() {
// The default gate still measures hardcoded values: their reviews do
Expand Down
2 changes: 1 addition & 1 deletion src/schema/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub use report::*;
pub const RUBRIC: &str = "jevgate-units-v1";
/// Changes how saved answers become a status. Included in the report identity
/// and not in the judgment cache, so unchanged questions are not sent again.
pub const COMPOSITION: &str = "unit-composition-v13";
pub const COMPOSITION: &str = "unit-composition-v14";
pub const SCHEMA_VERSION: u32 = 2;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
Expand Down
14 changes: 14 additions & 0 deletions src/schema/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,20 @@ pub struct Finding {
/// gate never fails on it. None elsewhere and for custom questions.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preview: Option<String>,
/// For a shared-logic finding of a check of changed lines, the copies
/// the change did not touch; the finding points at a copy it did.
/// Empty when the change touched every copy, or judged whole files.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub untouched: Vec<Untouched>,
}

/// A copy of a shared-logic finding that its change did not touch.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Untouched {
#[serde(flatten)]
pub location: Location,
/// Whether its file is one the change edits, elsewhere.
pub file_changed: bool,
}

impl Finding {
Expand Down
1 change: 1 addition & 0 deletions src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ pub(super) fn finding(strength: crate::schema::Strength) -> crate::schema::Findi
gate: None,
precision: None,
preview: None,
untouched: Vec::new(),
}
}

Expand Down
1 change: 1 addition & 0 deletions src/units/compose/comments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ pub(super) fn comment_findings(
gate: None,
precision: None,
preview: None,
untouched: Vec::new(),
}
})
.collect()
Expand Down
17 changes: 15 additions & 2 deletions src/units/compose/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ pub fn compose(plan: &FilePlan, judgments: &[Judgment], first: &[&Planned]) -> C
(rule.to_string(), dimension)
})
.collect();
// Strongest first, so a note never sits above a review or consider.
findings.sort_by(|a, b| b.strength.cmp(&a.strength).then(b.rank.total_cmp(&a.rank)));
strongest_first(&mut findings);
let status = file_status(&dimensions, &findings);
Composed {
dimensions,
Expand Down Expand Up @@ -509,6 +508,19 @@ fn one_level_counts(rule: &str, count: &mut UnitCounts) {
}
}

/// Strongest first, so a note never sits above a review or consider, then
/// by rank.
fn strongest_first(findings: &mut [Finding]) {
findings.sort_by(|a, b| b.strength.cmp(&a.strength).then(b.rank.total_cmp(&a.rank)));
}

/// A file's findings in order and its status again, after findings were
/// grouped or moved between files.
pub(super) fn reorder(file: &mut crate::schema::FileResult) {
strongest_first(&mut file.findings);
file.status = file_status(&file.dimensions, &file.findings);
}

pub(super) fn file_status(
dimensions: &BTreeMap<String, Dimension>,
findings: &[Finding],
Expand Down Expand Up @@ -791,5 +803,6 @@ fn finding(
gate: None,
precision: None,
preview: None,
untouched: Vec::new(),
}
}
1 change: 1 addition & 0 deletions src/units/compose/redundant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,6 @@ pub(super) fn group_finding(plan: &FilePlan, cluster: Cluster<'_>) -> Finding {
gate: None,
precision: None,
preview: None,
untouched: Vec::new(),
}
}
Loading
Loading