Wave 2: data layer (C) + metadata/scanning (D)
Room entities/DAOs/DB, PocketBase Retrofit client + auth interceptor, SyncEngine (push-then-pull, LWW, tombstones, client-generated ids), SettingsStore, AppContainer. Open Library + Google Books merge, ISBN validation, CameraX + ML Kit scanner plumbing. Verified by orchestrator: assembleDebug exit 0; testDebugUnitTest exit 0, 68 tests, 0 failures, 0 errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bThmkmyUUdqQpy3MXFFe5
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
You are Worker C on the Bookshelf project (~/bookshelf). Implement the DATA LAYER.
|
||||
|
||||
FIRST, READ THESE — they are the contract, follow them exactly, do not invent
|
||||
alternative names or restate them back to me:
|
||||
~/bookshelf/docs/SPEC.md (authoritative product/technical spec)
|
||||
~/bookshelf/docs/HANDOFF.md (operational state and gotchas already paid for)
|
||||
|
||||
## Your scope — these packages ONLY, under app/app/src/main/java/org/modg/bookshelf/
|
||||
data.local Room: BookEntity/BookcaseEntity/ShelfEntity, SyncState, Converters,
|
||||
BookDao/BookcaseDao/ShelfDao, BookshelfDatabase
|
||||
data.remote PocketBaseApi (Retrofit), request/response DTOs, PbAuthInterceptor
|
||||
data.repo BookRepository, LocationRepository, AuthRepository, SyncEngine
|
||||
data.prefs SettingsStore (DataStore: server URL, auth token, per-collection
|
||||
sync cursors, last-sync time)
|
||||
Plus `AppContainer` (manual DI, per SPEC "NO Hilt/kapt") and its wiring into
|
||||
the existing BookshelfApplication.kt.
|
||||
|
||||
## HARD BOUNDARIES — you share this repo with Worker D, running right now
|
||||
- DO NOT create or edit anything under `data.metadata`, `ui.scan`, or any `ui.*`
|
||||
package. Those are Worker D's / wave 3's. Touching them WILL cause a conflict.
|
||||
- DO NOT edit `app/build.gradle.kts` or `gradle/libs.versions.toml`. Every
|
||||
dependency you need (room+ksp, retrofit, kotlinx-serialization, okhttp,
|
||||
datastore, work-runtime, robolectric, coroutines-test) is ALREADY declared and
|
||||
wired. If you genuinely believe something is missing, DO NOT add it — say so in
|
||||
your final report and work around it.
|
||||
- Worker D will need metadata lookup reachable from AppContainer. Do NOT try to
|
||||
wire it. Just leave AppContainer easy to extend; D exposes a plain class that
|
||||
gets wired later.
|
||||
|
||||
## Build/verify — CRITICAL
|
||||
Never run `./gradlew` directly; a second worker builds concurrently and you will
|
||||
corrupt each other's build. ALWAYS build with the serialized wrapper:
|
||||
~/bookshelf/tasks/gw assembleDebug
|
||||
~/bookshelf/tasks/gw testDebugUnitTest
|
||||
It takes the lock and may block until the other worker's build finishes. That is
|
||||
expected — wait for it, do not bypass it.
|
||||
|
||||
## Definition of done — all must actually pass, verified by you, not assumed
|
||||
1. `~/bookshelf/tasks/gw assembleDebug` exits 0.
|
||||
2. `~/bookshelf/tasks/gw testDebugUnitTest` exits 0.
|
||||
3. Real unit tests with real assertions (SPEC: "Do not write assertion-free tests"):
|
||||
- SyncEngine conflict resolution / last-write-wins on `updated`
|
||||
- push ordering + syncState transitions, incl. 404-on-update -> drop local,
|
||||
409/duplicate-id -> switch to PATCH
|
||||
- client-side 15-char lowercase-alnum id generation
|
||||
- DAO queries via Robolectric, proving `deleted = 0` filtering works
|
||||
4. Soft delete everywhere. All reads come from Room. Nothing blocks on network.
|
||||
|
||||
## Report back (keep it short — it is read by a token-constrained orchestrator)
|
||||
- exact pass/fail of the two gradle commands above
|
||||
- files created, one line each
|
||||
- anything in SPEC.md you could NOT satisfy, and why. Do not paper over gaps:
|
||||
a truthfully reported gap is worth more than a false green.
|
||||
@@ -0,0 +1,32 @@
|
||||
Your wave-2 work is mostly good and `assembleDebug` passes. But you ended your turn
|
||||
reporting that `testDebugUnitTest` was "running in background, will report once it
|
||||
completes" — you never confirmed it. The orchestrator ran it. IT FAILS.
|
||||
|
||||
68 tests ran, 2 failed, both yours, both in
|
||||
`app/app/src/test/java/org/modg/bookshelf/ui/scan/ScannerControllerTest.kt`:
|
||||
|
||||
ScannerControllerTest > "valid barcode emits on scanResults"
|
||||
ScannerControllerTest > "invalid barcode does not reach scanResults"
|
||||
both: kotlinx.coroutines.test.UncompletedCoroutinesError:
|
||||
After waiting for 1m, the test body did not run to completion
|
||||
|
||||
Diagnosis (confirm it yourself before acting): `ScannerController._scanResults` is a
|
||||
`MutableSharedFlow<String>(extraBufferCapacity = 1)` with NO `replay`. Both tests call
|
||||
`onBarcodeScanned(...)` BEFORE anything subscribes, so with replay=0 the emission goes
|
||||
nowhere, and the later `scanResults.first()` suspends forever until runTest's timeout.
|
||||
|
||||
Fix the TEST, not the production semantics, unless you have a concrete reason to do
|
||||
otherwise. replay=0 is correct for a barcode scanner — a newly-attached collector must
|
||||
not receive a stale scan from earlier. So make the test subscribe BEFORE emitting, e.g.
|
||||
start the collector with `async`/`backgroundScope`, use `runCurrent()` to let it
|
||||
subscribe, then call `onBarcodeScanned(...)`. Keep both tests' original intent intact:
|
||||
the second one must still prove the invalid barcode is filtered out and only the valid
|
||||
ISBN arrives. Do not weaken a test into an assertion-free or trivially-true test, and
|
||||
do not delete a test to make the suite green.
|
||||
|
||||
Then VERIFY, and this time actually wait for the result before you answer:
|
||||
~/bookshelf/tasks/gw testDebugUnitTest
|
||||
(use that wrapper, never ./gradlew directly). It must exit 0 with 0 failures.
|
||||
|
||||
Reply with: the command's real exit code, the failure count, and one line on what you
|
||||
changed. Nothing else.
|
||||
@@ -0,0 +1,67 @@
|
||||
You are Worker D on the Bookshelf project (~/bookshelf). Implement BOOK METADATA
|
||||
LOOKUP + BARCODE SCANNING.
|
||||
|
||||
FIRST, READ THESE — they are the contract, follow them exactly, do not invent
|
||||
alternative names or restate them back to me:
|
||||
~/bookshelf/docs/SPEC.md (authoritative product/technical spec — see the
|
||||
"Book metadata lookup" and "Barcode scanning"
|
||||
sections especially)
|
||||
~/bookshelf/docs/HANDOFF.md (operational state and gotchas already paid for)
|
||||
|
||||
## Your scope — these packages ONLY, under app/app/src/main/java/org/modg/bookshelf/
|
||||
data.metadata
|
||||
- IsbnUtils: ISBN-13 checksum validation, ISBN-10 -> 13 conversion,
|
||||
normalization (strip hyphens/spaces, handle trailing X)
|
||||
- OpenLibraryClient and GoogleBooksClient (Retrofit or OkHttp + kotlinx-
|
||||
serialization; endpoints are in SPEC)
|
||||
- BookMetadata (source-agnostic result model) and MetadataMerger implementing
|
||||
SPEC's merge rule: prefer whichever has a title, fill blanks from the other,
|
||||
return null if both miss
|
||||
- MetadataRepository: the single entry point, `suspend fun lookup(isbn): BookMetadata?`
|
||||
ui.scan — SCANNER PLUMBING ONLY, no finished screen (wave 3 builds the screen):
|
||||
- a CameraX ImageAnalysis analyzer wrapping ML Kit BarcodeScanning
|
||||
(EAN_13, EAN_8, UPC_A), validating the ISBN-13 checksum before emitting,
|
||||
debouncing repeat reads of the same code
|
||||
- a small stateful holder exposing scan results as a Flow, plus torch toggle
|
||||
and camera-permission-denied states
|
||||
|
||||
## HARD BOUNDARIES — you share this repo with Worker C, running right now
|
||||
- DO NOT create or edit anything under `data.local`, `data.remote`, `data.repo`,
|
||||
`data.prefs`, `AppContainer`, or `BookshelfApplication.kt`. Those are Worker C's.
|
||||
Touching them WILL cause a conflict.
|
||||
- DO NOT edit any `ui.theme` or `ui.components` file — wave 1B finished those and
|
||||
they are verified green. Reuse them; do not modify them.
|
||||
- DO NOT edit `app/build.gradle.kts` or `gradle/libs.versions.toml`. Everything
|
||||
you need (camerax core/camera2/lifecycle/view, mlkit barcode-scanning, retrofit,
|
||||
kotlinx-serialization, okhttp, coroutines-test, robolectric) is ALREADY declared
|
||||
and wired. If you think something is missing, DO NOT add it — report it instead.
|
||||
- Your MetadataRepository must be a plain class with an explicit constructor
|
||||
(e.g. taking OkHttpClient/Json). Do NOT wire it into AppContainer — Worker C owns
|
||||
that file. In your final report, give the exact one-line wiring snippet needed.
|
||||
|
||||
## Build/verify — CRITICAL
|
||||
Never run `./gradlew` directly; a second worker builds concurrently and you will
|
||||
corrupt each other's build. ALWAYS build with the serialized wrapper:
|
||||
~/bookshelf/tasks/gw assembleDebug
|
||||
~/bookshelf/tasks/gw testDebugUnitTest
|
||||
It takes the lock and may block until the other worker's build finishes. That is
|
||||
expected — wait for it, do not bypass it.
|
||||
|
||||
## Definition of done — all must actually pass, verified by you, not assumed
|
||||
1. `~/bookshelf/tasks/gw assembleDebug` exits 0.
|
||||
2. `~/bookshelf/tasks/gw testDebugUnitTest` exits 0.
|
||||
3. Real unit tests with real assertions (SPEC: "Do not write assertion-free tests"):
|
||||
- ISBN-13 checksum: known-valid and known-invalid ISBNs, ISBN-10 conversion,
|
||||
hyphen/space handling
|
||||
- MetadataMerger: OL-only, GB-only, both, neither(-> null), and blank-filling
|
||||
- client JSON parsing against CHECKED-IN SAMPLE JSON FIXTURES, not live network.
|
||||
Tests must pass offline with no network access.
|
||||
4. Network code must never be called on the main thread and must fail soft
|
||||
(return null / empty) rather than throw on timeout or malformed JSON.
|
||||
|
||||
## Report back (keep it short — it is read by a token-constrained orchestrator)
|
||||
- exact pass/fail of the two gradle commands above
|
||||
- files created, one line each
|
||||
- the one-line AppContainer wiring snippet for MetadataRepository
|
||||
- anything in SPEC.md you could NOT satisfy, and why. Do not paper over gaps:
|
||||
a truthfully reported gap is worth more than a false green.
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
# Serialized gradle wrapper. Two workers share one Gradle project dir; concurrent
|
||||
# builds clobber each other's outputs. Always build via this, never ./gradlew.
|
||||
export JAVA_HOME="$HOME/toolchain/jdk21"
|
||||
export ANDROID_HOME="$HOME/toolchain/android-sdk"
|
||||
export ANDROID_SDK_ROOT="$ANDROID_HOME"
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
cd "$HOME/bookshelf/app" || exit 1
|
||||
exec flock "$HOME/bookshelf/.gradle-build.lock" ./gradlew "$@"
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
# run-resume.sh <task-name> <prompt-file>
|
||||
# Like run-task.sh, but resumes an EXISTING session with a SPECIFIC prompt
|
||||
# (run-task.sh sends only a generic "continue" message on resume).
|
||||
set -u
|
||||
NAME="$1"; PROMPT_FILE="$2"
|
||||
L="$HOME/bookshelf/logs"; LOG="$L/${NAME}.json"; ERR="$L/${NAME}.err"
|
||||
SIDF="$L/${NAME}.sid"; ST="$L/${NAME}.state"
|
||||
MAX_WALL="${MAX_WALL:-86400}"; POLL="${POLL:-600}"; MAX_HARD_FAILS="${MAX_HARD_FAILS:-3}"
|
||||
export JAVA_HOME="$HOME/toolchain/jdk21"
|
||||
export ANDROID_HOME="$HOME/toolchain/android-sdk"; export ANDROID_SDK_ROOT="$ANDROID_HOME"
|
||||
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$PATH"
|
||||
cd "$HOME/bookshelf" || exit 1
|
||||
SID="$(cat "$SIDF")"
|
||||
say(){ echo "[$(date -Is)] $NAME: $*" >> "$ST"; }
|
||||
say "RESUME-with-prompt sid=$SID file=$PROMPT_FILE"
|
||||
deadline=$(( $(date +%s) + MAX_WALL )); attempt=0; hard=0; qw=0
|
||||
while [ "$(date +%s)" -lt "$deadline" ]; do
|
||||
attempt=$((attempt+1)); say "attempt $attempt: resume $SID"
|
||||
claude -p --model sonnet --permission-mode bypassPermissions \
|
||||
--output-format json --add-dir "$HOME/bookshelf" --resume "$SID" \
|
||||
< "$PROMPT_FILE" > "$LOG" 2>"$ERR"
|
||||
rc=$?
|
||||
blob="$(cat "$LOG" "$ERR" 2>/dev/null | head -c 20000)"
|
||||
if printf '%s' "$blob" | grep -qiE 'usage limit|limit will reset|limit resets|rate_limit_error|rate limit exceeded|429|too many requests|overloaded_error'; then
|
||||
qw=$((qw+1)); say "QUOTA hit (wait #$qw), sleeping ${POLL}s"; sleep "$POLL"; continue; fi
|
||||
isErr="$(jq -r '.is_error // false' "$LOG" 2>/dev/null)"
|
||||
if [ "$rc" -eq 0 ] && [ "$isErr" != "true" ]; then say "SUCCESS after $attempt attempt(s)"; break; fi
|
||||
hard=$((hard+1)); say "hard failure #$hard (rc=$rc)"
|
||||
[ "$hard" -ge "$MAX_HARD_FAILS" ] && { say "GIVING UP"; break; }
|
||||
sleep 60
|
||||
done
|
||||
{ echo "=== $NAME attempts=$attempt quota_waits=$qw hard_fails=$hard ==="
|
||||
jq -r '"cost=$" + ((.total_cost_usd//0)|tostring) + " turns=" + ((.num_turns//0)|tostring)' "$LOG" 2>/dev/null
|
||||
echo "--- result ---"; jq -r '.result // "no result"' "$LOG" 2>/dev/null | tail -c 1500
|
||||
} > "$L/${NAME}.summary"
|
||||
Reference in New Issue
Block a user