feat(docs): complete P2.6 type safety cleanup and P2.7 system invariants

P2.6: Type Safety Cleanup
- Replaced 'any' return types in vite-plugin.ts with concrete types (UserConfig, transform return type)
- Documented TypeScript mixin 'any[]' exception in PlatformServiceMixin.ts
- Audit confirmed: zero 'any' in codebase except documented TS mixin limitation
- All external boundaries use 'unknown', all data payloads use 'Record<string, unknown>'

P2.7: System Invariants Documentation
- Created SYSTEM_INVARIANTS.md documenting all 6 enforced invariants
- Added to docs/00-INDEX.md under Policy & Contracts section
- Each invariant includes: What, Why, How, Where

Progress Docs Updates:
- Updated 00-STATUS.md: marked P2.6/P2.7 complete, added type safety invariant note
- Updated 01-CHANGELOG-WORK.md: added 2025-12-22 entries for P2.6/P2.7
- Updated 03-TEST-RUNS.md: added P2.6 type safety audit test run
- Updated P2-DESIGN.md: marked P2.6 acceptance criteria complete
- Updated SYSTEM_INVARIANTS.md: added Type Safety Notes section

Baseline Tag:
- Created v1.0.11-p0-p1.4-p1.5-p2.6-p2.7-complete

TypeScript compilation:  PASSES
Build:  PASSES
CI:  All checks pass
This commit is contained in:
Matthew Raymer
2025-12-22 10:56:00 +00:00
parent 3f15352d8f
commit eb1fc9f220
85 changed files with 5199 additions and 10989 deletions

569
scripts/verify.sh Executable file
View File

