Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion .claude/board/AGENT_LOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,34 @@
## 2026-06-18 — PR #525 follow-up: `_inherit`-only binding scoped to `inherit[0]`

**Main thread (Opus) — single implementer**, branch `claude/odoo-spo-fk-target-deep-reads`. Addresses the one unresolved codex P2 on #525: the prior `_inherit`-only fix bound a no-`_name` class's relational fields to the WHOLE `_inherit` list, so a multi-element `_inherit = ['a','b']` would attach local fields to every inherited mixin and let `build_relation_map()` emit bogus `target`/`reads_field` triples for secondary parents.

**Fix:** `spo_enrich.py` no-`_name` case now binds to `inherit[0]` only (`inherit_models[:1]`), matching Odoo in-place extension semantics and the repo's own `parsers/classes.py` collapse. `test_inherit_list_binds_field_to_each_model` → `..._to_first_model_only` (asserts `sale_order` bound, `purchase_order` NOT). Docstrings reworded.

**Corpus impact: NONE.** Regenerated from base `1ec76f5b` (22 245) with the fixed enrich → `out=24166`, `target=842`, `inverse_name=144` — **byte-identical** to the committed corpus (`diff -q` clean). No real scanned-addons class triggers the bogus secondary-mixin binding scoped to a corpus-declared model, so the Rust count assertions (24 166 / 842 / 144 / 3 030) are unchanged; no `odoo_ontology.rs` edit. The fix is defensive tooling-correctness.

**Tests:** `python3 -m unittest tests.test_spo_enrich` 20/20 green.

## 2026-06-17 — PR #523 review fixes: spo_enrich multi-emitter + `_inherit`-only

**Main thread (Opus) — single implementer**, branch `claude/odoo-spo-fk-target-deep-reads` (review-fix commit on top of the enrichment commit, no rebase). Addresses 4 valid review findings (codex P1 + codex P2/CodeRabbit Major + 2 CodeRabbit doc nits). Scope: the lance-graph SPO corpus + the stdlib Python extractor tooling under `tools/odoo-blueprint-extractor/` (no odoo-rs change).

**Fixed:**
- **Fix 1 (codex P1, real bug):** `spo_enrich.py` deep-read lift kept only the LAST `emitted_by` method per field (`field_emitter` dict). A field with multiple emitters (confirmed `stock_move.quantity` ← `_compute_quantity` AND `_onchange_product_uom_qty`) lifted the deep `reads_field` onto one only. Changed to a per-field sorted emitter LIST (`field_emitters`); the deep read is now emitted for EACH emitter (self-loop drop preserved per-emitter, determinism kept).
- **Fix 2 (codex P2 / CodeRabbit Major):** `_scan_file` bound relational fields only when `_name` was present, dropping the `_inherit`-only extension form (`_inherit = "account.move"` / `["a","b"]` with no `_name`). Now resolves `model_names` from `_name` if present ELSE from `_inherit` (string→1, list/tuple→each, via new `_const_str_list`), mapping `local_fields` onto every resolved model — mirroring the package class parser's `_inherit` handling.
- **Fix 3 (CodeRabbit doc nit):** AGENT_LOG scope reworded from "lance-graph only" to "lance-graph corpus + the stdlib Python extractor tooling" (the diff includes `spo_enrich.py` + tests).
- **Fix 4 (CodeRabbit doc nit):** the EPIPHANIES "27 compute edges / no-cycle verified locally" claim used a discarded worktree. Re-anchored to a persistent in-repo fixture test (`tools/odoo-blueprint-extractor/tests/test_spo_enrich.py` `TestMultiEmitterDeepReads` + `TestInheritOnlyRelationMap`).

**Corpus regenerated from base (22 245):** 22 245 → **24 166** triples. `target` 618→**842** (+224, `_inherit`-only extension fields), `inverse_name` 102→**144** (+42), deep `reads_field` 736→**935** (+199, per-emitter lift); `reads_field` total 2 095→**3 030**. Unknown-hop skips 567→337 (more hops now resolve via `_inherit` targets). `rdf:type`/`depends_on`/`emitted_by`/`has_function` unchanged.

