1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
//! Named thresholds and weights (AGENTS.md §20, §29: "重みやthresholdをコード内に
//! 散在させず、versioned rulesetに集約する" / "magic numberを散在させない").
//!
//! Every number a component or the aggregator uses to make a decision lives
//! here, named, with a one-line rationale. Bump [`RULESET_VERSION`] whenever
//! any constant below changes — it's recorded in every report's
//! `Provenance`.
pub const RULESET_VERSION: &str = "0.7.0";
// ---------------------------------------------------------------------------
// Applicability
// ---------------------------------------------------------------------------
/// Curated organic-chemistry element subset. AGENTS.md §28 explicitly rules
/// out full periodic-table coverage as a v0.1 goal; anything outside this
/// set is treated as out-of-domain rather than silently scored.
pub const SUPPORTED_ELEMENTS: & = &;
/// Confidence multiplier applied when `validate_valence` reports any
/// violation. A soft penalty, not a hard `OutOfDomain` trigger — valence
/// heuristics can flag legitimate-but-unusual structures.
pub const CONFIDENCE_PENALTY_UNUSUAL_VALENCE: f64 = 0.5;
/// Confidence multiplier applied when at least one stereocenter is
/// unspecified. Soft penalty — incomplete stereo is common in real input
/// and not itself evidence the molecule is out of domain.
pub const CONFIDENCE_PENALTY_STEREO_INCOMPLETE: f64 = 0.85;
/// Confidence multiplier applied when stereo analysis could not be run at
/// all (currently: any negatively charged atom — see
/// `components::has_negatively_charged_atom`'s doc for why). Mutually
/// exclusive with [`CONFIDENCE_PENALTY_STEREO_INCOMPLETE`] per molecule
/// (either the real check ran, or it didn't), and deliberately a stronger
/// penalty than that one: "zero stereo information because the check
/// itself couldn't run" is a bigger gap than "checked, and some centers
/// happen to be unspecified." Not as severe as
/// [`CONFIDENCE_PENALTY_UNUSUAL_VALENCE`]'s territory either — a charged
/// atom isn't itself a structural irregularity, it's a tooling limitation
/// unrelated to whether the input molecule is well-formed.
pub const CONFIDENCE_PENALTY_STEREO_UNCHECKABLE: f64 = 0.6;
// ---------------------------------------------------------------------------
// Ring topology burden
// ---------------------------------------------------------------------------
/// Per-family weight for an isolated (non-fused) ring.
pub const RING_WEIGHT_SIMPLE: f64 = 0.15;
/// Base weight for a fused ring system, before the density term.
pub const RING_WEIGHT_FUSED_BASE: f64 = 0.35;
/// Multiplier on fusion density (shared-atom overlap fraction) for fused
/// systems — denser fusion (more shared atoms per ring) burdens more.
pub const RING_WEIGHT_FUSED_DENSITY: f64 = 0.5;
pub const RING_WEIGHT_SPIRO: f64 = 0.3;
pub const RING_WEIGHT_BRIDGED: f64 = 0.6;
/// Added on top of the kind-based weight when any ring in the family is at
/// least [`MACROCYCLE_MIN_RING_SIZE`] atoms.
pub const RING_WEIGHT_MACROCYCLE_BONUS: f64 = 0.25;
/// Ring size at/above which a ring counts as a macrocycle. Chosen to match
/// the threshold `chematic-3d`'s internal (non-public) macrocycle
/// classifier uses, for consistency even though that constant isn't
/// reachable from the `perception`/`chem` features yomitoki depends on.
pub const MACROCYCLE_MIN_RING_SIZE: usize = 9;
/// Fusion density above which a `RingFusedDense` finding is emitted (in
/// addition to the family already contributing fused burden).
pub const RING_FUSED_DENSITY_FINDING_THRESHOLD: f64 = 0.5;
/// Scale in the `normalized = 1 - exp(-raw / scale)` burden transform
/// (AGENTS.md §5.1: burden should be non-linear, not a simple capped sum).
pub const RING_BURDEN_SCALE: f64 = 1.5;
// ---------------------------------------------------------------------------
// Size / topology burden
// ---------------------------------------------------------------------------
/// Burden per dalton of molecular weight. Weight, not heavy-atom count, is
/// used as the size signal — the two are highly correlated, and MW carries
/// a more recognizable reference point (see
/// [`SIZE_LARGE_MOLECULAR_WEIGHT_THRESHOLD`]) than an arbitrary atom-count
/// cutoff would.
pub const SIZE_WEIGHT_PER_MOLECULAR_WEIGHT_UNIT: f64 = 0.0006;
/// Burden per acyclic rotatable bond. AGENTS.md §5.1 lists rotatable bond
/// count as a candidate structural-burden indicator directly; the
/// rationale used here is "more independent single-bond connections
/// roughly tracks more bond-forming steps," not a flexibility/ADME claim
/// (Veber's rule, which uses the same threshold, is about oral
/// bioavailability, a different question — the number is reused, the
/// rationale is not).
///
/// Known weak spot (documented, not hidden): this over-penalizes simple,
/// commercially available long unbranched chains, which have many
/// rotatable bonds but essentially no synthetic difficulty — exactly the
/// "structural complexity vs. actual difficulty" conflation AGENTS.md §2
/// names as a problem with existing tools. Fragment rarity (§5.4, not yet
/// implemented) is what's supposed to correct for this by recognizing such
/// fragments as common/precedented; until it exists, this component alone
/// will overstate difficulty for that specific case.
pub const SIZE_WEIGHT_PER_ROTATABLE_BOND: f64 = 0.03;
/// Scale in the `normalized = 1 - exp(-raw / scale)` burden transform
/// (AGENTS.md §5.1: burden should be non-linear).
pub const SIZE_BURDEN_SCALE: f64 = 2.0;
/// Molecular weight (daltons) above which a `SizeLargeMolecularWeight`
/// finding is emitted. 500 Da is a widely recognized "large molecule"
/// reference point (the same number appears in Lipinski's Rule of Five),
/// reused here purely as a size cutoff — not a druglikeness/permeability
/// claim, which is what that rule is actually about.
pub const SIZE_LARGE_MOLECULAR_WEIGHT_THRESHOLD: f64 = 500.0;
/// Rotatable bond count above which a `SizeHighRotatableBondCount` finding
/// is emitted. Matches the same number used in Veber's rule; see the
/// rationale note on [`SIZE_WEIGHT_PER_ROTATABLE_BOND`] for why the
/// citation is reused but the underlying claim is not.
pub const SIZE_HIGH_ROTATABLE_BOND_THRESHOLD: usize = 10;
// ---------------------------------------------------------------------------
// Stereochemical burden
// ---------------------------------------------------------------------------
/// Burden per tetrahedral stereocenter (specified or unspecified — the
/// molecule needs the same synthetic control over its configuration either
/// way; whether the input SMILES wrote it out is an input-quality/
/// confidence concern, handled separately by the applicability component,
/// not a difficulty concern).
pub const STEREO_WEIGHT_PER_CENTER: f64 = 0.12;
/// Multiplier on stereocenter density (`total_centers / heavy_atom_count`)
/// — AGENTS.md §8.3's own worked example frames the concern as "three
/// defined stereocenters within a compact eight-heavy-atom region," i.e.
/// concentration, not raw count alone: the same center count in a much
/// larger molecule leaves more room for orthogonal, staged control.
pub const STEREO_WEIGHT_DENSITY: f64 = 0.6;
/// Scale in the `normalized = 1 - exp(-raw / scale)` burden transform
/// (AGENTS.md §5.1: burden should be non-linear).
pub const STEREO_BURDEN_SCALE: f64 = 1.5;
/// Stereocenter density above which a `StereoDensityHigh` finding is
/// emitted. Chosen independently (not copied from AGENTS.md §8.2's
/// worked-example value of 0.12, which pairs with an unspecified,
/// differently-defined local density metric, not this whole-molecule
/// `centers / heavy_atom_count` ratio — reusing that number without
/// knowing what it actually measures would be false precision, not
/// consistency). 0.25 means roughly one in every four heavy atoms is a
/// stereocenter.
pub const STEREO_DENSITY_FINDING_THRESHOLD: f64 = 0.25;
// ---------------------------------------------------------------------------
// Functional-group liabilities
// ---------------------------------------------------------------------------
/// Burden per distinct triggered Brenk (2008) structural alert
/// (`chematic::chem::brenk_matches_detailed`). AGENTS.md §5.5's "reactive/
/// unstable functional groups" category, deliberately scoped to Brenk's own
/// set rather than a hand-curated one — reusing an existing, published set
/// is narrower than writing new SMARTS from scratch. Brenk also already
/// includes `strained_ring_three`/`strained_ring_four`, covering AGENTS.md
/// §5.5's "strained motifs" example for free. Dense functionalization,
/// chemoselectivity burden, and oxidation-state combinations are NOT
/// covered — chematic has no oxidation-state API (confirmed absent) and no
/// FG-density metric wired in yet; see `docs/architecture.md`.
///
/// Known weak spot (documented, not hidden — same treatment as
/// [`SIZE_WEIGHT_PER_ROTATABLE_BOND`]'s caveat): Brenk et al. 2008 was
/// validated as a med-chem screening-library *desirability* filter
/// (reactivity toward assay components, metabolic liability, promiscuity),
/// not a synthetic-difficulty signal. Several of its ~105 alerts fire on
/// common, cheaply-precedented groups — confirmed by probing real drugs:
/// aspirin (`CC(=O)Oc1ccccc1C(=O)O`) trips `phenol`, `phenolic_aldehyde`,
/// `active_ester`, and `acetal_ketal`; paracetamol trips `phenol`,
/// `aniline`, and `secondary_amine`. Neither molecule is remotely
/// difficult to make. Fragment rarity (§5.4, not yet implemented) is
/// expected to correct for this the same way it's expected to correct the
/// rotatable-bond term, by recognizing such fragments as common/
/// precedented.
pub const FG_WEIGHT_PER_REACTIVE_GROUP: f64 = 0.12;
/// Scale in the `normalized = 1 - exp(-raw / scale)` burden transform
/// (AGENTS.md §5.1: burden should be non-linear).
pub const FG_BURDEN_SCALE: f64 = 1.5;
/// Per-finding confidence when `brenk_matches_detailed` reports an alert
/// whose VF2 enumeration was cut off by the visit budget before completing
/// (empty `atom_indices` — see that function's doc: still a real flagged
/// alert, just one whose full match extent couldn't be resolved). Lower
/// than the `1.0` used for a fully-resolved match, never dropped silently
/// (AGENTS.md §4.4: abstain/flag uncertainty, don't hide it).
pub const FG_CONFIDENCE_BUDGET_EXHAUSTED: f64 = 0.5;
/// Burden per distinct functional-group cluster (`chematic::chem::
/// identify_functional_groups`, Ertl 2017) *beyond the first*. AGENTS.md
/// §5.5's "dense functionalization" example — a separate signal from Brenk
/// alerts above: this counts ordinary, non-reactive functional groups too,
/// on the theory that more independent reactive/functional regions in one
/// molecule means more competing sites to sequence and protect, regardless
/// of whether any single one is individually unstable.
///
/// The first cluster is free (weight applies to `count.saturating_sub(1)`):
/// a single connected region of functionality is the ordinary case for any
/// non-trivial organic molecule (confirmed empirically — ethanol, a bare
/// C-O environment, is `count == 1`) and isn't itself evidence of anything
/// unusual; burden only starts once a *second*, topologically disconnected
/// region exists.
///
/// Known weak spot (documented, not hidden — same treatment as
/// [`SIZE_WEIGHT_PER_ROTATABLE_BOND`]/[`FG_WEIGHT_PER_REACTIVE_GROUP`]'s
/// caveats): `identify_functional_groups` merges adjacent/fused heteroatom
/// environments into one cluster, so a single densely interconnected
/// polyfunctional system undercounts here — confirmed empirically:
/// glucose (6 hydroxyls in one ring) and penicillin V (β-lactam +
/// thioether + amide + carboxylic acid + aryl ether, all ring-fused) both
/// come back as `count == 1`, identical to ethanol. This term only catches
/// *disconnected* multi-site burden (e.g. several separate esters on one
/// scaffold), not fused polyfunctional density.
pub const FG_WEIGHT_PER_DISTINCT_GROUP: f64 = 0.08;
/// Distinct functional-group cluster count above which a
/// `FunctionalGroupDense` finding is emitted. Chosen empirically: real
/// drug-like molecules with ordinary functionality (aspirin, paracetamol)
/// both come back at `count == 2`; a threshold of 3 (finding fires at 4+)
/// sits comfortably above that baseline while still catching genuinely
/// multi-site molecules (a tetraester on a branched core comes back at
/// `count == 4`; five scattered, non-adjacent amines come back at
/// `count == 5`).
pub const FG_DENSE_GROUP_COUNT_THRESHOLD: usize = 3;
// ---------------------------------------------------------------------------
// Aggregation / verdict
// ---------------------------------------------------------------------------
/// Weighted **sum**, not a weighted average — matches AGENTS.md §20's own
/// formula (`difficulty = w_topology*topology + w_rings*ring_topology +
/// ...`), which adds each `w * normalized` term directly rather than
/// dividing by the weight total. `ProbabilityLikeScore::new` clamps the
/// result to `0.0..=1.0`, so weights don't need to sum to 1.
///
/// Ring topology's weight is 1.0 (full pass-through): a molecule with only
/// ring-topology burden and negligible size burden scores identically to
/// the single-component model this crate started with — adding a second
/// component should never silently water down a signal the first
/// component already gave full confidence in. Size topology is additive
/// on top, at a fraction of that weight, so it registers as extra burden
/// when large but can't drag a small, ring-driven molecule down into a
/// lower verdict bucket by "diluting" the average.
pub const AGGREGATE_WEIGHT_RING_TOPOLOGY: f64 = 1.0;
pub const AGGREGATE_WEIGHT_SIZE_TOPOLOGY: f64 = 0.4;
pub const AGGREGATE_WEIGHT_STEREOCHEMICAL_BURDEN: f64 = 0.5;
pub const AGGREGATE_WEIGHT_FUNCTIONAL_GROUP_LIABILITY: f64 = 0.4;
/// Below this confidence (and absent a hard applicability failure), the
/// verdict is `Indeterminate` rather than a difficulty-based bucket.
/// Strictness-dependent — see [`indeterminate_confidence_threshold`].
/// `Standard`'s value (0.45) is deliberately above the lowest confidence
/// floor applicability's soft penalties can combine to reach at Standard
/// strictness — a threshold below that floor would make `Indeterminate`
/// unreachable at that strictness level. Applicability currently has three
/// soft-penalty sources (`components/applicability.rs`); two are mutually
/// exclusive per molecule (`CONFIDENCE_PENALTY_STEREO_INCOMPLETE` and
/// `CONFIDENCE_PENALTY_STEREO_UNCHECKABLE` can't both apply — either the
/// stereo check ran or it didn't), so the actual floor is
/// `CONFIDENCE_PENALTY_UNUSUAL_VALENCE * CONFIDENCE_PENALTY_STEREO_
/// UNCHECKABLE = 0.5 * 0.6 = 0.3` (lower than the pre-`STEREO_UNCHECKABLE`
/// floor of 0.5 * 0.85 = 0.425), well below 0.45.
const INDETERMINATE_CONFIDENCE_THRESHOLD_LENIENT: f64 = 0.3;
const INDETERMINATE_CONFIDENCE_THRESHOLD_STANDARD: f64 = 0.45;
const INDETERMINATE_CONFIDENCE_THRESHOLD_STRICT: f64 = 0.6;
/// The confidence floor below which the verdict becomes `Indeterminate`,
/// for a given [`crate::config::Strictness`]. Higher strictness abstains
/// more readily (higher threshold).
pub
/// Difficulty upper bounds for the three lowest verdict buckets; anything
/// above the last bound is `HighlyChallenging`.
pub const DIFFICULTY_LIKELY_ACCESSIBLE_MAX: f64 = 0.25;
pub const DIFFICULTY_MODERATE_MAX: f64 = 0.5;
pub const DIFFICULTY_CHALLENGING_MAX: f64 = 0.75;
// ---------------------------------------------------------------------------
// Simplification suggestions
// ---------------------------------------------------------------------------
/// Confidence assigned to every v0.1 simplification suggestion (AGENTS.md
/// §9), regardless of which finding it was derived from. Deliberately flat
/// rather than per-suggestion-code: nothing in v0.1 has been calibrated
/// against real synthesis outcomes (no corpus exists — same gap as
/// `fragment_rarity`), so differentiating confidence between, say, a
/// bridged-ring suggestion and a stereocenter-density one would imply a
/// precision this crate doesn't have. `0.5` reads as "this follows from our
/// own scoring model's causality, not from validated outcomes" — high
/// enough to be worth surfacing, not so high it reads as calibrated.
pub const SUGGESTION_CONFIDENCE_HEURISTIC: f64 = 0.5;