Skip to content
Open
Show file tree
Hide file tree
Changes from 15 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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# ARK Memo: Notes App by ARK Builders

_Implementation in progress_
14 changes: 6 additions & 8 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ android {
namespace 'dev.arkbuilders.arkmemo'
compileSdk 33

namespace 'dev.arkbuilders.arkmemo'

defaultConfig {
applicationId "dev.arkbuilders.arkmemo"
minSdk 26
Expand Down Expand Up @@ -69,13 +71,12 @@ android {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
buildFeatures{
buildFeatures {
buildConfig true
viewBinding true
}
}


dependencies {

implementation 'androidx.core:core-ktx:1.7.0'
Expand All @@ -90,19 +91,16 @@ dependencies {
implementation 'dev.arkbuilders:arklib:0.3.3'

implementation 'androidx.preference:preference:1.2.0'
implementation "com.google.dagger:hilt-android:2.42"
kapt "com.google.dagger:hilt-compiler:2.42"
kapt 'androidx.hilt:hilt-compiler:1.0.0'
implementation "com.google.dagger:hilt-android:2.57"
kapt "com.google.dagger:hilt-compiler:2.57"
kapt 'androidx.hilt:hilt-compiler:1.2.0'

implementation 'com.github.kirich1409:viewbindingpropertydelegate-noreflection:1.5.6'

implementation 'com.google.code.gson:gson:2.10.1'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

implementation 'ch.acra:acra-http:5.9.6'
implementation 'ch.acra:acra-dialog:5.9.6'
implementation 'com.simplemobiletools:commons:5.29.20'
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import dev.arkbuilders.arkmemo.models.GraphicNote
import dev.arkbuilders.arkmemo.models.TextNote
import dev.arkbuilders.arkmemo.preferences.MemoPreferences
import dev.arkbuilders.arkmemo.repo.NotesRepoHelper
import dev.arkbuilders.arkmemo.repo.versions.VersionStorageRepo


@InstallIn(SingletonComponent::class)
Expand All @@ -30,5 +31,10 @@ abstract class RepositoryModule {
memoPreferences: MemoPreferences,
propertiesStorageRepo: PropertiesStorageRepo
) = NotesRepoHelper(memoPreferences, propertiesStorageRepo)

@Provides
fun provideVersionStorageRepo(
memoPreferences: MemoPreferences
) = VersionStorageRepo(memoPreferences)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Scope VersionStorageRepo as a Singleton and make the return type explicit

Without @singleton, Hilt may create multiple instances, defeating the cache-per-root behavior and increasing I/O. Also, adding an explicit return type improves DI clarity.

Apply this diff:

-        @Provides
-        fun provideVersionStorageRepo(
-            memoPreferences: MemoPreferences
-        ) = VersionStorageRepo(memoPreferences)
+        @Singleton
+        @Provides
+        fun provideVersionStorageRepo(
+            memoPreferences: MemoPreferences
+        ): VersionStorageRepo = VersionStorageRepo(memoPreferences)

Additionally, add the missing import:

import javax.inject.Singleton
🤖 Prompt for AI Agents
In app/src/main/java/dev/arkbuilders/arkmemo/di/RepositoryModule.kt around lines
35 to 38, the provideVersionStorageRepo provider is missing a @Singleton scope
and an explicit return type; annotate the function with @Singleton, change its
signature to return VersionStorageRepo explicitly, and add the import
javax.inject.Singleton at the top of the file so Hilt will create a single
instance and the DI contract is clear.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class GraphicNote(
override val title: String = "",
val description: String = "",
override val description: String = "",
override var isForked: Boolean = false,
@IgnoredOnParcel
val svg: SVG? = null,
@IgnoredOnParcel
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/dev/arkbuilders/arkmemo/models/Note.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package dev.arkbuilders.arkmemo.models

import android.os.Parcelable
import dev.arkbuilders.arklib.data.index.Resource

interface Note {
interface Note: Parcelable {
val title: String
val description: String
var resource: Resource?
var isForked: Boolean
}
5 changes: 3 additions & 2 deletions app/src/main/java/dev/arkbuilders/arkmemo/models/TextNote.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class TextNote (
override val title: String = "",
val description: String = "",
override val description: String = "",
val text: String = "",
override var isForked: Boolean = false,
@IgnoredOnParcel
override var resource: Resource? = null
): Note, Parcelable
): Note, Parcelable
10 changes: 10 additions & 0 deletions app/src/main/java/dev/arkbuilders/arkmemo/models/VersionsResult.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.arkbuilders.arkmemo.models

import dev.arkbuilders.arklib.ResourceId
import dev.arkbuilders.arkmemo.repo.versions.Version

data class VersionsResult (
val versions: List<Version>,
val parents: Set<ResourceId>,
val children: List<ResourceId>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package dev.arkbuilders.arkmemo.repo.versions

import android.util.Log
import dev.arkbuilders.arklib.ResourceId
import dev.arkbuilders.arklib.arkFolder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import dev.arkbuilders.arkmemo.models.VersionsResult
import space.taran.arkmemo.utils.arkVersions
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.attribute.FileTime
import kotlin.io.path.writeLines

class RootVersionStorage(private val root: Path) : VersionStorage {

private val storageFile = root.arkFolder().arkVersions()
private var lastModified = FileTime.fromMillis(0L)
private val versions = mutableListOf<Version>()
private val parents = mutableSetOf<ResourceId>()
private val children = mutableListOf<ResourceId>()


suspend fun init() =
withContext(Dispatchers.IO) {
if (Files.exists(storageFile)) {
val result = readStorage()
lastModified = Files.getLastModifiedTime(storageFile)
Log.d(
VERSIONS_STORAGE,
"file $storageFile exists," +
" last modified at $lastModified"
)
versions.addAll(result.versions)
parents.addAll(result.parents)
children.addAll(result.children)
} else Log.d(
VERSIONS_STORAGE,
"file $storageFile doesn't exists"
)
}

override fun isLatestResourceVersion(id: ResourceId): Boolean =
childrenNotParents().contains(id)

private fun replace(oldVersion: Version, newVersion: Version) {
val replaceIndex = versions.indexOf(oldVersion)
versions[replaceIndex] = newVersion
}

override fun contains(id: ResourceId): Boolean {
return parents.contains(id) || children.contains(id)
}

override suspend fun add(version: Version) {
versions.add(version)
parents.add(version.parent)
children.add(version.child)
persist()
}

override suspend fun forget(id: ResourceId) {
if (!parents.contains(id)) {
val myParents = parentsTreeByChild(id)
myParents[id]?.forEach { parent ->
val version = versions
.find { it.parent == parent }
versions.remove(version)
parents.remove(version?.parent)
children.remove(version?.child)
}
}
if (parents.contains(id) && !children.contains(id)) {
val version = versions.find {
it.parent == id
}
versions.remove(version)
parents.remove(id)
}
if (parents.contains(id) && children.contains(id)) {
val versionIdIsChild = versions.find {
it.child == id
}
val versionIdIsParent = versions.find {
it.parent == id
}
val newVersion = Version(
versionIdIsChild?.parent!!,
versionIdIsParent?.child!!
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Force unwraps could cause NPE in version graph operations

Lines 88-89 use !! which will crash if either versionIdIsChild or versionIdIsParent is null. This could happen if the version graph is corrupted or if there's a race condition.

Add null safety checks:

+    if (versionIdIsChild == null || versionIdIsParent == null) {
+        Log.e(VERSIONS_STORAGE, "Inconsistent version graph state for id: $id")
+        return
+    }
     val newVersion = Version(
-        versionIdIsChild?.parent!!,
-        versionIdIsParent?.child!!
+        versionIdIsChild.parent,
+        versionIdIsParent.child
     )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val newVersion = Version(
versionIdIsChild?.parent!!,
versionIdIsParent?.child!!
if (versionIdIsChild == null || versionIdIsParent == null) {
Log.e(VERSIONS_STORAGE, "Inconsistent version graph state for id: $id")
return
}
val newVersion = Version(
versionIdIsChild.parent,
versionIdIsParent.child
)
🤖 Prompt for AI Agents
In app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionStorage.kt
around lines 87 to 89, the code force-unwraps versionIdIsChild and
versionIdIsParent with !! which can throw an NPE when either is null; replace
the force-unwraps with null-safe handling: check both variables before
constructing the new Version, and if either is null return a controlled failure
(e.g. throw a clear IllegalStateException/IllegalArgumentException with
contextual message or return a Result/nullable value) so you avoid crashing on
corrupted or racing version graphs and log the offending state for debugging.

)
replace(versionIdIsChild, newVersion)
versions.remove(versionIdIsParent)
parents.remove(id)
children.remove(id)
}
persist()
}

override fun versions() = versions

override fun parentsTreeByChild(
child: ResourceId
): Map<ResourceId, List<ResourceId>> {
var localChild = child
var parent: ResourceId?
val parents = mutableListOf<ResourceId>()
for (version in versions) {
parent = versions.find {
it.child == localChild
}?.parent
if (parent != null && children.contains(parent))
localChild = parent
if (parent != null) parents.add(parent)
if (!children.contains(parent))
break
}
return mapOf(child to parents)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Inefficient parent tree traversal algorithm

The parentsTreeByChild function has nested loops that could be optimized. The outer loop iterates through all versions, but the inner logic only needs to traverse the parent chain once.

Optimize the traversal:

 override fun parentsTreeByChild(
     child: ResourceId
 ): Map<ResourceId, List<ResourceId>> {
     var localChild = child
-    var parent: ResourceId?
     val parents = mutableListOf<ResourceId>()
-    for (version in versions) {
-        parent = versions.find {
-            it.child == localChild
-        }?.parent
+    while (true) {
+        val parent = versions.find { it.child == localChild }?.parent ?: break
         if (parent != null && children.contains(parent))
             localChild = parent
-        if (parent != null) parents.add(parent)
-        if (!children.contains(parent))
-            break
+        parents.add(parent)
+        if (!children.contains(parent)) break
     }
     return mapOf(child to parents)
 }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionStorage.kt
around lines 101 to 118, replace the current nested-loop traversal with a
single-parent-chain walk using a precomputed map: build a Map<ResourceId,
ResourceId?> from version.child -> version.parent once, then set localChild =
child and loop while true to lookup parent = map[localChild]; if parent is null
break; add parent to parents; if children.contains(parent) then set localChild =
parent and continue, otherwise break; return mapOf(child to parents). This
eliminates repeated versions.find calls and reduces complexity to O(n) for the
map build plus O(depth) per lookup.


override fun childrenNotParents(): List<ResourceId> {
return children.filter {
!parents.contains(it)
}
}

private suspend fun writeToStorage() =
withContext(Dispatchers.IO) {
val lines = mutableListOf<String>()
lines.add(
"$STORAGE_VERSION_PREFIX$STORAGE_VERSION"
)
lines.addAll(
versions.map {
"${it.parent}$KEY_VALUE_SEPARATOR${it.child}"
}
)
storageFile.writeLines(lines, Charsets.UTF_8)
}

private suspend fun readStorage(): VersionsResult =
withContext(Dispatchers.IO) {
val lines = Files.readAllLines(storageFile)
val storageVersion = lines.removeAt(0)
verifyVersion(storageVersion)
val versions = lines.map {
val parts = it.split(KEY_VALUE_SEPARATOR)
val parent = ResourceId.fromString(parts[0])
val child = ResourceId.fromString(parts[1])
Log.d(
VERSIONS_STORAGE,
it
)
Version(parent, child)
}
val parents = versions.map {
Log.d(
VERSIONS_STORAGE,
"parent: ${it.parent}"
)
it.parent
}.toSet()
val children = versions.map {
Log.d(
VERSIONS_STORAGE,
"child: ${it.child}"
)
it.child
}
return@withContext VersionsResult(
versions,
parents,
children
)
}
Comment on lines 148 to 185
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

No error handling for malformed storage file

The readStorage() function could throw various exceptions (IndexOutOfBoundsException, IllegalArgumentException) if the storage file is corrupted or malformed.

Add proper error handling:

 private suspend fun readStorage(): VersionsResult =
     withContext(Dispatchers.IO) {
+        try {
             val lines = Files.readAllLines(storageFile)
+            if (lines.isEmpty()) {
+                Log.w(VERSIONS_STORAGE, "Storage file is empty")
+                return@withContext VersionsResult(emptyList(), emptySet(), emptyList())
+            }
             val storageVersion = lines.removeAt(0)
             verifyVersion(storageVersion)
             val versions = lines.map {
                 val parts = it.split(KEY_VALUE_SEPARATOR)
+                if (parts.size != 2) {
+                    Log.e(VERSIONS_STORAGE, "Malformed version line: $it")
+                    throw IllegalStateException("Malformed storage file")
+                }
                 val parent = ResourceId.fromString(parts[0])
                 val child = ResourceId.fromString(parts[1])
                 // ... rest of the code
             }
             // ... rest of the code
+        } catch (e: Exception) {
+            Log.e(VERSIONS_STORAGE, "Failed to read storage", e)
+            throw e
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private suspend fun readStorage(): VersionsResult =
withContext(Dispatchers.IO) {
val lines = Files.readAllLines(storageFile)
val storageVersion = lines.removeAt(0)
verifyVersion(storageVersion)
val versions = lines.map {
val parts = it.split(KEY_VALUE_SEPARATOR)
val parent = ResourceId.fromString(parts[0])
val child = ResourceId.fromString(parts[1])
Log.d(
VERSIONS_STORAGE,
it
)
Version(parent, child)
}
val parents = versions.map {
Log.d(
VERSIONS_STORAGE,
"parent: ${it.parent}"
)
it.parent
}.toSet()
val children = versions.map {
Log.d(
VERSIONS_STORAGE,
"child: ${it.child}"
)
it.child
}
return@withContext VersionsResult(
versions,
parents,
children
)
}
private suspend fun readStorage(): VersionsResult =
withContext(Dispatchers.IO) {
try {
val lines = Files.readAllLines(storageFile)
if (lines.isEmpty()) {
Log.w(VERSIONS_STORAGE, "Storage file is empty")
return@withContext VersionsResult(emptyList(), emptySet(), emptyList())
}
val storageVersion = lines.removeAt(0)
verifyVersion(storageVersion)
val versions = lines.map {
val parts = it.split(KEY_VALUE_SEPARATOR)
if (parts.size != 2) {
Log.e(VERSIONS_STORAGE, "Malformed version line: $it")
throw IllegalStateException("Malformed storage file")
}
val parent = ResourceId.fromString(parts[0])
val child = ResourceId.fromString(parts[1])
Log.d(
VERSIONS_STORAGE,
it
)
Version(parent, child)
}
val parents = versions.map {
Log.d(
VERSIONS_STORAGE,
"parent: ${it.parent}"
)
it.parent
}.toSet()
val children = versions.map {
Log.d(
VERSIONS_STORAGE,
"child: ${it.child}"
)
it.child
}
return@withContext VersionsResult(
versions,
parents,
children
)
} catch (e: Exception) {
Log.e(VERSIONS_STORAGE, "Failed to read storage", e)
throw e
}
}
🤖 Prompt for AI Agents
In app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionStorage.kt
around lines 140 to 174, readStorage() currently assumes the file format is
correct and will throw on malformed input; wrap the file parsing in a try/catch
that catches IO and parsing-related exceptions (IndexOutOfBoundsException,
IllegalArgumentException, any exceptions from ResourceId.fromString), validate
the file has at least one line before removeAt(0), ensure each data line splits
into exactly two parts before using parts[0]/parts[1], handle or log malformed
lines (skip them) and either return an empty/safe VersionsResult or rethrow a
clear, custom exception with context; ensure errors are logged with details so
callers can react appropriately.


override fun getValue(id: ResourceId): Version2 {
TODO("Not yet implemented")
}

override suspend fun persist() =
withContext(Dispatchers.IO) {
writeToStorage()
return@withContext
}

override fun remove(id: ResourceId) {
TODO("Not yet implemented")
}

override fun setValue(
id: ResourceId,
value: Version2
) {
TODO("Not yet implemented")
}

companion object {
private const val VERSIONS_STORAGE = "versions"
private const val STORAGE_VERSION_PREFIX = "version "
private const val STORAGE_VERSION = 1
private const val KEY_VALUE_SEPARATOR = "->"

private fun verifyVersion(header: String) {
if (!header.startsWith(STORAGE_VERSION_PREFIX))
throw IllegalStateException("Unknown storage version")
val version = header.removePrefix(STORAGE_VERSION_PREFIX).toInt()
if (version > STORAGE_VERSION)
throw IllegalStateException("Storage version is newer than app")
if (version < STORAGE_VERSION)
throw IllegalStateException("Storage version is older than app")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.arkbuilders.arkmemo.repo.versions

import dev.arkbuilders.arklib.ResourceId
import dev.arkbuilders.arklib.arkFolder
import dev.arkbuilders.arklib.data.storage.FileStorage
import kotlinx.coroutines.CoroutineScope
import space.taran.arkmemo.utils.arkVersions
import java.nio.file.Path

class RootVersionsStorage(
private val scope: CoroutineScope,
private val root: Path
):
FileStorage<Versions>("versions", scope, root.arkFolder().arkVersions(), VersionsMonoid),
VersionsStorage {

override fun valueFromString(raw: String): Versions =
raw.split(",").filter { it.isNotEmpty() }.map {
ResourceId.fromString(it)
}.toSet()
Comment on lines +16 to +19
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Handle potential parsing errors when converting strings to ResourceIds.

The valueFromString method could throw exceptions if ResourceId.fromString() encounters malformed input. This could happen if the storage file gets corrupted or manually edited.

Add error handling to prevent crashes:

 override fun valueFromString(raw: String): Versions =
-    raw.split(",").filter { it.isNotEmpty() }.map {
-        ResourceId.fromString(it)
-    }.toSet()
+    raw.split(",").filter { it.isNotEmpty() }.mapNotNull {
+        try {
+            ResourceId.fromString(it)
+        } catch (e: Exception) {
+            Log.e("RootVersionsStorage", "Failed to parse ResourceId: $it", e)
+            null
+        }
+    }.toSet()

Don't forget to add the import:

import android.util.Log
🤖 Prompt for AI Agents
In
app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/RootVersionsStorage.kt
around lines 17 to 20, the current valueFromString blindly calls
ResourceId.fromString and can throw on malformed items; wrap the mapping in safe
parsing (try/catch per item), skip or filter out invalid entries, and log parse
failures with android.util.Log so the method never throws—return the set of
successfully parsed ResourceId objects (or an empty set if none) and add the
import android.util.Log.


override fun valueToString(value: Versions): String = value.joinToString(",")
}
10 changes: 10 additions & 0 deletions app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/Version.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package dev.arkbuilders.arkmemo.repo.versions

import dev.arkbuilders.arklib.ResourceId

typealias Version2 = ResourceId

data class Version(
val parent: ResourceId,
val child: ResourceId
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package dev.arkbuilders.arkmemo.repo.versions

import dev.arkbuilders.arklib.ResourceId
import dev.arkbuilders.arklib.data.storage.Storage

interface VersionStorage: Storage<Version2> {

suspend fun add(version: Version)

suspend fun forget(id: ResourceId)

fun versions(): List<Version>

fun contains(id: ResourceId): Boolean

fun parentsTreeByChild(
child: ResourceId
): Map<ResourceId, List<ResourceId>>

fun childrenNotParents(): List<ResourceId>

fun isLatestResourceVersion(id: ResourceId): Boolean
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Clarify API semantics and ordering with KDoc; consider naming tweaks for discoverability

The API surface is solid, but the behavior of key methods is ambiguous without docs (e.g., ordering of versions(), semantics of forget, and the shape of parentsTreeByChild). This makes it hard for third-party storage implementors (per PR discussion) to implement correctly.

  • Document ordering guarantees for versions() (e.g., insertion order, topological, or unspecified).
  • Clarify whether forget removes a node, edges, or both, and how it affects descendants.
  • Specify the returned map semantics of parentsTreeByChild (key/value meanings, inclusion of the child itself, traversal order).
  • Define childrenNotParents (is it “leaves” in the version graph?) and whether order is stable.
  • Define “latest” precisely in isLatestResourceVersion (latest across the whole graph vs per-branch? how ties are handled).

Example KDoc skeleton (apply near each method):

/**
 * Adds an edge (parent -> child) to the version graph. If nodes are missing, they are created.
 * Should be idempotent.
 */
suspend fun add(version: Version)

/**
 * Forgets a resource from the version graph. Clarify whether this:
 * - removes only the node `id` and re-links children to its parents, or
 * - removes the node and all incident edges, or
 * - removes a leaf only and fails otherwise.
 */
suspend fun forget(id: ResourceId)

/**
 * Returns all version edges. Specify ordering (e.g., insertion order, topological, or unspecified).
 */
fun versions(): List<Version>

/** True if the graph contains the resource id as a node. */
fun contains(id: ResourceId): Boolean

/**
 * Returns an ancestor tree for the given child.
 * Map semantics: ResourceId -> direct parents of that node.
 * Include whether the child itself appears as a key and the traversal/ordering guarantees.
 */
fun parentsTreeByChild(child: ResourceId): Map<ResourceId, List<ResourceId>>

/**
 * Returns resources that are not parents of any other resource (i.e., graph leaves).
 * Specify ordering if any.
 */
fun childrenNotParents(): List<ResourceId>

/**
 * True if `id` is a latest version (leaf) under the configured semantics (global vs per-branch).
 */
fun isLatestResourceVersion(id: ResourceId): Boolean

Optional: consider renaming parentsTreeByChild to ancestorTreeFrom(child) and childrenNotParents to leaves() for brevity. I can update call sites if you want to adopt that.

🤖 Prompt for AI Agents
In app/src/main/java/dev/arkbuilders/arkmemo/repo/versions/VersionStorage.kt
around lines 6 to 23, the interface methods lack KDoc clarifying ordering and
semantics which makes third‑party implementations ambiguous; add concise KDoc
for each method: describe ordering guarantees for versions() (e.g.,
insertion/topological/unspecified), specify exactly what add(version) does (edge
creation, node creation, idempotency), define forget(id) behavior (remove node
only, remove incident edges, reparent children, fail on non‑leaf), document
parentsTreeByChild(child) map shape (keys -> node, values -> direct parents,
whether child is included, traversal order), define childrenNotParents() meaning
(graph leaves) and ordering stability, and precisely define
isLatestResourceVersion(id) (global vs branch latest and tie handling);
optionally note a recommended rename (e.g., ancestorTreeFrom / leaves) if you
want to improve discoverability.

Loading
Loading