From b1f2fcb85c74cff5bbf92c6733a3e10750f66966 Mon Sep 17 00:00:00 2001 From: boris Date: Sat, 12 Sep 2026 16:23:18 +0800 Subject: [PATCH] fix(stock-pool): scan decoded native conditions instead of serialized source echoes --- crates/fidc-core/src/pattern_context.rs | 29 ++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/fidc-core/src/pattern_context.rs b/crates/fidc-core/src/pattern_context.rs index 9d6d7e1..033adf6 100644 --- a/crates/fidc-core/src/pattern_context.rs +++ b/crates/fidc-core/src/pattern_context.rs @@ -221,7 +221,15 @@ pub fn specs_in_value(value: &Value) -> Result, String> { } } Value::Object(items) => { - for v in items.values() { + let typed_pool = items.get("stockPool").or_else(|| items.get("stock_pool")) + .is_some_and(Value::is_object); + for (key, v) in items { + // The executable pool already supplies decoded expressions. + // Its display/source serialization escapes those expressions + // one more time and is not another executable program. + if typed_pool && matches!(key.as_str(), "sourceCode" | "source_code") { + continue; + } specs.extend(specs_in_value(v)?); } } @@ -248,6 +256,25 @@ mod tests { use super::*; use crate::{BenchmarkSnapshot, DailyFactorSnapshot, DailyMarketSnapshot, Instrument}; use serde_json::json; + #[test] + fn structured_pool_conditions_are_not_rescanned_inside_serialized_source_code() { + let pattern = json!({"template":"expression","parameters":{"history_window":20}, + "expression":{"kind":"operator","name":"GT","args":[{"kind":"field","name":"amount"},{"kind":"number","value":0}]}}); + let expr = format!("pattern_signal({})", serde_json::to_string(&pattern.to_string()).unwrap()); + let pool = json!({"schema_version":1,"pool_id":"fixture","version_id":"v1","members":[], + "allocation_policy":{},"timing_policy":{},"stop_take_policy":{},"out_of_pool_policy":"hold", + "exit_signals":[{"role":"risk_exit","when_expr":expr,"remaining_position_bps":5000,"reason":"fixture"}]}); + let source = format!("stock_pool.config({pool})"); + for (pool_key, source_key) in [("stockPool", "sourceCode"), ("stock_pool", "source_code")] { + let value = json!({pool_key:pool,source_key:source,"runtimeExpressions":{"trading":{"buyFilterExpr":expr}}}); + assert_eq!(specs_in_value(&value).unwrap().len(), 2); + let mut invalid = value.clone(); + invalid[pool_key]["exit_signals"][0]["when_expr"] = json!("pattern_signal(not-json)"); + assert!(specs_in_value(&invalid).is_err(), "invalid actual conditions must still fail"); + } + assert_eq!(specs_in_value(&json!({"sourceCode":format!("risk.stop_loss({expr})")})).unwrap().len(),1); + } + #[test] fn normalized_rule_does_not_turn_an_omitted_window_into_explicit_null() { let expression:Expr=serde_json::from_value(json!({"kind":"operator","name":"GT","args":[{"kind":"field","name":"close"},{"kind":"number","value":1}]})).unwrap();