Conceptual reference for the trait-based, multi-language code metamodel
(FamixNG lineage). Every concept is described with its attributes and its
relations to other concepts. The executable form of this document is
@codegraph/core (Zod schemas) and the generated schemas/ contract;
if they ever disagree, the code is authoritative and this file has a bug.
Concept map:
graph TB
subgraph Model["Model (one extraction run)"]
E[Entity]
ED[Edge]
end
P[Language Profile] -- "licenses kinds & trait\ncompositions, edge kinds" --> E
P -- licenses --> ED
T[Trait] -- "composes into\n(traits set)" --> E
ED -- "from / to" --> E
A[SourceAnchor] -- "evidence on" --> E
A -- "evidence on" --> ED
PR[Provenance] -- "qualifies" --> ED
CF[CodeFile] -- "definedIn / anchors" --> E
S[Stub] -- "degraded Entity\n(isStub = true)" --> E
Identity is the structured key (lang, module, symbol, disambiguator?)
(MM-1). It is compared component-wise, never by parsing a rendered string.
Uniqueness is per model union: no two entities share
(lang, module, symbol, disambiguator).
| Component | Role |
|---|---|
lang |
language id, frozen per profile (java, ts, clj, …) |
module |
the owning module's path. A module names itself here, with an empty symbol |
symbol |
the path below the module — dots for nesting; empty only for a module, or for a nameless entity at module top level, which then carries a disambiguator (a TypeScript top-level arrow: ts:src%2Fa.ts#3:15) |
disambiguator |
optional: file:line:column for anonymous entities (lambdas, impl blocks), param:/local: markers for sub-members; absent when the symbol is already unique. For Java invocables the erased-FQN parameter list is part of symbol, not of this component (§10). The COLUMN is load-bearing: one line can start several nameless entities, and without it they collapse into one id — and a nested one then becomes its own parent |
A rendered id — java:com.acme.order/OrderService.bill(com.acme.order.Order),
produced by core's renderId as <lang>:<module>[/<symbol>][#<disambiguator>] —
is a display projection: written into v1 files and shown to users, and
never parsed. v1 carries nothing else, so the analyzer compares those
strings as opaque tokens; from M6 the key travels structurally and comparison is
component-wise. Because the id is a projection it must not lose information, so
/ and # are reserved: a module may contain neither, a symbol may not
contain #, and a present disambiguator is non-empty. Under those rules rendering is injective — two distinct keys can
never produce one id — which is the property that keeps an entity from silently
vanishing into another (the overload collision of §10, one level up).
Why a module names itself rather than its parent: the alternative renders the
package java:com.acme.order as java:com.acme/order and, worse, needs a
fabricated java module to place the stub package java:java.util, whose
parent no corpus declares — exactly the fabrication §6 exists to prevent.
Multi-declaration tolerant: one key may be declared in several places (TypeScript declaration merging, C# partial classes).
Canonical order is sort by natural key, component by component, a missing
disambiguator before any present one. It belongs to the model, not to an
encoding: it is what makes snapshot diffs reviewable and extractor
cross-validation meaningful. It is deliberately not the same as sorting the
rendered ids as strings, where / and . are ordinary characters.
Relations: every reference between concepts (edge endpoints, parent,
children, attachedTo, declaredType, candidates) identifies an entity by
its natural key. The v1 file format spells each of those as the rendered id
string (EntityId in core); an encoding is free to spell them otherwise — M6
uses file-scoped integer surrogates — as long as they resolve to the same key.
A surrogate is not identity and never crosses a file boundary.
The evidence concept: where in the source a fact was observed.
| Attribute | Type | Meaning |
|---|---|---|
file |
string | path of the CodeFile, relative to the analyzed root |
span |
[int, int] |
[startLine, endLine], 1-based, inclusive |
Relations: attached to Entities (via the TSourceAnchor trait) and to
Edges (directly). Anchors on edges are what make every dependency claim
auditable.
Enum qualifying how we know an edge exists. Never mix facts and inferences.
| Value | Meaning | Examples |
|---|---|---|
declared |
written literally in the source | explicit extends, direct call |
derived |
computed by analysis | Go implicit interface satisfaction, TS structural conformance, Python protocols, Rust blanket impls |
dynamic-candidate |
dispatch not statically resolvable; targets are guesses | multimethods, PHP __call, duck typing, dyn Trait |
generated |
produced by macro/annotation expansion | Rust #[derive], Lombok, Python decorators, Clojure macros |
Relations: mandatory attribute of every Edge. Analyses wanting facts only
filter on declared.
TypeScript-family concept: which declaration space(s) an entity occupies.
| Value | Meaning |
|---|---|
type |
exists only at type-check time (TS interface, type) — dependencies on it are erased at runtime |
value |
exists at runtime |
An entity may occupy both (a TS class). Optional attribute; only meaningful
in profiles that declare it (TypeScript).
A source file. Not reified as a first-class entity in most profiles — it
appears as anchor.file values and in TModule.definedIn. It becomes an
explicit node only where a language has file-level dependencies (PHP
FileInclude edges).
A written, declaration-site value: what an annotation argument, a constant initializer, or a default carries in the source. A tagged union whose tags are a closed core-owned vocabulary (MM-3):
| Form | Shape | Notes |
|---|---|---|
string |
{k: "string", v: string} |
chars ride as one-character strings; the declared type keeps 'a' and "a" apart |
number |
{k: "number", v: string} |
the evaluated constant's canonical decimal text — lossless where JSON numbers are not (a Java long, a big decimal) |
boolean |
{k: "boolean", v: boolean} |
|
null |
{k: "null"} |
|
enum |
{k: "enum", type: EntityId, name: string} |
a reference to the type plus the constant's simple name — a member is never fabricated to close a value (§6, verbatim) |
type |
{k: "type", type: EntityId} |
Foo.class and kin. The written type use still emits its own reference edge — a value never replaces a dependency |
array |
{k: "array", items: Literal[]} |
written order kept — a source fact, like parameter order |
annotation |
{k: "annotation", type: EntityId, arguments: NamedArgument[]} |
a nested annotation value |
unevaluated |
{k: "unevaluated", source: string} |
a written constant expression the extractor did not fold. The source text is still a fact — kept, and honest about what it is, mirroring stub degradation |
NamedArgument is {name: string, value: Literal}; a language-implicit name
is normalized explicit (Java's @Foo("x") is value = "x").
Two rules:
- A Literal is what is written, never runtime state. Emitted only when the
language fixes the value at the declaration — a literal, or an expression
that folds from constants (Java: JLS compile-time constant expressions).
Anything else is
unevaluatedor absent; nothing downstream may "run" one. - Ids inside values count for closure (§8a):
enum.type,type.typeand nested annotation types resolve to a declared entity or a stub exactly like an edge endpoint — in the M6 encoding they are file-scoped surrogates, so a dangling one is unwritable.
Relations: carried by Entities via TWithValue (§3.6) and by
annotationUse edges (§4).
The single node concept. There is no entity class hierarchy: an entity is a kind plus a sum of traits, and each trait contributes attributes.
| Attribute | Type | Always present | Meaning |
|---|---|---|---|
id |
EntityId | yes | the natural key of §1.1, rendered — compared, never parsed |
kind |
string | yes | language-profile-defined classification (class, method, function, namespace, …) |
traits |
TraitName[] | yes | the capabilities this entity composes |
space |
Space[] | no | TS-family only, see §1.4 |
| (trait keys) | — | per trait | every trait in traits contributes its keys (§3) |
Validity (per the owning Language Profile, §5):
required(kind) ⊆ traits ⊆ required(kind) ∪ optional(kind), and every declared
trait's keys are present and well-typed.
Relations:
- composed of Traits (§3);
- source or target of Edges (§4);
- classified and licensed by a Language Profile (§5);
- may be a Stub (§6).
A trait is a named micro-capability: a partial schema contributing zero or more attributes to the entity that declares it. Composition is commutative and associative; the trait vocabulary is closed and canonical (never renamed).
Traits marked (marker) contribute no attributes: they declare a
capability whose data lives in edges[] (edges are stored once, outgoing
direction only — see §4).
| Trait | Attributes contributed | Notes |
|---|---|---|
TNamed |
name: string |
Not universal: lambdas, closures, Rust impl blocks, Java constructors have no own name |
TSourceAnchor |
anchor: SourceAnchor |
Evidence for the entity's declaration |
TComment |
comments: string[] |
Attached documentation/comments |
A strong design decision: where an entity is written (lexical containment) and what it semantically belongs to (attachment) are different relations and both are kept.
| Trait | Attributes contributed | Relation expressed |
|---|---|---|
TWithChildren |
children: EntityId[] (v1 only, see below) |
lexical containment, downward |
TChildOf |
parent: EntityId |
lexical containment, upward — the STORED direction |
TAttachedTo |
attachedTo: EntityId |
semantic attachment. Required by: Go receiver methods, Rust impl blocks, C# extension methods, Clojure extend-type/defmethod |
Example where they diverge: a Go method with receiver (o *Order) is a
child of its file/package (where it is written) but attached to Order.
children is an inverse index, not a fact (MM-2). It is the exact inverse
of parent, and §4's rule — inverse views are derived in memory, never
serialized — has always applied to it; v1 carrying the key was an inherited
inconsistency (11.6MB of it on fineract). TWithChildren stays a declared
trait: it says an entity is a container, which is what §5's containment ≠
attachment distinction needs. Only the serialized key goes, in M6.
| Trait | Attributes contributed | Notes |
|---|---|---|
TModule |
definedIn: string[] (CodeFile paths), isStub: boolean |
module↔file cardinality varies by language: 1-1 (JS/TS/Python: module is the file), 1-N (Java package, C#/Go/PHP namespace), N-N (Rust inline mod). Rust modules are hierarchical (crate = root). isStub mirrors TType's: the import graph is module-level (§9), so an import of an external module needs an endpoint that exists — a stub module has definedIn: [], which is exactly what makes it external |
| Trait | Attributes contributed | Notes |
|---|---|---|
TType |
isStub: boolean |
any type-like entity: class, interface, struct, enum, protocol, PHP/Rust trait |
TWithInheritances |
(marker) — see Inheritance edges |
multiple inheritance = N edges (Python). Absent from Go/Rust profiles — that absence is profile information, not a gap |
TWithImplements |
(marker) — see InterfaceImplementation edges |
|
TTypedEntity |
declaredType?: EntityId |
optional even when the trait is present: absent value in JS/Python/Clojure, C# var, inferred Go/TS/Rust |
| Trait | Attributes contributed | Notes |
|---|---|---|
TInvocable |
signature: string |
signature is part of identity (Java/C# overloads, Go receivers) |
TWithParameters |
parameters: EntityId[] |
ordered |
TWithLocalVariables |
localVariables: EntityId[] |
|
TWithInvocations |
(marker) — see Invocation edges |
outgoing only; incoming is derived by the analyzer, never stored |
| Trait | Attributes contributed | Notes |
|---|---|---|
TStructural |
(marker) — value holder | attributes, variables, parameters, Clojure vars. Legal target of Access edges |
TWithAccesses |
(marker) — see Access edges |
outgoing only |
TWithValue |
value: Literal (§1.6) |
the entity's declaration-site constant value: a Java static final constant initializer, an annotation element's default, a TS enum member's value, a Clojure def'd constant. Optional wherever licensed — an attribute whose initializer is not a compile-time constant simply carries no value, and absence means "not constant", never "empty" |
A Clojure var holding a function is simultaneously named, a value holder, and
invocable: traits: [TNamed, TStructural, TInvocable]. No tree-shaped
hierarchy can place it; trait composition expresses it directly.
| Trait | Attributes contributed | Notes |
|---|---|---|
TMetrics |
metrics: Record<string, number> |
open map of measured finite numbers. Only the extractor writes it: every value here required reading source no consumer sees. Optional on every kind that licenses it — a profile without measures is complete, not deficient |
Two rules give the map meaning:
- Absence means "not measured", never zero. A consumer may not default a missing key: "nothing measured this" and "measured as zero" are different statements, and only the second is a fact about the code (the city's metric sources already honor this — an unmeasured building is floored and counted, not zeroed).
- Keys are canonical by documentation, not by schema. Deliberately not a
closed MM-3 vocabulary: a new measure must not wait on a core release.
Validation checks the values (finite numbers); the canonical names below are
what makes one key comparable across extractors — an extractor inventing a
key documents it in its profile
notes.
| Key | Unit | Definition |
|---|---|---|
sloc |
source lines | lines in the entity's own span that are neither blank nor comment-only |
cyclomatic |
branches + 1 | per invocable: 1 + count of decision points — if, loops, non-default case labels, catch, ?:, short-circuit &&/||, pattern guards. A lambda's branches belong to the lambda: it is its own invocable |
The derived counterpart: gross span length (end − start + 1) is
computable from TSourceAnchor alone and is not a measure (it is the city's
loc source); a module's size is a sum over its derived children. sloc
exists because blanks and comments are invisible downstream — the two claims
("how much is written here" vs "how much of it is code") are never conflated.
The single relationship concept. Every edge, regardless of kind, carries:
| Attribute | Type | Required | Meaning |
|---|---|---|---|
edge |
EdgeKind | yes | discriminant, see table below |
from |
EntityId | yes | source |
to |
EntityId | yes | primary target (best candidate if uncertain) |
provenance |
Provenance | yes | how we know (§1.3) |
anchor |
SourceAnchor | yes | where observed |
candidates |
EntityId[] | no | possible targets when dispatch is uncertain; non-empty iff resolution was ambiguous |
sourceFile |
string | no | disambiguates which declaration site produced the edge (C# partial classes, TS declaration merging) |
Rules:
- Outgoing only, stored once. All inverse views — callers of X, subtypes
of Y, importers of M, and
children(the inverse ofparent, §3.2) — are derived in memory, never serialized. Onlyparentis a stored fact; v1 files still carrychildrenand M6 drops the key (MM-2). - Closure:
fromandtomust resolve to a known entity or a stub — a tested property of every model. - No self-reference:
from ≠ to.
| Kind | From → To | Extra attributes | Notes |
|---|---|---|---|
import |
Module → Module | First-class layer: the only relation reliable ≈100% across all languages, hence the granularity for cross-language comparison. Rust has two levels (mod, crate) | |
inheritance |
Type → Type | N edges for multiple inheritance (Python); optional MRO order left to profile notes | |
interfaceImplementation |
Type → Type (interface/trait/protocol) | declared (Java implements) or derived (Go, TS structural) depending on language. For Rust/Clojure the anchor is the reified impl block (§7) |
|
invocation |
Invocable → Invocable | candidates |
uncertain dispatch → provenance: dynamic-candidate + candidates list |
access |
Invocable → Structural | isRead: bool, isWrite: bool |
field/variable reads and writes |
reference |
Entity → Type | type usage that is none of the above (declarations, generics, casts) | |
annotationUse |
Entity → Type (annotation) | arguments: NamedArgument[] (§1.6) |
a written annotation on any entity, with its arguments. Formerly a plain reference; the dedicated kind both carries the values and lets a consumer select annotation usages without guessing from the target's kind — which a stub target cannot answer |
throws |
Invocable → Type (exception) | a written throw statement whose static exception type resolved; the anchor is the throw SITE — the evidence a guard clause leaves. A declared propagation clause (Java throws E) is a plain reference, not this. Distinct from reference so failure exits are selectable without reading the target's kind, which a stub target cannot answer |
|
embedding |
Type → Type | Go struct { Base } — neither inheritance nor attribute (method promotion); dedicated relation |
|
traitUsage |
Type → Trait (PHP) | PHP use TraitX; — kept as a usage edge, never flattened into the class |
|
fileInclude |
CodeFile → CodeFile | PHP include/require — the only file-to-file dependency in the metamodel |
Relations: edges connect Entities; are licensed per Language Profile (a profile lists which edge kinds its extractor can emit); carry SourceAnchor and Provenance.
A profile is data, not code: the contract stating what a given language's extractor may produce. A profile must be specifiable without being implemented (robustness test of the schema).
| Attribute | Type | Meaning |
|---|---|---|
lang |
string | language id, matches the EntityId prefix |
kinds |
Record<kind, {required: TraitName[], optional: TraitName[]}> |
the licit trait compositions per entity kind |
edges |
EdgeKind[] | edge kinds this language can emit |
notes |
string[] | documented static-analysis blind spots (reflection, Class.forName, dynamic require, macros pre-expansion, …) |
Validation of an entity against its profile:
kindexists in the profile;required(kind) ⊆ entity.traits ⊆ required(kind) ∪ optional(kind)(strict equality rejected — too brittle forTComment; free subset rejected — hides extractor bugs);- each declared trait's attributes are present and well-typed.
Relations: licenses Entities and Edges; cross-language analyses operate on
the intersection of the profiles involved (in practice: the import layer
plus whatever both profiles share).
Kinds, trait names, edge kinds and provenance values are finite sets owned by core. The consequence, stated so encodings need not each invent it: an encoding may represent a member by reference — an index into a dictionary, a foreign key — as long as the reference resolves to a canonical name that core validates. An unknown name is a hard error, never a passthrough.
This is what licenses the header dictionaries of the M6 JSONL encoding and the lookup tables of the M7 store without either of them redefining the vocabulary.
The rule above reads only the kind and the set of traits — never the entity
carrying them. So a consumer may decide each distinct (kind, trait set) pair
once and share the verdict across every entity with that composition
(fineract: a few dozen pairs across 240 910 entities). Step 3 — the trait-key
check — still runs per entity, because the keys' values differ.
Two bounds on that licence, both load-bearing: a stub's exemption from the lower bound (§6) is an entity-level fact, so it is part of what the verdict is keyed on; and the verdict is per profile, since two profiles may name the same kind under different rules. It is a reader-side optimisation and requires no encoding support — trait sets are deliberately not a wire-level concept.
Not a separate node type — an Entity with isStub: true, representing something
external to the corpus (JDK, npm packages, …). Exactly two traits contribute
isStub, so exactly two things are stubbable:
| Stub of | Trait | Shape | Why it must exist |
|---|---|---|---|
| a type | TType |
TNamed + TType, optionally TChildOf; no children, no anchor |
every corpus references types it does not declare |
| a module | TModule |
TNamed + TModule + TWithChildren, definedIn: [] |
the import graph is module-level (§9), so import java.util.List points at the package java:java.util — with no module stub the first-class import layer could never satisfy closure (§11 invariant) |
A member (method, field) is deliberately not stubbable: an external member
folds up to its declaring type's stub. Fabricating a class named bill(Order)
to close an endpoint is the one thing stub synthesis exists to prevent — an
unresolvable member id is reported and left dangling instead.
The one non-degraded thing a stub type may hold is TChildOf — the external
module it belongs to — and a stub module lists such types in its children.
Two rules bound it, and both are extractor-side obligations:
- A stub's parent must itself be a stub. Attributing an external type to a
corpus module would make it read as internal to every module-level
analysis. Where the parent cannot honestly be named — a static-analysis
artefact invented inside the corpus's own package (§6, and the
noClasspathnote in the Java profile), or a primitive, which has no module at all — the stub stays parentless and is reported as unplaceable at module level. - Only the extractor may derive it. Ids are opaque to everything downstream (§1.1); the extractor owns the id scheme and already knows the package, so the knowledge is recorded here rather than re-derived by parsing.
Without this, an external type could not be folded to module level at all, and the tempting workaround — treating such a stub as its own module — silently changes the granularity of the result: classes, and even primitives, become nodes of a module dependency graph.
- Edges to stubs are kept; the internal-only view is obtained by filtering stubs out at analysis time — uniformly, for types and modules alike.
- Membership is decided by a whitelist of corpus-declared ids built in a first pass — never by package/name prefix (Spoon in noClasspath mode invents plausible FQNs; prefix filters would launder them into facts).
Verified against Rust (and improving Clojure extend-type): a block like
impl Display for Order attaches to two entities and owns methods, so it is
reified as an anonymous entity:
traits: [TWithChildren, TAttachedTo, TSourceAnchor]— noTNamed;attachedTo→ the type (Order);- the
interfaceImplementationedgeOrder → Displaytakes the block as itsanchor; - its methods are its
children.
The unit of exchange between an extractor and the analyzer: the result of one extraction run. Its content is format-independent.
| Attribute | Type | Meaning |
|---|---|---|
schemaVersion |
semver string | version of the interchange contract |
lang |
string | profile id of the extractor |
extractor |
{name, version, ...flags} |
provenance of the model itself (e.g. noClasspath: true) |
root |
string | analyzed source root (anchors are relative to it) |
repository |
{remote, commit, root, provider?} |
optional — where the analyzed root lives in a hosted repository (see below) |
entities |
Entity[] | all nodes, stubs included |
edges |
Edge[] | all relations, outgoing direction |
repository records provenance of the corpus, as facts:
| Field | Meaning |
|---|---|
remote |
normalized https clone URL (https://github.com/google/gson — never the ssh form, never trailing .git) |
commit |
the sha the corpus was extracted at — a permalink; a branch name moves and is not a fact |
root |
path of the analyzed root relative to the repository root. Anchors are relative to the analyzed root, which may sit below the repo root (gson: gson/src/main/java) — without this prefix no anchor can be projected back to a repo path |
provider |
github | gitlab, only when the hostname does not say (self-hosted); consumers guess from the host otherwise |
The model stores these facts; the blob-URL of a particular host is a projection consumers derive (§9), never serialized — a stored URL would freeze one host's scheme into the interchange. The extractor copies the four strings verbatim from its invocation (it has no git knowledge); the values are supplied by whoever runs it, e.g. the CLI's snapshot orchestration, which already knows the per-frame sha.
The integrity properties a model must satisfy, whatever encodes it:
- closure — every reference resolves to a declared entity or a stub (§4,
§6), the ids inside
Literalvalues included (§1.6); - no self-reference —
from ≠ toon every edge; - provenance set on every edge, from the closed set of §1.3;
- profile validity for every entity (§5);
- natural-key uniqueness — no two entities share
(lang, module, symbol, disambiguator)(§1.1); - canonical order — entities and edges sorted by natural key (§1.1), so two runs over one corpus are diffable.
Relations: a model conforms to exactly one Language Profile; multi-language
analysis is the union of models (keys are globally unique thanks to the lang
component), queried through profile intersection.
A file conforms to this metamodel iff its decoded content satisfies §8a, so
several encodings may conform at once and each decides for itself how the
canonical order manifests physically and how references are spelled (integer
surrogates, rowids). The interchange is model.jsonl, specified by
schemas/ — one JSON Schema per record type plus the
container contract — and designed in
docs/model-encoding.md, which also covers the
planned model.db analysis store: a derived, disposable cache, never the
contract.
What the encoding decides, and what it therefore enforces. Because a reference is a surrogate into the file's own entity section, closure (§8a) is not something a reader checks afterwards — a dangling reference is not expressible. A producer that cannot close a reference must drop it and say so; it cannot write it and hope. The same applies across models: each file is closed on its own, so a reference to another language's entity is a stub (§6) that the union merges by natural key.
Computed by the analyzer from the stored model; listed here because they are part of the conceptual vocabulary even though no encoding stores them:
| Concept | Derived from | Definition |
|---|---|---|
| Incoming indexes | all edges | callers-of, importers-of, subtypes-of, accessors-of — inverse of stored outgoing edges |
| Children index | parent |
children-of — the inverse of the stored containment link (§3.2, MM-2) |
| Type-level dependency graph | all edge kinds | edges folded up to the containing TType entities |
| Module import graph | import edges |
the cross-language comparison layer |
| Coupling metrics | folded graphs | fan-in/fan-out, afferent/efferent coupling, instability |
| Cycles | folded graphs | strongly connected components at module or type level |
| Tangle / minimum feedback set | folded graphs | per-SCC minimal weighted edge set whose removal leaves the component acyclic; tangle metric = feedback references / cyclic references |
| Internal view | isStub |
model minus stubs and their edges |
| Facts-only view | provenance |
model restricted to declared edges |
| Source links | repository + anchors |
per-anchor host permalink — {remote}/blob/{commit}/{root}/{file}#L{s}-L{e} on GitHub, {remote}/-/blob/{commit}/{root}/{file}#L{s}-{e} on GitLab; template chosen by hostname, provider overriding |
| Framework roles | annotationUse edges + a framework profile (§9.1) |
stereotype classification of types (@Service, @RestController, …) and identification of injection points |
| DI wiring | injection points + the interfaceImplementation inverse index | dynamic-candidate edges from a consumer to every corpus implementation of the injected interface — §1.3's definition verbatim: dispatch not statically resolvable, targets are guesses |
Mirror of §5's rule at the framework level: what an annotation means
(@Autowired marks an injection point, @Service a stereotype, @Qualifier
narrows candidates, @Primary wins a tie) is a declarative table — annotation
identity (simple name + declaring module) → role —
specifiable without being implemented, extensible to another framework (CDI,
Micronaut) without code. It lives in the analyzer: the extractor stays
framework-blind (it already emits the facts — a reference edge per written
annotation), and the wiring it licenses is derived in memory, never
serialized. Matching reads the referenced entity's name and parent chain —
never a parsed id — and must tolerate the target being a stub, which is the
normal case for a corpus whose framework jars are absent. Injection points and
roles are selected on annotationUse edges, and qualifier narrowing reads
their arguments (§1.6) — exact values, not guesswork.
A framework RULE that is not an annotation is data too: Spring 4.3+ treats the
sole constructor of a bean as an injection point with nothing written on it, so
the profile carries that as a flag rather than the analyzer as a branch. And a
meta-annotated stereotype is deliberately NOT followed: resolving @MyService
to the @Service it carries needs the annotation type's own declaration, which
is exactly what a stub does not have — so such a type is unclassified, honestly,
rather than classified by a guess.
{
"id": "java:com.acme.order/OrderService.bill(com.acme.order.Order)",
"kind": "method",
"traits": ["TNamed", "TInvocable", "TWithParameters", "TWithLocalVariables",
"TWithInvocations", "TWithAccesses", "TTypedEntity", "TChildOf",
"TSourceAnchor"],
"name": "bill",
"signature": "bill(com.acme.order.Order)",
"declaredType": "java:com.acme.billing/Invoice",
"parent": "java:com.acme.order/OrderService",
"anchor": { "file": "OrderService.java", "span": [15, 22] }
}The id shown is the rendering of the natural key (§1.1)
| component | value |
|---|---|
lang |
java |
module |
com.acme.order |
symbol |
OrderService.bill(com.acme.order.Order) |
disambiguator |
(absent — the symbol is already unique) |
and it is the key, not the string, that decides whether two entities are the same. The string is what a v1 file carries and what a report prints.
Note the id's parameter types: erased fully-qualified names, not simple
names. Simple names genuinely collide — archive(java.util.List) and
archive(com.acme.order.legacy.List) are legal Java overloads that both render
as archive(List), so under simple names they would merge into a single id and
one method would vanish from the model with no error. Ids must be unique, so the
FQN form wins; the signature attribute keeps the same form. (The fixture
corpus contains exactly that overload pair as a regression case.)
Reading it through the metamodel: the entity is a method kind whose
traits license each attribute — TNamed brings name, TInvocable brings
signature (also serving as the id's disambiguator), TTypedEntity brings
declaredType (the return type, itself an EntityId), TChildOf brings
parent, TSourceAnchor brings anchor. TWithParameters and
TWithLocalVariables reference child entities; TWithInvocations and
TWithAccesses are markers announcing outgoing invocation/access edges:
{
"edge": "invocation",
"from": "java:com.acme.order/OrderService.bill(com.acme.order.Order)",
"to": "java:com.acme.order/TaxCalculator.apply(double)",
"provenance": "declared",
"anchor": { "file": "OrderService.java", "span": [19, 19] }
}A fact (declared), with evidence (line 19), between two internal entities
(neither is a stub) — it will survive both the internal-only and the facts-only
filters of §9.