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:
- 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.
- 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.
- One label, no information lost. Anything that wants "only class members" filters on
NodeType.METHOD, which is where that fact actually lives.
Options
- 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.
- 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.
- 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.
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:
TypeScript does the same by hand:
file → classandfile → functiongetCONTAINS,class → methodgetsDECLARES(typescript_extractor.py:189,210,225).So the label carries no information the graph does not already have —
NodeTypeis right there on the target. And both extractors build the edge id in one namespace regardless of which label they attach:The identifier already says these are one kind of thing.
The code relies on that. Domain propagation walks them as a single transitive chain:
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:
cli.py:299is a "cycle-safe recursive tree builder" — today a structural cycle is something the traversal survives. If the relation is asymmetric, a cycle inCONTAINS/DECLARESis a graph integrity violation and belongs incgis_validateoutput beside the edge-resolution stats. Right now nothing would tell us we had one.upliftalready depends on transitivity. Declared, it is a property being used; undeclared, it is an assumption that happens to hold.NodeType.METHOD, which is where that fact actually lives.Options
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 ofDECLARES.DECLARESisCONTAINSnarrowed 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.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
EdgeTypeis 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_ONsits besideCALLSandUSESwith 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.