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

- File organization: a long application file (400 lines or more) whose outline, recheck and kind of file raised no finding is asked about its candidate parts, one request per part with the part's source: whether it does a job of its own that a reader would look for apart from the rest, and what it is within the file. A part of 100 lines or more whose answer reaches 0.65 and whose role leans to a job of its own is a consider naming its members. The kind of file cleared long single-type files wholesale: of 50 such files labeled from the code, 15 were worth splitting (a URL scraper inside lobsters' `Story`, a diff engine inside a renderer, a JSON parser inside a protocol module), and asking each part found 4 of them, at the part the labeler named, and no file to keep. The parts are the outline's groups without the links that merged every method of a large class into one group and without links through a helper most members call, with links for neighbours and for names sharing a distinctive word. On the corpus, 11 considers were added and nothing else changed: 9 right and 2 wrong (a demo page's placeholder table, a class's public API), 5 of 7 right outside the files used for tuning and 1 of 1 on the held-out projects; on 9 projects never used before (click, rich, zod, hono, viper, ripgrep, sinatra, jsoup, guzzle), 2 of 2 (Guzzle's `WWW-Authenticate` parser inside `DigestAuth`, ripgrep's `--hyperlink-format` language). File-organization considers were 63% right before. Benchmarks, examples and `scripts` and `docs` directories are not asked (5 of 5 such findings were wrong), nor is a part holding `main`. About $0.08 on the corpus. JevGate's own large files stay clear: their parts read as one job, and what decided them by hand, which other files use a part, is missing for Rust functions passed by name.
- A request whose answer is already cached fits the provider limit, whatever the token calibration says. The calibration (`.jevgate/token-budget.json`) is replaced after each run by the bytes per token of that run's fresh requests, and a request near the limit, such as a recheck that sends a whole file, was sent in one run and not the next: the file's finding changed with no change to its code. On the corpus, 0.23.1 under three calibrations (the last run's, a snapshot's and a stricter one) differed in one consider and five undecided units, and under the stricter one asked 42 thousand new tokens of questions; with this, only three units whose recheck the provider refused as too long still differ, and nothing new is asked. `--refresh` skips the cache, so it plans by the estimate alone.

## [0.23.1] - 2026-09-27

