fix reposting on feeds
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
# Development Workflow & GitHub
|
||||
|
||||
## Attribution Policy
|
||||
|
||||
**NEVER** attribute any commit, PR, code, or work to Claude, AI, or any automated tool.
|
||||
- No `Co-Authored-By` lines mentioning Claude or AI
|
||||
- No "Generated by AI" or similar notes in PR descriptions
|
||||
- No references to AI assistance in commit messages, comments, or code
|
||||
- All work is attributed solely to the developer
|
||||
|
||||
## Branch Strategy
|
||||
|
||||
Every feature, fix, or change gets its own branch off `main`:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git checkout -b <branch-name>
|
||||
```
|
||||
|
||||
Branch naming:
|
||||
- `feat/<short-description>` — new features
|
||||
- `fix/<short-description>` — bug fixes
|
||||
- `refactor/<short-description>` — refactoring
|
||||
- `docs/<short-description>` — documentation changes
|
||||
|
||||
## Commits
|
||||
|
||||
Commits must be **focused and atomic** — one logical change per commit.
|
||||
|
||||
Commit message format:
|
||||
```
|
||||
<type>: <concise summary in imperative mood>
|
||||
|
||||
<optional body explaining why, not what>
|
||||
```
|
||||
|
||||
Types: `feat`, `fix`, `refactor`, `docs`, `chore`, `perf`
|
||||
|
||||
Rules:
|
||||
- Keep the summary line under 72 characters
|
||||
- Use imperative mood ("add filter" not "added filter")
|
||||
- The body explains *why* the change was made when it's not obvious
|
||||
- Do not bundle unrelated changes
|
||||
- Do not include `Co-Authored-By` lines
|
||||
|
||||
## Pull Requests
|
||||
|
||||
PRs are reviewed by experienced developers. Quality matters.
|
||||
|
||||
### Creating a PR
|
||||
|
||||
```bash
|
||||
git push -u origin <branch-name>
|
||||
gh pr create --title "<type>: <summary>" --body "$(cat <<'EOF'
|
||||
## Summary
|
||||
<1-3 bullet points explaining what and why>
|
||||
|
||||
## Changes
|
||||
<list of specific changes made>
|
||||
|
||||
## Testing
|
||||
<how this was tested>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
### PR Standards
|
||||
|
||||
- **Small and focused** — one concern per PR. If a PR touches unrelated areas, split it.
|
||||
- **Clear title** — follows the same `<type>: <summary>` format as commits
|
||||
- **Descriptive body** — reviewers should understand the change without reading every line of code
|
||||
- **No noise** — don't include formatting-only changes, unrelated refactors, or generated files unless that's the point of the PR
|
||||
|
||||
### Merging
|
||||
|
||||
After the user approves:
|
||||
|
||||
```bash
|
||||
gh pr merge <pr-number> --squash --delete-branch
|
||||
```
|
||||
|
||||
Use `--squash` for feature branches to keep main history clean. Use `--merge` only when individual commits are meaningful.
|
||||
|
||||
After merging, update local main:
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
## Workflow Checklist
|
||||
|
||||
1. Create branch from up-to-date `main`
|
||||
2. Make focused changes
|
||||
3. Commit with clear message (no AI attribution)
|
||||
4. Push branch and create PR
|
||||
5. Address review feedback if any
|
||||
6. Merge when approved (squash by default)
|
||||
7. Clean up: switch to main, pull, delete local branch
|
||||
|
||||
## What NOT to Do
|
||||
|
||||
- Do not push directly to `main`
|
||||
- Do not create PRs with dozens of unrelated changes
|
||||
- Do not reference AI, Claude, or automated tools in any git metadata
|
||||
- Do not amend published commits without explicit instruction
|
||||
- Do not force-push without explicit instruction
|
||||
- Do not include generated binary files (APKs, .dm files) in PRs unless specifically requested
|
||||
@@ -0,0 +1,76 @@
|
||||
kotlin version: 2.0.21
|
||||
error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing /home/barry/dev/wisp/app/src/main/kotlin/com/wisp/app/MainActivity.kt:19:5: java.lang.IllegalArgumentException: source must not be null
|
||||
at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
|
||||
at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
|
||||
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
|
||||
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
|
||||
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
|
||||
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
|
||||
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
|
||||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
|
||||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
|
||||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
|
||||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
|
||||
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
|
||||
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
|
||||
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
|
||||
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
|
||||
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
|
||||
at java.base/java.lang.Thread.run(Unknown Source)
|
||||
Caused by: java.lang.IllegalArgumentException: source must not be null
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
|
||||
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
|
||||
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
|
||||
... 33 more
|
||||
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
kotlin version: 2.0.21
|
||||
error message: org.jetbrains.kotlin.util.FileAnalysisException: While analysing /home/barry/dev/wisp/app/src/main/kotlin/com/wisp/app/MainActivity.kt:19:5: java.lang.IllegalArgumentException: source must not be null
|
||||
at org.jetbrains.kotlin.util.AnalysisExceptionsKt.wrapIntoFileAnalysisExceptionIfNeeded(AnalysisExceptions.kt:57)
|
||||
at org.jetbrains.kotlin.fir.FirCliExceptionHandler.handleExceptionOnFileAnalysis(Utils.kt:249)
|
||||
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:46)
|
||||
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.resolveAndCheckFir(firUtils.kt:77)
|
||||
at org.jetbrains.kotlin.fir.pipeline.FirUtilsKt.buildResolveAndCheckFirViaLightTree(firUtils.kt:88)
|
||||
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModuleToAnalyzedFir(jvmCompilerPipeline.kt:319)
|
||||
at org.jetbrains.kotlin.cli.jvm.compiler.pipeline.JvmCompilerPipelineKt.compileModulesUsingFrontendIrAndLightTree(jvmCompilerPipeline.kt:118)
|
||||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:148)
|
||||
at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:43)
|
||||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:103)
|
||||
at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:49)
|
||||
at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:464)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:73)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:506)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:423)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:301)
|
||||
at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:129)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:675)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:92)
|
||||
at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1660)
|
||||
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
|
||||
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
|
||||
at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.Transport$1.run(Unknown Source)
|
||||
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.Transport.serviceCall(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(Unknown Source)
|
||||
at java.base/java.security.AccessController.doPrivileged(Unknown Source)
|
||||
at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(Unknown Source)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)
|
||||
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
|
||||
at java.base/java.lang.Thread.run(Unknown Source)
|
||||
Caused by: java.lang.IllegalArgumentException: source must not be null
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.requireNotNull(KtDiagnosticReportHelpers.kt:68)
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn(KtDiagnosticReportHelpers.kt:39)
|
||||
at org.jetbrains.kotlin.diagnostics.KtDiagnosticReportHelpersKt.reportOn$default(KtDiagnosticReportHelpers.kt:31)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkSourceElement(FirIncompatibleClassExpressionChecker.kt:50)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.expression.FirIncompatibleClassExpressionChecker.checkType$checkers(FirIncompatibleClassExpressionChecker.kt:42)
|
||||
at org.jetbrains.kotlin.fir.analysis.checkers.type.FirIncompatibleClassTypeChecker.check(FirIncompatibleClassTypeChecker.kt:17)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.check(TypeCheckersDiagnosticComponent.kt:81)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:53)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.components.TypeCheckersDiagnosticComponent.visitResolvedTypeRef(TypeCheckersDiagnosticComponent.kt:19)
|
||||
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.CheckerRunningDiagnosticCollectorVisitor.checkElement(CheckerRunningDiagnosticCollectorVisitor.kt:24)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:248)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitResolvedTypeRef(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.types.FirResolvedTypeRef.accept(FirResolvedTypeRef.kt:28)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirSimpleFunctionImpl.acceptChildren(FirSimpleFunctionImpl.kt:63)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:118)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitSimpleFunction(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirSimpleFunction.accept(FirSimpleFunction.kt:51)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirRegularClassImpl.acceptChildren(FirRegularClassImpl.kt:63)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitWithDeclarationAndReceiver(AbstractDiagnosticCollectorVisitor.kt:311)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitClassAndChildren(AbstractDiagnosticCollectorVisitor.kt:87)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:92)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitRegularClass(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirRegularClass.accept(FirRegularClass.kt:48)
|
||||
at org.jetbrains.kotlin.fir.declarations.impl.FirFileImpl.acceptChildren(FirFileImpl.kt:57)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitNestedElements(AbstractDiagnosticCollectorVisitor.kt:38)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:1151)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollectorVisitor.visitFile(AbstractDiagnosticCollectorVisitor.kt:30)
|
||||
at org.jetbrains.kotlin.fir.declarations.FirFile.accept(FirFile.kt:42)
|
||||
at org.jetbrains.kotlin.fir.analysis.collectors.AbstractDiagnosticCollector.collectDiagnostics(AbstractDiagnosticCollector.kt:36)
|
||||
at org.jetbrains.kotlin.fir.pipeline.AnalyseKt.runCheckers(analyse.kt:34)
|
||||
... 33 more
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Build Commands
|
||||
|
||||
```bash
|
||||
./gradlew assembleDebug # Build debug APK
|
||||
./gradlew assembleRelease # Build release APK (minified with R8)
|
||||
./gradlew installDebug # Build and install on connected device/emulator
|
||||
./gradlew clean # Clean build artifacts
|
||||
```
|
||||
|
||||
No test suite exists yet. JDK 17 and Android SDK 35 are required.
|
||||
|
||||
## Architecture
|
||||
|
||||
Wisp is a minimal Android Nostr client using Kotlin + Jetpack Compose (Material 3). MVVM with five layers:
|
||||
|
||||
**UI** (`ui/screen/`, `ui/component/`) → **ViewModel** (`viewmodel/`) → **Repository** (`repo/`) → **Protocol** (`nostr/`) → **Relay** (`relay/`)
|
||||
|
||||
All source lives under `app/src/main/kotlin/com/wisp/app/`.
|
||||
|
||||
### Key Design Decisions
|
||||
|
||||
- **No database** — all state in-memory (LRU caches) or SharedPreferences/EncryptedSharedPreferences. Events re-fetched from relays each session.
|
||||
- **Flow-based reactivity** — SharedFlow for relay events, StateFlow for UI state. No RxJava or LiveData.
|
||||
- **NIP objects** — each NIP implemented as a Kotlin `object` with static helpers (e.g., `Nip17.createGiftWrap()`). New NIPs go in `NipXX.kt`.
|
||||
- **Outbox/inbox relay model** — `OutboxRouter` routes queries to author write relays and delivers to recipient read relays based on NIP-65.
|
||||
- **Relay pool** — `RelayPool` manages persistent connections (max 30) and ephemeral connections (max 50) with automatic cleanup and cooldowns.
|
||||
- **Encrypted key storage** — private keys in EncryptedSharedPreferences (AES256-GCM), never plain SharedPreferences.
|
||||
|
||||
### Protocol Layer (`nostr/`)
|
||||
|
||||
Each NIP is a standalone Kotlin `object`:
|
||||
- Events created via `NostrEvent.create(privkey, pubkey, kind, content, tags)`
|
||||
- Hex encoding: `ByteArray.toHex()` / `String.hexToByteArray()` (extensions in Event.kt)
|
||||
- Condensed NIP reference docs at `.claude/nips/*.md` with index at `.claude/nips/README.md`
|
||||
|
||||
### Relay Layer (`relay/`)
|
||||
|
||||
- `Relay` — single WebSocket connection via OkHttp
|
||||
- `RelayPool` — connection pooling with persistent/ephemeral split
|
||||
- `OutboxRouter` — outbox/inbox routing per NIP-65
|
||||
- `RelayScoreBoard` — tracks relay reliability and author coverage
|
||||
- `SubscriptionManager` — REQ subscription lifecycle
|
||||
|
||||
### Repository Layer (`repo/`)
|
||||
|
||||
- `EventRepository` — LRU cache (5,000 events), profile parsing, reaction/repost/zap tracking
|
||||
- `ContactRepository` — follow list with SharedPreferences persistence
|
||||
- `KeyRepository` — EncryptedSharedPreferences for private keys
|
||||
- `DmRepository` — conversation caching with ECDH key cache
|
||||
|
||||
## Code Conventions
|
||||
|
||||
- Kotlin with Jetpack Compose — no XML layouts
|
||||
- `Dispatchers.Default` for CPU-bound work, `Dispatchers.IO` for network
|
||||
- `StateFlow` for UI state, `SharedFlow` for relay events
|
||||
- Default relays: `wss://relay.damus.io`, `wss://relay.primal.net`
|
||||
- Navigation routes defined in `Navigation.kt`
|
||||
|
||||
## Crypto Stack
|
||||
|
||||
- **Signing**: secp256k1-kmp (Schnorr) with JNI Android bindings
|
||||
- **NIP-44 encryption**: ECDH + HKDF + XChaCha20 + HMAC-SHA256 (Bouncy Castle)
|
||||
- **Key storage**: Android Security Crypto (AES256-GCM)
|
||||
|
||||
## ProGuard / R8
|
||||
|
||||
Release builds use R8 minification. Keep rules in `app/proguard-rules.pro` cover kotlinx.serialization, secp256k1 JNI, Bouncy Castle, OkHttp, Coil, Security Crypto, Media3, and ZXing.
|
||||
@@ -59,6 +59,8 @@ class EventRepository(val profileRepo: ProfileRepository? = null, val muteRepo:
|
||||
|
||||
// Repost tracking: inner event id -> set of reposter pubkeys
|
||||
private val repostAuthors = LruCache<String, MutableSet<String>>(5000)
|
||||
// Feed sort time override: eventId -> effective sort timestamp (e.g. repost time)
|
||||
private val feedSortTime = LruCache<String, Long>(5000)
|
||||
// Track which events the current user has reposted: eventId -> true
|
||||
private val userReposts = LruCache<String, Boolean>(5000)
|
||||
private val _repostVersion = MutableStateFlow(0)
|
||||
@@ -197,7 +199,10 @@ class EventRepository(val profileRepo: ProfileRepository? = null, val muteRepo:
|
||||
if (seenEventIds.add(inner.id)) {
|
||||
eventCache.put(inner.id, inner)
|
||||
val isReply = inner.tags.any { it.size >= 2 && it[0] == "e" }
|
||||
if (!isReply) binaryInsert(inner)
|
||||
if (!isReply) {
|
||||
feedSortTime.put(inner.id, event.created_at)
|
||||
binaryInsert(inner, sortTime = event.created_at)
|
||||
}
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
}
|
||||
@@ -277,14 +282,17 @@ class EventRepository(val profileRepo: ProfileRepository? = null, val muteRepo:
|
||||
markVersionDirty()
|
||||
}
|
||||
|
||||
private fun binaryInsert(event: NostrEvent) {
|
||||
private fun effectiveSortTime(event: NostrEvent): Long =
|
||||
feedSortTime.get(event.id) ?: event.created_at
|
||||
|
||||
private fun binaryInsert(event: NostrEvent, sortTime: Long = event.created_at) {
|
||||
synchronized(feedList) {
|
||||
if (!feedIds.add(event.id)) return // already in feed
|
||||
var low = 0
|
||||
var high = feedList.size
|
||||
while (low < high) {
|
||||
val mid = (low + high) / 2
|
||||
if (feedList[mid].created_at > event.created_at) low = mid + 1 else high = mid
|
||||
if (effectiveSortTime(feedList[mid]) > sortTime) low = mid + 1 else high = mid
|
||||
}
|
||||
feedList.add(low, event)
|
||||
}
|
||||
@@ -538,8 +546,8 @@ class EventRepository(val profileRepo: ProfileRepository? = null, val muteRepo:
|
||||
// Return oldest from the filtered feed so loadMore pages correctly
|
||||
val filter = _authorFilter.value
|
||||
return synchronized(feedList) {
|
||||
if (filter == null) feedList.lastOrNull()?.created_at
|
||||
else feedList.lastOrNull { it.pubkey in filter || isRepostedByAny(it.id, filter) }?.created_at
|
||||
if (filter == null) feedList.lastOrNull()?.let { effectiveSortTime(it) }
|
||||
else feedList.lastOrNull { it.pubkey in filter || isRepostedByAny(it.id, filter) }?.let { effectiveSortTime(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,6 +598,7 @@ class EventRepository(val profileRepo: ProfileRepository? = null, val muteRepo:
|
||||
}
|
||||
eventCache.evictAll()
|
||||
seenEventIds.clear()
|
||||
feedSortTime.evictAll()
|
||||
_feed.value = emptyList()
|
||||
_newNoteCount.value = 0
|
||||
countNewNotes = false
|
||||
|
||||
Reference in New Issue
Block a user