diff --git a/crates/od-ontology/tests/region_digest.rs b/crates/od-ontology/tests/region_digest.rs new file mode 100644 index 0000000..e63c720 --- /dev/null +++ b/crates/od-ontology/tests/region_digest.rs @@ -0,0 +1,143 @@ +//! **Region digest (Edit 3)** — the consumer half of the region-grammar +//! knowledge transfer, closed end-to-end over REAL Odoo `account` views. +//! +//! Edit 2 (ruff `ruff_python_spo::extract_odoo_view_regions`, branch +//! `claude/odoo-region-grammar-arm`) harvested the vendored `data/nav/*.xml` +//! into `docked_at`/`tab_order`/`opens_popup` triples; the byte-frozen result +//! is carried here as `data/nav/account_regions.spo.ndjson` (corpus carriage, +//! same pattern as `account_nav.spo.ndjson`). This test folds it through the +//! `region=` convention (`odoo_regions.conf`) using ruff's OWN +//! `build_nav_digest` — the consumer REUSES the digest, never reimplements it +//! (no parallel structure). +//! +//! The load-bearing proof: **every dock token the real arm emitted over real +//! Odoo source is covered by the config** (no `unmapped:` leak) and resolves +//! to one of the six canonical regions. That is the structure-oracle render +//! half proven on real data, decoupled from the ruff merge (float on main; +//! when Edit 2 lands, the live harvest replaces the committed corpus). + +use std::collections::BTreeSet; +use std::path::{Path, PathBuf}; + +use ruff_python_spo::{extract_odoo_view_regions, region_triples}; +use ruff_spo_triplet::{build_nav_digest, from_ndjson, parse, to_ndjson}; + +/// Repo `data/` root (mirror of `klickweg_parity.rs`). +fn data_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../../data") +} + +const REGIONS_NDJSON: &str = include_str!("../../../data/nav/account_regions.spo.ndjson"); +const REGIONS_CONF: &str = include_str!("../../../data/nav/odoo_regions.conf"); + +const CANONICAL_REGIONS: &[&str] = &[ + "top_bar", + "left_nav", + "center", + "right_panel", + "bottom_bar", + "popup", +]; + +#[test] +fn live_harvest_reproduces_the_frozen_corpus() { + // The loop, closed LIVE (ruff #79 on main): run the real arm over the + // vendored `account` views TODAY and assert it reproduces the committed + // corpus byte-for-byte. The frozen ndjson is no longer a hand-authored + // stand-in — it is exactly what `extract_odoo_view_regions` emits. If the + // arm intentionally changes, regenerate the corpus (this fails loudly). + let facts = extract_odoo_view_regions(&data_root().join("nav")); + let live = to_ndjson(®ion_triples(&facts)); + assert_eq!( + live, REGIONS_NDJSON, + "the live ruff arm output must equal data/nav/account_regions.spo.ndjson" + ); +} + +#[test] +fn region_corpus_round_trips_byte_for_byte() { + // Corpus carriage drift fuse: the committed ndjson survives a closed-vocab + // from_ndjson -> to_ndjson round-trip unchanged (a hand-edit or a harvester + // regression that reshapes a triple fails here). + let triples = from_ndjson(REGIONS_NDJSON).expect("committed region corpus parses"); + assert!(!triples.is_empty(), "region corpus is non-empty"); + assert_eq!( + to_ndjson(&triples), + REGIONS_NDJSON, + "data/nav/account_regions.spo.ndjson must be byte-stable through the closed vocab" + ); +} + +#[test] +fn every_real_dock_token_is_covered_by_the_config() { + // THE proof: fold the REAL harvest through the REAL config and assert no + // token leaks. Every `docked_at` object emitted over the vendored account + // views must be a key in odoo_regions.conf — the render frame is total. + let triples = from_ndjson(REGIONS_NDJSON).expect("corpus parses"); + let cfg = parse(REGIONS_CONF); + let mapped: BTreeSet<&str> = cfg.regions.iter().map(|(t, _)| t.as_str()).collect(); + + let dock_tokens: BTreeSet = triples + .iter() + .filter(|t| t.p == "docked_at") + .map(|t| t.o.clone()) + .collect(); + assert!(!dock_tokens.is_empty(), "corpus carries docked_at facts"); + + for tok in &dock_tokens { + assert!( + mapped.contains(tok.as_str()), + "real dock token {tok:?} is NOT in odoo_regions.conf — it would render \ + `unmapped:{tok}`; add a `region={tok}:` row" + ); + } +} + +#[test] +fn every_resolved_region_is_canonical() { + let triples = from_ndjson(REGIONS_NDJSON).expect("corpus parses"); + let cfg = parse(REGIONS_CONF); + let token_to_region: std::collections::HashMap<&str, &str> = cfg + .regions + .iter() + .map(|(t, r)| (t.as_str(), r.as_str())) + .collect(); + + let regions: BTreeSet<&str> = triples + .iter() + .filter(|t| t.p == "docked_at") + .filter_map(|t| token_to_region.get(t.o.as_str()).copied()) + .collect(); + + for region in ®ions { + assert!( + CANONICAL_REGIONS.contains(region), + "real harvest resolves to non-canonical region {region:?}" + ); + } + // The real account views populate more than one region (a genuine frame, + // not everything-in-center). + assert!( + regions.len() >= 2, + "real account views should populate multiple regions, got {regions:?}" + ); +} + +#[test] +fn build_nav_digest_folds_the_regions_section() { + // Consume ruff's OWN digest builder (no reimplementation). It must produce + // a non-empty digest that carries the [regions] section for our facts. + let triples = from_ndjson(REGIONS_NDJSON).expect("corpus parses"); + let cfg = parse(REGIONS_CONF); + let digest = build_nav_digest(&triples, &cfg); + assert!(!digest.is_empty(), "digest is non-empty"); + assert!( + digest.contains("[regions]"), + "digest must carry the [regions] section (build_nav_digest folds docked_at)" + ); + // No token leaked as `unmapped:` (the config is total over the real harvest). + assert!( + !digest.contains("unmapped:"), + "no dock token should render `unmapped:` — the config covers the real harvest" + ); +} diff --git a/crates/od-ontology/tests/region_grammar.rs b/crates/od-ontology/tests/region_grammar.rs new file mode 100644 index 0000000..b99ca8a --- /dev/null +++ b/crates/od-ontology/tests/region_grammar.rs @@ -0,0 +1,124 @@ +//! **Region-grammar config pin** — proof-by-execution for the knowledge +//! transfer of ruff PR #76's region grammar into the odoo-rs transcode. +//! +//! `docs/knowledge/ODOO-REGION-GRAMMAR.md` carries #76's six-region pattern +//! (`docked_at`/`tab_order`/`opens_popup` → `{top_bar, left_nav, center, +//! right_panel, bottom_bar, popup}`) onto Odoo's XML view arch. The mapping +//! lives as data — `data/nav/odoo_regions.conf` — consumed by ruff's own +//! `region=` directive (`ruff_spo_triplet::parse`). This test proves the +//! config PARSES through that real directive and produces the pinned +//! arch-token → region map, and that every region is one of the six +//! canonical names (a malformed row can never silently mis-dock). +//! +//! This pins the convention-config half. Edit 1 (vocab) is on ruff main; +//! Edit 2 (the ruff Odoo `docked_at` harvester arm, +//! `ruff_python_spo::extract_odoo_view_regions`) is built on branch +//! `claude/odoo-region-grammar-arm`; Edit 3 (the `[regions]` digest over the +//! real harvest) is `tests/region_digest.rs`. All three now exist. + +use ruff_spo_triplet::parse; + +/// The six canonical regions (`exam_config.rs:36-37`, verbatim on ruff main). +const CANONICAL_REGIONS: &[&str] = &[ + "top_bar", + "left_nav", + "center", + "right_panel", + "bottom_bar", + "popup", +]; + +/// The committed Odoo arch-token → region convention config. +const ODOO_REGIONS_CONF: &str = include_str!("../../../data/nav/odoo_regions.conf"); + +/// The full pinned map (drift fuse — a hand-edit to the conf that changes a +/// mapping or adds/drops a token fails here loudly). +const EXPECTED: &[(&str, &str)] = &[ + // top_bar — top action/filter strip + ("header", "top_bar"), + ("search", "top_bar"), + ("filter", "top_bar"), + // left_nav — left category rail + ("searchpanel", "left_nav"), + // center — main body / primary data view + ("sheet", "center"), + ("form", "center"), + ("group", "center"), + ("field", "center"), + ("separator", "center"), + ("list", "center"), + ("tree", "center"), + ("kanban", "center"), + ("notebook", "center"), + ("page", "center"), + ("pivot", "center"), + ("graph", "center"), + ("calendar", "center"), + ("activity", "center"), + ("gantt", "center"), + ("root", "center"), + // right_panel — chatter / activity + ("chatter", "right_panel"), + // bottom_bar — wizard dialog action bar + ("footer", "bottom_bar"), + // popup — dropdown / cog / act_window target="new" + ("action_menu", "popup"), +]; + +#[test] +fn odoo_regions_conf_parses_through_the_ruff_region_directive() { + let cfg = parse(ODOO_REGIONS_CONF); + + // Every pinned (token -> region) pair is present, exactly once, verbatim. + for (tok, region) in EXPECTED { + let hits: Vec<&String> = cfg + .regions + .iter() + .filter(|(t, _)| t == tok) + .map(|(_, r)| r) + .collect(); + assert_eq!( + hits.len(), + 1, + "dock token {tok:?} must map exactly once (found {})", + hits.len() + ); + assert_eq!( + hits[0], region, + "dock token {tok:?} maps to the wrong region" + ); + } + + // No extra rows leaked in (comments/blanks dropped, malformed rows dropped + // by the `:`-split — so the parsed count is exactly the pinned count). + assert_eq!( + cfg.regions.len(), + EXPECTED.len(), + "odoo_regions.conf has {} region rows; pin expects {}", + cfg.regions.len(), + EXPECTED.len() + ); +} + +#[test] +fn every_mapped_region_is_one_of_the_six_canonical() { + let cfg = parse(ODOO_REGIONS_CONF); + for (tok, region) in &cfg.regions { + assert!( + CANONICAL_REGIONS.contains(®ion.as_str()), + "token {tok:?} docks to non-canonical region {region:?} (allowed: {CANONICAL_REGIONS:?})" + ); + } +} + +#[test] +fn all_six_canonical_regions_are_covered_by_at_least_one_token() { + let cfg = parse(ODOO_REGIONS_CONF); + for region in CANONICAL_REGIONS { + assert!( + cfg.regions.iter().any(|(_, r)| r == region), + "no Odoo arch token maps to canonical region {region:?} — the six-region \ + frame must be fully reachable from Odoo arch" + ); + } +} diff --git a/data/nav/account_regions.spo.ndjson b/data/nav/account_regions.spo.ndjson new file mode 100644 index 0000000..84ef59b --- /dev/null +++ b/data/nav/account_regions.spo.ndjson @@ -0,0 +1,103 @@ +{"s":"account_account_views.xml#view_account_account_kanban.name","p":"docked_at","o":"kanban","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_account_kanban.name","p":"tab_order","o":"0","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_account_kanban.code","p":"docked_at","o":"kanban","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_account_kanban.code","p":"tab_order","o":"1","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_account_kanban.account_type","p":"docked_at","o":"kanban","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_account_kanban.account_type","p":"tab_order","o":"2","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.action_open_related_taxes","p":"docked_at","o":"sheet","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.action_open_related_taxes","p":"tab_order","o":"0","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.related_taxes_amount","p":"docked_at","o":"sheet","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.related_taxes_amount","p":"tab_order","o":"1","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.account.action_move_line_select","p":"docked_at","o":"sheet","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.account.action_move_line_select","p":"tab_order","o":"2","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.account.action_move_line_select","p":"opens_popup","o":"account.action_move_line_select","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.current_balance","p":"docked_at","o":"sheet","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.current_balance","p":"tab_order","o":"3","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.name","p":"docked_at","o":"sheet","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.name","p":"tab_order","o":"4","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.placeholder_code","p":"docked_at","o":"sheet","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.placeholder_code","p":"tab_order","o":"5","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.code","p":"docked_at","o":"sheet","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.code","p":"tab_order","o":"6","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.account_type","p":"docked_at","o":"group","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.account_type","p":"tab_order","o":"7","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.tax_ids","p":"docked_at","o":"group","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.tax_ids","p":"tab_order","o":"8","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.tag_ids","p":"docked_at","o":"group","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.tag_ids","p":"tab_order","o":"9","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.internal_group","p":"docked_at","o":"group","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.internal_group","p":"tab_order","o":"10","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.currency_id","p":"docked_at","o":"group","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.currency_id","p":"tab_order","o":"11","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.active","p":"docked_at","o":"group","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.active","p":"tab_order","o":"12","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.group_id","p":"docked_at","o":"group","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.group_id","p":"tab_order","o":"13","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.company_ids","p":"docked_at","o":"group","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.company_ids","p":"tab_order","o":"14","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.description","p":"docked_at","o":"page","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.description","p":"tab_order","o":"15","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.code_mapping_ids","p":"docked_at","o":"page","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_form.code_mapping_ids","p":"tab_order","o":"16","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.placeholder_code","p":"docked_at","o":"list","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.placeholder_code","p":"tab_order","o":"0","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.code","p":"docked_at","o":"list","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.code","p":"tab_order","o":"1","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.name","p":"docked_at","o":"list","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.name","p":"tab_order","o":"2","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.account_type","p":"docked_at","o":"list","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.account_type","p":"tab_order","o":"3","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.group_id","p":"docked_at","o":"list","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.group_id","p":"tab_order","o":"4","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.internal_group","p":"docked_at","o":"list","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.internal_group","p":"tab_order","o":"5","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.reconcile","p":"docked_at","o":"list","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.reconcile","p":"tab_order","o":"6","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.active","p":"docked_at","o":"list","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.active","p":"tab_order","o":"7","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.non_trade","p":"docked_at","o":"list","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.non_trade","p":"tab_order","o":"8","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.tax_ids","p":"docked_at","o":"list","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.tax_ids","p":"tab_order","o":"9","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.tag_ids","p":"docked_at","o":"list","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.tag_ids","p":"tab_order","o":"10","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.currency_id","p":"docked_at","o":"list","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.currency_id","p":"tab_order","o":"11","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.company_ids","p":"docked_at","o":"list","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_list.company_ids","p":"tab_order","o":"12","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_search.name","p":"docked_at","o":"search","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_search.name","p":"tab_order","o":"0","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_search.account_type","p":"docked_at","o":"search","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_search.account_type","p":"tab_order","o":"1","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_search.root_id","p":"docked_at","o":"searchpanel","f":0.95,"c":0.9} +{"s":"account_account_views.xml#view_account_search.root_id","p":"tab_order","o":"2","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_filter_inherit_account.auto_account_id","p":"docked_at","o":"root","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_filter_inherit_account.auto_account_id","p":"tab_order","o":"0","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_filter_inherit_account.account_id","p":"docked_at","o":"root","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_filter_inherit_account.account_id","p":"tab_order","o":"1","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_filter_inherit_account.product_id","p":"docked_at","o":"root","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_filter_inherit_account.product_id","p":"tab_order","o":"2","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_filter_inherit_account.general_account_id","p":"docked_at","o":"root","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_filter_inherit_account.general_account_id","p":"tab_order","o":"3","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_filter_inherit_account.partner_id","p":"docked_at","o":"root","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_filter_inherit_account.partner_id","p":"tab_order","o":"4","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_form_inherit_account.ref","p":"docked_at","o":"root","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_form_inherit_account.ref","p":"tab_order","o":"0","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_form_inherit_account.partner_id","p":"docked_at","o":"root","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_form_inherit_account.partner_id","p":"tab_order","o":"1","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_form_inherit_account.product_id","p":"docked_at","o":"root","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_form_inherit_account.product_id","p":"tab_order","o":"2","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_form_inherit_account.move_line_id","p":"docked_at","o":"group","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_form_inherit_account.move_line_id","p":"tab_order","o":"3","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_form_inherit_account.general_account_id","p":"docked_at","o":"group","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_form_inherit_account.general_account_id","p":"tab_order","o":"4","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_pivot.account_id","p":"docked_at","o":"root","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_pivot.account_id","p":"tab_order","o":"0","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_tree_inherit_account.ref","p":"docked_at","o":"root","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_tree_inherit_account.ref","p":"tab_order","o":"0","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_tree_inherit_account.general_account_id","p":"docked_at","o":"root","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_tree_inherit_account.general_account_id","p":"tab_order","o":"1","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_tree_inherit_account.move_line_id","p":"docked_at","o":"root","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_tree_inherit_account.move_line_id","p":"tab_order","o":"2","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_tree_inherit_account.product_id","p":"docked_at","o":"root","f":0.95,"c":0.9} +{"s":"account_analytic_line_views.xml#view_account_analytic_line_tree_inherit_account.product_id","p":"tab_order","o":"3","f":0.95,"c":0.9} diff --git a/data/nav/odoo_regions.conf b/data/nav/odoo_regions.conf new file mode 100644 index 0000000..aabcb0d --- /dev/null +++ b/data/nav/odoo_regions.conf @@ -0,0 +1,47 @@ +# Odoo arch-token -> six-region map (furnace convention config, sanctioned +# output #2). Knowledge transfer of ruff PR #76's region grammar; see +# docs/knowledge/ODOO-REGION-GRAMMAR.md for the grounding and the three-edit +# recipe. Consumed by ruff exam_config's `region=` directive +# (ruff_spo_triplet/src/exam_config.rs). Format: region=:. +# The six canonical regions: top_bar / left_nav / center / right_panel / +# bottom_bar / popup. Region names are free strings FROM config — edit here, +# never in Rust. A row with no `:` is dropped (malformed can't mis-dock). + +# top_bar — the top action/filter strip (DockStyle.Top analogue) +region=header:top_bar +region=search:top_bar +region=filter:top_bar + +# left_nav — the left category/filter rail (DockStyle.Left) +region=searchpanel:left_nav + +# center — the main record body / primary data view (DockStyle.Fill) +region=sheet:center +region=form:center +region=group:center +region=field:center +region=separator:center +region=list:center +region=tree:center +region=kanban:center +region=notebook:center +region=page:center +# content view roots (pivot/graph/calendar/activity/gantt) — also center +region=pivot:center +region=graph:center +region=calendar:center +region=activity:center +region=gantt:center +# root — the extension-view ( patch) fallback: a control with no +# container ancestor, whose true region needs the inherit_id join. Safe +# default = the main content region until that join lands. +region=root:center + +# right_panel — the messaging/activity side panel (DockStyle.Right) +region=chatter:right_panel + +# bottom_bar — wizard dialog action bar (DockStyle.Bottom) +region=footer:bottom_bar + +# popup — dropdown / cog menu / act_window target="new" wizard +region=action_menu:popup diff --git a/docs/knowledge/ODOO-REGION-GRAMMAR.md b/docs/knowledge/ODOO-REGION-GRAMMAR.md new file mode 100644 index 0000000..e5aeee0 --- /dev/null +++ b/docs/knowledge/ODOO-REGION-GRAMMAR.md @@ -0,0 +1,226 @@ +# Odoo region grammar — the render half of the structure oracle + +> **Knowledge transfer of ruff PR #76** (region-grammar plane, merged to +> `ruff` `main` @ `a0eb4988` / tip `48f2374`) **into the odoo-rs transcode.** +> PR #76 built the pattern for C# WinForms (`DockStyle`/`TabIndex`/ +> `ContextMenuStrip`); the furnace playbook §8.1 says *"Odoo XML arch tags +> (`form`/`tree`/`kanban`) map to regions exactly like `DockStyle`."* This doc +> makes that mapping concrete and grounded, reusing the **same closed-vocab +> predicates** — no new predicate, no hand-rolled layout. +> +> **READ BY:** any odoo-rs session touching view rendering, the Klickweg +> render skin, or the six-region frame. Companion to +> `docs/knowledge/RUFF-SPO-SURFACE.md` (the predicate census) and the +> `klickweg_parity.rs` structure-oracle tests. +> **Cross-ref:** ruff `.claude/knowledge/consumer-transcode-furnace-playbook.md` +> §6 (the render side + render equation) and §8.1 (odoo portability row); +> ruff `crates/ruff_spo_triplet/src/{exam_config,nav_digest}.rs` (the +> `region=` directive + `[regions]` digest section). +> **Status:** APPLIED + LIVE — all three edits merged. Edit 1 (vocab) on ruff +> main; Edit 2 (`ruff_python_spo::extract_odoo_view_regions`) **merged to ruff +> main via PR #79** (the merge promoted `RegionFact`/`region_triples` into the +> shared `ruff_spo_triplet` crate — one type for Odoo/Rails/WinForms — and +> canonicalized the subject to `{screen}.{control}` so it round-trips through +> the one `build_nav_digest`); Edit 3 in +> `crates/od-ontology/tests/region_digest.rs` now runs the arm LIVE over the +> real `account` views and asserts it reproduces the frozen corpus +> (`data/nav/account_regions.spo.ndjson`, 51 facts) byte-for-byte. See §4. + +______________________________________________________________________ + +## 0. The pattern in one sentence (what #76 established) + +**Every legacy screen is a declarative layout in disguise; harvest *where a +control docks* as a fact, then re-render it into ONE universal six-region +frame — never emulate the widget tree.** Three closed-vocab predicates carry +it, and a `region=` config (data, not Rust) names the mapping: + +``` +{ top_bar, left_nav, center, right_panel, bottom_bar, popup } +``` + +**Proof-of-read (ruff main `48f2374`):** +- `crates/ruff_spo_triplet/src/exam_config.rs:36-37` — the six canonical + region names, verbatim: `top_bar / left_nav / right_panel / bottom_bar / + center / popup`. +- `crates/ruff_spo_triplet/src/triple.rs:1159` — + `fn predicate_count_locked_at_76()` (was 73; #76 added the three below). +- `crates/ruff_spo_triplet/src/nav_digest.rs:165,172,179` — the digest folds + `docked_at` → region, `tab_order` → intra-region order, `opens_popup` → + `→popup` suffix; unmapped dock tokens land in `unmapped:` (never + dropped). + +The convention (playbook §6): `predicate → (region, order, interaction)`: + +| Predicate (wire) | `Predicate` variant | Carries | WinForms source (#76) | **Odoo source (this doc)** | +|---|---|---|---|---| +| `docked_at` | `DockedAt` | **region** | `Dock = DockStyle.X` | **XML arch element** (see §2) | +| `tab_order` | `TabOrder` | **order within region** | `TabIndex` literal | **DOM position** in the arch | +| `opens_popup` | `OpensPopup` | **popup interaction** | `ContextMenuStrip = m` | **`act_window target="new"`** / dropdown (see §3) | + +______________________________________________________________________ + +## 1. Why odoo-rs already has half of this + +The Klickweg arc (`HANDOVER-2026-07-08-polyglot-transpiler.md` §2) closed the +**structure oracle** — but only its *value/field-set* half: + +- `ruff_python_spo::odoo_views.rs` harvests **which fields** a view projects + (`` inside `arch`) → `WideFieldMask` (the "does the Rust + show the same fields?" question). +- `crates/od-ontology/src/view_mask.rs` is the local hop-aware refinement. + +That answers *what is on the screen*. Region grammar answers the orthogonal +*where each thing sits* — the **render** half of the same structure oracle +(playbook §2 "structure parity" + §6 "the render side"). Same harvest source +(the view `arch`), same closed vocab, disjoint question. Together they are the +diverse-redundant structure witness; neither replaces the value oracle +(PostgreSQL rows). + +______________________________________________________________________ + +## 2. The Odoo-arch → six-region map (grounded in the vendored `account` views) + +Dock tokens are the **Odoo arch element name** (or a `class=`-qualified +variant); the region is assigned by config (§5), never hardcoded in Rust. +Grounded in the real arch tags present in `data/nav/*.xml` +(`account_account_views.xml` + `account_analytic_line_views.xml`): `form`, +`sheet`, `group`, `field`, `notebook`, `page`, `list`, `kanban`, `search`, +`filter`, `searchpanel`, `button`, `separator`, `div`. + +| Odoo arch element | dock token | → region | Rationale | +|---|---|---|---| +| `
` (form action bar + ``) | `header` | **top_bar** | top workflow-button + status strip — the `DockStyle.Top` analogue | +| `` root, `` | `search` | **top_bar** | the filter/search bar above a list/kanban | +| `` | `searchpanel` | **left_nav** | Odoo's left category-filter rail (`DockStyle.Left`) | +| ``, `
` body, ``, ``, `` | `sheet` | **center** | the main record body (`DockStyle.Fill`) | +| ``/``, `` | `list` / `kanban` | **center** | the primary data view | +| `` / `` | `notebook` | **center** | tabbed sub-sections *within* center (order = page index) | +| chatter (`
`, `` in 17+) | `chatter` | **right_panel** | messaging/activity side panel (`DockStyle.Right`) | +| `