From a6c56c8655a7b5f61198f1b8ec59966171a8cbac Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 24 Aug 2026 20:40:51 +0000 Subject: [PATCH] Resolve index levels from the registry instead of persisting them --- node-graph/graph-craft/src/proto.rs | 24 +++++++++++++++---- .../libraries/core-types/src/context.rs | 16 ++++++------- node-graph/preprocessor/src/lib.rs | 4 ++-- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/node-graph/graph-craft/src/proto.rs b/node-graph/graph-craft/src/proto.rs index 8842aab74b..e3de3e22bf 100644 --- a/node-graph/graph-craft/src/proto.rs +++ b/node-graph/graph-craft/src/proto.rs @@ -498,17 +498,34 @@ impl ProtoNetwork { nullification_node_id } + /// The node's declared index levels and per-input pushed levels, both read + /// from the registry since they follow the node's signature. A node with no + /// registry entry pushes nothing and names no level. + fn registry_index_levels(&self, node_index: usize) -> (core_types::context::IndexLevels, Vec) { + let identifier = &self.nodes[node_index].1.identifier; + let metadata = core_types::registry::NODE_METADATA.lock().unwrap(); + match metadata.get(identifier) { + Some(entry) => ( + ContextDependencies::from(entry.context_features.as_slice()).index_levels, + entry.fields.iter().map(|field| field.pushed_levels).collect(), + ), + None => (core_types::context::IndexLevels::empty(), Vec::new()), + } + } + fn find_context_dependencies(&mut self, id: NodeId) -> (ContextModification, Option) { let mut branch_dependencies = Vec::new(); let mut combined_deps = ContextModification::default(); let node_index = id.0 as usize; + let (declared_levels, pushed_levels) = self.registry_index_levels(node_index); let (extract, inject, own_deps) = { let dependencies = &self.nodes[node_index].1.context_features; - // Documents predating the level mask deserialize as all-levels, so a - // node that declares no index read must not contribute one. let index_levels = match dependencies.extract.contains(core_types::context::ContextFeatures::INDEX) { - true => dependencies.index_levels, + // A wrapper node declaring its dependencies by hand keeps `INDEX` + // without the signature naming a level, and addresses the innermost. + true if declared_levels.is_empty() => core_types::context::IndexLevels::innermost(), + true => declared_levels, false => core_types::context::IndexLevels::empty(), }; let own_deps = ContextModification::from_sources(dependencies.extract, dependencies.sources()).with_index_levels(index_levels); @@ -523,7 +540,6 @@ impl ProtoNetwork { }; // Compute the dependencies for each branch and combine all of them - let pushed_levels = self.nodes[node_index].1.context_features.pushed_levels.clone(); for (input, &node) in inputs.iter().enumerate() { let branch = self.find_context_dependencies(node); diff --git a/node-graph/libraries/core-types/src/context.rs b/node-graph/libraries/core-types/src/context.rs index db1b3b3a44..0b8b004a39 100644 --- a/node-graph/libraries/core-types/src/context.rs +++ b/node-graph/libraries/core-types/src/context.rs @@ -219,7 +219,7 @@ impl IndexLevels { } /// The innermost level alone, which is what every reader but `read_index` - /// addresses, so documents predating the mask migrate to it. + /// addresses. pub const fn innermost() -> Self { Self(1) } @@ -325,12 +325,14 @@ impl ContextFeatures { pub struct ContextDependencies { pub extract: ContextFeatures, pub inject: ContextFeatures, - /// Which index levels the node reads; empty means it reads none. Documents - /// written before the field existed default to the whole chain. - #[cfg_attr(feature = "serde", serde(default = "IndexLevels::innermost"))] + /// Which index levels the node reads, non-empty exactly when `extract` keeps + /// `INDEX`. Follows the node's signature, so it resolves from the registry + /// instead of persisting. + #[cfg_attr(feature = "serde", serde(skip))] pub index_levels: IndexLevels, /// Index levels pushed per input, in input order; a missing entry is 0. - #[cfg_attr(feature = "serde", serde(default))] + /// Resolves from the registry alongside [`Self::index_levels`]. + #[cfg_attr(feature = "serde", serde(skip))] pub pushed_levels: Vec, #[cfg_attr(feature = "serde", serde(default, deserialize_with = "deserialize_sorted_sources"))] sources: Vec, @@ -356,10 +358,6 @@ impl ContextDependencies { self } - pub fn with_pushed_levels(mut self, pushed_levels: Vec) -> Self { - self.pushed_levels = pushed_levels; - self - } pub fn sources(&self) -> &[SourceId] { &self.sources diff --git a/node-graph/preprocessor/src/lib.rs b/node-graph/preprocessor/src/lib.rs index 8fa7447271..200839afe8 100644 --- a/node-graph/preprocessor/src/lib.rs +++ b/node-graph/preprocessor/src/lib.rs @@ -205,7 +205,7 @@ impl Preprocessor { implementation: DocumentNodeImplementation::ProtoNode(id.clone()), visible: true, skip_deduplication: false, - context_features: ContextDependencies::from(metadata.context_features.as_slice()).with_pushed_levels(metadata.fields.iter().map(|field| field.pushed_levels).collect()), + context_features: ContextDependencies::from(metadata.context_features.as_slice()), ..Default::default() }; @@ -256,7 +256,7 @@ impl Preprocessor { call_argument: node_io.call_argument.clone(), implementation: DocumentNodeImplementation::ProtoNode(id.clone()), visible: true, - context_features: ContextDependencies::from(metadata.context_features.as_slice()).with_pushed_levels(metadata.fields.iter().map(|field| field.pushed_levels).collect()), + context_features: ContextDependencies::from(metadata.context_features.as_slice()), ..Default::default() }; inject_scopes.insert(id.clone(), (template, node_io.return_value.clone()));