diff --git a/app/env.sample b/app/env.sample index 44572752e..de867c802 100644 --- a/app/env.sample +++ b/app/env.sample @@ -11,4 +11,3 @@ MIXPANEL_NFC_PROJECT_TOKEN= SEGMENT_KEY= SENTRY_DSN= SUMSUB_TEE_URL= -IS_TEST_BUILD= diff --git a/package.json b/package.json index 8d79b2be0..92adaadd7 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "kmp:format": "yarn workspace @selfxyz/kmp-test-app format", "kmp:ios": "yarn workspace @selfxyz/kmp-test-app ios:open", "kmp:lint": "yarn workspace @selfxyz/kmp-test-app lint", + "kmp:start": "bash scripts/kmp-start.sh", "kmp:test": "yarn workspace @selfxyz/kmp-sdk test", "lint": "yarn lint:headers && yarn workspaces foreach --parallel -i --all --exclude self-workspace-root run lint", "lint:headers": "node scripts/check-duplicate-headers.cjs . && node scripts/check-license-headers.mjs . --check", diff --git a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/api/SelfSdk.android.kt b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/api/SelfSdk.android.kt index 65f60fb66..c9c2381e6 100644 --- a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/api/SelfSdk.android.kt +++ b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/api/SelfSdk.android.kt @@ -4,13 +4,16 @@ package xyz.self.sdk.api -import android.app.Activity +import android.content.ActivityNotFoundException import android.content.Intent import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import kotlinx.serialization.json.Json import xyz.self.sdk.webview.SelfVerificationActivity +import java.lang.ref.WeakReference /** * Android implementation of the Self SDK. @@ -20,44 +23,65 @@ actual class SelfSdk private constructor( private val config: SelfSdkConfig, ) { private var activityLauncher: ActivityResultLauncher? = null + private var launcherOwner: WeakReference? = null + private var boundActivity: WeakReference? = null private var pendingCallback: SelfSdkCallback? = null + private var lifecycleObserver: DefaultLifecycleObserver? = null + private var observerActivity: WeakReference? = null actual companion object { private var instance: SelfSdk? = null + private var configuredWith: SelfSdkConfig? = null + private var currentActivity: WeakReference? = null /** * Configures and returns a singleton SelfSdk instance. */ actual fun configure(config: SelfSdkConfig): SelfSdk { - if (instance == null) { + if (instance == null || configuredWith != config) { + instance?.cleanup() instance = SelfSdk(config) + configuredWith = config + } + val activity = currentActivity?.get() + if (activity != null) { + instance?.bindActivity(activity) } return instance!! } + + /** + * Binds the currently active host Activity so common launch(request, callback) + * can work without Android-specific overloads. + */ + fun bindActivity(activity: ComponentActivity) { + currentActivity = WeakReference(activity) + instance?.bindActivity(activity) + } } /** - * Launches the verification flow. - * The calling Activity must be a ComponentActivity for result handling. - * - * Note: For production use, the host app should register the ActivityResultLauncher - * in onCreate() and pass it to this method, rather than registering it here. - * This implementation is simplified for the initial version. + * Launches the verification flow through the common API surface. + * On Android, this requires a bound ComponentActivity via SelfSdk.bindActivity(activity). */ actual fun launch( request: VerificationRequest, callback: SelfSdkCallback, ) { - // Store callback for later - pendingCallback = callback - - // Get current activity context - // Note: In production, the host app should pass the activity explicitly - // For now, we'll require the activity to be passed via a helper method - throw NotImplementedError( - "Please use launch(activity, request, callback) instead. " + - "The Activity parameter is required on Android.", - ) + val activity = + resolveActivity() + ?: run { + callback.onFailure( + SelfSdkError( + code = "MISSING_ACTIVITY", + message = + "No bound ComponentActivity found. " + + "Call SelfSdk.bindActivity(activity) in your Activity before launch().", + ), + ) + return + } + launchInternal(activity, request, callback) } /** @@ -73,6 +97,28 @@ actual class SelfSdk private constructor( request: VerificationRequest, callback: SelfSdkCallback, ) { + bindActivity(activity) + Companion.currentActivity = WeakReference(activity) + launchInternal(activity, request, callback) + } + + private fun launchInternal( + activity: ComponentActivity, + request: VerificationRequest, + callback: SelfSdkCallback, + ) { + if (pendingCallback != null) { + callback.onFailure( + SelfSdkError( + code = "VERIFICATION_IN_PROGRESS", + message = "A verification flow is already in progress", + ), + ) + return + } + + pendingCallback = callback + // Create intent for SelfVerificationActivity val intent = Intent(activity, SelfVerificationActivity::class.java).apply { @@ -81,21 +127,120 @@ actual class SelfSdk private constructor( putExtra(SelfVerificationActivity.EXTRA_CONFIG, serializeConfig(config)) } - // Register using the ActivityResultRegistry directly (without LifecycleOwner) - // so it can be called after onStart(). The host app's Activity may already - // be in RESUMED state when the user taps "verify". - if (activityLauncher == null) { - activityLauncher = - activity.activityResultRegistry.register( - "self-sdk-verification", - ActivityResultContracts.StartActivityForResult(), - ) { result -> - handleActivityResult(result.resultCode, result.data, callback) - } + // Launch the verification activity + val launcher = activityLauncher + if (launcher == null) { + pendingCallback = null + callback.onFailure( + SelfSdkError( + code = "LAUNCHER_NOT_AVAILABLE", + message = "Could not initialize Android activity launcher", + ), + ) + return + } + try { + launcher.launch(intent) + } catch (e: ActivityNotFoundException) { + pendingCallback = null + callback.onFailure( + SelfSdkError( + code = "ACTIVITY_NOT_FOUND", + message = "Could not launch verification activity: ${e.message}", + ), + ) + } catch (e: IllegalStateException) { + pendingCallback = null + callback.onFailure( + SelfSdkError( + code = "LAUNCH_FAILED", + message = "Could not launch verification activity: ${e.message}", + ), + ) + } + } + + private fun bindActivity(activity: ComponentActivity) { + boundActivity = WeakReference(activity) + ensureLauncher(activity) + + if (observerActivity?.get() === activity) { + return } - // Launch the verification activity - activityLauncher?.launch(intent) + val previousActivity = observerActivity?.get() + val previousObserver = lifecycleObserver + if (previousActivity != null && previousObserver != null) { + previousActivity.lifecycle.removeObserver(previousObserver) + } + + val observer = + object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + if (launcherOwner?.get() === activity) { + activityLauncher?.unregister() + activityLauncher = null + launcherOwner = null + } + if (boundActivity?.get() === activity) { + boundActivity = null + } + pendingCallback?.onCancelled() + pendingCallback = null + lifecycleObserver = null + observerActivity = null + } + } + lifecycleObserver = observer + observerActivity = WeakReference(activity) + activity.lifecycle.addObserver(observer) + } + + private fun cleanup() { + activityLauncher?.unregister() + activityLauncher = null + launcherOwner = null + + val activity = observerActivity?.get() + val observer = lifecycleObserver + if (activity != null && observer != null) { + activity.lifecycle.removeObserver(observer) + } + lifecycleObserver = null + observerActivity = null + boundActivity = null + + pendingCallback?.onCancelled() + pendingCallback = null + } + + private fun resolveActivity(): ComponentActivity? { + val resolved = boundActivity?.get() ?: Companion.currentActivity?.get() + if (resolved != null) { + bindActivity(resolved) + } + return resolved + } + + private fun ensureLauncher(activity: ComponentActivity) { + val currentOwner = launcherOwner?.get() + if (activityLauncher != null && currentOwner === activity) { + return + } + + activityLauncher?.unregister() + launcherOwner = WeakReference(activity) + activityLauncher = + activity.activityResultRegistry.register( + "self-sdk-verification", + ActivityResultContracts.StartActivityForResult(), + ) { result -> + val callback = pendingCallback + pendingCallback = null + if (callback != null) { + handleActivityResult(result.resultCode, result.data, callback) + } + } } /** @@ -107,7 +252,7 @@ actual class SelfSdk private constructor( callback: SelfSdkCallback, ) { when (resultCode) { - Activity.RESULT_OK -> { + SelfVerificationActivity.RESULT_CODE_SUCCESS -> { val resultDataJson = data?.getStringExtra(SelfVerificationActivity.EXTRA_RESULT_DATA) val resultType = data?.getStringExtra(SelfVerificationActivity.EXTRA_RESULT_TYPE) if (resultDataJson != null) { @@ -135,7 +280,7 @@ actual class SelfSdk private constructor( ) } } - Activity.RESULT_CANCELED -> { + SelfVerificationActivity.RESULT_CODE_CANCELLED -> { // User cancelled callback.onCancelled() } diff --git a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/handlers/LifecycleBridgeHandler.kt b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/handlers/LifecycleBridgeHandler.kt index f1378037a..f49eb3ecf 100644 --- a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/handlers/LifecycleBridgeHandler.kt +++ b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/handlers/LifecycleBridgeHandler.kt @@ -51,7 +51,7 @@ class LifecycleBridgeHandler( */ private fun dismiss(): JsonElement? { activity.runOnUiThread { - activity.setResult(Activity.RESULT_CANCELED) + activity.setResult(SelfVerificationActivity.RESULT_CODE_CANCELLED) activity.finish() } return null @@ -74,19 +74,19 @@ class LifecycleBridgeHandler( if (type != null) { // Flat lifecycle payload (e.g. { type: "proofRequested" }) — treat as success intent.putExtra(SelfVerificationActivity.EXTRA_RESULT_TYPE, type) - activity.setResult(Activity.RESULT_OK, intent) + activity.setResult(SelfVerificationActivity.RESULT_CODE_SUCCESS, intent) } else if (success && data != null) { // Success result intent.putExtra(SelfVerificationActivity.EXTRA_RESULT_DATA, data) - activity.setResult(Activity.RESULT_OK, intent) + activity.setResult(SelfVerificationActivity.RESULT_CODE_SUCCESS, intent) } else if (!success && errorCode != null) { // Error result intent.putExtra(SelfVerificationActivity.EXTRA_ERROR_CODE, errorCode) intent.putExtra(SelfVerificationActivity.EXTRA_ERROR_MESSAGE, errorMessage ?: "Unknown error") - activity.setResult(Activity.RESULT_FIRST_USER, intent) + activity.setResult(SelfVerificationActivity.RESULT_CODE_ERROR, intent) } else { // Cancelled or invalid result - activity.setResult(Activity.RESULT_CANCELED, intent) + activity.setResult(SelfVerificationActivity.RESULT_CODE_CANCELLED, intent) } activity.finish() diff --git a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt index f9b6be1f2..1d9b745ab 100644 --- a/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt +++ b/packages/kmp-sdk/shared/src/androidMain/kotlin/xyz/self/sdk/webview/AndroidWebViewHost.kt @@ -108,7 +108,7 @@ class AndroidWebViewHost( val url = request?.url?.toString() ?: return true val assetHost = "https://appassets.androidplatform.net/" if (url.startsWith(assetHost)) return false - if (isDebugMode && url.startsWith("http://10.0.2.2:5173")) return false + if (isDebugMode && url.startsWith("http://127.0.0.1:5173")) return false return true // block everything else } @@ -128,8 +128,8 @@ class AndroidWebViewHost( // Load appropriate URL based on mode if (isDebugMode) { // Development mode: connect to Vite dev server - // Android emulator uses 10.0.2.2 to access host machine's localhost - loadUrl("http://10.0.2.2:5173") + // With adb reverse, Android devices can use localhost. + loadUrl("http://127.0.0.1:5173") } else { // Production mode: load via WebViewAssetLoader. // The custom PathHandler prepends self-wallet/ to all paths, diff --git a/packages/kmp-sdk/shared/src/commonTest/kotlin/xyz/self/sdk/models/ModelSerializationTest.kt b/packages/kmp-sdk/shared/src/commonTest/kotlin/xyz/self/sdk/models/ModelSerializationTest.kt index 84f61bbf0..aa337bb9d 100644 --- a/packages/kmp-sdk/shared/src/commonTest/kotlin/xyz/self/sdk/models/ModelSerializationTest.kt +++ b/packages/kmp-sdk/shared/src/commonTest/kotlin/xyz/self/sdk/models/ModelSerializationTest.kt @@ -8,6 +8,7 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import xyz.self.sdk.api.SelfSdkConfig import xyz.self.sdk.api.VerificationRequest +import xyz.self.sdk.api.VerificationResult import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -122,6 +123,22 @@ class ModelSerializationTest { assertEquals(request, decoded) } + @Test + fun verificationResult_roundtrip() { + val result = + VerificationResult( + success = true, + type = "proofGenerated", + userId = "user-1", + verificationId = "verification-123", + proof = "proof-bytes", + claims = mapOf("nationality" to "UTO"), + ) + val encoded = json.encodeToString(result) + val decoded = json.decodeFromString(encoded) + assertEquals(result, decoded) + } + @Test fun selfSdkConfig_defaults() { val config = SelfSdkConfig() diff --git a/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/api/SelfSdk.ios.kt b/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/api/SelfSdk.ios.kt index f3813641d..67f773d5e 100644 --- a/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/api/SelfSdk.ios.kt +++ b/packages/kmp-sdk/shared/src/iosMain/kotlin/xyz/self/sdk/api/SelfSdk.ios.kt @@ -44,10 +44,12 @@ actual class SelfSdk private constructor( actual companion object { private var instance: SelfSdk? = null + private var configuredWith: SelfSdkConfig? = null actual fun configure(config: SelfSdkConfig): SelfSdk { - if (instance == null) { + if (instance == null || configuredWith != config) { instance = SelfSdk(config) + configuredWith = config } return instance!! } diff --git a/packages/kmp-test-app/composeApp/src/androidMain/kotlin/xyz/self/testapp/MainActivity.kt b/packages/kmp-test-app/composeApp/src/androidMain/kotlin/xyz/self/testapp/MainActivity.kt index d43053b10..684751a38 100644 --- a/packages/kmp-test-app/composeApp/src/androidMain/kotlin/xyz/self/testapp/MainActivity.kt +++ b/packages/kmp-test-app/composeApp/src/androidMain/kotlin/xyz/self/testapp/MainActivity.kt @@ -8,10 +8,12 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import xyz.self.sdk.api.SelfSdk class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + SelfSdk.bindActivity(this) enableEdgeToEdge() setContent { App() diff --git a/packages/kmp-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/App.kt b/packages/kmp-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/App.kt index f1edb3661..830a07ecd 100644 --- a/packages/kmp-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/App.kt +++ b/packages/kmp-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/App.kt @@ -11,6 +11,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import xyz.self.testapp.screens.PassportDetailsScreen import xyz.self.testapp.screens.ResultScreen +import xyz.self.testapp.screens.SdkLaunchScreen import xyz.self.testapp.theme.SelfTestTheme import xyz.self.testapp.viewmodels.VerificationViewModel @@ -22,8 +23,12 @@ fun App() { NavHost( navController = navController, - startDestination = "passport_details", + startDestination = "sdk_launch", ) { + composable("sdk_launch") { + SdkLaunchScreen(navController) + } + composable("passport_details") { PassportDetailsScreen(navController, viewModel) } diff --git a/packages/kmp-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/screens/SdkLaunchScreen.kt b/packages/kmp-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/screens/SdkLaunchScreen.kt new file mode 100644 index 000000000..5c65f20a9 --- /dev/null +++ b/packages/kmp-test-app/composeApp/src/commonMain/kotlin/xyz/self/testapp/screens/SdkLaunchScreen.kt @@ -0,0 +1,180 @@ +// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc. +// SPDX-License-Identifier: BUSL-1.1 +// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. + +package xyz.self.testapp.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import kotlinx.coroutines.launch +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import xyz.self.sdk.api.SelfSdk +import xyz.self.sdk.api.SelfSdkCallback +import xyz.self.sdk.api.SelfSdkConfig +import xyz.self.sdk.api.SelfSdkError +import xyz.self.sdk.api.VerificationRequest +import xyz.self.sdk.api.VerificationResult + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SdkLaunchScreen(navController: NavController) { + var userId by remember { mutableStateOf("test-user") } + var scope by remember { mutableStateOf("identity") } + var callbackStatus by remember { mutableStateOf("Idle") } + var callbackPayload by remember { mutableStateOf(null) } + var callbackError by remember { mutableStateOf(null) } + + val coroutineScope = rememberCoroutineScope() + val sdk = remember { SelfSdk.configure(SelfSdkConfig(debug = true)) } + val json = remember { Json { prettyPrint = true } } + + Scaffold( + topBar = { TopAppBar(title = { Text("SDK Public API Test") }) }, + ) { paddingValues -> + Column( + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues) + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = "This button validates SelfSdk.configure(...).launch(...) end-to-end.", + style = MaterialTheme.typography.bodyMedium, + ) + + OutlinedTextField( + value = userId, + onValueChange = { userId = it }, + label = { Text("User ID") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + + OutlinedTextField( + value = scope, + onValueChange = { scope = it }, + label = { Text("Scope") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + ) + + Button( + onClick = { + callbackStatus = "Launching verification..." + callbackPayload = null + callbackError = null + + val request = + VerificationRequest( + userId = userId.ifBlank { null }, + scope = scope.ifBlank { null }, + disclosures = listOf("name", "nationality", "date_of_birth"), + ) + + sdk.launch( + request = request, + callback = + object : SelfSdkCallback { + override fun onSuccess(result: VerificationResult) { + coroutineScope.launch { + callbackStatus = "Success" + callbackError = null + callbackPayload = + json.encodeToString( + VerificationResult.serializer(), + result, + ) + } + } + + override fun onFailure(error: SelfSdkError) { + coroutineScope.launch { + callbackStatus = "Failure" + callbackError = error + callbackPayload = null + } + } + + override fun onCancelled() { + coroutineScope.launch { + callbackStatus = "Cancelled" + callbackError = null + callbackPayload = null + } + } + }, + ) + }, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Launch Verification") + } + + OutlinedButton( + onClick = { navController.navigate("passport_details") }, + modifier = Modifier.fillMaxWidth(), + ) { + Text("Open Manual MRZ/NFC Flow") + } + + Spacer(modifier = Modifier.height(8.dp)) + + Card( + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + ), + modifier = Modifier.fillMaxWidth(), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text("Callback Status: $callbackStatus") + if (callbackError != null) { + Text("Error Code: ${callbackError?.code}") + Text("Error Message: ${callbackError?.message}") + } + if (callbackPayload != null) { + Text( + text = callbackPayload!!, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + ) + } + } + } + } + } +} diff --git a/packages/kmp-test-app/composeApp/src/debug/AndroidManifest.xml b/packages/kmp-test-app/composeApp/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..36f3d2cf3 --- /dev/null +++ b/packages/kmp-test-app/composeApp/src/debug/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/packages/rn-sdk-test-app/App.tsx b/packages/rn-sdk-test-app/App.tsx index 136c7ecdc..147cfeb6a 100644 --- a/packages/rn-sdk-test-app/App.tsx +++ b/packages/rn-sdk-test-app/App.tsx @@ -3,16 +3,20 @@ // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. import React, { useMemo, useState } from 'react'; -import { NativeModules, SafeAreaView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; +import { + NativeModules, + SafeAreaView, + ScrollView, + StatusBar, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; import { SelfVerification, type SelfSdkError, type VerificationResult } from '@selfxyz/rn-sdk'; -const defaultRequest = { - userId: 'rn-test-user', - scope: 'rn-sdk-test', - disclosures: [], -}; - const fallbackMrzScannerModule = { startScanning: async () => ({ documentNumber: 'XK0000000', @@ -38,8 +42,6 @@ function ensureMrzScannerModule(): void { typeof legacyScanner?.startScanning === 'function'; if (!hasScanner) { - // Keep camera bridge round-trip testable in this harness when host-native MRZ isn't wired. - // Hermes NativeModules host object can reject writes, so this fallback is best-effort. try { nativeModules.SelfMRZScannerModule = fallbackMrzScannerModule; } catch { @@ -50,24 +52,47 @@ function ensureMrzScannerModule(): void { ensureMrzScannerModule(); +type CallbackState = + | { status: 'Idle' } + | { status: 'Launching verification...' } + | { status: 'Success'; payload: string } + | { status: 'Failure'; code: string; message: string } + | { status: 'Cancelled' }; + function App(): React.JSX.Element { const [isVerifying, setIsVerifying] = useState(false); - const [status, setStatus] = useState('Ready'); + const [userId, setUserId] = useState('test-user'); + const [scope, setScope] = useState('identity'); + const [callback, setCallback] = useState({ status: 'Idle' }); - const request = useMemo(() => defaultRequest, []); + const request = useMemo( + () => ({ + userId: userId || undefined, + scope: scope || undefined, + disclosures: ['name', 'nationality', 'date_of_birth'], + }), + [userId, scope], + ); const handleSuccess = (result: VerificationResult) => { - setStatus(`Success: ${result.verificationId ?? 'no verificationId'}`); + setCallback({ + status: 'Success', + payload: JSON.stringify(result, null, 2), + }); setIsVerifying(false); }; const handleFailure = (error: SelfSdkError) => { - setStatus(`Failure: ${error.code} - ${error.message}`); + setCallback({ + status: 'Failure', + code: error.code, + message: error.message, + }); setIsVerifying(false); }; const handleCancelled = () => { - setStatus('Cancelled'); + setCallback({ status: 'Cancelled' }); setIsVerifying(false); }; @@ -90,13 +115,57 @@ function App(): React.JSX.Element { return ( - - RN SDK Test Harness - Status: {status} - setIsVerifying(true)}> - Launch Verification - + + SDK Public API Test + + + This button validates SelfSdk.configure(...).launch(...) end-to-end. + + + User ID + + + Scope + + + { + setCallback({ status: 'Launching verification...' }); + setIsVerifying(true); + }} + > + Launch Verification + + + + Callback Status: {callback.status} + {callback.status === 'Failure' && ( + <> + Error Code: {callback.code} + Error Message: {callback.message} + + )} + {callback.status === 'Success' && ( + {callback.payload} + )} + + ); } @@ -106,34 +175,74 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: '#f6f7f8', }, - content: { - flex: 1, - alignItems: 'center', - justifyContent: 'center', - paddingHorizontal: 24, - gap: 16, + topBar: { + paddingHorizontal: 16, + paddingVertical: 14, + backgroundColor: '#f6f7f8', }, - title: { - fontSize: 24, + topBarTitle: { + fontSize: 22, fontWeight: '700', color: '#111827', }, - subtitle: { + content: { + paddingHorizontal: 16, + paddingBottom: 32, + gap: 12, + }, + description: { fontSize: 14, color: '#374151', - textAlign: 'center', + lineHeight: 20, }, - button: { + label: { + fontSize: 12, + fontWeight: '500', + color: '#6b7280', + marginBottom: -8, + }, + input: { + borderWidth: 1, + borderColor: '#d1d5db', + borderRadius: 8, + paddingHorizontal: 12, + paddingVertical: 10, + fontSize: 16, + color: '#111827', + backgroundColor: '#ffffff', + }, + primaryButton: { backgroundColor: '#111827', borderRadius: 10, - paddingHorizontal: 20, - paddingVertical: 12, + paddingVertical: 14, + alignItems: 'center', }, - buttonText: { + primaryButtonText: { color: '#ffffff', fontSize: 16, fontWeight: '600', }, + callbackCard: { + backgroundColor: '#e8e5f0', + borderRadius: 12, + padding: 16, + gap: 6, + }, + callbackLabel: { + fontSize: 14, + fontWeight: '500', + color: '#1f2937', + }, + callbackDetail: { + fontSize: 13, + color: '#374151', + }, + callbackPayload: { + fontSize: 12, + fontFamily: 'monospace', + color: '#374151', + lineHeight: 18, + }, verificationView: { flex: 1, }, diff --git a/packages/webview-app/src/screens/onboarding/DocumentCameraScreen.tsx b/packages/webview-app/src/screens/onboarding/DocumentCameraScreen.tsx index d7eca43e4..f9699182a 100644 --- a/packages/webview-app/src/screens/onboarding/DocumentCameraScreen.tsx +++ b/packages/webview-app/src/screens/onboarding/DocumentCameraScreen.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: BUSL-1.1 // NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE. -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { Button, @@ -15,14 +15,16 @@ import { spacing, } from '@selfxyz/euclid-web'; -import { useBridge } from '../../providers/BridgeProvider'; import { useSelfClient } from '../../providers/SelfClientProvider'; +const GENERIC_SCAN_ERROR_MESSAGE = 'We could not read your document. Please try again.'; +const CAMERA_UNAVAILABLE_MESSAGE = 'Camera is not available on this device.'; +const MRZ_INVALID_DATA_ERROR = 'MRZ_INVALID_DATA'; + export const DocumentCameraScreen: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); - const bridge = useBridge(); - const { analytics, haptic } = useSelfClient(); + const { analytics, haptic, camera } = useSelfClient(); const { countryCode = '', documentType = 'p' } = (location.state as { @@ -32,24 +34,50 @@ export const DocumentCameraScreen: React.FC = () => { const [scanning, setScanning] = useState(false); const [error, setError] = useState(null); + const mountedRef = useRef(true); + const scanGenerationRef = useRef(0); + const scanInFlightRef = useRef(false); const scanPrompt = documentType === 'i' ? 'Scan your ID card' : 'Scan your passport'; const startMRZScan = useCallback(async () => { + if (scanInFlightRef.current) return; + scanInFlightRef.current = true; + + const scanGeneration = scanGenerationRef.current + 1; + scanGenerationRef.current = scanGeneration; + setScanning(true); setError(null); - analytics.trackEvent('camera_mrz_scan_started', { - documentType, - countryCode, - }); try { - const result = await bridge.request<{ - documentNumber: string; - dateOfBirth: string; - dateOfExpiry: string; - }>('camera', 'scanMRZ', { documentType, countryCode }); + const available = await camera.isAvailable(); + if (!mountedRef.current || scanGenerationRef.current !== scanGeneration) { + return; + } + if (!available) { + setError(CAMERA_UNAVAILABLE_MESSAGE); + analytics.trackEvent('camera_mrz_scan_failed', { errorCode: 'CAMERA_NOT_AVAILABLE' }); + return; + } + + analytics.trackEvent('camera_mrz_scan_started', { + documentType, + countryCode, + }); + + const result = await camera.scanMRZ({ documentType, countryCode }); + if (!mountedRef.current || scanGenerationRef.current !== scanGeneration) { + return; + } + + const passportNumber = result.documentNumber?.trim() ?? ''; + const dateOfBirth = result.dateOfBirth?.trim() ?? ''; + const dateOfExpiry = result.dateOfExpiry?.trim() ?? ''; + if (!passportNumber || !dateOfBirth || !dateOfExpiry) { + throw new Error(MRZ_INVALID_DATA_ERROR); + } haptic.trigger('success'); analytics.trackEvent('camera_mrz_scan_success'); @@ -58,25 +86,49 @@ export const DocumentCameraScreen: React.FC = () => { state: { countryCode, documentType, - passportNumber: result.documentNumber, - dateOfBirth: result.dateOfBirth, - dateOfExpiry: result.dateOfExpiry, + passportNumber, + dateOfBirth, + dateOfExpiry, }, }); } catch (err) { - const message = err instanceof Error ? err.message : 'MRZ scan failed'; - setError(message); - analytics.trackEvent('camera_mrz_scan_failed', { error: message }); + if (!mountedRef.current || scanGenerationRef.current !== scanGeneration) { + return; + } + + const bridgeErrorCode = + typeof err === 'object' && + err !== null && + 'code' in err && + typeof (err as { code?: unknown }).code === 'string' + ? (err as { code: string }).code + : undefined; + const errorCode = + bridgeErrorCode === MRZ_INVALID_DATA_ERROR || + (err instanceof Error && err.message === MRZ_INVALID_DATA_ERROR) + ? 'MRZ_INVALID_DATA' + : 'MRZ_SCAN_FAILED'; + setError(GENERIC_SCAN_ERROR_MESSAGE); + analytics.trackEvent('camera_mrz_scan_failed', { errorCode }); } finally { - setScanning(false); + scanInFlightRef.current = false; + if (mountedRef.current && scanGenerationRef.current === scanGeneration) { + setScanning(false); + } } - }, [bridge, navigate, analytics, haptic, documentType, countryCode]); + }, [camera, navigate, analytics, haptic, documentType, countryCode]); useEffect(() => { + mountedRef.current = true; startMRZScan(); + return () => { + mountedRef.current = false; + scanGenerationRef.current += 1; + }; }, [startMRZScan]); const onCancel = useCallback(() => { + scanGenerationRef.current += 1; analytics.trackEvent('camera_screen_closed'); navigate('/'); }, [navigate, analytics]); diff --git a/packages/webview-bridge/src/adapters/camera.ts b/packages/webview-bridge/src/adapters/camera.ts index dff5031bb..15f289713 100644 --- a/packages/webview-bridge/src/adapters/camera.ts +++ b/packages/webview-bridge/src/adapters/camera.ts @@ -5,13 +5,19 @@ import type { WebViewBridge } from '../bridge'; export interface MrzScanResult { - documentNumber: string; - dateOfBirth: string; - dateOfExpiry: string; + documentNumber?: string; + dateOfBirth?: string; + dateOfExpiry?: string; +} + +export interface MrzScanParams { + documentType?: string; + countryCode?: string; + [key: string]: unknown; } export interface BridgeCameraAdapter { - scanMRZ(): Promise; + scanMRZ(params?: MrzScanParams): Promise; isAvailable(): Promise; } @@ -19,10 +25,10 @@ export function bridgeCameraAdapter( bridge: WebViewBridge, ): BridgeCameraAdapter { return { - async scanMRZ(): Promise { + async scanMRZ(params?: MrzScanParams): Promise { // Native handler parses the MRZ JSON string into a JsonElement, // which arrives as an object with documentNumber, dateOfBirth, dateOfExpiry. - return bridge.request('camera', 'scanMRZ', {}); + return bridge.request('camera', 'scanMRZ', params ?? {}); }, async isAvailable(): Promise { diff --git a/packages/webview-bridge/src/adapters/index.ts b/packages/webview-bridge/src/adapters/index.ts index db82cde80..834466598 100644 --- a/packages/webview-bridge/src/adapters/index.ts +++ b/packages/webview-bridge/src/adapters/index.ts @@ -36,4 +36,4 @@ export { bridgeBiometricsAdapter } from './biometrics'; export type { BridgeBiometricsAdapter } from './biometrics'; export { bridgeCameraAdapter } from './camera'; -export type { BridgeCameraAdapter, MrzScanResult } from './camera'; +export type { BridgeCameraAdapter, MrzScanParams, MrzScanResult } from './camera'; diff --git a/scripts/kmp-start.sh b/scripts/kmp-start.sh new file mode 100644 index 000000000..01603b610 --- /dev/null +++ b/scripts/kmp-start.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +PORT=5173 + +if command -v adb >/dev/null 2>&1; then + adb start-server >/dev/null 2>&1 || true + + device_serials="$(adb devices | awk '$2 == "device" { print $1 }')" + if [ -n "${device_serials}" ]; then + echo "Setting adb reverse tcp:${PORT} -> tcp:${PORT} for connected Android devices..." + while IFS= read -r serial; do + [ -z "${serial}" ] && continue + if adb -s "${serial}" reverse "tcp:${PORT}" "tcp:${PORT}" >/dev/null 2>&1; then + echo " ${serial}: ok" + else + echo " ${serial}: failed (continuing)" + fi + done <<< "${device_serials}" + echo "Use http://127.0.0.1:${PORT} on Android devices with adb reverse." + else + echo "No adb devices detected. If using emulator, use http://10.0.2.2:${PORT}." + fi +else + echo "adb not found. Skipping reverse setup." +fi + +echo "Starting webview dev server on 0.0.0.0:${PORT}..." +exec yarn workspace @selfxyz/webview-app dev --host 0.0.0.0 --port "${PORT}" diff --git a/specs/ARCHIVE.md b/specs/ARCHIVE.md new file mode 100644 index 000000000..0d3a4f0f8 --- /dev/null +++ b/specs/ARCHIVE.md @@ -0,0 +1,8 @@ +# Spec Archive + +Append-only log of retired specs. When a spec is fully done and no longer needed for active reference, add a row here. + +For full retirement process, see [SPECS-REORG-PLAN.md](./SPECS-REORG-PLAN.md) placement rule 6. + +| Spec | Retired | Outcome | Key decisions / lessons | Final PR(s) | +| ---- | ------- | ------- | ----------------------- | ----------- | diff --git a/specs/EUCLID-WEB-CONSOLIDATION.md b/specs/EUCLID-WEB-CONSOLIDATION.md new file mode 100644 index 000000000..de276cc74 --- /dev/null +++ b/specs/EUCLID-WEB-CONSOLIDATION.md @@ -0,0 +1,88 @@ +# Euclid Web Consolidation Plan (Draft) + +## Status + +Draft and evolving. This document captures the current direction and will be refined as migration work progresses. + +## Goal + +Use `euclid-web` as the single source of truth for new SDK flow screens and business flow logic, while KMP/RN/mobile hosts become thin wrappers around a shared WebView flow. + +## Target Architecture + +1. `euclid-web` owns screen UI and flow orchestration. +2. Host apps (KMP demo, RN demo, mobile app) own only: + - WebView hosting + - bridge wiring + - platform permissions/lifecycle + - callback handoff to native caller +3. SDK surface remains stable while underlying UI/flow logic converges to web. + +## Scope Boundary (Current) + +In scope now: + +- WebView launch reliability +- Host-to-web bridge contract +- Callback plumbing and smoke verification + +Out of scope now: + +- Full verification journey implementation parity in all hosts +- Consolidating/removing demo apps +- Reworking non-bridge native UI beyond what is needed for hosting + +## Phased Plan + +### Phase 0: Stabilize Launch (Current PR) + +- Keep changes limited to launch reliability and local dev host setup. +- Ensure `kmp:start` reliably starts the web server and supports emulator/device access. +- Keep debug-only code removed unless required for ongoing validation. + +### Phase 1: Shared Host Contract (Next PR) + +- Define one shared config contract for URL/env/flags consumed by KMP + RN hosts. +- Define one shared event/callback contract (`success`, `error`, `cancel`, optional progress). +- Add smoke tests in each host for launch -> callback -> close path. + +### Phase 2: Incremental Screen Migration + +- All new flow screens land in `euclid-web` first. +- Remove duplicate native screens from demo apps as each flow area reaches parity. +- Keep demos as thin bridge/host harnesses during migration. + +### Phase 3: Consolidate Demo Surface + +- Decide final demo strategy once parity is high: + - single primary demo + one bridge harness, or + - both retained with explicit ownership and minimal overlap. +- De-scope duplicated flow logic from host apps. + +### Phase 4: Mobile App Convergence + +- Mobile app consumes the same `euclid-web` flow path as SDK demos. +- Native app keeps only host/platform concerns and app-specific shell concerns. + +## Decision Rules + +- If a change is launch/config/bridge related: belongs in host SDK work. +- If a change is flow UI/logic: belongs in `euclid-web`. +- If work duplicates flow code across hosts: treat as temporary and track removal. + +## Open Questions + +1. What is the long-term canonical demo app for partner validation? +2. What is the minimum bridge API required before flow migration accelerates? +3. Do we need a compatibility matrix per host (KMP/RN/mobile app) for each migrated flow? +4. What release gating is required before removing duplicated native flow screens? + +## Proposed Near-Term PR Slices + +1. Host contract + callback schema standardization. +2. RN and KMP smoke tests aligned to the same launch/callback assertions. +3. First euclid-web-only flow segment integrated into both hosts. + +## Notes + +This is intentionally incomplete. Add decisions and open items as migration discoveries are made. diff --git a/specs/HANDOFF.md b/specs/HANDOFF.md index b9a958c5f..1cbd41240 100644 --- a/specs/HANDOFF.md +++ b/specs/HANDOFF.md @@ -65,3 +65,7 @@ These decisions were made during this PR cycle. They are now documented in [SDK- 1. **Correctness cleanup** — Adapter consolidation, dynamic proving config, crypto adapter interface gap 2. **Publishing** — npm publish rn-sdk, finalize AAR/XCFramework packaging 3. **Self Wallet migration** — Wire `SelfVerification` into the main app (Phase 2) + +## Architecture Notes + +- Draft consolidation plan: [EUCLID-WEB-CONSOLIDATION.md](./EUCLID-WEB-CONSOLIDATION.md) diff --git a/specs/README.md b/specs/README.md index 8d7436121..369645f4b 100644 --- a/specs/README.md +++ b/specs/README.md @@ -1,62 +1,64 @@ -# Self SDK — Spec System +# Specs -> Table of contents for the spec folder. Start here. +> Project-first table of contents for all specs. Start here. -## What This Is +## How Specs Are Organized -A three-tier spec system designed for parallel AI agent execution: +Specs are organized by **project** first (`kmp`, `sdk`, `lottie`, `euclid`), not by document intent. -1. **Project Overview** — architecture, contracts, cross-workstream dependencies -2. **Workstream Overviews** — orientation for each person/scope (what you own, context, status) -3. **Implementation Specs** — exact code changes, I/O examples, token-budgeted chunks +- Use `specs/projects//` for project-owned docs. +- Use `specs/framework/` for generic spec-writing rules/templates. +- Use `specs/shared/` for cross-project handoffs. -Specs double as AI agent prompts. Written in second person, sized for single context windows, with validation commands after every chunk. +## Top-Level Navigation -## Meta-Framework +- `framework/` + - `SPEC-GUIDE.md` + - `TEMPLATES.md` + - `PROJECT-RULES.md` + - `PRODUCT-SPEC-ENHANCEMENT-PROMPT.md` -| File | Purpose | When to Read | -| -------------------------------------- | ----------------------------------------------------- | --------------------------------------- | -| [SPEC-GUIDE.md](./SPEC-GUIDE.md) | How to write specs (generic, portable to any project) | Before writing or reviewing any spec | -| [TEMPLATES.md](./TEMPLATES.md) | Copy-paste templates for all three tiers | When creating a new spec | -| [PROJECT-RULES.md](./PROJECT-RULES.md) | Project-specific rules and guardrails | Before starting any implementation work | +- `projects/sdk/` + - SDK-wide architecture, wave plan, handoff, and workstreams -## Project-Level Specs +- `projects/kmp/` + - KMP initiative, architecture, status, and KMP-specific planning -| File | Purpose | When to Read | -| ------------------------------------ | ---------------------------------------------------------------------- | ------------------------------------------ | -| [SDK-OVERVIEW.md](./SDK-OVERVIEW.md) | Architecture, bridge protocol, module table, decision matrix, glossary | First. Always. | -| [WAVE-PLAN.md](./WAVE-PLAN.md) | Dependency-ordered execution plan for parallel agent work | When planning which chunks to execute next | -| [KMP-STATUS.md](./KMP-STATUS.md) | At-a-glance SDK completion percentages (web + KMP) | When you need quick status only | +- `projects/lottie/` + - Lottie migration/review specs -## Workstream Specs +- `projects/euclid/` + - Euclid consolidation specs -Each workstream has two files: `OVERVIEW.md` (stable orientation) and `SPEC.md` (living implementation details). +- `shared/handoffs/` + - Cross-project security and transition docs -| Workstream | Overview | Implementation Spec | Status | -| -------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------- | ----------------------------------------------- | -| Person 1 — WebView UI + Bridge | [OVERVIEW](./person1-webview/OVERVIEW.md) | [SPEC](./person1-webview/SPEC.md) | 28/29 done, 1 pending (dynamic proving config) | -| Person 2 — Native Shells (KMP + Swift) | [OVERVIEW](./person2-native-shells/OVERVIEW.md) | [SPEC](./person2-native-shells/SPEC.md) | 27/28 done, 1 pending (KMP test app validation) | -| Person 3 — Integrations | [OVERVIEW](./person3-integrations/OVERVIEW.md) | [MiniPay Spec](./person3-integrations/SPEC-MINIPAY-SAMPLE.md) | 26/26 done | -| Person 4 — SDK Core | [OVERVIEW](./person4-sdk-core/OVERVIEW.md) | [SPEC](./person4-sdk-core/SPEC.md) | 23/25 done, 2 pending (adapter dedup, crypto) | -| Person 5 — RN SDK | [OVERVIEW](./person5-rn-sdk/OVERVIEW.md) | [SPEC](./person5-rn-sdk/SPEC.md) | 21/23 done, 2 pending (wallet integration, npm) | +## Current Canonical Entry Points + +- SDK: `specs/projects/sdk/INDEX.md` (planned) +- KMP: `specs/projects/kmp/KMP-SPECS-INDEX.md` +- Folder migration: `specs/SPECS-REORG-PLAN.md` + +## Migration Map (Legacy -> Target) + +- `specs/SDK-OVERVIEW.md` -> `specs/projects/sdk/SDK-OVERVIEW.md` +- `specs/WAVE-PLAN.md` -> `specs/projects/sdk/WAVE-PLAN.md` +- `specs/HANDOFF.md` -> `specs/projects/sdk/HANDOFF.md` + +- `specs/person1-webview/*` -> `specs/projects/sdk/workstreams/webview/*` +- `specs/person2-native-shells/*` -> `specs/projects/sdk/workstreams/native-shells/*` +- `specs/person3-integrations/*` -> `specs/projects/sdk/workstreams/integrations/*` +- `specs/person4-sdk-core/*` -> `specs/projects/sdk/workstreams/sdk-core/*` +- `specs/person5-rn-sdk/*` -> `specs/projects/sdk/workstreams/rn-sdk/*` + +- `specs/KMP-STATUS.md` -> `specs/projects/kmp/status/KMP-STATUS.md` +- `specs/lottie-dotlottie-migration/REVIEW.md` -> `specs/projects/lottie/REVIEW.md` +- `specs/EUCLID-WEB-CONSOLIDATION.md` -> `specs/projects/euclid/EUCLID-WEB-CONSOLIDATION.md` +- `specs/handoff-p1-fixes/*` -> `specs/shared/handoffs/p1-fixes/*` ## Reading Order -**New to the project?** - -1. This README -2. [SDK-OVERVIEW.md](./SDK-OVERVIEW.md) — understand the architecture -3. Your workstream's `OVERVIEW.md` — understand what you own -4. Your workstream's `SPEC.md` — understand what to build - -**Starting a work session?** - -1. [WAVE-PLAN.md](./WAVE-PLAN.md) — find the next available chunk -2. Your workstream's `SPEC.md` — read the chunk, check status -3. [PROJECT-RULES.md](./PROJECT-RULES.md) — refresh on guardrails - -**Writing a new spec?** - -1. [SPEC-GUIDE.md](./SPEC-GUIDE.md) — how to write specs -2. [TEMPLATES.md](./TEMPLATES.md) — copy-paste the right template -3. [PROJECT-RULES.md](./PROJECT-RULES.md) — project-specific constraints +1. `specs/README.md` +2. Your project index under `specs/projects//` +3. Relevant framework docs in `specs/framework/` +4. Project workstream specs diff --git a/specs/SPECS-REORG-PLAN.md b/specs/SPECS-REORG-PLAN.md new file mode 100644 index 000000000..983af443a --- /dev/null +++ b/specs/SPECS-REORG-PLAN.md @@ -0,0 +1,162 @@ +# Specs Folder Reorganization Plan (Project-First) + +Last updated: March 5, 2026 +Owner: SDK/specs maintainers +Status: Proposed + +## Decision + +Use a **project-first** structure for `specs/`. + +Why: + +- Work typically starts from project context (`kmp`, `lottie`, `sdk`, `euclid`). +- Agents can resolve scope faster with one project root. +- Cross-project docs are the minority and can live in shared buckets. + +## Goals + +- Make each project's docs discoverable under one folder. +- Keep generic authoring rules separate from project specs. +- Reduce mixed flat files and stale links. +- Preserve compatibility during migration with path mapping. + +## Target Structure + +```text +specs/ + README.md + SPECS-REORG-PLAN.md + + framework/ + SPEC-GUIDE.md + TEMPLATES.md + PROJECT-RULES.md + PRODUCT-SPEC-ENHANCEMENT-PROMPT.md + + projects/ + sdk/ + INDEX.md + SDK-OVERVIEW.md + WAVE-PLAN.md + HANDOFF.md + workstreams/ + webview/ + native-shells/ + integrations/ + sdk-core/ + rn-sdk/ + + kmp/ + INDEX.md + KMP-ARCHITECTURE.md + KMP-INITIATIVE.md + KMP-REORG-PLAN.md + status/ + KMP-STATUS.md + workstreams/ + + lottie/ + INDEX.md + REVIEW.md + + euclid/ + INDEX.md + EUCLID-WEB-CONSOLIDATION.md + + shared/ + handoffs/ + p1-fixes/ + SECURITY-HARDENING.md + + archive/ + ARCHIVE.md # append-only table of retired specs + sdk/ # full-text copies of retired SDK specs (optional) + kmp/ # full-text copies of retired KMP specs (optional) +``` + +## Placement Rules + +1. If a spec is mainly about one project, place it under `specs/projects//`. +2. Only generic spec system docs go in `specs/framework/`. +3. Cross-project coordination and follow-ups go in `specs/shared/`. +4. Every project folder should have an `INDEX.md` as its entrypoint. +5. New implementation specs should include: `Owner`, `Status`, `Last updated`, `Validation commands`. +6. When a spec is fully done: add a row to `specs/ARCHIVE.md` with outcome + key decisions. Either delete the source files (if the "What Was Built" appendix was added per SPEC-GUIDE) or move them to `specs/archive//`. Workstream OVERVIEW.md files stay until the workstream itself is retired. + +## Migration Map (Current -> Target) + +Framework: + +- `specs/SPEC-GUIDE.md` -> `specs/framework/SPEC-GUIDE.md` +- `specs/TEMPLATES.md` -> `specs/framework/TEMPLATES.md` +- `specs/PROJECT-RULES.md` -> `specs/framework/PROJECT-RULES.md` +- `specs/PRODUCT-SPEC-ENHANCEMENT-PROMPT.md` -> `specs/framework/PRODUCT-SPEC-ENHANCEMENT-PROMPT.md` + +SDK project: + +- `specs/SDK-OVERVIEW.md` -> `specs/projects/sdk/SDK-OVERVIEW.md` +- `specs/WAVE-PLAN.md` -> `specs/projects/sdk/WAVE-PLAN.md` +- `specs/HANDOFF.md` -> `specs/projects/sdk/HANDOFF.md` +- `specs/person1-webview/*` -> `specs/projects/sdk/workstreams/webview/*` +- `specs/person2-native-shells/*` -> `specs/projects/sdk/workstreams/native-shells/*` +- `specs/person3-integrations/*` -> `specs/projects/sdk/workstreams/integrations/*` +- `specs/person4-sdk-core/*` -> `specs/projects/sdk/workstreams/sdk-core/*` +- `specs/person5-rn-sdk/*` -> `specs/projects/sdk/workstreams/rn-sdk/*` + +KMP project: + +- `specs/KMP-STATUS.md` -> `specs/projects/kmp/status/KMP-STATUS.md` +- `specs/projects/kmp/KMP-*.md` -> keep under `specs/projects/kmp/` + +Lottie project: + +- `specs/lottie-dotlottie-migration/REVIEW.md` -> `specs/projects/lottie/REVIEW.md` + +Euclid project: + +- `specs/EUCLID-WEB-CONSOLIDATION.md` -> `specs/projects/euclid/EUCLID-WEB-CONSOLIDATION.md` + +Shared: + +- `specs/handoff-p1-fixes/*` -> `specs/shared/handoffs/p1-fixes/*` + +## Rollout Phases + +### Phase 1: Index and Policy + +- Update `specs/README.md` to project-first navigation. +- Keep a migration mapping table in `README` during transition. + +### Phase 2: Create Target Dirs + +- Create `framework/`, `projects/*`, `shared/` roots. +- Add `INDEX.md` placeholders for `sdk`, `kmp`, `lottie`, `euclid`. + +### Phase 3: Move Project-Level Docs + +- Move `SDK-OVERVIEW`, `WAVE-PLAN`, `HANDOFF`, KMP status, lottie review, euclid consolidation. +- Update links in moved docs. + +### Phase 4: Move Workstreams + +- Move `person*` directories to `projects/sdk/workstreams/*`. +- Update references from old paths. + +### Phase 5: Cleanup + +- Remove old-path mapping table after link convergence. +- Add optional metadata validation script for required headings. + +## Validation Checklist + +- `find specs -maxdepth 5 -type f | sort` +- `rg -n "person[1-5]-|KMP-STATUS.md|lottie-dotlottie-migration|EUCLID-WEB-CONSOLIDATION" specs` +- `rg -n "\]\(\./" specs` (relative-link sanity) + +## Immediate Next Steps + +1. Create project `INDEX.md` files for `sdk`, `kmp`, `lottie`, `euclid`. +2. Move `KMP-STATUS.md` into `projects/kmp/status/`. +3. Move `lottie-dotlottie-migration/REVIEW.md` into `projects/lottie/`. +4. Move `EUCLID-WEB-CONSOLIDATION.md` into `projects/euclid/`. diff --git a/specs/person1-webview/SPEC.md b/specs/person1-webview/SPEC.md index 50178edce..c5741f3b4 100644 --- a/specs/person1-webview/SPEC.md +++ b/specs/person1-webview/SPEC.md @@ -32,11 +32,11 @@ The Self Wallet is a monolithic React Native app where all logic, NFC, proving, 2. Bridges to native only for hardware/OS capabilities (NFC, camera, biometrics, keychain, lifecycle) 3. Provides web-native fallback adapters for everything the browser can handle (documents via IndexedDB, crypto hashing via Web Crypto, analytics via console/fetch) -| Area | Issue | -| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| `packages/webview-bridge/` | Implemented with current protocol/adapters and validated by tests (63 tests passing). | -| `packages/webview-app/` | Screens built, routing works. Missing: biometrics + camera adapter wiring, web fallback adapters not all connected in SelfClientProvider. | -| Web fallback adapters | IndexedDB documents adapter, Web Crypto hashing adapter, and console analytics adapter exist in bridge package but need wiring in webview-app. | +| Area | Issue | +| -------------------------- | --------------------------------------------------------------------------------------------------------------------------- | +| `packages/webview-bridge/` | Implemented with current protocol/adapters and validated by tests (63 tests passing). | +| `packages/webview-app/` | Screens built, routing works. SelfClientProvider wiring complete (biometrics, camera, and web fallback adapters connected). | +| Web fallback adapters | IndexedDB documents, Web Crypto hashing, console analytics, navigation, and no-op haptic are wired in webview-app. | ## Design Principles @@ -1331,13 +1331,13 @@ Chunk 1F: Bridge Package (no deps — start here) ## Completion Status -| Chunk | Description | Size | Status | -| ----- | ------------------------ | ---- | -------------------------------------------------------------------------------------------------------------------- | -| 1F | Bridge Package | L | **Done** — 63 tests pass, bridge package and adapters implemented | -| 1B | Onboarding Screens | M | **Done** — all 5 screens render | -| 1C | Proving + Result Screens | M | **Done** — screens render, proving wired | -| 1D | Remaining Screens | S | **Done** — home, settings, coming-soon render | -| 1E | WebView App Shell | M | **In Progress** — providers wired, missing: biometrics adapter, camera wiring, some web fallback adapter connections | +| Chunk | Description | Size | Status | +| ----- | ------------------------ | ---- | ------------------------------------------------------------------------------------------------------------------------------------ | +| 1F | Bridge Package | L | **Done** — 63 tests pass, bridge package and adapters implemented | +| 1B | Onboarding Screens | M | **Done** — all 5 screens render | +| 1C | Proving + Result Screens | M | **Done** — screens render, proving wired | +| 1D | Remaining Screens | S | **Done** — home, settings, coming-soon render | +| 1E | WebView App Shell | M | **Done** — providers/router/entry wired, biometrics + camera adapters connected, web fallbacks connected, lifecycle.ready() on mount | ## Validation Plan diff --git a/specs/person2-native-shells/OVERVIEW.md b/specs/person2-native-shells/OVERVIEW.md index 135efe17c..2570fc839 100644 --- a/specs/person2-native-shells/OVERVIEW.md +++ b/specs/person2-native-shells/OVERVIEW.md @@ -22,7 +22,7 @@ - [x] iOS Swift providers are implemented and wired (NFC, Biometrics, Lifecycle, WebView host + additional providers) - [x] `SelfSdk.launch()` flow is implemented on iOS - [x] Shared KMP validation baseline captured (`:shared:compileKotlinIosSimulatorArm64` + `:shared:jvmTest` successful) -- [ ] KMP test app validation on both platforms remains a follow-up validation task +- [x] KMP test app validation on both platforms completed (Android assemble + iOS compile) - [x] Platform asymmetry contract documented and signed off (iOS 9-handler superset vs Android 5-handler core set) - [x] MiniPay sample integration is wired (`SelfSdk.launch()` call path present) diff --git a/specs/person2-native-shells/SPEC.md b/specs/person2-native-shells/SPEC.md index 3585efb28..a27d25f8b 100644 --- a/specs/person2-native-shells/SPEC.md +++ b/specs/person2-native-shells/SPEC.md @@ -1694,7 +1694,7 @@ SelfSdkCallback fires on completion/dismissal | `SelfSdkCallback.onSuccess` fires | Integration | Result delivery from WebView through lifecycle handler | | `SelfSdkCallback.onCancelled` fires on dismiss | Integration | Dismiss wiring works correctly | -**Status: PARTIAL** (Android + iOS implementation paths are present; full cross-platform validation remains) +**Status: DONE** (Android common launch signature fixed, test app now exercises `SelfSdk.configure(...).launch(...)`, and validation gates pass) --- @@ -2058,20 +2058,20 @@ Chunk 2A: KMP Setup + Bridge Protocol (no deps -- start here) ## Completion Status -| Chunk | Description | Size | Status | -| ----- | ------------------------------------------ | ------ | --------------------------------------------------------------------------- | -| 2A | KMP Setup + Bridge Protocol | S ~3k | **Done** | -| 2B | Android WebView Host | S ~2k | **Done** | -| 2C | Android Native Handlers (5 handlers) | L ~12k | **Done** | -| 2D | iOS WebView Host + Provider Infrastructure | M ~6k | **Superseded** by 2G-2K (Swift wrapper pattern) | -| 2E | iOS Native Handlers (3 handlers) | M ~6k | **Superseded** by 2G-2K (Swift wrapper pattern) | -| 2F | SDK Public API + Test App | M ~5k | **Partial** (implementation present; validation/contract alignment pending) | -| 2G | Factory Infrastructure | S ~3k | **Done** | -| 2H | Biometric Handler (iOS) | S ~2k | **Done** | -| 2I | Lifecycle Handler (iOS) | S ~2k | **Done** | -| 2J | iOS WebView Host + SelfSdk.launch() | M ~5k | **Done** | -| 2K | NFC Handler (iOS) | M ~5k | **Done** | -| 2L | Camera MRZ (iOS, Phase 2) | S ~2k | **Skipped** (deferred) | +| Chunk | Description | Size | Status | +| ----- | ------------------------------------------ | ------ | ----------------------------------------------------------------------------------------- | +| 2A | KMP Setup + Bridge Protocol | S ~3k | **Done** | +| 2B | Android WebView Host | S ~2k | **Done** | +| 2C | Android Native Handlers (5 handlers) | L ~12k | **Done** | +| 2D | iOS WebView Host + Provider Infrastructure | M ~6k | **Superseded** by 2G-2K (Swift wrapper pattern) | +| 2E | iOS Native Handlers (3 handlers) | M ~6k | **Superseded** by 2G-2K (Swift wrapper pattern) | +| 2F | SDK Public API + Test App | M ~5k | **Done** (common Android launch fixed, test app launch screen wired, validation complete) | +| 2G | Factory Infrastructure | S ~3k | **Done** | +| 2H | Biometric Handler (iOS) | S ~2k | **Done** | +| 2I | Lifecycle Handler (iOS) | S ~2k | **Done** | +| 2J | iOS WebView Host + SelfSdk.launch() | M ~5k | **Done** | +| 2K | NFC Handler (iOS) | M ~5k | **Done** | +| 2L | Camera MRZ (iOS, Phase 2) | S ~2k | **Skipped** (deferred) | ## Validation Plan @@ -2161,11 +2161,12 @@ cd packages/self-sdk-swift && swift build ## Follow-Up (Out of Scope) -| Item | Discovered during | Suggested spec | -| ------------------------------ | ----------------- | --------------------------------------------------------------------------------------------- | -| Camera MRZ handler for iOS | Chunk 2L scoping | Phase 2 -- add to this spec when needed | -| SecureStorage handler for iOS | Design review | **Decided:** Add `SecureStorageProvider` to factory pattern (see SDK-OVERVIEW canonical rule) | -| Crypto signing handler for iOS | Design review | Depends on whether secure enclave signing is needed vs. Web Crypto | +| Item | Discovered during | Suggested spec | +| -------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Camera MRZ handler for iOS | Chunk 2L scoping | Phase 2 -- add to this spec when needed | +| SecureStorage handler for iOS | Design review | **Decided:** Add `SecureStorageProvider` to factory pattern (see SDK-OVERVIEW canonical rule) | +| Crypto signing handler for iOS | Design review | Depends on whether secure enclave signing is needed vs. Web Crypto | +| LifecycleBridgeHandler thin-wrapper refactor | PR #1805 review | Both Android and iOS `setResult()` have 4-branch business logic (interpreting `type`/`success`/`errorCode` to decide result codes / callback methods). Per PROJECT-RULES.md rule 22 ("no error mapping in native"), TypeScript should send an explicit `resultCode` or `outcome` field, and the handler should pass it through without interpretation. Touches both platform handlers + bridge protocol. | ## Spec Deviations diff --git a/specs/projects/kmp/KMP-ARCHITECTURE.md b/specs/projects/kmp/KMP-ARCHITECTURE.md new file mode 100644 index 000000000..a0edb3770 --- /dev/null +++ b/specs/projects/kmp/KMP-ARCHITECTURE.md @@ -0,0 +1,104 @@ +# KMP Architecture + +Last updated: March 5, 2026 +Owner: KMP program +Status: Draft + +## Purpose + +Define the canonical architecture for KMP SDK delivery, runtime behavior, and integration contracts. + +## Scope + +In scope: + +- KMP SDK (`packages/kmp-sdk`) +- KMP SDK test app (`packages/kmp-sdk-test-app` target name) +- Swift companion package (`packages/self-sdk-swift`) +- Bridge contract and handler lifecycle + +Out of scope: + +- Product requirements and roadmap sequencing (see `KMP-INITIATIVE.md`) +- Detailed task breakdowns per implementation chunk + +## System Context + +- Host apps launch SDK runtime. +- SDK runtime hosts WebView flow and routes bridge messages. +- Native handlers execute platform-specific actions. +- Results are returned through bridge responses to web layer. + +## Module Boundaries + +1. `kmp-sdk` + +- Public API surface (`configure`, `launch`, callbacks) +- Shared bridge models and router +- Android and iOS native handler bindings + +2. `self-sdk-swift` + +- iOS-native provider implementations +- Factory/config wiring for KMP iOS side + +3. `kmp-sdk-test-app` + +- Integration harness for Android/iOS manual validation +- Non-production sample host and verification scenarios + +## Runtime Flow + +1. App initializes SDK config. +2. SDK launches WebView host. +3. Web layer sends bridge request. +4. Router dispatches to native handler. +5. Native result/error marshalled to bridge response. +6. Web flow proceeds or fails with typed error. + +## Bridge Contract + +Required sections for future expansion: + +- Message envelope format +- Request/response typing rules +- Error code taxonomy +- Timeout/retry semantics +- Version compatibility policy + +## Platform Notes + +### Android + +- Activity/webview host lifecycle ownership +- NFC/camera/permission handling responsibilities +- Threading model expectations + +### iOS + +- Provider delegation into Swift package +- View controller presentation ownership +- Permission and lifecycle edge cases + +## Security and Privacy + +- Sensitive data handling boundaries +- Logging restrictions (no secrets/PII) +- Storage and retention rules + +## Validation Matrix + +- Unit: router + handler contracts +- Build: android + ios compile targets +- Integration: test app verification flows +- Device: physical NFC/passport success and failure cases + +## Open Decisions + +- [ ] Decision 1: _TBD_ +- [ ] Decision 2: _TBD_ +- [ ] Decision 3: _TBD_ + +## Change Log + +- 2026-03-05: Initial architecture skeleton created. diff --git a/specs/projects/kmp/KMP-INITIATIVE.md b/specs/projects/kmp/KMP-INITIATIVE.md new file mode 100644 index 000000000..cb321ea6d --- /dev/null +++ b/specs/projects/kmp/KMP-INITIATIVE.md @@ -0,0 +1,96 @@ +# KMP Initiative + +Last updated: March 5, 2026 +Owner: KMP program +Status: Draft + +## Problem Statement + +KMP specs and DX entrypoints are currently fragmented across mixed naming and locations, increasing onboarding time and execution errors for contributors and agents. + +## Goals + +- Standardize KMP naming across package folders, commands, and specs. +- Consolidate KMP specs under one project-intent hierarchy. +- Establish architecture and initiative docs as canonical entrypoints. +- Improve agent execution reliability with explicit ownership and validation. + +## Non-Goals + +- Redesigning non-KMP project spec systems in this initiative. +- Reworking product requirements outside KMP scope. +- Renaming KMP workspace package names. + +## Deliverables + +1. KMP spec tree under `specs/projects/kmp/` +2. Command taxonomy (`kmp:sdk:*`, `kmp:test-app:*`, `kmp:all:*`) +3. Test app folder rename to `kmp-sdk-test-app` (workspace package name unchanged) +4. Migration map from legacy paths and command aliases +5. Agent hygiene fields enforced in KMP workstream specs + +## Milestones + +1. Foundation docs + +- `KMP-SPECS-INDEX.md` +- `KMP-ARCHITECTURE.md` +- `KMP-INITIATIVE.md` + +2. Command migration + +- Add new namespaced commands +- Keep compatibility aliases + +3. Naming migration + +- Folder rename for test app (`packages/kmp-test-app` -> `packages/kmp-sdk-test-app`) +- Keep `@selfxyz/kmp-test-app` unchanged +- Repo-wide path reference update + +4. Spec migration + +- Move KMP-relevant docs into new hierarchy +- Add redirects/mapping + +5. Cleanup + +- Remove deprecated aliases after signoff window + +## Owners + +- Initiative lead: _TBD_ +- DX/commands: _TBD_ +- Specs migration: _TBD_ +- Validation/CI: _TBD_ + +## Dependencies + +- Agreement on final package folder naming +- Agreement on command namespace policy +- Coordination with ongoing KMP implementation work + +## Risks + +1. Stale links and references after migration +2. Temporary confusion during alias period +3. Spec drift without ownership/date stamping + +## Acceptance Criteria + +- New KMP entrypoint exists and is linked from top-level specs index. +- New commands cover all existing KMP workflows. +- Legacy references are either migrated or mapped. +- KMP specs include owner/dependencies/validation metadata. + +## Rollout Plan + +1. Land docs and command taxonomy. +2. Land package folder rename. +3. Land spec migration and mapping. +4. Remove deprecated aliases after agreed window. + +## Change Log + +- 2026-03-05: Initial initiative skeleton created. +- 2026-03-05: Updated naming plan to folder rename only; workspace package name unchanged. diff --git a/specs/projects/kmp/KMP-REORG-PLAN.md b/specs/projects/kmp/KMP-REORG-PLAN.md new file mode 100644 index 000000000..baa2aa4b4 --- /dev/null +++ b/specs/projects/kmp/KMP-REORG-PLAN.md @@ -0,0 +1,190 @@ +# KMP Specs and DX Reorganization Plan + +Last updated: March 5, 2026 +Owner: KMP program +Status: Proposed + +## Objective + +Reorganize KMP specs and developer experience surface areas so contributors and agents can find the right docs quickly, run the right commands consistently, and execute changes with clear ownership boundaries. + +## Success Criteria + +- KMP specs are grouped by project intent under a dedicated KMP project tree. +- KMP package naming is consistent (`kmp-sdk`, `kmp-sdk-test-app` folder naming). +- Root command surface is explicit, discoverable, and backward-compatible during migration. +- Architecture and initiative docs exist and become the canonical KMP entrypoints. +- Agents can navigate and execute work with minimal ambiguity. + +## Naming and Structure Standards + +1. Package naming standard + +- Keep SDK package as `kmp-sdk`. +- Rename test host app folder from `kmp-test-app` to `kmp-sdk-test-app`. +- Keep workspace package name as `@selfxyz/kmp-test-app` (no package rename). + +2. Spec naming standard + +- Use `KMP-` prefix for KMP-wide docs. +- Use `KMP--.md` for scoped docs (example: `KMP-NATIVE-API.md`). + +3. Canonical KMP spec tree + +- `specs/projects/kmp/README.md` +- `specs/projects/kmp/KMP-ARCHITECTURE.md` +- `specs/projects/kmp/KMP-INITIATIVE.md` +- `specs/projects/kmp/status/KMP-STATUS.md` +- `specs/projects/kmp/workstreams/*` +- `specs/projects/kmp/KMP-SPECS-INDEX.md` +- `specs/projects/kmp/KMP-DECISIONS.md` +- `specs/projects/kmp/KMP-CHANGELOG.md` + +## Command Surface Reorganization + +Update root `package.json` KMP scripts into explicit namespaces: + +1. SDK commands + +- `kmp:sdk:build` +- `kmp:sdk:test` +- `kmp:sdk:lint` +- `kmp:sdk:format` +- `kmp:sdk:clean` + +2. Test app commands + +- `kmp:test-app:android` +- `kmp:test-app:ios` +- `kmp:test-app:build` +- `kmp:test-app:test` +- `kmp:test-app:lint` +- `kmp:test-app:format` +- `kmp:test-app:clean` + +3. Orchestration commands + +- `kmp:all:check` (lint + test + build) +- `kmp:all:clean` +- `kmp:all:dev` + +4. Backward compatibility + +- Keep existing `kmp:*` aliases mapped to new commands for 1-2 release cycles. +- Add deprecation notes in script descriptions and docs. + +## Required New Docs + +1. `KMP-ARCHITECTURE.md` + +- Module boundaries and ownership +- Runtime flow diagrams (Android/iOS) +- Bridge contract and handler lifecycle +- Integration points with RN SDK/WebView artifacts +- Risk areas and validation matrix + +2. `KMP-INITIATIVE.md` + +- Problem statement +- Goals and non-goals +- Milestones and deliverables +- Owners and decision records +- Rollout and acceptance criteria + +## Agent-Focused Spec Hygiene + +Add the following to each KMP workstream spec: + +- `Owner` +- `Depends On` +- `Inputs` +- `Outputs` +- `Safe-to-edit paths` +- `Do-not-edit paths` +- `Validation commands` +- `Last verified` date + +Add a lightweight validator script to enforce required headings in KMP spec files. + +## Migration Plan + +### Phase 1: Foundations + +- Create KMP spec project tree under `specs/projects/kmp/`. +- Add `KMP-ARCHITECTURE.md` and `KMP-INITIATIVE.md` skeletons. +- Publish `KMP-SPECS-INDEX.md` as the KMP entrypoint. + +Exit criteria: + +- KMP entry docs exist and are linked from `specs/README.md`. + +### Phase 2: Command Taxonomy + +- Add new namespaced KMP commands to root `package.json`. +- Add compatibility aliases from old `kmp:*` commands. +- Document command matrix (`task -> command -> expected output`). + +Exit criteria: + +- All existing KMP workflows work through new commands. + +### Phase 3: Package Folder Rename + +- Rename folder `packages/kmp-test-app` to `packages/kmp-sdk-test-app`. +- Keep workspace package name as `@selfxyz/kmp-test-app`. +- Update all path references in scripts, Gradle settings, docs, and specs. + +Exit criteria: + +- No path references remain to `packages/kmp-test-app`. +- Workspace package name remains unchanged. + +### Phase 4: Spec Migration + +- Move KMP-relevant specs into `specs/projects/kmp/` buckets. +- Add compatibility index in root `specs/README.md` mapping old paths to new paths. +- Add `KMP-CHANGELOG.md` and `KMP-DECISIONS.md`. + +Exit criteria: + +- KMP spec navigation starts at one path and old references are redirected. + +### Phase 5: Deprecation Cleanup + +- Remove old command aliases after signoff window. +- Remove transitional links once references converge. +- Run full lint/types/build/tests for impacted workspaces. + +Exit criteria: + +- No transitional aliases or duplicate KMP spec locations remain. + +## Risks and Mitigations + +1. Broken references after rename + +- Mitigation: perform repo-wide search/replace + validation pass before merge. + +2. Temporary confusion during command transition + +- Mitigation: keep aliases and publish command matrix with examples. + +3. Spec drift after migration + +- Mitigation: add `Last verified` and heading-validator checks in CI. + +## Validation Checklist + +- `yarn lint` +- `yarn types` +- `yarn build` +- `yarn test` +- `rg -n "packages/kmp-test-app" .` +- `rg -n "specs/projects/kmp" specs/README.md` + +## Immediate Next Actions + +1. Create architecture and initiative doc skeletons in `specs/projects/kmp/`. +2. Add new root KMP command taxonomy with alias compatibility. +3. Prepare and execute package folder rename in one focused PR. +4. Migrate KMP specs and add redirect mapping. diff --git a/specs/projects/kmp/KMP-SPECS-INDEX.md b/specs/projects/kmp/KMP-SPECS-INDEX.md new file mode 100644 index 000000000..a34e83c33 --- /dev/null +++ b/specs/projects/kmp/KMP-SPECS-INDEX.md @@ -0,0 +1,41 @@ +# KMP Specs Index + +Last updated: March 5, 2026 +Owner: KMP program +Status: Draft + +## Start Here + +1. `KMP-INITIATIVE.md` — goals, scope, milestones. +2. `KMP-ARCHITECTURE.md` — technical boundaries and runtime model. +3. `KMP-REORG-PLAN.md` — migration phases and execution checklist. + +## Canonical Paths + +- `specs/projects/kmp/KMP-INITIATIVE.md` +- `specs/projects/kmp/KMP-ARCHITECTURE.md` +- `specs/projects/kmp/KMP-REORG-PLAN.md` +- `specs/projects/kmp/status/KMP-STATUS.md` (planned) +- `specs/projects/kmp/workstreams/` (planned) +- `specs/projects/kmp/KMP-DECISIONS.md` (planned) +- `specs/projects/kmp/KMP-CHANGELOG.md` (planned) + +## Working Rules for Agents + +- Prefer canonical KMP paths over legacy `person*` docs for KMP planning. +- Record `Last updated` date on each material change. +- Include validation commands in implementation-facing specs. +- Mark unresolved items in `Open Decisions` sections. + +## Migration Tracking + +- [x] Reorg plan created +- [x] Architecture skeleton created +- [x] Initiative skeleton created +- [ ] Status moved to `specs/projects/kmp/status/KMP-STATUS.md` +- [ ] Workstreams reorganized under `specs/projects/kmp/workstreams/` +- [ ] Legacy path mapping added to top-level `specs/README.md` + +## Change Log + +- 2026-03-05: Initial KMP spec index skeleton created.