Skip to content

Commit e6ab630

Browse files
committed
ogar-vocab/recipe: fix Odoo relation labels to lowercased relation_kind tokens (Bugbot)
Cursor Bugbot (OGAR #157): RECIPE_LABELS registered Python relation surfaces as Many2one/One2many/Many2many (the capitalized source constructor), but ruff lowercases the relation_kind token at harvest (ruff_python_spo::walk kind.to_lowercase(); ruff_spo_triplet ir.rs Field::relation_kind; ogar_from_ruff::odoo_relation_kind all match lowercase). So recipe_concept_from_surface(RecipeLang::Python) would have missed the harvested strings and the advertised Ruby<->Odoo relation convergence would silently break once the resolver is wired. Fix: register the lowercase tokens (many2one/one2many/many2many) that ruff actually emits, with a comment citing the source of truth; update the doctest, the convergence test, and the doc prose to match. Verified in code before fixing. 10 tests + 3 doctests green, clippy + fmt clean, -p ogar-vocab scoped. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqGtdddWRV8BHwhrtuaWLN
1 parent 180e591 commit e6ab630

1 file changed

Lines changed: 21 additions & 12 deletions

File tree

crates/ogar-vocab/src/recipe.rs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
//! *zoo*. [`recipe_concept_from_surface`] is the **lift-time resolver**
2121
//! that maps a surface string to the shared canonical id, keeping the
2222
//! string as the per-language label skin. Rails `belongs_to` and Odoo
23-
//! `Many2one` resolve to the **same** [`recipe_ids::REL_MANY_TO_ONE`] —
24-
//! the verb-side twin of `WorkPackage ≡ Issue ≡ 0x0102`.
23+
//! `many2one` (the lowercased `relation_kind` token ruff emits) resolve to
24+
//! the **same** [`recipe_ids::REL_MANY_TO_ONE`] — the verb-side twin of
25+
//! `WorkPackage ≡ Issue ≡ 0x0102`.
2526
//!
2627
//! Per RAILS-COVERAGE-KIT §5 the recipe vocabulary MUST converge the same
2728
//! way class concepts do, or the behavioural arm fragments back into the
@@ -37,7 +38,7 @@
3738
//! |---|---|---|
3839
//! | `0x01` | [`RecipeFamily::Lifecycle`] | `before_save` / `after_commit` ‖ `@api.constrains` / `@depends` |
3940
//! | `0x02` | [`RecipeFamily::Guard`] | `validates presence:/uniqueness:` ‖ `_check_*` |
40-
//! | `0x03` | [`RecipeFamily::Relation`] | `belongs_to`/`has_many` ‖ `Many2one`/`One2many` |
41+
//! | `0x03` | [`RecipeFamily::Relation`] | `belongs_to`/`has_many` ‖ `many2one`/`one2many` |
4142
//! | `0x04` | [`RecipeFamily::Action`] | controller `HandlerKind` ‖ `action_*` |
4243
//!
4344
//! Ids are stable forever: they only ARRIVE (append a new low byte / a new
@@ -276,7 +277,7 @@ pub const RECIPE_CODEBOOK: &[(&str, RecipeConceptId)] = &[
276277
/// Per-language **label DTO** table — `(id, lang, surface)`. The surface
277278
/// string is render-only and per-language; the id is the truth. Multiple
278279
/// surfaces (across languages) map to one id — that IS the cross-consumer
279-
/// convergence (Rails `belongs_to` ≡ Odoo `Many2one` → `rel_many_to_one`).
280+
/// convergence (Rails `belongs_to` ≡ Odoo `many2one` → `rel_many_to_one`).
280281
///
281282
/// Append only; a new surface for an existing concept adds a row, mints
282283
/// nothing (`RAILS-COVERAGE-KIT` §5 rule 2). Fuzzy Odoo body-pattern
@@ -347,13 +348,18 @@ const RECIPE_LABELS: &[(RecipeConceptId, RecipeLang, &str)] = &[
347348
),
348349
(recipe_ids::REL_ONE_TO_ONE, RecipeLang::Ruby, "has_one"),
349350
(recipe_ids::REL_THROUGH, RecipeLang::Ruby, "through"),
350-
// Relation — Python/Odoo field kinds (the convergence pins).
351-
(recipe_ids::REL_MANY_TO_ONE, RecipeLang::Python, "Many2one"),
352-
(recipe_ids::REL_ONE_TO_MANY, RecipeLang::Python, "One2many"),
351+
// Relation — Python/Odoo field kinds (the convergence pins). The
352+
// surface is the LOWERCASED `relation_kind` token ruff emits (Odoo's
353+
// `fields.Many2one(...)` constructor lowercased at harvest —
354+
// `ruff_python_spo::walk` `kind.to_lowercase()`, `ruff_spo_triplet`
355+
// `ir.rs` Field::relation_kind, `ogar_from_ruff::odoo_relation_kind`),
356+
// NOT the capitalized source constructor. Match the emitted token.
357+
(recipe_ids::REL_MANY_TO_ONE, RecipeLang::Python, "many2one"),
358+
(recipe_ids::REL_ONE_TO_MANY, RecipeLang::Python, "one2many"),
353359
(
354360
recipe_ids::REL_MANY_TO_MANY,
355361
RecipeLang::Python,
356-
"Many2many",
362+
"many2many",
357363
),
358364
// Action — controller HandlerKind surfaces (framework-neutral ids;
359365
// tagged Ruby as the harvest frontend that produces them today).
@@ -426,7 +432,7 @@ pub fn recipe_concept_name(id: RecipeConceptId) -> Option<&'static str> {
426432
/// // resolve to ONE canonical concept.
427433
/// assert_eq!(
428434
/// recipe_concept_from_surface("belongs_to", RecipeLang::Ruby),
429-
/// recipe_concept_from_surface("Many2one", RecipeLang::Python),
435+
/// recipe_concept_from_surface("many2one", RecipeLang::Python),
430436
/// );
431437
/// assert_eq!(
432438
/// recipe_concept_from_surface("belongs_to", RecipeLang::Ruby),
@@ -568,12 +574,15 @@ mod tests {
568574
/// relation surfaces resolve to ONE canonical recipe concept.
569575
#[test]
570576
fn relation_verbs_converge_across_ruby_and_python() {
577+
// The Python surface is the lowercased `relation_kind` token ruff
578+
// emits (`ruff_python_spo` lowercases the `fields.Many2one(...)`
579+
// constructor at harvest), NOT the capitalized source name.
571580
let pairs = [
572-
("belongs_to", "Many2one", recipe_ids::REL_MANY_TO_ONE),
573-
("has_many", "One2many", recipe_ids::REL_ONE_TO_MANY),
581+
("belongs_to", "many2one", recipe_ids::REL_MANY_TO_ONE),
582+
("has_many", "one2many", recipe_ids::REL_ONE_TO_MANY),
574583
(
575584
"has_and_belongs_to_many",
576-
"Many2many",
585+
"many2many",
577586
recipe_ids::REL_MANY_TO_MANY,
578587
),
579588
];

0 commit comments

Comments
 (0)