@@ -64,18 +64,36 @@ const PERDURANT_SUFFIXES: &[&str] = &[
6464] ;
6565
6666/// Name suffixes indicating a Quality (attribute / classification / rate).
67+ ///
68+ /// NOTE on `.groups` (plural): the canonical Odoo class is `res.groups`,
69+ /// which ends in `.groups` (plural), NOT `.group`. The bare `.group` rule
70+ /// alone misses `res.groups` despite the comment claiming to match it.
71+ /// Both forms are listed so plural-form Odoo classes are caught. Surfaced
72+ /// 2026-06-23 by cross-validation against `od_ontology::alignment` (see
73+ /// `.claude/board/EPIPHANIES.md § E-DOLCE-ODOO-SILENT-SUFFIX-DRIFT`).
6774const QUALITY_SUFFIXES : & [ & str ] = & [
6875 ".tag" , // crm.tag, account.account.tag
6976 ".category" , // product.category, res.partner.category
7077 ".type" , // account.account.type, sale.order.type
71- ".group" , // res.groups, account.tax.group
78+ ".groups" , // res.groups (the canonical Odoo class — PLURAL)
79+ ".group" , // account.tax.group, *.group (singular variants)
7280 ".tax" , // account.tax (it's a rate, a quality, not an event)
7381] ;
7482
7583/// Name suffixes indicating an AbstractEntity (reference / config / template).
84+ ///
85+ /// NOTE on `.settings`: the canonical Odoo settings shape is
86+ /// `*.config.settings` (e.g. `res.config.settings`, `sale.config.settings`),
87+ /// which ends in `.settings`, NOT `.config`. The bare `.config` rule alone
88+ /// misses `*.config.settings` despite the comment claiming to match it.
89+ /// Both forms are listed so settings models are caught regardless of the
90+ /// trailing segment. Surfaced 2026-06-23 by cross-validation against
91+ /// `od_ontology::alignment` (see
92+ /// `.claude/board/EPIPHANIES.md § E-DOLCE-ODOO-SILENT-SUFFIX-DRIFT`).
7693const ABSTRACT_SUFFIXES : & [ & str ] = & [
7794 ".template" , // mail.template, account.chart.template
78- ".config" , // *.config.settings
95+ ".settings" , // *.config.settings (res.config.settings, sale.config.settings)
96+ ".config" , // *.config (catches direct *.config models)
7997 ".policy" , // any *.policy
8098 ".rule" , // account.reconcile.model rules
8199 ".formula" , // hr.payroll.structure.line formulas
@@ -160,4 +178,55 @@ mod tests {
160178 DolceCategory :: Endurant
161179 ) ;
162180 }
181+
182+ // ─── Regression: silent suffix-rule drift caught by cross-validation ───
183+ //
184+ // The two assertions below would fail BEFORE the 2026-06-23 fix in this
185+ // module that added `.groups` (plural) to QUALITY_SUFFIXES and `.settings`
186+ // to ABSTRACT_SUFFIXES. The previous module-doc comments claimed to match
187+ // `res.groups` (via `.group`) and `*.config.settings` (via `.config`) but
188+ // the rules didn't actually catch those class names. See
189+ // `.claude/board/EPIPHANIES.md § E-DOLCE-ODOO-SILENT-SUFFIX-DRIFT`.
190+
191+ #[ test]
192+ fn classifies_res_groups_as_quality_via_plural_suffix ( ) {
193+ // `res.groups` (plural) is the canonical Odoo class — `res.users`'s
194+ // role/permission cohort. Must classify as Quality (a classification
195+ // dimension), not get swept into the default (Endurant) bucket.
196+ assert_eq ! ( classify_odoo( "res.groups" ) , DolceCategory :: Quality ) ;
197+ assert_eq ! ( classify_odoo( "odoo:res.groups" ) , DolceCategory :: Quality ) ;
198+ }
199+
200+ #[ test]
201+ fn classifies_config_settings_models_as_abstract_via_settings_suffix ( ) {
202+ // `*.config.settings` is the canonical Odoo settings shape — a
203+ // configuration form, an AbstractEntity (rule/policy/template). The
204+ // `.settings` suffix catches it; `.config` alone never matched it.
205+ assert_eq ! (
206+ classify_odoo( "res.config.settings" ) ,
207+ DolceCategory :: AbstractEntity
208+ ) ;
209+ assert_eq ! (
210+ classify_odoo( "sale.config.settings" ) ,
211+ DolceCategory :: AbstractEntity
212+ ) ;
213+ assert_eq ! (
214+ classify_odoo( "odoo:res.config.settings" ) ,
215+ DolceCategory :: AbstractEntity
216+ ) ;
217+ }
218+
219+ #[ test]
220+ fn singular_dot_group_still_matches_for_tax_group_style_classes ( ) {
221+ // Adding `.groups` must NOT accidentally drop the original `.group`
222+ // singular match for `*.group` / `account.tax.group` / etc.
223+ assert_eq ! ( classify_odoo( "account.tax.group" ) , DolceCategory :: Quality ) ;
224+ }
225+
226+ #[ test]
227+ fn singular_dot_config_still_matches_for_direct_config_classes ( ) {
228+ // Adding `.settings` must NOT drop the original `.config` match for
229+ // direct `*.config` models.
230+ assert_eq ! ( classify_odoo( "crm.config" ) , DolceCategory :: AbstractEntity ) ;
231+ }
163232}
0 commit comments