Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
66becba
Add CodeGraphModel package with shared graph schema
kyleve Jun 17, 2026
c387a9d
Add code-graph-extract CLI that opens the compiler index store
kyleve Jun 17, 2026
d8a93db
Harvest nodes and relationship edges from the index store
kyleve Jun 17, 2026
a7ccc85
Filter compiler-synthesized symbols out of the graph
kyleve Jun 17, 2026
e74bd9f
Add --watch mode that re-extracts on index-store changes
kyleve Jun 17, 2026
5bed6a5
Split CodeGraphModel into its own dependency-free package
kyleve Jun 17, 2026
8118263
Add Mac Catalyst viewer that loads and hot-reloads graph.json
kyleve Jun 17, 2026
c7ef375
Add force-directed layout engine actor
kyleve Jun 17, 2026
d8617cf
Render the interactive graph canvas
kyleve Jun 17, 2026
b37e7f5
Add client-side filtering and focus
kyleve Jun 17, 2026
acee963
Persist pins, filters, and named saved views
kyleve Jun 17, 2026
e520d4e
Add run.sh, README, and .gitignore for CodeGraph
kyleve Jun 17, 2026
37a98ef
Open the graph by drag-and-drop or launch path
kyleve Jun 17, 2026
b76d935
Add the CodeGraph plan under Tools/CodeGraph/Plans
kyleve Jun 19, 2026
9a4d023
Cut canvas re-render and relayout churn
kyleve Jun 19, 2026
3c581e7
Cull off-screen chips and trim chip shadows
kyleve Jun 19, 2026
1103945
Add unit tests for the graph's deterministic logic
kyleve Jun 19, 2026
9f91ff2
Drain stdout and stderr concurrently in Shell.capture
kyleve Jun 19, 2026
b9b1da1
Keep the settling flag honest and seed newly-visible nodes
kyleve Jun 19, 2026
a01b0e1
Isolate node drags so only the dragged chip + its edges re-render
kyleve Jun 19, 2026
a99ce86
Render type members as rows inside the chip (UML box)
kyleve Jun 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Tools/CodeGraph/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# code-graph-extract output: the dedicated build's DerivedData + graph.json.
.codegraph/

# SwiftPM / Tuist build products (the Xcode project/workspace are generated).
.build/
Derived/
*.xcodeproj
*.xcworkspace
30 changes: 30 additions & 0 deletions Tools/CodeGraph/CodeGraphModel/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// swift-tools-version: 6.2
import PackageDescription

