Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions desktop/macos-claude/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

101 changes: 101 additions & 0 deletions desktop/macos-claude/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
// swift-tools-version: 5.10
import PackageDescription

let package = Package(
name: "LionsDesktop",
platforms: [.macOS(.v14)],
products: [
.executable(name: "LionsApp", targets: ["LionsApp"]),
.library(name: "LionsModels", targets: ["LionsModels"]),
.library(name: "LionsStorage", targets: ["LionsStorage"]),
.library(name: "LionsSources", targets: ["LionsSources"]),
.library(name: "LionsAnalysis", targets: ["LionsAnalysis"]),
.library(name: "LionsLLM", targets: ["LionsLLM"]),
.library(name: "LionsPipeline", targets: ["LionsPipeline"]),
],
dependencies: [
.package(url: "https://github.com/groue/GRDB.swift", from: "6.24.0"),
],
targets: [
// --- Library targets ---
.target(
name: "LionsModels",
dependencies: [],
path: "Sources/LionsModels"
),
.target(
name: "LionsStorage",
dependencies: [
"LionsModels",
.product(name: "GRDB", package: "GRDB.swift"),
],
path: "Sources/LionsStorage"
),
.target(
name: "LionsSources",
dependencies: ["LionsModels"],
path: "Sources/LionsSources"
),
.target(
name: "LionsAnalysis",
dependencies: ["LionsModels"],
path: "Sources/LionsAnalysis"
),
.target(
name: "LionsLLM",
dependencies: ["LionsModels"],
path: "Sources/LionsLLM"
),
.target(
name: "LionsPipeline",
dependencies: [
"LionsModels", "LionsAnalysis",
"LionsLLM", "LionsStorage", "LionsSources",
],
path: "Sources/LionsPipeline"
),
.executableTarget(
name: "LionsApp",
dependencies: [
"LionsModels", "LionsStorage", "LionsSources",
"LionsAnalysis", "LionsLLM", "LionsPipeline",
],
path: "Sources/LionsApp"
),

// --- Test targets ---
.testTarget(
name: "LionsModelsTests",
dependencies: ["LionsModels"],
path: "Tests/LionsModelsTests"
),
.testTarget(
name: "LionsStorageTests",
dependencies: ["LionsStorage", "LionsModels"],
path: "Tests/LionsStorageTests"
),
.testTarget(
name: "LionsSourcesTests",
dependencies: ["LionsSources", "LionsModels"],
path: "Tests/LionsSourcesTests"
),
.testTarget(
name: "LionsAnalysisTests",
dependencies: ["LionsAnalysis", "LionsModels"],
path: "Tests/LionsAnalysisTests"
),
.testTarget(
name: "LionsLLMTests",
dependencies: ["LionsLLM", "LionsModels"],
path: "Tests/LionsLLMTests"
),
.testTarget(
name: "LionsPipelineTests",
dependencies: [
"LionsPipeline", "LionsModels", "LionsLLM",
"LionsStorage", "LionsSources", "LionsAnalysis",
],
path: "Tests/LionsPipelineTests"
),
]
)
11 changes: 11 additions & 0 deletions desktop/macos-claude/Sources/LionsAnalysis/AnalyzerProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Foundation
import LionsModels

public protocol AnalyzerProtocol: Sendable {
func analyzeRepo(
allAtoms: [String: ExtendedFileAtoms],
provider: String,
resourceId: String,
version: String
) -> RepoAnalysis
}
68 changes: 68 additions & 0 deletions desktop/macos-claude/Sources/LionsAnalysis/CallGraph.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Foundation
import LionsModels

