global: snapshot

This commit is contained in:
nym21
2026-01-14 20:09:51 +01:00
parent d75c2a881b
commit 1c7434ff83
25 changed files with 4059 additions and 22606 deletions
+12
View File
@@ -120,6 +120,18 @@ pub fn find_common_suffix(names: &[&str]) -> Option<String> {
None
}
/// Normalize a prefix string by ensuring it ends with underscore.
/// Returns empty string if input is empty.
pub fn normalize_prefix(s: &str) -> String {
if s.is_empty() {
String::new()
} else if s.ends_with('_') {
s.to_string()
} else {
format!("{}_", s)
}
}
#[cfg(test)]
mod tests {
use super::*;
+7 -4
View File
@@ -8,7 +8,7 @@ use std::collections::{BTreeSet, HashMap};
use brk_types::{TreeNode, extract_json_type};
use super::analyze_pattern_modes;
use crate::{PatternField, StructuralPattern, to_pascal_case};
use crate::{PatternBaseResult, PatternField, StructuralPattern, to_pascal_case};
/// Context for pattern detection, holding all intermediate state.
struct PatternContext {
@@ -38,14 +38,16 @@ impl PatternContext {
/// Detect structural patterns in the tree using a bottom-up approach.
///
/// Returns (patterns, concrete_to_pattern, concrete_to_type_param).
/// Returns (patterns, concrete_to_pattern, concrete_to_type_param, node_bases).
/// Each pattern has its `mode` set based on analysis of all instances.
/// `node_bases` maps tree paths to their computed PatternBaseResult for use during generation.
pub fn detect_structural_patterns(
tree: &TreeNode,
) -> (
Vec<StructuralPattern>,
HashMap<Vec<PatternField>, String>,
HashMap<Vec<PatternField>, String>,
HashMap<String, PatternBaseResult>,
) {
let mut ctx = PatternContext::new();
resolve_branch_patterns(tree, "root", &mut ctx);
@@ -99,10 +101,11 @@ pub fn detect_structural_patterns(
let concrete_to_pattern = pattern_lookup.clone();
// Analyze pattern modes (suffix vs prefix) from all instances
analyze_pattern_modes(tree, &mut patterns, &pattern_lookup);
// Also collects node bases for each tree path
let node_bases = analyze_pattern_modes(tree, &mut patterns, &pattern_lookup);
patterns.sort_by(|a, b| b.fields.len().cmp(&a.fields.len()));
(patterns, concrete_to_pattern, type_mappings)
(patterns, concrete_to_pattern, type_mappings, node_bases)
}
/// Detect generic patterns by grouping signatures by their normalized form.
+53 -25
View File
@@ -8,8 +8,8 @@ use std::collections::HashMap;
use brk_types::TreeNode;
use super::{find_common_prefix, find_common_suffix, get_node_fields};
use crate::{PatternField, PatternMode, StructuralPattern};
use super::{find_common_prefix, find_common_suffix, get_node_fields, normalize_prefix};
use crate::{PatternBaseResult, PatternField, PatternMode, StructuralPattern, build_child_path};
/// Result of analyzing a single pattern instance.
#[derive(Debug, Clone)]
@@ -28,16 +28,21 @@ struct InstanceAnalysis {
/// This is the main entry point for mode detection. It processes
/// the tree bottom-up, collecting analysis for each pattern instance,
/// then determines the consistent mode for each pattern.
///
/// Returns a map from tree paths to their computed PatternBaseResult.
/// This map is used during generation to check pattern compatibility.
pub fn analyze_pattern_modes(
tree: &TreeNode,
patterns: &mut [StructuralPattern],
pattern_lookup: &HashMap<Vec<PatternField>, String>,
) {
) -> HashMap<String, PatternBaseResult> {
// Collect analyses from all instances, keyed by pattern name
let mut all_analyses: HashMap<String, Vec<InstanceAnalysis>> = HashMap::new();
// Also collect base results for each node, keyed by tree path
let mut node_bases: HashMap<String, PatternBaseResult> = HashMap::new();
// Bottom-up traversal
collect_instance_analyses(tree, pattern_lookup, &mut all_analyses);
collect_instance_analyses(tree, "", pattern_lookup, &mut all_analyses, &mut node_bases);
// For each pattern, determine mode from collected instances
for pattern in patterns.iter_mut() {
@@ -45,14 +50,20 @@ pub fn analyze_pattern_modes(
pattern.mode = determine_pattern_mode(analyses, &pattern.fields);
}
}
node_bases
}
/// Recursively collect instance analyses bottom-up.
/// Returns the "base" for this node (used by parent for its analysis).
///
/// Also stores the PatternBaseResult for each node in `node_bases`, keyed by path.
fn collect_instance_analyses(
node: &TreeNode,
path: &str,
pattern_lookup: &HashMap<Vec<PatternField>, String>,
all_analyses: &mut HashMap<String, Vec<InstanceAnalysis>>,
node_bases: &mut HashMap<String, PatternBaseResult>,
) -> Option<String> {
match node {
TreeNode::Leaf(leaf) => {
@@ -63,9 +74,14 @@ fn collect_instance_analyses(
// First, process all children recursively (bottom-up)
let mut child_bases: HashMap<String, String> = HashMap::new();
for (field_name, child_node) in children {
if let Some(base) =
collect_instance_analyses(child_node, pattern_lookup, all_analyses)
{
let child_path = build_child_path(path, field_name);
if let Some(base) = collect_instance_analyses(
child_node,
&child_path,
pattern_lookup,
all_analyses,
node_bases,
) {
child_bases.insert(field_name.clone(), base);
}
}
@@ -77,6 +93,19 @@ fn collect_instance_analyses(
// Analyze this instance
let analysis = analyze_instance(&child_bases);
// Store the base result for this node
// Note: has_outlier is false because we use recursive base computation
// which gives correct bases without needing outlier detection
node_bases.insert(
path.to_string(),
PatternBaseResult {
base: analysis.base.clone(),
has_outlier: false,
is_suffix_mode: analysis.is_suffix_mode,
field_parts: analysis.field_parts.clone(),
},
);
// Get the pattern name for this node (if any)
let fields = get_node_fields(children, pattern_lookup);
if let Some(pattern_name) = pattern_lookup.get(&fields) {
@@ -128,19 +157,10 @@ fn analyze_instance(child_bases: &HashMap<String, String>) -> InstanceAnalysis {
let mut field_parts = HashMap::new();
for (field_name, child_base) in child_bases {
// Prefix = child_base with common suffix stripped
// Prefix = child_base with common suffix stripped, normalized to end with _
let prefix = child_base
.strip_suffix(&common_suffix)
.map(|s| {
// Ensure prefix ends with underscore if non-empty
if s.is_empty() {
String::new()
} else if s.ends_with('_') {
s.to_string()
} else {
format!("{}_", s)
}
})
.map(normalize_prefix)
.unwrap_or_default();
field_parts.insert(field_name.clone(), prefix);
}
@@ -152,16 +172,16 @@ fn analyze_instance(child_bases: &HashMap<String, String>) -> InstanceAnalysis {
};
}
// No common prefix or suffix - use first child's base and treat as suffix mode
// with full metric names as relatives
let base = child_bases.values().next().cloned().unwrap_or_default();
// No common prefix or suffix - use empty base so _m(base, relative) returns just the relative.
// This handles cases like utxo_cohorts.all.activity where children have completely
// different bases (coinblocks_destroyed, coindays_destroyed, etc.)
let field_parts = child_bases
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
InstanceAnalysis {
base,
base: String::new(),
field_parts,
is_suffix_mode: true,
}
@@ -197,7 +217,9 @@ fn determine_pattern_mode(
// Convert to sorted Vec for comparison since HashMap isn't hashable
let mut parts_counts: HashMap<Vec<(String, String)>, usize> = HashMap::new();
for analysis in &majority_instances {
let mut sorted: Vec<_> = analysis.field_parts.iter()
let mut sorted: Vec<_> = analysis
.field_parts
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
sorted.sort();
@@ -244,7 +266,10 @@ mod tests {
assert_eq!(analysis.base, "lth_cost_basis");
assert_eq!(analysis.field_parts.get("max"), Some(&"max".to_string()));
assert_eq!(analysis.field_parts.get("min"), Some(&"min".to_string()));
assert_eq!(analysis.field_parts.get("percentiles"), Some(&"".to_string()));
assert_eq!(
analysis.field_parts.get("percentiles"),
Some(&"".to_string())
);
}
#[test]
@@ -280,7 +305,10 @@ mod tests {
assert_eq!(analysis.base, "cost_basis");
assert_eq!(analysis.field_parts.get("max"), Some(&"max".to_string()));
assert_eq!(analysis.field_parts.get("min"), Some(&"min".to_string()));
assert_eq!(analysis.field_parts.get("percentiles"), Some(&"".to_string()));
assert_eq!(
analysis.field_parts.get("percentiles"),
Some(&"".to_string())
);
}
#[test]
+77 -43
View File
@@ -9,15 +9,7 @@ use brk_types::{Index, TreeNode, extract_json_type};
use crate::{IndexSetPattern, PatternField, child_type_name};
use super::{find_common_prefix, find_common_suffix};
/// Get the first leaf name from a tree node.
pub fn get_first_leaf_name(node: &TreeNode) -> Option<String> {
match node {
TreeNode::Leaf(leaf) => Some(leaf.name().to_string()),
TreeNode::Branch(children) => children.values().find_map(get_first_leaf_name),
}
}
use super::{find_common_prefix, find_common_suffix, normalize_prefix};
/// Get the shortest leaf name from a tree node.
///
@@ -128,6 +120,30 @@ pub struct PatternBaseResult {
pub field_parts: HashMap<String, String>,
}
impl PatternBaseResult {
/// Create a default result that forces inlining (has_outlier = true).
/// Use when no pattern base could be computed during lookup.
pub fn force_inline() -> Self {
Self {
base: String::new(),
has_outlier: true,
is_suffix_mode: true,
field_parts: HashMap::new(),
}
}
/// Create an empty result with no outlier.
/// Use for root-level patterns or when children have no common pattern.
pub fn empty() -> Self {
Self {
base: String::new(),
has_outlier: false,
is_suffix_mode: true,
field_parts: HashMap::new(),
}
}
}
/// Get the metric base for a pattern instance by analyzing direct children.
///
/// Uses the shortest leaf names from direct children to find common prefix/suffix.
@@ -140,12 +156,7 @@ pub struct PatternBaseResult {
pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
let child_names = get_direct_children_for_analysis(node);
if child_names.is_empty() {
return PatternBaseResult {
base: String::new(),
has_outlier: false,
is_suffix_mode: true, // default
field_parts: HashMap::new(),
};
return PatternBaseResult::empty();
}
// Try to find common base from leaf names
@@ -181,12 +192,7 @@ pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
// Fallback: no common prefix/suffix found - this is a root-level pattern
// Return empty base so metric names are used directly
PatternBaseResult {
base: String::new(),
has_outlier: false,
is_suffix_mode: true, // default
field_parts: HashMap::new(),
}
PatternBaseResult::empty()
}
/// Result of try_find_base: base name, has_outlier flag, is_suffix_mode flag, and field_parts.
@@ -199,7 +205,10 @@ struct FindBaseResult {
/// Try to find a common base from child names using prefix/suffix detection.
/// Returns Some(FindBaseResult) if found.
fn try_find_base(child_names: &[(String, String)], is_outlier_attempt: bool) -> Option<FindBaseResult> {
fn try_find_base(
child_names: &[(String, String)],
is_outlier_attempt: bool,
) -> Option<FindBaseResult> {
let leaf_names: Vec<&str> = child_names.iter().map(|(_, n)| n.as_str()).collect();
// Try common prefix first (suffix mode)
@@ -231,18 +240,10 @@ fn try_find_base(child_names: &[(String, String)], is_outlier_attempt: bool) ->
let base = suffix.trim_start_matches('_').to_string();
let mut field_parts = HashMap::new();
for (field_name, leaf_name) in child_names {
// Compute the prefix part for this field
// Compute the prefix part for this field, normalized to end with _
let prefix_part = leaf_name
.strip_suffix(&suffix)
.map(|s| {
if s.is_empty() {
String::new()
} else if s.ends_with('_') {
s.to_string()
} else {
format!("{}_", s)
}
})
.map(normalize_prefix)
.unwrap_or_default();
field_parts.insert(field_name.clone(), prefix_part);
}
@@ -366,9 +367,18 @@ mod tests {
fn test_get_pattern_instance_base_with_base_field() {
// Simulates vbytes tree: has base field with block_vbytes leaf
let tree = make_branch(vec![
("base", make_branch(vec![("dateindex", make_leaf("block_vbytes"))])),
("average", make_branch(vec![("dateindex", make_leaf("block_vbytes_average"))])),
("sum", make_branch(vec![("dateindex", make_leaf("block_vbytes_sum"))])),
(
"base",
make_branch(vec![("dateindex", make_leaf("block_vbytes"))]),
),
(
"average",
make_branch(vec![("dateindex", make_leaf("block_vbytes_average"))]),
),
(
"sum",
make_branch(vec![("dateindex", make_leaf("block_vbytes_sum"))]),
),
]);
let result = get_pattern_instance_base(&tree);
@@ -380,11 +390,26 @@ mod tests {
fn test_get_pattern_instance_base_without_base_field() {
// Simulates weight tree: NO base field, only suffixed metrics
let tree = make_branch(vec![
("average", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
("sum", make_branch(vec![("dateindex", make_leaf("block_weight_sum"))])),
("cumulative", make_branch(vec![("dateindex", make_leaf("block_weight_cumulative"))])),
("max", make_branch(vec![("dateindex", make_leaf("block_weight_max"))])),
("min", make_branch(vec![("dateindex", make_leaf("block_weight_min"))])),
(
"average",
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
),
(
"sum",
make_branch(vec![("dateindex", make_leaf("block_weight_sum"))]),
),
(
"cumulative",
make_branch(vec![("dateindex", make_leaf("block_weight_cumulative"))]),
),
(
"max",
make_branch(vec![("dateindex", make_leaf("block_weight_max"))]),
),
(
"min",
make_branch(vec![("dateindex", make_leaf("block_weight_min"))]),
),
]);
let result = get_pattern_instance_base(&tree);
@@ -397,9 +422,18 @@ mod tests {
// What if there's a "base" field that points to the same leaf as "average"?
// This could happen if the tree generation creates a base field that shares leaves with average
let tree = make_branch(vec![
("base", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
("average", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
("sum", make_branch(vec![("dateindex", make_leaf("block_weight_sum"))])),
(
"base",
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
),
(
"average",
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
),
(
"sum",
make_branch(vec![("dateindex", make_leaf("block_weight_sum"))]),
),
]);
let result = get_pattern_instance_base(&tree);
+29 -6
View File
@@ -4,10 +4,18 @@ use std::collections::{HashMap, HashSet};
use brk_types::TreeNode;
use crate::{
ClientMetadata, PatternBaseResult, PatternField, child_type_name, get_fields_with_child_info,
get_pattern_instance_base,
};
use crate::{ClientMetadata, PatternBaseResult, PatternField, child_type_name, get_fields_with_child_info};
/// Build a child path by appending a child name to a parent path.
/// Uses "/" as separator. If parent is empty, returns just the child name.
#[inline]
pub fn build_child_path(parent: &str, child: &str) -> String {
if parent.is_empty() {
child.to_string()
} else {
format!("{}/{}", parent, child)
}
}
/// Pre-computed context for a single child node.
pub struct ChildContext<'a> {
@@ -38,9 +46,13 @@ pub struct TreeNodeContext<'a> {
/// Prepare a tree node for generation.
/// Returns None if the node should be skipped (not a branch, already generated,
/// or matches a parameterizable pattern).
///
/// The `path` parameter is the tree path to this node (e.g., "distribution/utxoCohorts").
/// It's used to look up pre-computed PatternBaseResult from the analysis phase.
pub fn prepare_tree_node<'a>(
node: &'a TreeNode,
name: &str,
path: &str,
pattern_lookup: &HashMap<Vec<PatternField>, String>,
metadata: &ClientMetadata,
generated: &mut HashSet<String>,
@@ -55,8 +67,13 @@ pub fn prepare_tree_node<'a>(
.map(|(f, _)| f.clone())
.collect();
// Look up the pre-computed base result, or use a default that forces inlining
let base_result = metadata
.get_node_base(path)
.cloned()
.unwrap_or_else(PatternBaseResult::force_inline);
// Skip if this matches a parameterizable pattern AND has no outlier AND field parts match
let base_result = get_pattern_instance_base(node);
let pattern_compatible = pattern_lookup
.get(&fields)
.and_then(|name| metadata.find_pattern(name))
@@ -85,7 +102,13 @@ pub fn prepare_tree_node<'a>(
.zip(fields_with_child_info)
.map(|((child_name, child_node), (field, child_fields))| {
let is_leaf = matches!(child_node, TreeNode::Leaf(_));
let base_result = get_pattern_instance_base(child_node);
// Build child path and look up its pre-computed base result
let child_path = build_child_path(path, child_name);
let base_result = metadata
.get_node_base(&child_path)
.cloned()
.unwrap_or_else(PatternBaseResult::force_inline);
// For type annotations: use pattern type if ANY pattern matches
let matches_any_pattern = child_fields
@@ -6,8 +6,8 @@ use std::fmt::Write;
use brk_types::TreeNode;
use crate::{
ClientMetadata, Endpoint, GenericSyntax, JavaScriptSyntax, PatternField, generate_leaf_field,
prepare_tree_node, to_camel_case,
ClientMetadata, Endpoint, GenericSyntax, JavaScriptSyntax, PatternField, build_child_path,
generate_leaf_field, prepare_tree_node, to_camel_case,
};
use super::api::generate_api_methods;
@@ -22,6 +22,7 @@ pub fn generate_tree_typedefs(output: &mut String, catalog: &TreeNode, metadata:
generate_tree_typedef(
output,
"MetricsTree",
"",
catalog,
&pattern_lookup,
metadata,
@@ -32,12 +33,13 @@ pub fn generate_tree_typedefs(output: &mut String, catalog: &TreeNode, metadata:
fn generate_tree_typedef(
output: &mut String,
name: &str,
path: &str,
node: &TreeNode,
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
metadata: &ClientMetadata,
generated: &mut HashSet<String>,
) {
let Some(ctx) = prepare_tree_node(node, name, pattern_lookup, metadata, generated) else {
let Some(ctx) = prepare_tree_node(node, name, path, pattern_lookup, metadata, generated) else {
return;
};
@@ -71,9 +73,11 @@ fn generate_tree_typedef(
// Generate child typedefs
for child in &ctx.children {
if child.should_inline {
let child_path = build_child_path(path, child.name);
generate_tree_typedef(
output,
&child.inline_type_name,
&child_path,
child.node,
pattern_lookup,
metadata,
@@ -125,6 +129,7 @@ pub fn generate_main_client(
output,
catalog,
"MetricsTree",
"",
3,
&pattern_lookup,
metadata,
@@ -166,10 +171,12 @@ pub fn generate_main_client(
writeln!(output, "export {{ BrkClient, BrkError }};").unwrap();
}
#[allow(clippy::too_many_arguments)]
fn generate_tree_initializer(
output: &mut String,
node: &TreeNode,
name: &str,
path: &str,
indent: usize,
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
metadata: &ClientMetadata,
@@ -177,7 +184,7 @@ fn generate_tree_initializer(
) {
let indent_str = " ".repeat(indent);
let Some(ctx) = prepare_tree_node(node, name, pattern_lookup, metadata, generated) else {
let Some(ctx) = prepare_tree_node(node, name, path, pattern_lookup, metadata, generated) else {
return;
};
@@ -199,11 +206,13 @@ fn generate_tree_initializer(
}
} else if child.should_inline {
// Inline object
let child_path = build_child_path(path, child.name);
writeln!(output, "{}{}: {{", indent_str, field_name).unwrap();
generate_tree_initializer(
output,
child.node,
&child.inline_type_name,
&child_path,
indent + 1,
pattern_lookup,
metadata,
@@ -6,8 +6,8 @@ use std::fmt::Write;
use brk_types::TreeNode;
use crate::{
ClientMetadata, GenericSyntax, PatternField, PythonSyntax, generate_leaf_field,
prepare_tree_node, to_snake_case,
ClientMetadata, GenericSyntax, PatternField, PythonSyntax, build_child_path,
generate_leaf_field, prepare_tree_node, to_snake_case,
};
/// Generate tree classes
@@ -19,6 +19,7 @@ pub fn generate_tree_classes(output: &mut String, catalog: &TreeNode, metadata:
generate_tree_class(
output,
"MetricsTree",
"",
catalog,
&pattern_lookup,
metadata,
@@ -30,12 +31,13 @@ pub fn generate_tree_classes(output: &mut String, catalog: &TreeNode, metadata:
fn generate_tree_class(
output: &mut String,
name: &str,
path: &str,
node: &TreeNode,
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
metadata: &ClientMetadata,
generated: &mut HashSet<String>,
) {
let Some(ctx) = prepare_tree_node(node, name, pattern_lookup, metadata, generated) else {
let Some(ctx) = prepare_tree_node(node, name, path, pattern_lookup, metadata, generated) else {
return;
};
@@ -43,9 +45,11 @@ fn generate_tree_class(
// This ensures children are defined before parent references them
for child in &ctx.children {
if child.should_inline {
let child_path = build_child_path(path, child.name);
generate_tree_class(
output,
&child.inline_type_name,
&child_path,
child.node,
pattern_lookup,
metadata,
@@ -6,7 +6,7 @@ use std::fmt::Write;
use brk_types::TreeNode;
use crate::{
ClientMetadata, GenericSyntax, LanguageSyntax, PatternField, RustSyntax,
ClientMetadata, GenericSyntax, LanguageSyntax, PatternField, RustSyntax, build_child_path,
generate_leaf_field, generate_tree_node_field, prepare_tree_node, to_snake_case,
};
@@ -19,6 +19,7 @@ pub fn generate_tree(output: &mut String, catalog: &TreeNode, metadata: &ClientM
generate_tree_node(
output,
"MetricsTree",
"",
catalog,
&pattern_lookup,
metadata,
@@ -29,12 +30,13 @@ pub fn generate_tree(output: &mut String, catalog: &TreeNode, metadata: &ClientM
fn generate_tree_node(
output: &mut String,
name: &str,
path: &str,
node: &TreeNode,
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
metadata: &ClientMetadata,
generated: &mut HashSet<String>,
) {
let Some(ctx) = prepare_tree_node(node, name, pattern_lookup, metadata, generated) else {
let Some(ctx) = prepare_tree_node(node, name, path, pattern_lookup, metadata, generated) else {
return;
};
@@ -117,9 +119,11 @@ fn generate_tree_node(
// Generate child structs
for child in &ctx.children {
if child.should_inline {
let child_path = build_child_path(path, child.name);
generate_tree_node(
output,
&child.inline_type_name,
&child_path,
child.node,
pattern_lookup,
metadata,
+10 -2
View File
@@ -6,7 +6,7 @@ use brk_query::Vecs;
use brk_types::{Index, MetricLeafWithSchema};
use super::{GenericSyntax, IndexSetPattern, PatternField, StructuralPattern, extract_inner_type};
use crate::analysis;
use crate::{PatternBaseResult, analysis};
/// Metadata extracted from brk_query for client generation.
#[derive(Debug)]
@@ -21,6 +21,8 @@ pub struct ClientMetadata {
concrete_to_pattern: HashMap<Vec<PatternField>, String>,
/// Maps concrete field signatures to their type parameter (for generic patterns)
concrete_to_type_param: HashMap<Vec<PatternField>, String>,
/// Maps tree paths to their computed PatternBaseResult
node_bases: HashMap<String, PatternBaseResult>,
}
impl ClientMetadata {
@@ -31,7 +33,7 @@ impl ClientMetadata {
/// Extract metadata from a catalog TreeNode directly.
pub fn from_catalog(catalog: brk_types::TreeNode) -> Self {
let (structural_patterns, concrete_to_pattern, concrete_to_type_param) =
let (structural_patterns, concrete_to_pattern, concrete_to_type_param, node_bases) =
analysis::detect_structural_patterns(&catalog);
let index_set_patterns = analysis::detect_index_patterns(&catalog);
@@ -41,6 +43,7 @@ impl ClientMetadata {
index_set_patterns,
concrete_to_pattern,
concrete_to_type_param,
node_bases,
}
}
@@ -139,6 +142,11 @@ impl ClientMetadata {
lookup
}
/// Get the pre-computed PatternBaseResult for a tree path.
pub fn get_node_base(&self, path: &str) -> Option<&PatternBaseResult> {
self.node_bases.get(path)
}
/// Generate type annotation for a field with language-specific syntax.
pub fn field_type_annotation(
&self,
+59 -29
View File
@@ -79,7 +79,7 @@ fn test_all_leaves_have_names() {
fn test_pattern_detection() {
let catalog = load_catalog();
let (patterns, concrete_to_pattern, concrete_to_type_param) =
let (patterns, concrete_to_pattern, concrete_to_type_param, _node_bases) =
brk_bindgen::detect_structural_patterns(&catalog);
println!("Detected {} structural patterns", patterns.len());
@@ -142,7 +142,7 @@ fn test_pattern_detection() {
fn test_cost_basis_pattern() {
let catalog = load_catalog();
let (patterns, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
// Find CostBasisPattern2 and inspect it
let cost_basis = patterns
@@ -211,7 +211,7 @@ fn test_realized_pattern3_fields() {
#[test]
fn test_parameterizable_patterns_have_mode() {
let catalog = load_catalog();
let (patterns, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
// All patterns that appear 2+ times should either:
// 1. Be parameterizable (have a mode)
@@ -272,7 +272,7 @@ fn test_parameterizable_patterns_have_mode() {
#[test]
fn test_fee_rate_pattern_relatives() {
let catalog = load_catalog();
let (patterns, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
let fee_rate_pattern = patterns
.iter()
@@ -900,17 +900,14 @@ fn test_price_sats_vs_usd_different_field_parts() {
&[],
);
// Verify price.sats uses sats-suffixed metrics
// With improved pattern detection, price.sats now correctly uses a SatsPattern factory
// which eliminates duplication. Verify that it's being used:
assert!(
js_output.contains("'price_ohlc_sats'"),
"price.sats.ohlc should use 'price_ohlc_sats'"
);
assert!(
js_output.contains("'price_sats'") || js_output.contains("createSplitPattern2(this, 'price_sats')"),
"price.sats.split should use 'price_sats' base"
js_output.contains("sats: createSatsPattern(this, 'price')"),
"price.sats should use SatsPattern factory"
);
// Verify price.usd uses non-sats metrics (no _sats suffix)
// Verify price.usd is inlined and uses non-sats metrics (no _sats suffix)
assert!(
js_output.contains("createMetricPattern1(this, 'price_ohlc')"),
"price.usd.ohlc should use 'price_ohlc' (without _sats)"
@@ -920,24 +917,57 @@ fn test_price_sats_vs_usd_different_field_parts() {
"price.usd.split should use 'price' base (without _sats)"
);
// Verify they don't incorrectly share the same metric names
// Count occurrences to ensure usd doesn't use sats metrics
let sats_ohlc_count = js_output.matches("'price_ohlc_sats'").count();
let usd_ohlc_count = js_output.matches("'price_ohlc')").count();
println!("price_ohlc_sats occurrences: {}", sats_ohlc_count);
println!("price_ohlc occurrences: {}", usd_ohlc_count);
assert!(
sats_ohlc_count >= 1,
"Should have at least one 'price_ohlc_sats' for price.sats"
);
assert!(
usd_ohlc_count >= 1,
"Should have at least one 'price_ohlc' for price.usd"
);
println!("\nPrice sats vs usd field_parts test passed!");
println!(" - price.sats correctly uses sats-suffixed metrics");
println!(" - price.usd correctly uses non-sats metrics");
}
#[test]
fn test_utxo_cohorts_all_activity_base() {
// Test that distribution.utxo_cohorts.all.activity uses empty base
// because its children (coinblocks_destroyed, coindays_destroyed, etc.)
// have no common prefix or suffix.
let catalog = load_catalog();
let metadata = ClientMetadata::from_catalog(catalog.clone());
// Generate JavaScript output
let mut js_output = String::new();
writeln!(js_output, "// Test output").unwrap();
brk_bindgen::javascript::client::generate_base_client(&mut js_output);
brk_bindgen::javascript::client::generate_index_accessors(
&mut js_output,
&metadata.index_set_patterns,
);
brk_bindgen::javascript::client::generate_structural_patterns(
&mut js_output,
&metadata.structural_patterns,
&metadata,
);
brk_bindgen::javascript::tree::generate_tree_typedefs(
&mut js_output,
&metadata.catalog,
&metadata,
);
brk_bindgen::javascript::tree::generate_main_client(
&mut js_output,
&metadata.catalog,
&metadata,
&[],
);
// The all.activity should use empty base, so metrics don't get duplicated
// Look for: activity: createActivityPattern2(this, '')
// NOT: activity: createActivityPattern2(this, 'coinblocks_destroyed')
assert!(
!js_output.contains("createActivityPattern2(this, 'coinblocks_destroyed')"),
"all.activity should NOT use 'coinblocks_destroyed' as base (causes duplication)"
);
// Check that it uses empty string as base
assert!(
js_output.contains("activity: createActivityPattern2(this, '')"),
"all.activity should use empty base"
);
println!("utxo_cohorts.all.activity base test passed!");
}
+631 -7442
View File
File diff suppressed because it is too large Load Diff