From a4ccf4754f8fe4b83ab04a11ce412e2c1ec6ce0b Mon Sep 17 00:00:00 2001 From: bompus Date: Tue, 27 Oct 2026 16:45:26 -0600 Subject: [PATCH 1/2] resolve: bound the name-match pool for generated/artifact-dominated refs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Profiling the pretix straggler page (2.1s of a 4.7s resolution) showed minified-bundle refs paying ~10x the normal per-ref cost: `t`-style names in fabric.min.js fetch candidate pools of thousands, and the retain/scope chain runs per candidate. Two bounds, both declining the name match entirely (unresolved, never a truncated winner; the framework merge still applies downstream): - refs whose file is generated-flagged bail on pools > 256; - any ref bails on a >256 pool that is >=75% generated nodes — an artifact-dominated pool cannot hold a real target, and this catches vendored-but-unflagged sources (d3.v6.js, fabric.js, pdf.worker.js). Handwritten pools that large (Migration, __init__) are untouched. `generated` rides ResolveRefIn via readPendingBatch (files.generated JOIN + the same path convention generatedPredicateFor uses) and via resolveChunkWithKernel (generatedPredicateFor). Measured: exact graph parity on pretix (32,799/73,404/165,700, identical edge kinds); the ~950ms pathological chunks are gone (worst chunk ~250ms). Batch-4 wall is unchanged within noise — its residual cost is ~19k vendored-JS refs at ~0.5-1ms each, spread, not one pool. Generated files still resolve 5,266 edges on pretix; a capped ref can only lose an edge when its name-match pool exceeded the bound (zero instances on pretix). --- .../src/resolve/declarations_upstream.rs | 2 +- codegraph-kernel/src/resolve/fields.rs | 4 +- codegraph-kernel/src/resolve/go_asserted.rs | 2 +- .../src/resolve/js_objects_upstream.rs | 2 +- codegraph-kernel/src/resolve/kotlin_calls.rs | 2 +- .../src/resolve/kotlin_type_scope.rs | 2 +- codegraph-kernel/src/resolve/mod.rs | 22 ++++++++--- codegraph-kernel/src/resolve/names.rs | 39 ++++++++++++++++++- codegraph-kernel/src/resolve/php_scope.rs | 2 +- .../src/resolve/resolver_upstream.rs | 2 +- codegraph-kernel/src/resolve/rust_paths.rs | 2 +- src/extraction/kernel/loader.ts | 3 ++ src/resolution/index.ts | 33 ++++++++++------ 13 files changed, 89 insertions(+), 28 deletions(-) diff --git a/codegraph-kernel/src/resolve/declarations_upstream.rs b/codegraph-kernel/src/resolve/declarations_upstream.rs index 69c2181fc1..4aa57d7224 100644 --- a/codegraph-kernel/src/resolve/declarations_upstream.rs +++ b/codegraph-kernel/src/resolve/declarations_upstream.rs @@ -465,7 +465,7 @@ impl KernelResolver { candidates: None, file_path: n.file_path.clone(), language: n.language.clone(), - failure_reason: None, + failure_reason: None, generated: None, }; self.ensure_cpp_default_index(&n.file_path, &site)?; let namespaces = self diff --git a/codegraph-kernel/src/resolve/fields.rs b/codegraph-kernel/src/resolve/fields.rs index 519b53f21a..b6bf6326a4 100644 --- a/codegraph-kernel/src/resolve/fields.rs +++ b/codegraph-kernel/src/resolve/fields.rs @@ -227,7 +227,7 @@ impl KernelResolver { .map(|line| strip_line_comments(line)).collect::>().join("\n"); if owner.kind == "type_alias" { // Alias method sets follow their targets; defined types do not. - let site = ResolveRefIn { from_node_id: owner.id.clone(), reference_name: owner.name.clone(), reference_kind: "references".to_string(), line: owner.start_line, column: owner.start_column, file_path: owner.file_path.clone(), language: "go".to_string(), row_id: None, candidates: None, failure_reason: None }; + let site = ResolveRefIn { from_node_id: owner.id.clone(), reference_name: owner.name.clone(), reference_kind: "references".to_string(), line: owner.start_line, column: owner.start_column, file_path: owner.file_path.clone(), language: "go".to_string(), row_id: None, candidates: None, failure_reason: None, generated: None }; if let Some(tree) = self.parsed_tree(&lines, &site) { let mut declaration = super::iteration::descendant_for_position( tree.root_node(), lines.text(), @@ -542,7 +542,7 @@ impl KernelResolver { candidates: None, file_path: cls.file_path.clone(), language: cls.language.clone(), - failure_reason: None, + failure_reason: None, generated: None, }; let mut owner: Option> = Some(Arc::new(cls.clone())); if let Some(holder) = holder { diff --git a/codegraph-kernel/src/resolve/go_asserted.rs b/codegraph-kernel/src/resolve/go_asserted.rs index 68c15daf02..2641b80f5d 100644 --- a/codegraph-kernel/src/resolve/go_asserted.rs +++ b/codegraph-kernel/src/resolve/go_asserted.rs @@ -303,7 +303,7 @@ fn local_at(tree: &tree_sitter::Tree, source: &str, name: &str, line: i64, colum impl KernelResolver { pub(super) fn go_local_declaration_at(&mut self, name: String, file_path: String, line: i64, column: i64) -> Result> { let r = ResolveRefIn { row_id: None, from_node_id: String::new(), reference_name: name.clone(), reference_kind: "references".into(), - line, column, candidates: None, file_path: file_path.clone(), language: "go".into(), failure_reason: None }; + line, column, candidates: None, file_path: file_path.clone(), language: "go".into(), failure_reason: None, generated: None }; let Some(file) = self.read_file(&file_path) else { return Ok(None) }; let Some(tree) = self.parsed_tree(&file, &r) else { return Ok(None) }; Ok(local_at(&tree, file.text(), &name, line, column)) diff --git a/codegraph-kernel/src/resolve/js_objects_upstream.rs b/codegraph-kernel/src/resolve/js_objects_upstream.rs index 0f9f809454..60114eb03d 100644 --- a/codegraph-kernel/src/resolve/js_objects_upstream.rs +++ b/codegraph-kernel/src/resolve/js_objects_upstream.rs @@ -165,7 +165,7 @@ impl KernelResolver { let site = ResolveRefIn { row_id: None, from_node_id: n.id.clone(), reference_name: root.to_string(), reference_kind: "references".to_string(), line: n.start_line, column: n.start_column, candidates: None, - file_path: n.file_path.clone(), language: n.language.clone(), failure_reason: None }; + file_path: n.file_path.clone(), language: n.language.clone(), failure_reason: None, generated: None }; if self.js_root_binding_scope(&site, root).is_some() { return Ok(false); } let global = if global_host(n) { let host = root; diff --git a/codegraph-kernel/src/resolve/kotlin_calls.rs b/codegraph-kernel/src/resolve/kotlin_calls.rs index b1199f7bc5..7d5901f29f 100644 --- a/codegraph-kernel/src/resolve/kotlin_calls.rs +++ b/codegraph-kernel/src/resolve/kotlin_calls.rs @@ -91,7 +91,7 @@ impl KernelResolver { let call = re!(r"(?:^|[^\w$])([a-z_]\w*)\s*(?:<[^<>{}]*>)?\s*(?:\([^(){}]*\))?\s*$").captures(&pending).map(|m| m[1].to_string()); if let Some(call) = call.filter(|name| !matches!(name.as_str(), "if" | "else" | "for" | "while" | "do" | "when" | "try" | "catch" | "finally" | "init" | "get" | "set" | "constructor" | "fun" | "class" | "object" | "interface" | "return" | "by" | "lazy" | "apply" | "run" | "also" | "let" | "with" | "use")) { if !type_lambdas { stack.push((line, vec![UNTYPED_LAMBDA.to_string()])); pending.clear(); continue; } - let site = ResolveRefIn { row_id: None, from_node_id: String::new(), reference_name: call.clone(), reference_kind: "calls".to_string(), line, column: 0, candidates: None, file_path: file.to_string(), language: "kotlin".to_string(), failure_reason: None }; + let site = ResolveRefIn { row_id: None, from_node_id: String::new(), reference_name: call.clone(), reference_kind: "calls".to_string(), line, column: 0, candidates: None, file_path: file.to_string(), language: "kotlin".to_string(), failure_reason: None, generated: None }; let mut owners: HashSet = stack.iter().flat_map(|(_, names): &(i64, Vec)| names.iter().filter(|name| name.as_str() != "*").cloned()).collect(); // An explicit typed receiver also proves dispatch: m.single { ... }, m: Module. // One shared pattern with the callee captured, not a pattern compiled per call name: diff --git a/codegraph-kernel/src/resolve/kotlin_type_scope.rs b/codegraph-kernel/src/resolve/kotlin_type_scope.rs index 7e5e6ce8aa..41cbdc9127 100644 --- a/codegraph-kernel/src/resolve/kotlin_type_scope.rs +++ b/codegraph-kernel/src/resolve/kotlin_type_scope.rs @@ -67,7 +67,7 @@ impl KernelResolver { candidates: None, file_path: declaration.file_path.clone(), language: "kotlin".to_string(), - failure_reason: None, + failure_reason: None, generated: None, }; let Some(tree) = self.parsed_tree(&source, &site) else { continue; diff --git a/codegraph-kernel/src/resolve/mod.rs b/codegraph-kernel/src/resolve/mod.rs index fd48aeb95b..9157b8e780 100644 --- a/codegraph-kernel/src/resolve/mod.rs +++ b/codegraph-kernel/src/resolve/mod.rs @@ -175,6 +175,11 @@ pub struct ResolveRefIn { pub file_path: String, pub language: String, pub failure_reason: Option, + /// Files marked generated at index time (minified bundles, codegen + /// output) get a bounded name-match: their same-name candidate pools + /// are minification artifacts, and scanning all of them costs ~10× a + /// normal ref. Absent/NULL means false. + pub generated: Option, } impl ResolveRefIn { @@ -1086,10 +1091,11 @@ impl KernelResolver { ) -> Result> { const PREREQ_KINDS: &str = "('imports','extends','implements')"; let sql = format!( - "SELECT id, from_node_id, reference_name, reference_kind, line, col, \ - candidates, file_path, language, failure_reason FROM unresolved_refs \ - WHERE status = 'pending' AND id > ?1 \ - AND reference_kind {} {} ORDER BY id LIMIT ?2", + "SELECT r.id, r.from_node_id, r.reference_name, r.reference_kind, r.line, r.col, \ + r.candidates, r.file_path, r.language, r.failure_reason, f.generated \ + FROM unresolved_refs r LEFT JOIN files f ON f.path = r.file_path \ + WHERE r.status = 'pending' AND r.id > ?1 \ + AND r.reference_kind {} {} ORDER BY r.id LIMIT ?2", if prerequisites { "IN" } else { "NOT IN" }, PREREQ_KINDS ); @@ -1110,6 +1116,7 @@ impl KernelResolver { row.get::<_, Option>(7)?.unwrap_or_default(), row.get::<_, Option>(8)?.unwrap_or_default(), row.get::<_, Option>(9)?, + row.get::<_, Option>(10)?.map(|g| g != 0), )) }) .map_err(|e| Error::from_reason(e.to_string()))?; @@ -1119,7 +1126,7 @@ impl KernelResolver { } drop(stmt); let mut out = Vec::new(); - for (row_id, from_node_id, reference_name, reference_kind, line, column, candidates, mut file_path, mut language, failure_reason) in collected + for (row_id, from_node_id, reference_name, reference_kind, line, column, candidates, mut file_path, mut language, failure_reason, generated) in collected { // Denormalized fallback — mirrors UnresolvedRef construction in // resolveBatchYielding: `raw.filePath || nodePath`, @@ -1135,6 +1142,10 @@ impl KernelResolver { } } } + // Union with the path convention — same rule as + // generatedPredicateFor on the TS side: the DB flag OR a + // generated-looking filename. + let generated = generated == Some(true) || crate::textutil::is_generated_file(&file_path); out.push(ResolveRefIn { row_id: Some(row_id), from_node_id, @@ -1146,6 +1157,7 @@ impl KernelResolver { file_path, language, failure_reason, + generated: Some(generated), }); } Ok(out) diff --git a/codegraph-kernel/src/resolve/names.rs b/codegraph-kernel/src/resolve/names.rs index 35000c2fde..d1ad0b09d7 100644 --- a/codegraph-kernel/src/resolve/names.rs +++ b/codegraph-kernel/src/resolve/names.rs @@ -7,6 +7,14 @@ use super::name_scope::{can_name_in_type_position, is_bare_rust_name}; /// no repository class uses may claim: a guess, not a proven link. const PHP_UNSEEN_ANCESTOR_CONFIDENCE: f64 = 0.5; +/// Max same-name candidate pool a generated-file ref will evaluate: real +/// same-name pools sit in the tens; minified bundles push thousands of +/// artifacts into one pool and turn each ref's retain/scope chain +/// quadratic (pretix's fabric.min.js: ~950ms per 250-ref chunk vs a ~90ms +/// floor). Over the cap the name-match declines — unresolved, never a +/// truncated winner. +const GENERATED_NAME_POOL_CAP: usize = 256; + /// A module a bundler loads as data, not code: a stylesheet, an image, a GraphQL document, JSON, a font. const ASSET_MODULE: &str = r"(?i)\.(?:css|scss|sass|less|styl|stylus|pcss|graphql|gql|png|jpe?g|gif|svg|webp|avif|ico|bmp|tiff?|json5?|ya?ml|toml|md|mdx|txt|html?|xml|csv|woff2?|ttf|otf|eot|mp3|mp4|webm|ogg|wav|flac|pdf|wasm)$"; @@ -280,7 +288,7 @@ impl KernelResolver { row_id: None, from_node_id: candidate.id.clone(), reference_name: candidate.name.clone(), reference_kind: "calls".to_string(), line: candidate.start_line, column: candidate.start_column, candidates: None, file_path: candidate.file_path.clone(), - language: "rust".to_string(), failure_reason: None, + language: "rust".to_string(), failure_reason: None, generated: None, }; let is_trait = self.rust_self_context(&site).is_some_and(|cx| cx.trait_name.is_some()); self.rust_trait_memo.insert(candidate.id.clone(), is_trait); @@ -686,6 +694,26 @@ impl KernelResolver { } } let named = self.nodes_by_name(&r.reference_name)?; + // Big same-name pools make a bare match pathological: a name like + // `t` gets thousands of candidates whose retain/scope checks + // dominate a whole batch (pretix's fabric.min.js: ~950ms per + // 250-ref chunk vs a ~90ms floor). Two bounds, both declining the + // name-match entirely — `unresolved`, never a truncated winner — + // with the framework merge still applying downstream: + // * the ref's own file is generated-flagged (minified/codegen + // sources degrade by precedent); + // * the pool is dominated by generated nodes — an artifact pool + // cannot hold a real target anyway, so handwritten refs in + // vendored sources (d3.v6.js, fabric.js, pdf.worker.js — + // unminified, unflagged) get the same bound, while real big + // pools (Migration, __init__) proceed untouched. + if named.len() > GENERATED_NAME_POOL_CAP + && (r.generated == Some(true) + || named.iter().filter(|n| crate::textutil::is_generated_file(&n.file_path)).count() * 4 + >= named.len() * 3) + { + return Ok(None); + } // Macro constants are not callees (#1838). Where the name is a // function-like macro, the call can only reach the function another // build configuration compiles, never a same-named type @@ -973,6 +1001,15 @@ impl KernelResolver { } } let candidates = self.nodes_by_lower_name(&r.reference_name)?; + // Same bound as match_by_exact_name_uncapped: the lowercase pool is + // at least as wide, so decline before the retain chain. + if candidates.len() > GENERATED_NAME_POOL_CAP + && (r.generated == Some(true) + || candidates.iter().filter(|n| crate::textutil::is_generated_file(&n.file_path)).count() * 4 + >= candidates.len() * 3) + { + return Ok(None); + } let type_ref = self.is_dotnet_type_ref(r); let rust_bare = is_bare_rust_name(r); // Names are case-sensitive in every language but a handful: Rust's diff --git a/codegraph-kernel/src/resolve/php_scope.rs b/codegraph-kernel/src/resolve/php_scope.rs index cf29c1c6bc..e784facaae 100644 --- a/codegraph-kernel/src/resolve/php_scope.rs +++ b/codegraph-kernel/src/resolve/php_scope.rs @@ -364,7 +364,7 @@ impl KernelResolver { return false; } let Some(lines) = self.read_file(&candidate.file_path) else { return false }; - let site = ResolveRefIn { row_id: None, from_node_id: candidate.id.clone(), reference_name: candidate.name.clone(), reference_kind: "references".to_string(), line: candidate.start_line, column: candidate.start_column, candidates: None, file_path: candidate.file_path.clone(), language: candidate.language.clone(), failure_reason: None }; + let site = ResolveRefIn { row_id: None, from_node_id: candidate.id.clone(), reference_name: candidate.name.clone(), reference_kind: "references".to_string(), line: candidate.start_line, column: candidate.start_column, candidates: None, file_path: candidate.file_path.clone(), language: candidate.language.clone(), failure_reason: None, generated: None }; if let Some(tree) = self.parsed_tree(&lines, &site) { let mut node = super::iteration::descendant_for_position(tree.root_node(), lines.text(), ((site.line - 1).max(0) as usize, site.column.max(0) as usize)); while let Some(parent) = node.parent() { diff --git a/codegraph-kernel/src/resolve/resolver_upstream.rs b/codegraph-kernel/src/resolve/resolver_upstream.rs index 3f12b26bc3..ff754c5f50 100644 --- a/codegraph-kernel/src/resolve/resolver_upstream.rs +++ b/codegraph-kernel/src/resolve/resolver_upstream.rs @@ -455,7 +455,7 @@ impl KernelResolver { let Some(source) = self.read_file(file) else { return !name.starts_with('_') }; let site = ResolveRefIn { row_id: None, from_node_id: String::new(), reference_name: String::new(), reference_kind: "imports".to_string(), line: 1, column: 0, candidates: None, - file_path: file.to_string(), language: "python".to_string(), failure_reason: None }; + file_path: file.to_string(), language: "python".to_string(), failure_reason: None, generated: None }; let Some(tree) = self.parsed_tree(&source, &site) else { return !name.starts_with('_') }; let mut names = HashSet::new(); let mut literal = true; diff --git a/codegraph-kernel/src/resolve/rust_paths.rs b/codegraph-kernel/src/resolve/rust_paths.rs index a94be91e92..df222d7579 100644 --- a/codegraph-kernel/src/resolve/rust_paths.rs +++ b/codegraph-kernel/src/resolve/rust_paths.rs @@ -142,7 +142,7 @@ impl KernelResolver { line: 1, column: 0, candidates: None, - failure_reason: None, + failure_reason: None, generated: None, }; let mut tree = self.parsed_tree(&source, &r)?; // pin-project exposes its struct declaration inside a macro token tree. diff --git a/src/extraction/kernel/loader.ts b/src/extraction/kernel/loader.ts index 301988bdcb..607a324c2f 100644 --- a/src/extraction/kernel/loader.ts +++ b/src/extraction/kernel/loader.ts @@ -231,6 +231,9 @@ export interface ResolveRefIn { filePath: string; language: string; failureReason?: string; + /** files.generated at index time — bounds the name-match pool for + * minified/codegen sources (see kernel GENERATED_NAME_POOL_CAP). */ + generated?: boolean; } export interface KernelCandidateOut { diff --git a/src/resolution/index.ts b/src/resolution/index.ts index 6586a5eae0..646961d083 100644 --- a/src/resolution/index.ts +++ b/src/resolution/index.ts @@ -1896,18 +1896,27 @@ export class ReferenceResolver { /** One kernel `resolveChunk` over `refs`, in order. Throws on any native failure. */ private resolveChunkWithKernel(kernel: KernelResolverLike, refs: UnresolvedReference[]): ResolveOutcome[] { - const kernelRows: ResolveRefIn[] = refs.map((raw) => ({ - rowId: raw.rowId, - fromNodeId: raw.fromNodeId, - referenceName: raw.referenceName, - referenceKind: raw.referenceKind, - line: raw.line, - column: raw.column, - candidates: raw.candidates ? JSON.stringify(raw.candidates) : undefined, - filePath: raw.filePath || this.getFilePathFromNodeId(raw.fromNodeId) || '', - language: raw.language || this.getLanguageFromNodeId(raw.fromNodeId) || '', - failureReason: raw.failureReason, - })); + // generatedPredicateFor unions the indexed flag with the path convention — + // the kernel applies its bounded name-match only to generated sources + // (minified bundles' same-name pools cost ~10x a normal ref to scan). + const filePaths: string[] = refs.map((raw) => raw.filePath || this.getFilePathFromNodeId(raw.fromNodeId) || ''); + const isGenerated = this.queries.generatedPredicateFor(filePaths); + const kernelRows: ResolveRefIn[] = refs.map((raw, i) => { + const filePath = filePaths[i] ?? ''; + return { + rowId: raw.rowId, + fromNodeId: raw.fromNodeId, + referenceName: raw.referenceName, + referenceKind: raw.referenceKind, + line: raw.line, + column: raw.column, + candidates: raw.candidates ? JSON.stringify(raw.candidates) : undefined, + filePath, + language: raw.language || this.getLanguageFromNodeId(raw.fromNodeId) || '', + failureReason: raw.failureReason, + generated: isGenerated(filePath), + }; + }); const outcomes = kernel.resolveChunk(kernelRows); if (outcomes.length !== refs.length) { throw new Error(`kernel resolveChunk returned ${outcomes.length} outcomes for ${refs.length} refs`); From 5224a5bb28d1b41ab3155c708bcf3f2e0d2ca331 Mon Sep 17 00:00:00 2001 From: bompus Date: Tue, 27 Oct 2026 16:55:17 -0600 Subject: [PATCH 2/2] resolve: look up files.generated under the effective ref path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit review on #565: the LEFT JOIN files on r.file_path misses the generated flag when a ref's stored file_path is empty — the origin-node fallback fills the path only after the join ran. The bound then treats refs from generated files as ordinary and misses the pool cap. After the fallback, when the stored path was empty and the flag is not already set, query files.generated under the path the origin node supplied — the same effective path generatedPredicateFor unions with is_generated_file on the TS side. --- codegraph-kernel/src/resolve/mod.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/codegraph-kernel/src/resolve/mod.rs b/codegraph-kernel/src/resolve/mod.rs index 9157b8e780..81fe5e357a 100644 --- a/codegraph-kernel/src/resolve/mod.rs +++ b/codegraph-kernel/src/resolve/mod.rs @@ -29,7 +29,7 @@ use crate::native::{Error, Result}; #[cfg(feature = "addon")] use napi_derive::napi; use regex::Regex; -use rusqlite::{Connection, OpenFlags}; +use rusqlite::{Connection, OpenFlags, OptionalExtension}; use std::cell::{OnceCell, RefCell}; use std::collections::{HashMap, HashSet, VecDeque}; use std::rc::Rc; @@ -1126,13 +1126,14 @@ impl KernelResolver { } drop(stmt); let mut out = Vec::new(); - for (row_id, from_node_id, reference_name, reference_kind, line, column, candidates, mut file_path, mut language, failure_reason, generated) in collected + for (row_id, from_node_id, reference_name, reference_kind, line, column, candidates, mut file_path, mut language, failure_reason, mut generated) in collected { // Denormalized fallback — mirrors UnresolvedRef construction in // resolveBatchYielding: `raw.filePath || nodePath`, // `raw.language || nodeLang` — falsy (empty) only; a stored // 'unknown' passes through unchanged. - if file_path.is_empty() || language.is_empty() { + let stored_path_empty = file_path.is_empty(); + if stored_path_empty || language.is_empty() { if let Some(node) = self.node_by_id(&from_node_id)? { if file_path.is_empty() { file_path = node.file_path.clone(); @@ -1142,6 +1143,12 @@ impl KernelResolver { } } } + // The files.generated join could not match an empty stored + // path: look the flag up under the path the fallback supplied, + // as generatedPredicateFor does on the TS side. + if stored_path_empty && generated != Some(true) { + generated = self.file_generated(&file_path)?; + } // Union with the path convention — same rule as // generatedPredicateFor on the TS side: the DB flag OR a // generated-looking filename. @@ -1163,6 +1170,20 @@ impl KernelResolver { Ok(out) } + /// `SELECT generated FROM files WHERE path = ?` — the join's fallback + /// when a ref's stored file_path was empty (see read_pending_batch). + fn file_generated(&mut self, path: &str) -> Result> { + if path.is_empty() { + return Ok(None); + } + let hit = self + .conn()? + .query_row("SELECT generated FROM files WHERE path = ?1", [path], |row| row.get::<_, i64>(0)) + .optional() + .map_err(|e| Error::from_reason(e.to_string()))?; + Ok(hit.map(|g| g != 0)) + } + /// The `settle` stage — one verdict per ref in input order. #[cfg_attr(feature = "addon", napi)] pub fn resolve_chunk(&mut self, refs: Vec) -> Result> {