public func buildCallGraph(
allAtoms: [String: ExtendedFileAtoms],
symbolTable: [String: [(String, SymbolDefinition)]],
importEdges: [(String, String)]
) -> (refs: [CrossFileRef], callEdges: [(String, String)]) {
var importsOf: [String: Set<String>] = [:]
for (fromF, toF) in importEdges {
importsOf[fromF, default: []].insert(toF)
}

var refs: [CrossFileRef] = []
var callEdgesSet: Set<String> = []

for (filePath, atoms) in allAtoms {
let localNames = Set(atoms.definitions.map(\.name))

for call in atoms.callSites {
if localNames.contains(call.callee) { continue }

if let (targetFile, targetDefn) = resolveCall(
calleeName: call.callee, fromFile: filePath,
importedFiles: importsOf[filePath] ?? [], symbolTable: symbolTable
), targetFile != filePath {
refs.append(CrossFileRef(
fromFile: filePath, fromLine: call.line, fromSymbol: call.caller,
toFile: targetFile, toLine: targetDefn.startLine, toSymbol: targetDefn.name,
kind: "calls"
))
let edgeKey = "\(filePath)||\(targetFile)"
if !callEdgesSet.contains(edgeKey) {
callEdgesSet.insert(edgeKey)
}
}
}
}

let callEdges = callEdgesSet.map { key -> (String, String) in
let parts = key.split(separator: "|", maxSplits: 2).map(String.init)
return (parts[0], parts.count > 1 ? parts[1] : "")
}

return (refs, callEdges)
}

func resolveCall(
calleeName: String, fromFile: String,
importedFiles: Set<String>,
symbolTable: [String: [(String, SymbolDefinition)]]
) -> (String, SymbolDefinition)? {
guard let entries = symbolTable[calleeName], !entries.isEmpty else { return nil }

for (filePath, defn) in entries {
if importedFiles.contains(filePath) && defn.visibility == "exported" {
return (filePath, defn)
}
}

for (filePath, defn) in entries {
if filePath != fromFile && defn.visibility == "exported" {
return (filePath, defn)
}
}

return nil
}
140 changes: 140 additions & 0 deletions desktop/macos-claude/Sources/LionsAnalysis/ImportGraph.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import Foundation
import LionsModels

public func resolveImports(
allAtoms: [String: ExtendedFileAtoms]
) -> [(String, String)] {
let knownFiles = Set(allAtoms.keys)
var stemToFiles: [String: [String]] = [:]
for fp in knownFiles {
let stem = fileStem(fp)
stemToFiles[stem, default: []].append(fp)
let name = fileName(fp)
if name != stem {
stemToFiles[name, default: []].append(fp)
}
}

var edges: [(String, String)] = []

for (filePath, atoms) in allAtoms {
let fileDir = parentDir(filePath)
let lang = atoms.language

for imp in atoms.imports {
if let resolved = resolveSingleImport(
module: imp.module, fromFile: filePath, fileDir: fileDir,
lang: lang, knownFiles: knownFiles, stemToFiles: stemToFiles
), resolved != filePath {
edges.append((filePath, resolved))
}
}
}

return edges
}