/// Dependency-free model package shared by the code-graph-extract CLI and the
/// Catalyst viewer. Keeping it free of heavy/platform-specific dependencies
/// (e.g. indexstore-db) lets the iOS/Mac Catalyst viewer consume it cleanly.
let package = Package(
name: "CodeGraphModel",
platforms: [
.macOS(.v13),
.iOS(.v16),
],
products: [
.library(name: "CodeGraphModel", targets: ["CodeGraphModel"]),
],
targets: [
.target(
name: "CodeGraphModel",
resources: [
.process("Resources"),
],
),
.testTarget(
name: "CodeGraphModelTests",
dependencies: [
"CodeGraphModel",
],
),
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import Foundation

/// A complete snapshot of a repository's types and the relationships between
/// them, produced by `code-graph-extract` and rendered by the viewer.
///
/// Everything discovered is kept; narrowing the graph down to something
/// readable is purely a viewer-side filtering concern.
public struct CodeGraph: Codable, Sendable, Hashable {
/// Bumped when the on-disk shape changes so the viewer can reject snapshots
/// it cannot read.
public var schemaVersion: Int
/// When the snapshot was generated.
public var generatedAt: Date
/// Absolute path of the repository the snapshot was taken from.
public var repoPath: String
/// `git` commit the snapshot reflects, when known.
public var commit: String?
public var modules: [Module]
public var nodes: [Node]
public var edges: [Edge]

public init(
schemaVersion: Int = CodeGraph.currentSchemaVersion,
generatedAt: Date,
repoPath: String,
commit: String? = nil,
modules: [Module],
nodes: [Node],
edges: [Edge],
) {
self.schemaVersion = schemaVersion
self.generatedAt = generatedAt
self.repoPath = repoPath
self.commit = commit
self.modules = modules
self.nodes = nodes
self.edges = edges
}

/// The schema version this build of the model produces and understands.
public static let currentSchemaVersion = 1
}

extension CodeGraph {
/// Decode a snapshot from `graph.json` data using the shared coding config.
public static func decoded(from data: Data) throws -> CodeGraph {
try CodeGraphCoding.decoder.decode(CodeGraph.self, from: data)
}

/// Encode this snapshot to pretty-printed `graph.json` data.
public func encoded() throws -> Data {
try CodeGraphCoding.encoder.encode(self)
}

/// A small hand-written snapshot bundled for tests, previews, and for
/// developing the viewer before the extractor produces real data.
public static func sample() throws -> CodeGraph {
guard
let url = Bundle.module.url(forResource: "sample-graph", withExtension: "json")
else {
throw CodeGraphError.resourceMissing("sample-graph.json")
}
return try decoded(from: Data(contentsOf: url))
}
}

/// Errors surfaced by the model layer.
public enum CodeGraphError: Error, CustomStringConvertible {
case resourceMissing(String)

public var description: String {
switch self {
case let .resourceMissing(name):
"Bundled resource is missing: \(name)"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Foundation

/// Shared JSON coding configuration so the extractor (writer) and the viewer
/// (reader) always agree on the `graph.json` format: ISO-8601 dates and a
/// stable key order that diffs cleanly in source control.
public enum CodeGraphCoding {
public static var encoder: JSONEncoder {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
return encoder
}

public static var decoder: JSONDecoder {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}
}
73 changes: 73 additions & 0 deletions Tools/CodeGraph/CodeGraphModel/Sources/CodeGraphModel/Edge.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Foundation

/// A directed relationship between two nodes.
public struct Edge: Codable, Sendable, Hashable, Identifiable {
public var id: String
/// The "from" end: the subclass, the conforming type, or the type that owns
/// the member through which the reference flows.
public var source: String
/// The "to" end: the superclass, the protocol, or the referenced type.
public var target: String
public var kind: EdgeKind
/// The member (property / method / initializer) the relationship flowed
/// through, when applicable — e.g. the property whose type is `target`.
public var viaMemberID: String?
/// How many distinct source occurrences were collapsed into this edge.
public var count: Int

public init(
id: String,
source: String,
target: String,
kind: EdgeKind,
viaMemberID: String? = nil,
count: Int = 1,
) {
self.id = id
self.source = source
self.target = target
self.kind = kind
self.viaMemberID = viaMemberID
self.count = count
}
}

/// The semantic of an ``Edge``. The extractor records every kind it can find;
/// the viewer decides which to show.
public enum EdgeKind: String, Codable, Sendable, CaseIterable, Hashable {
/// Subclass to superclass.
case inheritance
/// Conforming type to protocol.
case conformance
/// Overriding member to the member it overrides.
case override
/// Container to a nested type or member it owns.
case member
/// A type owns a stored / computed property of the target type.
case propertyType
/// A function or initializer references the target in its parameters or
/// return type.
case paramOrReturnType
/// A type constructs, or is initialized with, the target.
case construction
/// A generic parameter is constrained to the target.
case genericConstraint
/// A protocol's associated type relates to the target.
case associatedType
/// A module depends on another module.
case moduleDependency
/// A cross-type reference not captured by a more specific kind.
case reference

/// Whether the relationship is structural (the classic UML "is-a" / nesting
/// edges) as opposed to a usage / data-flow reference.
public var isStructural: Bool {
switch self {
case .inheritance, .conformance, .override, .member, .moduleDependency:
true
case .propertyType, .paramOrReturnType, .construction, .genericConstraint,
.associatedType, .reference:
false
}
}
}
30 changes: 30 additions & 0 deletions Tools/CodeGraph/CodeGraphModel/Sources/CodeGraphModel/Module.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

/// A build module (a SwiftPM / Tuist target) and the modules it depends on.
public struct Module: Codable, Sendable, Hashable, Identifiable {
public var id: String {
name
}

/// Module name, e.g. `WhereCore`.
public var name: String
public var kind: ModuleKind
/// Names of the modules this module depends on.
public var dependencies: [String]

public init(name: String, kind: ModuleKind, dependencies: [String] = []) {
self.name = name
self.kind = kind
self.dependencies = dependencies
}
}

/// The role a ``Module`` plays in the build.
public enum ModuleKind: String, Codable, Sendable, CaseIterable, Hashable {
case library
case app
case appExtension
case test
case external
case unknown
}
133 changes: 133 additions & 0 deletions Tools/CodeGraph/CodeGraphModel/Sources/CodeGraphModel/Node.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import Foundation

/// A single vertex in the graph: a type, a member of a type, a free function, a
/// module, or an external (SDK / third-party) symbol referenced by the repo.
public struct Node: Codable, Sendable, Hashable, Identifiable {
/// Stable identifier. For symbols this is the compiler USR; for modules it
/// is the module name prefixed with `module:`.
public var id: String
/// Display name (unqualified), e.g. `WhereSession` or `report`.
public var name: String
public var kind: NodeKind
/// Owning module name (e.g. `WhereCore`).
public var module: String
public var origin: Origin
/// Owning node id (the enclosing type or module) for membership / nesting.
public var parentID: String?
/// Source file path, relative to the repo root when known.
public var file: String?
/// 1-based line of the declaration.
public var line: Int?
/// Whether the declaration introduces generic parameters.
public var isGeneric: Bool

public init(
id: String,
name: String,
kind: NodeKind,
module: String,
origin: Origin,
parentID: String? = nil,
file: String? = nil,
line: Int? = nil,
isGeneric: Bool = false,
) {
self.id = id
self.name = name
self.kind = kind
self.module = module
self.origin = origin
self.parentID = parentID
self.file = file
self.line = line
self.isGeneric = isGeneric
}

private enum CodingKeys: String, CodingKey {
case id
case name
case kind
case module
case origin
case parentID
case file
case line
case isGeneric
}

/// Custom decoding so that hand-written and older snapshots can omit
/// naturally-defaulted fields (`isGeneric`) without failing to load.
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
kind = try container.decode(NodeKind.self, forKey: .kind)
module = try container.decode(String.self, forKey: .module)
origin = try container.decode(Origin.self, forKey: .origin)
parentID = try container.decodeIfPresent(String.self, forKey: .parentID)
file = try container.decodeIfPresent(String.self, forKey: .file)
line = try container.decodeIfPresent(Int.self, forKey: .line)
isGeneric = try container.decodeIfPresent(Bool.self, forKey: .isGeneric) ?? false
}
}

/// The flavor of a ``Node``.
public enum NodeKind: String, Codable, Sendable, CaseIterable, Hashable {
// Type declarations.
case `class`
case `struct`
case `enum`
case `protocol`
case actor
case `extension`
case typeAlias = "typealias"
case associatedType

// Members / free declarations.
case method
case property
case initializer
case `subscript`
case enumCase
case function

/// Grouping.
case module

case other

/// Whether this kind is a nominal type — the primary nodes a UML-style view
/// arranges (as opposed to members, modules, or loose declarations).
public var isType: Bool {
switch self {
case .class, .struct, .enum, .protocol, .actor, .extension, .typeAlias,
.associatedType:
true
case .method, .property, .initializer, .subscript, .enumCase, .function,
.module, .other:
false
}
}

/// Whether this kind is a member of an enclosing type (and so hidden until
/// its owner is expanded).
public var isMember: Bool {
switch self {
case .method, .property, .initializer, .subscript, .enumCase:
true
case .class, .struct, .enum, .protocol, .actor, .extension, .typeAlias,
.associatedType, .function, .module, .other:
false
}
}
}

/// Where a node's declaration lives relative to the repository.
public enum Origin: String, Codable, Sendable, CaseIterable, Hashable {
/// Declared in the repository's production code.
case firstParty
/// Declared in a test target of the repository.
case test
/// Declared outside the repository (Apple SDK, third-party packages).
case external
}
Loading
Loading