Skip to content

chore: add ktlint and apply one-shot format pass - #58

Merged
mibrahimdev merged 7 commits into
developfrom
chore/ktlint-format
Aug 31, 2026
Merged

mibrahimdev merged 7 commits into
developfrom
chore/ktlint-format

Conversation

@mibrahimdev

Copy link
Copy Markdown
Owner

Summary

Adds ktlint (Gradle plugin org.jlleitschuh.gradle.ktlint 14.2.0) and applies a one-shot mechanical format pass across the codebase. CI enforcement is intentionally not in this PR — it lands in a follow-up so this PR's diff stays purely mechanical.

What's here

  • Tooling: ktlint Gradle plugin 14.2.0 via version catalog; applied to :sharingan, :sharingan-noop, :sample:composeApp. ktlintCheck runs as part of check.
  • .editorconfig: ktlint_official style, trailing commas on (matches the Compose convention), max line 140. @Composable/@ObjCName functions are exempt from function-naming — the capitalized SharinganViewController() factory is public API and must keep its name.
  • Format pass: 67 files, mechanical only (indentation, trailing commas, import order). Three hand fixes: one raw-string line over 140 chars in PreviewData.kt; no renames, no logic changes.

Verification

  • ./gradlew ktlintCheck — green
  • ./gradlew check — green (unit tests + apiCheck + checkApiParity all pass; public API surface unchanged)

Follow-up

Enforcement in CI: a lint job (ubuntu, ./gradlew ktlintCheck) added first in build.yml with jvm/ios/android gated behind needs: lint.

- ktlint Gradle plugin 14.2.0 via version catalog, applied to all modules;
  ktlintCheck runs as part of check (CI enforcement lands in a follow-up PR)
- .editorconfig: ktlint_official style, trailing commas, max line 140;
  @Composable/@ObjCName functions exempt from function-naming
- ktlintFormat across the codebase: 67 files, mechanical changes only
  (indentation, trailing commas, import order); no logic changes
- .editorconfig: ij_kotlin_code_style = intellij_idea (JetBrains coding
  conventions from kotlinlang.org), trailing commas and 140-col limit kept
- ktlint comment mechanics: comment-wrapping, no-single-line-block-comment
- AGENTS.md: comments policy for agents - comments only for 'why', never
  narrating 'what'; AI-style narration comments prohibited
- ktlint-rules module: sharingan-comments ruleset (ktlint 1.5.0 API)
  - todo-without-issue: TODO/FIXME must reference an issue number
  - no-narration-comment: flags AI-style change notes ('// added ...')
- wired into every ktlint-enabled module via ktlintRuleset; excluded
  from BCV; generated code (SQLDelight) excluded from all ktlint runs
- rules are heuristics with a documented ceiling (see ponytail note in
  SharinganRules.kt); suppress with // ktlint-disable if a false
  positive appears
The member is deprecated in ktlint's 1.5.x API but remains the abstract
member RuleSetProviderV3 requires; no replacement exists on this version.
- ktlint-rules: no-multi-line-comment flags runs of 2+ adjacent // lines
  and non-KDoc block comments spanning lines; KDoc is exempt (docs)
- existing multi-line comments condensed to single lines, keeping the
  'why' (tests, descriptors, build scripts, notification guard)
- AGENTS.md: comments policy updated to cover the new rule
Audit of all remaining inline comments against the AGENTS.md policy:
- deleted 9 pure banner dividers (test names and function names already
  say what follows) and one what-narration comment (bottom fade)
- rewrote the DetailScreen banner to a plain single-line pointer
- everything else kept: issue-linked why-comments, invariants,
  workarounds, seam pointers
