refactor(construction_pattern): Phase B language-advantage pass — dichotomy wave
CI Pipeline / rust-surface (push) Failing after 13s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 16s
CI Pipeline / best-practices (push) Canceled after 3s
CI Pipeline / canary (push) Canceled after 16s
Auto-version and Release / release (push) Canceled after 22s
deploy / deploy (push) Canceled after 26s
CI Pipeline / lint (push) Canceled after 23s
CI Pipeline / unit-tests (push) Canceled after 23s
CI Pipeline / rust-surface (push) Failing after 13s
CI Pipeline / documentation (push) Canceled after 0s
CI Pipeline / type-check (push) Canceled after 0s
CI Pipeline / gitea-offline (push) Canceled after 0s
CI Pipeline / required-checks (push) Canceled after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Canceled after 0s
CI Pipeline / go-surface (push) Canceled after 16s
CI Pipeline / best-practices (push) Canceled after 3s
CI Pipeline / canary (push) Canceled after 16s
Auto-version and Release / release (push) Canceled after 22s
deploy / deploy (push) Canceled after 26s
CI Pipeline / lint (push) Canceled after 23s
CI Pipeline / unit-tests (push) Canceled after 23s
language advantage: ApplicabilityVerdict/ObligationVerdict enums replace string-soup verdicts, serialized at the JSON boundary via exhaustive as_str matches; language advantage: obligation when-conditions classify into a WhenShape enum (Unconditioned/Always/PathContains/ EntitiesMatch/ContractsTouched/Unrecognized) dispatched exhaustively — the unrecognized shape surfaces as UNKNOWN, never silently REQUIRED; language advantage: str_list borrows slices of the input JSON (owned copies only where the output needs them), derive reuses the obligation object instead of cloning it, zero-copy split_tokens returns token slices borrowing the lowered input (re.split leading/trailing empty fidelity kept); language advantage: contracts_match evidence and when-condition hits built as iterator chains; sort_unstable throughout. Battery 9/9 (56 frozen oracle cases) green before and after the pass; clippy 0; live ctypes parity 56/56; construction surface tests 51/51 green with RUNE_CP_CORE=python pin and with the kernel live.
This commit is contained in:
1 parent
681312c69f
commit
c42936b959
2 files changed
+257
-176
No files matched your search
@@ -61,8 +61,8 @@ mod battery {
|
||||
}
|
||||
return Ok(Value::Array(out));
|
||||
}
|
||||
let evidence: Vec<String> =
|
||||
i["evidence"].as_array().map(|a| a.iter().filter_map(|e| e.as_str().map(String::from)).collect()).unwrap_or_default();
|
||||
let evidence: Vec<&str> =
|
||||
i["evidence"].as_array().map(|a| a.iter().filter_map(|e| e.as_str()).collect()).unwrap_or_default();
|
||||
record_pattern_proposal(
|
||||
i["pattern_id"].as_str().unwrap_or(""),
|
||||
i["provider"].as_str().unwrap_or(""),
|
||||
|
||||
@@ -21,20 +21,101 @@
|
||||
//! The boundary-aware matchers are the oracle-verified kernels promoted by
|
||||
//! batch 155 (the 'ui'-in-'builtin' false-positive class is blocked by
|
||||
//! design): short (<4 char, char-counted) signals match whole tokens only.
|
||||
//!
|
||||
//! Language-advantage pass (Phase B, battery-pinned): the verdict lattices
|
||||
//! are enums with exhaustive `as_str` matches at the JSON boundary; the
|
||||
//! obligation `when`-condition is classified into a `WhenShape` enum and
|
||||
//! dispatched exhaustively (no string-soup if-chains); signal/path/entity
|
||||
//! lists borrow slices of the input JSON instead of cloning; tokenization
|
||||
//! is zero-copy over the lowered input.
|
||||
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
// ── verdict lattices (string constants, oracle-identical) ─────────────────
|
||||
// ── verdict lattices (enums with exhaustive matches at the JSON boundary) ──
|
||||
|
||||
pub const APPLIES: &str = "APPLIES";
|
||||
pub const PARTIAL: &str = "PARTIAL";
|
||||
pub const DOES_NOT_APPLY: &str = "DOES_NOT_APPLY";
|
||||
pub const UNKNOWN: &str = "UNKNOWN";
|
||||
/// Applicability verdict lattice (§4-5): APPLIES / PARTIAL /
|
||||
/// DOES_NOT_APPLY / UNKNOWN. UNKNOWN never becomes APPLIES because a name
|
||||
/// sounds similar.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum ApplicabilityVerdict {
|
||||
Applies,
|
||||
Partial,
|
||||
DoesNotApply,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
pub const OBL_REQUIRED: &str = "REQUIRED";
|
||||
pub const OBL_APPLICABLE: &str = "APPLICABLE";
|
||||
pub const OBL_NOT_APPLICABLE: &str = "NOT_APPLICABLE";
|
||||
pub const OBL_UNKNOWN: &str = "UNKNOWN";
|
||||
impl ApplicabilityVerdict {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
ApplicabilityVerdict::Applies => "APPLIES",
|
||||
ApplicabilityVerdict::Partial => "PARTIAL",
|
||||
ApplicabilityVerdict::DoesNotApply => "DOES_NOT_APPLY",
|
||||
ApplicabilityVerdict::Unknown => "UNKNOWN",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Obligation `when`-condition verdict lattice (§6-7): REQUIRED /
|
||||
/// APPLICABLE / NOT_APPLICABLE / UNKNOWN — an obligation with no
|
||||
/// recognized `when` is UNKNOWN (never silently required).
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum ObligationVerdict {
|
||||
Required,
|
||||
Applicable,
|
||||
NotApplicable,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl ObligationVerdict {
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
ObligationVerdict::Required => "REQUIRED",
|
||||
ObligationVerdict::Applicable => "APPLICABLE",
|
||||
ObligationVerdict::NotApplicable => "NOT_APPLICABLE",
|
||||
ObligationVerdict::Unknown => "UNKNOWN",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The `when`-condition shapes (all deterministic; §6-7). Classified once,
|
||||
/// dispatched exhaustively — a shape outside the lattice is Unrecognized
|
||||
/// and surfaces as UNKNOWN, never REQUIRED.
|
||||
enum WhenShape<'a> {
|
||||
Unconditioned,
|
||||
Always,
|
||||
PathContains(&'a Value),
|
||||
EntitiesMatch(&'a Value),
|
||||
ContractsTouched(&'a Value),
|
||||
Unrecognized(Vec<String>),
|
||||
}
|
||||
|
||||
fn when_shape(obligation: &Map<String, Value>) -> WhenShape<'_> {
|
||||
match obligation.get("when") {
|
||||
None | Some(Value::Null) => WhenShape::Unconditioned,
|
||||
Some(w) => {
|
||||
if w.get("always").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
return WhenShape::Always;
|
||||
}
|
||||
// Python checks KEY PRESENCE (`"path_contains" in when`), so a
|
||||
// null value still enters the branch — `get` mirrors that.
|
||||
if let Some(subs) = w.get("path_contains") {
|
||||
return WhenShape::PathContains(subs);
|
||||
}
|
||||
if let Some(subs) = w.get("entities_match") {
|
||||
return WhenShape::EntitiesMatch(subs);
|
||||
}
|
||||
if let Some(wanted) = w.get("contracts_touched") {
|
||||
return WhenShape::ContractsTouched(wanted);
|
||||
}
|
||||
match w {
|
||||
Value::Object(m) => WhenShape::Unrecognized(m.keys().cloned().collect()),
|
||||
// Python crashes on `when.get` before reaching the
|
||||
// sorted-shape message for non-dict `when` — unreachable.
|
||||
_ => WhenShape::Unrecognized(vec![]),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const KNOWN_KEYS: &[&str] = &[
|
||||
"pattern_id",
|
||||
@@ -91,8 +172,8 @@ fn py_repr(v: &Value) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn py_str_list(items: &[String]) -> String {
|
||||
let inner: Vec<String> = items.iter().map(|s| py_quote(s)).collect();
|
||||
fn py_str_list<T: AsRef<str>>(items: &[T]) -> String {
|
||||
let inner: Vec<String> = items.iter().map(|s| py_quote(s.as_ref())).collect();
|
||||
format!("[{}]", inner.join(", "))
|
||||
}
|
||||
|
||||
@@ -100,23 +181,39 @@ fn py_str_list(items: &[String]) -> String {
|
||||
|
||||
/// Oracle-faithful token split: runs of non-token chars are ONE separator,
|
||||
/// with leading/trailing empties kept (`re.split` semantics — only the
|
||||
/// empties are observable, via the empty-signal quirk).
|
||||
fn split_tokens(lowered: &str, keep_colon: bool) -> Vec<String> {
|
||||
/// empties are observable, via the empty-signal quirk). Zero-copy: tokens
|
||||
/// borrow slices of the lowered input.
|
||||
fn split_tokens<'a>(lowered: &'a str, keep_colon: bool) -> Vec<&'a str> {
|
||||
let is_tok = |c: char| c.is_ascii_lowercase() || c.is_ascii_digit() || (keep_colon && c == ':');
|
||||
let mut out = Vec::new();
|
||||
let mut cur = String::new();
|
||||
let mut pending_empty = true;
|
||||
for c in lowered.chars() {
|
||||
if lowered.is_empty() {
|
||||
return vec![""];
|
||||
}
|
||||
let mut toks: Vec<&'a str> = Vec::new();
|
||||
let mut start: Option<usize> = None;
|
||||
let mut ended_with_sep = false;
|
||||
for (i, c) in lowered.char_indices() {
|
||||
if is_tok(c) {
|
||||
cur.push(c);
|
||||
pending_empty = false;
|
||||
} else if !cur.is_empty() || pending_empty {
|
||||
out.push(std::mem::take(&mut cur));
|
||||
pending_empty = false;
|
||||
if start.is_none() {
|
||||
start = Some(i);
|
||||
}
|
||||
ended_with_sep = false;
|
||||
} else {
|
||||
if let Some(s) = start.take() {
|
||||
toks.push(&lowered[s..i]);
|
||||
}
|
||||
ended_with_sep = true;
|
||||
}
|
||||
}
|
||||
out.push(cur);
|
||||
out
|
||||
if let Some(s) = start {
|
||||
toks.push(&lowered[s..]);
|
||||
}
|
||||
if lowered.starts_with(|c: char| !is_tok(c)) {
|
||||
toks.insert(0, "");
|
||||
}
|
||||
if ended_with_sep {
|
||||
toks.push("");
|
||||
}
|
||||
toks
|
||||
}
|
||||
|
||||
/// Boundary-aware path-signal matching: short (<4 char) signals match path
|
||||
@@ -127,7 +224,7 @@ pub fn path_matches(signal: &str, path: &str) -> bool {
|
||||
if s.chars().count() >= 4 {
|
||||
return path.to_lowercase().contains(&s);
|
||||
}
|
||||
split_tokens(&path.to_lowercase(), false).iter().any(|t| t == &s)
|
||||
split_tokens(&path.to_lowercase(), false).iter().any(|t| *t == s)
|
||||
}
|
||||
|
||||
/// Same discipline for entity ids. Prefix signals ending in ':' (e.g.
|
||||
@@ -142,20 +239,19 @@ pub fn entity_matches(signal: &str, entity_id: &str) -> bool {
|
||||
if s.chars().count() >= 4 {
|
||||
return entity_id.to_lowercase().contains(&s);
|
||||
}
|
||||
split_tokens(&entity_id.to_lowercase(), true).iter().any(|t| t == &s)
|
||||
split_tokens(&entity_id.to_lowercase(), true).iter().any(|t| *t == s)
|
||||
}
|
||||
|
||||
// ── small JSON accessors ──────────────────────────────────────────────────
|
||||
// ── small JSON accessors (borrowed; owned only where the output needs it) ──
|
||||
|
||||
fn str_list(v: &Value) -> Vec<String> {
|
||||
fn str_list(v: &Value) -> Vec<&str> {
|
||||
v.as_array()
|
||||
.map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect())
|
||||
.map(|a| a.iter().filter_map(|x| x.as_str()).collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn sorted_strings(mut v: Vec<String>) -> Vec<String> {
|
||||
v.sort();
|
||||
v
|
||||
fn str_list_owned(v: &Value) -> Vec<String> {
|
||||
str_list(v).into_iter().map(String::from).collect()
|
||||
}
|
||||
|
||||
/// `ss.relationships(doc, from_id=e)` — including the falsy-filter quirk:
|
||||
@@ -216,8 +312,9 @@ pub fn construct_pattern(input: &Map<String, Value>) -> Result<Map<String, Value
|
||||
input.get("obligations").cloned().unwrap_or_else(|| Value::Array(vec![])),
|
||||
);
|
||||
for key in ["provenance", "exclusions", "toolchain_notes", "runtime_effects"] {
|
||||
let vals = input.get(key).map(str_list).unwrap_or_default();
|
||||
out.insert(key.into(), json!(sorted_strings(vals)));
|
||||
let mut vals = input.get(key).map(str_list_owned).unwrap_or_default();
|
||||
vals.sort_unstable();
|
||||
out.insert(key.into(), json!(vals));
|
||||
}
|
||||
for (k, v) in input.iter() {
|
||||
if !KNOWN_KEYS.contains(&k.as_str()) {
|
||||
@@ -246,9 +343,9 @@ pub fn revise_pattern(
|
||||
}
|
||||
merged.insert("revision".into(), json!(revision + 1));
|
||||
let mut ev: Vec<String> = evidence.iter().filter_map(|e| e.as_str().map(String::from)).collect();
|
||||
ev.sort();
|
||||
ev.sort_unstable();
|
||||
let mut changed: Vec<String> = changes.map(|ch| ch.keys().cloned().collect()).unwrap_or_default();
|
||||
changed.sort();
|
||||
changed.sort_unstable();
|
||||
let entry = json!({
|
||||
"prior_revision": revision,
|
||||
"reason": reason.chars().take(200).collect::<String>(),
|
||||
@@ -272,7 +369,7 @@ pub fn record_pattern_proposal(
|
||||
pattern_id: &str,
|
||||
provider: &str,
|
||||
model: &str,
|
||||
evidence: Vec<String>,
|
||||
evidence: Vec<&str>,
|
||||
confidence: f64,
|
||||
) -> Result<Value, String> {
|
||||
if provider.is_empty() || model.is_empty() {
|
||||
@@ -281,12 +378,14 @@ pub fn record_pattern_proposal(
|
||||
if !(0.0..=1.0).contains(&confidence) {
|
||||
return Err("confidence must be in [0,1]".to_string());
|
||||
}
|
||||
let mut ev = evidence;
|
||||
ev.sort_unstable();
|
||||
Ok(json!({
|
||||
"kind": "pattern_proposal",
|
||||
"pattern_id": pattern_id,
|
||||
"provider": provider,
|
||||
"model": model,
|
||||
"evidence": sorted_strings(evidence),
|
||||
"evidence": ev,
|
||||
"confidence": confidence,
|
||||
"verification_state": "PENDING",
|
||||
}))
|
||||
@@ -319,113 +418,93 @@ pub fn evaluate_applicability(
|
||||
surface: &Map<String, Value>,
|
||||
authorities: &Map<String, Value>,
|
||||
) -> Value {
|
||||
let pattern_id = pattern.get("pattern_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let objective = need.get("objective").and_then(|v| v.as_str()).unwrap_or("").to_string();
|
||||
let exclusions = pattern.get("exclusions").map(str_list).unwrap_or_default();
|
||||
let need_exclusions = need
|
||||
let pattern_id = pattern.get("pattern_id").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let objective = need.get("objective").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let exclusions: Vec<&str> = pattern.get("exclusions").map(str_list).unwrap_or_default();
|
||||
let need_exclusions: Vec<&str> = need
|
||||
.get("constraints")
|
||||
.and_then(|c| c.get("pattern_exclusions"))
|
||||
.map(str_list)
|
||||
.unwrap_or_default();
|
||||
|
||||
// §3/§18: PROJECT evidence overrides the pattern.
|
||||
let mut declared = exclusions;
|
||||
declared.extend(need_exclusions.clone());
|
||||
let objective_low = objective.to_lowercase();
|
||||
for ex in &declared {
|
||||
if objective_low.contains(&ex.to_lowercase()) || need_exclusions.iter().any(|x| x == ex) {
|
||||
for ex in exclusions.iter().copied().chain(need_exclusions.iter().copied()) {
|
||||
if objective_low.contains(&ex.to_lowercase()) || need_exclusions.contains(&ex) {
|
||||
return json!({
|
||||
"pattern": pattern_id,
|
||||
"verdict": DOES_NOT_APPLY,
|
||||
"verdict": ApplicabilityVerdict::DoesNotApply.as_str(),
|
||||
"reason": format!("declared exclusion matched: {ex}"),
|
||||
"evidence": {"exclusion": ex, "source": "project/need declaration"},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let signals = pattern.get("signals").and_then(|v| v.as_object()).cloned().unwrap_or_default();
|
||||
if signals.is_empty() {
|
||||
return json!({
|
||||
"pattern": pattern_id,
|
||||
"verdict": UNKNOWN,
|
||||
"reason": "pattern declares no applicability signals — nothing deterministic to check",
|
||||
"evidence": {},
|
||||
});
|
||||
}
|
||||
let signals = match pattern.get("signals").and_then(|v| v.as_object()) {
|
||||
Some(s) if !s.is_empty() => s,
|
||||
_ => {
|
||||
return json!({
|
||||
"pattern": pattern_id,
|
||||
"verdict": ApplicabilityVerdict::Unknown.as_str(),
|
||||
"reason": "pattern declares no applicability signals — nothing deterministic to check",
|
||||
"evidence": {},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let mut groups_hit: Vec<String> = Vec::new();
|
||||
let mut groups_declared: i64 = 0;
|
||||
let mut groups_hit: Vec<&'static str> = Vec::new();
|
||||
let mut groups_declared: usize = 0;
|
||||
let mut evidence = Map::new();
|
||||
|
||||
let paths: Vec<String> = surface
|
||||
let paths: Vec<&str> = surface
|
||||
.get("affected_paths")
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(surface.get("workflow_config_surfaces").map(str_list).unwrap_or_default())
|
||||
.collect();
|
||||
let entities: Vec<String> =
|
||||
surface.get("affected_existing_entities").map(str_list).unwrap_or_default();
|
||||
let entities: Vec<&str> = surface.get("affected_existing_entities").map(str_list).unwrap_or_default();
|
||||
|
||||
let mut check = |group: &str, candidates: Vec<String>, present: &dyn Fn(&str) -> bool| {
|
||||
let mut check = |group: &'static str, candidates: &[&str], present: &dyn Fn(&str) -> bool| {
|
||||
if candidates.is_empty() {
|
||||
return;
|
||||
}
|
||||
groups_declared += 1;
|
||||
let hits: Vec<String> = candidates.iter().filter(|c| present(c)).cloned().collect();
|
||||
let hits: Vec<&str> = candidates.iter().filter(|c| present(c)).copied().collect();
|
||||
if !hits.is_empty() {
|
||||
groups_hit.push(group.to_string());
|
||||
groups_hit.push(group);
|
||||
evidence.insert(group.to_string(), json!(hits.iter().take(5).collect::<Vec<_>>()));
|
||||
}
|
||||
};
|
||||
|
||||
check(
|
||||
"surface_kinds",
|
||||
signals.get("surface_kinds").map(str_list).unwrap_or_default(),
|
||||
&|c| surface_kind_present(c, surface),
|
||||
);
|
||||
check(
|
||||
"path_contains",
|
||||
signals.get("path_contains").map(str_list).unwrap_or_default(),
|
||||
&|c| paths.iter().any(|p| path_matches(c, p)),
|
||||
);
|
||||
check(
|
||||
"entities_match",
|
||||
signals.get("entities_match").map(str_list).unwrap_or_default(),
|
||||
&|c| entities.iter().any(|e| entity_matches(c, e)),
|
||||
);
|
||||
let surface_kinds = signals.get("surface_kinds").map(str_list).unwrap_or_default();
|
||||
check("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)));
|
||||
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)));
|
||||
let objective_words: Vec<String> =
|
||||
objective.replace('/', " ").split_whitespace().map(|w| w.to_lowercase()).collect();
|
||||
check(
|
||||
"objective_any",
|
||||
signals
|
||||
.get("objective_any")
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|w| w.to_lowercase())
|
||||
.collect(),
|
||||
&|c| objective_words.iter().any(|w| w == c),
|
||||
);
|
||||
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));
|
||||
|
||||
if let Some(contracts) = signals.get("contracts_match").and_then(|v| v.as_array()) {
|
||||
if !contracts.is_empty() {
|
||||
groups_declared += 1;
|
||||
let wanted: Vec<&str> = contracts.iter().filter_map(|c| c.as_str()).collect();
|
||||
let mut hits: Vec<String> = Vec::new();
|
||||
for doc in authorities.values() {
|
||||
for e in &entities {
|
||||
for r in relationships(doc, e) {
|
||||
let rtype = r.get("type").and_then(|t| t.as_str()).unwrap_or("");
|
||||
let to = r.get("to").and_then(|t| t.as_str()).unwrap_or("");
|
||||
if REL_TYPES.contains(&rtype) && wanted.contains(&to) {
|
||||
hits.push(format!("{e}->{to}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let hits: Vec<String> = authorities
|
||||
.values()
|
||||
.flat_map(|doc| entities.iter().map(move |&e| (doc, e)))
|
||||
.flat_map(|(doc, e)| relationships(doc, e).into_iter().map(move |r| (e, r)))
|
||||
.filter(|(_, r)| REL_TYPES.contains(&r.get("type").and_then(|t| t.as_str()).unwrap_or("")))
|
||||
.map(|(e, r)| (e, r.get("to").and_then(|t| t.as_str()).unwrap_or("")))
|
||||
.filter(|(_, to)| wanted.contains(to))
|
||||
.map(|(e, to)| format!("{e}->{to}"))
|
||||
.collect();
|
||||
if !hits.is_empty() {
|
||||
groups_hit.push("contracts_match".to_string());
|
||||
groups_hit.push("contracts_match");
|
||||
evidence.insert("contracts_match".into(), json!(hits.iter().take(5).collect::<Vec<_>>()));
|
||||
}
|
||||
}
|
||||
@@ -434,24 +513,24 @@ pub fn evaluate_applicability(
|
||||
if groups_declared == 0 {
|
||||
return json!({
|
||||
"pattern": pattern_id,
|
||||
"verdict": UNKNOWN,
|
||||
"verdict": ApplicabilityVerdict::Unknown.as_str(),
|
||||
"reason": "no applicable signal groups declared",
|
||||
"evidence": {},
|
||||
});
|
||||
}
|
||||
let verdict = if groups_hit.len() as i64 == groups_declared {
|
||||
APPLIES
|
||||
let verdict = if groups_hit.len() == groups_declared {
|
||||
ApplicabilityVerdict::Applies
|
||||
} else if !groups_hit.is_empty() {
|
||||
PARTIAL
|
||||
ApplicabilityVerdict::Partial
|
||||
} else {
|
||||
DOES_NOT_APPLY
|
||||
ApplicabilityVerdict::DoesNotApply
|
||||
};
|
||||
let mut hit_sorted = groups_hit.clone();
|
||||
hit_sorted.sort();
|
||||
hit_sorted.sort_unstable();
|
||||
let detail = if hit_sorted.is_empty() { "none".to_string() } else { py_str_list(&hit_sorted) };
|
||||
json!({
|
||||
"pattern": pattern_id,
|
||||
"verdict": verdict,
|
||||
"verdict": verdict.as_str(),
|
||||
"reason": format!("{}/{} signal groups produced evidence: {}", groups_hit.len(), groups_declared, detail),
|
||||
"evidence": evidence,
|
||||
})
|
||||
@@ -465,15 +544,15 @@ pub fn obligation_when_hit(
|
||||
need: &Map<String, Value>,
|
||||
surface: &Map<String, Value>,
|
||||
authorities: &Map<String, Value>,
|
||||
) -> (String, String) {
|
||||
let paths: Vec<String> = surface
|
||||
) -> (ObligationVerdict, String) {
|
||||
let paths: Vec<&str> = surface
|
||||
.get("affected_paths")
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.chain(surface.get("workflow_config_surfaces").map(str_list).unwrap_or_default())
|
||||
.collect();
|
||||
let entities: Vec<String> = surface
|
||||
let entities: Vec<&str> = surface
|
||||
.get("affected_existing_entities")
|
||||
.map(str_list)
|
||||
.unwrap_or_default()
|
||||
@@ -481,66 +560,69 @@ pub fn obligation_when_hit(
|
||||
.chain(surface.get("affected_ui_surfaces").map(str_list).unwrap_or_default())
|
||||
.chain(surface.get("affected_regions").map(str_list).unwrap_or_default())
|
||||
.collect();
|
||||
let when = match obligation.get("when") {
|
||||
None | Some(Value::Null) => {
|
||||
return (OBL_UNKNOWN.to_string(), "obligation declares no applicability condition".to_string());
|
||||
}
|
||||
Some(w) => w,
|
||||
};
|
||||
if when.get("always").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
return (
|
||||
OBL_REQUIRED.to_string(),
|
||||
|
||||
match when_shape(obligation) {
|
||||
WhenShape::Unconditioned => (
|
||||
ObligationVerdict::Unknown,
|
||||
"obligation declares no applicability condition".to_string(),
|
||||
),
|
||||
WhenShape::Always => (
|
||||
ObligationVerdict::Required,
|
||||
"pattern-level obligation (always applies when the pattern applies)".to_string(),
|
||||
);
|
||||
}
|
||||
if let Some(subs) = when.get("path_contains") {
|
||||
let hits: Vec<String> =
|
||||
str_list(subs).iter().flat_map(|sub| paths.iter().filter(|p| path_matches(sub, p)).cloned()).collect();
|
||||
if !hits.is_empty() {
|
||||
return (OBL_APPLICABLE.to_string(), format!("affected path(s): {}", py_str_list(&hits[..3.min(hits.len())])));
|
||||
),
|
||||
WhenShape::PathContains(subs) => {
|
||||
let hits: Vec<&str> = str_list(subs)
|
||||
.iter()
|
||||
.flat_map(|sub| paths.iter().filter(|p| path_matches(sub, p)).copied())
|
||||
.collect();
|
||||
if !hits.is_empty() {
|
||||
return (ObligationVerdict::Applicable, format!("affected path(s): {}", py_str_list(&hits[..3.min(hits.len())])));
|
||||
}
|
||||
(ObligationVerdict::NotApplicable, format!("no affected path matches {}", py_repr(subs)))
|
||||
}
|
||||
return (OBL_NOT_APPLICABLE.to_string(), format!("no affected path matches {}", py_repr(subs)));
|
||||
}
|
||||
if let Some(subs) = when.get("entities_match") {
|
||||
let hits: Vec<String> = str_list(subs)
|
||||
.iter()
|
||||
.flat_map(|sub| entities.iter().filter(|e| entity_matches(sub, e)).cloned())
|
||||
.collect();
|
||||
if !hits.is_empty() {
|
||||
return (
|
||||
OBL_APPLICABLE.to_string(),
|
||||
format!("affected entity/entities: {}", py_str_list(&hits[..3.min(hits.len())])),
|
||||
);
|
||||
WhenShape::EntitiesMatch(subs) => {
|
||||
let hits: Vec<&str> = str_list(subs)
|
||||
.iter()
|
||||
.flat_map(|sub| entities.iter().filter(|e| entity_matches(sub, e)).copied())
|
||||
.collect();
|
||||
if !hits.is_empty() {
|
||||
return (
|
||||
ObligationVerdict::Applicable,
|
||||
format!("affected entity/entities: {}", py_str_list(&hits[..3.min(hits.len())])),
|
||||
);
|
||||
}
|
||||
(ObligationVerdict::NotApplicable, format!("no affected entity matches {}", py_repr(subs)))
|
||||
}
|
||||
return (OBL_NOT_APPLICABLE.to_string(), format!("no affected entity matches {}", py_repr(subs)));
|
||||
}
|
||||
if let Some(wanted) = when.get("contracts_touched") {
|
||||
let mut touched: Vec<String> = need
|
||||
.get("constraints")
|
||||
.and_then(|c| c.get("contracts"))
|
||||
.map(str_list)
|
||||
.unwrap_or_default();
|
||||
for doc in authorities.values() {
|
||||
for e in &entities {
|
||||
for r in relationships(doc, e) {
|
||||
let rtype = r.get("type").and_then(|t| t.as_str()).unwrap_or("");
|
||||
let to = r.get("to").and_then(|t| t.as_str()).unwrap_or("").to_string();
|
||||
if REL_TYPES.contains(&rtype) && !touched.contains(&to) {
|
||||
touched.push(to);
|
||||
WhenShape::ContractsTouched(wanted) => {
|
||||
let mut touched: Vec<String> = need
|
||||
.get("constraints")
|
||||
.and_then(|c| c.get("contracts"))
|
||||
.map(str_list_owned)
|
||||
.unwrap_or_default();
|
||||
for doc in authorities.values() {
|
||||
for e in &entities {
|
||||
for r in relationships(doc, e) {
|
||||
let rtype = r.get("type").and_then(|t| t.as_str()).unwrap_or("");
|
||||
let to = r.get("to").and_then(|t| t.as_str()).unwrap_or("");
|
||||
if REL_TYPES.contains(&rtype) && !touched.iter().any(|t| t == to) {
|
||||
touched.push(to.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let hits: Vec<&str> =
|
||||
str_list(wanted).into_iter().filter(|c| touched.iter().any(|t| t == c)).collect();
|
||||
if !hits.is_empty() {
|
||||
return (ObligationVerdict::Applicable, format!("contract(s) touched: {}", py_str_list(&hits)));
|
||||
}
|
||||
(ObligationVerdict::NotApplicable, format!("touched contracts do not include {}", py_repr(wanted)))
|
||||
}
|
||||
let hits: Vec<String> =
|
||||
str_list(wanted).into_iter().filter(|c| touched.contains(c)).collect();
|
||||
if !hits.is_empty() {
|
||||
return (OBL_APPLICABLE.to_string(), format!("contract(s) touched: {}", py_str_list(&hits)));
|
||||
WhenShape::Unrecognized(keys) => {
|
||||
let mut keys = keys;
|
||||
keys.sort_unstable();
|
||||
(ObligationVerdict::Unknown, format!("unrecognized when-condition shape: {}", py_str_list(&keys)))
|
||||
}
|
||||
return (OBL_NOT_APPLICABLE.to_string(), format!("touched contracts do not include {}", py_repr(wanted)));
|
||||
}
|
||||
let mut keys: Vec<String> = when.as_object().map(|m| m.keys().cloned().collect()).unwrap_or_default();
|
||||
keys.sort();
|
||||
(OBL_UNKNOWN.to_string(), format!("unrecognized when-condition shape: {}", py_str_list(&keys)))
|
||||
}
|
||||
|
||||
/// Derive this pattern's obligations FILTERED by project evidence (§7).
|
||||
@@ -559,7 +641,8 @@ pub fn derive_pattern_obligations(
|
||||
let mut unresolved = Vec::new();
|
||||
if let Some(obs) = pattern.get("obligations").and_then(|v| v.as_array()) {
|
||||
for o in obs {
|
||||
let o_obj = o.as_object().cloned().unwrap_or_default();
|
||||
let empty = Map::new();
|
||||
let o_obj: &Map<String, Value> = o.as_object().unwrap_or(&empty);
|
||||
let mut entry = Map::new();
|
||||
entry.insert("id".into(), o.get("id").cloned().unwrap_or(Value::Null));
|
||||
entry.insert("obligation".into(), o.get("obligation").cloned().unwrap_or(Value::Null));
|
||||
@@ -590,15 +673,13 @@ pub fn derive_pattern_obligations(
|
||||
rejected.push(Value::Object(entry));
|
||||
continue;
|
||||
}
|
||||
let (verdict, reason) = obligation_when_hit(&o_obj, need, surface, authorities);
|
||||
entry.insert("verdict".into(), Value::String(verdict.clone()));
|
||||
let (verdict, reason) = obligation_when_hit(o_obj, need, surface, authorities);
|
||||
entry.insert("verdict".into(), Value::String(verdict.as_str().to_string()));
|
||||
entry.insert("reason".into(), Value::String(reason));
|
||||
if verdict == OBL_REQUIRED || verdict == OBL_APPLICABLE {
|
||||
accepted.push(Value::Object(entry));
|
||||
} else if verdict == OBL_NOT_APPLICABLE {
|
||||
rejected.push(Value::Object(entry));
|
||||
} else {
|
||||
unresolved.push(Value::Object(entry));
|
||||
match verdict {
|
||||
ObligationVerdict::Required | ObligationVerdict::Applicable => accepted.push(Value::Object(entry)),
|
||||
ObligationVerdict::NotApplicable => rejected.push(Value::Object(entry)),
|
||||
ObligationVerdict::Unknown => unresolved.push(Value::Object(entry)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -655,7 +736,7 @@ pub fn combine_pattern_obligations(derivations: &[Value]) -> Value {
|
||||
}
|
||||
}
|
||||
}
|
||||
order.sort();
|
||||
order.sort_unstable();
|
||||
let obligations: Vec<Value> = order.into_iter().filter_map(|k| merged.remove(&k)).collect();
|
||||
json!({"obligations": obligations, "conflicts": conflicts})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user