@@ -0,0 +1,569 @@
#!/usr/bin/env bash
#
# Daily Notification Plugin - Verification Script
#
# Single entrypoint to validate the project state.
# Used by CI and local development.
#
# @author Matthew Raymer
# @version 1.0.0
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Counters
PASSED=0
FAILED=0
SKIPPED=0
# Logging functions
print_header() {
echo -e "\n${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BLUE}$1${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}\n"
}
print_info() {
echo -e "${BLUE}${NC} $1"
}
print_success() {
echo -e "${GREEN}${NC} $1"
((PASSED++))
}
print_error() {
echo -e "${RED}${NC} $1"
((FAILED++))
}
print_warning() {
echo -e "${YELLOW}${NC} $1"
((SKIPPED++))
}
# Check if command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
}
# Run command and capture result
run_check() {
local name="$1"
shift
print_info "Checking: $name"
# Capture output for debugging on failure
local output
output=$("$@" 2>&1)
local exit_code=$?
if [ $exit_code -eq 0 ]; then
print_success "$name"
return 0
else
print_error "$name"
# Print captured output on failure for debugging
echo ""
echo "Command output:"
echo "$output" | head -20
if [ $(echo "$output" | wc -l) -gt 20 ]; then
echo "... (truncated, showing first 20 lines)"
fi
echo ""
return 1
fi
}
# Print environment diagnostics
print_environment() {
print_header "Environment Diagnostics"
echo "Project Root: $PROJECT_ROOT"
echo "Script Directory: $SCRIPT_DIR"
echo ""
# Node.js
if command_exists node; then
echo "Node.js: $(node --version)"
else
echo "Node.js: ❌ Not found"
fi
# npm
if command_exists npm; then
echo "npm: $(npm --version)"
else
echo "npm: ❌ Not found"
fi
# Java (for Android)
if command_exists java; then
echo "Java: $(java -version 2>&1 | head -n 1)"
else
echo "Java: ⚠ Not found (Android builds may fail)"
fi
# Gradle (for Android)
if command_exists gradle; then
echo "Gradle: $(gradle --version 2>&1 | grep 'Gradle' | head -n 1 || echo 'Unknown')"
else
echo "Gradle: ⚠ Not found (using wrapper)"
fi
# Swift (for iOS)
if command_exists swift; then
echo "Swift: $(swift --version 2>&1 | head -n 1)"
else
echo "Swift: ⚠ Not found (iOS builds may fail)"
fi
# xcodebuild (for iOS)
if command_exists xcodebuild; then
echo "xcodebuild: $(xcodebuild -version 2>&1 | head -n 1)"
else
echo "xcodebuild: ⚠ Not found (iOS builds may fail)"
fi
echo ""
}
# Install dependencies (best effort)
install_dependencies() {
print_header "Installing Dependencies"
if [ ! -d "$PROJECT_ROOT/node_modules" ]; then
print_info "Installing npm dependencies..."
cd "$PROJECT_ROOT"
npm install || print_warning "npm install failed (non-blocking)"
else
print_success "Dependencies already installed"
fi
echo ""
}
# TypeScript checks
check_typescript() {
print_header "TypeScript Checks"
cd "$PROJECT_ROOT"
# Type check
if run_check "TypeScript compilation" npm run typecheck; then
:
else
print_error "TypeScript type checking failed"
return 1
fi
# Lint
if run_check "ESLint" npm run lint; then
:
else
print_warning "ESLint found issues (non-blocking)"
fi
# Unit tests (if present)
if [ -f "$PROJECT_ROOT/package.json" ] && grep -q '"test"' "$PROJECT_ROOT/package.json"; then
if run_check "Unit tests" npm test; then
:
else
print_warning "Unit tests failed (non-blocking)"
fi
else
print_warning "No unit tests configured"
fi
echo ""
}
# Build checks
check_build() {
print_header "Build Checks"
cd "$PROJECT_ROOT"
# Run build
if run_check "npm run build" npm run build; then
:
else
print_error "Build failed - this will break publish"
return 1
fi
# Verify dist/ exists
if [ ! -d "$PROJECT_ROOT/dist" ]; then
print_error "dist/ directory not found after build"
return 1
else
print_success "dist/ directory exists"
fi
echo ""
}
# Package checks
check_package() {
print_header "Package Checks"
cd "$PROJECT_ROOT"
# Run npm pack --dry-run
print_info "Running npm pack --dry-run..."
PACK_OUTPUT=$(npm pack --dry-run 2>&1)
PACK_EXIT=$?
if [ $PACK_EXIT -ne 0 ]; then
print_error "npm pack --dry-run failed"
echo "$PACK_OUTPUT"
return 1
fi
# Extract file list from pack output (handle both "===" and plain "Tarball Contents" formats)
# Only include actual file entries (lines starting with size like "556B", "1.1kB", etc.)
# This excludes metadata lines like "filename: ... .tgz" from Tarball Details section
PACK_FILES=$(echo "$PACK_OUTPUT" | grep -A 10000 -E "npm notice === Tarball Contents ===|npm notice Tarball Contents" | grep "npm notice" | sed 's/npm notice //' | grep -v "^===" | grep -v "^Tarball Contents$" | grep -E '^[0-9]')
# If still empty, try alternative format (without "===" header)
if [ -z "$PACK_FILES" ]; then
# Extract only file entries (lines starting with size pattern)
PACK_FILES=$(echo "$PACK_OUTPUT" | grep "npm notice" | sed 's/npm notice //' | grep -E '^[0-9]' | grep -v "^package size:" | grep -v "^$")
fi
# If still empty, fallback to all npm notice lines (but exclude known metadata)
if [ -z "$PACK_FILES" ]; then
PACK_FILES=$(echo "$PACK_OUTPUT" | grep "npm notice" | sed 's/npm notice //' | grep -v "^package size:" | grep -v "^name:" | grep -v "^version:" | grep -v "^filename:" | grep -v "^shasum:" | grep -v "^integrity:" | grep -v "^total files:" | grep -v "^$")
fi
# Check for required files
if echo "$PACK_FILES" | grep -q "CapacitorDailyNotification.podspec"; then
print_success "Podspec included in package"
else
print_error "Podspec missing from package (check package.json 'files' field)"
return 1
fi
if echo "$PACK_FILES" | grep -q "dist/"; then
print_success "dist/ included in package"
else
print_error "dist/ missing from package"
return 1
fi
if echo "$PACK_FILES" | grep -q "android/"; then
print_success "android/ included in package"
else
print_warning "android/ not in package (may be intentional)"
fi
if echo "$PACK_FILES" | grep -q "ios/"; then
print_success "ios/ included in package"
else
print_warning "ios/ not in package (may be intentional)"
fi
# Check for forbidden files (hard fail)
# Patterns: Xcode user state, build artifacts, test apps, editor temp files, macOS junk
FORBIDDEN_PATTERNS="xcuserdata/|\.xcuserstate|DerivedData/|\.tgz|ios/App/|\.DS_Store|\.swp|\.swo|\.orig|\.rej"
FORBIDDEN_FOUND=$(echo "$PACK_FILES" | grep -E "$FORBIDDEN_PATTERNS" || true)
if [ -n "$FORBIDDEN_FOUND" ]; then
print_error "Forbidden files found in package (update package.json 'files' field):"
echo "$FORBIDDEN_FOUND" | while read -r line; do
echo " - $line"
done
print_info "Fix: Tighten package.json 'files' field to exclude ios/App/ and Xcode user state files"
print_info "Or add to .npmignore: **/xcuserdata/**, **/*.xcuserstate, **/DerivedData/**, ios/App/**, .DS_Store, *.swp, *.swo, *.orig, *.rej"
return 1
else
print_success "No forbidden files (xcuserdata, xcuserstate, DerivedData, ios/App/, .DS_Store, editor temp files) in package"
fi
# Check for unwanted files (warnings)
if echo "$PACK_FILES" | grep -q "test-apps/"; then
print_warning "test-apps/ found in package (should be excluded)"
fi
if echo "$PACK_FILES" | grep -q "docs/"; then
print_warning "docs/ found in package (should be excluded)"
fi
if echo "$PACK_FILES" | grep -q "node_modules/"; then
print_warning "node_modules/ found in package (should be excluded)"
fi
# Print package manifest summary (first 20 lines)
print_info "Package manifest summary (showing first 20 of $(echo "$PACK_FILES" | wc -l) files):"
echo "$PACK_FILES" | head -20 | while read -r line; do
echo " $line"
done
TOTAL_FILES=$(echo "$PACK_FILES" | wc -l)
if [ "$TOTAL_FILES" -gt 20 ]; then
print_info "... and $((TOTAL_FILES - 20)) more files"
fi
echo ""
}
# Android checks (best effort)
check_android() {
print_header "Android Checks"
cd "$PROJECT_ROOT"
if [ ! -d "$PROJECT_ROOT/android" ]; then
print_warning "Android directory not found, skipping Android checks"
return 0
fi
if ! command_exists java; then
print_warning "Java not found, skipping Android build checks"
return 0
fi
cd "$PROJECT_ROOT/android"
# Check if gradlew exists
if [ ! -f "$PROJECT_ROOT/android/gradlew" ]; then
print_warning "gradlew not found, skipping Android build"
return 0
fi
# Try to run a minimal gradle task
if run_check "Android build (compile)" ./gradlew compileDebugJavaWithJavac --no-daemon; then
:
else
print_warning "Android build check failed (non-blocking)"
fi
echo ""
}
# iOS checks (best effort)
check_ios() {
print_header "iOS Checks"
cd "$PROJECT_ROOT"
if [ ! -d "$PROJECT_ROOT/ios" ]; then
print_warning "iOS directory not found, skipping iOS checks"
return 0
fi
if ! command_exists xcodebuild; then
print_warning "xcodebuild not found, skipping iOS build checks"
print_info "Manual iOS build command: cd ios && xcodebuild -workspace DailyNotificationPlugin.xcworkspace -scheme DailyNotificationPlugin -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15' build"
return 0
fi
# Check if Podfile exists
if [ ! -f "$PROJECT_ROOT/ios/Podfile" ]; then
print_warning "Podfile not found, skipping iOS build"
return 0
fi
# Try to build (best effort, may fail in CI)
# Note: Don't use pipe in run_check - it won't work. Capture output separately.
cd "$PROJECT_ROOT/ios"
BUILD_OUTPUT=$(xcodebuild -workspace DailyNotificationPlugin.xcworkspace -scheme DailyNotificationPlugin -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15' build 2>&1)
BUILD_EXIT=$?
if [ $BUILD_EXIT -eq 0 ]; then
print_success "iOS build (compile)"
# Show first 10 lines of output for context
echo "$BUILD_OUTPUT" | head -10 | while read -r line; do
echo " $line"
done
else
print_warning "iOS build check failed (non-blocking - may require manual setup)"
print_info "Manual iOS build command: cd ios && xcodebuild -workspace DailyNotificationPlugin.xcworkspace -scheme DailyNotificationPlugin -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15' build"
fi
# Try to run tests (best effort)
print_info "Running iOS tests..."
TEST_OUTPUT=$(xcodebuild test -workspace DailyNotificationPlugin.xcworkspace -scheme DailyNotificationPlugin -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15' -only-testing:DailyNotificationPluginTests/DailyNotificationRecoveryTests 2>&1)
TEST_EXIT=$?
if [ $TEST_EXIT -eq 0 ]; then
print_success "iOS recovery tests passed"
# Show test summary if available
echo "$TEST_OUTPUT" | grep -E "Test Suite|Test Case|passed|failed" | head -10 | while read -r line; do
echo " $line"
done
else
print_warning "iOS tests failed or not available (non-blocking)"
print_info "Manual iOS test command: cd ios && xcodebuild test -workspace DailyNotificationPlugin.xcworkspace -scheme DailyNotificationPlugin -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 15'"
fi
echo ""
}
# Check for native code in src/
# Check core module source (can run before build)
check_core_source() {
print_header "Core Module (Source) Checks"
cd "$PROJECT_ROOT"
# Require core source dir + expected files
if [ ! -d "src/core" ]; then
print_error "Missing src/core/"
return 1
fi
local required=(
"src/core/index.ts"
"src/core/errors.ts"
"src/core/enums.ts"
"src/core/events.ts"
"src/core/contracts.ts"
"src/core/guards.ts"
)
local missing=0
for f in "${required[@]}"; do
if [ ! -f "$f" ]; then
print_error "Missing core file: $f"
missing=1
fi
done
if [ $missing -ne 0 ]; then
return 1
fi
# No platform imports inside core
# Block Node builtins, React, Capacitor, and other platform-specific modules
local NODE_BUILTINS="(fs|path|os|child_process|crypto|http|https|net|tls|zlib|stream|util|url|worker_threads|perf_hooks|vm)"
local bad
bad=$(grep -RInE \
"(from\s+['\"]|require\s*\(\s*['\"]|import\s*\(\s*['\"])(${NODE_BUILTINS}|react|@capacitor/|capacitor)['\"]" \
src/core 2>/dev/null || true)
if [ -n "$bad" ]; then
print_error "Core module contains forbidden platform imports:"
echo "$bad" | head -50 | while read -r line; do
echo " $line"
done
echo ""
echo "Policy: src/core must not import platform, Node, or framework-specific modules."
echo "Move platform-dependent code to src/web/ or platform adapters."
return 1
fi
print_success "Core source checks passed"
echo ""
}
# Check core module build artifacts (must run after build)
check_core_artifacts() {
print_header "Core Module (Build Artifacts) Checks"
cd "$PROJECT_ROOT"
# Require build outputs for core
local required=(
"dist/esm/core/index.js"
"dist/esm/core/index.d.ts"
)
local missing=0
for f in "${required[@]}"; do
if [ ! -f "$f" ]; then
print_error "Missing build artifact: $f (did build run?)"
missing=1
fi
done
if [ $missing -ne 0 ]; then
return 1
fi
# Require package.json export for ./core
if ! node -e "const p=require('./package.json'); if(!p.exports||!p.exports['./core']) process.exit(1);" 2>/dev/null; then
print_error "package.json missing exports['./core']"
return 1
fi
print_success "Core artifact checks passed"
echo ""
}
check_native_code_in_src() {
print_header "Checking for Native Code in src/"
cd "$PROJECT_ROOT"
# Check for Java files
if find src/android -name "*.java" -type f 2>/dev/null | grep -q .; then
print_error "Found Java files in src/android/ (should be removed)"
find src/android -name "*.java" -type f 2>/dev/null | while read -r file; do
echo " - $file"
done
return 1
else
print_success "No Java files in src/android/"
fi
# Check for Swift/Objective-C files
if find src/ios -name "*.swift" -o -name "*.m" -o -name "*.mm" -o -name "*.h" 2>/dev/null | grep -q .; then
print_error "Found native code files in src/ios/ (should be removed)"
find src/ios -name "*.swift" -o -name "*.m" -o -name "*.mm" -o -name "*.h" 2>/dev/null | while read -r file; do
echo " - $file"
done
return 1
else
print_success "No native code files in src/ios/"
fi
echo ""
}
# Main execution
main() {
print_header "Daily Notification Plugin - Verification"
print_environment
install_dependencies
run_check "Native code not in src/" check_native_code_in_src
# Core source checks must be before build
run_check "Core module source checks" check_core_source
run_check "TypeScript typecheck" check_typescript
run_check "Build" check_build
# Core artifacts checks must be after build
run_check "Core module artifact checks" check_core_artifacts
run_check "Package checks" check_package
check_android
check_ios
# Summary
print_header "Verification Summary"
echo "Passed: $PASSED"
echo "Failed: $FAILED"
echo "Skipped: $SKIPPED"
echo ""
if [ $FAILED -eq 0 ]; then
print_success "All critical checks passed!"
exit 0
else
print_error "Some checks failed. Review output above."
exit 1
fi
}
# Run main
main "$@"