Expand Down
16 changes: 12 additions & 4 deletions src/evaluate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use super::{
options::CheckArgs,
schema::{self, FileResult, Report, Status},
storage::Store,
token_budget::TokenBudget,
token_budget::{Limits, TokenBudget},
transport::Evaluator,
};
use crate::config::ConfigContext;
Expand Down Expand Up @@ -129,13 +129,16 @@ fn empty_report(args: &CheckArgs, current: &SnapshotContext<'_>, files: Vec<File
/// blocks) are not known yet.
fn preview(inputs: &[Input], args: &CheckArgs, root: &std::path::Path, report: &mut Report) {
let budget = &TokenBudget::load(root);
let answered =
|request: &serde_json::Value| crate::requests::answered(root, args, request).is_some();
let limits = Limits::new(budget, &answered);
let mut planned = Vec::new();
let mut views = BTreeMap::new();
for (owner, input) in inputs.iter().enumerate() {
if report.files[owner].status != Status::Pending {
continue;
}
match schedule(input, args, budget, &mut report.files[owner]) {
match schedule(input, args, limits, &mut report.files[owner]) {
Ok(Scheduled::None) => {}
Ok(Scheduled::Purpose(request)) => {
match cached_purpose(input, args, root, &request, &mut report.files[owner]) {
Expand Down Expand Up @@ -263,12 +266,17 @@ impl Session<'_> {
) {
let mut purpose = Vec::new();
let mut views = BTreeMap::new();
let root = &self.context.root;
let answered = |request: &serde_json::Value| {
crate::requests::answered(root, self.args, request).is_some()
};
let limits = Limits::new(&self.budget, &answered);
for (owner, file) in report.files.iter_mut().enumerate() {
if file.status != Status::Pending {
continue;
}
file.judgments.clear();
match schedule(&inputs[owner], self.args, &self.budget, file) {
match schedule(&inputs[owner], self.args, limits, file) {
Ok(Scheduled::None) => file.cached = false,
Ok(Scheduled::Purpose(request)) => {
file.cached = true;
Expand Down Expand Up @@ -551,7 +559,7 @@ fn apply_classification(file: &mut FileResult, class: crate::file_kind::Classifi
fn schedule(
input: &Input,
args: &CheckArgs,
budget: &TokenBudget,
budget: Limits<'_>,
file: &mut FileResult,
) -> Result<Scheduled> {
if file.status != Status::Pending {
Expand Down
10 changes: 5 additions & 5 deletions src/file_kind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::{
options::CheckArgs,
policy,
schema::{FileResult, SourceRange, Status},
token_budget::TokenBudget,
token_budget::Limits,
};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -165,7 +165,7 @@ pub fn language(path: &Path) -> &'static str {
}
}

pub(crate) fn plan(input: &Input, args: &CheckArgs, budget: &TokenBudget) -> Result<Plan> {
pub(crate) fn plan(input: &Input, args: &CheckArgs, budget: Limits<'_>) -> Result<Plan> {
let format = crate::docs::format::Format::of(&input.result.path).language();
if input.result.role == crate::inventory::INSTRUCTIONS {
return Ok(Plan::Ready(document(
Expand Down Expand Up @@ -640,8 +640,8 @@ fn extension(path: &Path) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::Status;
use crate::tests::{Project, args, run};
use crate::{schema::Status, token_budget::TokenBudget};

const MIXED: &str = "fn production(value: &str) -> String {\n value.trim().to_string()\n}\n\n#[cfg(test)]\nmod tests {\n use super::production;\n\n fn helper(value: &str) -> String {\n production(value)\n }\n\n #[test]\n fn checks_production() {\n assert_eq!(helper(\" a \"), \"a\");\n }\n}\n";

Expand All @@ -657,7 +657,7 @@ mod tests {
let input = crate::inventory::collect(options, &project.context(), &[])
.unwrap()
.remove(0);
match plan(&input, options, &TokenBudget::default()).unwrap() {
match plan(&input, options, TokenBudget::default().uncached()).unwrap() {
Plan::Ready(view) => view,
_ => panic!("expected a gate view"),
}
Expand Down Expand Up @@ -736,7 +736,7 @@ mod tests {
.unwrap()
.remove(0);
assert!(matches!(
plan(&input, &args(), &TokenBudget::default()).unwrap(),
plan(&input, &args(), TokenBudget::default().uncached()).unwrap(),
Plan::Purpose(..)
));
// Outside a test path, `main` is the program's entry.
Expand Down
37 changes: 37 additions & 0 deletions src/token_budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,43 @@ const MAX_BYTES_PER_TOKEN: f64 = 6.0;
/// through that the provider refused as beyond its context.
const STRUCTURED_BYTES_PER_TOKEN: f64 = 2.0;

/// The budget as a run applies it to one request: the calibrated estimate,
/// or the answer cache when it already holds that request's answer. The
/// calibration follows the fresh requests of the last run, so a request near
/// the limit fit in one run and not the next: two runs of one release on a
/// pinned project differed in a file's recheck, and so in its finding. A
/// request answered once fits from then on.
#[derive(Clone, Copy)]
pub struct Limits<'a> {
budget: &'a TokenBudget,
answered: &'a dyn Fn(&Value) -> bool,
}

impl<'a> Limits<'a> {
pub fn new(budget: &'a TokenBudget, answered: &'a dyn Fn(&Value) -> bool) -> Self {
Self { budget, answered }
}

pub fn fits(&self, request: &Value) -> bool {
self.budget.fits(request) || (self.answered)(request)
}

pub fn fits_structured(&self, request: &Value) -> bool {
self.budget.fits_structured(request) || (self.answered)(request)
}
}

#[cfg(test)]
impl TokenBudget {
/// The estimate alone, for plans made without an answer cache.
pub fn uncached(&self) -> Limits<'_> {
fn never(_: &Value) -> bool {
false
}
Limits::new(self, &never)
}
}

/// The bytes-per-token ratio, calibrated from observed `usage.input_tokens` and
/// saved in `.jevgate/`.
#[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize, PartialEq)]
Expand Down
4 changes: 2 additions & 2 deletions src/units/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
inventory::Input,
options::CheckArgs,
schema::Pass,
token_budget::TokenBudget,
token_budget::Limits,
};
use serde_json::{Value, json};
use std::{
Expand Down Expand Up @@ -46,7 +46,7 @@ fn project(path: &Path) -> PathBuf {
pub(super) fn plan(
files: &[(usize, &Input)],
args: &CheckArgs,
budget: &TokenBudget,
budget: Limits<'_>,
plans: &mut BTreeMap<usize, FilePlan>,
requests: &mut Vec<Planned>,
) {
Expand Down
4 changes: 2 additions & 2 deletions src/units/evidence.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Request building shared by every planner: one file's facts, the request
//! envelope, packing, and stable identities.
use super::{Asked, PACK_ITEMS, Questions};
use crate::{schema::Location, token_budget::TokenBudget};
use crate::{schema::Location, token_budget::Limits};
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use std::{collections::BTreeMap, path::Path};
Expand All @@ -19,7 +19,7 @@ pub(super) struct FileContext<'a> {
pub source: &'a str,
pub source_hash: &'a str,
pub model: &'a str,
pub budget: &'a TokenBudget,
pub budget: Limits<'a>,
/// What a web framework makes of the file, such as a Next.js route
/// handler or Server Actions module, sent beside its path.
pub framework: Option<String>,
Expand Down
4 changes: 2 additions & 2 deletions src/units/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use crate::{
catalog::SENSITIVE_DATA,
options::CheckArgs,
schema::Pass,
token_budget::TokenBudget,
token_budget::Limits,
};
use classes::error_classes;
use implemented::implemented;
Expand All @@ -39,7 +39,7 @@ pub(super) fn plan(
scope: &Scope<'_>,
evidence: &Evidence<'_>,
args: &CheckArgs,
budget: &TokenBudget,
budget: Limits<'_>,
result: &mut Plan,
) {
let handlers = error_handlers(scope, evidence.links);
Expand Down
6 changes: 3 additions & 3 deletions src/units/plan/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
file_kind::View,
inventory::Input,
options::CheckArgs,
token_budget::TokenBudget,
token_budget::Limits,
units::{
FileContext, FilePlan, Planned, comments, duplicates, functions, hardcoded, laws, outline,
spacetimedb, test_units,
Expand All @@ -29,7 +29,7 @@ pub(super) fn plan_file(
shared: &Shared<'_>,
owner: usize,
args: &CheckArgs,
budget: &TokenBudget,
budget: Limits<'_>,
requests: &mut Vec<Planned>,
) -> FilePlan {
let input = &scope.inputs[owner];
Expand Down Expand Up @@ -134,7 +134,7 @@ fn file_context<'a>(
input: &'a Input,
owner: usize,
args: &'a CheckArgs,
budget: &'a TokenBudget,
budget: Limits<'a>,
) -> FileContext<'a> {
FileContext {
owner,
Expand Down
9 changes: 6 additions & 3 deletions src/units/plan/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{
file_kind::View,
inventory::Input,
options::CheckArgs,
token_budget::TokenBudget,
token_budget::{Limits, TokenBudget},
};

use std::{
Expand Down Expand Up @@ -89,6 +89,9 @@ pub fn plan(
budget: &TokenBudget,
root: &std::path::Path,
) -> Plan {
let answered =
|request: &serde_json::Value| crate::requests::answered(root, args, request).is_some();
let budget = Limits::new(budget, &answered);
let mut result = Plan::default();
let scope = parsed_scope(inputs, views, &mut result.skipped);
let mut shared = Shared::new(&scope, args);
Expand Down Expand Up @@ -134,7 +137,7 @@ pub fn plan(
}

/// Each GitHub Actions workflow file's jobs.
fn plan_workflows(scope: &Scope<'_>, args: &CheckArgs, budget: &TokenBudget, result: &mut Plan) {
fn plan_workflows(scope: &Scope<'_>, args: &CheckArgs, budget: Limits<'_>, result: &mut Plan) {
for &owner in &scope.configuration {
let input = &scope.inputs[owner];
if input.result.role != crate::inventory::WORKFLOW {
Expand Down Expand Up @@ -164,7 +167,7 @@ fn plan_document(
input: &Input,
owner: usize,
args: &CheckArgs,
budget: &TokenBudget,
budget: Limits<'_>,
drift: &drift::Shared<'_>,
requests: &mut Vec<Planned>,
) -> FilePlan {
Expand Down
2 changes: 1 addition & 1 deletion src/units/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ fn planned(project: &Project, options: &CheckArgs) -> (Vec<Input>, Plan) {
.iter()
.enumerate()
.filter_map(
|(i, input)| match crate::file_kind::plan(input, options, &budget) {
|(i, input)| match crate::file_kind::plan(input, options, budget.uncached()) {
Ok(crate::file_kind::Plan::Ready(view)) => Some((i, view)),
_ => None,
},
Expand Down
27 changes: 27 additions & 0 deletions src/units/tests/organization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,33 @@ fn an_outline_too_long_for_a_recheck_is_decided_by_its_kind_alone() {
assert!(kind.request()["state"]["file"]["source"].is_null());
}

#[test]
fn a_request_answered_once_fits_whatever_the_calibration() {
// About 79 KB: the recheck sends it whole, which the estimate fits at the
// default 3.0 bytes per token and not at 2.0.
let padding = format!("// {}\n", "x".repeat(100)).repeat(700);
let (project, options) = organized("lib.rs", &format!("{}{padding}", two_concerns()));
let first = run(&project, &options, &mut scripted(3));
assert_eq!(first.stages["recheck"].successful_requests, 1);
let context = project.context();
let (inputs, mut report) = crate::tests::snapshot(&project, &options);
let store = crate::storage::Store::open(&project.0).unwrap();
let mut mock = Mock::default();
let mut session = crate::tests::session(&options, &context, &store, &mut mock);
session.budget = TokenBudget {
bytes_per_token: 2.0,
};
session.evaluate(&inputs, &mut report).unwrap();
assert_eq!(mock.calls, 0, "every request comes from the cache");
assert_eq!(report.stages["recheck"].cache_hits, 1);
let status = |report: &Report| {
report.files[0].dimensions["file_organization"]
.status
.clone()
};
assert_eq!(status(&report), status(&first));
}

#[test]
fn outlines_carry_member_and_file_sizes() {
let (project, options) = rule_project(&two_concerns(), catalog::FILE_ORGANIZATION);
Expand Down
2 changes: 1 addition & 1 deletion src/units/tests/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ fn packing_and_cache_identity_do_not_depend_on_token_calibration() {
let budget = TokenBudget { bytes_per_token };
let views = BTreeMap::from([(
0,
match crate::file_kind::plan(&inputs[0], &options, &budget).unwrap() {
match crate::file_kind::plan(&inputs[0], &options, budget.uncached()).unwrap() {
crate::file_kind::Plan::Ready(view) => view,
_ => unreachable!(),
},
Expand Down
Loading