Skip to content

Class-attribute literal values are dropped; Odoo model identity/composition (_name/_inherit) unrepresentable (CLDK-004) #83

Open

Description

@rahlk

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

  1. 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.
  2. 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

Activity

  1. rahlk commented on Jul 14, 2026

    @rahlk
    ContributorAuthor

    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.

  2. rahlk commented on Jul 15, 2026

    @rahlk
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions