Skip to content

CONTAINS and DECLARES encode one relation, split by what is at the other end #340

Description

@zaebee

Came out of reading Keet's The What and How of Modelling Information and Knowledge (2023), ch. 6.2 on parthood, against our edge vocabulary. Not a bug — the graph is correct today. It is a modelling question with three practical consequences.

The observation

The two structural edges are chosen by the type of the child node, not by the nature of the relation:

# extractors/_python_functions.py:88
edge_type = EdgeType.DECLARES if node_type == NodeType.METHOD else EdgeType.CONTAINS

TypeScript does the same by hand: file → class and file → function get CONTAINS, class → method gets DECLARES (typescript_extractor.py:189,210,225).

So the label carries no information the graph does not already have — NodeType is right there on the target. And both extractors build the edge id in one namespace regardless of which label they attach:

id=f"{parent_fqn}:structural:{node_id}"

The identifier already says these are one kind of thing.

The code relies on that. Domain propagation walks them as a single transitive chain:

# resolver/uplift.py:76
3. Structural propagation — domains flow FILE → CLASS → METHOD via CONTAINS/DECLARES

So we already treat them as one relation while the vocabulary says they are two.

Why it is worth a decision rather than a shrug

Keet's chapter is about exactly this. Parthood is one primitive relation whose content lives in its declared properties, not in its name — proper parthood being irreflexive, asymmetric and transitive. Her closing point in 6.2.1 is that the low-hanging fruit for conceptual modellers is to "finally nail down that semantic variation point" of UML's aggregation association, which is the same shape as our split.

Three things follow if the relation's properties are declared:

  1. A cycle becomes a finding, not a hazard. cli.py:299 is a "cycle-safe recursive tree builder" — today a structural cycle is something the traversal survives. If the relation is asymmetric, a cycle in CONTAINS/DECLARES is a graph integrity violation and belongs in cgis_validate output beside the edge-resolution stats. Right now nothing would tell us we had one.
  2. Transitive closure becomes justified. uplift already depends on transitivity. Declared, it is a property being used; undeclared, it is an assumption that happens to hold.
  3. One label, no information lost. Anything that wants "only class members" filters on NodeType.METHOD, which is where that fact actually lives.

Options

  1. Merge into one edge type, e.g. CONTAINS, and document it as proper parthood: irreflexive, asymmetric, transitive. Callers that care about members filter by node type. Cheapest to reason about; touches every consumer of DECLARES.
  2. Keep both labels, declare them one relation. Document that DECLARES is CONTAINS narrowed to class members, give the pair its properties once, and have traversal and validation treat them as a single relation explicitly rather than by listing both everywhere.
  3. Keep as-is and write down why. Perfectly legitimate if the split earns its keep somewhere I have not found — but then the reason belongs in docs/architecture/ONTOLOGY.md, which currently describes the two edges without saying they are the same relation.

Option 2 looks like the best ratio: no migration, and the properties are what buy the cycle check and the justified closure.

The larger point, for whatever it is worth

EdgeType is a modelling language — 24 relations across three layers. Keet's ch. 7.3 lays out a procedure for designing one, whose step 3 is ontological commitments, with the note that "ontological commitments are embedded in each language, even if you thought not". This split is one such commitment made implicitly. There are probably others worth a pass — DEPENDS_ON sits beside CALLS and USES with no declared relation between them, and L3 is called "the OWL-Lite layer" while carrying no OWL semantics, no reasoner and no axioms.

Those are separate conversations. This issue is only about the structural pair.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions