Skip to content

Commit 7a74a6f

Browse files
committed
feat(odoo_blueprint::extracted::pairing): D-ODOO-EXT-5 — curated-vs-extracted pairing table
Builds the cross-reference table that links every L-doc curated lane entity (`l{1..15}::*`, `OdooConfidence::Curated`) to its source-extracted counterpart (`extracted::*::EXT_*`, `OdooConfidence::Extracted`) when both exist for the same `model_name`. Outputs: - `crates/lance-graph-ontology/src/odoo_blueprint/extracted/pairing.rs` exposes `pub static CURATED_EXTRACTED_PAIRS: &[OdooEntityPairing]` — 48 pairings (53 curated model_names × 229 extracted in TIER-1). - `/tmp/pairings.json` (out-of-tree) records field/method count deltas per pairing for human review. Scanner: stdlib `re` over the generated Rust source; lives in `tools/odoo-blueprint-extractor/odoo_blueprint_extractor/pairing.py` with a `pair` CLI subcommand (`python -m odoo_blueprint_extractor pair`). Const selection rules (both sides): - Curated: highest field+method count wins (handles indirect-ref files like l3.rs that reference separate const slices); alphabetical const_name on tie. This correctly picks l9.rs over l3.rs for `account.fiscal.position` (17+11 vs 0+0 counted inline fields/methods). - Extracted: most fields+methods wins (picks richest coverage when a model appears in multiple addons, e.g. `uom.uom` in uom.rs preferred over account.rs/product.rs/stock.rs appearances). Side-effect: promoted 17 module-private (`const`) lane entities to `pub const` in l3.rs, l5.rs, l7.rs, l13.rs so they're accessible from `crate::odoo_blueprint::<lane>::<CONST>` paths. These consts were only reachable via the `ENTITIES` slice before; making them pub is additive (no tests change) and required for the pairing reference. Most striking deltas (curated savant-relevant subset vs full ORM): account.move: 24f/27m → 142f/352m (+118f/+325m) account.move.line: 20f/14m → 87f/146m (+67f/+132m) sale.order: 22f/13m → 65f/141m (+43f/+128m) stock.move: 23f/14m → 74f/130m (+51f/+116m) account.tax: 3f/0m → 36f/113m (+33f/+113m) 5 curated model_names have NO TIER-1 extracted backing (EXT-6 gap rows): hr.contract.type, hr.department, hr.employee, hr.job, stock.valuation.layer These are TIER-2 addons (hr, account_asset) — expected leakage. Curated stays canonical on conflict (per BP-1 plan §"merge ordering"); extracted is the audit backing. Tests: 2 new (`pairing_table_is_well_formed`, `pairing_table_has_expected_size`); all 199 pre-existing ontology tests stay green (201 total). Next: D-ODOO-EXT-6 (coverage report — uses this pairing table to quantify lane-level extracted-backing). Plan: `.claude/plans/odoo-source-extraction-v1.md`. https://claude.ai/code/session_017gZ6sPRXYPj5n7uJ7NBtRv
1 parent 1e0f477 commit 7a74a6f

8 files changed

Lines changed: 888 additions & 19 deletions

File tree

crates/lance-graph-ontology/src/odoo_blueprint/extracted/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ pub mod l10n_de;
3333
// orthogonal.
3434
pub mod l10n_de_chart;
3535
pub mod l10n_de_kennzahlen;
36+
37+
// Curated-vs-extracted reconciliation (D-ODOO-EXT-5)
38+
pub mod pairing;

crates/lance-graph-ontology/src/odoo_blueprint/extracted/pairing.rs

Lines changed: 410 additions & 0 deletions
Large diffs are not rendered by default.

crates/lance-graph-ontology/src/odoo_blueprint/l13.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ const STOCK_VALUATION_LAYER: OdooEntity = OdooEntity {
183183

184184
// ─── stock.warehouse.orderpoint ───────────────────────────────────────────────
185185

186-
const STOCK_WAREHOUSE_ORDERPOINT: OdooEntity = OdooEntity {
186+
pub const STOCK_WAREHOUSE_ORDERPOINT: OdooEntity = OdooEntity {
187187
model_name: "stock.warehouse.orderpoint",
188188
kind: OdooEntityKind::Model,
189189
description: "Min/max reorder rule for one product at one location; drives scheduler batch \
@@ -413,7 +413,7 @@ const STOCK_WAREHOUSE_ORDERPOINT: OdooEntity = OdooEntity {
413413

414414
// ─── stock.rule (procurement-priority extension) ──────────────────────────────
415415

416-
const STOCK_RULE: OdooEntity = OdooEntity {
416+
pub const STOCK_RULE: OdooEntity = OdooEntity {
417417
model_name: "stock.rule",
418418
kind: OdooEntityKind::Model,
419419
description: "Procurement rule mapping (dest, route) → action (pull/push/transparent); \
@@ -599,7 +599,7 @@ const STOCK_RULE: OdooEntity = OdooEntity {
599599

600600
// ─── stock.lot ────────────────────────────────────────────────────────────────
601601

602-
const STOCK_LOT: OdooEntity = OdooEntity {
602+
pub const STOCK_LOT: OdooEntity = OdooEntity {
603603
model_name: "stock.lot",
604604
kind: OdooEntityKind::Model,
605605
description: "Lot/serial number master; uniqueness per (product_id, company_id, name) \

crates/lance-graph-ontology/src/odoo_blueprint/l3.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ const TAX_GROUP_DECORATORS: &[OdooDecorator] = &[OdooDecorator {
108108
targets: &["company_id"],
109109
}];
110110

111-
const ACCOUNT_TAX_GROUP: OdooEntity = OdooEntity {
111+
pub const ACCOUNT_TAX_GROUP: OdooEntity = OdooEntity {
112112
model_name: "account.tax.group",
113113
kind: OdooEntityKind::Model,
114114
description: "Groups taxes for display + closing-entry accounts (USt/VSt/advance); \
@@ -464,7 +464,7 @@ const ACCOUNT_TAX_CONSTRAINTS: &[OdooConstraint] = &[
464464
},
465465
];
466466

467-
const ACCOUNT_TAX: OdooEntity = OdooEntity {
467+
pub const ACCOUNT_TAX: OdooEntity = OdooEntity {
468468
model_name: "account.tax",
469469
kind: OdooEntityKind::Model,
470470
description: "VAT / USt tax definition with computation type (percent/fixed/division/group), \
@@ -623,7 +623,7 @@ const REPARTITION_LINE_CONSTRAINTS: &[OdooConstraint] = &[OdooConstraint {
623623
source_method: Some("_validate_repartition_lines"),
624624
}];
625625

626-
const ACCOUNT_TAX_REPARTITION_LINE: OdooEntity = OdooEntity {
626+
pub const ACCOUNT_TAX_REPARTITION_LINE: OdooEntity = OdooEntity {
627627
model_name: "account.tax.repartition.line",
628628
kind: OdooEntityKind::Model,
629629
description: "Distribution rule mapping a tax computation result to a GL account and \
@@ -894,7 +894,7 @@ const FISCAL_POSITION_CONSTRAINTS: &[OdooConstraint] = &[
894894
},
895895
];
896896

897-
const ACCOUNT_FISCAL_POSITION: OdooEntity = OdooEntity {
897+
pub const ACCOUNT_FISCAL_POSITION: OdooEntity = OdooEntity {
898898
model_name: "account.fiscal.position",
899899
kind: OdooEntityKind::Model,
900900
description: "Tax regime mapping rule: translates taxes and GL accounts for a partner. \

crates/lance-graph-ontology/src/odoo_blueprint/l5.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ const PAYMENT_STATE_MACHINE: OdooStateMachine = OdooStateMachine {
8484
],
8585
};
8686

87-
const PAYMENT: OdooEntity = OdooEntity {
87+
pub const PAYMENT: OdooEntity = OdooEntity {
8888
model_name: "account.payment",
8989
kind: OdooEntityKind::Model,
9090
description: "A posted payment event generating double-entry journal lines; \
@@ -389,7 +389,7 @@ const PAYMENT: OdooEntity = OdooEntity {
389389

390390
// ─── account.payment.term ────────────────────────────────────────────────────
391391

392-
const PAYMENT_TERM: OdooEntity = OdooEntity {
392+
pub const PAYMENT_TERM: OdooEntity = OdooEntity {
393393
model_name: "account.payment.term",
394394
kind: OdooEntityKind::Model,
395395
description: "Structured payment obligation terms (installments, Skonto/early-discount, \
@@ -550,7 +550,7 @@ const PAYMENT_TERM: OdooEntity = OdooEntity {
550550

551551
// ─── account.payment.term.line ───────────────────────────────────────────────
552552

553-
const PAYMENT_TERM_LINE: OdooEntity = OdooEntity {
553+
pub const PAYMENT_TERM_LINE: OdooEntity = OdooEntity {
554554
model_name: "account.payment.term.line",
555555
kind: OdooEntityKind::Model,
556556
description: "One installment line within a payment term; computes due date via \
@@ -683,7 +683,7 @@ const PAYMENT_TERM_LINE: OdooEntity = OdooEntity {
683683
// full projection from L5-PAY-TERMS-MATCH.md RULE P5. When L2 is populated,
684684
// move primary coverage there and leave a cross-reference comment here.
685685

686-
const RECONCILE_MODEL: OdooEntity = OdooEntity {
686+
pub const RECONCILE_MODEL: OdooEntity = OdooEntity {
687687
model_name: "account.reconcile.model",
688688
kind: OdooEntityKind::Model,
689689
description: "Declarative rule for bank-statement-to-open-item matching \
@@ -892,7 +892,7 @@ const RECONCILE_MODEL: OdooEntity = OdooEntity {
892892
//
893893
// NOTE: L2 overlap — same as parent model above.
894894

895-
const RECONCILE_MODEL_LINE: OdooEntity = OdooEntity {
895+
pub const RECONCILE_MODEL_LINE: OdooEntity = OdooEntity {
896896
model_name: "account.reconcile.model.line",
897897
kind: OdooEntityKind::Model,
898898
description: "Write-off journal line template within a reconcile model; \

crates/lance-graph-ontology/src/odoo_blueprint/l7.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const STOCK_MOVE_STATE_MACHINE: OdooStateMachine = OdooStateMachine {
5353

5454
// ─── stock.move ───────────────────────────────────────────────────────────────
5555

56-
const STOCK_MOVE: OdooEntity = OdooEntity {
56+
pub const STOCK_MOVE: OdooEntity = OdooEntity {
5757
model_name: "stock.move",
5858
kind: OdooEntityKind::Model,
5959
description: "One product movement between two stock locations; state machine \
@@ -129,7 +129,7 @@ const STOCK_MOVE: OdooEntity = OdooEntity {
129129

130130
// ─── stock.move.line ──────────────────────────────────────────────────────────
131131

132-
const STOCK_MOVE_LINE: OdooEntity = OdooEntity {
132+
pub const STOCK_MOVE_LINE: OdooEntity = OdooEntity {
133133
model_name: "stock.move.line",
134134
kind: OdooEntityKind::Model,
135135
description: "One lot/package/owner reservation or done-qty record within a stock move; \
@@ -170,7 +170,7 @@ const STOCK_MOVE_LINE: OdooEntity = OdooEntity {
170170

171171
// ─── stock.quant ──────────────────────────────────────────────────────────────
172172

173-
const STOCK_QUANT: OdooEntity = OdooEntity {
173+
pub const STOCK_QUANT: OdooEntity = OdooEntity {
174174
model_name: "stock.quant",
175175
kind: OdooEntityKind::Model,
176176
description: "Persistent stock record: qty of one product at one location with \
@@ -270,7 +270,7 @@ const STOCK_PICKING_STATE_MACHINE: OdooStateMachine = OdooStateMachine {
270270

271271
// ─── stock.picking ────────────────────────────────────────────────────────────
272272

273-
const STOCK_PICKING: OdooEntity = OdooEntity {
273+
pub const STOCK_PICKING: OdooEntity = OdooEntity {
274274
model_name: "stock.picking",
275275
kind: OdooEntityKind::Model,
276276
description: "Group of stock moves for one logistics operation (receipt/delivery/internal); \
@@ -352,7 +352,7 @@ const STOCK_PICKING: OdooEntity = OdooEntity {
352352

353353
// ─── stock.location ───────────────────────────────────────────────────────────
354354

355-
const STOCK_LOCATION: OdooEntity = OdooEntity {
355+
pub const STOCK_LOCATION: OdooEntity = OdooEntity {
356356
model_name: "stock.location",
357357
kind: OdooEntityKind::Model,
358358
description: "Node in the stock location hierarchy (physical or virtual); \
@@ -418,7 +418,7 @@ const STOCK_LOCATION: OdooEntity = OdooEntity {
418418

419419
// ─── stock.warehouse ──────────────────────────────────────────────────────────
420420

421-
const STOCK_WAREHOUSE: OdooEntity = OdooEntity {
421+
pub const STOCK_WAREHOUSE: OdooEntity = OdooEntity {
422422
model_name: "stock.warehouse",
423423
kind: OdooEntityKind::Model,
424424
description: "Physical warehouse site with operational config (picking types, routes, \

tools/odoo-blueprint-extractor/odoo_blueprint_extractor/cli.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
--addon l10n_de \\
1313
--out crates/lance-graph-ontology/src/odoo_blueprint/extracted/
1414
[--addons /home/user/odoo/addons]
15+
16+
Usage (curated-vs-extracted pairing — D-ODOO-EXT-5):
17+
python -m odoo_blueprint_extractor pair \\
18+
--crate crates/lance-graph-ontology \\
19+
--out crates/lance-graph-ontology/src/odoo_blueprint/extracted/pairing.rs \\
20+
--audit /tmp/pairings.json
1521
"""
1622

1723
import argparse
@@ -29,9 +35,33 @@ def build_parser() -> argparse.ArgumentParser:
2935
prog="python -m odoo_blueprint_extractor",
3036
description="Extract Odoo ORM classes as OdooEntity Rust consts (D-ODOO-EXT-1).",
3137
)
32-
# Subparsers — 'data' is new in EXT-4; original args are now the default (no subcommand)
38+
# Subparsers — 'data' is EXT-4; 'pair' is EXT-5; original args are the default (no subcommand)
3339
subparsers = p.add_subparsers(dest="subcommand")
3440

41+
# ---- 'pair' subcommand (EXT-5) ----
42+
pair_p = subparsers.add_parser(
43+
"pair",
44+
help="Build curated-vs-extracted pairing table (D-ODOO-EXT-5).",
45+
)
46+
pair_p.add_argument(
47+
"--crate",
48+
required=True,
49+
metavar="DIR",
50+
help="Path to the lance-graph-ontology crate root (e.g. crates/lance-graph-ontology).",
51+
)
52+
pair_p.add_argument(
53+
"--out",
54+
required=True,
55+
metavar="PATH",
56+
help="Output path for the generated pairing.rs file.",
57+
)
58+
pair_p.add_argument(
59+
"--audit",
60+
metavar="JSON",
61+
default=None,
62+
help="Write mismatch audit (field/method count deltas) to this JSON file.",
63+
)
64+
3565
# ---- 'data' subcommand (EXT-4) ----
3666
data_p = subparsers.add_parser(
3767
"data",
@@ -81,6 +111,45 @@ def build_parser() -> argparse.ArgumentParser:
81111
return p
82112

83113

114+
def _run_pair_subcommand(args: argparse.Namespace) -> None:
115+
"""Run the 'pair' subcommand — curated-vs-extracted pairing table (EXT-5)."""
116+
from .pairing import build_pairings, emit_audit_json, emit_pairing_rs, scan_blueprint_dir
117+
118+
crate_dir = Path(args.crate)
119+
blueprint_dir = crate_dir / "src" / "odoo_blueprint"
120+
if not blueprint_dir.is_dir():
121+
sys.exit(f"ERROR: odoo_blueprint dir not found: {blueprint_dir}")
122+
123+
out_path = Path(args.out)
124+
audit_path: Optional[str] = args.audit
125+
126+
# Scan
127+
scan = scan_blueprint_dir(blueprint_dir)
128+
pairings = build_pairings(scan)
129+
130+
n = len(pairings)
131+
curated_count = len(scan["curated"])
132+
extracted_count = len(scan["extracted"])
133+
print(
134+
f"# Pairing: curated={curated_count} model_names, "
135+
f"extracted={extracted_count} model_names, "
136+
f"overlap={n} pairings",
137+
file=sys.stderr,
138+
)
139+
140+
# Emit Rust
141+
rust_src = emit_pairing_rs(pairings)
142+
out_path.parent.mkdir(parents=True, exist_ok=True)
143+
out_path.write_text(rust_src, encoding="utf-8")
144+
print(f"Written: {out_path}", file=sys.stderr)
145+
146+
# Emit audit JSON
147+
if audit_path:
148+
audit_json = emit_audit_json(pairings)
149+
Path(audit_path).write_text(audit_json, encoding="utf-8")
150+
print(f"Audit written: {audit_path}", file=sys.stderr)
151+
152+
84153
def _run_data_subcommand(args: argparse.Namespace) -> None:
85154
"""Run the 'data' subcommand — CSV/XML extraction for l10n_de (EXT-4)."""
86155
from .data_extractors.csv_chart import emit_chart_rs
@@ -126,6 +195,11 @@ def main() -> None:
126195
parser = build_parser()
127196
args = parser.parse_args()
128197

198+
# Route to 'pair' subcommand if requested
199+
if args.subcommand == "pair":
200+
_run_pair_subcommand(args)
201+
return
202+
129203
# Route to 'data' subcommand if requested
130204
if args.subcommand == "data":
131205
_run_data_subcommand(args)

0 commit comments

Comments
 (0)