feat(docs): add production readiness runbook and enhanced TODO scan

Add comprehensive production readiness checklist and improve TODO scanning.

Changes:
- Production Readiness Runbook (docs/progress/PRODUCTION-READINESS-RUNBOOK.md)
  - Complete mechanical execution checklist for TypeScript, Android, iOS
  - File anchors and search commands for verification
  - Cross-platform behavior consistency checks
  - Logging and observability requirements
  - Release packaging sanity checks
  - Troubleshooting guide
  - Quick reference for expected file anchors
- Enhanced TODO Scan Script (scripts/todo-scan.js)
  - Split reporting: core code vs docs/test-apps
  - Core code count (should be 0)
  - Docs/test-apps count (expected to be large)
  - Enhanced JSON output with summary statistics
  - Improved console output with visual indicators
  - Clear separation of production code vs planning artifacts

Implementation Details:
- Core code detection: ios/Plugin/, android/src/main/, src/, packages/, lib/
- Docs/test-apps detection: docs/, test-apps/, tests/, *Tests/
- JSON output includes summary with coreCount, docsTestCount, otherCount
- Markdown output includes summary section with split counts
- Console output shows visual indicators (/⚠️) for quick assessment

Benefits:
- Clear visibility into production code TODOs (should be 0)
- Acceptable TODOs in docs/test-apps are clearly separated
- Production readiness checklist provides deterministic verification
- File anchors enable quick verification of implementation completeness

