Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8ff07ad
Add local PR review scripts and code review docs
imedwei Mar 17, 2026
3ad9134
Refactor ScreenMetrics init and fix scaledDensity deprecation
imedwei Mar 17, 2026
f7f8e95
Add Boox MyScript handwriting recognition with ML Kit fallback
imedwei Mar 17, 2026
35d60a5
Add finger navigation with layered palm rejection
imedwei Mar 21, 2026
0fec24b
Remove side gutter, add floating menu icon with complementary scroll
imedwei Mar 21, 2026
68b12c7
Add review script improvements: --base flag and integration rebuild
imedwei Mar 21, 2026
a9d4361
Add diagram mode: shapes, connectors, scratch-out, and undo-to-unsnap
imedwei Mar 23, 2026
a9ee43e
Replace gesture-based diagram creation with shape-intent classifier
imedwei Mar 21, 2026
6018ffa
Recognize freehand text in diagrams with adaptive spatial grouping
imedwei Mar 22, 2026
285d699
Add magnetic arrow-to-shape snapping for diagram connectors
imedwei Mar 22, 2026
e3ddb40
Add centroid-based line segmenter, scratch-out intersection guard, an…
imedwei Mar 22, 2026
130fd6c
Add bug report infrastructure with always-on ring buffer
imedwei Mar 22, 2026
684c0f4
Fix false scratch-out on cursive descenders with focused coverage
imedwei Mar 22, 2026
b4b3a22
Add freeform diagram mode with start-dwell gesture and overflow expan…
imedwei Mar 23, 2026
467ad46
Fix oval misclassification with device-stroke regression tests
imedwei Mar 23, 2026
5865529
Use union of raw and snapped bounds for diagram overflow detection
imedwei Mar 23, 2026
374825c
Add diagram stroke classifier to separate text from drawing strokes
imedwei Mar 23, 2026
398e437
Fix drawing contagion: shapes must not trigger contagion on text labels
imedwei Mar 23, 2026
e44570c
Add diagram area shrink/expand on scratch-out with canvas redraw
imedwei Mar 23, 2026
85ebd34
Optimize scratch-out, event log, and proximity grouping performance
imedwei Mar 23, 2026
f6477be
Debounce diagram text recognition to avoid redundant pipeline runs
imedwei Mar 23, 2026
a826b6f
Extract DiagramManager from WritingCoordinator (Issue #18)
imedwei Mar 23, 2026
219a0b3
Extract DisplayManager and LineRecognitionManager from WritingCoordin…
imedwei Mar 23, 2026
3487922
Simplify: deduplicate code, encapsulate state, use StrokeExtensions
imedwei Mar 23, 2026
561b706
Fix false dwell creating unwanted diagram areas for text strokes
imedwei Mar 23, 2026
d90a2e1
Fix scratch-out unable to erase small dots (1-2 point strokes)
imedwei Mar 23, 2026
78ad2cf
Document test-first development workflow in CLAUDE.md
imedwei Mar 23, 2026
dfc6b57
Add tmp/ for debug artifacts, update .gitignore and .gitattributes
imedwei Mar 23, 2026
fd2176e
Add product vision document covering thesis, architecture, and roadmap
imedwei Mar 21, 2026
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
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Test fixture JSON files: treat as binary for cleaner diffs and mark as
# linguist-generated so they don't count toward language statistics.
app/src/androidTest/assets/fixtures/*.json binary linguist-generated
app/src/test/resources/fixtures/*.json binary linguist-generated
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,14 @@ local.properties

# Local environment config (not committed — copy and adapt per machine)
.envrc

# Claude Code local artifacts
.claude/settings.local.json
.claude/reviews/
.claude/plans/

# Codanna local config
.codannaignore

# Temporary debugging artifacts (screenshots, bug reports)
/tmp/
62 changes: 61 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,65 @@ JAVA_HOME="/c/Program Files/Android/Android Studio/jbr" ./gradlew assembleDebug
### Install to tablet

```bash
"/c/Users/Durham/AppData/Local/Android/Sdk/platform-tools/adb.exe" install -r app/build/outputs/apk/debug/app-debug.apk
./gradlew installDebug
```

### Tests

```bash
# Unit tests only (no device required)
./gradlew testDebugUnitTest

# All tests (unit + instrumented, requires connected device)
./gradlew allTests
```

## Development Workflow

Before starting any implementation, review [`docs/VISION.md`](docs/VISION.md) to ensure the new feature or UX aligns with the overall product vision and design principles.

Break implementation tasks into small, independently testable pieces. For each piece, follow this cycle:

1. **Write tests first**: Create unit tests that define the expected behavior before writing any implementation code. Run them to confirm they fail for the right reasons.
2. **Implement**: Write the minimum code to make the tests pass.
3. **Run all tests**: `./gradlew testDebugUnitTest` — ensure no regressions.
4. **Commit**: Commit the piece to the feature/fix/dev branch.

Repeat steps 1-4 for each piece. After all pieces are complete:

5. **Local review**: `REVIEW=$(./scripts/review-pr.sh --local --no-post)` — review the full diff and address all actionable items.
6. **Final test run**: `./gradlew testDebugUnitTest` — confirm everything still passes after review fixes.
7. **Push**: Push to both local and remote feature/fix/dev branch.

## Device Debugging

Screenshots and bug reports are saved to `tmp/` (gitignored).

```bash
# Capture screenshot from device
adb exec-out screencap -p > tmp/screenshot.png

# Find latest bug report path
adb logcat -d | grep "Bug report generated" | tail -1

# Pull bug report (use double quotes to prevent git bash path mangling)
adb exec-out "cat /storage/emulated/0/Android/data/com.writer.dev/files/Documents/bug-reports/bug-report-TIMESTAMP.json" > tmp/bug-report.json
```

The app generates bug reports via the menu (hamburger → Bug Report). Reports contain device info, recent stroke history, processing events, and document state.

## PR Workflow

Before creating a PR, run the self-review cycle locally:

1. **Review local changes**: `REVIEW=$(./scripts/review-pr.sh --local --no-post)` — reviews the branch diff against master (use `--base <branch>` to diff against a different branch), saves to `.claude/reviews/`.
2. **Read the review file** (`cat "$REVIEW"`) and address every actionable item by editing the code and committing.
3. **Verify fixes**: `./scripts/review-check.sh --local --no-post "$REVIEW"` — confirm all items are addressed.
4. If any items remain open, go back to step 2.
5. Once all items are resolved, **create the PR** and post the review:
- `gh pr create --draft` (or `gh pr create` if ready)
- `./scripts/review-pr.sh --post` (runs a fresh review and posts to the PR)
- `./scripts/review-check.sh --post "$REVIEW"` (posts the resolution checklist)
- `gh pr ready` (if created as draft)

The `--local` flag works without a remote or PR. The `--no-post` / `--post` flags make scripts non-interactive for agent use.
87 changes: 87 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ android {
targetSdk = 34
versionCode = 1
versionName = "0.1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments["notAnnotation"] = "com.writer.recognition.DevTool"
}

buildTypes {
Expand All @@ -39,8 +41,21 @@ android {
jvmTarget = "17"
}

testOptions {
unitTests {
isReturnDefaultValues = true
isIncludeAndroidResources = true
all {
it.jvmArgs(
"--add-opens", "java.base/jdk.internal.access=ALL-UNNAMED",
)
}
}
}

buildFeatures {
viewBinding = true
aidl = true
}

packaging {
Expand All @@ -50,6 +65,40 @@ android {
}
}

tasks.register("captureFixture") {
description = "Capture handwriting fixture from device: -PfixtureName=hello -PexpectedText=\"hello\""
dependsOn("installDebug", "installDebugAndroidTest")
doLast {
val name = project.property("fixtureName") as String
val text = project.property("expectedText") as String
val lang = project.findProperty("language") as? String ?: "en-US"
val line = project.findProperty("lineIndex") as? String ?: "0"
val adb = android.adbExecutable.absolutePath
val appId = "com.writer.dev"

exec {
commandLine(adb, "shell", "am", "instrument", "-w",
"-e", "class", "com.writer.recognition.StrokeFixtureCapture",
"-e", "fixtureName", name,
"-e", "expectedText", text,
"-e", "language", lang,
"-e", "lineIndex", line,
"$appId.test/androidx.test.runner.AndroidJUnitRunner")
}

exec {
commandLine(adb, "pull",
"/sdcard/Download/inkup-fixtures/$name.json",
"app/src/androidTest/assets/fixtures/$name.json")
}

exec {
commandLine(adb, "shell", "rm",
"/sdcard/Download/inkup-fixtures/$name.json")
}
}
}

configurations.all {
// Onyx SDK pulls in old pre-AndroidX support libraries that clash with AndroidX
exclude(group = "com.android.support", module = "support-compat")
Expand Down Expand Up @@ -86,4 +135,42 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("com.google.android.material:material:1.12.0")

// Testing
testImplementation("junit:junit:4.13.2")
testImplementation("org.json:json:20231013")
testImplementation("org.robolectric:robolectric:4.14.1")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")

androidTestImplementation("androidx.test:runner:1.6.2")
androidTestImplementation("androidx.test:rules:1.6.1")
androidTestImplementation("androidx.test.ext:junit:1.2.1")
androidTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
}

tasks.withType<Test> {
testLogging {
events(org.gradle.api.tasks.testing.logging.TestLogEvent.FAILED)
showStandardStreams = false
}
addTestListener(object : TestListener {
override fun beforeSuite(suite: TestDescriptor) {}
override fun afterSuite(suite: TestDescriptor, result: TestResult) {
if (suite.parent == null) {
println("${result.resultType}: ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped")
}
}
override fun beforeTest(testDescriptor: TestDescriptor) {}
override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) {}
})
}

tasks.register("allTests") {
description = "Runs all tests: unit tests and instrumented tests on connected device"
group = "verification"
dependsOn("testDebugUnitTest", "connectedDebugAndroidTest")
}
// Run unit tests first — fail fast before slower device tests
tasks.matching { it.name == "connectedDebugAndroidTest" }.configureEach {
mustRunAfter("testDebugUnitTest")
}
Loading
Loading