|
|
|
@@ -246,7 +246,7 @@ pub fn _entity_matches(signal: &str, entity_id: &str) -> bool {
|
|
|
|
|
/// canonical fields — objective, affected_surface, constraints. Everything
|
|
|
|
|
/// else a caller attached is dropped at the kernel boundary.
|
|
|
|
|
pub fn _need_dict(need: &Map<String, Value>) -> Map<String, Value> {
|
|
|
|
|
let mut out = Map::new();
|
|
|
|
|
let mut out = Map::with_capacity(3);
|
|
|
|
|
out.insert("objective".into(), need.get("objective").cloned().unwrap_or(Value::Null));
|
|
|
|
|
out.insert(
|
|
|
|
|
"affected_surface".into(),
|
|
|
|
@@ -262,9 +262,11 @@ pub fn _need_dict(need: &Map<String, Value>) -> Map<String, Value> {
|
|
|
|
|
/// Everything the affected surface offers as evidence: paths, entities,
|
|
|
|
|
/// ui surfaces, regions, workflow configs, the objective text tokens
|
|
|
|
|
/// (deterministic split — not semantics). Set semantics; sorted projection
|
|
|
|
|
/// because the frozen truth stores `sorted(set)`.
|
|
|
|
|
/// because the frozen truth stores `sorted(set)`. Phase B: tokens borrow
|
|
|
|
|
/// the input JSON and the one lowered objective — the only allocations are
|
|
|
|
|
/// the lowered objective and the final output array.
|
|
|
|
|
pub fn _surface_text(need: &Map<String, Value>, surface: &Map<String, Value>) -> Value {
|
|
|
|
|
let mut tokens: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
|
|
|
|
|
let mut tokens: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
|
|
|
|
|
for k in [
|
|
|
|
|
"affected_paths",
|
|
|
|
|
"affected_existing_entities",
|
|
|
|
@@ -272,19 +274,17 @@ pub fn _surface_text(need: &Map<String, Value>, surface: &Map<String, Value>) ->
|
|
|
|
|
"affected_regions",
|
|
|
|
|
"workflow_config_surfaces",
|
|
|
|
|
] {
|
|
|
|
|
for t in surface.get(k).map(str_list).unwrap_or_default() {
|
|
|
|
|
tokens.insert(t.to_string());
|
|
|
|
|
}
|
|
|
|
|
tokens.extend(surface.get(k).map(str_list).unwrap_or_default());
|
|
|
|
|
}
|
|
|
|
|
for t in need.get("affected_surface").map(str_list).unwrap_or_default() {
|
|
|
|
|
tokens.insert(t.to_string());
|
|
|
|
|
}
|
|
|
|
|
let objective = need.get("objective").and_then(|v| v.as_str()).unwrap_or("");
|
|
|
|
|
let lowered = objective.replace(['/', '-'], " ").to_lowercase();
|
|
|
|
|
for w in lowered.split_whitespace() {
|
|
|
|
|
tokens.insert(w.to_string());
|
|
|
|
|
}
|
|
|
|
|
Value::Array(tokens.into_iter().map(Value::String).collect())
|
|
|
|
|
tokens.extend(need.get("affected_surface").map(str_list).unwrap_or_default());
|
|
|
|
|
let lowered = need
|
|
|
|
|
.get("objective")
|
|
|
|
|
.and_then(|v| v.as_str())
|
|
|
|
|
.unwrap_or("")
|
|
|
|
|
.replace(['/', '-'], " ")
|
|
|
|
|
.to_lowercase();
|
|
|
|
|
tokens.extend(lowered.split_whitespace());
|
|
|
|
|
Value::Array(tokens.into_iter().map(|t| Value::String(t.to_string())).collect())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── small JSON accessors (borrowed; owned only where the output needs it) ──
|
|
|
|
@@ -456,6 +456,40 @@ fn surface_kind_present(kind: &str, surface: &Map<String, Value>) -> bool {
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The evaluate_applicability signal tally — Python's `_check` closure with
|
|
|
|
|
/// its three nonlocal accumulators as ONE struct; the free `_check` mutates
|
|
|
|
|
/// it and the contracts_match pass records hits directly.
|
|
|
|
|
struct SignalTally {
|
|
|
|
|
groups_declared: usize,
|
|
|
|
|
groups_hit: Vec<&'static str>,
|
|
|
|
|
evidence: Map<String, Value>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl SignalTally {
|
|
|
|
|
fn record_hits(&mut self, group: &'static str, hits: Vec<&str>) {
|
|
|
|
|
self.groups_hit.push(group);
|
|
|
|
|
self.evidence.insert(group.to_string(), json!(hits));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Python's nested `_check`: count a DECLARED signal group and record its
|
|
|
|
|
/// evidence when any candidate produces a hit (first 5 kept).
|
|
|
|
|
fn _check(
|
|
|
|
|
tally: &mut SignalTally,
|
|
|
|
|
group: &'static str,
|
|
|
|
|
candidates: &[&str],
|
|
|
|
|
present: &dyn Fn(&str) -> bool,
|
|
|
|
|
) {
|
|
|
|
|
if candidates.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
tally.groups_declared += 1;
|
|
|
|
|
let hits: Vec<&str> = candidates.iter().filter(|c| present(c)).copied().collect();
|
|
|
|
|
if !hits.is_empty() {
|
|
|
|
|
tally.record_hits(group, hits.iter().take(5).copied().collect());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Deterministic applicability verdict from the pattern's declared signals.
|
|
|
|
|
pub fn evaluate_applicability(
|
|
|
|
|
pattern: &Map<String, Value>,
|
|
|
|
@@ -497,9 +531,9 @@ pub fn evaluate_applicability(
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let mut groups_hit: Vec<&'static str> = Vec::new();
|
|
|
|
|
let mut groups_declared: usize = 0;
|
|
|
|
|
let mut evidence = Map::new();
|
|
|
|
|
let groups_hit: Vec<&'static str> = Vec::new();
|
|
|
|
|
let groups_declared: usize = 0;
|
|
|
|
|
let evidence = Map::new();
|
|
|
|
|
|
|
|
|
|
let paths: Vec<&str> = surface
|
|
|
|
|
.get("affected_paths")
|
|
|
|
@@ -510,34 +544,24 @@ pub fn evaluate_applicability(
|
|
|
|
|
.collect();
|
|
|
|
|
let entities: Vec<&str> = surface.get("affected_existing_entities").map(str_list).unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
let mut _check = |group: &'static str, candidates: &[&str], present: &dyn Fn(&str) -> bool| {
|
|
|
|
|
if candidates.is_empty() {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
groups_declared += 1;
|
|
|
|
|
let hits: Vec<&str> = candidates.iter().filter(|c| present(c)).copied().collect();
|
|
|
|
|
if !hits.is_empty() {
|
|
|
|
|
groups_hit.push(group);
|
|
|
|
|
evidence.insert(group.to_string(), json!(hits.iter().take(5).collect::<Vec<_>>()));
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let mut tally = SignalTally { groups_declared, groups_hit, evidence };
|
|
|
|
|
|
|
|
|
|
let surface_kinds = signals.get("surface_kinds").map(str_list).unwrap_or_default();
|
|
|
|
|
_check("surface_kinds", &surface_kinds, &|c| surface_kind_present(c, surface));
|
|
|
|
|
_check(&mut tally, "surface_kinds", &surface_kinds, &|c| surface_kind_present(c, surface));
|
|
|
|
|
let path_signals = signals.get("path_contains").map(str_list).unwrap_or_default();
|
|
|
|
|
_check("path_contains", &path_signals, &|c| paths.iter().any(|p| _path_matches(c, p)));
|
|
|
|
|
_check(&mut tally, "path_contains", &path_signals, &|c| paths.iter().any(|p| _path_matches(c, p)));
|
|
|
|
|
let entity_signals = signals.get("entities_match").map(str_list).unwrap_or_default();
|
|
|
|
|
_check("entities_match", &entity_signals, &|c| entities.iter().any(|e| _entity_matches(c, e)));
|
|
|
|
|
_check(&mut tally, "entities_match", &entity_signals, &|c| entities.iter().any(|e| _entity_matches(c, e)));
|
|
|
|
|
let objective_words: Vec<String> =
|
|
|
|
|
objective.replace('/', " ").split_whitespace().map(|w| w.to_lowercase()).collect();
|
|
|
|
|
let objective_signals_owned: Vec<String> =
|
|
|
|
|
signals.get("objective_any").map(str_list).unwrap_or_default().into_iter().map(str::to_lowercase).collect();
|
|
|
|
|
let objective_signals: Vec<&str> = objective_signals_owned.iter().map(String::as_str).collect();
|
|
|
|
|
_check("objective_any", &objective_signals, &|c| objective_words.iter().any(|w| w == c));
|
|
|
|
|
_check(&mut tally, "objective_any", &objective_signals, &|c| objective_words.iter().any(|w| w == c));
|
|
|
|
|
|
|
|
|
|
if let Some(contracts) = signals.get("contracts_match").and_then(|v| v.as_array()) {
|
|
|
|
|
if !contracts.is_empty() {
|
|
|
|
|
groups_declared += 1;
|
|
|
|
|
tally.groups_declared += 1;
|
|
|
|
|
let wanted: Vec<&str> = contracts.iter().filter_map(|c| c.as_str()).collect();
|
|
|
|
|
let hits: Vec<String> = authorities
|
|
|
|
|
.values()
|
|
|
|
@@ -549,12 +573,13 @@ pub fn evaluate_applicability(
|
|
|
|
|
.map(|(e, to)| format!("{e}->{to}"))
|
|
|
|
|
.collect();
|
|
|
|
|
if !hits.is_empty() {
|
|
|
|
|
groups_hit.push("contracts_match");
|
|
|
|
|
evidence.insert("contracts_match".into(), json!(hits.iter().take(5).collect::<Vec<_>>()));
|
|
|
|
|
tally.record_hits("contracts_match", hits.iter().take(5).map(|s| s.as_str()).collect());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let SignalTally { groups_declared, groups_hit, evidence } = tally;
|
|
|
|
|
|
|
|
|
|
if groups_declared == 0 {
|
|
|
|
|
return json!({
|
|
|
|
|
"pattern": pattern_id,
|
|
|
|
|