You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Class-attribute literal values are dropped; Odoo model identity/composition (_name/_inherit) unrepresentable (CLDK-004) #83
Is your feature request related to a problem? Please describe.
Target: canonical schema v2 (epic #69), with the model-composition layer routed through design first — it is new contract surface. From the external v0.3.0/Odoo quality audit, finding CLDK-004 (severity Critical).
Describe the solution you'd like
Not stated in the original issue.
Describe alternatives you've considered
Not stated in the original issue.
Additional context
Audit evidence (Odoo, native export)
1,235_name and 2,663_inherit class attributes exist as :PyAttribute nodes — with zero captured literal values (no value/initializer property).
Zero model-composition relationship types exist in the schema (nothing matching ODOO/MODEL/INHERIT).
So _name = 'account.account' is present as a node named _name whose defining string is unrecoverable from the graph, and Odoo's registry-based model composition (multiple classes contributing to one model via _inherit) has no representation at all.
Root cause — two independent gaps
Class-attribute values are dropped at capture.PyClassAttribute (codeanalyzer/schema/py_schema.py:293-300) has no value/initializer field, and _class_attributes (symbol_table_builder.py:356-432) extracts only the name and a Jedi-inferred type, never the assignment's RHS. This is an asymmetry: PyVariableDeclaration (module/local variables) does keep initializer.
No framework-semantics layer exists. The analyzer is fully framework-agnostic; the only inheritance edge is PY_EXTENDS over lexical base classes, which cannot express registry composition declared through _inherit strings.
Fix direction
v2 schema: give class attributes their initializer (general fix, benefits every consumer — the audit found the missing values via _name/_inherit but the drop is universal).
Status + design sketch for the remaining half of this issue
Landed (v1 batch, feat/audit-remediation-v1): class-attribute initializer capture. _name = 'account.account' / _inherit = ['mail.thread'] are now recoverable from both analysis.json (PyClassAttribute.initializer) and the graph (PyAttribute.initializer), which satisfies the audit's identity gate: a consumer can query every model identity and every _inherit contribution as literals.
Still open (this issue stays open for it): the model-composition layer — making registry dispatch traversable without consumers re-implementing Odoo semantics in Cypher.
Proposed overlay design (for a future designing-cldk-changes round)
Opt-in framework pass, e.g. canpy --framework odoo, running after the symbol table is built. The core analyzer stays framework-agnostic; overlays are additive passes that only read captured facts (the attribute initializers).
Derivation, literal-only: evaluate _name / _inherit / _inherits initializers when they are string or list-of-string literals (ast.literal_eval on the captured text). Dynamic values stay underived — recorded with an unresolved_reason rather than guessed.
Proposed graph shape (additive, MINOR bump when built):
:PyOdooModel {name} — one identity node per registry model name.
PY_DEFINES_MODEL (:PyClass → :PyOdooModel) from _name.
PY_EXTENDS_MODEL (:PyClass → :PyOdooModel) from each _inherit entry.
Model composition = all contributor classes of a model, reachable in one hop; env['account.account'].search(...) dispatch = contributors of account.account filtered by a search method (PY_HAS_METHOD), MRO ordering left to the consumer or a later pass.
JSON mirror: a top-level frameworks.odoo.models map (model name → {defining_classes, extending_classes}), so both projections stay in lockstep.
Non-goals for the first cut:_inherits delegation semantics, _auto/abstract flags, addon manifest awareness, cross-addon override ordering. Each can layer on later without changing the two edge types.
The audit acceptance gate this closes: "Every literal model identity and contributor class is represented, including multi-class model composition used by registry dispatch." Related: the callee-binding fix (#80/#89) already stopped env[...] calls from wrongly binding to the caller's class — with this overlay they become correctly resolvable instead of unresolved.
Progress: part 1 (initializer capture) shipped in v1.0.0 — PyClassAttribute.initializer captures the assignment RHS (landed via the #95 audit batch and carried into the v2 tree in the v0.3.1→v2 reconciliation merge; test_class_attribute_initializers_are_captured runs on the v2 suite). _name = 'account.account' is now recoverable from both projections (PyAttribute.initializer in Neo4j).
Part 2 — the model identity/composition layer (registry-based _inherit composition, env['model'] dispatch) — remains open and is design-rung scope (designing-cldk-changes): it is new cross-language contract surface, per this issue's own routing note. Keeping this issue open to track it.
Activity
rahlk commentedon Jul 14, 2026
Status + design sketch for the remaining half of this issue
Landed (v1 batch,
feat/audit-remediation-v1): class-attribute initializer capture._name = 'account.account'/_inherit = ['mail.thread']are now recoverable from bothanalysis.json(PyClassAttribute.initializer) and the graph (PyAttribute.initializer), which satisfies the audit's identity gate: a consumer can query every model identity and every_inheritcontribution as literals.Still open (this issue stays open for it): the model-composition layer — making registry dispatch traversable without consumers re-implementing Odoo semantics in Cypher.
Proposed overlay design (for a future
designing-cldk-changesround)canpy --framework odoo, running after the symbol table is built. The core analyzer stays framework-agnostic; overlays are additive passes that only read captured facts (the attribute initializers)._name/_inherit/_inheritsinitializers when they are string or list-of-string literals (ast.literal_evalon the captured text). Dynamic values stay underived — recorded with anunresolved_reasonrather than guessed.:PyOdooModel {name}— one identity node per registry model name.PY_DEFINES_MODEL(:PyClass → :PyOdooModel) from_name.PY_EXTENDS_MODEL(:PyClass → :PyOdooModel) from each_inheritentry.env['account.account'].search(...)dispatch = contributors ofaccount.accountfiltered by asearchmethod (PY_HAS_METHOD), MRO ordering left to the consumer or a later pass.frameworks.odoo.modelsmap (model name → {defining_classes, extending_classes}), so both projections stay in lockstep._inheritsdelegation semantics,_auto/abstract flags, addon manifest awareness, cross-addon override ordering. Each can layer on later without changing the two edge types.The audit acceptance gate this closes: "Every literal model identity and contributor class is represented, including multi-class model composition used by registry dispatch." Related: the callee-binding fix (#80/#89) already stopped
env[...]calls from wrongly binding to the caller's class — with this overlay they become correctly resolvable instead of unresolved.rahlk commentedon Jul 15, 2026
Progress: part 1 (initializer capture) shipped in v1.0.0 —
PyClassAttribute.initializercaptures the assignment RHS (landed via the #95 audit batch and carried into the v2 tree in the v0.3.1→v2 reconciliation merge;test_class_attribute_initializers_are_capturedruns on the v2 suite)._name = 'account.account'is now recoverable from both projections (PyAttribute.initializerin Neo4j).Part 2 — the model identity/composition layer (registry-based
_inheritcomposition,env['model']dispatch) — remains open and is design-rung scope (designing-cldk-changes): it is new cross-language contract surface, per this issue's own routing note. Keeping this issue open to track it.