func resolveSingleImport(
module: String, fromFile: String, fileDir: String,
lang: String, knownFiles: Set<String>, stemToFiles: [String: [String]]
) -> String? {
switch lang {
case "c", "cpp":
if module.hasPrefix("<") || module.hasSuffix(">") { return nil }
let candidate = fileDir == "." ? module : "\(fileDir)/\(module)"
if knownFiles.contains(candidate) { return candidate }
return fuzzyMatch(name: module, stemToFiles: stemToFiles)

case "python":
if module.hasPrefix(".") {
let relPath = String(module.drop(while: { $0 == "." }))
let parts = relPath.isEmpty ? [] : relPath.split(separator: ".").map(String.init)
if !parts.isEmpty {
let candidate = "\(fileDir)/\(parts.joined(separator: "/")).py"
if knownFiles.contains(candidate) { return candidate }
let initCandidate = "\(fileDir)/\(parts.joined(separator: "/"))/__init__.py"
if knownFiles.contains(initCandidate) { return initCandidate }
}
} else {
let parts = module.split(separator: ".").map(String.init)
let candidate = parts.joined(separator: "/") + ".py"
if knownFiles.contains(candidate) { return candidate }
let initCandidate = parts.joined(separator: "/") + "/__init__.py"
if knownFiles.contains(initCandidate) { return initCandidate }
}
let lastPart = module.split(separator: ".").last.map(String.init) ?? module
return fuzzyMatch(name: lastPart, stemToFiles: stemToFiles)

case "typescript", "tsx", "javascript":
if !module.hasPrefix(".") { return nil }
let base = fileDir == "." ? module : "\(fileDir)/\(module)"
for ext in ["", ".ts", ".tsx", ".js", ".jsx"] {
let candidate = normalizePath(base + ext)
if knownFiles.contains(candidate) { return candidate }
}
for idx in ["/index.ts", "/index.tsx", "/index.js"] {
let candidate = normalizePath(base + idx)
if knownFiles.contains(candidate) { return candidate }
}
return nil

case "go":
let tail = module.contains("/") ? String(module.split(separator: "/").last ?? "") : module
return fuzzyMatch(name: tail, stemToFiles: stemToFiles)

case "rust":
let cleaned = module.replacingOccurrences(of: "crate::", with: "")
let parts = cleaned.split(separator: "::").map(String.init)
if !parts.isEmpty {
let candidate = parts.joined(separator: "/") + ".rs"
if knownFiles.contains(candidate) { return candidate }
return fuzzyMatch(name: parts.last ?? "", stemToFiles: stemToFiles)
}
return nil

default:
return nil
}
}

func fuzzyMatch(name: String, stemToFiles: [String: [String]]) -> String? {
let stem = fileStem(name)
if let matches = stemToFiles[stem], matches.count == 1 { return matches[0] }
if let matches = stemToFiles[name], matches.count == 1 { return matches[0] }
return nil
}

// MARK: - Path Helpers

func parentDir(_ path: String) -> String {
let components = path.split(separator: "/")
if components.count <= 1 { return "." }
return components.dropLast().joined(separator: "/")
}

func fileStem(_ path: String) -> String {
let name = fileName(path)
if let dotIndex = name.lastIndex(of: ".") {
return String(name[name.startIndex..<dotIndex])
}
return name
}

func fileName(_ path: String) -> String {
String(path.split(separator: "/").last ?? Substring(path))
}

/// Normalize a path by resolving "." and ".." components.
func normalizePath(_ path: String) -> String {
var parts: [String] = []
for component in path.split(separator: "/").map(String.init) {
if component == "." {
continue
} else if component == ".." {
parts.removeLast()
} else {
parts.append(component)
}
}
return parts.joined(separator: "/")
}
45 changes: 45 additions & 0 deletions desktop/macos-claude/Sources/LionsAnalysis/PageRank.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Foundation

public func computePageRank(
files: Set<String>,
edges: [(String, String)],
damping: Double = 0.85,
iterations: Int = 50
) -> [String: Double] {
let n = files.count
if n == 0 { return [:] }

var scores: [String: Double] = [:]
for f in files { scores[f] = 1.0 / Double(n) }

var inbound: [String: [String]] = [:]
var outboundCount: [String: Int] = [:]

for (src, dst) in edges {
guard files.contains(src), files.contains(dst) else { continue }
inbound[dst, default: []].append(src)
outboundCount[src, default: 0] += 1
}

for _ in 0..<iterations {
var newScores: [String: Double] = [:]
for f in files {
var rank = (1.0 - damping) / Double(n)
for src in inbound[f] ?? [] {
let out = outboundCount[src] ?? 1
rank += damping * (scores[src] ?? 0) / Double(out)
}
newScores[f] = rank
}
scores = newScores
}

let maxScore = scores.values.max() ?? 1.0
if maxScore > 0 {
for f in files {
scores[f] = (scores[f] ?? 0) / maxScore
}
}

return scores
}
Loading