use crate::engine::{CompiledPolicy, Hit};
use crate::input::Prepared;
use crate::views::NormView;
use crate::Config;
pub const HANDLED: &[&str] = &["SLOP-F004"];
pub(crate) fn shared_tool_nouns(cp: &CompiledPolicy) -> Vec<String> {
cp.pkg
.rule_by_id("SLOP-C010")
.map(|r| super::contrast::str_list(r, "tool_nouns"))
.unwrap_or_default()
}
pub fn evaluate(
cp: &CompiledPolicy,
prepared: &Prepared,
norm: &NormView,
config: &Config,
hits: &mut Vec<Hit>,
) {
let Some(idx) = super::active(cp, config, "SLOP-F004") else {
return;
};
let rule = &cp.pkg.rules[idx];
let markers = super::contrast::str_list(rule, "design_markers");
let reception = super::contrast::str_list(rule, "reception_markers");
let tool_nouns = shared_tool_nouns(cp);
let text = norm.text.as_str();
let src = prepared.text.as_str();
for block in super::contrast::block_ranges(text) {
for sentence in super::contrast::sentence_ranges(text, block, &[]) {
let toks = super::contrast::tokenize(text, sentence.clone());
if toks.is_empty() {
continue;
}
if !toks.iter().any(|t| tool_nouns.contains(&t.word)) {
continue;
}
let mut at = 0usize;
while at < toks.len() {
let design = super::contrast::phrase_at_any(&toks, at, &markers);
let instruct = super::contrast::phrase_at_any(&toks, at, &reception);
let (matched, detail) = match (design, instruct) {
(Some(d), _) => (d, "the bargain behind the design"),
(None, Some(r)) => (r, "how to take the text"),
(None, None) => {
at += 1;
continue;
}
};
let last = at + matched.1.len - 1;
let span = toks[at].start..toks[last].end;
at = last + 1;
let Some(source_span) = norm.to_source(span.clone()) else {
continue;
};
let source_span = crate::widen_to_char_boundaries(src, source_span);
if source_span.start >= source_span.end {
continue;
}
let mut hit = Hit::new(idx, source_span);
hit.quoted = norm.all_quoted(&span);
hit.trigger = Some(text[span].to_string());
hit.detail = Some(detail.to_string());
hits.push(hit);
}
}
}
}