Verification:
- TODO scan runs successfully
- JSON output includes summary statistics
- Markdown output includes split summary
- Console output shows visual indicators
This commit is contained in:
Matthew Raymer
2025-12-24 08:19:48 +00:00
parent b51a1e4f75
commit cdbe51f46a
4 changed files with 63719 additions and 16519 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,476 @@
# DNP — Production Readiness Execution Checklist (Mechanical)
**Date:** 2025-12-24
**Repo:** `daily-notification-plugin/`
**Goal:** Prove the plugin is "shippable" by running a deterministic sequence of checks across **TypeScript**, **Android**, **iOS**, and **Docs/Drift**.
---
## 0) One-time setup
### 0.1 Confirm you're at repo root
```bash
pwd
ls
```
**✅ Expected to include folders like:**
- `src/`
- `android/`
- `ios/`
- `docs/`
- `scripts/`
### 0.2 Capture current revision (for receipts)
```bash
git rev-parse HEAD
git status --porcelain
```
**Record output into a log note** (or paste into your progress doc).
---
## 1) Repo-wide "truth checks" (fast)
### 1.1 Core code must have **zero TODO markers**
```bash
grep -RIn --exclude-dir=docs --exclude-dir=test-apps --exclude-dir=node_modules --exclude-dir=.git "TODO:" ios android src packages lib scripts tests || true
```
**✅ Pass condition:**
- No matches.
**If any show up:** treat as "production code TODO" and resolve.
### 1.2 Docs/test-apps TODOs are allowed, but must be measurable
```bash
npm run todo:scan
```
**✅ Pass condition:**
- `docs/TODO-CLASSIFICATION.md` and `docs/todo-scan.json` get updated successfully.
**Verify split reporting:**
- Check that `docs/todo-scan.json` includes `coreCount` and `docsCount` fields.
---
## 2) TypeScript layer: contract + build sanity
### 2.1 Unit tests
```bash
npm test
```
**✅ Pass condition:**
- exits 0.
**Expected output:**
```
Test Suites: 8 passed, 8 total
Tests: 115 passed, 115 total
```
### 2.2 Typecheck
```bash
npm run typecheck
```
**✅ Pass condition:**
- exits 0.
**Expected output:**
```
> @timesafari/daily-notification-plugin@1.0.11 typecheck
> tsc --noEmit
```
### 2.3 Lint (if present)
```bash
npm run lint
```
**✅ Pass condition:**
- exits 0 OR the project explicitly does not have a lint script.
---
## 3) Android: build + worker behavior
### 3.1 Compile sanity
```bash
cd android
./gradlew :assembleDebug
cd ..
```
**✅ Pass condition:**
- build succeeds.
**Expected output:**
```
BUILD SUCCESSFUL in Xs
```
### 3.2 Locate the fetch worker entrypoint
**File:**
- `android/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java`
**Search anchors:**
```bash
grep -n "class DailyNotificationFetchWorker" android/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java
grep -n "public Result doWork()" android/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java
grep -n "interface FetchWorkerMetrics" android/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java
grep -n "final class NoopFetchWorkerMetrics" android/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java
grep -n "private boolean isRetryable" android/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java
```
**✅ Pass condition:**
- Those anchors exist.
- `doWork()` increments metrics and records duration on every return path:
- `metrics.incRun()`
- `metrics.observeDurationMs(...)`
- `metrics.incSuccess()` / `metrics.incFailure()` / `metrics.incRetry()`
### 3.3 Rolling window logic must not be stubbed
**File:**
- `android/src/main/java/com/timesafari/dailynotification/DailyNotificationRollingWindow.java`
**Search anchors:**
```bash
grep -n "private int countPendingNotifications()" android/src/main/java/com/timesafari/dailynotification/DailyNotificationRollingWindow.java
grep -n "private int countNotificationsForDate" android/src/main/java/com/timesafari/dailynotification/DailyNotificationRollingWindow.java
grep -n "private List<NotificationContent> getNotificationsForDate" android/src/main/java/com/timesafari/dailynotification/DailyNotificationRollingWindow.java
grep -n "private long\[\] dateBoundsMillis" android/src/main/java/com/timesafari/dailynotification/DailyNotificationRollingWindow.java
```
**✅ Pass condition:**
- none of these return placeholder defaults like `return 0;` / `return new ArrayList<>();` without logic.
**Verify implementation:**
```bash
grep -A 5 "countPendingNotifications()" android/src/main/java/com/timesafari/dailynotification/DailyNotificationRollingWindow.java | head -10
```
Should show actual logic (storage access, date calculations), not just `return 0;`.
### 3.4 Android smoke test (manual, deterministic)
You need a host app (likely under `test-apps/`).
**Procedure:**
1. Install test host app on emulator/device.
2. Use a test UI action "Schedule notification in 2 minutes" (or equivalent).
3. Observe logs.
**Log capture:**
```bash
adb logcat | grep -i "DailyNotification\|dnp\|timesafari"
```
**✅ Pass condition:**
- notification schedules successfully
- pending count increases
- no retry storm (worker shouldn't loop endlessly)
> **Note:** If the test app doesn't expose a "+2 minutes" button, add it: that becomes your permanent "smoke lever."
---
## 4) iOS: build + scheduler behavior
### 4.1 Workspace exists
```bash
ls ios | grep -E "xcworkspace|xcodeproj" || true
```
**✅ Pass condition:**
- you see `DailyNotificationPlugin.xcworkspace` (or equivalent).
### 4.2 iOS build/test sanity
```bash
cd ios
xcodebuild -workspace DailyNotificationPlugin.xcworkspace -scheme DailyNotificationPlugin -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15' test
cd ..
```
**✅ Pass condition:**
- tests pass (or if there are no tests wired, build succeeds).
**Alternative (build only):**
```bash
cd ios
xcodebuild -workspace DailyNotificationPlugin.xcworkspace -scheme DailyNotificationPlugin -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15' build
cd ..
```
### 4.3 Scheduler must enforce TTL + call fetch scheduler hooks
**File:**
- `ios/Plugin/DailyNotificationScheduler.swift`
**Search anchors:**
```bash
grep -n "validateBeforeArming" ios/Plugin/DailyNotificationScheduler.swift
grep -n "protocol DailyNotificationFetchScheduling" ios/Plugin/DailyNotificationScheduler.swift
grep -n "NoopFetcherScheduler" ios/Plugin/DailyNotificationScheduler.swift
grep -n "fetchScheduler.scheduleFetch" ios/Plugin/DailyNotificationScheduler.swift
grep -n "fetchScheduler.scheduleImmediateFetch" ios/Plugin/DailyNotificationScheduler.swift
```
**✅ Pass condition:**
- TTL enforcement is present and returns false when invalid
- the old Phase-2 TODO lines are gone
- fetch scheduling calls are real method calls (even if Noop)
**Verify TTL enforcement:**
```bash
grep -A 10 "validateBeforeArming" ios/Plugin/DailyNotificationScheduler.swift | head -15
```
Should show actual validation logic, not just `return true;`.
### 4.4 SQLite persistence must be real (not stubs)
**File:**
- `ios/Plugin/DailyNotificationDatabase.swift`
**Search anchors:**
```bash
grep -n "func saveNotificationContent" ios/Plugin/DailyNotificationDatabase.swift
grep -n "INSERT OR REPLACE INTO" ios/Plugin/DailyNotificationDatabase.swift
grep -n "func deleteNotificationContent" ios/Plugin/DailyNotificationDatabase.swift
grep -n "func clearAllNotifications" ios/Plugin/DailyNotificationDatabase.swift
```
**✅ Pass condition:**
- SQL is executed for save/delete/clear (no placeholder prints-only).
**Verify SQL execution:**
```bash
grep -A 5 "INSERT OR REPLACE INTO" ios/Plugin/DailyNotificationDatabase.swift
```
Should show actual SQLite3 calls (`sqlite3_exec` or similar), not just `print()`.
### 4.5 Rolling window must use UNUserNotificationCenter pending requests
**File:**
- `ios/Plugin/DailyNotificationRollingWindow.swift`
**Search anchors:**
```bash
grep -n "UNUserNotificationCenter.current().getPendingNotificationRequests" ios/Plugin/DailyNotificationRollingWindow.swift
grep -n "fetchPendingRequestsSync" ios/Plugin/DailyNotificationRollingWindow.swift
grep -n "countPendingNotifications" ios/Plugin/DailyNotificationRollingWindow.swift
grep -n "countNotificationsForDate" ios/Plugin/DailyNotificationRollingWindow.swift
grep -n "getNotificationsForDate" ios/Plugin/DailyNotificationRollingWindow.swift
```
**✅ Pass condition:**
- functions do real work and don't return placeholder constants.
**Verify implementation:**
```bash
grep -A 10 "countPendingNotifications" ios/Plugin/DailyNotificationRollingWindow.swift | head -15
```
Should show actual `UNUserNotificationCenter` calls, not just `return 0;`.
---
## 5) Cross-platform behavior checklist (what must match)
### 5.1 "What is pending?" definition is consistent
**Expected behavior:**
- Android pending count: scheduledTime >= now from storage truth
- iOS pending count: UNNotificationCenter pending request count
**✅ Pass condition:**
- Both counts increase after scheduling a future notification and decrease after delivery/cancel.
**Test procedure:**
1. Schedule notification for +2 minutes on Android
2. Check pending count (should be > 0)
3. Schedule notification for +2 minutes on iOS
4. Check pending count (should be > 0)
5. Cancel all notifications
6. Check pending count (should be 0)
### 5.2 "Count for date" definition is consistent
**Expected behavior:**
- Date is `YYYY-MM-DD` local calendar day
- Android uses date bounds (midnight→midnight)
- iOS uses `nextTriggerDate()` and formats to date string
**✅ Pass condition:**
- scheduling a notification for "tomorrow morning" increments tomorrow's date bucket, not today.
**Test procedure:**
1. Get current date: `date +%Y-%m-%d`
2. Schedule notification for tomorrow 9:00 AM
3. Check count for today (should be unchanged)
4. Check count for tomorrow (should be +1)
### 5.3 TTL behavior is consistent
**Expected behavior:**
- TTL invalid → schedule is skipped (or returns false) and logs explain it.
**✅ Pass condition:**
- both platforms refuse to arm stale content in equivalent circumstances (if TTL logic exists on Android too; if not, document the difference).
**Test procedure:**
1. Create content with `fetchedAt` = 2 days ago
2. Set TTL = 1 day
3. Attempt to schedule
4. Verify schedule fails with TTL error log
---
## 6) Logging + observability receipts (minimal, but mandatory)
### 6.1 Required log lines (choose exact strings and standardize)
Create/confirm a short standard list like:
- `DNP: scheduling notification`
- `DNP: TTL validation failed`
- `DNP: rolling window count pending=`
- `DNP: fetch worker start`
- `DNP: fetch worker success itemsFetched= itemsSaved=`
- `DNP: fetch worker retry reason=`
**✅ Pass condition:**
- You can grep both Android logcat and iOS console for these.
**Verify logging:**
```bash
# Android
adb logcat | grep -i "DNP:"
# iOS (requires device/simulator console)
# Check Xcode console output or device logs
```
### 6.2 Decision logging for failures
When a schedule fails, logs must answer:
- why it failed
- whether it will retry
- what data was rejected (id / slot / date)
**✅ Pass condition:**
- at least one failure path is testable and produces a complete explanation.
**Test procedure:**
1. Attempt to schedule with invalid TTL
2. Check logs for:
- Error reason
- Notification ID
- TTL value
- Scheduled time
---
## 7) Release packaging sanity
### 7.1 Ensure `scripts/todo-scan.js` is executable
```bash
ls -l scripts/todo-scan.js
```
**✅ Pass condition:**
- executable bit set OR npm script runs regardless.
### 7.2 Clean archive recipe (no junk)
```bash
tar czvf daily-notification-plugin-release.tar.gz \
--exclude=.git \
--exclude=node_modules \
--exclude=.venv \
--exclude=dist \
--exclude=build \
--exclude='*.tar.gz' \
daily-notification-plugin/
```
**✅ Pass condition:**
- archive created successfully.
**Verify archive contents:**
```bash
tar tzf daily-notification-plugin-release.tar.gz | head -20
```
Should show source files, not build artifacts.
---
## 8) Stop conditions (fail fast rules)
**Stop and fix before proceeding if any occur:**
- Any TODO marker found in `ios/`, `android/`, `src/` (core code)
- Android `assembleDebug` fails
- iOS `xcodebuild test` fails (unless tests are explicitly not configured, in which case: build must succeed)
- Smoke scheduling fails to deliver a notification in ≤ 3 minutes
---
## 9) Final "ready" declaration (what you can say to yourself)
**You may mark "READY" only if:**
- ✅ TypeScript tests + typecheck pass
- ✅ Android builds
- ✅ iOS builds/tests
- ✅ One Android + one iOS smoke schedule succeeds
- ✅ TTL behavior is verified (at least iOS)
- ✅ todo-scan runs and docs reflect reality
- ✅ Core code has zero TODOs
- ✅ Logging is consistent and grep-able
---
## 10) Quick reference: Expected file anchors
### Android
- `android/src/main/java/com/timesafari/dailynotification/DailyNotificationFetchWorker.java` - Worker with metrics
- `android/src/main/java/com/timesafari/dailynotification/DailyNotificationRollingWindow.java` - Rolling window logic
- `android/src/main/java/com/timesafari/dailynotification/DailyNotificationPlugin.kt` - Main plugin (thin adapter)
### iOS
- `ios/Plugin/DailyNotificationScheduler.swift` - Scheduler with TTL + fetch hooks
- `ios/Plugin/DailyNotificationDatabase.swift` - SQLite persistence
- `ios/Plugin/DailyNotificationRollingWindow.swift` - Rolling window with UNUserNotificationCenter
- `ios/Plugin/DailyNotificationPlugin.swift` - Main plugin (thin adapter)
### TypeScript
- `src/` - Core TypeScript implementation
- `packages/` - Internal packages
- `tests/` - Unit tests
---
## 11) Troubleshooting common issues
### Issue: Android build fails
**Check:**
- Java version: `java -version` (should be 11+)
- Gradle wrapper: `./gradlew --version`
- Android SDK: `echo $ANDROID_HOME`
### Issue: iOS build fails
**Check:**
- Xcode version: `xcodebuild -version`
- Scheme exists: `xcodebuild -list -workspace DailyNotificationPlugin.xcworkspace`
- Simulator available: `xcrun simctl list devices`
### Issue: TODO scan shows core TODOs
**Action:**
1. Run: `npm run todo:scan`
2. Check `docs/todo-scan.json` for `coreCount`
3. If > 0, grep for TODOs in core directories
4. Resolve or move to docs/test-apps
### Issue: Logs not appearing
**Check:**
- Android: `adb logcat -c` (clear) then `adb logcat | grep DNP`
- iOS: Xcode console or device logs
- Verify log tags match expected patterns
---
**Last Updated:** 2025-12-24
**Status:** Active production readiness checklist

File diff suppressed because it is too large Load Diff

View File

@@ -94,6 +94,29 @@ function bucketForPath(rel) {
return "Other";
}
function isCoreCode(rel) {
// Core code directories (production code)
return (
rel.startsWith("ios/Plugin/") ||
rel.startsWith("android/src/main/") ||
rel.startsWith("src/") ||
rel.startsWith("packages/") ||
rel.startsWith("lib/") ||
(rel.startsWith("scripts/") && !rel.includes("test"))
);
}
function isDocsOrTestApp(rel) {
// Documentation and test harness directories
return (
rel.startsWith("docs/") ||
rel.startsWith("test-apps/") ||
rel.startsWith("ios/Tests/") ||
rel.startsWith("android/src/test/") ||
rel.startsWith("tests/")
);
}
function main() {
const results = [];
for (const d of TARGET_DIRS) {
@@ -113,7 +136,27 @@ function main() {
a.line - b.line
);
fs.writeFileSync(path.join(ROOT, "docs/todo-scan.json"), JSON.stringify(results, null, 2), "utf8");
// Split core vs docs/test-apps
const coreResults = results.filter(r => isCoreCode(r.file));
const docsTestResults = results.filter(r => isDocsOrTestApp(r.file));
const otherResults = results.filter(r => !isCoreCode(r.file) && !isDocsOrTestApp(r.file));
// Enhanced JSON output with split counts
const jsonOutput = {
summary: {
total: results.length,
coreCount: coreResults.length,
docsTestCount: docsTestResults.length,
otherCount: otherResults.length,
generatedAt: new Date().toISOString()
},
core: coreResults,
docsTest: docsTestResults,
other: otherResults,
all: results
};
fs.writeFileSync(path.join(ROOT, "docs/todo-scan.json"), JSON.stringify(jsonOutput, null, 2), "utf8");
// markdown
const byBucket = new Map();
@@ -125,7 +168,13 @@ function main() {
let md = "";
md += `# TODO Classification (auto-generated)\n\n`;
md += `Generated by \`scripts/todo-scan.js\`\n\n`;
md += `Total markers: **${results.length}**\n\n`;
md += `## Summary\n\n`;
md += `- **Total markers:** ${results.length}\n`;
md += `- **Core code (production):** ${coreResults.length} ⚠️\n`;
md += `- **Docs/test-apps:** ${docsTestResults.length} ✅ (expected)\n`;
md += `- **Other:** ${otherResults.length}\n\n`;
md += `> **Note:** Core code TODOs should be near zero. Docs/test-app TODOs are expected and acceptable.\n\n`;
md += `---\n\n`;
for (const [bucket, items] of byBucket.entries()) {
md += `## ${bucket} (${items.length})\n\n`;
@@ -146,7 +195,14 @@ function main() {
fs.writeFileSync(path.join(ROOT, "docs/TODO-CLASSIFICATION.md"), md, "utf8");
console.log(`todo-scan complete: ${results.length} markers`);
// Console output with split summary
console.log(`\n📊 TODO Scan Complete`);
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
console.log(`Total markers: ${results.length}`);
console.log(`Core code: ${coreResults.length} ${coreResults.length === 0 ? '✅' : '⚠️ (should be 0)'}`);
console.log(`Docs/test-apps: ${docsTestResults.length} ✅ (expected)`);
console.log(`Other: ${otherResults.length}`);
console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
}
main();