@mibrahimdev
mibrahimdev merged commit 3b61381 into develop Aug 31, 2026
5 checks passed
mibrahimdev added a commit that referenced this pull request Aug 31, 2026
develop gained a ktlint ruleset (#58) after this branch forked: standard
formatting rules plus a custom no-multi-line-comment rule. Apply
ktlintFormat and condense every multi-line // run this branch introduced
into a single line, per the AGENTS.md comments policy. KDoc is exempt, so
the Persistence lifecycle docblock is unchanged.

Also applies the ktlint plugin to :sharingan-db. The module was created on
this branch before the ruleset existed, so it was escaping the lint gate
the same way it was escaping the BCV gate — adding it surfaced eight
violations that were previously invisible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhi6Qx8rdo1aX9EnmtEJS
mibrahimdev added a commit that referenced this pull request Sep 1, 2026
* feat: write-behind capture — events survive process death (#50)

Slice 2 of the flight-recorder epic. Events now flow off the in-memory
ring buffer into the on-device DB, so logs survive process death.

- SharinganStore gains an internal `onRecord` seam, invoked after the
  unchanged lock-free CAS append (only while recording). No public change.
- PersistenceController owns CoroutineScope(SupervisorJob()+Dispatchers.Default),
  wires `onRecord = { channel.trySend(it) }` on a bounded channel, and drains
  it with a single flusher coroutine that writes each batch in one SQLDelight
  transaction (size ~50 / 250 ms, whichever first).
- Lazy session row created on the first flushed event of the launch.
- internal @serializable EventDto (Http/Mqtt/Ble) with fromEvent() encode-only;
  public event ABI untouched. Stored as the event payload_json blob.
- Persisted event PK is globally unique across sessions: session id is
  timestamp+random, and the event row id prefixes the raw EventIds value with
  the session id (EventIds resets each launch, so the raw value is not safe
  as a cross-session PK).
- Hands-free on Android via the existing manifest ContentProvider.
- Tests: burst > ring capacity all persisted, batching (not one-write-per-event),
  record() unchanged when persistence off, cross-session id uniqueness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: review follow-ups — DROP_OLDEST, idempotent start, drain-on-stop, session commit (#50)

Addresses both reviewers' REQUEST-CHANGES on #56.

- Channel now uses BufferOverflow.DROP_OLDEST so the crash-tail (newest events)
  survives backpressure, never the oldest. Pinned by a direct-channel test.
- start() guarded by an AtomicBoolean (idempotent — no double flusher).
- Persistence bootstrap made thread-safe with an AtomicBoolean CAS
  (`synchronized` is JVM-only, unavailable in commonMain).
- stop() drains the pending channel + in-flight batch (channel.close() then
  join), then cancels the scope and closes the SqlDriver.
- sessionId memoized only AFTER the batch transaction commits; reset on
  rollback so a later batch re-derives a rolled-back session row.
- Per-batch transaction wrapped in try/catch so one failure never kills the
  flusher coroutine.
- Tests: DROP_OLDEST overflow semantics, stop() drains pending batch; two-session
  test no longer stop()s (stop now closes the shared driver).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: use a real UUID for session ids (#50)

timestamp+Random collided on Kotlin/Native (same millis + Native Random.Default
returning the same value on rapid successive calls), so the two sessions in the
test produced one PK and the second insert hit a UNIQUE violation (swallowed by
the flusher's try/catch). kotlin.uuid.Uuid.random() is collision-free on every
target; started_at stays currentTimeMillis().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(ios): unique per-driver in-memory DB name (#50)

The constant "sharingan-test.db" with inMemory=true mapped to a single
file:...?cache=shared in-memory DB on Kotlin/Native, so every createTestDriver()
in the test binary shared one database and earlier tests leaked ~700 rows into
the two-sessions test. A per-driver UUID name gives each test a private DB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: enforce foreign keys so session deletes cascade (#50)

Enable PRAGMA foreign_keys / setForeignKeyConstraintsEnabled on Android,
iOS driver configuration, and JVM test drivers. Add deleteSession query
and a cascade test that fails when FKs are off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: flush on a real latency deadline not an idle timeout (#50)

Stamp a deadline when the first event enters an empty batch and use the
remaining time in withTimeoutOrNull. A slow steady stream now flushes within
the configured interval instead of waiting for the batch size.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: controller owns the driver it opens (#50)

Give PersistenceController a three-method lifecycle: start() wires the
seam, stop() drains and leaves the driver open for readers, close()
stop()s then cancels the scope and closes the driver. Tests inject the
driver through an internal constructor; production uses the no-driver
constructor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: extract :sharingan-db so persistence leaves the public ABI (#50)

Move SQLDelight schema, drivers, persistence controller and tests into a new
:sharingan-db module. The controller becomes a generic PersistenceController<T>
that receives a (T) -> EventRow mapping, keeping JSON encoding off the hot
record() path. :sharingan consumes :sharingan-db as an implementation
dependency, so the SQLDelight-generated types are no longer exported to the
iOS framework header.

- EventDto and toRow stay in :sharingan, package dev.sharingan.internal
- Add SharinganDbContext for the Android context seam
- Add Now.* equivalents for :sharingan-db's own time source
- Update CI, BCV config, and release docs for the third artifact

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: warn that PersistenceController.stop() does not detach the seam (#50)

Add KDoc warnings to stop() and close() so slice 4's configure()/shutdown
path knows that events submitted after stop() are silently dropped unless
the caller detaches store.onRecord first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: add EventDto encode coverage (#50)

Pin the JSON encoding of HttpEvent / MqttEvent / BleEvent via EventDto in
:sharingan, including the redacted-header value. Uses the same json instance
that toRow() uses (now internal for test visibility). No decode round-trip —
toEvent() is slice 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: swallow toRow mapper throws inside flush so the flusher survives (#50)

The caller-supplied (T) -> EventRow mapping can throw (e.g. JSON encode of
an unbounded body). Move it inside the existing batch try/catch so the
exception drops one batch instead of escaping runFlusher and crashing the
host process.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: use atomic select instead of withTimeoutOrNull in flusher (#50)

withTimeoutOrNull can prompt-cancel after taking an element from the
channel, losing events under backpressure. Replace the timeout path with
select { channel.onReceiveCatching { ... }; onTimeout { ... } } so the
receive and timeout are atomic.

The race itself is not deterministic in a unit test; the new focused test
pins the deadline-flush path that the select exercises.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: hold flusherJob in an AtomicReference to avoid a null race with stop (#50)

A concurrent stop() could read flusherJob before start() assigned it,
skip the join, and tear down the scope while a transaction was in flight.
Replace the AtomicBoolean + var pair with a single AtomicReference<Job?>
used as the start/stop gate; stop atomically reads-and-clears it before
joining.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor: make channel capacity an internal constructor param and test DROP_OLDEST (#50)

Expose channelCapacity through the internal constructor so tests can
exercise backpressure without relying on production defaults. Rename the
500-event burst test to reflect that it stays within the channel, and add
a focused DROP_OLDEST test that submits before start() so the flusher
cannot drain during the burst.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: document SharinganStore onRecord seam contract (#50)

Add focused contract tests showing that the in-memory store forwards
every accepted event to the internal persistence seam, skips forwarding
while paused, and still forwards events that are later evicted by the
ring buffer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: make PersistenceController single-use and start flusher LAZY (#50)

Reviewers found that the finding-#3 AtomicReference rewrite reintroduced
finding-#2 one method up: start() launched the flusher eagerly before the
CAS, so a redundant start() cancelled a coroutine that could already be
suspended in channel.receive(), losing events.

- Fix A: launch the flusher with CoroutineStart.LAZY and call job.start()
  only inside the CAS-won branch. Drop the losing job's cancel() entirely.
- Fix B: add a terminal `stopped` state. stop() sets it; start() after
  stop() throws IllegalStateException. Update KDoc on start()/stop() to
  state single-use semantics and remove the now-wrong 'Idempotent' label.
- Fix C: document that the `select`/`onTimeout` usage is
  ExperimentalCoroutinesApi and load-bearing for the published module.
- Optional nit: collapse readAndClearFlusherJob() to flusherJob.exchange(null).

Add a focused TDD test proving a redundant start() dispatches exactly one
flusher and loses no events, and rewrite the vacuous start/stop-cycle test
into an assertion that start()-after-stop() throws.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: PersistenceController native compile blocker, stop() race, and deterministic test (#50)

- Fix 1 (iOS CI blocker): replace the JVM-only CountingDispatcher/Runnable
  approach in PersistenceControllerTest with an internal
  `flusherStartCount()` seam backed by an AtomicInt. This removes the
  `java.lang.Runnable` reference that broke Kotlin/Native compilation.
- Fix 2 (single-use hole in stop()): always close the channel before reading
  and joining the flusher job. A concurrent start() that wins the CAS after
  stop() sets the terminal flag now starts on a closed channel and exits
  cleanly instead of leaving a live flusher behind.
- Fix 3 (deterministic double-start test): assert `flusherStartCount() == 1`
  after two start() calls instead of string-matching a kotlinx.coroutines
  internal class name.
- Optional nit: use `error(...)` for the single-use guard (still throws
  IllegalStateException).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: move flusherStartCount assertion after flush to remove race (#50)

The assertion that only one flusher started was read before the flusher
had demonstrably entered its body. Move it after the flushed.await() so
the count is deterministic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: trim over-explained comments in the persistence layer (#50)

Cut comments that restated the code or narrated review history; kept the
non-obvious why-notes (LAZY start, atomic select, map-inside-try, DROP_OLDEST,
UUID-not-Random, the stop() seam WARNING). Comment-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: apply reviewers clarity/minimalism cut-list (#50)

Comment-only edits in PersistenceController, EventDto, Persistence, and
PersistenceControllerTest per Vigil + Sage cut-list. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: BCV-guard :sharingan-db's published ABI (#50)

:sharingan-db ships to Maven Central, so its public surface must be
guarded by apiCheck like any other published module. Remove the
ignoredProjects exemption, commit the generated api/*.api dumps, and
verify apiCheck passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: describe shipped persistence (flight recorder) accurately (#50)

Three statements predate the flight recorder: ARCHITECTURE's overview
claimed nothing is persisted, section 5.4 described persistence as
hypothetical/opt-in, and CONTEXT.md's Store entry claimed memory-only.
All three now describe what ships: an in-memory ring buffer (300) that
is also mirrored to a SQLite flight recorder via the write-behind seam
(SharinganStore.onRecord -> PersistenceController), on by default in
debug, with request/response bodies never written to disk. CONTEXT.md
gains Flight Recorder, Write-Behind Seam, and Run entries, and the
Capture entry notes redaction applies before disk too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: fix remaining memory-only claims across README, AGENTS, llms.txt (#50)

Five more places still claimed "memory-only / never persisted": the
README feature bullet and SharinganStore note, ARCHITECTURE's design
properties and known-limitations entries, and AGENTS.md lines 3 and
106. All now say what ships: the ring buffer stays memory-only and is
still cleared on process death; events are additionally mirrored to
the on-disk SQLite flight recorder (debug only), which survives, and
bodies are never written to disk. AGENTS.md and llms.txt are a
hand-maintained mirror — regenerated llms.txt from AGENTS.md and
confirmed byte-identical before committing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: keep CONTEXT glossary formatting uniform (#50)

The Capture entry gained a stray blank line before its _Avoid_ line; every
other entry has them adjacent. Backtick `session` in the Run entry so it
reads as the schema table name rather than the term reserved for the v2 epic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhi6Qx8rdo1aX9EnmtEJS

* fix: clear() drops persisted rows and pending batch; bodies stay off disk (#50)

B1: SharinganStore.clear() now invokes an onClear seam; Persistence wires
it to a new PersistenceController.clear(). The clear travels as a command
on the same channel the writes use, so the single flusher coroutine
deletes rows at an exact point in the write order — a pending in-flight
batch cannot resurrect events after a clear.

B2: toRow() strips HttpDto request/response bodies and Mqtt/Ble payloads
on the persistence path only (design default persistBodies = false);
EventDto stays capable of carrying bodies for the slice-5 opt-in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: give the write-behind seam an owner (#50)

The seam had no lifecycle. Persistence.start() wired store.onRecord and
forgot both the controller and the store, so nothing could unwire it and
'started' latched true for the process lifetime.

- Wire the seams BEFORE starting the flusher. start() is LAZY so the old
  order was harmless, but the dependency is now encoded rather than implied.
- Add Persistence.stop(): unwires onRecord/onClear, closes the controller,
  and resets 'started' so a later start() works. Retains the store it wired.
- @volatile on both seam properties (kotlin.concurrent.Volatile, so it
  applies on Native as well as the JVM). They are written at process start
  and read on the capture path.

Left deliberately: the dropped-batch report stays a println, since the
project has no logging facility and a debug-only recorder does not justify
inventing one. Both simplifications carry ponytail: comments naming the
ceiling.

Not unit-tested, and the docblock says why: start() builds a real driver,
and DriverFactory.create() on Android needs the ContentProvider-installed
Context that a JVM unit test cannot supply. The alternative was Robolectric
or a controller-injection seam, neither justified by straight-line wiring.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhi6Qx8rdo1aX9EnmtEJS

* chore: refresh :sharingan-db API dump for clear() (#50)

The BCV gate added in 87f9e71 caught the ABI widening from the clear()
work: PersistenceController.clear() (called cross-module by :sharingan)
and the SQLDelight-generated deleteAllEvents(). Both are intended.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhi6Qx8rdo1aX9EnmtEJS

* style: conform to the ktlint comments policy from develop (#50)

develop gained a ktlint ruleset (#58) after this branch forked: standard
formatting rules plus a custom no-multi-line-comment rule. Apply
ktlintFormat and condense every multi-line // run this branch introduced
into a single line, per the AGENTS.md comments policy. KDoc is exempt, so
the Persistence lifecycle docblock is unchanged.

Also applies the ktlint plugin to :sharingan-db. The module was created on
this branch before the ruleset existed, so it was escaping the lint gate
the same way it was escaping the BCV gate — adding it surfaced eight
violations that were previously invisible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UBhi6Qx8rdo1aX9EnmtEJS

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant