Skip to main content

code_moniker_check/check/eval/
mod.rs

1// code-moniker: ignore-file[smell-clone-reflex]
2// Rule compilation builds owned executable specs from borrowed configuration entries.
3use std::cell::OnceCell;
4use std::collections::HashMap;
5
6mod collection;
7mod layout;
8mod local;
9mod metrics;
10mod pairs;
11mod value;
12
13use crate::check::config::{Config, ConfigError, KindRules, RuleSeverity, config_section};
14use crate::check::expr::{
15	self, Atom, Domain, Lhs, LhsExpr, Node, NumberExpr, Op, QuantKind, Rhs, SegmentScope,
16	VerticalLayout,
17};
18use code_moniker_core::core::code_graph::{CodeGraph, DefRecord};
19use code_moniker_core::core::kinds::{KIND_COMMENT, REF_CALLS, REF_METHOD_CALL};
20use code_moniker_core::core::moniker::query::bare_callable_name;
21use code_moniker_core::core::shape::Shape;
22use code_moniker_core::core::uri::{UriConfig, to_uri};
23use code_moniker_core::lang::Lang;
24use code_moniker_workspace::lines::line_range;
25
26use collection::{collection_has_pair_binding, eval_collection_size, eval_collection_subset};
27use layout::eval_vertical_layout;
28use local::{
29	AggregateEval, DomainItem, domain_items, eval_aggregate, eval_entropy, eval_mode,
30	project_def_lhs_value,
31};
32use metrics::eval_metric;
33use pairs::{eval_pair_count, eval_pair_quantifier};
34use value::{Value, apply_op, apply_op_values, number_expr_label};
35
36fn is_call_ref_kind(kind: &[u8]) -> bool {
37	matches!(kind, REF_CALLS | REF_METHOD_CALL)
38}
39
40#[derive(Debug, Clone, serde::Serialize)]
41pub struct Violation {
42	pub rule_id: String,
43	pub severity: RuleSeverity,
44	pub moniker: String,
45	pub kind: String,
46	#[serde(serialize_with = "serialize_lines")]
47	pub lines: (u32, u32),
48	pub message: String,
49	#[serde(skip_serializing_if = "Option::is_none")]
50	pub explanation: Option<String>,
51}
52
53fn serialize_lines<S: serde::Serializer>(v: &(u32, u32), s: S) -> Result<S::Ok, S::Error> {
54	use serde::ser::SerializeTuple;
55	let mut t = s.serialize_tuple(2)?;
56	t.serialize_element(&v.0)?;
57	t.serialize_element(&v.1)?;
58	t.end()
59}
60
61#[cfg(test)]
62pub(crate) fn evaluate(
63	graph: &CodeGraph,
64	source: &str,
65	lang: Lang,
66	cfg: &Config,
67	scheme: &str,
68) -> Result<Vec<Violation>, ConfigError> {
69	let compiled = compile_rules(cfg, lang, scheme)?;
70	Ok(evaluate_compiled(graph, source, lang, scheme, &compiled))
71}
72
73/// Build the compiled rule set for a single `lang`. Parses every rule
74/// expression, resolves aliases. Call once per language and reuse across
75/// many files of that language — the eval pipeline is shaped so the heavy
76/// work happens here, not per-file.
77pub fn compile_rules(cfg: &Config, lang: Lang, scheme: &str) -> Result<CompiledRules, ConfigError> {
78	CompiledRules::for_lang(cfg, lang, scheme)
79}
80
81pub fn evaluate_compiled(
82	graph: &CodeGraph,
83	source: &str,
84	lang: Lang,
85	scheme: &str,
86	compiled: &CompiledRules,
87) -> Vec<Violation> {
88	evaluate_compiled_with_requirements(graph, source, lang, scheme, compiled, None)
89}
90
91pub(in crate::check) trait RequirementResolver: Sync {
92	fn exists(&self, pattern: &str, source: &DefRecord, scheme: &str) -> bool;
93	fn descendant_defs<'a>(&'a self, _owner: &DefRecord, _inner: &Domain) -> Vec<&'a DefRecord> {
94		Vec::new()
95	}
96}
97
98pub(in crate::check) fn evaluate_compiled_with_requirements(
99	graph: &CodeGraph,
100	source: &str,
101	lang: Lang,
102	scheme: &str,
103	compiled: &CompiledRules,
104	requirements: Option<&dyn RequirementResolver>,
105) -> Vec<Violation> {
106	let need_doc_anchors = compiled
107		.by_kind
108		.values()
109		.any(|r| r.require_doc_for_vis.is_some())
110		|| compiled
111			.by_shape
112			.values()
113			.any(|r| r.require_doc_for_vis.is_some());
114	let ctx = EvalCtx {
115		graph,
116		requirements,
117		source,
118		lang,
119		uri_cfg: UriConfig { scheme },
120		parent_counts: parent_counts_by_kind(graph),
121		children_by_parent: children_by_parent(graph),
122		out_refs_by_source: out_refs_by_source(graph),
123		in_refs_by_target: in_refs_by_target(graph),
124		comment_ends: if need_doc_anchors {
125			comment_end_bytes(graph)
126		} else {
127			Vec::new()
128		},
129		doc_anchors: if need_doc_anchors {
130			doc_anchors_by_def(graph)
131		} else {
132			HashMap::new()
133		},
134		def_index: OnceCell::new(),
135	};
136	let mut out = Vec::new();
137
138	for (idx, d) in graph.defs().enumerate() {
139		let Ok(kind_str) = std::str::from_utf8(&d.kind) else {
140			continue;
141		};
142		let kind_rules = compiled.for_kind(kind_str);
143		if let Some(rules) = kind_rules {
144			let target = RuleTarget {
145				scope: DefScope { record: d, idx },
146				kind: kind_str,
147			};
148			for rule in &rules.rules {
149				eval_rule(rule, d, idx, kind_str, &ctx, &mut out);
150			}
151			check_require_doc_comment(target, rules, &ctx, &mut out);
152		}
153		if let Some(shape) = d.shape()
154			&& let Some(rules) = compiled.for_shape(shape)
155		{
156			for rule in &rules.rules {
157				if rule.explicit_id
158					&& kind_rules
159						.is_some_and(|kind_rules| kind_rules.has_explicit_rule_id(&rule.id))
160				{
161					continue;
162				}
163				eval_shape_rule(rule, d, idx, kind_str, &ctx, &mut out);
164			}
165			if kind_rules.is_none_or(|kind_rules| kind_rules.require_doc_for_vis.is_none()) {
166				if let Some(rule_id) = &rules.require_doc_rule_id {
167					let target = RuleTarget {
168						scope: DefScope { record: d, idx },
169						kind: kind_str,
170					};
171					check_require_doc_comment_with_id(
172						target,
173						rules,
174						rule_id.clone(),
175						&ctx,
176						&mut out,
177					);
178				}
179			}
180		}
181	}
182
183	for r in graph.refs() {
184		for rule in &compiled.refs {
185			eval_ref_rule(rule, r, graph, &ctx, &mut out);
186		}
187	}
188
189	out
190}
191
192#[derive(Debug, Clone, serde::Serialize)]
193pub struct RuleReport {
194	pub rule_id: String,
195	pub severity: RuleSeverity,
196	pub domain: String,
197	pub evaluated: usize,
198	pub matches: usize,
199	pub violations: usize,
200	#[serde(skip_serializing_if = "Option::is_none")]
201	pub antecedent_matches: Option<usize>,
202	#[serde(skip_serializing_if = "Option::is_none")]
203	pub warning: Option<String>,
204}
205
206pub fn rule_report_compiled(
207	graph: &CodeGraph,
208	source: &str,
209	lang: Lang,
210	scheme: &str,
211	compiled: &CompiledRules,
212) -> Vec<RuleReport> {
213	rule_report_compiled_with_requirements(graph, source, lang, scheme, compiled, None)
214}
215
216pub(in crate::check) fn rule_report_compiled_with_requirements(
217	graph: &CodeGraph,
218	source: &str,
219	lang: Lang,
220	scheme: &str,
221	compiled: &CompiledRules,
222	requirements: Option<&dyn RequirementResolver>,
223) -> Vec<RuleReport> {
224	let need_doc_anchors = compiled
225		.by_kind
226		.values()
227		.any(|r| r.require_doc_for_vis.is_some())
228		|| compiled
229			.by_shape
230			.values()
231			.any(|r| r.require_doc_for_vis.is_some());
232	let ctx = EvalCtx {
233		graph,
234		requirements,
235		source,
236		lang,
237		uri_cfg: UriConfig { scheme },
238		parent_counts: parent_counts_by_kind(graph),
239		children_by_parent: children_by_parent(graph),
240		out_refs_by_source: out_refs_by_source(graph),
241		in_refs_by_target: in_refs_by_target(graph),
242		comment_ends: if need_doc_anchors {
243			comment_end_bytes(graph)
244		} else {
245			Vec::new()
246		},
247		doc_anchors: if need_doc_anchors {
248			doc_anchors_by_def(graph)
249		} else {
250			HashMap::new()
251		},
252		def_index: OnceCell::new(),
253	};
254	let mut out = Vec::new();
255	push_kind_rule_reports(&mut out, graph, lang, &ctx, compiled);
256	push_shape_rule_reports(&mut out, graph, &ctx, compiled);
257	push_ref_rule_reports(&mut out, graph, &ctx, compiled);
258	out.sort_by(|a, b| a.rule_id.cmp(&b.rule_id));
259	out
260}
261
262fn push_kind_rule_reports(
263	out: &mut Vec<RuleReport>,
264	graph: &CodeGraph,
265	lang: Lang,
266	ctx: &EvalCtx<'_, '_>,
267	compiled: &CompiledRules,
268) {
269	for (kind, rules) in &compiled.by_kind {
270		for rule in &rules.rules {
271			let mut report = RuleReport::new(rule_id(lang, kind, &rule.id), kind.clone(), rule);
272			for (idx, d) in graph.defs().enumerate() {
273				if d.kind.as_ref() != kind.as_bytes() {
274					continue;
275				}
276				report.evaluated += 1;
277				let premise =
278					implication_premise(rule).map(|premise| eval_node(premise, d, idx, ctx));
279				report.record(eval_node(&rule.root, d, idx, ctx), premise);
280			}
281			out.push(report);
282		}
283		if rules.require_doc_for_vis.is_some() {
284			let mut report = RuleReport::new_require_doc(
285				rule_id(lang, kind, "require_doc_comment"),
286				kind.clone(),
287			);
288			for (idx, d) in graph.defs().enumerate() {
289				if d.kind.as_ref() != kind.as_bytes() {
290					continue;
291				}
292				report.evaluated += 1;
293				report.record(
294					eval_require_doc_comment(d, idx, rules, ctx).map_or(
295						NodeOutcome::NotApplicable,
296						|has_doc| {
297							if has_doc {
298								NodeOutcome::Pass
299							} else {
300								NodeOutcome::Fail(Failure {
301									atom_raw: "require_doc_comment".to_string(),
302									lhs_label: "doc_comment".to_string(),
303									actual: "missing".to_string(),
304									expected: "present".to_string(),
305									def_idx: None,
306									details: None,
307								})
308							}
309						},
310					),
311					None,
312				);
313			}
314			out.push(report);
315		}
316	}
317}
318
319fn push_shape_rule_reports(
320	out: &mut Vec<RuleReport>,
321	graph: &CodeGraph,
322	ctx: &EvalCtx<'_, '_>,
323	compiled: &CompiledRules,
324) {
325	for (shape, rules) in &compiled.by_shape {
326		for rule in &rules.rules {
327			let mut report =
328				RuleReport::new(rule.rule_id.clone(), format!("shape:{shape} defs"), rule);
329			for (idx, d) in graph.defs().enumerate() {
330				if !def_has_shape(d, shape) {
331					continue;
332				}
333				let Ok(kind_str) = std::str::from_utf8(&d.kind) else {
334					continue;
335				};
336				if compiled.for_kind(kind_str).is_some_and(|kind_rules| {
337					rule.explicit_id && kind_rules.has_explicit_rule_id(&rule.id)
338				}) {
339					continue;
340				}
341				report.evaluated += 1;
342				let premise =
343					implication_premise(rule).map(|premise| eval_node(premise, d, idx, ctx));
344				report.record(eval_node(&rule.root, d, idx, ctx), premise);
345			}
346			out.push(report);
347		}
348		if let Some(rule_id) = &rules.require_doc_rule_id {
349			let mut report =
350				RuleReport::new_require_doc(rule_id.clone(), format!("shape:{shape} defs"));
351			for (idx, d) in graph.defs().enumerate() {
352				if !def_has_shape(d, shape) {
353					continue;
354				}
355				let Ok(kind_str) = std::str::from_utf8(&d.kind) else {
356					continue;
357				};
358				if compiled
359					.for_kind(kind_str)
360					.is_some_and(|kind_rules| kind_rules.require_doc_for_vis.is_some())
361				{
362					continue;
363				}
364				report.evaluated += 1;
365				report.record(
366					eval_require_doc_comment(d, idx, rules, ctx).map_or(
367						NodeOutcome::NotApplicable,
368						|has_doc| {
369							if has_doc {
370								NodeOutcome::Pass
371							} else {
372								NodeOutcome::Fail(Failure {
373									atom_raw: "require_doc_comment".to_string(),
374									lhs_label: "doc_comment".to_string(),
375									actual: "missing".to_string(),
376									expected: "present".to_string(),
377									def_idx: None,
378									details: None,
379								})
380							}
381						},
382					),
383					None,
384				);
385			}
386			out.push(report);
387		}
388	}
389}
390
391fn push_ref_rule_reports(
392	out: &mut Vec<RuleReport>,
393	graph: &CodeGraph,
394	ctx: &EvalCtx<'_, '_>,
395	compiled: &CompiledRules,
396) {
397	for rule in &compiled.refs {
398		let mut report = RuleReport::new(rule.rule_id.clone(), "refs".to_string(), rule);
399		for r in graph.refs() {
400			report.evaluated += 1;
401			let premise = implication_premise(rule).map(|premise| eval_ref_node(premise, r, ctx));
402			report.record(eval_ref_node(&rule.root, r, ctx), premise);
403		}
404		out.push(report);
405	}
406}
407
408impl RuleReport {
409	fn new(rule_id: String, domain: String, rule: &CompiledRule) -> Self {
410		Self {
411			rule_id,
412			severity: rule.severity,
413			domain,
414			evaluated: 0,
415			matches: 0,
416			violations: 0,
417			antecedent_matches: implication_premise(rule).map(|_| 0),
418			warning: None,
419		}
420	}
421
422	fn new_require_doc(rule_id: String, domain: String) -> Self {
423		Self {
424			rule_id,
425			severity: RuleSeverity::Error,
426			domain,
427			evaluated: 0,
428			matches: 0,
429			violations: 0,
430			antecedent_matches: None,
431			warning: None,
432		}
433	}
434
435	fn record(&mut self, outcome: NodeOutcome, premise: Option<NodeOutcome>) {
436		if matches!(premise, Some(NodeOutcome::Pass)) {
437			self.antecedent_matches = Some(self.antecedent_matches.unwrap_or(0) + 1);
438		}
439		match outcome {
440			NodeOutcome::Pass => {
441				if premise.is_none() || matches!(premise, Some(NodeOutcome::Pass)) {
442					self.matches += 1;
443				}
444			}
445			NodeOutcome::Fail(_) => self.violations += 1,
446			NodeOutcome::NotApplicable => {}
447		}
448	}
449}
450
451fn implication_premise(rule: &CompiledRule) -> Option<&Node> {
452	match &rule.root {
453		Node::Implies(premise, _) => Some(premise),
454		_ => None,
455	}
456}
457
458struct EvalCtx<'g, 'src> {
459	graph: &'g CodeGraph,
460	requirements: Option<&'g dyn RequirementResolver>,
461	source: &'src str,
462	lang: Lang,
463	uri_cfg: UriConfig<'src>,
464	parent_counts: HashMap<(usize, &'g [u8]), u32>,
465	children_by_parent: HashMap<usize, Vec<usize>>,
466	out_refs_by_source: HashMap<usize, Vec<usize>>,
467	in_refs_by_target: HashMap<Vec<u8>, Vec<usize>>,
468	comment_ends: Vec<u32>,
469	doc_anchors: HashMap<usize, u32>,
470	def_index: OnceCell<HashMap<Vec<u8>, usize>>,
471}
472
473#[derive(Debug)]
474struct CompiledRule {
475	id: String,
476	explicit_id: bool,
477	rule_id: String,
478	raw_expr: String,
479	expanded_expr: String,
480	root: Node,
481	severity: RuleSeverity,
482	message: Option<String>,
483	rationale: Option<String>,
484}
485
486#[derive(Default)]
487struct CompiledKindRules {
488	rules: Vec<CompiledRule>,
489	require_doc_for_vis: Option<String>,
490	require_doc_rule_id: Option<String>,
491}
492
493pub struct CompiledRules {
494	by_kind: HashMap<String, CompiledKindRules>,
495	by_shape: HashMap<String, CompiledKindRules>,
496	refs: Vec<CompiledRule>,
497}
498
499#[derive(Debug, Clone, serde::Serialize)]
500pub struct CompiledRuleSpec {
501	pub rule_id: String,
502	pub severity: RuleSeverity,
503	pub lang: String,
504	pub domain: String,
505	pub kind: Option<String>,
506	pub expr: String,
507	pub expanded_expr: String,
508	pub message: Option<String>,
509	#[serde(skip_serializing_if = "Option::is_none")]
510	pub rationale: Option<String>,
511	pub require_doc_comment: Option<String>,
512}
513
514impl CompiledRules {
515	fn for_lang(cfg: &Config, lang: Lang, scheme: &str) -> Result<Self, ConfigError> {
516		compile_rules_for_lang(cfg, lang, scheme)
517	}
518
519	fn for_kind(&self, kind: &str) -> Option<&CompiledKindRules> {
520		self.by_kind.get(kind)
521	}
522
523	fn for_shape(&self, shape: Shape) -> Option<&CompiledKindRules> {
524		self.by_shape.get(shape.as_str())
525	}
526
527	pub fn specs(&self, lang: Lang) -> Vec<CompiledRuleSpec> {
528		compiled_rule_specs(self, lang)
529	}
530}
531
532fn compile_rules_for_lang(
533	cfg: &Config,
534	lang: Lang,
535	scheme: &str,
536) -> Result<CompiledRules, ConfigError> {
537	let section = config_section(lang);
538	let allowed = crate::check::config::allowed_kinds_for(lang);
539	let aliases = crate::check::config::resolve_aliases(&cfg.aliases)?;
540	let mut by_kind: HashMap<String, CompiledKindRules> = HashMap::new();
541	let mut by_shape: HashMap<String, CompiledKindRules> = HashMap::new();
542	let mut per_lang_refs: Vec<&crate::check::config::RuleEntry> = Vec::new();
543	for (kind, rules) in cfg.for_lang(lang).kinds.iter() {
544		if kind == "refs" {
545			per_lang_refs.extend(rules.rules.iter());
546			continue;
547		}
548		by_kind.insert(
549			kind.clone(),
550			compile(rules, section, kind, scheme, &allowed, &aliases)?,
551		);
552	}
553	for (kind, rules) in cfg.default.kinds.iter() {
554		if kind == "refs" {
555			continue;
556		}
557		if !allowed.contains(&kind.as_str()) {
558			continue;
559		}
560		if !by_kind.contains_key(kind.as_str()) {
561			by_kind.insert(
562				kind.clone(),
563				compile(rules, "default", kind, scheme, &allowed, &aliases)?,
564			);
565		}
566	}
567	compile_shape_rules_into(
568		&mut by_shape,
569		&cfg.shape,
570		"shape",
571		scheme,
572		&allowed,
573		&aliases,
574	)?;
575	compile_shape_rules_into(
576		&mut by_shape,
577		&cfg.for_lang(lang).shape,
578		&format!("{section}.shape"),
579		scheme,
580		&allowed,
581		&aliases,
582	)?;
583	let mut refs = Vec::with_capacity(cfg.refs.rules.len() + per_lang_refs.len());
584	for (idx, entry) in cfg.refs.rules.iter().enumerate() {
585		let id = entry.fallback_id(idx);
586		let at = format!("refs.{id}");
587		refs.push(compile_rule_entry(
588			entry, id, at, scheme, &allowed, &aliases,
589		)?);
590	}
591	for (idx, entry) in per_lang_refs.iter().enumerate() {
592		let id = entry.fallback_id(idx);
593		let at = format!("{section}.refs.{id}");
594		refs.push(compile_rule_entry(
595			entry, id, at, scheme, &allowed, &aliases,
596		)?);
597	}
598	Ok(CompiledRules {
599		by_kind,
600		by_shape,
601		refs,
602	})
603}
604
605fn compiled_rule_specs(rules: &CompiledRules, lang: Lang) -> Vec<CompiledRuleSpec> {
606	let mut out = Vec::new();
607	for (kind, rules) in &rules.by_kind {
608		for rule in &rules.rules {
609			out.push(CompiledRuleSpec {
610				rule_id: rule_id(lang, kind, &rule.id),
611				lang: lang.tag().to_string(),
612				domain: format!("{kind} defs"),
613				kind: Some(kind.clone()),
614				expr: rule.raw_expr.clone(),
615				expanded_expr: rule.expanded_expr.clone(),
616				message: rule.message.clone(),
617				severity: rule.severity,
618				rationale: rule.rationale.clone(),
619				require_doc_comment: None,
620			});
621		}
622		if let Some(value) = &rules.require_doc_for_vis {
623			out.push(CompiledRuleSpec {
624				rule_id: rule_id(lang, kind, "require_doc_comment"),
625				lang: lang.tag().to_string(),
626				domain: format!("{kind} defs"),
627				kind: Some(kind.clone()),
628				expr: format!("require_doc_comment = \"{value}\""),
629				expanded_expr: format!("require_doc_comment = \"{value}\""),
630				message: None,
631				severity: RuleSeverity::Error,
632				rationale: None,
633				require_doc_comment: Some(value.clone()),
634			});
635		}
636	}
637	for (shape, rules) in &rules.by_shape {
638		for rule in &rules.rules {
639			out.push(CompiledRuleSpec {
640				rule_id: rule.rule_id.clone(),
641				lang: lang.tag().to_string(),
642				domain: format!("shape:{shape} defs"),
643				kind: None,
644				expr: rule.raw_expr.clone(),
645				expanded_expr: rule.expanded_expr.clone(),
646				message: rule.message.clone(),
647				severity: rule.severity,
648				rationale: rule.rationale.clone(),
649				require_doc_comment: None,
650			});
651		}
652		if let (Some(value), Some(rule_id)) =
653			(&rules.require_doc_for_vis, &rules.require_doc_rule_id)
654		{
655			out.push(CompiledRuleSpec {
656				rule_id: rule_id.clone(),
657				lang: lang.tag().to_string(),
658				domain: format!("shape:{shape} defs"),
659				kind: None,
660				expr: format!("require_doc_comment = \"{value}\""),
661				expanded_expr: format!("require_doc_comment = \"{value}\""),
662				message: None,
663				severity: RuleSeverity::Error,
664				rationale: None,
665				require_doc_comment: Some(value.clone()),
666			});
667		}
668	}
669	for rule in &rules.refs {
670		out.push(CompiledRuleSpec {
671			rule_id: rule.rule_id.clone(),
672			lang: lang.tag().to_string(),
673			domain: "refs".to_string(),
674			kind: None,
675			expr: rule.raw_expr.clone(),
676			expanded_expr: rule.expanded_expr.clone(),
677			message: rule.message.clone(),
678			severity: rule.severity,
679			rationale: rule.rationale.clone(),
680			require_doc_comment: None,
681		});
682	}
683	out.sort_by(|a, b| a.rule_id.cmp(&b.rule_id));
684	out
685}
686
687fn compile_rule_entry(
688	entry: &crate::check::config::RuleEntry,
689	id: String,
690	at: String,
691	scheme: &str,
692	allowed_kinds: &[&str],
693	aliases: &HashMap<String, String>,
694) -> Result<CompiledRule, ConfigError> {
695	let expanded = crate::check::config::substitute_aliases(&entry.expr, aliases, &at)?;
696	let parsed = expr::parse(&expanded, scheme, allowed_kinds).map_err(|error| {
697		ConfigError::InvalidExpr {
698			at: at.clone(),
699			error,
700		}
701	})?;
702	Ok(CompiledRule {
703		id,
704		explicit_id: entry.id.is_some(),
705		rule_id: at,
706		raw_expr: entry.expr.clone(),
707		expanded_expr: expanded,
708		root: parsed.root,
709		message: entry.message.clone(),
710		severity: entry.severity,
711		rationale: entry.rationale.clone(),
712	})
713}
714
715fn compile(
716	rules: &KindRules,
717	section: &str,
718	kind: &str,
719	scheme: &str,
720	allowed_kinds: &[&str],
721	aliases: &HashMap<String, String>,
722) -> Result<CompiledKindRules, ConfigError> {
723	let mut compiled = Vec::with_capacity(rules.rules.len());
724	for (idx, entry) in rules.rules.iter().enumerate() {
725		let id = entry.fallback_id(idx);
726		let at = format!("{section}.{kind}.{id}");
727		let expanded = crate::check::config::substitute_aliases(&entry.expr, aliases, &at)?;
728		let parsed = expr::parse(&expanded, scheme, allowed_kinds).map_err(|error| {
729			ConfigError::InvalidExpr {
730				at: at.clone(),
731				error,
732			}
733		})?;
734		compiled.push(CompiledRule {
735			id,
736			explicit_id: entry.id.is_some(),
737			rule_id: at,
738			raw_expr: entry.expr.clone(),
739			expanded_expr: expanded,
740			root: parsed.root,
741			message: entry.message.clone(),
742			severity: entry.severity,
743			rationale: entry.rationale.clone(),
744		});
745	}
746	Ok(CompiledKindRules {
747		rules: compiled,
748		require_doc_for_vis: rules.require_doc_comment.clone(),
749		require_doc_rule_id: rules
750			.require_doc_comment
751			.as_ref()
752			.map(|_| format!("{section}.{kind}.require_doc_comment")),
753	})
754}
755
756fn compile_shape_rules_into(
757	dst: &mut HashMap<String, CompiledKindRules>,
758	src: &HashMap<String, KindRules>,
759	section: &str,
760	scheme: &str,
761	allowed_kinds: &[&str],
762	aliases: &HashMap<String, String>,
763) -> Result<(), ConfigError> {
764	for (shape, rules) in src {
765		let compiled = compile(rules, section, shape, scheme, allowed_kinds, aliases)?;
766		match dst.get_mut(shape) {
767			Some(existing) => merge_compiled_kind_rules(existing, compiled),
768			None => {
769				dst.insert(shape.clone(), compiled);
770			}
771		}
772	}
773	Ok(())
774}
775
776fn merge_compiled_kind_rules(base: &mut CompiledKindRules, ov: CompiledKindRules) {
777	for rule in ov.rules {
778		match rule
779			.explicit_id
780			.then(|| {
781				base.rules
782					.iter()
783					.position(|r| r.explicit_id && r.id == rule.id)
784			})
785			.flatten()
786		{
787			Some(idx) => base.rules[idx] = rule,
788			None => base.rules.push(rule),
789		}
790	}
791	if ov.require_doc_for_vis.is_some() {
792		base.require_doc_for_vis = ov.require_doc_for_vis;
793		base.require_doc_rule_id = ov.require_doc_rule_id;
794	}
795}
796
797impl CompiledKindRules {
798	fn has_explicit_rule_id(&self, id: &str) -> bool {
799		self.rules
800			.iter()
801			.any(|rule| rule.explicit_id && rule.id == id)
802	}
803}
804
805fn rule_id(lang: Lang, kind: &str, rule: &str) -> String {
806	format!("{}.{}.{}", config_section(lang), kind, rule)
807}
808
809fn lines_of(d: &DefRecord, source: &str) -> (u32, u32) {
810	match d.position {
811		Some((s, e)) => line_range(source, s, e),
812		None => (0, 0),
813	}
814}
815
816fn def_name(d: &DefRecord) -> Option<String> {
817	let last = d.moniker.as_view().segments().last()?;
818	let bare = bare_callable_name(last.name);
819	std::str::from_utf8(bare).ok().map(|s| s.to_string())
820}
821
822fn render_template(tpl: &str, vars: &[(&str, &str)]) -> String {
823	let mut out = tpl.to_string();
824	for (k, v) in vars {
825		let placeholder = format!("{{{k}}}");
826		if out.contains(&placeholder) {
827			out = out.replace(&placeholder, v);
828		}
829	}
830	out
831}
832
833#[derive(Clone, Copy)]
834struct DefScope<'a> {
835	record: &'a DefRecord,
836	idx: usize,
837}
838
839#[derive(Clone, Copy)]
840struct RuleTarget<'a> {
841	scope: DefScope<'a>,
842	kind: &'a str,
843}
844
845fn eval_rule(
846	rule: &CompiledRule,
847	d: &DefRecord,
848	def_idx: usize,
849	kind: &str,
850	ctx: &EvalCtx<'_, '_>,
851	out: &mut Vec<Violation>,
852) {
853	let target = RuleTarget {
854		scope: DefScope {
855			record: d,
856			idx: def_idx,
857		},
858		kind,
859	};
860	eval_rule_with_id(rule, target, rule_id(ctx.lang, kind, &rule.id), ctx, out);
861}
862
863fn eval_shape_rule(
864	rule: &CompiledRule,
865	d: &DefRecord,
866	def_idx: usize,
867	kind: &str,
868	ctx: &EvalCtx<'_, '_>,
869	out: &mut Vec<Violation>,
870) {
871	let target = RuleTarget {
872		scope: DefScope {
873			record: d,
874			idx: def_idx,
875		},
876		kind,
877	};
878	eval_rule_with_id(rule, target, rule.rule_id.clone(), ctx, out);
879}
880
881fn eval_rule_with_id(
882	rule: &CompiledRule,
883	target: RuleTarget<'_>,
884	rule_id: String,
885	ctx: &EvalCtx<'_, '_>,
886	out: &mut Vec<Violation>,
887) {
888	let Failure {
889		atom_raw,
890		lhs_label,
891		actual,
892		expected,
893		def_idx,
894		details,
895	} = match eval_node(&rule.root, target.scope.record, target.scope.idx, ctx) {
896		NodeOutcome::Pass | NodeOutcome::NotApplicable => return,
897		NodeOutcome::Fail(f) => f,
898	};
899	let diagnostic = def_idx
900		.map(|idx| ctx.graph.def_at(idx))
901		.unwrap_or(target.scope.record);
902	let diagnostic_kind = std::str::from_utf8(&diagnostic.kind).unwrap_or(target.kind);
903	let name = def_name(diagnostic).unwrap_or_default();
904	let name_snake = to_snake_case(&name);
905	let moniker = to_uri(&diagnostic.moniker, &ctx.uri_cfg);
906	let (start_line, end_line) = lines_of(diagnostic, ctx.source);
907	let message = format!(
908		"{diagnostic_kind} `{name}` fails `{atom_raw}` ({lhs_label} = {actual}, expected {expected})",
909	);
910	let explanation = rule
911		.message
912		.as_ref()
913		.map(|tpl| {
914			let mut rendered = render_template(
915				tpl,
916				&[
917					("name", &name),
918					("name.snake", &name_snake),
919					("kind", diagnostic_kind),
920					("moniker", &moniker),
921					("expr", &rule.raw_expr),
922					("actual", &actual),
923					("value", &actual),
924					("expected", &expected),
925					("pattern", &expected),
926					("lines", &actual),
927					("limit", &expected),
928					("count", &actual),
929				],
930			);
931			if let Some(details) = &details {
932				if !rendered.is_empty() {
933					rendered.push('\n');
934				}
935				rendered.push_str(details);
936			}
937			rendered
938		})
939		.or(details);
940	out.push(Violation {
941		rule_id,
942		severity: rule.severity,
943		moniker,
944		kind: diagnostic_kind.to_string(),
945		lines: (start_line, end_line),
946		message,
947		explanation,
948	});
949}
950
951fn eval_ref_rule(
952	rule: &CompiledRule,
953	r: &code_moniker_core::core::code_graph::RefRecord,
954	graph: &CodeGraph,
955	ctx: &EvalCtx<'_, '_>,
956	out: &mut Vec<Violation>,
957) {
958	let Failure {
959		atom_raw,
960		lhs_label,
961		actual,
962		expected,
963		def_idx: _,
964		details,
965	} = match eval_ref_node(&rule.root, r, ctx) {
966		NodeOutcome::Pass | NodeOutcome::NotApplicable => return,
967		NodeOutcome::Fail(f) => f,
968	};
969	let source_def = graph.def_at(r.source);
970	let source_uri = to_uri(&source_def.moniker, &ctx.uri_cfg);
971	let target_uri = to_uri(&r.target, &ctx.uri_cfg);
972	let ref_kind = std::str::from_utf8(&r.kind).unwrap_or_default();
973	let (start_line, end_line) = match r.position {
974		Some((s, e)) => line_range(ctx.source, s, e),
975		None => (0, 0),
976	};
977	let message = format!(
978		"ref {ref_kind} {source_uri} → {target_uri} fails `{atom_raw}` ({lhs_label} = {actual}, expected {expected})"
979	);
980	let source_name = name_of(&source_def.moniker).unwrap_or_default();
981	let source_kind = last_segment_kind(&source_def.moniker).unwrap_or_default();
982	let source_shape = shape_name_of_last_segment(&source_def.moniker);
983	let target_name = name_of(&r.target).unwrap_or_default();
984	let target_kind = last_segment_kind(&r.target).unwrap_or_default();
985	let target_shape = shape_name_of_last_segment(&r.target);
986	let explanation = rule
987		.message
988		.as_ref()
989		.map(|tpl| {
990			let mut rendered = render_template(
991				tpl,
992				&[
993					("kind", ref_kind),
994					("source.name", &source_name),
995					("source.kind", &source_kind),
996					("source.shape", &source_shape),
997					("source.moniker", &source_uri),
998					("target.name", &target_name),
999					("target.kind", &target_kind),
1000					("target.shape", &target_shape),
1001					("target.moniker", &target_uri),
1002					("atom", &atom_raw),
1003					("actual", &actual),
1004					("expected", &expected),
1005				],
1006			);
1007			if let Some(details) = &details {
1008				if !rendered.is_empty() {
1009					rendered.push('\n');
1010				}
1011				rendered.push_str(details);
1012			}
1013			rendered
1014		})
1015		.or(details);
1016	out.push(Violation {
1017		rule_id: rule.rule_id.clone(),
1018		severity: rule.severity,
1019		moniker: target_uri,
1020		kind: ref_kind.to_string(),
1021		lines: (start_line, end_line),
1022		message,
1023		explanation,
1024	});
1025}
1026
1027fn eval_ref_node(
1028	node: &Node,
1029	r: &code_moniker_core::core::code_graph::RefRecord,
1030	ctx: &EvalCtx<'_, '_>,
1031) -> NodeOutcome {
1032	eval_ref_node_with_current(node, r, r, ctx)
1033}
1034
1035fn eval_ref_node_with_current(
1036	node: &Node,
1037	r: &code_moniker_core::core::code_graph::RefRecord,
1038	current: &code_moniker_core::core::code_graph::RefRecord,
1039	ctx: &EvalCtx<'_, '_>,
1040) -> NodeOutcome {
1041	walk_node(
1042		node,
1043		&|a| eval_ref_atom(a, r, current, ctx),
1044		&|kind, domain, filter| eval_quantifier_ref(kind, domain, filter, r, ctx),
1045		&|_| NodeOutcome::NotApplicable,
1046		&|_| NodeOutcome::NotApplicable,
1047	)
1048}
1049
1050fn eval_ref_atom(
1051	atom: &Atom,
1052	r: &code_moniker_core::core::code_graph::RefRecord,
1053	current: &code_moniker_core::core::code_graph::RefRecord,
1054	ctx: &EvalCtx<'_, '_>,
1055) -> AtomOutcome {
1056	let Some(value) = eval_ref_lhs_expr_value(&atom.lhs, r, ctx) else {
1057		return AtomOutcome::NotApplicable;
1058	};
1059	if let Rhs::Projection(other) = &atom.rhs {
1060		let Some(rhs_val) = resolve_ref_lhs(*other, r, ctx) else {
1061			return AtomOutcome::NotApplicable;
1062		};
1063		return apply_op_values(&value, atom.op, &rhs_val);
1064	}
1065	if let Rhs::CurrentProjection(other) = &atom.rhs {
1066		let Some(rhs_val) = resolve_ref_lhs(*other, current, ctx) else {
1067			return AtomOutcome::NotApplicable;
1068		};
1069		return apply_op_values(&value, atom.op, &rhs_val);
1070	}
1071	if let Rhs::Number(expr) = &atom.rhs {
1072		let Some(rhs_val) = eval_number_expr_ref(expr, r, ctx).map(Value::Number) else {
1073			return AtomOutcome::NotApplicable;
1074		};
1075		return apply_op_values(&value, atom.op, &rhs_val);
1076	}
1077	apply_op(&value, atom)
1078}
1079
1080fn eval_ref_lhs_expr_value(
1081	lhs: &LhsExpr,
1082	r: &code_moniker_core::core::code_graph::RefRecord,
1083	ctx: &EvalCtx<'_, '_>,
1084) -> Option<Value> {
1085	match lhs {
1086		LhsExpr::Attr(lhs) => resolve_ref_lhs(*lhs, r, ctx),
1087		LhsExpr::SegmentOf { scope, kind } => match scope {
1088			SegmentScope::Def => None,
1089			SegmentScope::Source => {
1090				let source_def = ctx.graph.def_at(r.source);
1091				Some(Value::Str(first_segment_name(
1092					&source_def.moniker,
1093					kind.as_bytes(),
1094				)))
1095			}
1096			SegmentScope::Target => {
1097				Some(Value::Str(first_segment_name(&r.target, kind.as_bytes())))
1098			}
1099		},
1100		LhsExpr::Number(expr) => eval_number_expr_ref(expr, r, ctx).map(Value::Number),
1101		LhsExpr::Collection(_) | LhsExpr::Mode(_) | LhsExpr::PairProjection(_) => None,
1102	}
1103}
1104
1105fn eval_quantifier_ref(
1106	kind: QuantKind,
1107	domain: &Domain,
1108	filter: &Node,
1109	r: &code_moniker_core::core::code_graph::RefRecord,
1110	ctx: &EvalCtx<'_, '_>,
1111) -> NodeOutcome {
1112	let items = ref_domain_items(domain, r, ctx);
1113	if items.is_empty() {
1114		return match kind {
1115			QuantKind::All | QuantKind::None => NodeOutcome::Pass,
1116			QuantKind::Any => NodeOutcome::Fail(Failure {
1117				atom_raw: format!("any({})", domain_debug_label(domain)),
1118				lhs_label: "any".to_string(),
1119				actual: "0 matches".to_string(),
1120				expected: "at least one".to_string(),
1121				def_idx: None,
1122				details: None,
1123			}),
1124		};
1125	}
1126	let mut matched = 0usize;
1127	for item in items {
1128		let outcome = match item {
1129			DomainItem::Ref { record } => eval_ref_node_with_current(filter, record, r, ctx),
1130			DomainItem::Def {
1131				idx: Some(idx),
1132				def,
1133			} => eval_node_with_self(filter, def, idx, r.source, ctx),
1134			DomainItem::Def { idx: None, def } => eval_external_def_node(filter, def, ctx),
1135			DomainItem::Segment { kind, name } => eval_node_segment(filter, kind, name),
1136		};
1137		if matches!(outcome, NodeOutcome::Pass) {
1138			matched += 1;
1139			if kind == QuantKind::Any {
1140				return NodeOutcome::Pass;
1141			}
1142		} else if matches!(outcome, NodeOutcome::Fail(_)) && kind == QuantKind::All {
1143			return outcome;
1144		}
1145	}
1146	match kind {
1147		QuantKind::Any => NodeOutcome::Fail(Failure {
1148			atom_raw: format!("any({})", domain_debug_label(domain)),
1149			lhs_label: "any".to_string(),
1150			actual: format!("{matched} matches"),
1151			expected: "at least one".to_string(),
1152			def_idx: None,
1153			details: None,
1154		}),
1155		QuantKind::All => NodeOutcome::Pass,
1156		QuantKind::None if matched == 0 => NodeOutcome::Pass,
1157		QuantKind::None => NodeOutcome::Fail(Failure {
1158			atom_raw: format!("none({})", domain_debug_label(domain)),
1159			lhs_label: "none".to_string(),
1160			actual: format!("{matched} matches"),
1161			expected: "0 matches".to_string(),
1162			def_idx: None,
1163			details: None,
1164		}),
1165	}
1166}
1167
1168fn ref_domain_items<'a>(
1169	domain: &Domain,
1170	r: &code_moniker_core::core::code_graph::RefRecord,
1171	ctx: &'a EvalCtx<'_, '_>,
1172) -> Vec<DomainItem<'a>> {
1173	match domain {
1174		Domain::SourceOutRefs | Domain::OutRefs => ctx
1175			.out_refs_by_source
1176			.get(&r.source)
1177			.into_iter()
1178			.flatten()
1179			.map(|idx| DomainItem::Ref {
1180				record: ctx.graph.ref_at(*idx),
1181			})
1182			.collect(),
1183		Domain::SourceInRefs | Domain::InRefs => {
1184			let source = ctx.graph.def_at(r.source);
1185			let key = source.moniker.as_encoded();
1186			ctx.in_refs_by_target
1187				.get(key)
1188				.into_iter()
1189				.flatten()
1190				.map(|idx| DomainItem::Ref {
1191					record: ctx.graph.ref_at(*idx),
1192				})
1193				.collect()
1194		}
1195		Domain::SourceAncestorOutRefs => ancestor_ref_items(r.source, ctx, true),
1196		Domain::SourceAncestorInRefs => ancestor_ref_items(r.source, ctx, false),
1197		Domain::Segments => ctx
1198			.graph
1199			.def_at(r.source)
1200			.moniker
1201			.as_view()
1202			.segments()
1203			.map(|seg| DomainItem::Segment {
1204				kind: seg.kind,
1205				name: seg.name,
1206			})
1207			.collect(),
1208		Domain::Children(_) | Domain::ChildrenByShape(_) | Domain::Descendants(_) => {
1209			domain_items(domain, r.source, ctx)
1210		}
1211		Domain::Pairs(_) => Vec::new(),
1212	}
1213}
1214
1215fn domain_debug_label(domain: &Domain) -> &'static str {
1216	match domain {
1217		Domain::Children(_) => "children",
1218		Domain::ChildrenByShape(_) => "shape",
1219		Domain::Descendants(_) => "descendants",
1220		Domain::Pairs(_) => "pairs",
1221		Domain::Segments => "segment",
1222		Domain::OutRefs => "out_refs",
1223		Domain::InRefs => "in_refs",
1224		Domain::SourceOutRefs => "source.out_refs",
1225		Domain::SourceInRefs => "source.in_refs",
1226		Domain::SourceAncestorOutRefs => "source.ancestors.out_refs",
1227		Domain::SourceAncestorInRefs => "source.ancestors.in_refs",
1228	}
1229}
1230
1231fn ancestor_ref_items<'a>(
1232	def_idx: usize,
1233	ctx: &'a EvalCtx<'_, '_>,
1234	outgoing: bool,
1235) -> Vec<DomainItem<'a>> {
1236	ancestor_ref_indexes(def_idx, ctx, outgoing)
1237		.into_iter()
1238		.map(|ref_idx| DomainItem::Ref {
1239			record: ctx.graph.ref_at(ref_idx),
1240		})
1241		.collect()
1242}
1243
1244fn ancestor_ref_indexes(def_idx: usize, ctx: &EvalCtx<'_, '_>, outgoing: bool) -> Vec<usize> {
1245	let mut items = Vec::new();
1246	let mut parent = ctx.graph.def_at(def_idx).parent;
1247	while let Some(idx) = parent {
1248		if outgoing {
1249			if let Some(refs) = ctx.out_refs_by_source.get(&idx) {
1250				items.extend(refs.iter().copied());
1251			}
1252		} else {
1253			let key = ctx.graph.def_at(idx).moniker.as_encoded();
1254			if let Some(refs) = ctx.in_refs_by_target.get(key) {
1255				items.extend(refs.iter().copied());
1256			}
1257		}
1258		parent = ctx.graph.def_at(idx).parent;
1259	}
1260	items
1261}
1262
1263fn resolve_ref_lhs(
1264	lhs: Lhs,
1265	r: &code_moniker_core::core::code_graph::RefRecord,
1266	ctx: &EvalCtx<'_, '_>,
1267) -> Option<Value> {
1268	let graph = ctx.graph;
1269	let source_def = graph.def_at(r.source);
1270	Some(match lhs {
1271		Lhs::Kind => Value::Str(std::str::from_utf8(&r.kind).ok()?.to_string()),
1272		Lhs::Confidence => Value::Str(std::str::from_utf8(&r.confidence).ok()?.to_string()),
1273		Lhs::StartLine => {
1274			let (s, e) = r.position?;
1275			let (sl, _) = line_range(ctx.source, s, e);
1276			Value::Number(sl as f64)
1277		}
1278		Lhs::EndLine => {
1279			let (s, e) = r.position?;
1280			let (_, el) = line_range(ctx.source, s, e);
1281			Value::Number(el as f64)
1282		}
1283		Lhs::StartByte => {
1284			let (s, _) = r.position?;
1285			Value::Number(s as f64)
1286		}
1287		Lhs::EndByte => {
1288			let (_, e) = r.position?;
1289			Value::Number(e as f64)
1290		}
1291		Lhs::Text => Value::Str(ref_text(r, ctx)?),
1292		Lhs::Moniker | Lhs::SourceMoniker => Value::Moniker(source_def.moniker.clone()),
1293		Lhs::ParentMoniker => Value::Moniker(source_def.moniker.parent()?),
1294		Lhs::SourceParentMoniker => Value::Moniker(source_def.moniker.parent()?),
1295		Lhs::ParentName => Value::Str(name_of(&source_def.moniker.parent()?)?),
1296		Lhs::ParentKind => Value::Str(last_segment_kind(&source_def.moniker.parent()?)?),
1297		Lhs::TargetMoniker => Value::Moniker(r.target.clone()),
1298		Lhs::TargetParentMoniker => Value::Moniker(r.target.parent()?),
1299		Lhs::SourceName => Value::Str(name_of(&source_def.moniker)?),
1300		Lhs::TargetName => Value::Str(name_of(&r.target)?),
1301		Lhs::SourceKind => Value::Str(last_segment_kind(&source_def.moniker)?),
1302		Lhs::TargetKind => Value::Str(last_segment_kind(&r.target)?),
1303		Lhs::Shape | Lhs::SourceShape => Value::Str(
1304			shape_of_last_segment(&source_def.moniker)?
1305				.as_str()
1306				.to_string(),
1307		),
1308		Lhs::TargetShape => Value::Str(shape_of_last_segment(&r.target)?.as_str().to_string()),
1309		Lhs::ParentShape => {
1310			let segs: Vec<_> = source_def.moniker.as_view().segments().collect();
1311			if segs.len() < 2 {
1312				return None;
1313			}
1314			let parent_kind = segs[segs.len() - 2].kind;
1315			Value::Str(
1316				code_moniker_core::core::shape::shape_of(parent_kind)?
1317					.as_str()
1318					.to_string(),
1319			)
1320		}
1321		Lhs::SourceVisibility => Value::Str(
1322			std::str::from_utf8(&source_def.visibility)
1323				.ok()?
1324				.to_string(),
1325		),
1326		Lhs::TargetVisibility => {
1327			let def = resolve_local_def(graph, &r.target)?;
1328			Value::Str(std::str::from_utf8(&def.visibility).ok()?.to_string())
1329		}
1330		Lhs::Name
1331		| Lhs::Visibility
1332		| Lhs::Lines
1333		| Lhs::Depth
1334		| Lhs::SegmentName
1335		| Lhs::SegmentKind => return None,
1336	})
1337}
1338
1339fn ref_text(
1340	r: &code_moniker_core::core::code_graph::RefRecord,
1341	ctx: &EvalCtx<'_, '_>,
1342) -> Option<String> {
1343	let (start, end) = r.position?;
1344	ctx.source
1345		.get(start as usize..end as usize)
1346		.map(ToString::to_string)
1347}
1348
1349fn name_of(m: &code_moniker_core::core::moniker::Moniker) -> Option<String> {
1350	let last = m.as_view().segments().last()?;
1351	let bare = code_moniker_core::core::moniker::query::bare_callable_name(last.name);
1352	std::str::from_utf8(bare).ok().map(|s| s.to_string())
1353}
1354
1355fn first_segment_name(m: &code_moniker_core::core::moniker::Moniker, kind: &[u8]) -> String {
1356	for seg in m.as_view().segments() {
1357		if seg.kind == kind {
1358			return std::str::from_utf8(seg.name)
1359				.unwrap_or_default()
1360				.to_string();
1361		}
1362	}
1363	String::new()
1364}
1365
1366fn last_segment_kind(m: &code_moniker_core::core::moniker::Moniker) -> Option<String> {
1367	let last = m.as_view().segments().last()?;
1368	std::str::from_utf8(last.kind).ok().map(|s| s.to_string())
1369}
1370
1371fn shape_of_last_segment(
1372	m: &code_moniker_core::core::moniker::Moniker,
1373) -> Option<code_moniker_core::core::shape::Shape> {
1374	let last = m.as_view().segments().last()?;
1375	code_moniker_core::core::shape::shape_of(last.kind)
1376}
1377
1378fn shape_name_of_last_segment(m: &code_moniker_core::core::moniker::Moniker) -> String {
1379	shape_of_last_segment(m)
1380		.map(|shape| shape.as_str().to_string())
1381		.unwrap_or_default()
1382}
1383
1384fn resolve_local_def<'g>(
1385	graph: &'g CodeGraph,
1386	m: &code_moniker_core::core::moniker::Moniker,
1387) -> Option<&'g DefRecord> {
1388	graph.defs().find(|d| d.moniker == *m)
1389}
1390
1391fn describe_lhs(lhs: &LhsExpr) -> &str {
1392	match lhs {
1393		LhsExpr::Attr(a) => a.as_str(),
1394		LhsExpr::Number(n) => number_expr_label(n),
1395		LhsExpr::Collection(_) => "collection",
1396		LhsExpr::Mode(_) => "mode",
1397		LhsExpr::PairProjection(_) => "pair",
1398		LhsExpr::SegmentOf { .. } => "segment",
1399	}
1400}
1401
1402#[derive(Debug)]
1403struct Failure {
1404	atom_raw: String,
1405	lhs_label: String,
1406	actual: String,
1407	expected: String,
1408	def_idx: Option<usize>,
1409	details: Option<String>,
1410}
1411
1412#[derive(Debug)]
1413enum NodeOutcome {
1414	Pass,
1415	Fail(Failure),
1416	NotApplicable,
1417}
1418
1419enum AtomOutcome {
1420	Pass,
1421	Fail { actual: String, expected: String },
1422	NotApplicable,
1423}
1424
1425/// Trivalent-logic walker shared by def/ref/segment evaluators. `atom_eval`
1426/// produces the atom leaf outcome; `quant_eval` handles `Node::Quantifier`
1427/// (scopes that can't iterate, like ref and segment, return NotApplicable).
1428fn walk_node<A, Q, R, L>(
1429	node: &Node,
1430	atom_eval: &A,
1431	quant_eval: &Q,
1432	require_eval: &R,
1433	layout_eval: &L,
1434) -> NodeOutcome
1435where
1436	A: Fn(&Atom) -> AtomOutcome,
1437	Q: Fn(QuantKind, &Domain, &Node) -> NodeOutcome,
1438	R: Fn(&str) -> NodeOutcome,
1439	L: Fn(&VerticalLayout) -> NodeOutcome,
1440{
1441	match node {
1442		Node::Atom(atom) => match atom_eval(atom) {
1443			AtomOutcome::Pass => NodeOutcome::Pass,
1444			AtomOutcome::Fail { actual, expected } => NodeOutcome::Fail(Failure {
1445				atom_raw: atom.raw.clone(),
1446				lhs_label: describe_lhs(&atom.lhs).to_string(),
1447				actual,
1448				expected,
1449				def_idx: None,
1450				details: None,
1451			}),
1452			AtomOutcome::NotApplicable => NodeOutcome::NotApplicable,
1453		},
1454		Node::And(children) => {
1455			let mut na = false;
1456			for c in children {
1457				match walk_node(c, atom_eval, quant_eval, require_eval, layout_eval) {
1458					NodeOutcome::Pass => {}
1459					NodeOutcome::Fail(f) => return NodeOutcome::Fail(f),
1460					NodeOutcome::NotApplicable => na = true,
1461				}
1462			}
1463			if na {
1464				NodeOutcome::NotApplicable
1465			} else {
1466				NodeOutcome::Pass
1467			}
1468		}
1469		Node::Or(children) => {
1470			let mut last_fail: Option<Failure> = None;
1471			let mut na = false;
1472			for c in children {
1473				match walk_node(c, atom_eval, quant_eval, require_eval, layout_eval) {
1474					NodeOutcome::Pass => return NodeOutcome::Pass,
1475					NodeOutcome::Fail(f) => last_fail = Some(f),
1476					NodeOutcome::NotApplicable => na = true,
1477				}
1478			}
1479			if na {
1480				NodeOutcome::NotApplicable
1481			} else if let Some(f) = last_fail {
1482				NodeOutcome::Fail(f)
1483			} else {
1484				NodeOutcome::NotApplicable
1485			}
1486		}
1487		Node::Not(inner) => {
1488			match walk_node(inner, atom_eval, quant_eval, require_eval, layout_eval) {
1489				NodeOutcome::Pass => NodeOutcome::Fail(Failure {
1490					atom_raw: "NOT (...)".to_string(),
1491					lhs_label: "NOT".to_string(),
1492					actual: "true".to_string(),
1493					expected: "false".to_string(),
1494					def_idx: None,
1495					details: None,
1496				}),
1497				NodeOutcome::Fail(_) => NodeOutcome::Pass,
1498				NodeOutcome::NotApplicable => NodeOutcome::NotApplicable,
1499			}
1500		}
1501		Node::Implies(prem, cons) => {
1502			match walk_node(prem, atom_eval, quant_eval, require_eval, layout_eval) {
1503				NodeOutcome::Pass => {
1504					walk_node(cons, atom_eval, quant_eval, require_eval, layout_eval)
1505				}
1506				NodeOutcome::Fail(_) => NodeOutcome::Pass,
1507				NodeOutcome::NotApplicable => NodeOutcome::NotApplicable,
1508			}
1509		}
1510		Node::Require(pattern) => require_eval(pattern),
1511		Node::VerticalLayout(layout) => layout_eval(layout),
1512		Node::Quantifier {
1513			kind,
1514			domain,
1515			filter,
1516		} => quant_eval(*kind, domain, filter),
1517	}
1518}
1519
1520fn eval_node(node: &Node, d: &DefRecord, def_idx: usize, ctx: &EvalCtx<'_, '_>) -> NodeOutcome {
1521	eval_node_with_self(node, d, def_idx, def_idx, ctx)
1522}
1523
1524fn eval_node_with_self(
1525	node: &Node,
1526	d: &DefRecord,
1527	def_idx: usize,
1528	self_idx: usize,
1529	ctx: &EvalCtx<'_, '_>,
1530) -> NodeOutcome {
1531	walk_node(
1532		node,
1533		&|a| eval_atom(a, d, def_idx, self_idx, ctx),
1534		&|kind, domain, filter| {
1535			eval_quantifier_def(
1536				kind,
1537				domain,
1538				filter,
1539				DefScope {
1540					record: d,
1541					idx: def_idx,
1542				},
1543				self_idx,
1544				ctx,
1545			)
1546		},
1547		&|pattern| eval_require(pattern, d, ctx),
1548		&|layout| eval_vertical_layout(layout, d, def_idx, ctx),
1549	)
1550}
1551
1552fn eval_require(pattern: &str, d: &DefRecord, ctx: &EvalCtx<'_, '_>) -> NodeOutcome {
1553	let Some(rendered) = render_requirement_pattern(pattern, d) else {
1554		return NodeOutcome::NotApplicable;
1555	};
1556	if local_requirement_exists(&rendered, ctx)
1557		|| ctx
1558			.requirements
1559			.is_some_and(|resolver| resolver.exists(&rendered, d, ctx.uri_cfg.scheme))
1560	{
1561		return NodeOutcome::Pass;
1562	}
1563	NodeOutcome::Fail(Failure {
1564		atom_raw: format!("require(\"{pattern}\")"),
1565		lhs_label: "require".to_string(),
1566		actual: "missing".to_string(),
1567		expected: rendered,
1568		def_idx: None,
1569		details: None,
1570	})
1571}
1572
1573fn local_requirement_exists(pattern: &str, ctx: &EvalCtx<'_, '_>) -> bool {
1574	let Ok(pattern) = crate::check::path::parse(pattern) else {
1575		return false;
1576	};
1577	ctx.graph
1578		.defs()
1579		.any(|def| crate::check::path::matches(&pattern, &def.moniker))
1580}
1581
1582fn render_requirement_pattern(pattern: &str, d: &DefRecord) -> Option<String> {
1583	let name = def_name(d)?;
1584	Some(
1585		pattern
1586			.replace("{name}", &name)
1587			.replace("{name.snake}", &to_snake_case(&name)),
1588	)
1589}
1590
1591fn to_snake_case(name: &str) -> String {
1592	let mut out = String::new();
1593	for (idx, ch) in name.chars().enumerate() {
1594		if ch.is_ascii_uppercase() {
1595			if idx > 0 {
1596				out.push('_');
1597			}
1598			out.push(ch.to_ascii_lowercase());
1599		} else {
1600			out.push(ch);
1601		}
1602	}
1603	out
1604}
1605
1606fn resolve_def_lhs(lhs: Lhs, d: &DefRecord, ctx: &EvalCtx<'_, '_>) -> Option<Value> {
1607	let source = ctx.source;
1608	let value = match lhs {
1609		Lhs::Name => Value::Str(def_name(d)?),
1610		Lhs::Kind => Value::Str(std::str::from_utf8(&d.kind).ok()?.to_string()),
1611		Lhs::Visibility => Value::Str(std::str::from_utf8(&d.visibility).ok()?.to_string()),
1612		Lhs::Lines => {
1613			let (s, e) = d.position?;
1614			let (sl, el) = line_range(source, s, e);
1615			Value::Number((el - sl + 1) as f64)
1616		}
1617		Lhs::StartLine => {
1618			let (s, e) = d.position?;
1619			let (sl, _) = line_range(source, s, e);
1620			Value::Number(sl as f64)
1621		}
1622		Lhs::EndLine => {
1623			let (s, e) = d.position?;
1624			let (_, el) = line_range(source, s, e);
1625			Value::Number(el as f64)
1626		}
1627		Lhs::StartByte => {
1628			let (s, _) = d.position?;
1629			Value::Number(s as f64)
1630		}
1631		Lhs::EndByte => {
1632			let (_, e) = d.position?;
1633			Value::Number(e as f64)
1634		}
1635		Lhs::Text => {
1636			let (s, e) = d.position?;
1637			Value::Str(source.get(s as usize..e as usize).unwrap_or("").to_string())
1638		}
1639		Lhs::Moniker => Value::Moniker(d.moniker.clone()),
1640		Lhs::ParentMoniker => Value::Moniker(d.moniker.parent()?),
1641		Lhs::Depth => Value::Number(d.moniker.as_view().segments().count() as f64),
1642		Lhs::ParentName => {
1643			let segs: Vec<_> = d.moniker.as_view().segments().collect();
1644			if segs.len() < 2 {
1645				return None;
1646			}
1647			let p = &segs[segs.len() - 2];
1648			let bare = bare_callable_name(p.name);
1649			Value::Str(std::str::from_utf8(bare).ok()?.to_string())
1650		}
1651		Lhs::ParentKind => {
1652			let segs: Vec<_> = d.moniker.as_view().segments().collect();
1653			if segs.len() < 2 {
1654				return None;
1655			}
1656			let p = &segs[segs.len() - 2];
1657			Value::Str(std::str::from_utf8(p.kind).ok()?.to_string())
1658		}
1659		Lhs::Shape => Value::Str(d.shape()?.as_str().to_string()),
1660		Lhs::ParentShape => {
1661			let segs: Vec<_> = d.moniker.as_view().segments().collect();
1662			if segs.len() < 2 {
1663				return None;
1664			}
1665			let parent_kind = segs[segs.len() - 2].kind;
1666			Value::Str(
1667				code_moniker_core::core::shape::shape_of(parent_kind)?
1668					.as_str()
1669					.to_string(),
1670			)
1671		}
1672		Lhs::SourceName => Value::Str(def_name(d)?),
1673		Lhs::SourceKind => Value::Str(std::str::from_utf8(&d.kind).ok()?.to_string()),
1674		Lhs::SourceShape => Value::Str(d.shape()?.as_str().to_string()),
1675		Lhs::SourceVisibility => Value::Str(std::str::from_utf8(&d.visibility).ok()?.to_string()),
1676		Lhs::SourceMoniker => Value::Moniker(d.moniker.clone()),
1677		Lhs::SourceParentMoniker => Value::Moniker(d.moniker.parent()?),
1678		Lhs::Confidence
1679		| Lhs::TargetName
1680		| Lhs::TargetKind
1681		| Lhs::TargetShape
1682		| Lhs::TargetVisibility
1683		| Lhs::TargetMoniker
1684		| Lhs::TargetParentMoniker
1685		| Lhs::SegmentName
1686		| Lhs::SegmentKind => return None,
1687	};
1688	Some(value)
1689}
1690
1691/// `count(<domain>, <filter>?)` evaluated in def scope. Counts items in
1692/// the domain for which `filter` evaluates to Pass.
1693fn eval_count(
1694	domain: &Domain,
1695	filter: Option<&Node>,
1696	d: &DefRecord,
1697	def_idx: usize,
1698	self_idx: usize,
1699	ctx: &EvalCtx<'_, '_>,
1700) -> u32 {
1701	match domain {
1702		Domain::Children(kind) => match filter {
1703			None => ctx
1704				.parent_counts
1705				.get(&(def_idx, kind.as_bytes()))
1706				.copied()
1707				.unwrap_or(0),
1708			Some(node) => count_children_filtered(d, def_idx, self_idx, kind, node, ctx),
1709		},
1710		Domain::ChildrenByShape(shape) => {
1711			count_children_by_shape(def_idx, self_idx, shape, filter, ctx)
1712		}
1713		Domain::Descendants(_) => count_domain_items(domain, filter, def_idx, self_idx, ctx),
1714		Domain::Pairs(inner) => eval_pair_count(inner, filter, def_idx, self_idx, ctx),
1715		Domain::Segments => count_segments(d, filter),
1716		Domain::OutRefs | Domain::SourceOutRefs => count_out_refs(d, def_idx, filter, ctx),
1717		Domain::InRefs | Domain::SourceInRefs => count_in_refs(d, filter, ctx),
1718		Domain::SourceAncestorOutRefs | Domain::SourceAncestorInRefs => {
1719			count_domain_items(domain, filter, def_idx, self_idx, ctx)
1720		}
1721	}
1722}
1723
1724fn count_domain_items(
1725	domain: &Domain,
1726	filter: Option<&Node>,
1727	def_idx: usize,
1728	self_idx: usize,
1729	ctx: &EvalCtx<'_, '_>,
1730) -> u32 {
1731	let items = domain_items(domain, def_idx, ctx);
1732	let Some(node) = filter else {
1733		return items.len() as u32;
1734	};
1735	items
1736		.into_iter()
1737		.filter(|item| match item {
1738			DomainItem::Def {
1739				idx: Some(idx),
1740				def,
1741			} => {
1742				matches!(
1743					eval_node_with_self(node, def, *idx, self_idx, ctx),
1744					NodeOutcome::Pass
1745				)
1746			}
1747			DomainItem::Def { idx: None, def } => {
1748				matches!(eval_external_def_node(node, def, ctx), NodeOutcome::Pass)
1749			}
1750			DomainItem::Ref { record } => {
1751				matches!(eval_ref_node(node, record, ctx), NodeOutcome::Pass)
1752			}
1753			DomainItem::Segment { kind, name } => {
1754				matches!(eval_node_segment(node, kind, name), NodeOutcome::Pass)
1755			}
1756		})
1757		.count() as u32
1758}
1759
1760fn eval_number_expr_def(
1761	expr: &NumberExpr,
1762	d: &DefRecord,
1763	def_idx: usize,
1764	self_idx: usize,
1765	ctx: &EvalCtx<'_, '_>,
1766) -> Option<f64> {
1767	match expr {
1768		NumberExpr::Literal(n) => Some(*n),
1769		NumberExpr::Projection(lhs) => match resolve_def_lhs(*lhs, d, ctx)? {
1770			Value::Number(n) => Some(n),
1771			_ => None,
1772		},
1773		NumberExpr::Count { domain, filter } => {
1774			Some(eval_count(domain, filter.as_deref(), d, def_idx, self_idx, ctx) as f64)
1775		}
1776		NumberExpr::Aggregate {
1777			kind,
1778			domain,
1779			expr,
1780			percentile,
1781		} => eval_aggregate(
1782			AggregateEval {
1783				kind: *kind,
1784				domain,
1785				expr,
1786				percentile: *percentile,
1787				def_idx,
1788				self_idx,
1789			},
1790			ctx,
1791		),
1792		NumberExpr::Metric { kind, binding } => {
1793			eval_metric(*kind, *binding, def_idx, self_idx, ctx)
1794		}
1795		NumberExpr::Entropy(collection) => eval_entropy(collection, def_idx, self_idx, ctx),
1796		NumberExpr::Size(collection) => {
1797			if collection_has_pair_binding(collection) {
1798				return None;
1799			}
1800			Some(eval_collection_size(collection, def_idx, self_idx, ctx) as f64)
1801		}
1802	}
1803}
1804
1805fn eval_number_expr_ref(
1806	expr: &NumberExpr,
1807	r: &code_moniker_core::core::code_graph::RefRecord,
1808	ctx: &EvalCtx<'_, '_>,
1809) -> Option<f64> {
1810	match expr {
1811		NumberExpr::Literal(n) => Some(*n),
1812		NumberExpr::Projection(lhs) => match resolve_ref_lhs(*lhs, r, ctx)? {
1813			Value::Number(n) => Some(n),
1814			_ => None,
1815		},
1816		NumberExpr::Count { domain, filter } => {
1817			Some(eval_count_ref(domain, filter.as_deref(), r, ctx) as f64)
1818		}
1819		NumberExpr::Aggregate { .. }
1820		| NumberExpr::Metric { .. }
1821		| NumberExpr::Entropy(_)
1822		| NumberExpr::Size(_) => None,
1823	}
1824}
1825
1826fn eval_count_ref(
1827	domain: &Domain,
1828	filter: Option<&Node>,
1829	r: &code_moniker_core::core::code_graph::RefRecord,
1830	ctx: &EvalCtx<'_, '_>,
1831) -> u32 {
1832	ref_domain_items(domain, r, ctx)
1833		.into_iter()
1834		.filter(|item| {
1835			let Some(filter) = filter else {
1836				return true;
1837			};
1838			match item {
1839				DomainItem::Ref { record } => {
1840					matches!(
1841						eval_ref_node_with_current(filter, record, r, ctx),
1842						NodeOutcome::Pass
1843					)
1844				}
1845				DomainItem::Def {
1846					idx: Some(idx),
1847					def,
1848				} => matches!(
1849					eval_node_with_self(filter, def, *idx, r.source, ctx),
1850					NodeOutcome::Pass
1851				),
1852				DomainItem::Def { idx: None, def } => {
1853					matches!(eval_external_def_node(filter, def, ctx), NodeOutcome::Pass)
1854				}
1855				DomainItem::Segment { kind, name } => {
1856					matches!(eval_node_segment(filter, kind, name), NodeOutcome::Pass)
1857				}
1858			}
1859		})
1860		.count() as u32
1861}
1862
1863fn eval_number_expr_segment(expr: &NumberExpr) -> Option<f64> {
1864	match expr {
1865		NumberExpr::Literal(n) => Some(*n),
1866		NumberExpr::Projection(_)
1867		| NumberExpr::Count { .. }
1868		| NumberExpr::Aggregate { .. }
1869		| NumberExpr::Metric { .. }
1870		| NumberExpr::Entropy(_)
1871		| NumberExpr::Size(_) => None,
1872	}
1873}
1874
1875fn count_children_filtered(
1876	_d: &DefRecord,
1877	def_idx: usize,
1878	self_idx: usize,
1879	kind: &str,
1880	filter: &Node,
1881	ctx: &EvalCtx<'_, '_>,
1882) -> u32 {
1883	let Some(child_idxs) = ctx.children_by_parent.get(&def_idx) else {
1884		return 0;
1885	};
1886	let mut n = 0;
1887	for &ci in child_idxs {
1888		let cd = ctx.graph.def_at(ci);
1889		if cd.kind.as_ref() != kind.as_bytes() {
1890			continue;
1891		}
1892		if let NodeOutcome::Pass = eval_node_with_self(filter, cd, ci, self_idx, ctx) {
1893			n += 1;
1894		}
1895	}
1896	n
1897}
1898
1899fn count_children_by_shape(
1900	def_idx: usize,
1901	self_idx: usize,
1902	shape: &str,
1903	filter: Option<&Node>,
1904	ctx: &EvalCtx<'_, '_>,
1905) -> u32 {
1906	let Some(child_idxs) = ctx.children_by_parent.get(&def_idx) else {
1907		return 0;
1908	};
1909	let mut n = 0;
1910	for &ci in child_idxs {
1911		let cd = ctx.graph.def_at(ci);
1912		if !def_has_shape(cd, shape) {
1913			continue;
1914		}
1915		match filter {
1916			None => n += 1,
1917			Some(node) => {
1918				if let NodeOutcome::Pass = eval_node_with_self(node, cd, ci, self_idx, ctx) {
1919					n += 1;
1920				}
1921			}
1922		}
1923	}
1924	n
1925}
1926
1927fn def_has_shape(d: &DefRecord, shape: &str) -> bool {
1928	d.shape().is_some_and(|actual| actual.as_str() == shape)
1929}
1930
1931fn count_segments(d: &DefRecord, filter: Option<&Node>) -> u32 {
1932	let mut n = 0;
1933	for seg in d.moniker.as_view().segments() {
1934		match filter {
1935			None => n += 1,
1936			Some(node) => {
1937				if let NodeOutcome::Pass = eval_node_segment(node, seg.kind, seg.name) {
1938					n += 1;
1939				}
1940			}
1941		}
1942	}
1943	n
1944}
1945
1946fn count_out_refs(
1947	_d: &DefRecord,
1948	def_idx: usize,
1949	filter: Option<&Node>,
1950	ctx: &EvalCtx<'_, '_>,
1951) -> u32 {
1952	let Some(ref_idxs) = ctx.out_refs_by_source.get(&def_idx) else {
1953		return 0;
1954	};
1955	let mut n = 0;
1956	for &ri in ref_idxs {
1957		let r = ctx.graph.ref_at(ri);
1958		match filter {
1959			None => n += 1,
1960			Some(node) => {
1961				if let NodeOutcome::Pass = eval_ref_node(node, r, ctx) {
1962					n += 1;
1963				}
1964			}
1965		}
1966	}
1967	n
1968}
1969
1970fn count_in_refs(d: &DefRecord, filter: Option<&Node>, ctx: &EvalCtx<'_, '_>) -> u32 {
1971	let key = d.moniker.as_encoded();
1972	let Some(ref_idxs) = ctx.in_refs_by_target.get(key) else {
1973		return 0;
1974	};
1975	let mut n = 0;
1976	for &ri in ref_idxs {
1977		let r = ctx.graph.ref_at(ri);
1978		match filter {
1979			None => n += 1,
1980			Some(node) => {
1981				if let NodeOutcome::Pass = eval_ref_node(node, r, ctx) {
1982					n += 1;
1983				}
1984			}
1985		}
1986	}
1987	n
1988}
1989
1990/// Quantifier evaluation in def scope.
1991fn eval_quantifier_def(
1992	kind: QuantKind,
1993	domain: &Domain,
1994	filter: &Node,
1995	scope: DefScope<'_>,
1996	self_idx: usize,
1997	ctx: &EvalCtx<'_, '_>,
1998) -> NodeOutcome {
1999	let mut total = 0u32;
2000	let mut passes = 0u32;
2001	match domain {
2002		Domain::Children(_) | Domain::ChildrenByShape(_) | Domain::Descendants(_) => {
2003			match eval_def_domain_quantifier(kind, domain, filter, scope.idx, self_idx, ctx) {
2004				Ok((domain_total, domain_passes)) => {
2005					total = domain_total;
2006					passes = domain_passes;
2007				}
2008				Err(outcome) => return *outcome,
2009			}
2010		}
2011		Domain::Pairs(inner) => {
2012			return eval_pair_quantifier(kind, inner, filter, scope.idx, self_idx, ctx);
2013		}
2014		Domain::Segments => {
2015			for seg in scope.record.moniker.as_view().segments() {
2016				total += 1;
2017				if matches!(
2018					eval_node_segment(filter, seg.kind, seg.name),
2019					NodeOutcome::Pass
2020				) {
2021					passes += 1;
2022				}
2023			}
2024		}
2025		Domain::OutRefs | Domain::SourceOutRefs | Domain::SourceAncestorOutRefs => {
2026			let empty = Vec::new();
2027			let ancestor_refs;
2028			let ref_idxs: &[usize] = if matches!(domain, Domain::SourceAncestorOutRefs) {
2029				ancestor_refs = ancestor_ref_indexes(scope.idx, ctx, true);
2030				&ancestor_refs
2031			} else {
2032				ctx.out_refs_by_source.get(&scope.idx).unwrap_or(&empty)
2033			};
2034			for &ri in ref_idxs {
2035				let r = ctx.graph.ref_at(ri);
2036				total += 1;
2037				if matches!(eval_ref_node(filter, r, ctx), NodeOutcome::Pass) {
2038					passes += 1;
2039				}
2040			}
2041		}
2042		Domain::InRefs | Domain::SourceInRefs | Domain::SourceAncestorInRefs => {
2043			let empty = Vec::new();
2044			let ancestor_refs;
2045			let ref_idxs: &[usize] = if matches!(domain, Domain::SourceAncestorInRefs) {
2046				ancestor_refs = ancestor_ref_indexes(scope.idx, ctx, false);
2047				&ancestor_refs
2048			} else {
2049				let key = scope.record.moniker.as_encoded();
2050				ctx.in_refs_by_target.get(key).unwrap_or(&empty)
2051			};
2052			for &ri in ref_idxs {
2053				let r = ctx.graph.ref_at(ri);
2054				total += 1;
2055				if matches!(eval_ref_node(filter, r, ctx), NodeOutcome::Pass) {
2056					passes += 1;
2057				}
2058			}
2059		}
2060	}
2061	let label = match kind {
2062		QuantKind::Any => "any",
2063		QuantKind::All => "all",
2064		QuantKind::None => "none",
2065	};
2066	let ok = match kind {
2067		QuantKind::Any => passes > 0,
2068		QuantKind::All => total == 0 || passes == total,
2069		QuantKind::None => passes == 0,
2070	};
2071	if ok {
2072		NodeOutcome::Pass
2073	} else {
2074		NodeOutcome::Fail(Failure {
2075			atom_raw: format!("{label}(...)"),
2076			lhs_label: label.to_string(),
2077			actual: format!("{passes}/{total}"),
2078			expected: match kind {
2079				QuantKind::Any => "≥ 1 match".to_string(),
2080				QuantKind::All => "all match".to_string(),
2081				QuantKind::None => "zero matches".to_string(),
2082			},
2083			def_idx: None,
2084			details: None,
2085		})
2086	}
2087}
2088
2089fn eval_def_domain_quantifier(
2090	kind: QuantKind,
2091	domain: &Domain,
2092	filter: &Node,
2093	def_idx: usize,
2094	self_idx: usize,
2095	ctx: &EvalCtx<'_, '_>,
2096) -> Result<(u32, u32), Box<NodeOutcome>> {
2097	let mut total = 0u32;
2098	let mut passes = 0u32;
2099	for item in domain_items(domain, def_idx, ctx) {
2100		let DomainItem::Def { idx, def } = item else {
2101			continue;
2102		};
2103		total += 1;
2104		let outcome = match idx {
2105			Some(idx) => eval_node_with_self(filter, def, idx, self_idx, ctx),
2106			None => eval_external_def_node(filter, def, ctx),
2107		};
2108		match outcome {
2109			NodeOutcome::Pass => passes += 1,
2110			NodeOutcome::Fail(mut failure) if kind == QuantKind::All => {
2111				if let Some(idx) = idx {
2112					failure.def_idx.get_or_insert(idx);
2113				}
2114				return Err(Box::new(NodeOutcome::Fail(failure)));
2115			}
2116			NodeOutcome::Fail(_) | NodeOutcome::NotApplicable => {}
2117		}
2118	}
2119	Ok((total, passes))
2120}
2121
2122fn eval_external_def_node(node: &Node, def: &DefRecord, ctx: &EvalCtx<'_, '_>) -> NodeOutcome {
2123	walk_node(
2124		node,
2125		&|atom| eval_external_def_atom(atom, def, ctx),
2126		&|_, _, _| NodeOutcome::NotApplicable,
2127		&|_| NodeOutcome::NotApplicable,
2128		&|_| NodeOutcome::NotApplicable,
2129	)
2130}
2131
2132fn eval_external_def_atom(atom: &Atom, def: &DefRecord, ctx: &EvalCtx<'_, '_>) -> AtomOutcome {
2133	let LhsExpr::Attr(lhs) = &atom.lhs else {
2134		return AtomOutcome::NotApplicable;
2135	};
2136	let Some(value) = project_def_lhs_value(None, def, *lhs, ctx) else {
2137		return AtomOutcome::NotApplicable;
2138	};
2139	if let Rhs::Projection(rhs) = &atom.rhs
2140		&& let Some(rhs_value) = project_def_lhs_value(None, def, *rhs, ctx)
2141	{
2142		return apply_op_values(&value, atom.op, &rhs_value);
2143	}
2144	apply_op(&value, atom)
2145}
2146
2147fn eval_node_segment(node: &Node, seg_kind: &[u8], seg_name: &[u8]) -> NodeOutcome {
2148	walk_node(
2149		node,
2150		&|a| eval_atom_segment(a, seg_kind, seg_name),
2151		&|_, _, _| NodeOutcome::NotApplicable,
2152		&|_| NodeOutcome::NotApplicable,
2153		&|_| NodeOutcome::NotApplicable,
2154	)
2155}
2156
2157fn eval_atom_segment(atom: &Atom, seg_kind: &[u8], seg_name: &[u8]) -> AtomOutcome {
2158	let value: Value = match &atom.lhs {
2159		LhsExpr::Attr(Lhs::SegmentKind) => Value::Str(
2160			std::str::from_utf8(seg_kind)
2161				.unwrap_or_default()
2162				.to_string(),
2163		),
2164		LhsExpr::Attr(Lhs::SegmentName) => Value::Str(
2165			std::str::from_utf8(seg_name)
2166				.unwrap_or_default()
2167				.to_string(),
2168		),
2169		_ => return AtomOutcome::NotApplicable,
2170	};
2171	if let Rhs::Projection(other) = &atom.rhs {
2172		let rhs_val = match other {
2173			Lhs::SegmentKind => Value::Str(
2174				std::str::from_utf8(seg_kind)
2175					.unwrap_or_default()
2176					.to_string(),
2177			),
2178			Lhs::SegmentName => Value::Str(
2179				std::str::from_utf8(seg_name)
2180					.unwrap_or_default()
2181					.to_string(),
2182			),
2183			_ => return AtomOutcome::NotApplicable,
2184		};
2185		return apply_op_values(&value, atom.op, &rhs_val);
2186	}
2187	if let Rhs::Number(expr) = &atom.rhs {
2188		let Some(rhs_val) = eval_number_expr_segment(expr).map(Value::Number) else {
2189			return AtomOutcome::NotApplicable;
2190		};
2191		return apply_op_values(&value, atom.op, &rhs_val);
2192	}
2193	apply_op(&value, atom)
2194}
2195
2196fn eval_atom(
2197	atom: &Atom,
2198	d: &DefRecord,
2199	def_idx: usize,
2200	self_idx: usize,
2201	ctx: &EvalCtx<'_, '_>,
2202) -> AtomOutcome {
2203	if let (LhsExpr::Collection(left), Op::Subset, Rhs::Collection(right)) =
2204		(&atom.lhs, atom.op, &atom.rhs)
2205	{
2206		if collection_has_pair_binding(left) || collection_has_pair_binding(right) {
2207			return AtomOutcome::NotApplicable;
2208		}
2209		return if eval_collection_subset(left, right, def_idx, self_idx, ctx) {
2210			AtomOutcome::Pass
2211		} else {
2212			AtomOutcome::Fail {
2213				actual: "not subset".to_string(),
2214				expected: "subset".to_string(),
2215			}
2216		};
2217	}
2218	let value: Value = match &atom.lhs {
2219		LhsExpr::Attr(lhs) => {
2220			let Some(value) = resolve_def_lhs(*lhs, d, ctx) else {
2221				return AtomOutcome::NotApplicable;
2222			};
2223			value
2224		}
2225		LhsExpr::Number(expr) => {
2226			let Some(n) = eval_number_expr_def(expr, d, def_idx, self_idx, ctx) else {
2227				return AtomOutcome::NotApplicable;
2228			};
2229			Value::Number(n)
2230		}
2231		LhsExpr::Mode(collection) => {
2232			let Some(value) = eval_mode(collection, def_idx, self_idx, ctx) else {
2233				return AtomOutcome::NotApplicable;
2234			};
2235			value
2236		}
2237		LhsExpr::PairProjection(_) => return AtomOutcome::NotApplicable,
2238		LhsExpr::SegmentOf { scope, kind } => match scope {
2239			SegmentScope::Def => Value::Str(first_segment_name(&d.moniker, kind.as_bytes())),
2240			SegmentScope::Source | SegmentScope::Target => {
2241				return AtomOutcome::NotApplicable;
2242			}
2243		},
2244		LhsExpr::Collection(_) => return AtomOutcome::NotApplicable,
2245	};
2246	if let Rhs::Projection(other) = &atom.rhs {
2247		let Some(rhs_val) = resolve_def_lhs(*other, d, ctx) else {
2248			return AtomOutcome::NotApplicable;
2249		};
2250		return apply_op_values(&value, atom.op, &rhs_val);
2251	}
2252	if let Rhs::CurrentProjection(other) = &atom.rhs {
2253		let Some(rhs_val) = resolve_def_lhs(*other, ctx.graph.def_at(self_idx), ctx) else {
2254			return AtomOutcome::NotApplicable;
2255		};
2256		return apply_op_values(&value, atom.op, &rhs_val);
2257	}
2258	if let Rhs::Number(expr) = &atom.rhs {
2259		let Some(rhs_val) =
2260			eval_number_expr_def(expr, d, def_idx, self_idx, ctx).map(Value::Number)
2261		else {
2262			return AtomOutcome::NotApplicable;
2263		};
2264		return apply_op_values(&value, atom.op, &rhs_val);
2265	}
2266	apply_op(&value, atom)
2267}
2268
2269fn children_by_parent(graph: &CodeGraph) -> HashMap<usize, Vec<usize>> {
2270	let mut m: HashMap<usize, Vec<usize>> = HashMap::new();
2271	for (idx, d) in graph.defs().enumerate() {
2272		if let Some(p) = d.parent {
2273			m.entry(p).or_default().push(idx);
2274		}
2275	}
2276	m
2277}
2278
2279fn out_refs_by_source(graph: &CodeGraph) -> HashMap<usize, Vec<usize>> {
2280	let mut m: HashMap<usize, Vec<usize>> = HashMap::new();
2281	for (idx, r) in graph.refs().enumerate() {
2282		m.entry(r.source).or_default().push(idx);
2283	}
2284	m
2285}
2286
2287fn in_refs_by_target(graph: &CodeGraph) -> HashMap<Vec<u8>, Vec<usize>> {
2288	let mut m: HashMap<Vec<u8>, Vec<usize>> = HashMap::new();
2289	for (idx, r) in graph.refs().enumerate() {
2290		m.entry(r.target.as_encoded().to_vec())
2291			.or_default()
2292			.push(idx);
2293	}
2294	m
2295}
2296
2297fn parent_counts_by_kind(graph: &CodeGraph) -> HashMap<(usize, &[u8]), u32> {
2298	let mut m: HashMap<(usize, &[u8]), u32> = HashMap::new();
2299	for d in graph.defs() {
2300		if let Some(p) = d.parent {
2301			*m.entry((p, d.kind.as_ref())).or_insert(0) += 1;
2302		}
2303	}
2304	m
2305}
2306
2307fn comment_end_bytes(graph: &CodeGraph) -> Vec<u32> {
2308	let mut v: Vec<u32> = graph
2309		.defs()
2310		.filter(|d| d.kind.as_ref() == KIND_COMMENT)
2311		.filter_map(|d| d.position.map(|(_, e)| e))
2312		.collect();
2313	v.sort_unstable();
2314	v
2315}
2316
2317/// One pass over `graph.refs()` to bucket the earliest `annotates`-ref start
2318/// per annotated def. Saves the O(D × R) per-def filter that the old
2319/// `doc_anchor_byte` performed.
2320fn doc_anchors_by_def(graph: &CodeGraph) -> HashMap<usize, u32> {
2321	let mut m: HashMap<usize, u32> = HashMap::new();
2322	for r in graph.refs() {
2323		if r.kind != b"annotates" {
2324			continue;
2325		}
2326		let Some((start, _)) = r.position else {
2327			continue;
2328		};
2329		m.entry(r.source)
2330			.and_modify(|cur| {
2331				if start < *cur {
2332					*cur = start;
2333				}
2334			})
2335			.or_insert(start);
2336	}
2337	m
2338}
2339
2340fn comment_attaches_to(source: &str, comment_end: u32, header_start: u32) -> bool {
2341	if comment_end > header_start {
2342		return false;
2343	}
2344	let last_comment_byte = comment_end.saturating_sub(1);
2345	let (cl, _) = line_range(source, last_comment_byte, last_comment_byte + 1);
2346	let (hl, _) = line_range(source, header_start, header_start + 1);
2347	hl == cl || hl == cl + 1
2348}
2349
2350fn check_require_doc_comment(
2351	target: RuleTarget<'_>,
2352	rules: &CompiledKindRules,
2353	ctx: &EvalCtx<'_, '_>,
2354	out: &mut Vec<Violation>,
2355) {
2356	check_require_doc_comment_with_id(
2357		target,
2358		rules,
2359		rule_id(ctx.lang, target.kind, "require_doc_comment"),
2360		ctx,
2361		out,
2362	);
2363}
2364
2365fn check_require_doc_comment_with_id(
2366	target: RuleTarget<'_>,
2367	rules: &CompiledKindRules,
2368	rule_id: String,
2369	ctx: &EvalCtx<'_, '_>,
2370	out: &mut Vec<Violation>,
2371) {
2372	if eval_require_doc_comment(target.scope.record, target.scope.idx, rules, ctx) != Some(false) {
2373		return;
2374	}
2375
2376	let moniker = to_uri(&target.scope.record.moniker, &ctx.uri_cfg);
2377	let name = def_name(target.scope.record).unwrap_or_default();
2378	let (start_line, end_line) = lines_of(target.scope.record, ctx.source);
2379	out.push(Violation {
2380		rule_id,
2381		severity: RuleSeverity::Error,
2382		moniker,
2383		kind: target.kind.to_string(),
2384		lines: (start_line, end_line),
2385		message: format!(
2386			"{} `{name}` is missing a doc comment immediately before it",
2387			target.kind
2388		),
2389		explanation: None,
2390	});
2391}
2392
2393fn eval_require_doc_comment(
2394	d: &DefRecord,
2395	def_idx: usize,
2396	rules: &CompiledKindRules,
2397	ctx: &EvalCtx<'_, '_>,
2398) -> Option<bool> {
2399	let filter = rules.require_doc_for_vis.as_ref()?;
2400	let vis = std::str::from_utf8(&d.visibility).unwrap_or("");
2401	if filter != "any" && filter != vis {
2402		return None;
2403	}
2404	let (def_start, _) = d.position?;
2405	let header_start = ctx
2406		.doc_anchors
2407		.get(&def_idx)
2408		.copied()
2409		.map(|anc| anc.min(def_start))
2410		.unwrap_or(def_start);
2411
2412	let idx = ctx.comment_ends.partition_point(|&end| end <= header_start);
2413	let has_doc =
2414		idx > 0 && comment_attaches_to(ctx.source, ctx.comment_ends[idx - 1], header_start);
2415	Some(has_doc)
2416}
2417
2418#[cfg(test)]
2419mod tests;