From 730bc528bec28e19cd06a60452bde2e3f0950ae2 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Fri, 7 Aug 2026 18:00:38 +0800 Subject: [PATCH] feat: add installable Lingji Android shell --- .github/workflows/verify.yml | 4 + AGENTS.md | 16 +- app/build.gradle.kts | 141 +++++++++++++-- app/proguard-rules.pro | 1 + .../net/opcapp/flash/app/MainActivityTest.kt | 24 +++ app/src/main/AndroidManifest.xml | 24 +++ .../java/net/opcapp/flash/app/LingjiApp.kt | 34 ++++ .../net/opcapp/flash/app/LingjiApplication.kt | 7 + .../java/net/opcapp/flash/app/MainActivity.kt | 19 ++ .../flash/core/designsystem/LingjiTheme.kt | 84 +++++++++ .../flash}/core/model/HexagramContent.kt | 2 +- .../data/content/HexagramContentRepository.kt | 4 +- .../flash}/domain/casting/CastEngine.kt | 2 +- .../flash}/domain/casting/CastRecordDto.kt | 2 +- .../flash}/domain/casting/CastingModels.kt | 2 +- .../flash}/domain/casting/HexagramCatalog.kt | 2 +- .../feature/home/DevelopmentHomeScreen.kt | 131 ++++++++++++++ .../result/DomainVerificationScreen.kt | 164 ++++++++++++++++++ app/src/main/res/values-night/themes.xml | 10 ++ app/src/main/res/values/strings.xml | 18 ++ app/src/main/res/values/themes.xml | 10 ++ app/src/main/res/xml/backup_rules.xml | 8 + .../main/res/xml/data_extraction_rules.xml | 17 ++ .../content/FakeHexagramContentRepository.kt | 4 +- .../content/HexagramContentRepositoryTest.kt | 4 +- .../flash}/domain/casting/CastEngineTest.kt | 2 +- build.gradle.kts | 8 +- config/dependency-licenses.json | 120 ++++++++++++- docs/README.md | 7 +- docs/architecture.md | 8 +- docs/decisions.md | 15 +- docs/dependency-licenses.md | 43 +++-- docs/domain-rules.md | 2 +- docs/environment.md | 31 ++-- docs/implementation-plan.md | 16 +- docs/prototype.md | 2 +- docs/quality-gates.md | 17 +- gradle.properties | 3 + gradle/libs.versions.toml | 37 +++- scripts/verify-content-contract.mjs | 4 +- scripts/verify-dependency-licenses.mjs | 69 ++++++-- scripts/verify-domain-boundaries.mjs | 12 +- settings.gradle.kts | 2 +- 43 files changed, 1007 insertions(+), 125 deletions(-) create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/java/net/opcapp/flash/app/MainActivityTest.kt create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/java/net/opcapp/flash/app/LingjiApp.kt create mode 100644 app/src/main/java/net/opcapp/flash/app/LingjiApplication.kt create mode 100644 app/src/main/java/net/opcapp/flash/app/MainActivity.kt create mode 100644 app/src/main/java/net/opcapp/flash/core/designsystem/LingjiTheme.kt rename app/src/main/java/{brainwave => net/opcapp/flash}/core/model/HexagramContent.kt (97%) rename app/src/main/java/{brainwave => net/opcapp/flash}/data/content/HexagramContentRepository.kt (76%) rename app/src/main/java/{brainwave => net/opcapp/flash}/domain/casting/CastEngine.kt (98%) rename app/src/main/java/{brainwave => net/opcapp/flash}/domain/casting/CastRecordDto.kt (98%) rename app/src/main/java/{brainwave => net/opcapp/flash}/domain/casting/CastingModels.kt (98%) rename app/src/main/java/{brainwave => net/opcapp/flash}/domain/casting/HexagramCatalog.kt (99%) create mode 100644 app/src/main/java/net/opcapp/flash/feature/home/DevelopmentHomeScreen.kt create mode 100644 app/src/main/java/net/opcapp/flash/feature/result/DomainVerificationScreen.kt create mode 100644 app/src/main/res/values-night/themes.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/themes.xml create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml rename app/src/test/java/{brainwave => net/opcapp/flash}/data/content/FakeHexagramContentRepository.kt (92%) rename app/src/test/java/{brainwave => net/opcapp/flash}/data/content/HexagramContentRepositoryTest.kt (95%) rename app/src/test/java/{brainwave => net/opcapp/flash}/domain/casting/CastEngineTest.kt (99%) create mode 100644 gradle.properties diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index d6376e1..ff698f8 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -23,6 +23,10 @@ jobs: with: node-version: "22" package-manager-cache: false + - name: Set up Android SDK + uses: android-actions/setup-android@v3 + - name: Install pinned Android SDK components + run: sdkmanager "platforms;android-34" "build-tools;34.0.0" - name: Set up Gradle uses: gradle/actions/setup-gradle@v6 - name: Run local quality gates diff --git a/AGENTS.md b/AGENTS.md index 0f18a9b..dea6c17 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,19 +30,19 @@ tools are unavailable or insufficient. State the fallback in the handoff. ## Current build boundary -The repository uses `brainwave` only as a code name. Formal product name, -organization-owned application ID, and final `minSdk` remain product decisions. -Until they are accepted, the `app` project is a Kotlin/JVM domain harness, not a -publishable Android application. Do not invent placeholder release identity. +The repository keeps `brainwave` only as its code name. The accepted Android +identity is product name `灵机`, namespace/application ID `net.opcapp.flash`, and +`minSdk` 26. The `app` project is an installable Android application; the app icon +and store assets remain product decisions and must not be silently finalized. ## Verification Run from the repository root: ```powershell -.\gradlew.bat verifyLocal --offline +.\gradlew.bat verifyLocal ``` -After the Android application plugin is configured, also run the Android gates -listed in `docs/quality-gates.md`. Report skipped device or Android checks -explicitly; do not imply they passed. +`verifyLocal` includes Android lint, debug unit tests, APK assembly, repository +checks, and dependency-license coverage. Run `connectedDebugAndroidTest` separately +when an authorized device is present, and report skipped device checks explicitly. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a548856..ed1e105 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,26 +1,110 @@ import groovy.json.JsonSlurper +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.kapt) + alias(libs.plugins.hilt) +} + +android { + namespace = "net.opcapp.flash" + compileSdk = 34 + + defaultConfig { + applicationId = "net.opcapp.flash" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "0.1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true + + javaCompileOptions { + annotationProcessorOptions { + arguments += mapOf("room.schemaLocation" to "$projectDir/schemas") + } + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + + buildFeatures { + compose = true + buildConfig = false + } + + composeOptions { + kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + packaging { + resources.excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + + testOptions { + unitTests.isReturnDefaultValues = true + } } kotlin { jvmToolchain(17) - - sourceSets { - main { - kotlin.srcDir("src/main/java") - } - test { - kotlin.srcDir("src/test/java") - } - } } + +kapt { + correctErrorTypes = true +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = "17" +} + dependencies { + val composeBom = platform(libs.androidx.compose.bom) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.compose) + implementation(composeBom) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + kapt(libs.androidx.room.compiler) + implementation(libs.androidx.datastore.preferences) + implementation(libs.hilt.android) + kapt(libs.hilt.compiler) + testImplementation(libs.junit) + + androidTestImplementation(composeBom) + androidTestImplementation(libs.androidx.test.ext.junit) + androidTestImplementation(libs.androidx.test.runner) + androidTestImplementation(libs.androidx.compose.ui.test.junit4) + + debugImplementation(libs.androidx.compose.ui.tooling) + debugImplementation(libs.androidx.compose.ui.test.manifest) } -tasks.test { +tasks.withType().configureEach { useJUnit() testLogging { events("failed", "skipped") @@ -30,25 +114,44 @@ tasks.test { tasks.register("verifyDependencyLicenses") { group = "verification" - description = "Ensures every resolved JVM runtime/test artifact has a reviewed license entry." + description = "Ensures every resolved debug runtime/test artifact has a reviewed license entry." doLast { @Suppress("UNCHECKED_CAST") val manifest = JsonSlurper().parse(rootProject.file("config/dependency-licenses.json")) as Map @Suppress("UNCHECKED_CAST") val components = manifest.getValue("components") as List> + @Suppress("UNCHECKED_CAST") + val groupPolicies = manifest.getValue("groupPolicies") as List> val reviewedCoordinates = components.mapNotNull { it["coordinates"] as String? }.toSet() - val resolvedCoordinates = configurations.getByName("testRuntimeClasspath") - .resolvedConfiguration - .resolvedArtifacts - .map { artifact -> - val id = artifact.moduleVersion.id - "${id.group}:${id.name}:${id.version}" + val resolvedCoordinates = listOf("debugRuntimeClasspath", "debugUnitTestRuntimeClasspath") + .flatMap { configurationName -> + configurations.getByName(configurationName) + .resolvedConfiguration + .resolvedArtifacts + .mapNotNull { artifact -> + if (artifact.id.componentIdentifier !is org.gradle.api.artifacts.component.ModuleComponentIdentifier) { + return@mapNotNull null + } + val id = artifact.moduleVersion.id + "${id.group}:${id.name}:${id.version}" + } } .toSet() - val missing = resolvedCoordinates - reviewedCoordinates + val missing = resolvedCoordinates.filterNot { coordinates -> + if (coordinates in reviewedCoordinates) return@filterNot true + val group = coordinates.substringBefore(":") + groupPolicies.any { policy -> + val prefix = policy.getValue("groupPrefix") as String + group == prefix || group.startsWith("$prefix.") + } + } check(missing.isEmpty()) { "Dependencies missing reviewed license entries: ${missing.sorted().joinToString()}" } + logger.lifecycle( + "Dependency license coverage passed for ${resolvedCoordinates.size} artifacts " + + "with ${groupPolicies.size} reviewed group policies.", + ) } } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..6783d80 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1 @@ +# Project-specific R8 rules will be added when release minification is enabled. diff --git a/app/src/androidTest/java/net/opcapp/flash/app/MainActivityTest.kt b/app/src/androidTest/java/net/opcapp/flash/app/MainActivityTest.kt new file mode 100644 index 0000000..641cf16 --- /dev/null +++ b/app/src/androidTest/java/net/opcapp/flash/app/MainActivityTest.kt @@ -0,0 +1,24 @@ +package net.opcapp.flash.app + +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class MainActivityTest { + @get:Rule + val composeRule = createAndroidComposeRule() + + @Test + fun domainFixtureCanBeVerifiedFromHome() { + composeRule.onNodeWithText("灵机").assertIsDisplayed() + composeRule.onNodeWithText("验证起卦核心").performClick() + composeRule.onNodeWithText("复 24 → 坤 2").assertIsDisplayed() + composeRule.onNodeWithText("初爻动").assertIsDisplayed() + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..91f9a37 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/java/net/opcapp/flash/app/LingjiApp.kt b/app/src/main/java/net/opcapp/flash/app/LingjiApp.kt new file mode 100644 index 0000000..6e4abef --- /dev/null +++ b/app/src/main/java/net/opcapp/flash/app/LingjiApp.kt @@ -0,0 +1,34 @@ +package net.opcapp.flash.app + +import androidx.compose.runtime.Composable +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import net.opcapp.flash.feature.home.DevelopmentHomeScreen +import net.opcapp.flash.feature.result.DomainVerificationScreen + +private object Destination { + const val Home = "home" + const val DomainResult = "domain-result" +} + +@Composable +fun LingjiApp() { + val navController = rememberNavController() + + NavHost( + navController = navController, + startDestination = Destination.Home, + ) { + composable(Destination.Home) { + DevelopmentHomeScreen( + onVerifyDomain = { navController.navigate(Destination.DomainResult) }, + ) + } + composable(Destination.DomainResult) { + DomainVerificationScreen( + onBack = navController::popBackStack, + ) + } + } +} diff --git a/app/src/main/java/net/opcapp/flash/app/LingjiApplication.kt b/app/src/main/java/net/opcapp/flash/app/LingjiApplication.kt new file mode 100644 index 0000000..dcafd62 --- /dev/null +++ b/app/src/main/java/net/opcapp/flash/app/LingjiApplication.kt @@ -0,0 +1,7 @@ +package net.opcapp.flash.app + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class LingjiApplication : Application() diff --git a/app/src/main/java/net/opcapp/flash/app/MainActivity.kt b/app/src/main/java/net/opcapp/flash/app/MainActivity.kt new file mode 100644 index 0000000..db57180 --- /dev/null +++ b/app/src/main/java/net/opcapp/flash/app/MainActivity.kt @@ -0,0 +1,19 @@ +package net.opcapp.flash.app + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import dagger.hilt.android.AndroidEntryPoint +import net.opcapp.flash.core.designsystem.LingjiTheme + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + LingjiTheme { + LingjiApp() + } + } + } +} diff --git a/app/src/main/java/net/opcapp/flash/core/designsystem/LingjiTheme.kt b/app/src/main/java/net/opcapp/flash/core/designsystem/LingjiTheme.kt new file mode 100644 index 0000000..85c186e --- /dev/null +++ b/app/src/main/java/net/opcapp/flash/core/designsystem/LingjiTheme.kt @@ -0,0 +1,84 @@ +package net.opcapp.flash.core.designsystem + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Typography +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight + +private val Paper = Color(0xFFF5F0E6) +private val RaisedPaper = Color(0xFFFFFBF3) +private val Ink = Color(0xFF26231F) +private val MutedInk = Color(0xFF665F55) +private val Cinnabar = Color(0xFFA33A2B) +private val DarkPaper = Color(0xFF1E1B18) +private val DarkRaisedPaper = Color(0xFF292520) +private val PaleInk = Color(0xFFEAE2D5) +private val PaleCinnabar = Color(0xFFFFB4A5) + +private val LightColors = lightColorScheme( + primary = Cinnabar, + onPrimary = Color.White, + primaryContainer = Color(0xFFF4DAD3), + onPrimaryContainer = Color(0xFF3D0902), + background = Paper, + onBackground = Ink, + surface = RaisedPaper, + onSurface = Ink, + surfaceVariant = Color(0xFFE8E0D3), + onSurfaceVariant = MutedInk, + outline = Color(0xFF81786C), +) + +private val DarkColors = darkColorScheme( + primary = PaleCinnabar, + onPrimary = Color(0xFF5F160C), + primaryContainer = Color(0xFF7F261A), + onPrimaryContainer = Color(0xFFFFDAD2), + background = DarkPaper, + onBackground = PaleInk, + surface = DarkRaisedPaper, + onSurface = PaleInk, + surfaceVariant = Color(0xFF4C453D), + onSurfaceVariant = Color(0xFFD1C6B8), + outline = Color(0xFF9C9184), +) + +private val LingjiTypography = Typography().let { defaults -> + defaults.copy( + displaySmall = defaults.displaySmall.copy( + fontFamily = FontFamily.Serif, + fontWeight = FontWeight.SemiBold, + ), + headlineMedium = defaults.headlineMedium.copy( + fontFamily = FontFamily.Serif, + fontWeight = FontWeight.Medium, + ), + titleLarge = defaults.titleLarge.copy( + fontFamily = FontFamily.Serif, + fontWeight = FontWeight.Medium, + ), + bodyLarge = defaults.bodyLarge.copy(fontFamily = FontFamily.SansSerif), + bodyMedium = defaults.bodyMedium.copy(fontFamily = FontFamily.SansSerif), + labelLarge = defaults.labelLarge.copy( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.SemiBold, + ), + ) +} + +@Composable +fun LingjiTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + MaterialTheme( + colorScheme = if (darkTheme) DarkColors else LightColors, + typography = LingjiTypography, + content = content, + ) +} diff --git a/app/src/main/java/brainwave/core/model/HexagramContent.kt b/app/src/main/java/net/opcapp/flash/core/model/HexagramContent.kt similarity index 97% rename from app/src/main/java/brainwave/core/model/HexagramContent.kt rename to app/src/main/java/net/opcapp/flash/core/model/HexagramContent.kt index 089c5d5..71c6c81 100644 --- a/app/src/main/java/brainwave/core/model/HexagramContent.kt +++ b/app/src/main/java/net/opcapp/flash/core/model/HexagramContent.kt @@ -1,4 +1,4 @@ -package brainwave.core.model +package net.opcapp.flash.core.model data class HexagramContent( val kingWenNumber: Int, diff --git a/app/src/main/java/brainwave/data/content/HexagramContentRepository.kt b/app/src/main/java/net/opcapp/flash/data/content/HexagramContentRepository.kt similarity index 76% rename from app/src/main/java/brainwave/data/content/HexagramContentRepository.kt rename to app/src/main/java/net/opcapp/flash/data/content/HexagramContentRepository.kt index 7ce2adb..9839921 100644 --- a/app/src/main/java/brainwave/data/content/HexagramContentRepository.kt +++ b/app/src/main/java/net/opcapp/flash/data/content/HexagramContentRepository.kt @@ -1,6 +1,6 @@ -package brainwave.data.content +package net.opcapp.flash.data.content -import brainwave.core.model.HexagramContent +import net.opcapp.flash.core.model.HexagramContent interface HexagramContentRepository { val contentVersion: String diff --git a/app/src/main/java/brainwave/domain/casting/CastEngine.kt b/app/src/main/java/net/opcapp/flash/domain/casting/CastEngine.kt similarity index 98% rename from app/src/main/java/brainwave/domain/casting/CastEngine.kt rename to app/src/main/java/net/opcapp/flash/domain/casting/CastEngine.kt index 26ebe5d..bd6a1ca 100644 --- a/app/src/main/java/brainwave/domain/casting/CastEngine.kt +++ b/app/src/main/java/net/opcapp/flash/domain/casting/CastEngine.kt @@ -1,4 +1,4 @@ -package brainwave.domain.casting +package net.opcapp.flash.domain.casting data class CastComputation internal constructor( val roundsBottomUp: List, diff --git a/app/src/main/java/brainwave/domain/casting/CastRecordDto.kt b/app/src/main/java/net/opcapp/flash/domain/casting/CastRecordDto.kt similarity index 98% rename from app/src/main/java/brainwave/domain/casting/CastRecordDto.kt rename to app/src/main/java/net/opcapp/flash/domain/casting/CastRecordDto.kt index b035d77..8e5f815 100644 --- a/app/src/main/java/brainwave/domain/casting/CastRecordDto.kt +++ b/app/src/main/java/net/opcapp/flash/domain/casting/CastRecordDto.kt @@ -1,4 +1,4 @@ -package brainwave.domain.casting +package net.opcapp.flash.domain.casting data class CastRecordDto( val schemaVersion: Int, diff --git a/app/src/main/java/brainwave/domain/casting/CastingModels.kt b/app/src/main/java/net/opcapp/flash/domain/casting/CastingModels.kt similarity index 98% rename from app/src/main/java/brainwave/domain/casting/CastingModels.kt rename to app/src/main/java/net/opcapp/flash/domain/casting/CastingModels.kt index fc9d0b5..f8c05c6 100644 --- a/app/src/main/java/brainwave/domain/casting/CastingModels.kt +++ b/app/src/main/java/net/opcapp/flash/domain/casting/CastingModels.kt @@ -1,4 +1,4 @@ -package brainwave.domain.casting +package net.opcapp.flash.domain.casting enum class CoinSide(val score: Int) { CHARACTER(2), diff --git a/app/src/main/java/brainwave/domain/casting/HexagramCatalog.kt b/app/src/main/java/net/opcapp/flash/domain/casting/HexagramCatalog.kt similarity index 99% rename from app/src/main/java/brainwave/domain/casting/HexagramCatalog.kt rename to app/src/main/java/net/opcapp/flash/domain/casting/HexagramCatalog.kt index bf2e8f1..36ae372 100644 --- a/app/src/main/java/brainwave/domain/casting/HexagramCatalog.kt +++ b/app/src/main/java/net/opcapp/flash/domain/casting/HexagramCatalog.kt @@ -1,4 +1,4 @@ -package brainwave.domain.casting +package net.opcapp.flash.domain.casting object HexagramCatalog { private enum class Trigram(val patternBottomUp: String) { diff --git a/app/src/main/java/net/opcapp/flash/feature/home/DevelopmentHomeScreen.kt b/app/src/main/java/net/opcapp/flash/feature/home/DevelopmentHomeScreen.kt new file mode 100644 index 0000000..940b5ee --- /dev/null +++ b/app/src/main/java/net/opcapp/flash/feature/home/DevelopmentHomeScreen.kt @@ -0,0 +1,131 @@ +package net.opcapp.flash.feature.home + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +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.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import net.opcapp.flash.R + +@Composable +fun DevelopmentHomeScreen( + onVerifyDomain: () -> Unit, + modifier: Modifier = Modifier, +) { + Surface( + modifier = modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + Column( + modifier = Modifier + .windowInsetsPadding(WindowInsets.safeDrawing) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp, vertical = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.development_build), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(Modifier.height(28.dp)) + HexagramSeal() + Spacer(Modifier.height(20.dp)) + Text( + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.displaySmall, + color = MaterialTheme.colorScheme.onBackground, + ) + Text( + text = stringResource(R.string.home_tagline), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 8.dp), + ) + Spacer(Modifier.height(32.dp)) + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), + shape = RoundedCornerShape(20.dp), + ) { + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Text( + text = stringResource(R.string.core_check_title), + style = MaterialTheme.typography.titleLarge, + ) + Text( + text = stringResource(R.string.core_check_body), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Button( + onClick = onVerifyDomain, + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + shape = RoundedCornerShape(14.dp), + contentPadding = PaddingValues(horizontal = 20.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + ) { + Text(stringResource(R.string.verify_casting_core)) + } + } + } + Spacer(Modifier.height(32.dp)) + Text( + text = stringResource(R.string.local_data_notice), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + } +} + +@Composable +private fun HexagramSeal() { + Surface( + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(18.dp), + modifier = Modifier.height(72.dp), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.padding(horizontal = 22.dp), + ) { + Text( + text = stringResource(R.string.seal_character), + style = MaterialTheme.typography.headlineMedium, + color = MaterialTheme.colorScheme.onPrimary, + ) + } + } +} diff --git a/app/src/main/java/net/opcapp/flash/feature/result/DomainVerificationScreen.kt b/app/src/main/java/net/opcapp/flash/feature/result/DomainVerificationScreen.kt new file mode 100644 index 0000000..91d65ad --- /dev/null +++ b/app/src/main/java/net/opcapp/flash/feature/result/DomainVerificationScreen.kt @@ -0,0 +1,164 @@ +package net.opcapp.flash.feature.result + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +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.layout.safeDrawing +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import net.opcapp.flash.R +import net.opcapp.flash.domain.casting.CastEngine +import net.opcapp.flash.domain.casting.CastRound +import net.opcapp.flash.domain.casting.LineValue +import net.opcapp.flash.domain.casting.Polarity + +@Composable +fun DomainVerificationScreen( + onBack: () -> Unit, + modifier: Modifier = Modifier, +) { + val result = remember { + CastEngine.cast( + listOf( + CastRound.fromLineValue(LineValue.OLD_YANG), + CastRound.fromLineValue(LineValue.YOUNG_YIN), + CastRound.fromLineValue(LineValue.YOUNG_YIN), + CastRound.fromLineValue(LineValue.YOUNG_YIN), + CastRound.fromLineValue(LineValue.YOUNG_YIN), + CastRound.fromLineValue(LineValue.YOUNG_YIN), + ), + ) + } + + Surface( + modifier = modifier.fillMaxSize(), + color = MaterialTheme.colorScheme.background, + ) { + Column( + modifier = Modifier + .windowInsetsPadding(WindowInsets.safeDrawing) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp, vertical = 12.dp), + ) { + TextButton( + onClick = onBack, + modifier = Modifier.height(48.dp), + ) { + Text(stringResource(R.string.back)) + } + Spacer(Modifier.height(24.dp)) + Text( + text = stringResource(R.string.core_check_passed), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringResource( + R.string.fixture_result, + result.primaryHexagramId.value, + result.transformedHexagramId.value, + ), + style = MaterialTheme.typography.headlineMedium, + modifier = Modifier.padding(top = 12.dp), + ) + Text( + text = stringResource(R.string.moving_line_result), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 8.dp), + ) + Spacer(Modifier.height(36.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + HexagramDiagram( + linesBottomUp = result.primaryPatternBottomUp.linesBottomUp, + description = stringResource(R.string.primary_hexagram_description), + ) + HexagramDiagram( + linesBottomUp = result.transformedPatternBottomUp.linesBottomUp, + description = stringResource(R.string.transformed_hexagram_description), + ) + } + Spacer(Modifier.height(36.dp)) + Text( + text = stringResource(R.string.fixture_explanation), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } +} + +@Composable +private fun HexagramDiagram( + linesBottomUp: List, + description: String, +) { + val ink = MaterialTheme.colorScheme.onBackground + val lineWidth = with(LocalDensity.current) { 58.dp.toPx() } + val gap = with(LocalDensity.current) { 12.dp.toPx() } + val strokeWidth = with(LocalDensity.current) { 5.dp.toPx() } + + Canvas( + modifier = Modifier + .size(width = 88.dp, height = 136.dp) + .semantics { contentDescription = description }, + ) { + val verticalStep = size.height / 6f + linesBottomUp.forEachIndexed { index, polarity -> + val y = size.height - verticalStep * (index + 0.5f) + val left = (size.width - lineWidth) / 2f + val right = left + lineWidth + if (polarity == Polarity.YANG) { + drawLine( + color = ink, + start = Offset(left, y), + end = Offset(right, y), + strokeWidth = strokeWidth, + cap = StrokeCap.Square, + ) + } else { + val center = size.width / 2f + drawLine( + color = ink, + start = Offset(left, y), + end = Offset(center - gap / 2f, y), + strokeWidth = strokeWidth, + cap = StrokeCap.Square, + ) + drawLine( + color = ink, + start = Offset(center + gap / 2f, y), + end = Offset(right, y), + strokeWidth = strokeWidth, + cap = StrokeCap.Square, + ) + } + } + } +} diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..6db5f02 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..d353134 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,18 @@ + + + 灵机 + 内部构建 · 开发验证页 + 本机起卦 · 离线可用 + 易 + 领域核心已接入 + 用固定六爻夹具验证卦象、变卦与动爻计算链路。 + 验证起卦核心 + 当前验证页不写入记录。正式功能将默认保存在本机,只有明确发起 AI 解读时才会联网。 + 返回 + 计算通过 + 复 %1$d → 坤 %2$d + 初爻动 + 固定输入为初爻老阳、其余五爻少阴。结果来自纯 Kotlin 起卦核心,不是界面写死的演示值。 + 本卦复,第二十四卦 + 变卦坤,第二卦 + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..8c19c62 --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,10 @@ + + + + diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..ce0b324 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..491c90f --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/test/java/brainwave/data/content/FakeHexagramContentRepository.kt b/app/src/test/java/net/opcapp/flash/data/content/FakeHexagramContentRepository.kt similarity index 92% rename from app/src/test/java/brainwave/data/content/FakeHexagramContentRepository.kt rename to app/src/test/java/net/opcapp/flash/data/content/FakeHexagramContentRepository.kt index 95b35a4..05b54aa 100644 --- a/app/src/test/java/brainwave/data/content/FakeHexagramContentRepository.kt +++ b/app/src/test/java/net/opcapp/flash/data/content/FakeHexagramContentRepository.kt @@ -1,6 +1,6 @@ -package brainwave.data.content +package net.opcapp.flash.data.content -import brainwave.core.model.HexagramContent +import net.opcapp.flash.core.model.HexagramContent class FakeHexagramContentRepository( override val contentVersion: String = "fixture-only-v1", diff --git a/app/src/test/java/brainwave/data/content/HexagramContentRepositoryTest.kt b/app/src/test/java/net/opcapp/flash/data/content/HexagramContentRepositoryTest.kt similarity index 95% rename from app/src/test/java/brainwave/data/content/HexagramContentRepositoryTest.kt rename to app/src/test/java/net/opcapp/flash/data/content/HexagramContentRepositoryTest.kt index 4a074d0..9a60284 100644 --- a/app/src/test/java/brainwave/data/content/HexagramContentRepositoryTest.kt +++ b/app/src/test/java/net/opcapp/flash/data/content/HexagramContentRepositoryTest.kt @@ -1,6 +1,6 @@ -package brainwave.data.content +package net.opcapp.flash.data.content -import brainwave.core.model.HexagramContent +import net.opcapp.flash.core.model.HexagramContent import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Assert.fail diff --git a/app/src/test/java/brainwave/domain/casting/CastEngineTest.kt b/app/src/test/java/net/opcapp/flash/domain/casting/CastEngineTest.kt similarity index 99% rename from app/src/test/java/brainwave/domain/casting/CastEngineTest.kt rename to app/src/test/java/net/opcapp/flash/domain/casting/CastEngineTest.kt index 35c2ea4..31f213e 100644 --- a/app/src/test/java/brainwave/domain/casting/CastEngineTest.kt +++ b/app/src/test/java/net/opcapp/flash/domain/casting/CastEngineTest.kt @@ -1,4 +1,4 @@ -package brainwave.domain.casting +package net.opcapp.flash.domain.casting import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse diff --git a/build.gradle.kts b/build.gradle.kts index 3733714..02162b6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,6 +2,10 @@ import org.gradle.api.tasks.Exec plugins { base + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.kapt) apply false + alias(libs.plugins.hilt) apply false } fun registerNodeVerificationTask( @@ -61,7 +65,9 @@ tasks.register("verifyLocal") { group = LifecycleBasePlugin.VERIFICATION_GROUP description = "Runs every environment-independent local quality gate." dependsOn( - ":app:test", + ":app:lintDebug", + ":app:testDebugUnitTest", + ":app:assembleDebug", ":app:verifyDependencyLicenses", verifyDocs, scanSecrets, diff --git a/config/dependency-licenses.json b/config/dependency-licenses.json index 1361520..7966779 100644 --- a/config/dependency-licenses.json +++ b/config/dependency-licenses.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, - "reviewedAt": "2026-08-04", - "scopeNote": "Current JVM runtime/test graph and direct build entry points; Android dependencies are not configured yet.", + "reviewedAt": "2026-08-07", + "scopeNote": "Android debug runtime and debug unit-test runtime graphs, plus direct build entry points. Release notices still require a release-graph review.", "components": [ { "id": "gradle-wrapper", @@ -11,6 +11,14 @@ "license": "Apache-2.0", "source": "https://github.com/gradle/gradle/blob/v8.2.0/LICENSE" }, + { + "id": "android-gradle-plugin", + "coordinates": null, + "version": "8.2.0", + "scope": "build", + "license": "Apache-2.0", + "source": "https://android.googlesource.com/platform/tools/base/+/studio-2022.2.1-patch4/LICENSE" + }, { "id": "kotlin-gradle-plugin", "coordinates": null, @@ -19,6 +27,54 @@ "license": "Apache-2.0", "source": "https://github.com/JetBrains/kotlin/blob/v1.9.20/license/LICENSE.txt" }, + { + "id": "hilt-gradle-plugin", + "coordinates": null, + "version": "2.48.1", + "scope": "build", + "license": "Apache-2.0", + "source": "https://github.com/google/dagger/blob/dagger-2.48.1/LICENSE.txt" + }, + { + "id": "compose-bom", + "coordinates": "androidx.compose:compose-bom:2023.10.01", + "version": "2023.10.01", + "scope": "runtime-constraints", + "license": "Apache-2.0", + "source": "https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt" + }, + { + "id": "navigation-compose", + "coordinates": "androidx.navigation:navigation-compose:2.7.5", + "version": "2.7.5", + "scope": "runtime", + "license": "Apache-2.0", + "source": "https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt" + }, + { + "id": "room-runtime", + "coordinates": "androidx.room:room-runtime:2.6.1", + "version": "2.6.1", + "scope": "runtime", + "license": "Apache-2.0", + "source": "https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt" + }, + { + "id": "datastore-preferences", + "coordinates": "androidx.datastore:datastore-preferences:1.0.0", + "version": "1.0.0", + "scope": "runtime", + "license": "Apache-2.0", + "source": "https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt" + }, + { + "id": "hilt-android", + "coordinates": "com.google.dagger:hilt-android:2.48.1", + "version": "2.48.1", + "scope": "runtime", + "license": "Apache-2.0", + "source": "https://github.com/google/dagger/blob/dagger-2.48.1/LICENSE.txt" + }, { "id": "kotlin-stdlib", "coordinates": "org.jetbrains.kotlin:kotlin-stdlib:1.9.20", @@ -29,11 +85,35 @@ }, { "id": "jetbrains-annotations", - "coordinates": "org.jetbrains:annotations:13.0", - "version": "13.0", + "coordinates": "org.jetbrains:annotations:23.0.0", + "version": "23.0.0", "scope": "runtime-transitive", "license": "Apache-2.0", - "source": "https://repo1.maven.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0.pom" + "source": "https://repo1.maven.org/maven2/org/jetbrains/annotations/23.0.0/annotations-23.0.0.pom" + }, + { + "id": "jsr305", + "coordinates": "com.google.code.findbugs:jsr305:3.0.2", + "version": "3.0.2", + "scope": "runtime-transitive", + "license": "BSD-3-Clause", + "source": "https://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.pom" + }, + { + "id": "guava-listenablefuture", + "coordinates": "com.google.guava:listenablefuture:1.0", + "version": "1.0", + "scope": "runtime-transitive", + "license": "Apache-2.0", + "source": "https://repo1.maven.org/maven2/com/google/guava/listenablefuture/1.0/listenablefuture-1.0.pom" + }, + { + "id": "javax-inject", + "coordinates": "javax.inject:javax.inject:1", + "version": "1", + "scope": "runtime-transitive", + "license": "Apache-2.0", + "source": "https://repo1.maven.org/maven2/javax/inject/javax.inject/1/javax.inject-1.pom" }, { "id": "junit4", @@ -51,5 +131,35 @@ "license": "BSD-3-Clause", "source": "https://github.com/hamcrest/JavaHamcrest/blob/hamcrest-java-1.3/LICENSE.txt" } + ], + "groupPolicies": [ + { + "id": "androidx-runtime-policy", + "groupPrefix": "androidx", + "scope": "runtime-transitive", + "license": "Apache-2.0", + "source": "https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt" + }, + { + "id": "dagger-runtime-policy", + "groupPrefix": "com.google.dagger", + "scope": "runtime-transitive", + "license": "Apache-2.0", + "source": "https://github.com/google/dagger/blob/dagger-2.48.1/LICENSE.txt" + }, + { + "id": "kotlin-runtime-policy", + "groupPrefix": "org.jetbrains.kotlin", + "scope": "runtime-transitive", + "license": "Apache-2.0", + "source": "https://github.com/JetBrains/kotlin/blob/v1.9.20/license/LICENSE.txt" + }, + { + "id": "kotlinx-runtime-policy", + "groupPrefix": "org.jetbrains.kotlinx", + "scope": "runtime-transitive", + "license": "Apache-2.0", + "source": "https://github.com/Kotlin/kotlinx.coroutines/blob/1.7.1/LICENSE.txt" + } ] } diff --git a/docs/README.md b/docs/README.md index 05fb718..6f950f6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,14 +1,14 @@ # Brainwave 项目文档 > 文档状态:方案基线 -> 最后核验:2026-08-04 -> 当前阶段:P-1 v0.3 待视觉复核;P1 纯 Kotlin 领域核心已完成;P0 仓库门禁与 P2 内容契约已部分完成;可发布 Android 壳仍等待正式身份与 `minSdk` 决策 +> 最后核验:2026-08-07 +> 当前阶段:正式产品名“灵机”;P0 Android 壳与本地门禁已建立,真机闭环正在执行;P-1 v0.3 待品牌文案刷新和视觉复核;P1 已完成;P2 授权内容未开始 本目录是 Brainwave 的项目知识事实源。产品决策、领域算法、架构边界、验收标准和已知失败模式必须写入仓库;聊天记录、口头约定和临时提示不构成项目规范。 ## 项目一句话 -Brainwave 是一个以《易经》三枚铜币法为文化背景的 Android 个人反思工具:用户亲手投掷并录入六次结果,应用在本地确定本卦、之卦和动爻;用户主动点击「解」后,本地内容或 AI 才把既有结果翻译成现代语言,并收束到一项低风险、可撤销的现实行动。 +灵机(仓库代号 Brainwave)是一个以《易经》三枚铜币法为文化背景的 Android 个人反思工具:用户亲手投掷并录入六次结果,应用在本地确定本卦、之卦和动爻;用户主动点击「解」后,本地内容或 AI 才把既有结果翻译成现代语言,并收束到一项低风险、可撤销的现实行动。 ## 文档地图 @@ -69,6 +69,7 @@ Brainwave 是一个以《易经》三枚铜币法为文化背景的 Android 个 已确认: - 目标平台为原生 Android。 +- 正式产品名为“灵机”,namespace/application ID 为 `net.opcapp.flash`,`minSdk=26`。 - 使用 Kotlin、Jetpack Compose 和单 Activity 架构。 - 起卦完全在本地完成,AI 不得参与或更改起卦结果。 - 用户真实投币并录入;MVP 不提供随机起卦按钮。 diff --git a/docs/architecture.md b/docs/architecture.md index 17d4b66..4da011c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -39,11 +39,11 @@ Android 官方建议新应用采用清晰的 UI/数据分层、单向数据流 工程初始化后建议采用: ```text -app/src/main/java// +app/src/main/java/net/opcapp/flash/ ├── app/ -│ ├── BrainwaveApplication.kt +│ ├── LingjiApplication.kt │ ├── MainActivity.kt -│ └── BrainwaveNavHost.kt +│ └── LingjiApp.kt ├── core/ │ ├── model/ # 跨层不可变领域模型 │ ├── designsystem/ # 主题、字体、间距、卦象 Canvas @@ -180,7 +180,7 @@ App → HTTPS backend → model provider - 禁止直接引入完整“算命 SDK”或无法审计的卦象计算库。 - Compose 依赖使用 BOM 对齐版本。 - `compileSdk`/`targetSdk` 使用实现时最新稳定且满足商店要求的版本,不在方案文档硬编码会迅速过期的数值。 -- `minSdk` 默认提案为 26,最终值见[决策记录](decisions.md#未决问题)。 +- `minSdk=26` 已由 [ADR-014](decisions.md#adr-014正式-android-身份与最低版本) 确认;提高最低版本必须以依赖要求或覆盖率数据另立决策。 ## 11. 多模块化触发条件 diff --git a/docs/decisions.md b/docs/decisions.md index ba4fe0d..4274356 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -131,13 +131,22 @@ - 后果:`spotlessCheck` 当前是仓库内的无依赖格式兼容入口,不表示已引入 Spotless 插件。Android 壳建立后必须把 lint/debug build 加入 `verifyLocal`;将来若采用维护良好的专用插件,应保留命令兼容或同步更新 CI 与文档。 - 复审触发:Android 工具链启用、现有脚本无法表达新边界,或专用工具能以可接受成本提供明显更强的检查。 +## ADR-014:正式 Android 身份与最低版本 + +- 状态:`Accepted` +- 日期:2026-08-07 +- 关联:解决 TBD-001 中的产品名、TBD-002、TBD-003;2026-08-07 用户确认 +- 决定:正式产品名为“灵机”;用户提供的组织域名 `flash.opcapp.net` 按 Android 反向域名规则映射为 namespace/application ID `net.opcapp.flash`;`minSdk=26`。仓库根名 `brainwave` 继续只作内部代码代号,不进入 Android 发布身份。 +- 原因:稳定的正式身份是创建可安装应用、生成类路径、配置清单和建立设备测试的前置条件;API 26 与已接受架构及当前依赖兼容。 +- 备选:使用临时 application ID 会形成迁移、数据目录与签名身份风险;继续等待会阻塞 P0。`flash.opcapp.net` 不能直接作为 Android application ID,因为发布标识采用反向域名。 +- 后果:主代码迁移到 `net.opcapp.flash`;Android 壳使用“灵机”和 `minSdk=26`。应用图标、商店文案和签名仍未由本决策确认。当前 `compileSdk/targetSdk=34` 是已安装工具链基线,不代表永久商店目标;发布前须按当时商店要求复核升级。 +- 复审触发:组织域名所有权变化、发布账号要求更换 ID,或依赖/覆盖率数据要求提高最低系统版本。application ID 一旦发布不得轻率更换。 + ## 未决问题 | ID | 问题 | 推荐默认 | 阻塞阶段 | |---|---|---|---| -| TBD-001 | 正式产品名与应用图标 | Brainwave 仅作代码代号 | P0 商店配置 | -| TBD-002 | application ID | 使用组织所有的反向域名 | P0 | -| TBD-003 | minSdk | 26;创建工程时复核覆盖率和依赖要求 | P0 | +| TBD-001 | 应用图标与商店素材 | 产品名已由 ADR-014 确认为“灵机”;图标不使用未确认成稿 | P6 商店配置 | | TBD-004 | 问题是否允许留空 | 允许选择“不写具体内容”,但需显式操作 | P3 | | TBD-005 | 经典原文、现代白话的版本与授权 | 自有白话 + 可核验公版原文 | P2,发布阻塞 | | TBD-006 | 乾用九、坤用六是否纳入 MVP | 内容具备时展示 | P2/P3 | diff --git a/docs/dependency-licenses.md b/docs/dependency-licenses.md index 25de2b0..b9e013a 100644 --- a/docs/dependency-licenses.md +++ b/docs/dependency-licenses.md @@ -1,17 +1,40 @@ # 依赖与许可证清单 -> 状态:当前 JVM harness 已核验;Android application 依赖尚未配置 -> 适用范围:实际解析的 JVM runtime/test graph,以及直接使用的构建入口 +> 状态:Android debug runtime 与 debug unit-test runtime 依赖图已建立自动覆盖门禁 +> 适用范围:实际解析的 Android 调试运行时、单元测试运行时,以及直接使用的构建入口 -本清单不是未来 Android APK 的最终 third-party notices。每次新增或升级依赖时,必须同步 `config/dependency-licenses.json` 并运行 `.\gradlew.bat verifyLocal --offline`;Android 依赖启用后,还要生成 release runtime 的完整报告并复核是否需要在应用或分发包中附带许可证文本。 +本清单不是上架包的最终 third-party notices。每次新增或升级依赖时,必须同步 `config/dependency-licenses.json` 并运行 `\.\gradlew.bat verifyLocal`;发布前仍须对 release runtime 生成完整报告,复核许可证文本与随包义务。 + +## 直接条目 | ID | 版本 | 当前范围 | SPDX | 权威许可来源 | |---|---:|---|---|---| -| `gradle-wrapper` | 8.2 | 构建入口 | Apache-2.0 | [Gradle 8.2 license](https://github.com/gradle/gradle/blob/v8.2.0/LICENSE) | -| `kotlin-gradle-plugin` | 1.9.20 | 构建插件 | Apache-2.0 | [Kotlin 1.9.20 license](https://github.com/JetBrains/kotlin/blob/v1.9.20/license/LICENSE.txt) | -| `kotlin-stdlib` | 1.9.20 | JVM runtime | Apache-2.0 | [Kotlin 1.9.20 license](https://github.com/JetBrains/kotlin/blob/v1.9.20/license/LICENSE.txt) | -| `jetbrains-annotations` | 13.0 | JVM runtime transitive | Apache-2.0 | [Maven artifact metadata](https://repo1.maven.org/maven2/org/jetbrains/annotations/13.0/annotations-13.0.pom) | -| `junit4` | 4.13.2 | test | EPL-1.0 | [JUnit 4.13.2 license](https://github.com/junit-team/junit4/blob/r4.13.2/LICENSE-junit.txt) | -| `hamcrest-core` | 1.3 | test transitive | BSD-3-Clause | [Hamcrest 1.3 license](https://github.com/hamcrest/JavaHamcrest/blob/hamcrest-java-1.3/LICENSE.txt) | +| `gradle-wrapper` | 8.2 | 构建入口 | Apache-2.0 | [Gradle license](https://github.com/gradle/gradle/blob/v8.2.0/LICENSE) | +| `android-gradle-plugin` | 8.2.0 | 构建插件 | Apache-2.0 | [Android tools license](https://android.googlesource.com/platform/tools/base/+/studio-2022.2.1-patch4/LICENSE) | +| `kotlin-gradle-plugin` | 1.9.20 | 构建插件 | Apache-2.0 | [Kotlin license](https://github.com/JetBrains/kotlin/blob/v1.9.20/license/LICENSE.txt) | +| `hilt-gradle-plugin` | 2.48.1 | 构建插件 | Apache-2.0 | [Dagger/Hilt license](https://github.com/google/dagger/blob/dagger-2.48.1/LICENSE.txt) | +| `compose-bom` | 2023.10.01 | 运行时版本约束 | Apache-2.0 | [AndroidX license](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt) | +| `navigation-compose` | 2.7.5 | 运行时 | Apache-2.0 | [AndroidX license](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt) | +| `room-runtime` | 2.6.1 | 运行时 | Apache-2.0 | [AndroidX license](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt) | +| `datastore-preferences` | 1.0.0 | 运行时 | Apache-2.0 | [AndroidX license](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt) | +| `hilt-android` | 2.48.1 | 运行时 | Apache-2.0 | [Dagger/Hilt license](https://github.com/google/dagger/blob/dagger-2.48.1/LICENSE.txt) | +| `kotlin-stdlib` | 1.9.20 | 运行时 | Apache-2.0 | [Kotlin license](https://github.com/JetBrains/kotlin/blob/v1.9.20/license/LICENSE.txt) | +| `jetbrains-annotations` | 23.0.0 | 运行时传递依赖 | Apache-2.0 | [Maven artifact metadata](https://repo1.maven.org/maven2/org/jetbrains/annotations/23.0.0/annotations-23.0.0.pom) | +| `jsr305` | 3.0.2 | 运行时传递依赖 | BSD-3-Clause | [Maven artifact metadata](https://repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.pom) | +| `guava-listenablefuture` | 1.0 | 运行时传递依赖 | Apache-2.0 | [Maven artifact metadata](https://repo1.maven.org/maven2/com/google/guava/listenablefuture/1.0/listenablefuture-1.0.pom) | +| `javax-inject` | 1 | 运行时传递依赖 | Apache-2.0 | [Maven artifact metadata](https://repo1.maven.org/maven2/javax/inject/javax.inject/1/javax.inject-1.pom) | +| `junit4` | 4.13.2 | 单元测试 | EPL-1.0 | [JUnit license](https://github.com/junit-team/junit4/blob/r4.13.2/LICENSE-junit.txt) | +| `hamcrest-core` | 1.3 | 单元测试传递依赖 | BSD-3-Clause | [Hamcrest license](https://github.com/hamcrest/JavaHamcrest/blob/hamcrest-java-1.3/LICENSE.txt) | -当前没有第三方 Android runtime 库、字体、纹理、插画、音效或可发布《易经》内容进入工程;这句话只描述本提交时的依赖图,不构成未来授权。 +## 传递依赖组策略 + +下列策略只允许当前解析图中的 Maven group 命中对应许可证;未知 group 会让 Gradle 门禁失败,工程自身产物不参与第三方检查。 + +| ID | Maven group 前缀 | 当前范围 | SPDX | 权威许可来源 | +|---|---|---|---|---| +| `androidx-runtime-policy` | `androidx` | 运行时传递依赖 | Apache-2.0 | [AndroidX license](https://android.googlesource.com/platform/frameworks/support/+/androidx-main/LICENSE.txt) | +| `dagger-runtime-policy` | `com.google.dagger` | 运行时传递依赖 | Apache-2.0 | [Dagger/Hilt license](https://github.com/google/dagger/blob/dagger-2.48.1/LICENSE.txt) | +| `kotlin-runtime-policy` | `org.jetbrains.kotlin` | 运行时传递依赖 | Apache-2.0 | [Kotlin license](https://github.com/JetBrains/kotlin/blob/v1.9.20/license/LICENSE.txt) | +| `kotlinx-runtime-policy` | `org.jetbrains.kotlinx` | 运行时传递依赖 | Apache-2.0 | [Kotlinx Coroutines license](https://github.com/Kotlin/kotlinx.coroutines/blob/1.7.1/LICENSE.txt) | + +当前没有第三方字体、纹理、插画、音效或可发布《易经》内容进入工程。应用仅使用 Android 系统字体和代码绘制的卦象;这句话只描述本提交时的资源图,不构成未来授权。 diff --git a/docs/domain-rules.md b/docs/domain-rules.md index 3bedb2f..385fdca 100644 --- a/docs/domain-rules.md +++ b/docs/domain-rules.md @@ -5,7 +5,7 @@ 本文件定义本项目唯一允许的计算规则。UI 文案、AI 输出和数据源都不能覆盖这些规则。 -实现状态:`coin-v1` 纯 Kotlin 核心位于 `app/src/main/java/brainwave/domain/casting/`;`.\gradlew.bat :app:test --offline` 覆盖 8 种币面、4,096 种六爻、64 模式、已知夹具和 DTO 往返。记录时间与内容版本在确定性计算完成后附加。 +实现状态:`coin-v1` 纯 Kotlin 核心位于 `app/src/main/java/net/opcapp/flash/domain/casting/`;`.\gradlew.bat :app:testDebugUnitTest` 覆盖 8 种币面、4,096 种六爻、64 模式、已知夹具和 DTO 往返。记录时间与内容版本在确定性计算完成后附加。 ## 1. 术语和类型 diff --git a/docs/environment.md b/docs/environment.md index 889e2c0..379cdad 100644 --- a/docs/environment.md +++ b/docs/environment.md @@ -1,7 +1,7 @@ # 本地开发环境基线 > 状态:已从当前主机实测 -> 最后核验:2026-08-04 +> 最后核验:2026-08-07 > 适用范围:`D:\OPC\brainwave` 的 Android 开发、构建和设备验证 本文件记录项目当前可用的本地工具链,不是期望环境清单。后续实现必须先使用这里已经确认的能力;需要升级或安装新工具时,应说明原因并在完成后更新本文件。 @@ -39,7 +39,7 @@ | Node.js / npm | `22.22.1` / `11.12.1` | | Google Chrome | `150.0.7871.188`,`C:\Program Files\Google\Chrome\Application\chrome.exe` | -首次环境审计时仓库没有 Gradle Wrapper、`build.gradle*`、`settings.gradle*` 或根 `AGENTS.md`。当前这些仓库级入口已经建立;`app` 暂时是纯 Kotlin/JVM 领域 harness,不是可安装 Android application。 +首次环境审计时仓库没有 Gradle Wrapper、`build.gradle*`、`settings.gradle*` 或根 `AGENTS.md`。当前这些入口和可安装的 Android application 已建立;正式应用名为“灵机”,application ID 为 `net.opcapp.flash`。 Windows 路径可能较长;如果后续依赖缓存或生成代码触发路径长度错误,应优先缩短包/生成目录或评估仓库级长路径配置,并把实际决定写入[决策记录](decisions.md)。 @@ -111,23 +111,21 @@ Android Studio 不是当前环境的可用前提。项目必须先支持 PowerSh .\gradlew.bat ``` -不要要求用户安装全局 Gradle。当前缓存已实际证明 Gradle 8.2、Kotlin 1.9.20 与 JUnit 4.13.2 可离线完成领域构建和测试;Compose、Hilt、Room 等 Android 依赖仍未配置或验证,不能据此推断可离线解析。 +不要要求用户安装全局 Gradle。当前缓存已实际证明 Gradle 8.2、AGP 8.2.0、Kotlin 1.9.20、Compose、Hilt、Room、DataStore、Navigation 与 JUnit 可完成 Android debug 构建、lint、单元测试和测试 APK 编译;首次解析已经联网完成,后续离线可用性仍以实际命令为准。 ## 7. 已连接 Android 真机 -检测时存在 1 台已授权的物理设备。为避免持久化设备标识,本文件不记录序列号。 +2026-08-07 本轮曾检测到 1 台已授权的物理设备。为避免持久化设备标识,本文件不记录序列号。 | 项目 | 实测值 | |---|---| -| 厂商/型号 | OnePlus `PKG110` | -| Android | 16 | -| API Level | 36 | -| ABI | `arm64-v8a` | -| 物理分辨率 | `1264 × 2780` | -| 当前覆盖分辨率 | `1080 × 2376` | -| 物理/覆盖密度 | `560 / 480 dpi` | +| 厂商/型号 | Samsung `SM-G9700` | +| Android | 12 | +| API Level | 31 | +| 物理分辨率 | `1080 × 2280` | +| 物理密度 | `480 dpi` | -当前设备可用于 `adb` 安装、手工验收和 `connectedDebugAndroidTest`。测试脚本不得硬编码序列号;设备可能随时断开,纯 JVM 门禁必须独立可运行。 +该设备满足 `minSdk=26`,可用于 `adb` 安装、手工验收和 `connectedDebugAndroidTest`。测试脚本不得硬编码序列号;设备可能随时断开,环境独立门禁必须单独可运行。本机 `PATH` 还存在 `D:\Portable\adb\adb.exe`,曾运行旧版 ADB server;项目设备命令应显式使用 `$env:ANDROID_HOME\platform-tools\adb.exe`,避免与 SDK Platform Tools 37.0.0 争用 server。ADB server 重启后若接口消失,应先保持手机解锁并重新确认 USB 调试授权,不应把“Windows 能看到 USB 复合设备”误当作 ADB 已连接。 ## 8. 网络与代理状态 @@ -146,21 +144,20 @@ Android Studio 不是当前环境的可用前提。项目必须先支持 PowerSh 2. 使用项目自带 Gradle Wrapper,不依赖全局 `gradle` 或 `kotlinc`。 3. 首次可构建配置只能依赖已安装的 SDK Platform 34 和 Build Tools 34.0.0;若模板要求更高版本,应先记录差异,再决定调整模板或安装 SDK。 4. 命令行构建是必须能力,不假设 Android Studio 存在。 -5. 仪器测试优先使用已连接的 API 36、arm64 真机;不假设 Emulator/AVD 存在。 +5. 仪器测试优先使用当前可用的 API 31 三星真机;不假设 Emulator/AVD 或固定设备存在。 6. 不自动安装系统级工具、SDK 平台或模拟器;确需新增环境能力时,先说明影响。 7. 发布时的 `targetSdk` 和商店要求是独立发布门禁,不能因为本机只有 Platform 34 就永久锁定;工具链升级后必须更新本文件和相关 ADR。 8. P-1 原型截图使用现有 Node.js 与 Chrome DevTools 协议生成,不额外安装前端依赖;命令见[移动端交互原型](prototype.md#3-运行方式)。 ## 10. 已知缺口 -- 可安装的 Android application 壳尚未创建;正式名称、组织所有的 application ID 与最终 `minSdk` 仍待确认。 - Android Studio 未安装。 - Android Emulator、system image 和 AVD 不可用。 - 本机只确认安装了 Android Platform 34 / Build Tools 34.0.0。 -- Compose、Hilt、Room、DataStore 与 Navigation 的 Maven 依赖尚未完整离线验证。 -- 代理已配置,但外部仓库和 Android CLI 网络连通性尚未验证。 +- `PATH` 中旧便携 ADB 与 SDK ADB 可能争用 server;设备接口消失时需要手机端重新授权或重新连接。 +- 代理已配置;本轮 Maven 依赖下载成功,但不能据此保证未来网络始终可用。 -已解除的缺口:Gradle Wrapper、Version Catalog、根 `AGENTS.md`、CI 与可执行的 `verifyLocal` 已建立;`verifyLocal --offline` 已在 JDK 17 上通过。它当前不包含 Android lint、APK 构建或设备测试。 +已解除的缺口:Gradle Wrapper、Version Catalog、根 `AGENTS.md`、CI、正式 Android 身份和 application 壳均已建立;`assembleDebug`、13 个 debug 单元测试、`lintDebug` 与测试 APK 编译已在 JDK 17 上通过。`verifyLocal` 现已包含 Android lint 与 debug APK,设备测试仍按环境单独执行。 这些缺口分别由[实施计划](implementation-plan.md)的 P0 和[质量门禁](quality-gates.md)处理。环境缺口不是跳过验证的理由;无法运行的门禁必须在交付报告中准确说明。 diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index a076db6..4a6788d 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -1,6 +1,6 @@ # 分阶段实施计划 -> 状态:P-1 v0.3 待视觉复核;P0 仓库门禁已建立但 Android 壳受 TBD-001~003 阻塞;P1 已完成并通过穷举测试;P2 内容契约已建立但授权内容未开始 +> 状态:P-1 v0.3 待视觉复核;P0 Android 壳与本地门禁已完成,真机闭环正在执行;P1 已完成并通过穷举测试;P2 内容契约已建立但授权内容未开始 > 计划原则:先用原型确认高返工成本体验,再锁定确定性领域核心,随后接内容和 UI,最后接网络 AI ## 1. 依赖图 @@ -47,11 +47,11 @@ P1 与 P2 可并行,但 P3 不能在领域与内容契约未稳定时复制原 任务: - [x] 读取并遵守[本地开发环境](environment.md):JDK 17、SDK 34、命令行优先、Gradle Wrapper、真机验证。 -- [ ] 基于 Google `android/architecture-templates` 的 `base` 分支初始化 Android 壳;模板定制需要 TBD-002 的正式 application ID,不能用临时发布身份替代。 -- [ ] 确认正式应用名称、package/application ID、minSdk(TBD-001~003)。 -- [ ] 配置 Compose、Material 3、Hilt、Room、DataStore 和 Navigation;Version Catalog 已先用于 JVM harness。 -- [ ] 配置 Gradle Wrapper、格式化、lint、单元测试和 CI:Wrapper、无依赖格式门禁、JVM 单测与 CI 已完成;Android lint 要等 application 插件启用。 -- [ ] 建立 [系统架构](architecture.md)中的包结构和空 feature 边界:`domain/casting` 已落地,其余随 Android 壳建立。 +- [x] 参照 Google `android/architecture-templates` 的 `base` 分支建立单模块 Android 壳和分层边界。 +- [x] 确认正式应用名称“灵机”、namespace/application ID `net.opcapp.flash`、`minSdk=26`(ADR-014)。 +- [x] 配置 Compose、Material 3、Hilt、Room、DataStore 和 Navigation,并由 Version Catalog 固定兼容版本。 +- [x] 配置 Gradle Wrapper、格式、Android lint、单元测试、debug APK、依赖许可证门禁和 CI 同入口验证。 +- [x] 建立 [系统架构](architecture.md)中的 `app`、`core/designsystem`、`domain`、`data`、`feature/home` 与 `feature/result` 边界;未实施 feature 不创建空包。 - [x] 将根 `AGENTS.md` 设计为短地图,指向本目录和验证命令。 - [x] 增加 `verifyLocal` 聚合任务、文档链接、领域边界、内容契约与基础 secret scan。 @@ -59,7 +59,7 @@ P1 与 P2 可并行,但 P3 不能在领域与内容契约未稳定时复制原 - Windows 上一条命令可执行格式、lint、单测和 debug 构建。 - CI 使用同一组命令。 -- 空应用可在模拟器启动,导航到占位欢迎页。 +- 开发验证应用可在授权真机启动,并从首页导航到已知领域夹具结果页。 - 没有生产服务密钥或真实内容。 ## 4. P1:领域核心 @@ -82,7 +82,7 @@ P1 与 P2 可并行,但 P3 不能在领域与内容契约未稳定时复制原 - 测试无随机、无网络、无系统时间依赖。 - `CastEngine` API 经评审后冻结为 `coin-v1`。 -当前证据:`.\gradlew.bat :app:test --offline` 中 `CastEngineTest` 执行 10 个测试、0 失败;实现位于 `app/src/main/java/brainwave/domain/casting/`。`brainwave` 是内部代码命名空间,不是 TBD-002 的 application ID。 +当前证据:`.\gradlew.bat :app:testDebugUnitTest` 中 `CastEngineTest` 执行 10 个测试、0 失败;实现位于 `app/src/main/java/net/opcapp/flash/domain/casting/`,且领域边界脚本禁止 Android/Compose/数据库/网络依赖。 ## 5. P2:内容数据管线 diff --git a/docs/prototype.md b/docs/prototype.md index 7265cbc..9f6b7b4 100644 --- a/docs/prototype.md +++ b/docs/prototype.md @@ -8,7 +8,7 @@ 在初始化 Android 工程前,先用可点击原型确认最容易返工的产品决策:信息层级、六次录入方式、结果阅读顺序、AI 同意门和东方文化表达。原型确认后,P3 的 Compose 页面应复用这里的设计令牌和状态契约,而不是重新发明流程。 -本轮使用“一问”作为视觉工作名。它不构成正式命名决定;正式名称、应用图标和商店素材仍是 [TBD-001](decisions.md#未决问题)。 +v0.3 截图仍保留当时的视觉工作名“一问”,不得当作当前产品文案。正式产品名已由 [ADR-014](decisions.md#adr-014正式-android-身份与最低版本) 确认为“灵机”;应用图标与商店素材仍见 [TBD-001](decisions.md#未决问题),P3 固化正式页面前须刷新原型品牌文案。 ## 2. 交付范围 diff --git a/docs/quality-gates.md b/docs/quality-gates.md index 91dfa7e..a65e5b9 100644 --- a/docs/quality-gates.md +++ b/docs/quality-gates.md @@ -1,6 +1,6 @@ # 质量门禁与验证策略 -> 状态:JVM/harness 门禁已启用;Android lint、构建、UI 与设备门禁等待 Android application 壳 +> 状态:Android 本地门禁已启用;设备门禁在授权真机存在时单独执行 > 原则:完成必须有可重复证据,不能以“代码看起来正确”代替验证 ## 1. 反馈循环 @@ -21,26 +21,21 @@ ```powershell .\gradlew.bat spotlessCheck --offline -.\gradlew.bat :app:test --offline +.\gradlew.bat :app:testDebugUnitTest --offline .\gradlew.bat verifyContentContract --offline -.\gradlew.bat verifyLocal --offline +.\gradlew.bat verifyLocal ``` -`verifyLocal` 当前聚合无依赖格式检查、10 个领域测试、3 个内容 repository 测试、已解析 JVM 依赖许可证、domain 依赖边界、内容契约与负向夹具、文档链接、高置信 secret scan 和原型 JavaScript 语法检查。CI 执行同一个聚合任务。 +`verifyLocal` 聚合无依赖格式检查、10 个领域测试、3 个内容 repository 测试、Android lint、debug APK、已解析 Android 调试/单测依赖许可证、domain 依赖边界、内容契约与负向夹具、文档链接、高置信 secret scan 和原型 JavaScript 语法检查。CI 执行同一个聚合任务。首次解析 Android 依赖时不能强制 `--offline`;缓存完成后可用离线模式复核。 -Android application 插件配置后,Windows 环境还必须提供: +授权真机存在时,Windows 环境还必须执行: ```powershell .\gradlew.bat spotlessCheck -.\gradlew.bat lintDebug -.\gradlew.bat testDebugUnitTest -.\gradlew.bat assembleDebug .\gradlew.bat connectedDebugAndroidTest ``` -若采用不同格式化插件,命令可以调整,但必须在本文件和 CI 同步更新。`connectedDebugAndroidTest` 需要模拟器或设备,应与纯 JVM 快速门禁分开。 - -到那时必须把 Android lint 和 debug build 加入现有 `verifyLocal`,使代理仍不必猜测验证组合。在完成这一步之前,`verifyLocal` 通过只证明 JVM 与仓库门禁,不代表 APK 已构建或真机测试已通过。 +若采用不同格式化插件,命令可以调整,但必须在本文件和 CI 同步更新。`connectedDebugAndroidTest` 需要模拟器或设备,继续与环境独立的 `verifyLocal` 分开;`verifyLocal` 通过证明 APK 已构建,但不等于真机测试已经通过。 ## 3. 测试层次 diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..f53889c --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +android.useAndroidX=true +kotlin.code.style=official +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c0c6051..dd700d2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,44 @@ [versions] +agp = "8.2.0" kotlin = "1.9.20" +composeBom = "2023.10.01" +composeCompiler = "1.5.5" +coreKtx = "1.12.0" +activityCompose = "1.8.1" +lifecycle = "2.6.2" +navigation = "2.7.5" +room = "2.6.1" +dataStore = "1.0.0" +hilt = "2.48.1" junit = "4.13.2" +androidxTestExt = "1.1.5" +androidxTestRunner = "1.5.2" [libraries] +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } +androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" } +androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" } +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } +androidx-compose-ui = { module = "androidx.compose.ui:ui" } +androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3" } +androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } +androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } +androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation" } +androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } +androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "dataStore" } +hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } +hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" } junit = { module = "junit:junit", version.ref = "junit" } +androidx-test-ext-junit = { module = "androidx.test.ext:junit", version.ref = "androidxTestExt" } +androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidxTestRunner" } [plugins] -kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +android-application = { id = "com.android.application", version.ref = "agp" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } +hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } diff --git a/scripts/verify-content-contract.mjs b/scripts/verify-content-contract.mjs index 0c4a185..55640bd 100644 --- a/scripts/verify-content-contract.mjs +++ b/scripts/verify-content-contract.mjs @@ -19,7 +19,9 @@ const kotlinCatalogPath = path.join( "src", "main", "java", - "brainwave", + "net", + "opcapp", + "flash", "domain", "casting", "HexagramCatalog.kt", diff --git a/scripts/verify-dependency-licenses.mjs b/scripts/verify-dependency-licenses.mjs index 3512f21..68455ab 100644 --- a/scripts/verify-dependency-licenses.mjs +++ b/scripts/verify-dependency-licenses.mjs @@ -17,19 +17,42 @@ if (manifest.schemaVersion !== 1) failures.push("manifest schemaVersion must be if (!Array.isArray(manifest.components) || manifest.components.length === 0) { failures.push("manifest components must not be empty"); } +if (!Array.isArray(manifest.groupPolicies) || manifest.groupPolicies.length === 0) { + failures.push("manifest groupPolicies must not be empty"); +} const allowedLicenses = new Set(["Apache-2.0", "BSD-3-Clause", "EPL-1.0"]); const ids = new Set(); const coordinates = new Set(); -for (const [index, component] of (manifest.components ?? []).entries()) { - const prefix = `components[${index}]`; - for (const field of ["id", "version", "scope", "license", "source"]) { - if (typeof component[field] !== "string" || component[field].trim() === "") { +const validateCommonFields = (entry, prefix) => { + for (const field of ["id", "scope", "license", "source"]) { + if (typeof entry[field] !== "string" || entry[field].trim() === "") { failures.push(`${prefix}.${field} must be non-blank text`); } } - if (ids.has(component.id)) failures.push(`${prefix}.id must be unique`); - ids.add(component.id); + if (ids.has(entry.id)) failures.push(`${prefix}.id must be unique`); + ids.add(entry.id); + if (!allowedLicenses.has(entry.license)) failures.push(`${prefix}.license is not reviewed`); + try { + const url = new URL(entry.source); + if (url.protocol !== "https:") throw new Error("not HTTPS"); + } catch { + failures.push(`${prefix}.source must be an absolute HTTPS URL`); + } + if (!documentation.includes(entry.id) || !documentation.includes(entry.license)) { + failures.push(`${prefix} is not represented in docs/dependency-licenses.md`); + } +}; + +for (const [index, component] of (manifest.components ?? []).entries()) { + const prefix = `components[${index}]`; + validateCommonFields(component, prefix); + if (typeof component.version !== "string" || component.version.trim() === "") { + failures.push(`${prefix}.version must be non-blank text`); + } + if (!documentation.includes(component.version)) { + failures.push(`${prefix}.version is not represented in docs/dependency-licenses.md`); + } if (component.coordinates !== null) { if (typeof component.coordinates !== "string" || component.coordinates.split(":").length !== 3) { failures.push(`${prefix}.coordinates must be group:name:version or null`); @@ -38,24 +61,31 @@ for (const [index, component] of (manifest.components ?? []).entries()) { } coordinates.add(component.coordinates); } - if (!allowedLicenses.has(component.license)) failures.push(`${prefix}.license is not reviewed`); - try { - const url = new URL(component.source); - if (url.protocol !== "https:") throw new Error("not HTTPS"); - } catch { - failures.push(`${prefix}.source must be an absolute HTTPS URL`); - } - if (!documentation.includes(component.id) || - !documentation.includes(component.version) || - !documentation.includes(component.license)) { - failures.push(`${prefix} is not represented in docs/dependency-licenses.md`); +} + +const groupPrefixes = new Set(); +for (const [index, policy] of (manifest.groupPolicies ?? []).entries()) { + const prefix = `groupPolicies[${index}]`; + validateCommonFields(policy, prefix); + if (typeof policy.groupPrefix !== "string" || !/^[a-z][a-z0-9.]+$/u.test(policy.groupPrefix)) { + failures.push(`${prefix}.groupPrefix must be a Maven group prefix`); + } else if (groupPrefixes.has(policy.groupPrefix)) { + failures.push(`${prefix}.groupPrefix must be unique`); } + groupPrefixes.add(policy.groupPrefix); } const version = (name) => new RegExp(`^${name}\\s*=\\s*"([^"]+)"`, "mu").exec(catalog)?.[1]; const expectedVersions = new Map([ ["gradle-wrapper", /gradle-([\d.]+)-bin\.zip/u.exec(wrapper)?.[1]], + ["android-gradle-plugin", version("agp")], ["kotlin-gradle-plugin", version("kotlin")], + ["hilt-gradle-plugin", version("hilt")], + ["compose-bom", version("composeBom")], + ["navigation-compose", version("navigation")], + ["room-runtime", version("room")], + ["datastore-preferences", version("dataStore")], + ["hilt-android", version("hilt")], ["kotlin-stdlib", version("kotlin")], ["junit4", version("junit")], ]); @@ -71,5 +101,8 @@ if (failures.length > 0) { for (const failure of failures) console.error(`- ${failure}`); process.exitCode = 1; } else { - console.log(`Dependency-license manifest passed (${manifest.components.length} reviewed components).`); + console.log( + `Dependency-license manifest passed (${manifest.components.length} components, ` + + `${manifest.groupPolicies.length} group policies).`, + ); } diff --git a/scripts/verify-domain-boundaries.mjs b/scripts/verify-domain-boundaries.mjs index 2a9eb1f..f95ca98 100644 --- a/scripts/verify-domain-boundaries.mjs +++ b/scripts/verify-domain-boundaries.mjs @@ -6,7 +6,17 @@ import { walkFiles, } from "./repository-files.mjs"; -const domainRoot = path.join(repositoryRoot, "app", "src", "main", "java", "brainwave", "domain"); +const domainRoot = path.join( + repositoryRoot, + "app", + "src", + "main", + "java", + "net", + "opcapp", + "flash", + "domain", +); const forbiddenImports = [ /^android\./u, /^androidx\./u, diff --git a/settings.gradle.kts b/settings.gradle.kts index 5523ac9..15ce72c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -14,6 +14,6 @@ dependencyResolutionManagement { } } -// Repository code name only. This is not the formal product name (TBD-001). +// The repository keeps its original code name; the Android product identity is defined in :app. rootProject.name = "brainwave" include(":app")