**Rust:** `odoo_ontology.rs` module-doc + count test (24 166), histogram (`target`=842, `inverse_name`=144, `reads_field`=3 030) updated; 2 new in-corpus tests (`enrichment_lifts_deep_reads_onto_every_emitter` on `stock_move.quantity`; `enrichment_honors_inherit_only_extension_fields` on `account_move.authorized_transaction_ids` → `payment.transaction`).

**Tests:** extractor `python3 -m unittest discover` 20/20 green (14 prior + 6 new). lance-graph `cargo test -p lance-graph --lib odoo_ontology` green (counts updated). `cargo fmt` clean; `cargo clippy -p lance-graph -- -D warnings` clean on the touched crate (pre-existing causal-edge/p64-bridge/planner `-D warnings` debt untouched). See `EPIPHANIES.md` E-ODOO-FK-DEEP-READS (Status updated with the review-fix totals).

---

## 2026-06-17 — odoo SPO corpus enrichment: P1 FK-target + P0 deep-reads_field (UPSTREAM_WISHLIST)

**Main thread (Opus) — single implementer**, branch `claude/odoo-spo-fk-target-deep-reads`. Implements the odoo-rs `UPSTREAM_WISHLIST` P1 (FK `target`/`inverse_name`) + coupled P0 (deep `reads_field`) corpus enrichment, lance-graph only.
**Main thread (Opus) — single implementer**, branch `claude/odoo-spo-fk-target-deep-reads`. Implements the odoo-rs `UPSTREAM_WISHLIST` P1 (FK `target`/`inverse_name`) + coupled P0 (deep `reads_field`) corpus enrichment. Scope: the lance-graph SPO corpus + the stdlib Python extractor tooling under `tools/odoo-blueprint-extractor/` (no odoo-rs change). **Review-fix follow-up 2026-06-17 (PR #523, see entry above):** totals below (618/102/736 → 24 166) were corrected by the review pass; see the prepended entry for the new figures.

**Shipped:**
- `tools/odoo-blueprint-extractor/odoo_blueprint_extractor/spo_enrich.py` (new, stdlib-only): builds a `(model, field) → (comodel, inverse)` relation map from `/home/user/odoo/addons` via `ast`, then (P1) emits `target`/`inverse_name` sibling triples keyed by the relation IRI (ruff#18 shape, raw dotted comodel object) for every relational field on a corpus-declared model, and (P0) resolves each dotted `@api.depends` path through the map and lifts a deep `reads_field` onto the field's emitting method. Additive, deterministic, idempotent (`(s,p,o)` dedup); self-loops dropped; unknown-hop paths skipped + counted. CLI: `python3 -m odoo_blueprint_extractor.spo_enrich --corpus … --addons …`.
Expand Down
82 changes: 73 additions & 9 deletions crates/lance-graph/src/graph/spo/odoo_ontology.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,15 @@
//!
//! Data file `odoo_ontology.spo.ndjson` carries the base extraction (388
//! Object Types, 3 107 Properties, 3 328 Functions) plus the `spo_enrich`
//! P1/P0 layer (618 `target` + 102 `inverse_name` + 736 deep `reads_field`),
//! for 23 701 triples total. The base extraction's original generator
//! P1/P0 layer (842 `target` + 144 `inverse_name` + 935 deep `reads_field`),
//! for 24 166 triples total. The `target`/`inverse_name`/deep-read totals grew
//! over the initial enrichment (618/102/736) once `spo_enrich` (a) honored
//! `_inherit`-only extension classes — `_inherit = "account.move"` with no
//! `_name`, the common Odoo extension form whose relational fields were
//! previously dropped — and (b) lifted each deep `reads_field` onto EVERY
//! emitter of the dependent field, not just the last (a field such as
//! `stock_move.quantity` is emitted by both `_compute_quantity` and
//! `_onchange_product_uom_qty`). The base extraction's original generator
//! (`emit_ontology2.py` over a `methods.parquet`) is not present in this
//! tree — only its output is — so the enrichment is applied over the shipped
//! corpus + the Odoo source via
Expand Down Expand Up @@ -146,9 +153,9 @@ mod tests {
#[test]
fn parses_all_triples() {
let triples = parse_triples(ONTOLOGY);
// 22 245 base triples + 1 456 spo_enrich triples (618 target +
// 102 inverse_name + 736 deep reads_field) = 23 701.
assert_eq!(triples.len(), 23_701, "triple count drifted from data file");
// 22 245 base triples + 1 921 spo_enrich triples (842 target +
// 144 inverse_name + 935 deep reads_field) = 24 166.
assert_eq!(triples.len(), 24_166, "triple count drifted from data file");
}

#[test]
Expand Down Expand Up @@ -176,10 +183,13 @@ mod tests {
assert_eq!(hist.get("emitted_by"), Some(&3228));
assert_eq!(hist.get("rdf:type"), Some(&6823));
// spo_enrich P1/P0 layer: FK target/inverse_name + deep reads_field.
assert_eq!(hist.get("target"), Some(&618));
assert_eq!(hist.get("inverse_name"), Some(&102));
// reads_field grew from 2 095 (base) to 2 831 with 736 deep lifts.
assert_eq!(hist.get("reads_field"), Some(&2831));
// Totals grew from 618/102/736 once `_inherit`-only extension classes
// were honored (more `target`/`inverse_name`) and deep reads were
// lifted onto EVERY emitter of a field (more deep `reads_field`).
assert_eq!(hist.get("target"), Some(&842));
assert_eq!(hist.get("inverse_name"), Some(&144));
// reads_field grew from 2 095 (base) to 3 030 with 935 deep lifts.
assert_eq!(hist.get("reads_field"), Some(&3030));
assert_eq!(hist.get("other"), None, "unexpected predicate kind");
}

Expand Down Expand Up @@ -369,4 +379,58 @@ mod tests {
"function count drifted from module-doc claim (3 328)"
);
}

/// `spo_enrich` P0 multi-emitter — a field emitted by MORE than one method
/// must have its deep `reads_field` lifted onto EVERY emitter, not just the
/// last. `stock_move.quantity` is emitted by both `_compute_quantity` AND
/// `_onchange_product_uom_qty`; both must carry the cross-model deep reads
/// (`stock_move_line.quantity` / `stock_move_line.product_uom_id`). Before
/// the multi-emitter fix the deep read landed on only one of them, dropping
/// the recompute-ordering edge for the other (typically the `_compute_*`).
#[test]
fn enrichment_lifts_deep_reads_onto_every_emitter() {
let triples = parse_triples(ONTOLOGY);
let deep_on = |method: &str, obj: &str| {
triples
.iter()
.any(|t| t.s == method && t.p == "reads_field" && t.o == obj)
};
// Both emitters of stock_move.quantity carry the cross-model deep read.
assert!(
deep_on(
"odoo:stock_move._compute_quantity",
"odoo:stock_move_line.quantity"
),
"P0 multi-emitter: _compute_quantity must read stock_move_line.quantity"
);
assert!(
deep_on(
"odoo:stock_move._onchange_product_uom_qty",
"odoo:stock_move_line.quantity"
),
"P0 multi-emitter: _onchange_product_uom_qty must also read \
stock_move_line.quantity (every emitter, not just the last)"
);
}

/// `spo_enrich` P1 `_inherit`-only — relational fields declared on an
/// extension class (`_inherit = "account.move"` with no `_name`) must still
/// get their `target`. `account_move.authorized_transaction_ids` is declared
/// only on the `account_payment` extension of `account.move`; before the
/// `_inherit` fix the class was skipped (no `_name`) and the field never got
/// a target.
#[test]
fn enrichment_honors_inherit_only_extension_fields() {
let triples = parse_triples(ONTOLOGY);
let target = triples.iter().any(|t| {
t.s == "odoo:account_move.authorized_transaction_ids"
&& t.p == "target"
&& t.o == "payment.transaction"
});
assert!(
target,
"P1 _inherit-only: authorized_transaction_ids (declared on an \
_inherit='account.move' extension) must resolve to payment.transaction"
);
}
}
Loading
Loading