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
9 changes: 7 additions & 2 deletions .github/workflows/android-benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,13 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add benchmarks/android-history/
git diff --cached --quiet || git commit -m "chore(bench): android benchmark summary ${{ github.sha }}"
git push
git diff --cached --quiet && exit 0
git commit -m "chore(bench): android benchmark summary ${{ github.sha }}"
for i in 1 2 3 4 5; do
git push && break
echo "Push attempt $i failed — rebasing and retrying..."
git pull --rebase origin main
done

- name: Compare to baseline and post PR comment
if: github.event_name == 'pull_request'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
benchmark:
name: Load Benchmark
runs-on: ubuntu-latest
timeout-minutes: 30
if: github.event.pull_request.draft == false

steps:
Expand Down
28 changes: 26 additions & 2 deletions kmp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,10 @@ tasks.register<Test>("jvmTestProfile") {
classpath = tasks.named<Test>("jvmTest").get().classpath
testClassesDirs = tasks.named<Test>("jvmTest").get().testClassesDirs

val graphPath = (project.findProperty("graphPath") as? String).orEmpty()
systemProperty("STELEKIT_GRAPH_PATH", graphPath)
val graphPath = (project.findProperty("graphPath") as? String).orEmpty()
val benchConfig = (project.findProperty("benchConfig") as? String) ?: "XLARGE"
systemProperty("STELEKIT_GRAPH_PATH", graphPath)
systemProperty("STELEKIT_BENCH_CONFIG", benchConfig)
systemProperty("benchmark.output.dir", layout.buildDirectory.dir("reports").get().asFile.absolutePath)

filter {
Expand Down Expand Up @@ -454,6 +456,28 @@ print(out_file)
}
}

// ── library stats ("Spotify Wrapped" for your knowledge graph) ─────────────
// Usage: ./gradlew :kmp:graphStats -PgraphPath=/path/to/your/logseq
tasks.register<Test>("graphStats") {
group = "verification"
description = "Print library stats. Usage: -PgraphPath=/your/logseq"

classpath = tasks.named<Test>("jvmTest").get().classpath
testClassesDirs = tasks.named<Test>("jvmTest").get().testClassesDirs

val graphPath = (project.findProperty("graphPath") as? String).orEmpty()
systemProperty("STELEKIT_GRAPH_PATH", graphPath)

filter {
includeTestsMatching("dev.stapler.stelekit.stats.LibraryWrappedTest")
}

testLogging {
events("PASSED", "FAILED", "SKIPPED")
showStandardStreams = true
}
}

compose.desktop {
application {
mainClass = "dev.stapler.stelekit.desktop.MainKt"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) 2026 Tyler Stapler
// SPDX-License-Identifier: Elastic-2.0

package dev.stapler.stelekit.stats

/**
* A snapshot of a graph's size, connectivity, growth, and link topology.
* Computed by GraphStatsCollector from raw markdown files — no SQLite required.
* Intended for use by both the CLI tool and the future in-app Library Stats screen.
*/
data class GraphStatsReport(
val graphPath: String,

// ── Volume ────────────────────────────────────────────────────────────
val pageCount: Int,
val journalCount: Int,
val totalBlocks: Int,
val pagesWithNoContent: Int,

// ── Link topology ─────────────────────────────────────────────────────
val totalOutgoingLinks: Int,
val totalHashtags: Int,
/** Pages that reference at least one other page. */
val pagesWithOutgoingLinks: Int,
/** Pages that are referenced by at least one other page. */
val pagesWithIncomingLinks: Int,
val avgOutgoingLinksPerPage: Float,
val avgIncomingLinksPerPage: Float,
val maxIncomingLinks: Int,
val maxOutgoingLinks: Int,
/** Fraction of blocks that contain at least one [[link]]. Equivalent to SyntheticGraphGenerator.Config.linkDensity. */
val blockLinkDensity: Float,
/** Distribution: how many pages have exactly N incoming links (capped at 20 for display). */
val incomingLinkHistogram: Map<Int, Int>,
/** Distribution: how many pages have exactly N outgoing links (capped at 20 for display). */
val outgoingLinkHistogram: Map<Int, Int>,
/** Top 15 pages ranked by incoming link count. */
val topByIncomingLinks: List<PageConnectivity>,
/** Top 15 pages ranked by outgoing link count. */
val topByOutgoingLinks: List<PageConnectivity>,

// ── Time span ─────────────────────────────────────────────────────────
/** ISO date string "YYYY-MM-DD", or null if no dated journals found. */
val firstJournalDate: String?,
val lastJournalDate: String?,
/** Distinct days with journal entries. */
val journalDays: Int,
/** Calendar days between first and last journal. */
val journalSpanDays: Int,
/** journalDays / journalSpanDays — fraction of days that have a journal entry. */
val journalFillRate: Float,

// ── Density ───────────────────────────────────────────────────────────
val avgBlocksPerPage: Float,

// ── Growth over time ──────────────────────────────────────────────────
/** "YYYY" → count of journals in that year. */
val journalsByYear: Map<String, Int>,
/** "YYYY-MM" → count of journals in that month. */
val journalsByMonth: Map<String, Int>,

// ── Namespaces ────────────────────────────────────────────────────────
val topNamespaces: List<NamespaceStat>,

// ── Benchmark targets ─────────────────────────────────────────────────
/** Suggested SyntheticGraphGenerator.Config values calibrated to 2× this library. */
val benchmarkTargets: BenchmarkTargets,
)

data class PageConnectivity(
val name: String,
val incomingLinks: Int,
val outgoingLinks: Int,
)

data class NamespaceStat(val namespace: String, val count: Int)

data class BenchmarkTargets(
val pageCount: Int,
val journalCount: Int,
val linkDensity: Float,
val blocksPerPageMin: Int,
val blocksPerPageMax: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) 2026 Tyler Stapler
// SPDX-License-Identifier: Elastic-2.0

package dev.stapler.stelekit.stats

interface LibraryStatsProvider {
suspend fun collect(graphPath: String): GraphStatsReport?
}

object NoOpLibraryStatsProvider : LibraryStatsProvider {
override suspend fun collect(graphPath: String): GraphStatsReport? = null
}
14 changes: 14 additions & 0 deletions kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ import dev.stapler.stelekit.ui.i18n.t
import dev.stapler.stelekit.ui.onboarding.Onboarding
import dev.stapler.stelekit.ui.screens.AllPagesScreen
import dev.stapler.stelekit.ui.screens.AllPagesViewModel
import dev.stapler.stelekit.ui.screens.LibraryStatsScreen
import dev.stapler.stelekit.ui.screens.LibraryStatsViewModel
import dev.stapler.stelekit.stats.LibraryStatsProvider
import dev.stapler.stelekit.stats.NoOpLibraryStatsProvider
import dev.stapler.stelekit.ui.screens.GlobalUnlinkedReferencesScreen
import dev.stapler.stelekit.ui.screens.JournalsView
import dev.stapler.stelekit.ui.screens.JournalsViewModel
Expand Down Expand Up @@ -107,6 +111,7 @@ fun StelekitApp(
pluginHost: PluginHost = remember { PluginHost() },
encryptionManager: EncryptionManager = remember { DefaultEncryptionManager() },
urlFetcher: UrlFetcher = remember { NoOpUrlFetcher() },
libraryStatsProvider: LibraryStatsProvider = NoOpLibraryStatsProvider,
voicePipeline: VoicePipelineConfig = remember { VoicePipelineConfig() },
voiceSettings: VoiceSettings? = null,
onRebuildVoicePipeline: (() -> Unit)? = null,
Expand Down Expand Up @@ -240,6 +245,7 @@ fun StelekitApp(
graphManager = graphManager,
notificationManager = notificationManager,
urlFetcher = urlFetcher,
libraryStatsProvider = libraryStatsProvider,
voicePipeline = voicePipeline,
voiceSettings = voiceSettings,
onRebuildVoicePipeline = onRebuildVoicePipeline,
Expand Down Expand Up @@ -267,6 +273,7 @@ private fun GraphContent(
graphManager: GraphManager,
notificationManager: NotificationManager,
urlFetcher: UrlFetcher = NoOpUrlFetcher(),
libraryStatsProvider: LibraryStatsProvider = NoOpLibraryStatsProvider,
voicePipeline: VoicePipelineConfig = VoicePipelineConfig(),
voiceSettings: VoiceSettings? = null,
onRebuildVoicePipeline: (() -> Unit)? = null,
Expand Down Expand Up @@ -438,6 +445,9 @@ private fun GraphContent(
val allPagesViewModel = remember {
AllPagesViewModel(repos.pageRepository, repos.blockRepository)
}
val libraryStatsViewModel = remember {
LibraryStatsViewModel(libraryStatsProvider, graphManager.getActiveGraphInfo()?.path ?: "")
}
val searchViewModel = remember {
SearchViewModel(repos.searchRepository)
}
Expand All @@ -450,6 +460,7 @@ private fun GraphContent(
blockStateManager.close()
journalsViewModel.close()
allPagesViewModel.close()
libraryStatsViewModel.close()
searchViewModel.close()
voiceCaptureViewModel.close()
viewModel.close()
Expand Down Expand Up @@ -632,6 +643,7 @@ private fun GraphContent(
blockStateManager = blockStateManager,
journalsViewModel = journalsViewModel,
allPagesViewModel = allPagesViewModel,
libraryStatsViewModel = libraryStatsViewModel,
viewModel = viewModel,
searchViewModel = searchViewModel,
notificationManager = notificationManager,
Expand Down Expand Up @@ -751,6 +763,7 @@ private fun ScreenRouter(
blockStateManager: dev.stapler.stelekit.ui.state.BlockStateManager,
journalsViewModel: JournalsViewModel,
allPagesViewModel: AllPagesViewModel,
libraryStatsViewModel: LibraryStatsViewModel,
viewModel: StelekitViewModel,
searchViewModel: SearchViewModel,
notificationManager: NotificationManager,
Expand Down Expand Up @@ -816,6 +829,7 @@ private fun ScreenRouter(
onPageClick = { page -> viewModel.navigateTo(Screen.PageView(page)) },
onBulkDelete = { uuids -> viewModel.bulkDeletePages(uuids) }
)
is Screen.LibraryStats -> LibraryStatsScreen(viewModel = libraryStatsViewModel)
is Screen.Notifications -> {
NavigationTracingEffect("Notifications")
NotificationHistory(notificationManager)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ sealed class Screen {
@HelpPage(docs = AllPagesDocs::class)
data object AllPages : Screen()

data object LibraryStats : Screen()
data object Notifications : Screen()
data object Logs : Screen()
data object Performance : Screen()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,7 @@ class StelekitViewModel(
is Screen.Performance -> "Opened Performance"
is Screen.GlobalUnlinkedReferences -> "Opened Unlinked References"
is Screen.Import -> "Import text as new page"
is Screen.LibraryStats -> "Opened Library Stats"
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.StarBorder
import androidx.compose.material.icons.filled.Style
import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.BarChart
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
Expand Down Expand Up @@ -123,6 +124,7 @@ fun LeftSidebar(
NavigationItem("Journals", Icons.Default.DateRange, currentScreen is Screen.Journals) { onNavigate(Screen.Journals) }
NavigationItem("Flashcards", Icons.Default.Style, currentScreen is Screen.Flashcards) { onNavigate(Screen.Flashcards) }
NavigationItem("All Pages", Icons.AutoMirrored.Filled.List, currentScreen is Screen.AllPages) { onNavigate(Screen.AllPages) }
NavigationItem("Library Stats", Icons.Default.BarChart, currentScreen is Screen.LibraryStats) { onNavigate(Screen.LibraryStats) }
NavigationItem("Unlinked References", Icons.Default.Link, currentScreen is Screen.GlobalUnlinkedReferences) { onNavigate(Screen.GlobalUnlinkedReferences) }
NavigationItem("Notifications", Icons.Default.Notifications, currentScreen is Screen.Notifications) { onNavigate(Screen.Notifications) }

Expand Down
Loading
Loading