From b11554d7cc3159b7a6586a68515e70c01034d4da Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:43:05 +0100 Subject: [PATCH] Check folder inventory evidence with the scope it was recorded with (#461) core_archive_show records max_entries and max_total_bytes, and a shared recorded_inventory helper checks a folder's freshness at the scope it was recorded with, so core_archives_compare and the Android, Apple and Harmony graph tools no longer refuse unchanged folders as file_changed. --- crates/engine/src/tools/application_tools.rs | 10 +- crates/engine/src/tools/artifact_tools.rs | 96 ++++++++++++++++++-- 2 files changed, 90 insertions(+), 16 deletions(-) diff --git a/crates/engine/src/tools/application_tools.rs b/crates/engine/src/tools/application_tools.rs index 3a50e1f8..d2a1d715 100644 --- a/crates/engine/src/tools/application_tools.rs +++ b/crates/engine/src/tools/application_tools.rs @@ -3,7 +3,7 @@ //! and coverage, and platform projections of artifact inventories. Pure computation over //! evidence (or over a path, analysed on the spot). -use super::artifact_tools::{WalkOptions, abs_path, env, inventory_path, is_path_input, resolve_evidence_input, walk_dir}; +use super::artifact_tools::{WalkOptions, abs_path, env, inventory_path, is_path_input, recorded_inventory, resolve_evidence_input, walk_dir}; use super::js_tools::load_graph; use crate::tool::{Ctx, Effects, Requires, ToolOutput, ToolSpec, Toolset, parse_params, schema_of}; use craft_anything_analysis::js_inventory::{InvLimits, Inventory}; @@ -11,7 +11,7 @@ use craft_anything_analysis::js_ledger::{Binding, Review, build_ledger, evaluate use craft_anything_analysis::js_source::{SourceGraph, build_source_graph, diff_source_and_bundle}; use craft_anything_analysis::js_trace::{Direction, SeedKind, find_seeds, follow_feature, trace_semantics}; use craft_anything_analysis::{js_compare, js_projection}; -use craft_anything_evidence::{Confidence, Coverage, CoverageStatus, ErrorCode, EvidenceBuilder, Limitation, Location, Subject, ToolError}; +use craft_anything_evidence::{Confidence, Coverage, CoverageStatus, EvidenceBuilder, Limitation, Location, Subject, ToolError}; use schemars::JsonSchema; use serde::Deserialize; use serde_json::{Value, json}; @@ -716,11 +716,7 @@ fn projection_inventory(ctx: &Ctx<'_>, p: &ProjectionParams) -> Result<(Inventor let limits = InvLimits { max_depth: 2, ..InvLimits::default() }; if let Some(v) = &p.inventory { let ev = resolve_evidence_input(ctx, v, &["core_archive_show"])?; - let path = ev.params.get("path").and_then(Value::as_str).ok_or_else(|| ToolError::invalid("inventory evidence has no path"))?; - let (inv, subject) = inventory_path(std::path::Path::new(path), limits, &ctx.cancel)?; - if subject.sha256 != ev.subject.sha256 { - return Err(ToolError::new(ErrorCode::FileChanged, format!("{path} changed since evidence {} was recorded", ev.id))); - } + let (inv, subject) = recorded_inventory(&ev, limits, &ctx.cancel)?; return Ok((inv, subject, Some(ev.id))); } let path = match &p.path { diff --git a/crates/engine/src/tools/artifact_tools.rs b/crates/engine/src/tools/artifact_tools.rs index 0c959a78..e5d52334 100644 --- a/crates/engine/src/tools/artifact_tools.rs +++ b/crates/engine/src/tools/artifact_tools.rs @@ -471,11 +471,45 @@ fn integrity_error(inv: &Inventory) -> ToolError { .with_details(json!({"contradictions": inv.contradictions.iter().take(50).collect::>()})) } +fn show_limits(max_depth: u32, max_entries: usize, max_total_bytes: u64) -> InvLimits { + InvLimits { max_entries: max_entries.clamp(1, 1_000_000), max_depth: max_depth.min(6), max_total_bytes, ..InvLimits::default() } +} + +/// Re-read the path a core_archive_show record names and check it is unchanged, then return its inventory at `limits`. +/// The check uses the scope the record was made with, since a folder's id covers only the entries that scope lists. +pub(crate) fn recorded_inventory(ev: &Evidence, limits: InvLimits, cancel: &CancelToken) -> Result<(Inventory, Subject), ToolError> { + let path = ev.params.get("path").and_then(Value::as_str).ok_or_else(|| ToolError::invalid("inventory evidence has no path"))?; + let num = |k: &str| ev.params.get(k).and_then(Value::as_u64); + let recorded = show_limits( + num("max_depth").map_or(default_depth(), |v| u32::try_from(v).unwrap_or(u32::MAX)), + num("max_entries").map_or(default_max_entries(), |v| usize::try_from(v).unwrap_or(usize::MAX)), + num("max_total_bytes").unwrap_or_else(default_max_total), + ); + // A file's id is the hash of its bytes, whatever the scope; a folder's depends on the scope. + let check = if ev.subject.kind == "directory" { recorded } else { limits }; + let (inv, subject) = inventory_path(Path::new(path), check, cancel)?; + if subject.sha256 != ev.subject.sha256 { + return Err(ToolError::new(ErrorCode::FileChanged, format!("{path} changed since evidence {} was recorded", ev.id)) + .with_remediation("Run core_archive_show again, or pass the path directly.")); + } + if same_limits(&check, &limits) { + return Ok((inv, subject)); + } + inventory_path(Path::new(path), limits, cancel) +} + +fn same_limits(a: &InvLimits, b: &InvLimits) -> bool { + a.max_entries == b.max_entries + && a.max_depth == b.max_depth + && a.max_nested_bytes == b.max_nested_bytes + && a.max_entry_bytes == b.max_entry_bytes + && a.max_total_bytes == b.max_total_bytes +} + fn core_archive_show(ctx: &mut Ctx<'_>, params: Value) -> Result { let p: InspectArtifactParams = parse_params(params)?; let path = target_path(ctx, p.path.as_deref())?; - let limits = - InvLimits { max_entries: p.max_entries.clamp(1, 1_000_000), max_depth: p.max_depth.min(6), max_total_bytes: p.max_total_bytes, ..InvLimits::default() }; + let limits = show_limits(p.max_depth, p.max_entries, p.max_total_bytes); ctx.report(None, format!("Inventorying {}", path.display())); let (inv, subject) = inventory_path(&path, limits, &ctx.cancel)?; let blocking = inv.contradictions.iter().any(|c| c.get("reason").and_then(Value::as_str).is_some_and(|r| r.contains("integrity") || r.contains("CRC"))); @@ -505,7 +539,7 @@ fn core_archive_show(ctx: &mut Ctx<'_>, params: Value) -> Result, v: &Value) -> Result<(Inventory, Option